blob: fa7007bd2534ae4083b8a7e66296f4b133e7e333 [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
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000138def print_stats(args):
139 """Prints statistics about the change to the user."""
140 # --no-ext-diff is broken in some versions of Git, so try to work around
141 # this by overriding the environment (but there is still a problem if the
142 # git config key "diff.external" is used).
143 env = os.environ.copy()
144 if 'GIT_EXTERNAL_DIFF' in env:
145 del env['GIT_EXTERNAL_DIFF']
146 return subprocess2.call(
147 ['git', 'diff', '--no-ext-diff', '--stat', '--find-copies-harder'] + args,
148 env=env)
149
150
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000151class Settings(object):
152 def __init__(self):
153 self.default_server = None
154 self.cc = None
155 self.root = None
156 self.is_git_svn = None
157 self.svn_branch = None
158 self.tree_status_url = None
159 self.viewvc_url = None
160 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000161 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162
163 def LazyUpdateIfNeeded(self):
164 """Updates the settings from a codereview.settings file, if available."""
165 if not self.updated:
166 cr_settings_file = FindCodereviewSettingsFile()
167 if cr_settings_file:
168 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000169 self.updated = True
170 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000171 self.updated = True
172
173 def GetDefaultServerUrl(self, error_ok=False):
174 if not self.default_server:
175 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000176 self.default_server = gclient_utils.UpgradeToHttps(
177 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000178 if error_ok:
179 return self.default_server
180 if not self.default_server:
181 error_message = ('Could not find settings file. You must configure '
182 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000183 self.default_server = gclient_utils.UpgradeToHttps(
184 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000185 return self.default_server
186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000187 def GetRoot(self):
188 if not self.root:
189 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
190 return self.root
191
192 def GetIsGitSvn(self):
193 """Return true if this repo looks like it's using git-svn."""
194 if self.is_git_svn is None:
195 # If you have any "svn-remote.*" config keys, we think you're using svn.
196 self.is_git_svn = RunGitWithCode(
197 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
198 return self.is_git_svn
199
200 def GetSVNBranch(self):
201 if self.svn_branch is None:
202 if not self.GetIsGitSvn():
203 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
204
205 # Try to figure out which remote branch we're based on.
206 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000207 # 1) iterate through our branch history and find the svn URL.
208 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000209
210 # regexp matching the git-svn line that contains the URL.
211 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
212
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000213 # We don't want to go through all of history, so read a line from the
214 # pipe at a time.
215 # The -100 is an arbitrary limit so we don't search forever.
216 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000217 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000218 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000219 for line in proc.stdout:
220 match = git_svn_re.match(line)
221 if match:
222 url = match.group(1)
223 proc.stdout.close() # Cut pipe.
224 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000225
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000226 if url:
227 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
228 remotes = RunGit(['config', '--get-regexp',
229 r'^svn-remote\..*\.url']).splitlines()
230 for remote in remotes:
231 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000232 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000233 remote = match.group(1)
234 base_url = match.group(2)
235 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000236 ['config', 'svn-remote.%s.fetch' % remote],
237 error_ok=True).strip()
238 if fetch_spec:
239 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
240 if self.svn_branch:
241 break
242 branch_spec = RunGit(
243 ['config', 'svn-remote.%s.branches' % remote],
244 error_ok=True).strip()
245 if branch_spec:
246 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
247 if self.svn_branch:
248 break
249 tag_spec = RunGit(
250 ['config', 'svn-remote.%s.tags' % remote],
251 error_ok=True).strip()
252 if tag_spec:
253 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
254 if self.svn_branch:
255 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000256
257 if not self.svn_branch:
258 DieWithError('Can\'t guess svn branch -- try specifying it on the '
259 'command line')
260
261 return self.svn_branch
262
263 def GetTreeStatusUrl(self, error_ok=False):
264 if not self.tree_status_url:
265 error_message = ('You must configure your tree status URL by running '
266 '"git cl config".')
267 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
268 error_ok=error_ok,
269 error_message=error_message)
270 return self.tree_status_url
271
272 def GetViewVCUrl(self):
273 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000274 self.viewvc_url = gclient_utils.UpgradeToHttps(
275 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 return self.viewvc_url
277
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000278 def GetDefaultCCList(self):
279 return self._GetConfig('rietveld.cc', error_ok=True)
280
ukai@chromium.orge8077812012-02-03 03:41:46 +0000281 def GetIsGerrit(self):
282 """Return true if this repo is assosiated with gerrit code review system."""
283 if self.is_gerrit is None:
284 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
285 return self.is_gerrit
286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000287 def _GetConfig(self, param, **kwargs):
288 self.LazyUpdateIfNeeded()
289 return RunGit(['config', param], **kwargs).strip()
290
291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292def ShortBranchName(branch):
293 """Convert a name like 'refs/heads/foo' to just 'foo'."""
294 return branch.replace('refs/heads/', '')
295
296
297class Changelist(object):
298 def __init__(self, branchref=None):
299 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000300 global settings
301 if not settings:
302 # Happens when git_cl.py is used as a utility library.
303 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000304 settings.GetDefaultServerUrl()
305 self.branchref = branchref
306 if self.branchref:
307 self.branch = ShortBranchName(self.branchref)
308 else:
309 self.branch = None
310 self.rietveld_server = None
311 self.upstream_branch = None
312 self.has_issue = False
313 self.issue = None
314 self.has_description = False
315 self.description = None
316 self.has_patchset = False
317 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000318 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000319 self.cc = None
320 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000321 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000322
323 def GetCCList(self):
324 """Return the users cc'd on this CL.
325
326 Return is a string suitable for passing to gcl with the --cc flag.
327 """
328 if self.cc is None:
329 base_cc = settings .GetDefaultCCList()
330 more_cc = ','.join(self.watchers)
331 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
332 return self.cc
333
334 def SetWatchers(self, watchers):
335 """Set the list of email addresses that should be cc'd based on the changed
336 files in this CL.
337 """
338 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339
340 def GetBranch(self):
341 """Returns the short branch name, e.g. 'master'."""
342 if not self.branch:
343 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
344 self.branch = ShortBranchName(self.branchref)
345 return self.branch
346
347 def GetBranchRef(self):
348 """Returns the full branch name, e.g. 'refs/heads/master'."""
349 self.GetBranch() # Poke the lazy loader.
350 return self.branchref
351
352 def FetchUpstreamTuple(self):
353 """Returns a tuple containg remote and remote ref,
354 e.g. 'origin', 'refs/heads/master'
355 """
356 remote = '.'
357 branch = self.GetBranch()
358 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
359 error_ok=True).strip()
360 if upstream_branch:
361 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
362 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000363 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
364 error_ok=True).strip()
365 if upstream_branch:
366 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000367 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000368 # Fall back on trying a git-svn upstream branch.
369 if settings.GetIsGitSvn():
370 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000371 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000372 # Else, try to guess the origin remote.
373 remote_branches = RunGit(['branch', '-r']).split()
374 if 'origin/master' in remote_branches:
375 # Fall back on origin/master if it exits.
376 remote = 'origin'
377 upstream_branch = 'refs/heads/master'
378 elif 'origin/trunk' in remote_branches:
379 # Fall back on origin/trunk if it exists. Generally a shared
380 # git-svn clone
381 remote = 'origin'
382 upstream_branch = 'refs/heads/trunk'
383 else:
384 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000385Either pass complete "git diff"-style arguments, like
386 git cl upload origin/master
387or verify this branch is set up to track another (via the --track argument to
388"git checkout -b ...").""")
389
390 return remote, upstream_branch
391
392 def GetUpstreamBranch(self):
393 if self.upstream_branch is None:
394 remote, upstream_branch = self.FetchUpstreamTuple()
395 if remote is not '.':
396 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
397 self.upstream_branch = upstream_branch
398 return self.upstream_branch
399
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000400 def GetRemote(self):
401 if not self._remote:
402 self._remote = self.FetchUpstreamTuple()[0]
403 if self._remote == '.':
404
405 remotes = RunGit(['remote'], error_ok=True).split()
406 if len(remotes) == 1:
407 self._remote, = remotes
408 elif 'origin' in remotes:
409 self._remote = 'origin'
410 logging.warning('Could not determine which remote this change is '
411 'associated with, so defaulting to "%s". This may '
412 'not be what you want. You may prevent this message '
413 'by running "git svn info" as documented here: %s',
414 self._remote,
415 GIT_INSTRUCTIONS_URL)
416 else:
417 logging.warn('Could not determine which remote this change is '
418 'associated with. You may prevent this message by '
419 'running "git svn info" as documented here: %s',
420 GIT_INSTRUCTIONS_URL)
421 return self._remote
422
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000423 def GetGitBaseUrlFromConfig(self):
424 """Return the configured base URL from branch.<branchname>.baseurl.
425
426 Returns None if it is not set.
427 """
428 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
429 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 def GetRemoteUrl(self):
432 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
433
434 Returns None if there is no remote.
435 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000436 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
438
439 def GetIssue(self):
440 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
442 if issue:
443 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 else:
445 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 self.has_issue = True
447 return self.issue
448
449 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000450 if not self.rietveld_server:
451 # If we're on a branch then get the server potentially associated
452 # with that branch.
453 if self.GetIssue():
454 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
455 ['config', self._RietveldServer()], error_ok=True).strip())
456 if not self.rietveld_server:
457 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 return self.rietveld_server
459
460 def GetIssueURL(self):
461 """Get the URL for a particular issue."""
462 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
463
464 def GetDescription(self, pretty=False):
465 if not self.has_description:
466 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000467 issue = int(self.GetIssue())
468 try:
469 self.description = self.RpcServer().get_description(issue).strip()
470 except urllib2.HTTPError, e:
471 if e.code == 404:
472 DieWithError(
473 ('\nWhile fetching the description for issue %d, received a '
474 '404 (not found)\n'
475 'error. It is likely that you deleted this '
476 'issue on the server. If this is the\n'
477 'case, please run\n\n'
478 ' git cl issue 0\n\n'
479 'to clear the association with the deleted issue. Then run '
480 'this command again.') % issue)
481 else:
482 DieWithError(
483 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 self.has_description = True
485 if pretty:
486 wrapper = textwrap.TextWrapper()
487 wrapper.initial_indent = wrapper.subsequent_indent = ' '
488 return wrapper.fill(self.description)
489 return self.description
490
491 def GetPatchset(self):
492 if not self.has_patchset:
493 patchset = RunGit(['config', self._PatchsetSetting()],
494 error_ok=True).strip()
495 if patchset:
496 self.patchset = patchset
497 else:
498 self.patchset = None
499 self.has_patchset = True
500 return self.patchset
501
502 def SetPatchset(self, patchset):
503 """Set this branch's patchset. If patchset=0, clears the patchset."""
504 if patchset:
505 RunGit(['config', self._PatchsetSetting(), str(patchset)])
506 else:
507 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000508 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000509 self.has_patchset = False
510
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000511 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000512 patchset = self.RpcServer().get_issue_properties(
513 int(issue), False)['patchsets'][-1]
514 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000515 '/download/issue%s_%s.diff' % (issue, patchset))
516
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000517 def SetIssue(self, issue):
518 """Set this branch's issue. If issue=0, clears the issue."""
519 if issue:
520 RunGit(['config', self._IssueSetting(), str(issue)])
521 if self.rietveld_server:
522 RunGit(['config', self._RietveldServer(), self.rietveld_server])
523 else:
524 RunGit(['config', '--unset', self._IssueSetting()])
525 self.SetPatchset(0)
526 self.has_issue = False
527
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000528 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000529 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
530 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000531
532 # We use the sha1 of HEAD as a name of this change.
533 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000534 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000535 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000536 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000537 except subprocess2.CalledProcessError:
538 DieWithError(
539 ('\nFailed to diff against upstream branch %s!\n\n'
540 'This branch probably doesn\'t exist anymore. To reset the\n'
541 'tracking branch, please run\n'
542 ' git branch --set-upstream %s trunk\n'
543 'replacing trunk with origin/master or the relevant branch') %
544 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000545
546 issue = ConvertToInteger(self.GetIssue())
547 patchset = ConvertToInteger(self.GetPatchset())
548 if issue:
549 description = self.GetDescription()
550 else:
551 # If the change was never uploaded, use the log messages of all commits
552 # up to the branch point, as git cl upload will prefill the description
553 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000554 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
555 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000556
557 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000558 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000559 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000560 name,
561 description,
562 absroot,
563 files,
564 issue,
565 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000566 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000567
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000568 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
569 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
570 change = self.GetChange(upstream_branch, author)
571
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000572 # Apply watchlists on upload.
573 if not committing:
574 watchlist = watchlists.Watchlists(change.RepositoryRoot())
575 files = [f.LocalPath() for f in change.AffectedFiles()]
576 self.SetWatchers(watchlist.GetWatchersForPaths(files))
577
578 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000579 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000580 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000581 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000582 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000583 except presubmit_support.PresubmitFailure, e:
584 DieWithError(
585 ('%s\nMaybe your depot_tools is out of date?\n'
586 'If all fails, contact maruel@') % e)
587
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000589 """Updates the description and closes the issue."""
590 issue = int(self.GetIssue())
591 self.RpcServer().update_description(issue, self.description)
592 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000593
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000594 def SetFlag(self, flag, value):
595 """Patchset must match."""
596 if not self.GetPatchset():
597 DieWithError('The patchset needs to match. Send another patchset.')
598 try:
599 return self.RpcServer().set_flag(
600 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
601 except urllib2.HTTPError, e:
602 if e.code == 404:
603 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
604 if e.code == 403:
605 DieWithError(
606 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
607 'match?') % (self.GetIssue(), self.GetPatchset()))
608 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000609
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000610 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 """Returns an upload.RpcServer() to access this review's rietveld instance.
612 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000613 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000614 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
615 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000616 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617
618 def _IssueSetting(self):
619 """Return the git setting that stores this change's issue."""
620 return 'branch.%s.rietveldissue' % self.GetBranch()
621
622 def _PatchsetSetting(self):
623 """Return the git setting that stores this change's most recent patchset."""
624 return 'branch.%s.rietveldpatchset' % self.GetBranch()
625
626 def _RietveldServer(self):
627 """Returns the git setting that stores this change's rietveld server."""
628 return 'branch.%s.rietveldserver' % self.GetBranch()
629
630
631def GetCodereviewSettingsInteractively():
632 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000633 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 server = settings.GetDefaultServerUrl(error_ok=True)
635 prompt = 'Rietveld server (host[:port])'
636 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000637 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 if not server and not newserver:
639 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000640 if newserver:
641 newserver = gclient_utils.UpgradeToHttps(newserver)
642 if newserver != server:
643 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000645 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 prompt = caption
647 if initial:
648 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000649 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 if new_val == 'x':
651 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000652 elif new_val:
653 if is_url:
654 new_val = gclient_utils.UpgradeToHttps(new_val)
655 if new_val != initial:
656 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000658 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000660 'tree-status-url', False)
661 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662
663 # TODO: configure a default branch to diff against, rather than this
664 # svn-based hackery.
665
666
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000667class ChangeDescription(object):
668 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000669 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000670 self.log_desc = log_desc
671 self.reviewers = reviewers
672 self.description = self.log_desc
673
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000674 def Prompt(self):
675 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000676# This will displayed on the codereview site.
677# The first line will also be used as the subject of the review.
678"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000679 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000680 if ('\nR=' not in self.description and
681 '\nTBR=' not in self.description and
682 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000683 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000684 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000685 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000686 if '\nTEST=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000687 content += '\nTEST='
688 content = content.rstrip('\n') + '\n'
689 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000690 if not content:
691 DieWithError('Running editor failed')
692 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000693 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000694 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000695 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000696
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000697 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000698 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000699 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000700 # Retrieves all reviewer lines
701 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000702 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000703 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000704 if reviewers:
705 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000706
707 def IsEmpty(self):
708 return not self.description
709
710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711def FindCodereviewSettingsFile(filename='codereview.settings'):
712 """Finds the given file starting in the cwd and going up.
713
714 Only looks up to the top of the repository unless an
715 'inherit-review-settings-ok' file exists in the root of the repository.
716 """
717 inherit_ok_file = 'inherit-review-settings-ok'
718 cwd = os.getcwd()
719 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
720 if os.path.isfile(os.path.join(root, inherit_ok_file)):
721 root = '/'
722 while True:
723 if filename in os.listdir(cwd):
724 if os.path.isfile(os.path.join(cwd, filename)):
725 return open(os.path.join(cwd, filename))
726 if cwd == root:
727 break
728 cwd = os.path.dirname(cwd)
729
730
731def LoadCodereviewSettingsFromFile(fileobj):
732 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000733 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 def SetProperty(name, setting, unset_error_ok=False):
736 fullname = 'rietveld.' + name
737 if setting in keyvals:
738 RunGit(['config', fullname, keyvals[setting]])
739 else:
740 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
741
742 SetProperty('server', 'CODE_REVIEW_SERVER')
743 # Only server setting is required. Other settings can be absent.
744 # In that case, we ignore errors raised during option deletion attempt.
745 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
746 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
747 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
748
ukai@chromium.orge8077812012-02-03 03:41:46 +0000749 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
750 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
751 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000752
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
754 #should be of the form
755 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
756 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
757 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
758 keyvals['ORIGIN_URL_CONFIG']])
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000761def DownloadHooks(force):
762 """downloads hooks
763
764 Args:
765 force: True to update hooks. False to install hooks if not present.
766 """
767 if not settings.GetIsGerrit():
768 return
769 server_url = settings.GetDefaultServerUrl()
770 src = '%s/tools/hooks/commit-msg' % server_url
771 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
772 if not os.access(dst, os.X_OK):
773 if os.path.exists(dst):
774 if not force:
775 return
776 os.remove(dst)
777 try:
778 urllib.urlretrieve(src, dst)
779 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
780 except Exception:
781 if os.path.exists(dst):
782 os.remove(dst)
783 DieWithError('\nFailed to download hooks from %s' % src)
784
785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786@usage('[repo root containing codereview.settings]')
787def CMDconfig(parser, args):
788 """edit configuration for this tree"""
789
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000790 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 if len(args) == 0:
792 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000793 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 return 0
795
796 url = args[0]
797 if not url.endswith('codereview.settings'):
798 url = os.path.join(url, 'codereview.settings')
799
800 # Load code review settings and download hooks (if available).
801 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000802 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 return 0
804
805
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000806def CMDbaseurl(parser, args):
807 """get or set base-url for this branch"""
808 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
809 branch = ShortBranchName(branchref)
810 _, args = parser.parse_args(args)
811 if not args:
812 print("Current base-url:")
813 return RunGit(['config', 'branch.%s.base-url' % branch],
814 error_ok=False).strip()
815 else:
816 print("Setting base-url to %s" % args[0])
817 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
818 error_ok=False).strip()
819
820
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821def CMDstatus(parser, args):
822 """show status of changelists"""
823 parser.add_option('--field',
824 help='print only specific field (desc|id|patch|url)')
825 (options, args) = parser.parse_args(args)
826
827 # TODO: maybe make show_branches a flag if necessary.
828 show_branches = not options.field
829
830 if show_branches:
831 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
832 if branches:
833 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000834 changes = (Changelist(branchref=b) for b in branches.splitlines())
835 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
836 alignment = max(5, max(len(b) for b in branches))
837 for branch in sorted(branches):
838 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839
840 cl = Changelist()
841 if options.field:
842 if options.field.startswith('desc'):
843 print cl.GetDescription()
844 elif options.field == 'id':
845 issueid = cl.GetIssue()
846 if issueid:
847 print issueid
848 elif options.field == 'patch':
849 patchset = cl.GetPatchset()
850 if patchset:
851 print patchset
852 elif options.field == 'url':
853 url = cl.GetIssueURL()
854 if url:
855 print url
856 else:
857 print
858 print 'Current branch:',
859 if not cl.GetIssue():
860 print 'no issue assigned.'
861 return 0
862 print cl.GetBranch()
863 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
864 print 'Issue description:'
865 print cl.GetDescription(pretty=True)
866 return 0
867
868
869@usage('[issue_number]')
870def CMDissue(parser, args):
871 """Set or display the current code review issue number.
872
873 Pass issue number 0 to clear the current issue.
874"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000875 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876
877 cl = Changelist()
878 if len(args) > 0:
879 try:
880 issue = int(args[0])
881 except ValueError:
882 DieWithError('Pass a number to set the issue or none to list it.\n'
883 'Maybe you want to run git cl status?')
884 cl.SetIssue(issue)
885 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
886 return 0
887
888
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000889def CMDcomments(parser, args):
890 """show review comments of the current changelist"""
891 (_, args) = parser.parse_args(args)
892 if args:
893 parser.error('Unsupported argument: %s' % args)
894
895 cl = Changelist()
896 if cl.GetIssue():
897 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
898 for message in sorted(data['messages'], key=lambda x: x['date']):
899 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
900 if message['text'].strip():
901 print '\n'.join(' ' + l for l in message['text'].splitlines())
902 return 0
903
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905def CreateDescriptionFromLog(args):
906 """Pulls out the commit log to use as a base for the CL description."""
907 log_args = []
908 if len(args) == 1 and not args[0].endswith('.'):
909 log_args = [args[0] + '..']
910 elif len(args) == 1 and args[0].endswith('...'):
911 log_args = [args[0][:-1]]
912 elif len(args) == 2:
913 log_args = [args[0] + '..' + args[1]]
914 else:
915 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000916 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917
918
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000919def ConvertToInteger(inputval):
920 """Convert a string to integer, but returns either an int or None."""
921 try:
922 return int(inputval)
923 except (TypeError, ValueError):
924 return None
925
926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927def CMDpresubmit(parser, args):
928 """run presubmit tests on the current changelist"""
929 parser.add_option('--upload', action='store_true',
930 help='Run upload hook instead of the push/dcommit hook')
931 (options, args) = parser.parse_args(args)
932
933 # Make sure index is up-to-date before running diff-index.
934 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
935 if RunGit(['diff-index', 'HEAD']):
936 # TODO(maruel): Is this really necessary?
937 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
938 return 1
939
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000940 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941 if args:
942 base_branch = args[0]
943 else:
944 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000945 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000947 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000948 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000949 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000950 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951
952
ukai@chromium.orge8077812012-02-03 03:41:46 +0000953def GerritUpload(options, args, cl):
954 """upload the current branch to gerrit."""
955 # We assume the remote called "origin" is the one we want.
956 # It is probably not worthwhile to support different workflows.
957 remote = 'origin'
958 branch = 'master'
959 if options.target_branch:
960 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000962 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000963 if options.reviewers:
964 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000965 change_desc = ChangeDescription(log_desc, options.reviewers)
966 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000967 if change_desc.IsEmpty():
968 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 return 1
970
ukai@chromium.orge8077812012-02-03 03:41:46 +0000971 receive_options = []
972 cc = cl.GetCCList().split(',')
973 if options.cc:
974 cc += options.cc.split(',')
975 cc = filter(None, cc)
976 if cc:
977 receive_options += ['--cc=' + email for email in cc]
978 if change_desc.reviewers:
979 reviewers = filter(None, change_desc.reviewers.split(','))
980 if reviewers:
981 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982
ukai@chromium.orge8077812012-02-03 03:41:46 +0000983 git_command = ['push']
984 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000985 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000986 ' '.join(receive_options))
987 git_command += [remote, 'HEAD:refs/for/' + branch]
988 RunGit(git_command)
989 # TODO(ukai): parse Change-Id: and set issue number?
990 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992
ukai@chromium.orge8077812012-02-03 03:41:46 +0000993def RietveldUpload(options, args, cl):
994 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 upload_args = ['--assume_yes'] # Don't ask about untracked files.
996 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997 if options.emulate_svn_auto_props:
998 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999
1000 change_desc = None
1001
1002 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001003 if options.title:
1004 upload_args.extend(['--title', options.title])
1005 elif options.message:
1006 # TODO(rogerta): for now, the -m option will also set the --title option
1007 # for upload.py. Soon this will be changed to set the --message option.
1008 # Will wait until people are used to typing -t instead of -m.
1009 upload_args.extend(['--title', options.message])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 upload_args.extend(['--issue', cl.GetIssue()])
1011 print ("This branch is associated with issue %s. "
1012 "Adding patch to that issue." % cl.GetIssue())
1013 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001014 if options.title:
1015 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001016 message = options.message or CreateDescriptionFromLog(args)
1017 change_desc = ChangeDescription(message, options.reviewers)
1018 if not options.force:
1019 change_desc.Prompt()
1020 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001021
1022 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023 print "Description is empty; aborting."
1024 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001025
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001026 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001027 if change_desc.reviewers:
1028 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001029 if options.send_mail:
1030 if not change_desc.reviewers:
1031 DieWithError("Must specify reviewers to send email.")
1032 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001033 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001034 if cc:
1035 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036
1037 # Include the upstream repo's URL in the change -- this is useful for
1038 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001039 remote_url = cl.GetGitBaseUrlFromConfig()
1040 if not remote_url:
1041 if settings.GetIsGitSvn():
1042 # URL is dependent on the current directory.
1043 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1044 if data:
1045 keys = dict(line.split(': ', 1) for line in data.splitlines()
1046 if ': ' in line)
1047 remote_url = keys.get('URL', None)
1048 else:
1049 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1050 remote_url = (cl.GetRemoteUrl() + '@'
1051 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052 if remote_url:
1053 upload_args.extend(['--base_url', remote_url])
1054
1055 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001056 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001057 except KeyboardInterrupt:
1058 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 except:
1060 # If we got an exception after the user typed a description for their
1061 # change, back up the description before re-raising.
1062 if change_desc:
1063 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1064 print '\nGot exception while uploading -- saving description to %s\n' \
1065 % backup_path
1066 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001067 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 backup_file.close()
1069 raise
1070
1071 if not cl.GetIssue():
1072 cl.SetIssue(issue)
1073 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001074
1075 if options.use_commit_queue:
1076 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 return 0
1078
1079
ukai@chromium.orge8077812012-02-03 03:41:46 +00001080@usage('[args to "git diff"]')
1081def CMDupload(parser, args):
1082 """upload the current changelist to codereview"""
1083 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1084 help='bypass upload presubmit hook')
1085 parser.add_option('-f', action='store_true', dest='force',
1086 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001087 parser.add_option('-m', dest='message', help='message for patchset')
1088 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001089 parser.add_option('-r', '--reviewers',
1090 help='reviewer email addresses')
1091 parser.add_option('--cc',
1092 help='cc email addresses')
1093 parser.add_option('--send-mail', action='store_true',
1094 help='send email to reviewer immediately')
1095 parser.add_option("--emulate_svn_auto_props", action="store_true",
1096 dest="emulate_svn_auto_props",
1097 help="Emulate Subversion's auto properties feature.")
1098 parser.add_option("--desc_from_logs", action="store_true",
1099 dest="from_logs",
1100 help="""Squashes git commit logs into change description and
1101 uses message as subject""")
1102 parser.add_option('-c', '--use-commit-queue', action='store_true',
1103 help='tell the commit queue to commit this patchset')
1104 if settings.GetIsGerrit():
1105 parser.add_option('--target_branch', dest='target_branch', default='master',
1106 help='target branch to upload')
1107 (options, args) = parser.parse_args(args)
1108
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001109 # Print warning if the user used the -m/--message argument. This will soon
1110 # change to -t/--title.
1111 if options.message:
1112 print >> sys.stderr, (
1113 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1114 'In the near future, -m or --message will send a message instead.\n'
1115 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001116
ukai@chromium.orge8077812012-02-03 03:41:46 +00001117 # Make sure index is up-to-date before running diff-index.
1118 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1119 if RunGit(['diff-index', 'HEAD']):
1120 print 'Cannot upload with a dirty tree. You must commit locally first.'
1121 return 1
1122
1123 cl = Changelist()
1124 if args:
1125 # TODO(ukai): is it ok for gerrit case?
1126 base_branch = args[0]
1127 else:
1128 # Default to diffing against the "upstream" branch.
1129 base_branch = cl.GetUpstreamBranch()
1130 args = [base_branch + "..."]
1131
1132 if not options.bypass_hooks:
1133 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1134 may_prompt=not options.force,
1135 verbose=options.verbose,
1136 author=None)
1137 if not hook_results.should_continue():
1138 return 1
1139 if not options.reviewers and hook_results.reviewers:
1140 options.reviewers = hook_results.reviewers
1141
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001142 print_stats(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001143 if settings.GetIsGerrit():
1144 return GerritUpload(options, args, cl)
1145 return RietveldUpload(options, args, cl)
1146
1147
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001148def IsSubmoduleMergeCommit(ref):
1149 # When submodules are added to the repo, we expect there to be a single
1150 # non-git-svn merge commit at remote HEAD with a signature comment.
1151 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001152 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001153 return RunGit(cmd) != ''
1154
1155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156def SendUpstream(parser, args, cmd):
1157 """Common code for CmdPush and CmdDCommit
1158
1159 Squashed commit into a single.
1160 Updates changelog with metadata (e.g. pointer to review).
1161 Pushes/dcommits the code upstream.
1162 Updates review and closes.
1163 """
1164 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1165 help='bypass upload presubmit hook')
1166 parser.add_option('-m', dest='message',
1167 help="override review description")
1168 parser.add_option('-f', action='store_true', dest='force',
1169 help="force yes to questions (don't prompt)")
1170 parser.add_option('-c', dest='contributor',
1171 help="external contributor for patch (appended to " +
1172 "description and used as author for git). Should be " +
1173 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174 (options, args) = parser.parse_args(args)
1175 cl = Changelist()
1176
1177 if not args or cmd == 'push':
1178 # Default to merging against our best guess of the upstream branch.
1179 args = [cl.GetUpstreamBranch()]
1180
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001181 if options.contributor:
1182 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1183 print "Please provide contibutor as 'First Last <email@example.com>'"
1184 return 1
1185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001187 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001189 # Make sure index is up-to-date before running diff-index.
1190 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191 if RunGit(['diff-index', 'HEAD']):
1192 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1193 return 1
1194
1195 # This rev-list syntax means "show all commits not in my branch that
1196 # are in base_branch".
1197 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1198 base_branch]).splitlines()
1199 if upstream_commits:
1200 print ('Base branch "%s" has %d commits '
1201 'not in this branch.' % (base_branch, len(upstream_commits)))
1202 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1203 return 1
1204
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001205 # This is the revision `svn dcommit` will commit on top of.
1206 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1207 '--pretty=format:%H'])
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001210 # If the base_head is a submodule merge commit, the first parent of the
1211 # base_head should be a git-svn commit, which is what we're interested in.
1212 base_svn_head = base_branch
1213 if base_has_submodules:
1214 base_svn_head += '^1'
1215
1216 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 if extra_commits:
1218 print ('This branch has %d additional commits not upstreamed yet.'
1219 % len(extra_commits.splitlines()))
1220 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1221 'before attempting to %s.' % (base_branch, cmd))
1222 return 1
1223
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001224 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001225 author = None
1226 if options.contributor:
1227 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001228 hook_results = cl.RunHook(
1229 committing=True,
1230 upstream_branch=base_branch,
1231 may_prompt=not options.force,
1232 verbose=options.verbose,
1233 author=author)
1234 if not hook_results.should_continue():
1235 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236
1237 if cmd == 'dcommit':
1238 # Check the tree status if the tree status URL is set.
1239 status = GetTreeStatus()
1240 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001241 print('The tree is closed. Please wait for it to reopen. Use '
1242 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 return 1
1244 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001245 print('Unable to determine tree status. Please verify manually and '
1246 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001247 else:
1248 breakpad.SendStack(
1249 'GitClHooksBypassedCommit',
1250 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001251 (cl.GetRietveldServer(), cl.GetIssue()),
1252 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253
1254 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001255 if not description and cl.GetIssue():
1256 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001258 if not description:
1259 print 'No description set.'
1260 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1261 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001263 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265
1266 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 description += "\nPatch from %s." % options.contributor
1268 print 'Description:', repr(description)
1269
1270 branches = [base_branch, cl.GetBranchRef()]
1271 if not options.force:
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001272 print_stats(branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001273 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001275 # We want to squash all this branch's commits into one commit with the proper
1276 # description. We do this by doing a "reset --soft" to the base branch (which
1277 # keeps the working copy the same), then dcommitting that. If origin/master
1278 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1279 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001281 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1282 # Delete the branches if they exist.
1283 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1284 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1285 result = RunGitWithCode(showref_cmd)
1286 if result[0] == 0:
1287 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
1289 # We might be in a directory that's present in this branch but not in the
1290 # trunk. Move up to the top of the tree so that git commands that expect a
1291 # valid CWD won't fail after we check out the merge branch.
1292 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1293 if rel_base_path:
1294 os.chdir(rel_base_path)
1295
1296 # Stuff our change into the merge branch.
1297 # We wrap in a try...finally block so if anything goes wrong,
1298 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001299 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001301 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1302 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 if options.contributor:
1304 RunGit(['commit', '--author', options.contributor, '-m', description])
1305 else:
1306 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001307 if base_has_submodules:
1308 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1309 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1310 RunGit(['checkout', CHERRY_PICK_BRANCH])
1311 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 if cmd == 'push':
1313 # push the merge branch.
1314 remote, branch = cl.FetchUpstreamTuple()
1315 retcode, output = RunGitWithCode(
1316 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1317 logging.debug(output)
1318 else:
1319 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001320 retcode, output = RunGitWithCode(['svn', 'dcommit',
1321 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 finally:
1323 # And then swap back to the original branch and clean up.
1324 RunGit(['checkout', '-q', cl.GetBranch()])
1325 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001326 if base_has_submodules:
1327 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328
1329 if cl.GetIssue():
1330 if cmd == 'dcommit' and 'Committed r' in output:
1331 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1332 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001333 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1334 for l in output.splitlines(False))
1335 match = filter(None, match)
1336 if len(match) != 1:
1337 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1338 output)
1339 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 else:
1341 return 1
1342 viewvc_url = settings.GetViewVCUrl()
1343 if viewvc_url and revision:
1344 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1345 print ('Closing issue '
1346 '(you may be prompted for your codereview password)...')
1347 cl.CloseIssue()
1348 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001349
1350 if retcode == 0:
1351 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1352 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001353 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 return 0
1356
1357
1358@usage('[upstream branch to apply against]')
1359def CMDdcommit(parser, args):
1360 """commit the current changelist via git-svn"""
1361 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001362 message = """This doesn't appear to be an SVN repository.
1363If your project has a git mirror with an upstream SVN master, you probably need
1364to run 'git svn init', see your project's git mirror documentation.
1365If your project has a true writeable upstream repository, you probably want
1366to run 'git cl push' instead.
1367Choose wisely, if you get this wrong, your commit might appear to succeed but
1368will instead be silently ignored."""
1369 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001370 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 return SendUpstream(parser, args, 'dcommit')
1372
1373
1374@usage('[upstream branch to apply against]')
1375def CMDpush(parser, args):
1376 """commit the current changelist via git"""
1377 if settings.GetIsGitSvn():
1378 print('This appears to be an SVN repository.')
1379 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001380 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 return SendUpstream(parser, args, 'push')
1382
1383
1384@usage('<patch url or issue id>')
1385def CMDpatch(parser, args):
1386 """patch in a code review"""
1387 parser.add_option('-b', dest='newbranch',
1388 help='create a new branch off trunk for the patch')
1389 parser.add_option('-f', action='store_true', dest='force',
1390 help='with -b, clobber any existing branch')
1391 parser.add_option('--reject', action='store_true', dest='reject',
1392 help='allow failed patches and spew .rej files')
1393 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1394 help="don't commit after patch applies")
1395 (options, args) = parser.parse_args(args)
1396 if len(args) != 1:
1397 parser.print_help()
1398 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001399 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001401 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001402 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001403
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001404 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001406 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001407 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001409 # Assume it's a URL to the patch. Default to https.
1410 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001411 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001412 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 DieWithError('Must pass an issue ID or full URL for '
1414 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001415 issue = match.group(1)
1416 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417
1418 if options.newbranch:
1419 if options.force:
1420 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001421 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 RunGit(['checkout', '-b', options.newbranch,
1423 Changelist().GetUpstreamBranch()])
1424
1425 # Switch up to the top-level directory, if necessary, in preparation for
1426 # applying the patch.
1427 top = RunGit(['rev-parse', '--show-cdup']).strip()
1428 if top:
1429 os.chdir(top)
1430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 # Git patches have a/ at the beginning of source paths. We strip that out
1432 # with a sed script rather than the -p flag to patch so we can feed either
1433 # Git or svn-style patches into the same apply command.
1434 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001435 try:
1436 patch_data = subprocess2.check_output(
1437 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1438 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 DieWithError('Git patch mungling failed.')
1440 logging.info(patch_data)
1441 # We use "git apply" to apply the patch instead of "patch" so that we can
1442 # pick up file adds.
1443 # The --index flag means: also insert into the index (so we catch adds).
1444 cmd = ['git', 'apply', '--index', '-p0']
1445 if options.reject:
1446 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001447 try:
1448 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1449 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 DieWithError('Failed to apply the patch')
1451
1452 # If we had an issue, commit the current state and register the issue.
1453 if not options.nocommit:
1454 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1455 cl = Changelist()
1456 cl.SetIssue(issue)
1457 print "Committed patch."
1458 else:
1459 print "Patch applied to index."
1460 return 0
1461
1462
1463def CMDrebase(parser, args):
1464 """rebase current branch on top of svn repo"""
1465 # Provide a wrapper for git svn rebase to help avoid accidental
1466 # git svn dcommit.
1467 # It's the only command that doesn't use parser at all since we just defer
1468 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001469 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470
1471
1472def GetTreeStatus():
1473 """Fetches the tree status and returns either 'open', 'closed',
1474 'unknown' or 'unset'."""
1475 url = settings.GetTreeStatusUrl(error_ok=True)
1476 if url:
1477 status = urllib2.urlopen(url).read().lower()
1478 if status.find('closed') != -1 or status == '0':
1479 return 'closed'
1480 elif status.find('open') != -1 or status == '1':
1481 return 'open'
1482 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483 return 'unset'
1484
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001485
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486def GetTreeStatusReason():
1487 """Fetches the tree status from a json url and returns the message
1488 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001489 url = settings.GetTreeStatusUrl()
1490 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491 connection = urllib2.urlopen(json_url)
1492 status = json.loads(connection.read())
1493 connection.close()
1494 return status['message']
1495
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001496
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497def CMDtree(parser, args):
1498 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001499 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 status = GetTreeStatus()
1501 if 'unset' == status:
1502 print 'You must configure your tree status URL by running "git cl config".'
1503 return 2
1504
1505 print "The tree is %s" % status
1506 print
1507 print GetTreeStatusReason()
1508 if status != 'open':
1509 return 1
1510 return 0
1511
1512
1513def CMDupstream(parser, args):
1514 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001515 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001516 if args:
1517 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 cl = Changelist()
1519 print cl.GetUpstreamBranch()
1520 return 0
1521
1522
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001523def CMDset_commit(parser, args):
1524 """set the commit bit"""
1525 _, args = parser.parse_args(args)
1526 if args:
1527 parser.error('Unrecognized args: %s' % ' '.join(args))
1528 cl = Changelist()
1529 cl.SetFlag('commit', '1')
1530 return 0
1531
1532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533def Command(name):
1534 return getattr(sys.modules[__name__], 'CMD' + name, None)
1535
1536
1537def CMDhelp(parser, args):
1538 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001539 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540 if len(args) == 1:
1541 return main(args + ['--help'])
1542 parser.print_help()
1543 return 0
1544
1545
1546def GenUsage(parser, command):
1547 """Modify an OptParse object with the function's documentation."""
1548 obj = Command(command)
1549 more = getattr(obj, 'usage_more', '')
1550 if command == 'help':
1551 command = '<command>'
1552 else:
1553 # OptParser.description prefer nicely non-formatted strings.
1554 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1555 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1556
1557
1558def main(argv):
1559 """Doesn't parse the arguments here, just find the right subcommand to
1560 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001561 if sys.hexversion < 0x02060000:
1562 print >> sys.stderr, (
1563 '\nYour python version %s is unsupported, please upgrade.\n' %
1564 sys.version.split(' ', 1)[0])
1565 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001566 # Reload settings.
1567 global settings
1568 settings = Settings()
1569
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001570 # Do it late so all commands are listed.
1571 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1572 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1573 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1574
1575 # Create the option parse and add --verbose support.
1576 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001577 parser.add_option(
1578 '-v', '--verbose', action='count', default=0,
1579 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 old_parser_args = parser.parse_args
1581 def Parse(args):
1582 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001583 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001585 elif options.verbose:
1586 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587 else:
1588 logging.basicConfig(level=logging.WARNING)
1589 return options, args
1590 parser.parse_args = Parse
1591
1592 if argv:
1593 command = Command(argv[0])
1594 if command:
1595 # "fix" the usage and the description now that we know the subcommand.
1596 GenUsage(parser, argv[0])
1597 try:
1598 return command(parser, argv[1:])
1599 except urllib2.HTTPError, e:
1600 if e.code != 500:
1601 raise
1602 DieWithError(
1603 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1604 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1605
1606 # Not a known command. Default to help.
1607 GenUsage(parser, 'help')
1608 return CMDhelp(parser, argv)
1609
1610
1611if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001612 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613 sys.exit(main(sys.argv[1:]))