blob: 5bd6d1be4db4d54e49cc3abd2c9fe409ba1643c4 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import urllib2
21
22try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000023 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024except ImportError:
25 pass
26
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027
28from third_party import upload
29import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000030import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000031import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000033import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000035import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import watchlists
37
38
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000039DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000040POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000042GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043
maruel@chromium.org90541732011-04-01 17:54:18 +000044
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045# Initialized in main()
46settings = None
47
48
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000050 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051 sys.exit(1)
52
53
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000056 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057 except subprocess2.CalledProcessError, e:
58 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060 'Command "%s" failed.\n%s' % (
61 ' '.join(args), error_message or e.stdout or ''))
62 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063
64
65def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 """Returns stdout."""
67 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000072 try:
73 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
74 return code, out[0]
75 except ValueError:
76 # When the subprocess fails, it returns None. That triggers a ValueError
77 # when trying to unpack the return value into (out, code).
78 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
80
81def usage(more):
82 def hook(fn):
83 fn.usage_more = more
84 return fn
85 return hook
86
87
maruel@chromium.org90541732011-04-01 17:54:18 +000088def ask_for_data(prompt):
89 try:
90 return raw_input(prompt)
91 except KeyboardInterrupt:
92 # Hide the exception.
93 sys.exit(1)
94
95
bauerb@chromium.org866276c2011-03-18 20:09:31 +000096def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
97 """Return the corresponding git ref if |base_url| together with |glob_spec|
98 matches the full |url|.
99
100 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
101 """
102 fetch_suburl, as_ref = glob_spec.split(':')
103 if allow_wildcards:
104 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
105 if glob_match:
106 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
107 # "branches/{472,597,648}/src:refs/remotes/svn/*".
108 branch_re = re.escape(base_url)
109 if glob_match.group(1):
110 branch_re += '/' + re.escape(glob_match.group(1))
111 wildcard = glob_match.group(2)
112 if wildcard == '*':
113 branch_re += '([^/]*)'
114 else:
115 # Escape and replace surrounding braces with parentheses and commas
116 # with pipe symbols.
117 wildcard = re.escape(wildcard)
118 wildcard = re.sub('^\\\\{', '(', wildcard)
119 wildcard = re.sub('\\\\,', '|', wildcard)
120 wildcard = re.sub('\\\\}$', ')', wildcard)
121 branch_re += wildcard
122 if glob_match.group(3):
123 branch_re += re.escape(glob_match.group(3))
124 match = re.match(branch_re, url)
125 if match:
126 return re.sub('\*$', match.group(1), as_ref)
127
128 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
129 if fetch_suburl:
130 full_url = base_url + '/' + fetch_suburl
131 else:
132 full_url = base_url
133 if full_url == url:
134 return as_ref
135 return None
136
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138class Settings(object):
139 def __init__(self):
140 self.default_server = None
141 self.cc = None
142 self.root = None
143 self.is_git_svn = None
144 self.svn_branch = None
145 self.tree_status_url = None
146 self.viewvc_url = None
147 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000148 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000149
150 def LazyUpdateIfNeeded(self):
151 """Updates the settings from a codereview.settings file, if available."""
152 if not self.updated:
153 cr_settings_file = FindCodereviewSettingsFile()
154 if cr_settings_file:
155 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000156 self.updated = True
157 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000158 self.updated = True
159
160 def GetDefaultServerUrl(self, error_ok=False):
161 if not self.default_server:
162 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000163 self.default_server = gclient_utils.UpgradeToHttps(
164 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000165 if error_ok:
166 return self.default_server
167 if not self.default_server:
168 error_message = ('Could not find settings file. You must configure '
169 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000170 self.default_server = gclient_utils.UpgradeToHttps(
171 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000172 return self.default_server
173
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000174 def GetRoot(self):
175 if not self.root:
176 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
177 return self.root
178
179 def GetIsGitSvn(self):
180 """Return true if this repo looks like it's using git-svn."""
181 if self.is_git_svn is None:
182 # If you have any "svn-remote.*" config keys, we think you're using svn.
183 self.is_git_svn = RunGitWithCode(
184 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
185 return self.is_git_svn
186
187 def GetSVNBranch(self):
188 if self.svn_branch is None:
189 if not self.GetIsGitSvn():
190 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
191
192 # Try to figure out which remote branch we're based on.
193 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000194 # 1) iterate through our branch history and find the svn URL.
195 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000196
197 # regexp matching the git-svn line that contains the URL.
198 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
199
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000200 # We don't want to go through all of history, so read a line from the
201 # pipe at a time.
202 # The -100 is an arbitrary limit so we don't search forever.
203 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000204 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000205 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000206 for line in proc.stdout:
207 match = git_svn_re.match(line)
208 if match:
209 url = match.group(1)
210 proc.stdout.close() # Cut pipe.
211 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000212
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000213 if url:
214 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
215 remotes = RunGit(['config', '--get-regexp',
216 r'^svn-remote\..*\.url']).splitlines()
217 for remote in remotes:
218 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000219 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000220 remote = match.group(1)
221 base_url = match.group(2)
222 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000223 ['config', 'svn-remote.%s.fetch' % remote],
224 error_ok=True).strip()
225 if fetch_spec:
226 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
227 if self.svn_branch:
228 break
229 branch_spec = RunGit(
230 ['config', 'svn-remote.%s.branches' % remote],
231 error_ok=True).strip()
232 if branch_spec:
233 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
234 if self.svn_branch:
235 break
236 tag_spec = RunGit(
237 ['config', 'svn-remote.%s.tags' % remote],
238 error_ok=True).strip()
239 if tag_spec:
240 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
241 if self.svn_branch:
242 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000243
244 if not self.svn_branch:
245 DieWithError('Can\'t guess svn branch -- try specifying it on the '
246 'command line')
247
248 return self.svn_branch
249
250 def GetTreeStatusUrl(self, error_ok=False):
251 if not self.tree_status_url:
252 error_message = ('You must configure your tree status URL by running '
253 '"git cl config".')
254 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
255 error_ok=error_ok,
256 error_message=error_message)
257 return self.tree_status_url
258
259 def GetViewVCUrl(self):
260 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000261 self.viewvc_url = gclient_utils.UpgradeToHttps(
262 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000263 return self.viewvc_url
264
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000265 def GetDefaultCCList(self):
266 return self._GetConfig('rietveld.cc', error_ok=True)
267
ukai@chromium.orge8077812012-02-03 03:41:46 +0000268 def GetIsGerrit(self):
269 """Return true if this repo is assosiated with gerrit code review system."""
270 if self.is_gerrit is None:
271 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
272 return self.is_gerrit
273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274 def _GetConfig(self, param, **kwargs):
275 self.LazyUpdateIfNeeded()
276 return RunGit(['config', param], **kwargs).strip()
277
278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000279def ShortBranchName(branch):
280 """Convert a name like 'refs/heads/foo' to just 'foo'."""
281 return branch.replace('refs/heads/', '')
282
283
284class Changelist(object):
285 def __init__(self, branchref=None):
286 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000287 global settings
288 if not settings:
289 # Happens when git_cl.py is used as a utility library.
290 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291 settings.GetDefaultServerUrl()
292 self.branchref = branchref
293 if self.branchref:
294 self.branch = ShortBranchName(self.branchref)
295 else:
296 self.branch = None
297 self.rietveld_server = None
298 self.upstream_branch = None
299 self.has_issue = False
300 self.issue = None
301 self.has_description = False
302 self.description = None
303 self.has_patchset = False
304 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000305 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000306 self.cc = None
307 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000308 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000309
310 def GetCCList(self):
311 """Return the users cc'd on this CL.
312
313 Return is a string suitable for passing to gcl with the --cc flag.
314 """
315 if self.cc is None:
316 base_cc = settings .GetDefaultCCList()
317 more_cc = ','.join(self.watchers)
318 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
319 return self.cc
320
321 def SetWatchers(self, watchers):
322 """Set the list of email addresses that should be cc'd based on the changed
323 files in this CL.
324 """
325 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
327 def GetBranch(self):
328 """Returns the short branch name, e.g. 'master'."""
329 if not self.branch:
330 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
331 self.branch = ShortBranchName(self.branchref)
332 return self.branch
333
334 def GetBranchRef(self):
335 """Returns the full branch name, e.g. 'refs/heads/master'."""
336 self.GetBranch() # Poke the lazy loader.
337 return self.branchref
338
339 def FetchUpstreamTuple(self):
340 """Returns a tuple containg remote and remote ref,
341 e.g. 'origin', 'refs/heads/master'
342 """
343 remote = '.'
344 branch = self.GetBranch()
345 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
346 error_ok=True).strip()
347 if upstream_branch:
348 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
349 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000350 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
351 error_ok=True).strip()
352 if upstream_branch:
353 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000354 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000355 # Fall back on trying a git-svn upstream branch.
356 if settings.GetIsGitSvn():
357 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000358 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000359 # Else, try to guess the origin remote.
360 remote_branches = RunGit(['branch', '-r']).split()
361 if 'origin/master' in remote_branches:
362 # Fall back on origin/master if it exits.
363 remote = 'origin'
364 upstream_branch = 'refs/heads/master'
365 elif 'origin/trunk' in remote_branches:
366 # Fall back on origin/trunk if it exists. Generally a shared
367 # git-svn clone
368 remote = 'origin'
369 upstream_branch = 'refs/heads/trunk'
370 else:
371 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372Either pass complete "git diff"-style arguments, like
373 git cl upload origin/master
374or verify this branch is set up to track another (via the --track argument to
375"git checkout -b ...").""")
376
377 return remote, upstream_branch
378
379 def GetUpstreamBranch(self):
380 if self.upstream_branch is None:
381 remote, upstream_branch = self.FetchUpstreamTuple()
382 if remote is not '.':
383 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
384 self.upstream_branch = upstream_branch
385 return self.upstream_branch
386
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000387 def GetRemote(self):
388 if not self._remote:
389 self._remote = self.FetchUpstreamTuple()[0]
390 if self._remote == '.':
391
392 remotes = RunGit(['remote'], error_ok=True).split()
393 if len(remotes) == 1:
394 self._remote, = remotes
395 elif 'origin' in remotes:
396 self._remote = 'origin'
397 logging.warning('Could not determine which remote this change is '
398 'associated with, so defaulting to "%s". This may '
399 'not be what you want. You may prevent this message '
400 'by running "git svn info" as documented here: %s',
401 self._remote,
402 GIT_INSTRUCTIONS_URL)
403 else:
404 logging.warn('Could not determine which remote this change is '
405 'associated with. You may prevent this message by '
406 'running "git svn info" as documented here: %s',
407 GIT_INSTRUCTIONS_URL)
408 return self._remote
409
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000410 def GetGitBaseUrlFromConfig(self):
411 """Return the configured base URL from branch.<branchname>.baseurl.
412
413 Returns None if it is not set.
414 """
415 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
416 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000417
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418 def GetRemoteUrl(self):
419 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
420
421 Returns None if there is no remote.
422 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000423 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000424 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
425
426 def GetIssue(self):
427 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
429 if issue:
430 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 else:
432 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 self.has_issue = True
434 return self.issue
435
436 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000437 if not self.rietveld_server:
438 # If we're on a branch then get the server potentially associated
439 # with that branch.
440 if self.GetIssue():
441 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
442 ['config', self._RietveldServer()], error_ok=True).strip())
443 if not self.rietveld_server:
444 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000445 return self.rietveld_server
446
447 def GetIssueURL(self):
448 """Get the URL for a particular issue."""
449 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
450
451 def GetDescription(self, pretty=False):
452 if not self.has_description:
453 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000454 issue = int(self.GetIssue())
455 try:
456 self.description = self.RpcServer().get_description(issue).strip()
457 except urllib2.HTTPError, e:
458 if e.code == 404:
459 DieWithError(
460 ('\nWhile fetching the description for issue %d, received a '
461 '404 (not found)\n'
462 'error. It is likely that you deleted this '
463 'issue on the server. If this is the\n'
464 'case, please run\n\n'
465 ' git cl issue 0\n\n'
466 'to clear the association with the deleted issue. Then run '
467 'this command again.') % issue)
468 else:
469 DieWithError(
470 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 self.has_description = True
472 if pretty:
473 wrapper = textwrap.TextWrapper()
474 wrapper.initial_indent = wrapper.subsequent_indent = ' '
475 return wrapper.fill(self.description)
476 return self.description
477
478 def GetPatchset(self):
479 if not self.has_patchset:
480 patchset = RunGit(['config', self._PatchsetSetting()],
481 error_ok=True).strip()
482 if patchset:
483 self.patchset = patchset
484 else:
485 self.patchset = None
486 self.has_patchset = True
487 return self.patchset
488
489 def SetPatchset(self, patchset):
490 """Set this branch's patchset. If patchset=0, clears the patchset."""
491 if patchset:
492 RunGit(['config', self._PatchsetSetting(), str(patchset)])
493 else:
494 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000495 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000496 self.has_patchset = False
497
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000498 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000499 patchset = self.RpcServer().get_issue_properties(
500 int(issue), False)['patchsets'][-1]
501 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000502 '/download/issue%s_%s.diff' % (issue, patchset))
503
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504 def SetIssue(self, issue):
505 """Set this branch's issue. If issue=0, clears the issue."""
506 if issue:
507 RunGit(['config', self._IssueSetting(), str(issue)])
508 if self.rietveld_server:
509 RunGit(['config', self._RietveldServer(), self.rietveld_server])
510 else:
511 RunGit(['config', '--unset', self._IssueSetting()])
512 self.SetPatchset(0)
513 self.has_issue = False
514
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000515 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000516 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
517 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000518
519 # We use the sha1 of HEAD as a name of this change.
520 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000521 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000522 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000523 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000524 except subprocess2.CalledProcessError:
525 DieWithError(
526 ('\nFailed to diff against upstream branch %s!\n\n'
527 'This branch probably doesn\'t exist anymore. To reset the\n'
528 'tracking branch, please run\n'
529 ' git branch --set-upstream %s trunk\n'
530 'replacing trunk with origin/master or the relevant branch') %
531 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000532
533 issue = ConvertToInteger(self.GetIssue())
534 patchset = ConvertToInteger(self.GetPatchset())
535 if issue:
536 description = self.GetDescription()
537 else:
538 # If the change was never uploaded, use the log messages of all commits
539 # up to the branch point, as git cl upload will prefill the description
540 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000541 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
542 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000543
544 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000545 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000546 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000547 name,
548 description,
549 absroot,
550 files,
551 issue,
552 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000553 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000554
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000555 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
556 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
557 change = self.GetChange(upstream_branch, author)
558
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000559 # Apply watchlists on upload.
560 if not committing:
561 watchlist = watchlists.Watchlists(change.RepositoryRoot())
562 files = [f.LocalPath() for f in change.AffectedFiles()]
563 self.SetWatchers(watchlist.GetWatchersForPaths(files))
564
565 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000566 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000567 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000568 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000569 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000570 except presubmit_support.PresubmitFailure, e:
571 DieWithError(
572 ('%s\nMaybe your depot_tools is out of date?\n'
573 'If all fails, contact maruel@') % e)
574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000576 """Updates the description and closes the issue."""
577 issue = int(self.GetIssue())
578 self.RpcServer().update_description(issue, self.description)
579 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000581 def SetFlag(self, flag, value):
582 """Patchset must match."""
583 if not self.GetPatchset():
584 DieWithError('The patchset needs to match. Send another patchset.')
585 try:
586 return self.RpcServer().set_flag(
587 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
588 except urllib2.HTTPError, e:
589 if e.code == 404:
590 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
591 if e.code == 403:
592 DieWithError(
593 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
594 'match?') % (self.GetIssue(), self.GetPatchset()))
595 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000597 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598 """Returns an upload.RpcServer() to access this review's rietveld instance.
599 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000600 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000601 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
602 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000603 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604
605 def _IssueSetting(self):
606 """Return the git setting that stores this change's issue."""
607 return 'branch.%s.rietveldissue' % self.GetBranch()
608
609 def _PatchsetSetting(self):
610 """Return the git setting that stores this change's most recent patchset."""
611 return 'branch.%s.rietveldpatchset' % self.GetBranch()
612
613 def _RietveldServer(self):
614 """Returns the git setting that stores this change's rietveld server."""
615 return 'branch.%s.rietveldserver' % self.GetBranch()
616
617
618def GetCodereviewSettingsInteractively():
619 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000620 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 server = settings.GetDefaultServerUrl(error_ok=True)
622 prompt = 'Rietveld server (host[:port])'
623 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000624 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 if not server and not newserver:
626 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000627 if newserver:
628 newserver = gclient_utils.UpgradeToHttps(newserver)
629 if newserver != server:
630 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000632 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 prompt = caption
634 if initial:
635 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000636 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 if new_val == 'x':
638 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000639 elif new_val:
640 if is_url:
641 new_val = gclient_utils.UpgradeToHttps(new_val)
642 if new_val != initial:
643 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000645 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000647 'tree-status-url', False)
648 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649
650 # TODO: configure a default branch to diff against, rather than this
651 # svn-based hackery.
652
653
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000654class ChangeDescription(object):
655 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000656 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000657 self.log_desc = log_desc
658 self.reviewers = reviewers
659 self.description = self.log_desc
660
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000661 def Prompt(self):
662 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000663# This will displayed on the codereview site.
664# The first line will also be used as the subject of the review.
665"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000666 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000667 if ('\nR=' not in self.description and
668 '\nTBR=' not in self.description and
669 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000670 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000671 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000672 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000673 if '\nTEST=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000674 content += '\nTEST='
675 content = content.rstrip('\n') + '\n'
676 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000677 if not content:
678 DieWithError('Running editor failed')
679 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000680 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000681 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000682 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000683
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000684 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000685 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000686 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000687 # Retrieves all reviewer lines
688 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000689 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000690 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000691 if reviewers:
692 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000693
694 def IsEmpty(self):
695 return not self.description
696
697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698def FindCodereviewSettingsFile(filename='codereview.settings'):
699 """Finds the given file starting in the cwd and going up.
700
701 Only looks up to the top of the repository unless an
702 'inherit-review-settings-ok' file exists in the root of the repository.
703 """
704 inherit_ok_file = 'inherit-review-settings-ok'
705 cwd = os.getcwd()
706 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
707 if os.path.isfile(os.path.join(root, inherit_ok_file)):
708 root = '/'
709 while True:
710 if filename in os.listdir(cwd):
711 if os.path.isfile(os.path.join(cwd, filename)):
712 return open(os.path.join(cwd, filename))
713 if cwd == root:
714 break
715 cwd = os.path.dirname(cwd)
716
717
718def LoadCodereviewSettingsFromFile(fileobj):
719 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000720 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 def SetProperty(name, setting, unset_error_ok=False):
723 fullname = 'rietveld.' + name
724 if setting in keyvals:
725 RunGit(['config', fullname, keyvals[setting]])
726 else:
727 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
728
729 SetProperty('server', 'CODE_REVIEW_SERVER')
730 # Only server setting is required. Other settings can be absent.
731 # In that case, we ignore errors raised during option deletion attempt.
732 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
733 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
734 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
735
ukai@chromium.orge8077812012-02-03 03:41:46 +0000736 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
737 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
738 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
741 #should be of the form
742 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
743 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
744 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
745 keyvals['ORIGIN_URL_CONFIG']])
746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000748def DownloadHooks(force):
749 """downloads hooks
750
751 Args:
752 force: True to update hooks. False to install hooks if not present.
753 """
754 if not settings.GetIsGerrit():
755 return
756 server_url = settings.GetDefaultServerUrl()
757 src = '%s/tools/hooks/commit-msg' % server_url
758 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
759 if not os.access(dst, os.X_OK):
760 if os.path.exists(dst):
761 if not force:
762 return
763 os.remove(dst)
764 try:
765 urllib.urlretrieve(src, dst)
766 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
767 except Exception:
768 if os.path.exists(dst):
769 os.remove(dst)
770 DieWithError('\nFailed to download hooks from %s' % src)
771
772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773@usage('[repo root containing codereview.settings]')
774def CMDconfig(parser, args):
775 """edit configuration for this tree"""
776
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000777 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 if len(args) == 0:
779 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000780 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 return 0
782
783 url = args[0]
784 if not url.endswith('codereview.settings'):
785 url = os.path.join(url, 'codereview.settings')
786
787 # Load code review settings and download hooks (if available).
788 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000789 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 return 0
791
792
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000793def CMDbaseurl(parser, args):
794 """get or set base-url for this branch"""
795 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
796 branch = ShortBranchName(branchref)
797 _, args = parser.parse_args(args)
798 if not args:
799 print("Current base-url:")
800 return RunGit(['config', 'branch.%s.base-url' % branch],
801 error_ok=False).strip()
802 else:
803 print("Setting base-url to %s" % args[0])
804 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
805 error_ok=False).strip()
806
807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808def CMDstatus(parser, args):
809 """show status of changelists"""
810 parser.add_option('--field',
811 help='print only specific field (desc|id|patch|url)')
812 (options, args) = parser.parse_args(args)
813
814 # TODO: maybe make show_branches a flag if necessary.
815 show_branches = not options.field
816
817 if show_branches:
818 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
819 if branches:
820 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000821 changes = (Changelist(branchref=b) for b in branches.splitlines())
822 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
823 alignment = max(5, max(len(b) for b in branches))
824 for branch in sorted(branches):
825 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
827 cl = Changelist()
828 if options.field:
829 if options.field.startswith('desc'):
830 print cl.GetDescription()
831 elif options.field == 'id':
832 issueid = cl.GetIssue()
833 if issueid:
834 print issueid
835 elif options.field == 'patch':
836 patchset = cl.GetPatchset()
837 if patchset:
838 print patchset
839 elif options.field == 'url':
840 url = cl.GetIssueURL()
841 if url:
842 print url
843 else:
844 print
845 print 'Current branch:',
846 if not cl.GetIssue():
847 print 'no issue assigned.'
848 return 0
849 print cl.GetBranch()
850 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
851 print 'Issue description:'
852 print cl.GetDescription(pretty=True)
853 return 0
854
855
856@usage('[issue_number]')
857def CMDissue(parser, args):
858 """Set or display the current code review issue number.
859
860 Pass issue number 0 to clear the current issue.
861"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000862 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
864 cl = Changelist()
865 if len(args) > 0:
866 try:
867 issue = int(args[0])
868 except ValueError:
869 DieWithError('Pass a number to set the issue or none to list it.\n'
870 'Maybe you want to run git cl status?')
871 cl.SetIssue(issue)
872 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
873 return 0
874
875
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000876def CMDcomments(parser, args):
877 """show review comments of the current changelist"""
878 (_, args) = parser.parse_args(args)
879 if args:
880 parser.error('Unsupported argument: %s' % args)
881
882 cl = Changelist()
883 if cl.GetIssue():
884 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
885 for message in sorted(data['messages'], key=lambda x: x['date']):
886 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
887 if message['text'].strip():
888 print '\n'.join(' ' + l for l in message['text'].splitlines())
889 return 0
890
891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892def CreateDescriptionFromLog(args):
893 """Pulls out the commit log to use as a base for the CL description."""
894 log_args = []
895 if len(args) == 1 and not args[0].endswith('.'):
896 log_args = [args[0] + '..']
897 elif len(args) == 1 and args[0].endswith('...'):
898 log_args = [args[0][:-1]]
899 elif len(args) == 2:
900 log_args = [args[0] + '..' + args[1]]
901 else:
902 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000903 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904
905
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000906def ConvertToInteger(inputval):
907 """Convert a string to integer, but returns either an int or None."""
908 try:
909 return int(inputval)
910 except (TypeError, ValueError):
911 return None
912
913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914def CMDpresubmit(parser, args):
915 """run presubmit tests on the current changelist"""
916 parser.add_option('--upload', action='store_true',
917 help='Run upload hook instead of the push/dcommit hook')
918 (options, args) = parser.parse_args(args)
919
920 # Make sure index is up-to-date before running diff-index.
921 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
922 if RunGit(['diff-index', 'HEAD']):
923 # TODO(maruel): Is this really necessary?
924 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
925 return 1
926
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000927 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 if args:
929 base_branch = args[0]
930 else:
931 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000932 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000934 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000935 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000936 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000937 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938
939
ukai@chromium.orge8077812012-02-03 03:41:46 +0000940def GerritUpload(options, args, cl):
941 """upload the current branch to gerrit."""
942 # We assume the remote called "origin" is the one we want.
943 # It is probably not worthwhile to support different workflows.
944 remote = 'origin'
945 branch = 'master'
946 if options.target_branch:
947 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000949 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000950 if options.reviewers:
951 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000952 change_desc = ChangeDescription(log_desc, options.reviewers)
953 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000954 if change_desc.IsEmpty():
955 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 return 1
957
ukai@chromium.orge8077812012-02-03 03:41:46 +0000958 receive_options = []
959 cc = cl.GetCCList().split(',')
960 if options.cc:
961 cc += options.cc.split(',')
962 cc = filter(None, cc)
963 if cc:
964 receive_options += ['--cc=' + email for email in cc]
965 if change_desc.reviewers:
966 reviewers = filter(None, change_desc.reviewers.split(','))
967 if reviewers:
968 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969
ukai@chromium.orge8077812012-02-03 03:41:46 +0000970 git_command = ['push']
971 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000972 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000973 ' '.join(receive_options))
974 git_command += [remote, 'HEAD:refs/for/' + branch]
975 RunGit(git_command)
976 # TODO(ukai): parse Change-Id: and set issue number?
977 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
ukai@chromium.orge8077812012-02-03 03:41:46 +0000980def RietveldUpload(options, args, cl):
981 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982 upload_args = ['--assume_yes'] # Don't ask about untracked files.
983 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984 if options.emulate_svn_auto_props:
985 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986
987 change_desc = None
988
989 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +0000990 if options.title:
991 upload_args.extend(['--title', options.title])
992 elif options.message:
993 # TODO(rogerta): for now, the -m option will also set the --title option
994 # for upload.py. Soon this will be changed to set the --message option.
995 # Will wait until people are used to typing -t instead of -m.
996 upload_args.extend(['--title', options.message])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997 upload_args.extend(['--issue', cl.GetIssue()])
998 print ("This branch is associated with issue %s. "
999 "Adding patch to that issue." % cl.GetIssue())
1000 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001001 if options.title:
1002 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001003 message = options.message or CreateDescriptionFromLog(args)
1004 change_desc = ChangeDescription(message, options.reviewers)
1005 if not options.force:
1006 change_desc.Prompt()
1007 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001008
1009 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 print "Description is empty; aborting."
1011 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001012
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001013 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001014 if change_desc.reviewers:
1015 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001016 if options.send_mail:
1017 if not change_desc.reviewers:
1018 DieWithError("Must specify reviewers to send email.")
1019 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001020 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001021 if cc:
1022 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
1024 # Include the upstream repo's URL in the change -- this is useful for
1025 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001026 remote_url = cl.GetGitBaseUrlFromConfig()
1027 if not remote_url:
1028 if settings.GetIsGitSvn():
1029 # URL is dependent on the current directory.
1030 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1031 if data:
1032 keys = dict(line.split(': ', 1) for line in data.splitlines()
1033 if ': ' in line)
1034 remote_url = keys.get('URL', None)
1035 else:
1036 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1037 remote_url = (cl.GetRemoteUrl() + '@'
1038 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if remote_url:
1040 upload_args.extend(['--base_url', remote_url])
1041
1042 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001043 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001044 except KeyboardInterrupt:
1045 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 except:
1047 # If we got an exception after the user typed a description for their
1048 # change, back up the description before re-raising.
1049 if change_desc:
1050 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1051 print '\nGot exception while uploading -- saving description to %s\n' \
1052 % backup_path
1053 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001054 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 backup_file.close()
1056 raise
1057
1058 if not cl.GetIssue():
1059 cl.SetIssue(issue)
1060 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001061
1062 if options.use_commit_queue:
1063 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 return 0
1065
1066
ukai@chromium.orge8077812012-02-03 03:41:46 +00001067@usage('[args to "git diff"]')
1068def CMDupload(parser, args):
1069 """upload the current changelist to codereview"""
1070 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1071 help='bypass upload presubmit hook')
1072 parser.add_option('-f', action='store_true', dest='force',
1073 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001074 parser.add_option('-m', dest='message', help='message for patchset')
1075 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001076 parser.add_option('-r', '--reviewers',
1077 help='reviewer email addresses')
1078 parser.add_option('--cc',
1079 help='cc email addresses')
1080 parser.add_option('--send-mail', action='store_true',
1081 help='send email to reviewer immediately')
1082 parser.add_option("--emulate_svn_auto_props", action="store_true",
1083 dest="emulate_svn_auto_props",
1084 help="Emulate Subversion's auto properties feature.")
1085 parser.add_option("--desc_from_logs", action="store_true",
1086 dest="from_logs",
1087 help="""Squashes git commit logs into change description and
1088 uses message as subject""")
1089 parser.add_option('-c', '--use-commit-queue', action='store_true',
1090 help='tell the commit queue to commit this patchset')
1091 if settings.GetIsGerrit():
1092 parser.add_option('--target_branch', dest='target_branch', default='master',
1093 help='target branch to upload')
1094 (options, args) = parser.parse_args(args)
1095
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001096 # Print warning if the user used the -m/--message argument. This will soon
1097 # change to -t/--title.
1098 if options.message:
1099 print >> sys.stderr, (
1100 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1101 'In the near future, -m or --message will send a message instead.\n'
1102 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001103
ukai@chromium.orge8077812012-02-03 03:41:46 +00001104 # Make sure index is up-to-date before running diff-index.
1105 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1106 if RunGit(['diff-index', 'HEAD']):
1107 print 'Cannot upload with a dirty tree. You must commit locally first.'
1108 return 1
1109
1110 cl = Changelist()
1111 if args:
1112 # TODO(ukai): is it ok for gerrit case?
1113 base_branch = args[0]
1114 else:
1115 # Default to diffing against the "upstream" branch.
1116 base_branch = cl.GetUpstreamBranch()
1117 args = [base_branch + "..."]
1118
1119 if not options.bypass_hooks:
1120 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1121 may_prompt=not options.force,
1122 verbose=options.verbose,
1123 author=None)
1124 if not hook_results.should_continue():
1125 return 1
1126 if not options.reviewers and hook_results.reviewers:
1127 options.reviewers = hook_results.reviewers
1128
1129 # --no-ext-diff is broken in some versions of Git, so try to work around
1130 # this by overriding the environment (but there is still a problem if the
1131 # git config key "diff.external" is used).
1132 env = os.environ.copy()
1133 if 'GIT_EXTERNAL_DIFF' in env:
1134 del env['GIT_EXTERNAL_DIFF']
1135 subprocess2.call(
cmp@chromium.org5cf70222012-06-12 18:20:13 +00001136 ['git', 'diff', '--no-ext-diff', '--stat', '--find-copies-harder'] + args,
1137 env=env)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001138
1139 if settings.GetIsGerrit():
1140 return GerritUpload(options, args, cl)
1141 return RietveldUpload(options, args, cl)
1142
1143
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001144def IsSubmoduleMergeCommit(ref):
1145 # When submodules are added to the repo, we expect there to be a single
1146 # non-git-svn merge commit at remote HEAD with a signature comment.
1147 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001148 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001149 return RunGit(cmd) != ''
1150
1151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152def SendUpstream(parser, args, cmd):
1153 """Common code for CmdPush and CmdDCommit
1154
1155 Squashed commit into a single.
1156 Updates changelog with metadata (e.g. pointer to review).
1157 Pushes/dcommits the code upstream.
1158 Updates review and closes.
1159 """
1160 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1161 help='bypass upload presubmit hook')
1162 parser.add_option('-m', dest='message',
1163 help="override review description")
1164 parser.add_option('-f', action='store_true', dest='force',
1165 help="force yes to questions (don't prompt)")
1166 parser.add_option('-c', dest='contributor',
1167 help="external contributor for patch (appended to " +
1168 "description and used as author for git). Should be " +
1169 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 (options, args) = parser.parse_args(args)
1171 cl = Changelist()
1172
1173 if not args or cmd == 'push':
1174 # Default to merging against our best guess of the upstream branch.
1175 args = [cl.GetUpstreamBranch()]
1176
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001177 if options.contributor:
1178 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1179 print "Please provide contibutor as 'First Last <email@example.com>'"
1180 return 1
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001183 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001185 # Make sure index is up-to-date before running diff-index.
1186 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 if RunGit(['diff-index', 'HEAD']):
1188 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1189 return 1
1190
1191 # This rev-list syntax means "show all commits not in my branch that
1192 # are in base_branch".
1193 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1194 base_branch]).splitlines()
1195 if upstream_commits:
1196 print ('Base branch "%s" has %d commits '
1197 'not in this branch.' % (base_branch, len(upstream_commits)))
1198 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1199 return 1
1200
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001201 # This is the revision `svn dcommit` will commit on top of.
1202 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1203 '--pretty=format:%H'])
1204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001206 # If the base_head is a submodule merge commit, the first parent of the
1207 # base_head should be a git-svn commit, which is what we're interested in.
1208 base_svn_head = base_branch
1209 if base_has_submodules:
1210 base_svn_head += '^1'
1211
1212 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 if extra_commits:
1214 print ('This branch has %d additional commits not upstreamed yet.'
1215 % len(extra_commits.splitlines()))
1216 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1217 'before attempting to %s.' % (base_branch, cmd))
1218 return 1
1219
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001220 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001221 author = None
1222 if options.contributor:
1223 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001224 hook_results = cl.RunHook(
1225 committing=True,
1226 upstream_branch=base_branch,
1227 may_prompt=not options.force,
1228 verbose=options.verbose,
1229 author=author)
1230 if not hook_results.should_continue():
1231 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
1233 if cmd == 'dcommit':
1234 # Check the tree status if the tree status URL is set.
1235 status = GetTreeStatus()
1236 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001237 print('The tree is closed. Please wait for it to reopen. Use '
1238 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 return 1
1240 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001241 print('Unable to determine tree status. Please verify manually and '
1242 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001243 else:
1244 breakpad.SendStack(
1245 'GitClHooksBypassedCommit',
1246 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001247 (cl.GetRietveldServer(), cl.GetIssue()),
1248 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249
1250 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001251 if not description and cl.GetIssue():
1252 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001254 if not description:
1255 print 'No description set.'
1256 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1257 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001259 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
1262 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 description += "\nPatch from %s." % options.contributor
1264 print 'Description:', repr(description)
1265
1266 branches = [base_branch, cl.GetBranchRef()]
1267 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001268 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001269 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001271 # We want to squash all this branch's commits into one commit with the proper
1272 # description. We do this by doing a "reset --soft" to the base branch (which
1273 # keeps the working copy the same), then dcommitting that. If origin/master
1274 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1275 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001277 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1278 # Delete the branches if they exist.
1279 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1280 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1281 result = RunGitWithCode(showref_cmd)
1282 if result[0] == 0:
1283 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284
1285 # We might be in a directory that's present in this branch but not in the
1286 # trunk. Move up to the top of the tree so that git commands that expect a
1287 # valid CWD won't fail after we check out the merge branch.
1288 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1289 if rel_base_path:
1290 os.chdir(rel_base_path)
1291
1292 # Stuff our change into the merge branch.
1293 # We wrap in a try...finally block so if anything goes wrong,
1294 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001295 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001297 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1298 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 if options.contributor:
1300 RunGit(['commit', '--author', options.contributor, '-m', description])
1301 else:
1302 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001303 if base_has_submodules:
1304 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1305 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1306 RunGit(['checkout', CHERRY_PICK_BRANCH])
1307 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 if cmd == 'push':
1309 # push the merge branch.
1310 remote, branch = cl.FetchUpstreamTuple()
1311 retcode, output = RunGitWithCode(
1312 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1313 logging.debug(output)
1314 else:
1315 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001316 retcode, output = RunGitWithCode(['svn', 'dcommit',
1317 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 finally:
1319 # And then swap back to the original branch and clean up.
1320 RunGit(['checkout', '-q', cl.GetBranch()])
1321 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001322 if base_has_submodules:
1323 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324
1325 if cl.GetIssue():
1326 if cmd == 'dcommit' and 'Committed r' in output:
1327 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1328 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001329 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1330 for l in output.splitlines(False))
1331 match = filter(None, match)
1332 if len(match) != 1:
1333 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1334 output)
1335 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 else:
1337 return 1
1338 viewvc_url = settings.GetViewVCUrl()
1339 if viewvc_url and revision:
1340 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1341 print ('Closing issue '
1342 '(you may be prompted for your codereview password)...')
1343 cl.CloseIssue()
1344 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001345
1346 if retcode == 0:
1347 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1348 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001349 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 return 0
1352
1353
1354@usage('[upstream branch to apply against]')
1355def CMDdcommit(parser, args):
1356 """commit the current changelist via git-svn"""
1357 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001358 message = """This doesn't appear to be an SVN repository.
1359If your project has a git mirror with an upstream SVN master, you probably need
1360to run 'git svn init', see your project's git mirror documentation.
1361If your project has a true writeable upstream repository, you probably want
1362to run 'git cl push' instead.
1363Choose wisely, if you get this wrong, your commit might appear to succeed but
1364will instead be silently ignored."""
1365 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001366 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 return SendUpstream(parser, args, 'dcommit')
1368
1369
1370@usage('[upstream branch to apply against]')
1371def CMDpush(parser, args):
1372 """commit the current changelist via git"""
1373 if settings.GetIsGitSvn():
1374 print('This appears to be an SVN repository.')
1375 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001376 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 return SendUpstream(parser, args, 'push')
1378
1379
1380@usage('<patch url or issue id>')
1381def CMDpatch(parser, args):
1382 """patch in a code review"""
1383 parser.add_option('-b', dest='newbranch',
1384 help='create a new branch off trunk for the patch')
1385 parser.add_option('-f', action='store_true', dest='force',
1386 help='with -b, clobber any existing branch')
1387 parser.add_option('--reject', action='store_true', dest='reject',
1388 help='allow failed patches and spew .rej files')
1389 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1390 help="don't commit after patch applies")
1391 (options, args) = parser.parse_args(args)
1392 if len(args) != 1:
1393 parser.print_help()
1394 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001395 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001397 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001398 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001399
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001400 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001402 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001403 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001405 # Assume it's a URL to the patch. Default to https.
1406 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001407 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001408 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 DieWithError('Must pass an issue ID or full URL for '
1410 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001411 issue = match.group(1)
1412 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
1414 if options.newbranch:
1415 if options.force:
1416 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001417 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 RunGit(['checkout', '-b', options.newbranch,
1419 Changelist().GetUpstreamBranch()])
1420
1421 # Switch up to the top-level directory, if necessary, in preparation for
1422 # applying the patch.
1423 top = RunGit(['rev-parse', '--show-cdup']).strip()
1424 if top:
1425 os.chdir(top)
1426
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 # Git patches have a/ at the beginning of source paths. We strip that out
1428 # with a sed script rather than the -p flag to patch so we can feed either
1429 # Git or svn-style patches into the same apply command.
1430 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001431 try:
1432 patch_data = subprocess2.check_output(
1433 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1434 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 DieWithError('Git patch mungling failed.')
1436 logging.info(patch_data)
1437 # We use "git apply" to apply the patch instead of "patch" so that we can
1438 # pick up file adds.
1439 # The --index flag means: also insert into the index (so we catch adds).
1440 cmd = ['git', 'apply', '--index', '-p0']
1441 if options.reject:
1442 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001443 try:
1444 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1445 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 DieWithError('Failed to apply the patch')
1447
1448 # If we had an issue, commit the current state and register the issue.
1449 if not options.nocommit:
1450 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1451 cl = Changelist()
1452 cl.SetIssue(issue)
1453 print "Committed patch."
1454 else:
1455 print "Patch applied to index."
1456 return 0
1457
1458
1459def CMDrebase(parser, args):
1460 """rebase current branch on top of svn repo"""
1461 # Provide a wrapper for git svn rebase to help avoid accidental
1462 # git svn dcommit.
1463 # It's the only command that doesn't use parser at all since we just defer
1464 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001465 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466
1467
1468def GetTreeStatus():
1469 """Fetches the tree status and returns either 'open', 'closed',
1470 'unknown' or 'unset'."""
1471 url = settings.GetTreeStatusUrl(error_ok=True)
1472 if url:
1473 status = urllib2.urlopen(url).read().lower()
1474 if status.find('closed') != -1 or status == '0':
1475 return 'closed'
1476 elif status.find('open') != -1 or status == '1':
1477 return 'open'
1478 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 return 'unset'
1480
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001481
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482def GetTreeStatusReason():
1483 """Fetches the tree status from a json url and returns the message
1484 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001485 url = settings.GetTreeStatusUrl()
1486 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 connection = urllib2.urlopen(json_url)
1488 status = json.loads(connection.read())
1489 connection.close()
1490 return status['message']
1491
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001492
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493def CMDtree(parser, args):
1494 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001495 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496 status = GetTreeStatus()
1497 if 'unset' == status:
1498 print 'You must configure your tree status URL by running "git cl config".'
1499 return 2
1500
1501 print "The tree is %s" % status
1502 print
1503 print GetTreeStatusReason()
1504 if status != 'open':
1505 return 1
1506 return 0
1507
1508
1509def CMDupstream(parser, args):
1510 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001511 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001512 if args:
1513 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001514 cl = Changelist()
1515 print cl.GetUpstreamBranch()
1516 return 0
1517
1518
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001519def CMDset_commit(parser, args):
1520 """set the commit bit"""
1521 _, args = parser.parse_args(args)
1522 if args:
1523 parser.error('Unrecognized args: %s' % ' '.join(args))
1524 cl = Changelist()
1525 cl.SetFlag('commit', '1')
1526 return 0
1527
1528
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529def Command(name):
1530 return getattr(sys.modules[__name__], 'CMD' + name, None)
1531
1532
1533def CMDhelp(parser, args):
1534 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001535 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 if len(args) == 1:
1537 return main(args + ['--help'])
1538 parser.print_help()
1539 return 0
1540
1541
1542def GenUsage(parser, command):
1543 """Modify an OptParse object with the function's documentation."""
1544 obj = Command(command)
1545 more = getattr(obj, 'usage_more', '')
1546 if command == 'help':
1547 command = '<command>'
1548 else:
1549 # OptParser.description prefer nicely non-formatted strings.
1550 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1551 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1552
1553
1554def main(argv):
1555 """Doesn't parse the arguments here, just find the right subcommand to
1556 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001557 if sys.hexversion < 0x02060000:
1558 print >> sys.stderr, (
1559 '\nYour python version %s is unsupported, please upgrade.\n' %
1560 sys.version.split(' ', 1)[0])
1561 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001562 # Reload settings.
1563 global settings
1564 settings = Settings()
1565
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566 # Do it late so all commands are listed.
1567 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1568 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1569 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1570
1571 # Create the option parse and add --verbose support.
1572 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001573 parser.add_option(
1574 '-v', '--verbose', action='count', default=0,
1575 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576 old_parser_args = parser.parse_args
1577 def Parse(args):
1578 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001579 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001581 elif options.verbose:
1582 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583 else:
1584 logging.basicConfig(level=logging.WARNING)
1585 return options, args
1586 parser.parse_args = Parse
1587
1588 if argv:
1589 command = Command(argv[0])
1590 if command:
1591 # "fix" the usage and the description now that we know the subcommand.
1592 GenUsage(parser, argv[0])
1593 try:
1594 return command(parser, argv[1:])
1595 except urllib2.HTTPError, e:
1596 if e.code != 500:
1597 raise
1598 DieWithError(
1599 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1600 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1601
1602 # Not a known command. Default to help.
1603 GenUsage(parser, 'help')
1604 return CMDhelp(parser, argv)
1605
1606
1607if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001608 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 sys.exit(main(sys.argv[1:]))