blob: 143ad2854d00072877a7d42dd30908983f1d0145 [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.org71e12a92012-02-14 02:34:15 +0000686 content = content.rstrip('\n') + '\n'
687 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000688 if not content:
689 DieWithError('Running editor failed')
690 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000691 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000692 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000693 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000694
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000695 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000696 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000697 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000698 # Retrieves all reviewer lines
699 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000700 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000701 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000702 if reviewers:
703 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000704
705 def IsEmpty(self):
706 return not self.description
707
708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709def FindCodereviewSettingsFile(filename='codereview.settings'):
710 """Finds the given file starting in the cwd and going up.
711
712 Only looks up to the top of the repository unless an
713 'inherit-review-settings-ok' file exists in the root of the repository.
714 """
715 inherit_ok_file = 'inherit-review-settings-ok'
716 cwd = os.getcwd()
717 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
718 if os.path.isfile(os.path.join(root, inherit_ok_file)):
719 root = '/'
720 while True:
721 if filename in os.listdir(cwd):
722 if os.path.isfile(os.path.join(cwd, filename)):
723 return open(os.path.join(cwd, filename))
724 if cwd == root:
725 break
726 cwd = os.path.dirname(cwd)
727
728
729def LoadCodereviewSettingsFromFile(fileobj):
730 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000731 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 def SetProperty(name, setting, unset_error_ok=False):
734 fullname = 'rietveld.' + name
735 if setting in keyvals:
736 RunGit(['config', fullname, keyvals[setting]])
737 else:
738 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
739
740 SetProperty('server', 'CODE_REVIEW_SERVER')
741 # Only server setting is required. Other settings can be absent.
742 # In that case, we ignore errors raised during option deletion attempt.
743 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
744 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
745 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
746
ukai@chromium.orge8077812012-02-03 03:41:46 +0000747 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
748 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
749 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000750
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
752 #should be of the form
753 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
754 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
755 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
756 keyvals['ORIGIN_URL_CONFIG']])
757
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000759def DownloadHooks(force):
760 """downloads hooks
761
762 Args:
763 force: True to update hooks. False to install hooks if not present.
764 """
765 if not settings.GetIsGerrit():
766 return
767 server_url = settings.GetDefaultServerUrl()
768 src = '%s/tools/hooks/commit-msg' % server_url
769 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
770 if not os.access(dst, os.X_OK):
771 if os.path.exists(dst):
772 if not force:
773 return
774 os.remove(dst)
775 try:
776 urllib.urlretrieve(src, dst)
777 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
778 except Exception:
779 if os.path.exists(dst):
780 os.remove(dst)
781 DieWithError('\nFailed to download hooks from %s' % src)
782
783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784@usage('[repo root containing codereview.settings]')
785def CMDconfig(parser, args):
786 """edit configuration for this tree"""
787
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000788 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 if len(args) == 0:
790 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000791 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 return 0
793
794 url = args[0]
795 if not url.endswith('codereview.settings'):
796 url = os.path.join(url, 'codereview.settings')
797
798 # Load code review settings and download hooks (if available).
799 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000800 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 return 0
802
803
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000804def CMDbaseurl(parser, args):
805 """get or set base-url for this branch"""
806 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
807 branch = ShortBranchName(branchref)
808 _, args = parser.parse_args(args)
809 if not args:
810 print("Current base-url:")
811 return RunGit(['config', 'branch.%s.base-url' % branch],
812 error_ok=False).strip()
813 else:
814 print("Setting base-url to %s" % args[0])
815 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
816 error_ok=False).strip()
817
818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819def CMDstatus(parser, args):
820 """show status of changelists"""
821 parser.add_option('--field',
822 help='print only specific field (desc|id|patch|url)')
823 (options, args) = parser.parse_args(args)
824
825 # TODO: maybe make show_branches a flag if necessary.
826 show_branches = not options.field
827
828 if show_branches:
829 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
830 if branches:
831 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000832 changes = (Changelist(branchref=b) for b in branches.splitlines())
833 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
834 alignment = max(5, max(len(b) for b in branches))
835 for branch in sorted(branches):
836 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837
838 cl = Changelist()
839 if options.field:
840 if options.field.startswith('desc'):
841 print cl.GetDescription()
842 elif options.field == 'id':
843 issueid = cl.GetIssue()
844 if issueid:
845 print issueid
846 elif options.field == 'patch':
847 patchset = cl.GetPatchset()
848 if patchset:
849 print patchset
850 elif options.field == 'url':
851 url = cl.GetIssueURL()
852 if url:
853 print url
854 else:
855 print
856 print 'Current branch:',
857 if not cl.GetIssue():
858 print 'no issue assigned.'
859 return 0
860 print cl.GetBranch()
861 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
862 print 'Issue description:'
863 print cl.GetDescription(pretty=True)
864 return 0
865
866
867@usage('[issue_number]')
868def CMDissue(parser, args):
869 """Set or display the current code review issue number.
870
871 Pass issue number 0 to clear the current issue.
872"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000873 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874
875 cl = Changelist()
876 if len(args) > 0:
877 try:
878 issue = int(args[0])
879 except ValueError:
880 DieWithError('Pass a number to set the issue or none to list it.\n'
881 'Maybe you want to run git cl status?')
882 cl.SetIssue(issue)
883 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
884 return 0
885
886
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000887def CMDcomments(parser, args):
888 """show review comments of the current changelist"""
889 (_, args) = parser.parse_args(args)
890 if args:
891 parser.error('Unsupported argument: %s' % args)
892
893 cl = Changelist()
894 if cl.GetIssue():
895 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
896 for message in sorted(data['messages'], key=lambda x: x['date']):
897 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
898 if message['text'].strip():
899 print '\n'.join(' ' + l for l in message['text'].splitlines())
900 return 0
901
902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903def CreateDescriptionFromLog(args):
904 """Pulls out the commit log to use as a base for the CL description."""
905 log_args = []
906 if len(args) == 1 and not args[0].endswith('.'):
907 log_args = [args[0] + '..']
908 elif len(args) == 1 and args[0].endswith('...'):
909 log_args = [args[0][:-1]]
910 elif len(args) == 2:
911 log_args = [args[0] + '..' + args[1]]
912 else:
913 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000914 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915
916
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000917def ConvertToInteger(inputval):
918 """Convert a string to integer, but returns either an int or None."""
919 try:
920 return int(inputval)
921 except (TypeError, ValueError):
922 return None
923
924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925def CMDpresubmit(parser, args):
926 """run presubmit tests on the current changelist"""
927 parser.add_option('--upload', action='store_true',
928 help='Run upload hook instead of the push/dcommit hook')
929 (options, args) = parser.parse_args(args)
930
931 # Make sure index is up-to-date before running diff-index.
932 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
933 if RunGit(['diff-index', 'HEAD']):
934 # TODO(maruel): Is this really necessary?
935 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
936 return 1
937
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000938 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 if args:
940 base_branch = args[0]
941 else:
942 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000943 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000945 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000946 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000947 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000948 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949
950
ukai@chromium.orge8077812012-02-03 03:41:46 +0000951def GerritUpload(options, args, cl):
952 """upload the current branch to gerrit."""
953 # We assume the remote called "origin" is the one we want.
954 # It is probably not worthwhile to support different workflows.
955 remote = 'origin'
956 branch = 'master'
957 if options.target_branch:
958 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000960 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000961 if options.reviewers:
962 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000963 change_desc = ChangeDescription(log_desc, options.reviewers)
964 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000965 if change_desc.IsEmpty():
966 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 return 1
968
ukai@chromium.orge8077812012-02-03 03:41:46 +0000969 receive_options = []
970 cc = cl.GetCCList().split(',')
971 if options.cc:
972 cc += options.cc.split(',')
973 cc = filter(None, cc)
974 if cc:
975 receive_options += ['--cc=' + email for email in cc]
976 if change_desc.reviewers:
977 reviewers = filter(None, change_desc.reviewers.split(','))
978 if reviewers:
979 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980
ukai@chromium.orge8077812012-02-03 03:41:46 +0000981 git_command = ['push']
982 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000983 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000984 ' '.join(receive_options))
985 git_command += [remote, 'HEAD:refs/for/' + branch]
986 RunGit(git_command)
987 # TODO(ukai): parse Change-Id: and set issue number?
988 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000989
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000990
ukai@chromium.orge8077812012-02-03 03:41:46 +0000991def RietveldUpload(options, args, cl):
992 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993 upload_args = ['--assume_yes'] # Don't ask about untracked files.
994 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 if options.emulate_svn_auto_props:
996 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997
998 change_desc = None
999
1000 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001001 if options.title:
1002 upload_args.extend(['--title', options.title])
1003 elif options.message:
1004 # TODO(rogerta): for now, the -m option will also set the --title option
1005 # for upload.py. Soon this will be changed to set the --message option.
1006 # Will wait until people are used to typing -t instead of -m.
1007 upload_args.extend(['--title', options.message])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 upload_args.extend(['--issue', cl.GetIssue()])
1009 print ("This branch is associated with issue %s. "
1010 "Adding patch to that issue." % cl.GetIssue())
1011 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001012 if options.title:
1013 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001014 message = options.message or CreateDescriptionFromLog(args)
1015 change_desc = ChangeDescription(message, options.reviewers)
1016 if not options.force:
1017 change_desc.Prompt()
1018 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001019
1020 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 print "Description is empty; aborting."
1022 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001023
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001024 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001025 if change_desc.reviewers:
1026 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001027 if options.send_mail:
1028 if not change_desc.reviewers:
1029 DieWithError("Must specify reviewers to send email.")
1030 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001031 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001032 if cc:
1033 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034
1035 # Include the upstream repo's URL in the change -- this is useful for
1036 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001037 remote_url = cl.GetGitBaseUrlFromConfig()
1038 if not remote_url:
1039 if settings.GetIsGitSvn():
1040 # URL is dependent on the current directory.
1041 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1042 if data:
1043 keys = dict(line.split(': ', 1) for line in data.splitlines()
1044 if ': ' in line)
1045 remote_url = keys.get('URL', None)
1046 else:
1047 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1048 remote_url = (cl.GetRemoteUrl() + '@'
1049 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 if remote_url:
1051 upload_args.extend(['--base_url', remote_url])
1052
1053 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001054 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001055 except KeyboardInterrupt:
1056 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 except:
1058 # If we got an exception after the user typed a description for their
1059 # change, back up the description before re-raising.
1060 if change_desc:
1061 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1062 print '\nGot exception while uploading -- saving description to %s\n' \
1063 % backup_path
1064 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001065 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 backup_file.close()
1067 raise
1068
1069 if not cl.GetIssue():
1070 cl.SetIssue(issue)
1071 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001072
1073 if options.use_commit_queue:
1074 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 return 0
1076
1077
ukai@chromium.orge8077812012-02-03 03:41:46 +00001078@usage('[args to "git diff"]')
1079def CMDupload(parser, args):
1080 """upload the current changelist to codereview"""
1081 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1082 help='bypass upload presubmit hook')
1083 parser.add_option('-f', action='store_true', dest='force',
1084 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001085 parser.add_option('-m', dest='message', help='message for patchset')
1086 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001087 parser.add_option('-r', '--reviewers',
1088 help='reviewer email addresses')
1089 parser.add_option('--cc',
1090 help='cc email addresses')
1091 parser.add_option('--send-mail', action='store_true',
1092 help='send email to reviewer immediately')
1093 parser.add_option("--emulate_svn_auto_props", action="store_true",
1094 dest="emulate_svn_auto_props",
1095 help="Emulate Subversion's auto properties feature.")
1096 parser.add_option("--desc_from_logs", action="store_true",
1097 dest="from_logs",
1098 help="""Squashes git commit logs into change description and
1099 uses message as subject""")
1100 parser.add_option('-c', '--use-commit-queue', action='store_true',
1101 help='tell the commit queue to commit this patchset')
1102 if settings.GetIsGerrit():
1103 parser.add_option('--target_branch', dest='target_branch', default='master',
1104 help='target branch to upload')
1105 (options, args) = parser.parse_args(args)
1106
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001107 # Print warning if the user used the -m/--message argument. This will soon
1108 # change to -t/--title.
1109 if options.message:
1110 print >> sys.stderr, (
1111 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1112 'In the near future, -m or --message will send a message instead.\n'
1113 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001114
ukai@chromium.orge8077812012-02-03 03:41:46 +00001115 # Make sure index is up-to-date before running diff-index.
1116 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1117 if RunGit(['diff-index', 'HEAD']):
1118 print 'Cannot upload with a dirty tree. You must commit locally first.'
1119 return 1
1120
1121 cl = Changelist()
1122 if args:
1123 # TODO(ukai): is it ok for gerrit case?
1124 base_branch = args[0]
1125 else:
1126 # Default to diffing against the "upstream" branch.
1127 base_branch = cl.GetUpstreamBranch()
1128 args = [base_branch + "..."]
1129
1130 if not options.bypass_hooks:
1131 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1132 may_prompt=not options.force,
1133 verbose=options.verbose,
1134 author=None)
1135 if not hook_results.should_continue():
1136 return 1
1137 if not options.reviewers and hook_results.reviewers:
1138 options.reviewers = hook_results.reviewers
1139
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001140 print_stats(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001141 if settings.GetIsGerrit():
1142 return GerritUpload(options, args, cl)
1143 return RietveldUpload(options, args, cl)
1144
1145
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001146def IsSubmoduleMergeCommit(ref):
1147 # When submodules are added to the repo, we expect there to be a single
1148 # non-git-svn merge commit at remote HEAD with a signature comment.
1149 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001150 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001151 return RunGit(cmd) != ''
1152
1153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154def SendUpstream(parser, args, cmd):
1155 """Common code for CmdPush and CmdDCommit
1156
1157 Squashed commit into a single.
1158 Updates changelog with metadata (e.g. pointer to review).
1159 Pushes/dcommits the code upstream.
1160 Updates review and closes.
1161 """
1162 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1163 help='bypass upload presubmit hook')
1164 parser.add_option('-m', dest='message',
1165 help="override review description")
1166 parser.add_option('-f', action='store_true', dest='force',
1167 help="force yes to questions (don't prompt)")
1168 parser.add_option('-c', dest='contributor',
1169 help="external contributor for patch (appended to " +
1170 "description and used as author for git). Should be " +
1171 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 (options, args) = parser.parse_args(args)
1173 cl = Changelist()
1174
1175 if not args or cmd == 'push':
1176 # Default to merging against our best guess of the upstream branch.
1177 args = [cl.GetUpstreamBranch()]
1178
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001179 if options.contributor:
1180 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1181 print "Please provide contibutor as 'First Last <email@example.com>'"
1182 return 1
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001185 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001187 # Make sure index is up-to-date before running diff-index.
1188 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 if RunGit(['diff-index', 'HEAD']):
1190 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1191 return 1
1192
1193 # This rev-list syntax means "show all commits not in my branch that
1194 # are in base_branch".
1195 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1196 base_branch]).splitlines()
1197 if upstream_commits:
1198 print ('Base branch "%s" has %d commits '
1199 'not in this branch.' % (base_branch, len(upstream_commits)))
1200 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1201 return 1
1202
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001203 # This is the revision `svn dcommit` will commit on top of.
1204 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1205 '--pretty=format:%H'])
1206
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001208 # If the base_head is a submodule merge commit, the first parent of the
1209 # base_head should be a git-svn commit, which is what we're interested in.
1210 base_svn_head = base_branch
1211 if base_has_submodules:
1212 base_svn_head += '^1'
1213
1214 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if extra_commits:
1216 print ('This branch has %d additional commits not upstreamed yet.'
1217 % len(extra_commits.splitlines()))
1218 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1219 'before attempting to %s.' % (base_branch, cmd))
1220 return 1
1221
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001222 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001223 author = None
1224 if options.contributor:
1225 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001226 hook_results = cl.RunHook(
1227 committing=True,
1228 upstream_branch=base_branch,
1229 may_prompt=not options.force,
1230 verbose=options.verbose,
1231 author=author)
1232 if not hook_results.should_continue():
1233 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
1235 if cmd == 'dcommit':
1236 # Check the tree status if the tree status URL is set.
1237 status = GetTreeStatus()
1238 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001239 print('The tree is closed. Please wait for it to reopen. Use '
1240 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 return 1
1242 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001243 print('Unable to determine tree status. Please verify manually and '
1244 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001245 else:
1246 breakpad.SendStack(
1247 'GitClHooksBypassedCommit',
1248 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001249 (cl.GetRietveldServer(), cl.GetIssue()),
1250 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251
1252 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001253 if not description and cl.GetIssue():
1254 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001256 if not description:
1257 print 'No description set.'
1258 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1259 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001261 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
1264 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 description += "\nPatch from %s." % options.contributor
1266 print 'Description:', repr(description)
1267
1268 branches = [base_branch, cl.GetBranchRef()]
1269 if not options.force:
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001270 print_stats(branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001271 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001273 # We want to squash all this branch's commits into one commit with the proper
1274 # description. We do this by doing a "reset --soft" to the base branch (which
1275 # keeps the working copy the same), then dcommitting that. If origin/master
1276 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1277 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001279 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1280 # Delete the branches if they exist.
1281 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1282 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1283 result = RunGitWithCode(showref_cmd)
1284 if result[0] == 0:
1285 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286
1287 # We might be in a directory that's present in this branch but not in the
1288 # trunk. Move up to the top of the tree so that git commands that expect a
1289 # valid CWD won't fail after we check out the merge branch.
1290 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1291 if rel_base_path:
1292 os.chdir(rel_base_path)
1293
1294 # Stuff our change into the merge branch.
1295 # We wrap in a try...finally block so if anything goes wrong,
1296 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001297 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001299 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1300 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if options.contributor:
1302 RunGit(['commit', '--author', options.contributor, '-m', description])
1303 else:
1304 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001305 if base_has_submodules:
1306 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1307 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1308 RunGit(['checkout', CHERRY_PICK_BRANCH])
1309 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 if cmd == 'push':
1311 # push the merge branch.
1312 remote, branch = cl.FetchUpstreamTuple()
1313 retcode, output = RunGitWithCode(
1314 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1315 logging.debug(output)
1316 else:
1317 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001318 retcode, output = RunGitWithCode(['svn', 'dcommit',
1319 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 finally:
1321 # And then swap back to the original branch and clean up.
1322 RunGit(['checkout', '-q', cl.GetBranch()])
1323 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001324 if base_has_submodules:
1325 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326
1327 if cl.GetIssue():
1328 if cmd == 'dcommit' and 'Committed r' in output:
1329 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1330 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001331 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1332 for l in output.splitlines(False))
1333 match = filter(None, match)
1334 if len(match) != 1:
1335 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1336 output)
1337 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 else:
1339 return 1
1340 viewvc_url = settings.GetViewVCUrl()
1341 if viewvc_url and revision:
1342 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1343 print ('Closing issue '
1344 '(you may be prompted for your codereview password)...')
1345 cl.CloseIssue()
1346 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001347
1348 if retcode == 0:
1349 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1350 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001351 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 return 0
1354
1355
1356@usage('[upstream branch to apply against]')
1357def CMDdcommit(parser, args):
1358 """commit the current changelist via git-svn"""
1359 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001360 message = """This doesn't appear to be an SVN repository.
1361If your project has a git mirror with an upstream SVN master, you probably need
1362to run 'git svn init', see your project's git mirror documentation.
1363If your project has a true writeable upstream repository, you probably want
1364to run 'git cl push' instead.
1365Choose wisely, if you get this wrong, your commit might appear to succeed but
1366will instead be silently ignored."""
1367 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001368 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 return SendUpstream(parser, args, 'dcommit')
1370
1371
1372@usage('[upstream branch to apply against]')
1373def CMDpush(parser, args):
1374 """commit the current changelist via git"""
1375 if settings.GetIsGitSvn():
1376 print('This appears to be an SVN repository.')
1377 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001378 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return SendUpstream(parser, args, 'push')
1380
1381
1382@usage('<patch url or issue id>')
1383def CMDpatch(parser, args):
1384 """patch in a code review"""
1385 parser.add_option('-b', dest='newbranch',
1386 help='create a new branch off trunk for the patch')
1387 parser.add_option('-f', action='store_true', dest='force',
1388 help='with -b, clobber any existing branch')
1389 parser.add_option('--reject', action='store_true', dest='reject',
1390 help='allow failed patches and spew .rej files')
1391 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1392 help="don't commit after patch applies")
1393 (options, args) = parser.parse_args(args)
1394 if len(args) != 1:
1395 parser.print_help()
1396 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001397 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001399 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001400 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001401
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001402 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001404 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001405 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001407 # Assume it's a URL to the patch. Default to https.
1408 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001409 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001410 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 DieWithError('Must pass an issue ID or full URL for '
1412 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001413 issue = match.group(1)
1414 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
1416 if options.newbranch:
1417 if options.force:
1418 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001419 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 RunGit(['checkout', '-b', options.newbranch,
1421 Changelist().GetUpstreamBranch()])
1422
1423 # Switch up to the top-level directory, if necessary, in preparation for
1424 # applying the patch.
1425 top = RunGit(['rev-parse', '--show-cdup']).strip()
1426 if top:
1427 os.chdir(top)
1428
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 # Git patches have a/ at the beginning of source paths. We strip that out
1430 # with a sed script rather than the -p flag to patch so we can feed either
1431 # Git or svn-style patches into the same apply command.
1432 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001433 try:
1434 patch_data = subprocess2.check_output(
1435 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1436 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 DieWithError('Git patch mungling failed.')
1438 logging.info(patch_data)
1439 # We use "git apply" to apply the patch instead of "patch" so that we can
1440 # pick up file adds.
1441 # The --index flag means: also insert into the index (so we catch adds).
1442 cmd = ['git', 'apply', '--index', '-p0']
1443 if options.reject:
1444 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001445 try:
1446 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1447 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448 DieWithError('Failed to apply the patch')
1449
1450 # If we had an issue, commit the current state and register the issue.
1451 if not options.nocommit:
1452 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1453 cl = Changelist()
1454 cl.SetIssue(issue)
1455 print "Committed patch."
1456 else:
1457 print "Patch applied to index."
1458 return 0
1459
1460
1461def CMDrebase(parser, args):
1462 """rebase current branch on top of svn repo"""
1463 # Provide a wrapper for git svn rebase to help avoid accidental
1464 # git svn dcommit.
1465 # It's the only command that doesn't use parser at all since we just defer
1466 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001467 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
1469
1470def GetTreeStatus():
1471 """Fetches the tree status and returns either 'open', 'closed',
1472 'unknown' or 'unset'."""
1473 url = settings.GetTreeStatusUrl(error_ok=True)
1474 if url:
1475 status = urllib2.urlopen(url).read().lower()
1476 if status.find('closed') != -1 or status == '0':
1477 return 'closed'
1478 elif status.find('open') != -1 or status == '1':
1479 return 'open'
1480 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 return 'unset'
1482
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001483
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001484def GetTreeStatusReason():
1485 """Fetches the tree status from a json url and returns the message
1486 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001487 url = settings.GetTreeStatusUrl()
1488 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489 connection = urllib2.urlopen(json_url)
1490 status = json.loads(connection.read())
1491 connection.close()
1492 return status['message']
1493
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495def CMDtree(parser, args):
1496 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001497 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 status = GetTreeStatus()
1499 if 'unset' == status:
1500 print 'You must configure your tree status URL by running "git cl config".'
1501 return 2
1502
1503 print "The tree is %s" % status
1504 print
1505 print GetTreeStatusReason()
1506 if status != 'open':
1507 return 1
1508 return 0
1509
1510
1511def CMDupstream(parser, args):
1512 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001513 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001514 if args:
1515 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 cl = Changelist()
1517 print cl.GetUpstreamBranch()
1518 return 0
1519
1520
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001521def CMDset_commit(parser, args):
1522 """set the commit bit"""
1523 _, args = parser.parse_args(args)
1524 if args:
1525 parser.error('Unrecognized args: %s' % ' '.join(args))
1526 cl = Changelist()
1527 cl.SetFlag('commit', '1')
1528 return 0
1529
1530
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531def Command(name):
1532 return getattr(sys.modules[__name__], 'CMD' + name, None)
1533
1534
1535def CMDhelp(parser, args):
1536 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001537 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538 if len(args) == 1:
1539 return main(args + ['--help'])
1540 parser.print_help()
1541 return 0
1542
1543
1544def GenUsage(parser, command):
1545 """Modify an OptParse object with the function's documentation."""
1546 obj = Command(command)
1547 more = getattr(obj, 'usage_more', '')
1548 if command == 'help':
1549 command = '<command>'
1550 else:
1551 # OptParser.description prefer nicely non-formatted strings.
1552 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1553 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1554
1555
1556def main(argv):
1557 """Doesn't parse the arguments here, just find the right subcommand to
1558 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001559 if sys.hexversion < 0x02060000:
1560 print >> sys.stderr, (
1561 '\nYour python version %s is unsupported, please upgrade.\n' %
1562 sys.version.split(' ', 1)[0])
1563 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001564 # Reload settings.
1565 global settings
1566 settings = Settings()
1567
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001568 # Do it late so all commands are listed.
1569 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1570 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1571 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1572
1573 # Create the option parse and add --verbose support.
1574 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001575 parser.add_option(
1576 '-v', '--verbose', action='count', default=0,
1577 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578 old_parser_args = parser.parse_args
1579 def Parse(args):
1580 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001581 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001583 elif options.verbose:
1584 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585 else:
1586 logging.basicConfig(level=logging.WARNING)
1587 return options, args
1588 parser.parse_args = Parse
1589
1590 if argv:
1591 command = Command(argv[0])
1592 if command:
1593 # "fix" the usage and the description now that we know the subcommand.
1594 GenUsage(parser, argv[0])
1595 try:
1596 return command(parser, argv[1:])
1597 except urllib2.HTTPError, e:
1598 if e.code != 500:
1599 raise
1600 DieWithError(
1601 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1602 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1603
1604 # Not a known command. Default to help.
1605 GenUsage(parser, 'help')
1606 return CMDhelp(parser, argv)
1607
1608
1609if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001610 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 sys.exit(main(sys.argv[1:]))