blob: cf113b89b3372ca3d8b6ba66d2bdeaf068096d25 [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
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000414 def GetGitBaseUrlFromConfig(self):
415 """Return the configured base URL from branch.<branchname>.baseurl.
416
417 Returns None if it is not set.
418 """
419 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
420 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000421
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000422 def GetRemoteUrl(self):
423 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
424
425 Returns None if there is no remote.
426 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000427 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
429
430 def GetIssue(self):
431 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000432 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
433 if issue:
434 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 else:
436 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 self.has_issue = True
438 return self.issue
439
440 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000441 if not self.rietveld_server:
442 # If we're on a branch then get the server potentially associated
443 # with that branch.
444 if self.GetIssue():
445 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
446 ['config', self._RietveldServer()], error_ok=True).strip())
447 if not self.rietveld_server:
448 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 return self.rietveld_server
450
451 def GetIssueURL(self):
452 """Get the URL for a particular issue."""
453 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
454
455 def GetDescription(self, pretty=False):
456 if not self.has_description:
457 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000458 issue = int(self.GetIssue())
459 try:
460 self.description = self.RpcServer().get_description(issue).strip()
461 except urllib2.HTTPError, e:
462 if e.code == 404:
463 DieWithError(
464 ('\nWhile fetching the description for issue %d, received a '
465 '404 (not found)\n'
466 'error. It is likely that you deleted this '
467 'issue on the server. If this is the\n'
468 'case, please run\n\n'
469 ' git cl issue 0\n\n'
470 'to clear the association with the deleted issue. Then run '
471 'this command again.') % issue)
472 else:
473 DieWithError(
474 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475 self.has_description = True
476 if pretty:
477 wrapper = textwrap.TextWrapper()
478 wrapper.initial_indent = wrapper.subsequent_indent = ' '
479 return wrapper.fill(self.description)
480 return self.description
481
482 def GetPatchset(self):
483 if not self.has_patchset:
484 patchset = RunGit(['config', self._PatchsetSetting()],
485 error_ok=True).strip()
486 if patchset:
487 self.patchset = patchset
488 else:
489 self.patchset = None
490 self.has_patchset = True
491 return self.patchset
492
493 def SetPatchset(self, patchset):
494 """Set this branch's patchset. If patchset=0, clears the patchset."""
495 if patchset:
496 RunGit(['config', self._PatchsetSetting(), str(patchset)])
497 else:
498 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000499 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000500 self.has_patchset = False
501
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000502 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000503 patchset = self.RpcServer().get_issue_properties(
504 int(issue), False)['patchsets'][-1]
505 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000506 '/download/issue%s_%s.diff' % (issue, patchset))
507
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000508 def SetIssue(self, issue):
509 """Set this branch's issue. If issue=0, clears the issue."""
510 if issue:
511 RunGit(['config', self._IssueSetting(), str(issue)])
512 if self.rietveld_server:
513 RunGit(['config', self._RietveldServer(), self.rietveld_server])
514 else:
515 RunGit(['config', '--unset', self._IssueSetting()])
516 self.SetPatchset(0)
517 self.has_issue = False
518
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000519 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000520 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
521 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000522
523 # We use the sha1 of HEAD as a name of this change.
524 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000525 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000526 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000527 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000528 except subprocess2.CalledProcessError:
529 DieWithError(
530 ('\nFailed to diff against upstream branch %s!\n\n'
531 'This branch probably doesn\'t exist anymore. To reset the\n'
532 'tracking branch, please run\n'
533 ' git branch --set-upstream %s trunk\n'
534 'replacing trunk with origin/master or the relevant branch') %
535 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000536
537 issue = ConvertToInteger(self.GetIssue())
538 patchset = ConvertToInteger(self.GetPatchset())
539 if issue:
540 description = self.GetDescription()
541 else:
542 # If the change was never uploaded, use the log messages of all commits
543 # up to the branch point, as git cl upload will prefill the description
544 # with these log messages.
545 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
546 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000547
548 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000549 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000550 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000551 name,
552 description,
553 absroot,
554 files,
555 issue,
556 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000557 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000558
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000559 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
560 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
561 change = self.GetChange(upstream_branch, author)
562
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000563 # Apply watchlists on upload.
564 if not committing:
565 watchlist = watchlists.Watchlists(change.RepositoryRoot())
566 files = [f.LocalPath() for f in change.AffectedFiles()]
567 self.SetWatchers(watchlist.GetWatchersForPaths(files))
568
569 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000570 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000571 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000572 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000573 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000574 except presubmit_support.PresubmitFailure, e:
575 DieWithError(
576 ('%s\nMaybe your depot_tools is out of date?\n'
577 'If all fails, contact maruel@') % e)
578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000580 """Updates the description and closes the issue."""
581 issue = int(self.GetIssue())
582 self.RpcServer().update_description(issue, self.description)
583 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000585 def SetFlag(self, flag, value):
586 """Patchset must match."""
587 if not self.GetPatchset():
588 DieWithError('The patchset needs to match. Send another patchset.')
589 try:
590 return self.RpcServer().set_flag(
591 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
592 except urllib2.HTTPError, e:
593 if e.code == 404:
594 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
595 if e.code == 403:
596 DieWithError(
597 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
598 'match?') % (self.GetIssue(), self.GetPatchset()))
599 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000601 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 """Returns an upload.RpcServer() to access this review's rietveld instance.
603 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000604 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000605 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
606 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000607 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608
609 def _IssueSetting(self):
610 """Return the git setting that stores this change's issue."""
611 return 'branch.%s.rietveldissue' % self.GetBranch()
612
613 def _PatchsetSetting(self):
614 """Return the git setting that stores this change's most recent patchset."""
615 return 'branch.%s.rietveldpatchset' % self.GetBranch()
616
617 def _RietveldServer(self):
618 """Returns the git setting that stores this change's rietveld server."""
619 return 'branch.%s.rietveldserver' % self.GetBranch()
620
621
622def GetCodereviewSettingsInteractively():
623 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000624 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 server = settings.GetDefaultServerUrl(error_ok=True)
626 prompt = 'Rietveld server (host[:port])'
627 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000628 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 if not server and not newserver:
630 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000631 if newserver:
632 newserver = gclient_utils.UpgradeToHttps(newserver)
633 if newserver != server:
634 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000636 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 prompt = caption
638 if initial:
639 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000640 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 if new_val == 'x':
642 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000643 elif new_val:
644 if is_url:
645 new_val = gclient_utils.UpgradeToHttps(new_val)
646 if new_val != initial:
647 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000648
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000649 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000651 'tree-status-url', False)
652 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653
654 # TODO: configure a default branch to diff against, rather than this
655 # svn-based hackery.
656
657
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000658class ChangeDescription(object):
659 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000660 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000661 self.log_desc = log_desc
662 self.reviewers = reviewers
663 self.description = self.log_desc
664
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000665 def Prompt(self):
666 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000667# This will displayed on the codereview site.
668# The first line will also be used as the subject of the review.
669"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000670 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000671 if ('\nR=' not in self.description and
672 '\nTBR=' not in self.description and
673 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000674 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000675 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000676 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000677 if '\nTEST=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000678 content += '\nTEST='
679 content = content.rstrip('\n') + '\n'
680 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000681 if not content:
682 DieWithError('Running editor failed')
683 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000684 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000685 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000686 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000687
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000688 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000689 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000690 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000691 # Retrieves all reviewer lines
692 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000693 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000694 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000695 if reviewers:
696 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000697
698 def IsEmpty(self):
699 return not self.description
700
701
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702def FindCodereviewSettingsFile(filename='codereview.settings'):
703 """Finds the given file starting in the cwd and going up.
704
705 Only looks up to the top of the repository unless an
706 'inherit-review-settings-ok' file exists in the root of the repository.
707 """
708 inherit_ok_file = 'inherit-review-settings-ok'
709 cwd = os.getcwd()
710 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
711 if os.path.isfile(os.path.join(root, inherit_ok_file)):
712 root = '/'
713 while True:
714 if filename in os.listdir(cwd):
715 if os.path.isfile(os.path.join(cwd, filename)):
716 return open(os.path.join(cwd, filename))
717 if cwd == root:
718 break
719 cwd = os.path.dirname(cwd)
720
721
722def LoadCodereviewSettingsFromFile(fileobj):
723 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000724 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726 def SetProperty(name, setting, unset_error_ok=False):
727 fullname = 'rietveld.' + name
728 if setting in keyvals:
729 RunGit(['config', fullname, keyvals[setting]])
730 else:
731 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
732
733 SetProperty('server', 'CODE_REVIEW_SERVER')
734 # Only server setting is required. Other settings can be absent.
735 # In that case, we ignore errors raised during option deletion attempt.
736 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
737 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
738 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
739
ukai@chromium.orge8077812012-02-03 03:41:46 +0000740 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
741 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
742 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
745 #should be of the form
746 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
747 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
748 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
749 keyvals['ORIGIN_URL_CONFIG']])
750
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000752def DownloadHooks(force):
753 """downloads hooks
754
755 Args:
756 force: True to update hooks. False to install hooks if not present.
757 """
758 if not settings.GetIsGerrit():
759 return
760 server_url = settings.GetDefaultServerUrl()
761 src = '%s/tools/hooks/commit-msg' % server_url
762 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
763 if not os.access(dst, os.X_OK):
764 if os.path.exists(dst):
765 if not force:
766 return
767 os.remove(dst)
768 try:
769 urllib.urlretrieve(src, dst)
770 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
771 except Exception:
772 if os.path.exists(dst):
773 os.remove(dst)
774 DieWithError('\nFailed to download hooks from %s' % src)
775
776
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777@usage('[repo root containing codereview.settings]')
778def CMDconfig(parser, args):
779 """edit configuration for this tree"""
780
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000781 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 if len(args) == 0:
783 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000784 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 return 0
786
787 url = args[0]
788 if not url.endswith('codereview.settings'):
789 url = os.path.join(url, 'codereview.settings')
790
791 # Load code review settings and download hooks (if available).
792 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000793 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 return 0
795
796
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000797def CMDbaseurl(parser, args):
798 """get or set base-url for this branch"""
799 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
800 branch = ShortBranchName(branchref)
801 _, args = parser.parse_args(args)
802 if not args:
803 print("Current base-url:")
804 return RunGit(['config', 'branch.%s.base-url' % branch],
805 error_ok=False).strip()
806 else:
807 print("Setting base-url to %s" % args[0])
808 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
809 error_ok=False).strip()
810
811
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812def CMDstatus(parser, args):
813 """show status of changelists"""
814 parser.add_option('--field',
815 help='print only specific field (desc|id|patch|url)')
816 (options, args) = parser.parse_args(args)
817
818 # TODO: maybe make show_branches a flag if necessary.
819 show_branches = not options.field
820
821 if show_branches:
822 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
823 if branches:
824 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000825 changes = (Changelist(branchref=b) for b in branches.splitlines())
826 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
827 alignment = max(5, max(len(b) for b in branches))
828 for branch in sorted(branches):
829 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830
831 cl = Changelist()
832 if options.field:
833 if options.field.startswith('desc'):
834 print cl.GetDescription()
835 elif options.field == 'id':
836 issueid = cl.GetIssue()
837 if issueid:
838 print issueid
839 elif options.field == 'patch':
840 patchset = cl.GetPatchset()
841 if patchset:
842 print patchset
843 elif options.field == 'url':
844 url = cl.GetIssueURL()
845 if url:
846 print url
847 else:
848 print
849 print 'Current branch:',
850 if not cl.GetIssue():
851 print 'no issue assigned.'
852 return 0
853 print cl.GetBranch()
854 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
855 print 'Issue description:'
856 print cl.GetDescription(pretty=True)
857 return 0
858
859
860@usage('[issue_number]')
861def CMDissue(parser, args):
862 """Set or display the current code review issue number.
863
864 Pass issue number 0 to clear the current issue.
865"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000866 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867
868 cl = Changelist()
869 if len(args) > 0:
870 try:
871 issue = int(args[0])
872 except ValueError:
873 DieWithError('Pass a number to set the issue or none to list it.\n'
874 'Maybe you want to run git cl status?')
875 cl.SetIssue(issue)
876 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
877 return 0
878
879
880def CreateDescriptionFromLog(args):
881 """Pulls out the commit log to use as a base for the CL description."""
882 log_args = []
883 if len(args) == 1 and not args[0].endswith('.'):
884 log_args = [args[0] + '..']
885 elif len(args) == 1 and args[0].endswith('...'):
886 log_args = [args[0][:-1]]
887 elif len(args) == 2:
888 log_args = [args[0] + '..' + args[1]]
889 else:
890 log_args = args[:] # Hope for the best!
891 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
892
893
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000894def ConvertToInteger(inputval):
895 """Convert a string to integer, but returns either an int or None."""
896 try:
897 return int(inputval)
898 except (TypeError, ValueError):
899 return None
900
901
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902def CMDpresubmit(parser, args):
903 """run presubmit tests on the current changelist"""
904 parser.add_option('--upload', action='store_true',
905 help='Run upload hook instead of the push/dcommit hook')
906 (options, args) = parser.parse_args(args)
907
908 # Make sure index is up-to-date before running diff-index.
909 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
910 if RunGit(['diff-index', 'HEAD']):
911 # TODO(maruel): Is this really necessary?
912 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
913 return 1
914
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000915 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 if args:
917 base_branch = args[0]
918 else:
919 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000920 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000922 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000923 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000924 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000925 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926
927
ukai@chromium.orge8077812012-02-03 03:41:46 +0000928def GerritUpload(options, args, cl):
929 """upload the current branch to gerrit."""
930 # We assume the remote called "origin" is the one we want.
931 # It is probably not worthwhile to support different workflows.
932 remote = 'origin'
933 branch = 'master'
934 if options.target_branch:
935 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000937 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000938 if options.reviewers:
939 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000940 change_desc = ChangeDescription(log_desc, options.reviewers)
941 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000942 if change_desc.IsEmpty():
943 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944 return 1
945
ukai@chromium.orge8077812012-02-03 03:41:46 +0000946 receive_options = []
947 cc = cl.GetCCList().split(',')
948 if options.cc:
949 cc += options.cc.split(',')
950 cc = filter(None, cc)
951 if cc:
952 receive_options += ['--cc=' + email for email in cc]
953 if change_desc.reviewers:
954 reviewers = filter(None, change_desc.reviewers.split(','))
955 if reviewers:
956 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957
ukai@chromium.orge8077812012-02-03 03:41:46 +0000958 git_command = ['push']
959 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000960 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000961 ' '.join(receive_options))
962 git_command += [remote, 'HEAD:refs/for/' + branch]
963 RunGit(git_command)
964 # TODO(ukai): parse Change-Id: and set issue number?
965 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967
ukai@chromium.orge8077812012-02-03 03:41:46 +0000968def RietveldUpload(options, args, cl):
969 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 upload_args = ['--assume_yes'] # Don't ask about untracked files.
971 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 if options.emulate_svn_auto_props:
973 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974
975 change_desc = None
976
977 if cl.GetIssue():
978 if options.message:
979 upload_args.extend(['--message', options.message])
980 upload_args.extend(['--issue', cl.GetIssue()])
981 print ("This branch is associated with issue %s. "
982 "Adding patch to that issue." % cl.GetIssue())
983 else:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000984 message = options.message or CreateDescriptionFromLog(args)
985 change_desc = ChangeDescription(message, options.reviewers)
986 if not options.force:
987 change_desc.Prompt()
988 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000989
990 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991 print "Description is empty; aborting."
992 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000993
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000994 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000995 if change_desc.reviewers:
996 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000997 if options.send_mail:
998 if not change_desc.reviewers:
999 DieWithError("Must specify reviewers to send email.")
1000 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001001 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001002 if cc:
1003 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004
1005 # Include the upstream repo's URL in the change -- this is useful for
1006 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001007 remote_url = cl.GetGitBaseUrlFromConfig()
1008 if not remote_url:
1009 if settings.GetIsGitSvn():
1010 # URL is dependent on the current directory.
1011 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1012 if data:
1013 keys = dict(line.split(': ', 1) for line in data.splitlines()
1014 if ': ' in line)
1015 remote_url = keys.get('URL', None)
1016 else:
1017 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1018 remote_url = (cl.GetRemoteUrl() + '@'
1019 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 if remote_url:
1021 upload_args.extend(['--base_url', remote_url])
1022
1023 try:
1024 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001025 except KeyboardInterrupt:
1026 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 except:
1028 # If we got an exception after the user typed a description for their
1029 # change, back up the description before re-raising.
1030 if change_desc:
1031 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1032 print '\nGot exception while uploading -- saving description to %s\n' \
1033 % backup_path
1034 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001035 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 backup_file.close()
1037 raise
1038
1039 if not cl.GetIssue():
1040 cl.SetIssue(issue)
1041 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001042
1043 if options.use_commit_queue:
1044 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 return 0
1046
1047
ukai@chromium.orge8077812012-02-03 03:41:46 +00001048@usage('[args to "git diff"]')
1049def CMDupload(parser, args):
1050 """upload the current changelist to codereview"""
1051 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1052 help='bypass upload presubmit hook')
1053 parser.add_option('-f', action='store_true', dest='force',
1054 help="force yes to questions (don't prompt)")
1055 parser.add_option('-m', dest='message', help='message for patch')
1056 parser.add_option('-r', '--reviewers',
1057 help='reviewer email addresses')
1058 parser.add_option('--cc',
1059 help='cc email addresses')
1060 parser.add_option('--send-mail', action='store_true',
1061 help='send email to reviewer immediately')
1062 parser.add_option("--emulate_svn_auto_props", action="store_true",
1063 dest="emulate_svn_auto_props",
1064 help="Emulate Subversion's auto properties feature.")
1065 parser.add_option("--desc_from_logs", action="store_true",
1066 dest="from_logs",
1067 help="""Squashes git commit logs into change description and
1068 uses message as subject""")
1069 parser.add_option('-c', '--use-commit-queue', action='store_true',
1070 help='tell the commit queue to commit this patchset')
1071 if settings.GetIsGerrit():
1072 parser.add_option('--target_branch', dest='target_branch', default='master',
1073 help='target branch to upload')
1074 (options, args) = parser.parse_args(args)
1075
1076 # Make sure index is up-to-date before running diff-index.
1077 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1078 if RunGit(['diff-index', 'HEAD']):
1079 print 'Cannot upload with a dirty tree. You must commit locally first.'
1080 return 1
1081
1082 cl = Changelist()
1083 if args:
1084 # TODO(ukai): is it ok for gerrit case?
1085 base_branch = args[0]
1086 else:
1087 # Default to diffing against the "upstream" branch.
1088 base_branch = cl.GetUpstreamBranch()
1089 args = [base_branch + "..."]
1090
1091 if not options.bypass_hooks:
1092 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1093 may_prompt=not options.force,
1094 verbose=options.verbose,
1095 author=None)
1096 if not hook_results.should_continue():
1097 return 1
1098 if not options.reviewers and hook_results.reviewers:
1099 options.reviewers = hook_results.reviewers
1100
1101 # --no-ext-diff is broken in some versions of Git, so try to work around
1102 # this by overriding the environment (but there is still a problem if the
1103 # git config key "diff.external" is used).
1104 env = os.environ.copy()
1105 if 'GIT_EXTERNAL_DIFF' in env:
1106 del env['GIT_EXTERNAL_DIFF']
1107 subprocess2.call(
1108 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1109
1110 if settings.GetIsGerrit():
1111 return GerritUpload(options, args, cl)
1112 return RietveldUpload(options, args, cl)
1113
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115def SendUpstream(parser, args, cmd):
1116 """Common code for CmdPush and CmdDCommit
1117
1118 Squashed commit into a single.
1119 Updates changelog with metadata (e.g. pointer to review).
1120 Pushes/dcommits the code upstream.
1121 Updates review and closes.
1122 """
1123 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1124 help='bypass upload presubmit hook')
1125 parser.add_option('-m', dest='message',
1126 help="override review description")
1127 parser.add_option('-f', action='store_true', dest='force',
1128 help="force yes to questions (don't prompt)")
1129 parser.add_option('-c', dest='contributor',
1130 help="external contributor for patch (appended to " +
1131 "description and used as author for git). Should be " +
1132 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 (options, args) = parser.parse_args(args)
1134 cl = Changelist()
1135
1136 if not args or cmd == 'push':
1137 # Default to merging against our best guess of the upstream branch.
1138 args = [cl.GetUpstreamBranch()]
1139
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001140 if options.contributor:
1141 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1142 print "Please provide contibutor as 'First Last <email@example.com>'"
1143 return 1
1144
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 base_branch = args[0]
1146
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001147 # Make sure index is up-to-date before running diff-index.
1148 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 if RunGit(['diff-index', 'HEAD']):
1150 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1151 return 1
1152
1153 # This rev-list syntax means "show all commits not in my branch that
1154 # are in base_branch".
1155 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1156 base_branch]).splitlines()
1157 if upstream_commits:
1158 print ('Base branch "%s" has %d commits '
1159 'not in this branch.' % (base_branch, len(upstream_commits)))
1160 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1161 return 1
1162
1163 if cmd == 'dcommit':
1164 # This is the revision `svn dcommit` will commit on top of.
1165 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1166 '--pretty=format:%H'])
1167 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1168 if extra_commits:
1169 print ('This branch has %d additional commits not upstreamed yet.'
1170 % len(extra_commits.splitlines()))
1171 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1172 'before attempting to %s.' % (base_branch, cmd))
1173 return 1
1174
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001175 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001176 author = None
1177 if options.contributor:
1178 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001179 hook_results = cl.RunHook(
1180 committing=True,
1181 upstream_branch=base_branch,
1182 may_prompt=not options.force,
1183 verbose=options.verbose,
1184 author=author)
1185 if not hook_results.should_continue():
1186 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
1188 if cmd == 'dcommit':
1189 # Check the tree status if the tree status URL is set.
1190 status = GetTreeStatus()
1191 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001192 print('The tree is closed. Please wait for it to reopen. Use '
1193 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194 return 1
1195 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001196 print('Unable to determine tree status. Please verify manually and '
1197 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001198 else:
1199 breakpad.SendStack(
1200 'GitClHooksBypassedCommit',
1201 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001202 (cl.GetRietveldServer(), cl.GetIssue()),
1203 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204
1205 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001206 if not description and cl.GetIssue():
1207 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001209 if not description:
1210 print 'No description set.'
1211 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1212 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001214 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
1217 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 description += "\nPatch from %s." % options.contributor
1219 print 'Description:', repr(description)
1220
1221 branches = [base_branch, cl.GetBranchRef()]
1222 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001223 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001224 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225
1226 # We want to squash all this branch's commits into one commit with the
1227 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001228 # We do this by doing a "reset --soft" to the base branch (which keeps
1229 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 MERGE_BRANCH = 'git-cl-commit'
1231 # Delete the merge branch if it already exists.
1232 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1233 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1234 RunGit(['branch', '-D', MERGE_BRANCH])
1235
1236 # We might be in a directory that's present in this branch but not in the
1237 # trunk. Move up to the top of the tree so that git commands that expect a
1238 # valid CWD won't fail after we check out the merge branch.
1239 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1240 if rel_base_path:
1241 os.chdir(rel_base_path)
1242
1243 # Stuff our change into the merge branch.
1244 # We wrap in a try...finally block so if anything goes wrong,
1245 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001246 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001248 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1249 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 if options.contributor:
1251 RunGit(['commit', '--author', options.contributor, '-m', description])
1252 else:
1253 RunGit(['commit', '-m', description])
1254 if cmd == 'push':
1255 # push the merge branch.
1256 remote, branch = cl.FetchUpstreamTuple()
1257 retcode, output = RunGitWithCode(
1258 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1259 logging.debug(output)
1260 else:
1261 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001262 retcode, output = RunGitWithCode(['svn', 'dcommit',
1263 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 finally:
1265 # And then swap back to the original branch and clean up.
1266 RunGit(['checkout', '-q', cl.GetBranch()])
1267 RunGit(['branch', '-D', MERGE_BRANCH])
1268
1269 if cl.GetIssue():
1270 if cmd == 'dcommit' and 'Committed r' in output:
1271 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1272 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001273 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1274 for l in output.splitlines(False))
1275 match = filter(None, match)
1276 if len(match) != 1:
1277 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1278 output)
1279 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 else:
1281 return 1
1282 viewvc_url = settings.GetViewVCUrl()
1283 if viewvc_url and revision:
1284 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1285 print ('Closing issue '
1286 '(you may be prompted for your codereview password)...')
1287 cl.CloseIssue()
1288 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001289
1290 if retcode == 0:
1291 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1292 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001293 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 return 0
1296
1297
1298@usage('[upstream branch to apply against]')
1299def CMDdcommit(parser, args):
1300 """commit the current changelist via git-svn"""
1301 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001302 message = """This doesn't appear to be an SVN repository.
1303If your project has a git mirror with an upstream SVN master, you probably need
1304to run 'git svn init', see your project's git mirror documentation.
1305If your project has a true writeable upstream repository, you probably want
1306to run 'git cl push' instead.
1307Choose wisely, if you get this wrong, your commit might appear to succeed but
1308will instead be silently ignored."""
1309 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001310 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 return SendUpstream(parser, args, 'dcommit')
1312
1313
1314@usage('[upstream branch to apply against]')
1315def CMDpush(parser, args):
1316 """commit the current changelist via git"""
1317 if settings.GetIsGitSvn():
1318 print('This appears to be an SVN repository.')
1319 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001320 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 return SendUpstream(parser, args, 'push')
1322
1323
1324@usage('<patch url or issue id>')
1325def CMDpatch(parser, args):
1326 """patch in a code review"""
1327 parser.add_option('-b', dest='newbranch',
1328 help='create a new branch off trunk for the patch')
1329 parser.add_option('-f', action='store_true', dest='force',
1330 help='with -b, clobber any existing branch')
1331 parser.add_option('--reject', action='store_true', dest='reject',
1332 help='allow failed patches and spew .rej files')
1333 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1334 help="don't commit after patch applies")
1335 (options, args) = parser.parse_args(args)
1336 if len(args) != 1:
1337 parser.print_help()
1338 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001339 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001341 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001342 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001343
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001344 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001346 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001347 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001349 # Assume it's a URL to the patch. Default to https.
1350 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001351 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001352 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 DieWithError('Must pass an issue ID or full URL for '
1354 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001355 issue = match.group(1)
1356 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357
1358 if options.newbranch:
1359 if options.force:
1360 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001361 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 RunGit(['checkout', '-b', options.newbranch,
1363 Changelist().GetUpstreamBranch()])
1364
1365 # Switch up to the top-level directory, if necessary, in preparation for
1366 # applying the patch.
1367 top = RunGit(['rev-parse', '--show-cdup']).strip()
1368 if top:
1369 os.chdir(top)
1370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 # Git patches have a/ at the beginning of source paths. We strip that out
1372 # with a sed script rather than the -p flag to patch so we can feed either
1373 # Git or svn-style patches into the same apply command.
1374 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001375 try:
1376 patch_data = subprocess2.check_output(
1377 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1378 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 DieWithError('Git patch mungling failed.')
1380 logging.info(patch_data)
1381 # We use "git apply" to apply the patch instead of "patch" so that we can
1382 # pick up file adds.
1383 # The --index flag means: also insert into the index (so we catch adds).
1384 cmd = ['git', 'apply', '--index', '-p0']
1385 if options.reject:
1386 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001387 try:
1388 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1389 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 DieWithError('Failed to apply the patch')
1391
1392 # If we had an issue, commit the current state and register the issue.
1393 if not options.nocommit:
1394 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1395 cl = Changelist()
1396 cl.SetIssue(issue)
1397 print "Committed patch."
1398 else:
1399 print "Patch applied to index."
1400 return 0
1401
1402
1403def CMDrebase(parser, args):
1404 """rebase current branch on top of svn repo"""
1405 # Provide a wrapper for git svn rebase to help avoid accidental
1406 # git svn dcommit.
1407 # It's the only command that doesn't use parser at all since we just defer
1408 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001409 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410
1411
1412def GetTreeStatus():
1413 """Fetches the tree status and returns either 'open', 'closed',
1414 'unknown' or 'unset'."""
1415 url = settings.GetTreeStatusUrl(error_ok=True)
1416 if url:
1417 status = urllib2.urlopen(url).read().lower()
1418 if status.find('closed') != -1 or status == '0':
1419 return 'closed'
1420 elif status.find('open') != -1 or status == '1':
1421 return 'open'
1422 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return 'unset'
1424
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001425
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426def GetTreeStatusReason():
1427 """Fetches the tree status from a json url and returns the message
1428 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001429 url = settings.GetTreeStatusUrl()
1430 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 connection = urllib2.urlopen(json_url)
1432 status = json.loads(connection.read())
1433 connection.close()
1434 return status['message']
1435
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437def CMDtree(parser, args):
1438 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001439 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 status = GetTreeStatus()
1441 if 'unset' == status:
1442 print 'You must configure your tree status URL by running "git cl config".'
1443 return 2
1444
1445 print "The tree is %s" % status
1446 print
1447 print GetTreeStatusReason()
1448 if status != 'open':
1449 return 1
1450 return 0
1451
1452
1453def CMDupstream(parser, args):
1454 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001455 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001456 if args:
1457 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 cl = Changelist()
1459 print cl.GetUpstreamBranch()
1460 return 0
1461
1462
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001463def CMDset_commit(parser, args):
1464 """set the commit bit"""
1465 _, args = parser.parse_args(args)
1466 if args:
1467 parser.error('Unrecognized args: %s' % ' '.join(args))
1468 cl = Changelist()
1469 cl.SetFlag('commit', '1')
1470 return 0
1471
1472
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473def Command(name):
1474 return getattr(sys.modules[__name__], 'CMD' + name, None)
1475
1476
1477def CMDhelp(parser, args):
1478 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001479 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480 if len(args) == 1:
1481 return main(args + ['--help'])
1482 parser.print_help()
1483 return 0
1484
1485
1486def GenUsage(parser, command):
1487 """Modify an OptParse object with the function's documentation."""
1488 obj = Command(command)
1489 more = getattr(obj, 'usage_more', '')
1490 if command == 'help':
1491 command = '<command>'
1492 else:
1493 # OptParser.description prefer nicely non-formatted strings.
1494 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1495 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1496
1497
1498def main(argv):
1499 """Doesn't parse the arguments here, just find the right subcommand to
1500 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001501 if sys.hexversion < 0x02060000:
1502 print >> sys.stderr, (
1503 '\nYour python version %s is unsupported, please upgrade.\n' %
1504 sys.version.split(' ', 1)[0])
1505 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001506 # Reload settings.
1507 global settings
1508 settings = Settings()
1509
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 # Do it late so all commands are listed.
1511 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1512 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1513 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1514
1515 # Create the option parse and add --verbose support.
1516 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001517 parser.add_option(
1518 '-v', '--verbose', action='count', default=0,
1519 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520 old_parser_args = parser.parse_args
1521 def Parse(args):
1522 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001523 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001524 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001525 elif options.verbose:
1526 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 else:
1528 logging.basicConfig(level=logging.WARNING)
1529 return options, args
1530 parser.parse_args = Parse
1531
1532 if argv:
1533 command = Command(argv[0])
1534 if command:
1535 # "fix" the usage and the description now that we know the subcommand.
1536 GenUsage(parser, argv[0])
1537 try:
1538 return command(parser, argv[1:])
1539 except urllib2.HTTPError, e:
1540 if e.code != 500:
1541 raise
1542 DieWithError(
1543 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1544 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1545
1546 # Not a known command. Default to help.
1547 GenUsage(parser, 'help')
1548 return CMDhelp(parser, argv)
1549
1550
1551if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001552 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553 sys.exit(main(sys.argv[1:]))