blob: 9716ef1258a786ffd163ac69c013b8d1d94765a8 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042
maruel@chromium.org90541732011-04-01 17:54:18 +000043
maruel@chromium.orgddd59412011-11-30 14:20:38 +000044# Initialized in main()
45settings = None
46
47
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000049 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050 sys.exit(1)
51
52
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000055 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000056 except subprocess2.CalledProcessError, e:
57 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059 'Command "%s" failed.\n%s' % (
60 ' '.join(args), error_message or e.stdout or ''))
61 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062
63
64def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 """Returns stdout."""
66 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
68
69def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000071 try:
72 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
73 return code, out[0]
74 except ValueError:
75 # When the subprocess fails, it returns None. That triggers a ValueError
76 # when trying to unpack the return value into (out, code).
77 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
79
80def usage(more):
81 def hook(fn):
82 fn.usage_more = more
83 return fn
84 return hook
85
86
maruel@chromium.org90541732011-04-01 17:54:18 +000087def ask_for_data(prompt):
88 try:
89 return raw_input(prompt)
90 except KeyboardInterrupt:
91 # Hide the exception.
92 sys.exit(1)
93
94
bauerb@chromium.org866276c2011-03-18 20:09:31 +000095def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
96 """Return the corresponding git ref if |base_url| together with |glob_spec|
97 matches the full |url|.
98
99 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
100 """
101 fetch_suburl, as_ref = glob_spec.split(':')
102 if allow_wildcards:
103 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
104 if glob_match:
105 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
106 # "branches/{472,597,648}/src:refs/remotes/svn/*".
107 branch_re = re.escape(base_url)
108 if glob_match.group(1):
109 branch_re += '/' + re.escape(glob_match.group(1))
110 wildcard = glob_match.group(2)
111 if wildcard == '*':
112 branch_re += '([^/]*)'
113 else:
114 # Escape and replace surrounding braces with parentheses and commas
115 # with pipe symbols.
116 wildcard = re.escape(wildcard)
117 wildcard = re.sub('^\\\\{', '(', wildcard)
118 wildcard = re.sub('\\\\,', '|', wildcard)
119 wildcard = re.sub('\\\\}$', ')', wildcard)
120 branch_re += wildcard
121 if glob_match.group(3):
122 branch_re += re.escape(glob_match.group(3))
123 match = re.match(branch_re, url)
124 if match:
125 return re.sub('\*$', match.group(1), as_ref)
126
127 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
128 if fetch_suburl:
129 full_url = base_url + '/' + fetch_suburl
130 else:
131 full_url = base_url
132 if full_url == url:
133 return as_ref
134 return None
135
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000137def print_stats(args):
138 """Prints statistics about the change to the user."""
139 # --no-ext-diff is broken in some versions of Git, so try to work around
140 # this by overriding the environment (but there is still a problem if the
141 # git config key "diff.external" is used).
142 env = os.environ.copy()
143 if 'GIT_EXTERNAL_DIFF' in env:
144 del env['GIT_EXTERNAL_DIFF']
145 return subprocess2.call(
146 ['git', 'diff', '--no-ext-diff', '--stat', '--find-copies-harder'] + args,
147 env=env)
148
149
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150class Settings(object):
151 def __init__(self):
152 self.default_server = None
153 self.cc = None
154 self.root = None
155 self.is_git_svn = None
156 self.svn_branch = None
157 self.tree_status_url = None
158 self.viewvc_url = None
159 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000160 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000161
162 def LazyUpdateIfNeeded(self):
163 """Updates the settings from a codereview.settings file, if available."""
164 if not self.updated:
165 cr_settings_file = FindCodereviewSettingsFile()
166 if cr_settings_file:
167 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000168 self.updated = True
169 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000170 self.updated = True
171
172 def GetDefaultServerUrl(self, error_ok=False):
173 if not self.default_server:
174 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000175 self.default_server = gclient_utils.UpgradeToHttps(
176 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000177 if error_ok:
178 return self.default_server
179 if not self.default_server:
180 error_message = ('Could not find settings file. You must configure '
181 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000182 self.default_server = gclient_utils.UpgradeToHttps(
183 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000184 return self.default_server
185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000186 def GetRoot(self):
187 if not self.root:
188 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
189 return self.root
190
191 def GetIsGitSvn(self):
192 """Return true if this repo looks like it's using git-svn."""
193 if self.is_git_svn is None:
194 # If you have any "svn-remote.*" config keys, we think you're using svn.
195 self.is_git_svn = RunGitWithCode(
196 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
197 return self.is_git_svn
198
199 def GetSVNBranch(self):
200 if self.svn_branch is None:
201 if not self.GetIsGitSvn():
202 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
203
204 # Try to figure out which remote branch we're based on.
205 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000206 # 1) iterate through our branch history and find the svn URL.
207 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000208
209 # regexp matching the git-svn line that contains the URL.
210 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
211
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000212 # We don't want to go through all of history, so read a line from the
213 # pipe at a time.
214 # The -100 is an arbitrary limit so we don't search forever.
215 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000216 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000217 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000218 for line in proc.stdout:
219 match = git_svn_re.match(line)
220 if match:
221 url = match.group(1)
222 proc.stdout.close() # Cut pipe.
223 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000224
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000225 if url:
226 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
227 remotes = RunGit(['config', '--get-regexp',
228 r'^svn-remote\..*\.url']).splitlines()
229 for remote in remotes:
230 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000231 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000232 remote = match.group(1)
233 base_url = match.group(2)
234 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000235 ['config', 'svn-remote.%s.fetch' % remote],
236 error_ok=True).strip()
237 if fetch_spec:
238 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
239 if self.svn_branch:
240 break
241 branch_spec = RunGit(
242 ['config', 'svn-remote.%s.branches' % remote],
243 error_ok=True).strip()
244 if branch_spec:
245 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
246 if self.svn_branch:
247 break
248 tag_spec = RunGit(
249 ['config', 'svn-remote.%s.tags' % remote],
250 error_ok=True).strip()
251 if tag_spec:
252 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
253 if self.svn_branch:
254 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000255
256 if not self.svn_branch:
257 DieWithError('Can\'t guess svn branch -- try specifying it on the '
258 'command line')
259
260 return self.svn_branch
261
262 def GetTreeStatusUrl(self, error_ok=False):
263 if not self.tree_status_url:
264 error_message = ('You must configure your tree status URL by running '
265 '"git cl config".')
266 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
267 error_ok=error_ok,
268 error_message=error_message)
269 return self.tree_status_url
270
271 def GetViewVCUrl(self):
272 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000273 self.viewvc_url = gclient_utils.UpgradeToHttps(
274 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 return self.viewvc_url
276
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000277 def GetDefaultCCList(self):
278 return self._GetConfig('rietveld.cc', error_ok=True)
279
ukai@chromium.orge8077812012-02-03 03:41:46 +0000280 def GetIsGerrit(self):
281 """Return true if this repo is assosiated with gerrit code review system."""
282 if self.is_gerrit is None:
283 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
284 return self.is_gerrit
285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286 def _GetConfig(self, param, **kwargs):
287 self.LazyUpdateIfNeeded()
288 return RunGit(['config', param], **kwargs).strip()
289
290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291def ShortBranchName(branch):
292 """Convert a name like 'refs/heads/foo' to just 'foo'."""
293 return branch.replace('refs/heads/', '')
294
295
296class Changelist(object):
297 def __init__(self, branchref=None):
298 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000299 global settings
300 if not settings:
301 # Happens when git_cl.py is used as a utility library.
302 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303 settings.GetDefaultServerUrl()
304 self.branchref = branchref
305 if self.branchref:
306 self.branch = ShortBranchName(self.branchref)
307 else:
308 self.branch = None
309 self.rietveld_server = None
310 self.upstream_branch = None
311 self.has_issue = False
312 self.issue = None
313 self.has_description = False
314 self.description = None
315 self.has_patchset = False
316 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000317 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000318 self.cc = None
319 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000320 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000321
322 def GetCCList(self):
323 """Return the users cc'd on this CL.
324
325 Return is a string suitable for passing to gcl with the --cc flag.
326 """
327 if self.cc is None:
328 base_cc = settings .GetDefaultCCList()
329 more_cc = ','.join(self.watchers)
330 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
331 return self.cc
332
333 def SetWatchers(self, watchers):
334 """Set the list of email addresses that should be cc'd based on the changed
335 files in this CL.
336 """
337 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000338
339 def GetBranch(self):
340 """Returns the short branch name, e.g. 'master'."""
341 if not self.branch:
342 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
343 self.branch = ShortBranchName(self.branchref)
344 return self.branch
345
346 def GetBranchRef(self):
347 """Returns the full branch name, e.g. 'refs/heads/master'."""
348 self.GetBranch() # Poke the lazy loader.
349 return self.branchref
350
351 def FetchUpstreamTuple(self):
352 """Returns a tuple containg remote and remote ref,
353 e.g. 'origin', 'refs/heads/master'
354 """
355 remote = '.'
356 branch = self.GetBranch()
357 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
358 error_ok=True).strip()
359 if upstream_branch:
360 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
361 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
363 error_ok=True).strip()
364 if upstream_branch:
365 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000367 # Fall back on trying a git-svn upstream branch.
368 if settings.GetIsGitSvn():
369 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000370 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000371 # Else, try to guess the origin remote.
372 remote_branches = RunGit(['branch', '-r']).split()
373 if 'origin/master' in remote_branches:
374 # Fall back on origin/master if it exits.
375 remote = 'origin'
376 upstream_branch = 'refs/heads/master'
377 elif 'origin/trunk' in remote_branches:
378 # Fall back on origin/trunk if it exists. Generally a shared
379 # git-svn clone
380 remote = 'origin'
381 upstream_branch = 'refs/heads/trunk'
382 else:
383 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000384Either pass complete "git diff"-style arguments, like
385 git cl upload origin/master
386or verify this branch is set up to track another (via the --track argument to
387"git checkout -b ...").""")
388
389 return remote, upstream_branch
390
391 def GetUpstreamBranch(self):
392 if self.upstream_branch is None:
393 remote, upstream_branch = self.FetchUpstreamTuple()
394 if remote is not '.':
395 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
396 self.upstream_branch = upstream_branch
397 return self.upstream_branch
398
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000399 def GetRemote(self):
400 if not self._remote:
401 self._remote = self.FetchUpstreamTuple()[0]
402 if self._remote == '.':
403
404 remotes = RunGit(['remote'], error_ok=True).split()
405 if len(remotes) == 1:
406 self._remote, = remotes
407 elif 'origin' in remotes:
408 self._remote = 'origin'
409 logging.warning('Could not determine which remote this change is '
410 'associated with, so defaulting to "%s". This may '
411 'not be what you want. You may prevent this message '
412 'by running "git svn info" as documented here: %s',
413 self._remote,
414 GIT_INSTRUCTIONS_URL)
415 else:
416 logging.warn('Could not determine which remote this change is '
417 'associated with. You may prevent this message by '
418 'running "git svn info" as documented here: %s',
419 GIT_INSTRUCTIONS_URL)
420 return self._remote
421
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000422 def GetGitBaseUrlFromConfig(self):
423 """Return the configured base URL from branch.<branchname>.baseurl.
424
425 Returns None if it is not set.
426 """
427 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
428 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 def GetRemoteUrl(self):
431 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
432
433 Returns None if there is no remote.
434 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000435 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
437
438 def GetIssue(self):
439 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
441 if issue:
442 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000443 else:
444 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000445 self.has_issue = True
446 return self.issue
447
448 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000449 if not self.rietveld_server:
450 # If we're on a branch then get the server potentially associated
451 # with that branch.
452 if self.GetIssue():
453 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
454 ['config', self._RietveldServer()], error_ok=True).strip())
455 if not self.rietveld_server:
456 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000457 return self.rietveld_server
458
459 def GetIssueURL(self):
460 """Get the URL for a particular issue."""
461 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
462
463 def GetDescription(self, pretty=False):
464 if not self.has_description:
465 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000466 issue = int(self.GetIssue())
467 try:
468 self.description = self.RpcServer().get_description(issue).strip()
469 except urllib2.HTTPError, e:
470 if e.code == 404:
471 DieWithError(
472 ('\nWhile fetching the description for issue %d, received a '
473 '404 (not found)\n'
474 'error. It is likely that you deleted this '
475 'issue on the server. If this is the\n'
476 'case, please run\n\n'
477 ' git cl issue 0\n\n'
478 'to clear the association with the deleted issue. Then run '
479 'this command again.') % issue)
480 else:
481 DieWithError(
482 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483 self.has_description = True
484 if pretty:
485 wrapper = textwrap.TextWrapper()
486 wrapper.initial_indent = wrapper.subsequent_indent = ' '
487 return wrapper.fill(self.description)
488 return self.description
489
490 def GetPatchset(self):
491 if not self.has_patchset:
492 patchset = RunGit(['config', self._PatchsetSetting()],
493 error_ok=True).strip()
494 if patchset:
495 self.patchset = patchset
496 else:
497 self.patchset = None
498 self.has_patchset = True
499 return self.patchset
500
501 def SetPatchset(self, patchset):
502 """Set this branch's patchset. If patchset=0, clears the patchset."""
503 if patchset:
504 RunGit(['config', self._PatchsetSetting(), str(patchset)])
505 else:
506 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000507 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000508 self.has_patchset = False
509
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000510 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000511 patchset = self.RpcServer().get_issue_properties(
512 int(issue), False)['patchsets'][-1]
513 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000514 '/download/issue%s_%s.diff' % (issue, patchset))
515
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000516 def SetIssue(self, issue):
517 """Set this branch's issue. If issue=0, clears the issue."""
518 if issue:
519 RunGit(['config', self._IssueSetting(), str(issue)])
520 if self.rietveld_server:
521 RunGit(['config', self._RietveldServer(), self.rietveld_server])
522 else:
523 RunGit(['config', '--unset', self._IssueSetting()])
524 self.SetPatchset(0)
525 self.has_issue = False
526
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000527 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000528 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
529 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000530
531 # We use the sha1 of HEAD as a name of this change.
532 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000533 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000534 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000535 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000536 except subprocess2.CalledProcessError:
537 DieWithError(
538 ('\nFailed to diff against upstream branch %s!\n\n'
539 'This branch probably doesn\'t exist anymore. To reset the\n'
540 'tracking branch, please run\n'
541 ' git branch --set-upstream %s trunk\n'
542 'replacing trunk with origin/master or the relevant branch') %
543 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000544
545 issue = ConvertToInteger(self.GetIssue())
546 patchset = ConvertToInteger(self.GetPatchset())
547 if issue:
548 description = self.GetDescription()
549 else:
550 # If the change was never uploaded, use the log messages of all commits
551 # up to the branch point, as git cl upload will prefill the description
552 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000553 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
554 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000555
556 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000557 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000558 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000559 name,
560 description,
561 absroot,
562 files,
563 issue,
564 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000565 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000566
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000567 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
568 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
569 change = self.GetChange(upstream_branch, author)
570
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000571 # Apply watchlists on upload.
572 if not committing:
573 watchlist = watchlists.Watchlists(change.RepositoryRoot())
574 files = [f.LocalPath() for f in change.AffectedFiles()]
575 self.SetWatchers(watchlist.GetWatchersForPaths(files))
576
577 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000578 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000579 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000580 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000581 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000582 except presubmit_support.PresubmitFailure, e:
583 DieWithError(
584 ('%s\nMaybe your depot_tools is out of date?\n'
585 'If all fails, contact maruel@') % e)
586
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000588 """Updates the description and closes the issue."""
589 issue = int(self.GetIssue())
590 self.RpcServer().update_description(issue, self.description)
591 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000592
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000593 def SetFlag(self, flag, value):
594 """Patchset must match."""
595 if not self.GetPatchset():
596 DieWithError('The patchset needs to match. Send another patchset.')
597 try:
598 return self.RpcServer().set_flag(
599 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
600 except urllib2.HTTPError, e:
601 if e.code == 404:
602 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
603 if e.code == 403:
604 DieWithError(
605 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
606 'match?') % (self.GetIssue(), self.GetPatchset()))
607 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000609 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 """Returns an upload.RpcServer() to access this review's rietveld instance.
611 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000612 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000613 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
614 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000615 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616
617 def _IssueSetting(self):
618 """Return the git setting that stores this change's issue."""
619 return 'branch.%s.rietveldissue' % self.GetBranch()
620
621 def _PatchsetSetting(self):
622 """Return the git setting that stores this change's most recent patchset."""
623 return 'branch.%s.rietveldpatchset' % self.GetBranch()
624
625 def _RietveldServer(self):
626 """Returns the git setting that stores this change's rietveld server."""
627 return 'branch.%s.rietveldserver' % self.GetBranch()
628
629
630def GetCodereviewSettingsInteractively():
631 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000632 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 server = settings.GetDefaultServerUrl(error_ok=True)
634 prompt = 'Rietveld server (host[:port])'
635 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000636 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 if not server and not newserver:
638 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000639 if newserver:
640 newserver = gclient_utils.UpgradeToHttps(newserver)
641 if newserver != server:
642 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000644 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 prompt = caption
646 if initial:
647 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000648 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 if new_val == 'x':
650 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000651 elif new_val:
652 if is_url:
653 new_val = gclient_utils.UpgradeToHttps(new_val)
654 if new_val != initial:
655 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000657 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000659 'tree-status-url', False)
660 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661
662 # TODO: configure a default branch to diff against, rather than this
663 # svn-based hackery.
664
665
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000666class ChangeDescription(object):
667 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000668 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000669 self.log_desc = log_desc
670 self.reviewers = reviewers
671 self.description = self.log_desc
672
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000673 def Prompt(self):
674 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000675# This will displayed on the codereview site.
676# The first line will also be used as the subject of the review.
677"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000678 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000679 if ('\nR=' not in self.description and
680 '\nTBR=' not in self.description and
681 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000682 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000683 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000684 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000685 content = content.rstrip('\n') + '\n'
686 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000687 if not content:
688 DieWithError('Running editor failed')
689 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000690 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000691 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000692 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000693
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000694 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000695 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000696 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000697 # Retrieves all reviewer lines
698 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000699 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000700 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000701 if reviewers:
702 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000703
704 def IsEmpty(self):
705 return not self.description
706
707
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708def FindCodereviewSettingsFile(filename='codereview.settings'):
709 """Finds the given file starting in the cwd and going up.
710
711 Only looks up to the top of the repository unless an
712 'inherit-review-settings-ok' file exists in the root of the repository.
713 """
714 inherit_ok_file = 'inherit-review-settings-ok'
715 cwd = os.getcwd()
716 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
717 if os.path.isfile(os.path.join(root, inherit_ok_file)):
718 root = '/'
719 while True:
720 if filename in os.listdir(cwd):
721 if os.path.isfile(os.path.join(cwd, filename)):
722 return open(os.path.join(cwd, filename))
723 if cwd == root:
724 break
725 cwd = os.path.dirname(cwd)
726
727
728def LoadCodereviewSettingsFromFile(fileobj):
729 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000730 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732 def SetProperty(name, setting, unset_error_ok=False):
733 fullname = 'rietveld.' + name
734 if setting in keyvals:
735 RunGit(['config', fullname, keyvals[setting]])
736 else:
737 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
738
739 SetProperty('server', 'CODE_REVIEW_SERVER')
740 # Only server setting is required. Other settings can be absent.
741 # In that case, we ignore errors raised during option deletion attempt.
742 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
743 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
744 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
745
ukai@chromium.orge8077812012-02-03 03:41:46 +0000746 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
747 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
748 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
751 #should be of the form
752 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
753 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
754 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
755 keyvals['ORIGIN_URL_CONFIG']])
756
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000758def urlretrieve(source, destination):
759 """urllib is broken for SSL connections via a proxy therefore we
760 can't use urllib.urlretrieve()."""
761 with open(destination, 'w') as f:
762 f.write(urllib2.urlopen(source).read())
763
764
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000765def DownloadHooks(force):
766 """downloads hooks
767
768 Args:
769 force: True to update hooks. False to install hooks if not present.
770 """
771 if not settings.GetIsGerrit():
772 return
773 server_url = settings.GetDefaultServerUrl()
774 src = '%s/tools/hooks/commit-msg' % server_url
775 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
776 if not os.access(dst, os.X_OK):
777 if os.path.exists(dst):
778 if not force:
779 return
780 os.remove(dst)
781 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000782 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000783 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
784 except Exception:
785 if os.path.exists(dst):
786 os.remove(dst)
787 DieWithError('\nFailed to download hooks from %s' % src)
788
789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790@usage('[repo root containing codereview.settings]')
791def CMDconfig(parser, args):
792 """edit configuration for this tree"""
793
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000794 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 if len(args) == 0:
796 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000797 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798 return 0
799
800 url = args[0]
801 if not url.endswith('codereview.settings'):
802 url = os.path.join(url, 'codereview.settings')
803
804 # Load code review settings and download hooks (if available).
805 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000806 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 return 0
808
809
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000810def CMDbaseurl(parser, args):
811 """get or set base-url for this branch"""
812 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
813 branch = ShortBranchName(branchref)
814 _, args = parser.parse_args(args)
815 if not args:
816 print("Current base-url:")
817 return RunGit(['config', 'branch.%s.base-url' % branch],
818 error_ok=False).strip()
819 else:
820 print("Setting base-url to %s" % args[0])
821 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
822 error_ok=False).strip()
823
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825def CMDstatus(parser, args):
826 """show status of changelists"""
827 parser.add_option('--field',
828 help='print only specific field (desc|id|patch|url)')
829 (options, args) = parser.parse_args(args)
830
831 # TODO: maybe make show_branches a flag if necessary.
832 show_branches = not options.field
833
834 if show_branches:
835 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
836 if branches:
837 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000838 changes = (Changelist(branchref=b) for b in branches.splitlines())
839 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
840 alignment = max(5, max(len(b) for b in branches))
841 for branch in sorted(branches):
842 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843
844 cl = Changelist()
845 if options.field:
846 if options.field.startswith('desc'):
847 print cl.GetDescription()
848 elif options.field == 'id':
849 issueid = cl.GetIssue()
850 if issueid:
851 print issueid
852 elif options.field == 'patch':
853 patchset = cl.GetPatchset()
854 if patchset:
855 print patchset
856 elif options.field == 'url':
857 url = cl.GetIssueURL()
858 if url:
859 print url
860 else:
861 print
862 print 'Current branch:',
863 if not cl.GetIssue():
864 print 'no issue assigned.'
865 return 0
866 print cl.GetBranch()
867 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
868 print 'Issue description:'
869 print cl.GetDescription(pretty=True)
870 return 0
871
872
873@usage('[issue_number]')
874def CMDissue(parser, args):
875 """Set or display the current code review issue number.
876
877 Pass issue number 0 to clear the current issue.
878"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000879 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880
881 cl = Changelist()
882 if len(args) > 0:
883 try:
884 issue = int(args[0])
885 except ValueError:
886 DieWithError('Pass a number to set the issue or none to list it.\n'
887 'Maybe you want to run git cl status?')
888 cl.SetIssue(issue)
889 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
890 return 0
891
892
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000893def CMDcomments(parser, args):
894 """show review comments of the current changelist"""
895 (_, args) = parser.parse_args(args)
896 if args:
897 parser.error('Unsupported argument: %s' % args)
898
899 cl = Changelist()
900 if cl.GetIssue():
901 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
902 for message in sorted(data['messages'], key=lambda x: x['date']):
903 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
904 if message['text'].strip():
905 print '\n'.join(' ' + l for l in message['text'].splitlines())
906 return 0
907
908
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909def CreateDescriptionFromLog(args):
910 """Pulls out the commit log to use as a base for the CL description."""
911 log_args = []
912 if len(args) == 1 and not args[0].endswith('.'):
913 log_args = [args[0] + '..']
914 elif len(args) == 1 and args[0].endswith('...'):
915 log_args = [args[0][:-1]]
916 elif len(args) == 2:
917 log_args = [args[0] + '..' + args[1]]
918 else:
919 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000920 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921
922
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000923def ConvertToInteger(inputval):
924 """Convert a string to integer, but returns either an int or None."""
925 try:
926 return int(inputval)
927 except (TypeError, ValueError):
928 return None
929
930
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931def CMDpresubmit(parser, args):
932 """run presubmit tests on the current changelist"""
933 parser.add_option('--upload', action='store_true',
934 help='Run upload hook instead of the push/dcommit hook')
935 (options, args) = parser.parse_args(args)
936
937 # Make sure index is up-to-date before running diff-index.
938 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
939 if RunGit(['diff-index', 'HEAD']):
940 # TODO(maruel): Is this really necessary?
941 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
942 return 1
943
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000944 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945 if args:
946 base_branch = args[0]
947 else:
948 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000949 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000951 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000952 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000953 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000954 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955
956
ukai@chromium.orge8077812012-02-03 03:41:46 +0000957def GerritUpload(options, args, cl):
958 """upload the current branch to gerrit."""
959 # We assume the remote called "origin" is the one we want.
960 # It is probably not worthwhile to support different workflows.
961 remote = 'origin'
962 branch = 'master'
963 if options.target_branch:
964 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000966 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000967 if options.reviewers:
968 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000969 change_desc = ChangeDescription(log_desc, options.reviewers)
970 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000971 if change_desc.IsEmpty():
972 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 return 1
974
ukai@chromium.orge8077812012-02-03 03:41:46 +0000975 receive_options = []
976 cc = cl.GetCCList().split(',')
977 if options.cc:
978 cc += options.cc.split(',')
979 cc = filter(None, cc)
980 if cc:
981 receive_options += ['--cc=' + email for email in cc]
982 if change_desc.reviewers:
983 reviewers = filter(None, change_desc.reviewers.split(','))
984 if reviewers:
985 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986
ukai@chromium.orge8077812012-02-03 03:41:46 +0000987 git_command = ['push']
988 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000989 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000990 ' '.join(receive_options))
991 git_command += [remote, 'HEAD:refs/for/' + branch]
992 RunGit(git_command)
993 # TODO(ukai): parse Change-Id: and set issue number?
994 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996
ukai@chromium.orge8077812012-02-03 03:41:46 +0000997def RietveldUpload(options, args, cl):
998 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1000 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001 if options.emulate_svn_auto_props:
1002 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003
1004 change_desc = None
1005
1006 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001007 if options.title:
1008 upload_args.extend(['--title', options.title])
1009 elif options.message:
1010 # TODO(rogerta): for now, the -m option will also set the --title option
1011 # for upload.py. Soon this will be changed to set the --message option.
1012 # Will wait until people are used to typing -t instead of -m.
1013 upload_args.extend(['--title', options.message])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 upload_args.extend(['--issue', cl.GetIssue()])
1015 print ("This branch is associated with issue %s. "
1016 "Adding patch to that issue." % cl.GetIssue())
1017 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001018 if options.title:
1019 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001020 message = options.message or CreateDescriptionFromLog(args)
1021 change_desc = ChangeDescription(message, options.reviewers)
1022 if not options.force:
1023 change_desc.Prompt()
1024 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001025
1026 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 print "Description is empty; aborting."
1028 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001029
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001030 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001031 if change_desc.reviewers:
1032 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001033 if options.send_mail:
1034 if not change_desc.reviewers:
1035 DieWithError("Must specify reviewers to send email.")
1036 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001037 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001038 if cc:
1039 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040
1041 # Include the upstream repo's URL in the change -- this is useful for
1042 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001043 remote_url = cl.GetGitBaseUrlFromConfig()
1044 if not remote_url:
1045 if settings.GetIsGitSvn():
1046 # URL is dependent on the current directory.
1047 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1048 if data:
1049 keys = dict(line.split(': ', 1) for line in data.splitlines()
1050 if ': ' in line)
1051 remote_url = keys.get('URL', None)
1052 else:
1053 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1054 remote_url = (cl.GetRemoteUrl() + '@'
1055 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 if remote_url:
1057 upload_args.extend(['--base_url', remote_url])
1058
1059 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001060 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001061 except KeyboardInterrupt:
1062 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063 except:
1064 # If we got an exception after the user typed a description for their
1065 # change, back up the description before re-raising.
1066 if change_desc:
1067 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1068 print '\nGot exception while uploading -- saving description to %s\n' \
1069 % backup_path
1070 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001071 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 backup_file.close()
1073 raise
1074
1075 if not cl.GetIssue():
1076 cl.SetIssue(issue)
1077 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001078
1079 if options.use_commit_queue:
1080 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 return 0
1082
1083
ukai@chromium.orge8077812012-02-03 03:41:46 +00001084@usage('[args to "git diff"]')
1085def CMDupload(parser, args):
1086 """upload the current changelist to codereview"""
1087 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1088 help='bypass upload presubmit hook')
1089 parser.add_option('-f', action='store_true', dest='force',
1090 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001091 parser.add_option('-m', dest='message', help='message for patchset')
1092 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001093 parser.add_option('-r', '--reviewers',
1094 help='reviewer email addresses')
1095 parser.add_option('--cc',
1096 help='cc email addresses')
1097 parser.add_option('--send-mail', action='store_true',
1098 help='send email to reviewer immediately')
1099 parser.add_option("--emulate_svn_auto_props", action="store_true",
1100 dest="emulate_svn_auto_props",
1101 help="Emulate Subversion's auto properties feature.")
1102 parser.add_option("--desc_from_logs", action="store_true",
1103 dest="from_logs",
1104 help="""Squashes git commit logs into change description and
1105 uses message as subject""")
1106 parser.add_option('-c', '--use-commit-queue', action='store_true',
1107 help='tell the commit queue to commit this patchset')
1108 if settings.GetIsGerrit():
1109 parser.add_option('--target_branch', dest='target_branch', default='master',
1110 help='target branch to upload')
1111 (options, args) = parser.parse_args(args)
1112
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001113 # Print warning if the user used the -m/--message argument. This will soon
1114 # change to -t/--title.
1115 if options.message:
1116 print >> sys.stderr, (
1117 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1118 'In the near future, -m or --message will send a message instead.\n'
1119 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001120
ukai@chromium.orge8077812012-02-03 03:41:46 +00001121 # Make sure index is up-to-date before running diff-index.
1122 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1123 if RunGit(['diff-index', 'HEAD']):
1124 print 'Cannot upload with a dirty tree. You must commit locally first.'
1125 return 1
1126
1127 cl = Changelist()
1128 if args:
1129 # TODO(ukai): is it ok for gerrit case?
1130 base_branch = args[0]
1131 else:
1132 # Default to diffing against the "upstream" branch.
1133 base_branch = cl.GetUpstreamBranch()
1134 args = [base_branch + "..."]
1135
1136 if not options.bypass_hooks:
1137 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1138 may_prompt=not options.force,
1139 verbose=options.verbose,
1140 author=None)
1141 if not hook_results.should_continue():
1142 return 1
1143 if not options.reviewers and hook_results.reviewers:
1144 options.reviewers = hook_results.reviewers
1145
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001146 print_stats(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001147 if settings.GetIsGerrit():
1148 return GerritUpload(options, args, cl)
1149 return RietveldUpload(options, args, cl)
1150
1151
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001152def IsSubmoduleMergeCommit(ref):
1153 # When submodules are added to the repo, we expect there to be a single
1154 # non-git-svn merge commit at remote HEAD with a signature comment.
1155 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001156 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001157 return RunGit(cmd) != ''
1158
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160def SendUpstream(parser, args, cmd):
1161 """Common code for CmdPush and CmdDCommit
1162
1163 Squashed commit into a single.
1164 Updates changelog with metadata (e.g. pointer to review).
1165 Pushes/dcommits the code upstream.
1166 Updates review and closes.
1167 """
1168 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1169 help='bypass upload presubmit hook')
1170 parser.add_option('-m', dest='message',
1171 help="override review description")
1172 parser.add_option('-f', action='store_true', dest='force',
1173 help="force yes to questions (don't prompt)")
1174 parser.add_option('-c', dest='contributor',
1175 help="external contributor for patch (appended to " +
1176 "description and used as author for git). Should be " +
1177 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 (options, args) = parser.parse_args(args)
1179 cl = Changelist()
1180
1181 if not args or cmd == 'push':
1182 # Default to merging against our best guess of the upstream branch.
1183 args = [cl.GetUpstreamBranch()]
1184
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001185 if options.contributor:
1186 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1187 print "Please provide contibutor as 'First Last <email@example.com>'"
1188 return 1
1189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001191 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001193 # Make sure index is up-to-date before running diff-index.
1194 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 if RunGit(['diff-index', 'HEAD']):
1196 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1197 return 1
1198
1199 # This rev-list syntax means "show all commits not in my branch that
1200 # are in base_branch".
1201 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1202 base_branch]).splitlines()
1203 if upstream_commits:
1204 print ('Base branch "%s" has %d commits '
1205 'not in this branch.' % (base_branch, len(upstream_commits)))
1206 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1207 return 1
1208
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001209 # This is the revision `svn dcommit` will commit on top of.
1210 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1211 '--pretty=format:%H'])
1212
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001214 # If the base_head is a submodule merge commit, the first parent of the
1215 # base_head should be a git-svn commit, which is what we're interested in.
1216 base_svn_head = base_branch
1217 if base_has_submodules:
1218 base_svn_head += '^1'
1219
1220 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 if extra_commits:
1222 print ('This branch has %d additional commits not upstreamed yet.'
1223 % len(extra_commits.splitlines()))
1224 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1225 'before attempting to %s.' % (base_branch, cmd))
1226 return 1
1227
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001228 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001229 author = None
1230 if options.contributor:
1231 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001232 hook_results = cl.RunHook(
1233 committing=True,
1234 upstream_branch=base_branch,
1235 may_prompt=not options.force,
1236 verbose=options.verbose,
1237 author=author)
1238 if not hook_results.should_continue():
1239 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240
1241 if cmd == 'dcommit':
1242 # Check the tree status if the tree status URL is set.
1243 status = GetTreeStatus()
1244 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001245 print('The tree is closed. Please wait for it to reopen. Use '
1246 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 return 1
1248 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001249 print('Unable to determine tree status. Please verify manually and '
1250 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001251 else:
1252 breakpad.SendStack(
1253 'GitClHooksBypassedCommit',
1254 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001255 (cl.GetRietveldServer(), cl.GetIssue()),
1256 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
1258 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001259 if not description and cl.GetIssue():
1260 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001262 if not description:
1263 print 'No description set.'
1264 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1265 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001267 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269
1270 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 description += "\nPatch from %s." % options.contributor
1272 print 'Description:', repr(description)
1273
1274 branches = [base_branch, cl.GetBranchRef()]
1275 if not options.force:
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001276 print_stats(branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001277 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001279 # We want to squash all this branch's commits into one commit with the proper
1280 # description. We do this by doing a "reset --soft" to the base branch (which
1281 # keeps the working copy the same), then dcommitting that. If origin/master
1282 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1283 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001285 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1286 # Delete the branches if they exist.
1287 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1288 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1289 result = RunGitWithCode(showref_cmd)
1290 if result[0] == 0:
1291 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292
1293 # We might be in a directory that's present in this branch but not in the
1294 # trunk. Move up to the top of the tree so that git commands that expect a
1295 # valid CWD won't fail after we check out the merge branch.
1296 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1297 if rel_base_path:
1298 os.chdir(rel_base_path)
1299
1300 # Stuff our change into the merge branch.
1301 # We wrap in a try...finally block so if anything goes wrong,
1302 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001303 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001305 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1306 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 if options.contributor:
1308 RunGit(['commit', '--author', options.contributor, '-m', description])
1309 else:
1310 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001311 if base_has_submodules:
1312 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1313 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1314 RunGit(['checkout', CHERRY_PICK_BRANCH])
1315 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 if cmd == 'push':
1317 # push the merge branch.
1318 remote, branch = cl.FetchUpstreamTuple()
1319 retcode, output = RunGitWithCode(
1320 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1321 logging.debug(output)
1322 else:
1323 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001324 retcode, output = RunGitWithCode(['svn', 'dcommit',
1325 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326 finally:
1327 # And then swap back to the original branch and clean up.
1328 RunGit(['checkout', '-q', cl.GetBranch()])
1329 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001330 if base_has_submodules:
1331 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
1333 if cl.GetIssue():
1334 if cmd == 'dcommit' and 'Committed r' in output:
1335 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1336 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001337 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1338 for l in output.splitlines(False))
1339 match = filter(None, match)
1340 if len(match) != 1:
1341 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1342 output)
1343 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 else:
1345 return 1
1346 viewvc_url = settings.GetViewVCUrl()
1347 if viewvc_url and revision:
1348 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1349 print ('Closing issue '
1350 '(you may be prompted for your codereview password)...')
1351 cl.CloseIssue()
1352 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001353
1354 if retcode == 0:
1355 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1356 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001357 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 return 0
1360
1361
1362@usage('[upstream branch to apply against]')
1363def CMDdcommit(parser, args):
1364 """commit the current changelist via git-svn"""
1365 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001366 message = """This doesn't appear to be an SVN repository.
1367If your project has a git mirror with an upstream SVN master, you probably need
1368to run 'git svn init', see your project's git mirror documentation.
1369If your project has a true writeable upstream repository, you probably want
1370to run 'git cl push' instead.
1371Choose wisely, if you get this wrong, your commit might appear to succeed but
1372will instead be silently ignored."""
1373 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001374 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 return SendUpstream(parser, args, 'dcommit')
1376
1377
1378@usage('[upstream branch to apply against]')
1379def CMDpush(parser, args):
1380 """commit the current changelist via git"""
1381 if settings.GetIsGitSvn():
1382 print('This appears to be an SVN repository.')
1383 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001384 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385 return SendUpstream(parser, args, 'push')
1386
1387
1388@usage('<patch url or issue id>')
1389def CMDpatch(parser, args):
1390 """patch in a code review"""
1391 parser.add_option('-b', dest='newbranch',
1392 help='create a new branch off trunk for the patch')
1393 parser.add_option('-f', action='store_true', dest='force',
1394 help='with -b, clobber any existing branch')
1395 parser.add_option('--reject', action='store_true', dest='reject',
1396 help='allow failed patches and spew .rej files')
1397 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1398 help="don't commit after patch applies")
1399 (options, args) = parser.parse_args(args)
1400 if len(args) != 1:
1401 parser.print_help()
1402 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001403 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001405 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001406 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001407
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001408 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001410 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001411 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001413 # Assume it's a URL to the patch. Default to https.
1414 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001415 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001416 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 DieWithError('Must pass an issue ID or full URL for '
1418 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001419 issue = match.group(1)
1420 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
1422 if options.newbranch:
1423 if options.force:
1424 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001425 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 RunGit(['checkout', '-b', options.newbranch,
1427 Changelist().GetUpstreamBranch()])
1428
1429 # Switch up to the top-level directory, if necessary, in preparation for
1430 # applying the patch.
1431 top = RunGit(['rev-parse', '--show-cdup']).strip()
1432 if top:
1433 os.chdir(top)
1434
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 # Git patches have a/ at the beginning of source paths. We strip that out
1436 # with a sed script rather than the -p flag to patch so we can feed either
1437 # Git or svn-style patches into the same apply command.
1438 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001439 try:
1440 patch_data = subprocess2.check_output(
1441 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1442 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 DieWithError('Git patch mungling failed.')
1444 logging.info(patch_data)
1445 # We use "git apply" to apply the patch instead of "patch" so that we can
1446 # pick up file adds.
1447 # The --index flag means: also insert into the index (so we catch adds).
1448 cmd = ['git', 'apply', '--index', '-p0']
1449 if options.reject:
1450 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001451 try:
1452 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1453 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 DieWithError('Failed to apply the patch')
1455
1456 # If we had an issue, commit the current state and register the issue.
1457 if not options.nocommit:
1458 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1459 cl = Changelist()
1460 cl.SetIssue(issue)
1461 print "Committed patch."
1462 else:
1463 print "Patch applied to index."
1464 return 0
1465
1466
1467def CMDrebase(parser, args):
1468 """rebase current branch on top of svn repo"""
1469 # Provide a wrapper for git svn rebase to help avoid accidental
1470 # git svn dcommit.
1471 # It's the only command that doesn't use parser at all since we just defer
1472 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001473 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
1475
1476def GetTreeStatus():
1477 """Fetches the tree status and returns either 'open', 'closed',
1478 'unknown' or 'unset'."""
1479 url = settings.GetTreeStatusUrl(error_ok=True)
1480 if url:
1481 status = urllib2.urlopen(url).read().lower()
1482 if status.find('closed') != -1 or status == '0':
1483 return 'closed'
1484 elif status.find('open') != -1 or status == '1':
1485 return 'open'
1486 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 return 'unset'
1488
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001489
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490def GetTreeStatusReason():
1491 """Fetches the tree status from a json url and returns the message
1492 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001493 url = settings.GetTreeStatusUrl()
1494 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 connection = urllib2.urlopen(json_url)
1496 status = json.loads(connection.read())
1497 connection.close()
1498 return status['message']
1499
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501def CMDtree(parser, args):
1502 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001503 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 status = GetTreeStatus()
1505 if 'unset' == status:
1506 print 'You must configure your tree status URL by running "git cl config".'
1507 return 2
1508
1509 print "The tree is %s" % status
1510 print
1511 print GetTreeStatusReason()
1512 if status != 'open':
1513 return 1
1514 return 0
1515
1516
1517def CMDupstream(parser, args):
1518 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001519 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001520 if args:
1521 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001522 cl = Changelist()
1523 print cl.GetUpstreamBranch()
1524 return 0
1525
1526
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001527def CMDset_commit(parser, args):
1528 """set the commit bit"""
1529 _, args = parser.parse_args(args)
1530 if args:
1531 parser.error('Unrecognized args: %s' % ' '.join(args))
1532 cl = Changelist()
1533 cl.SetFlag('commit', '1')
1534 return 0
1535
1536
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537def Command(name):
1538 return getattr(sys.modules[__name__], 'CMD' + name, None)
1539
1540
1541def CMDhelp(parser, args):
1542 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001543 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544 if len(args) == 1:
1545 return main(args + ['--help'])
1546 parser.print_help()
1547 return 0
1548
1549
1550def GenUsage(parser, command):
1551 """Modify an OptParse object with the function's documentation."""
1552 obj = Command(command)
1553 more = getattr(obj, 'usage_more', '')
1554 if command == 'help':
1555 command = '<command>'
1556 else:
1557 # OptParser.description prefer nicely non-formatted strings.
1558 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1559 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1560
1561
1562def main(argv):
1563 """Doesn't parse the arguments here, just find the right subcommand to
1564 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001565 if sys.hexversion < 0x02060000:
1566 print >> sys.stderr, (
1567 '\nYour python version %s is unsupported, please upgrade.\n' %
1568 sys.version.split(' ', 1)[0])
1569 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001570 # Reload settings.
1571 global settings
1572 settings = Settings()
1573
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001574 # Do it late so all commands are listed.
1575 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1576 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1577 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1578
1579 # Create the option parse and add --verbose support.
1580 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001581 parser.add_option(
1582 '-v', '--verbose', action='count', default=0,
1583 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584 old_parser_args = parser.parse_args
1585 def Parse(args):
1586 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001587 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001589 elif options.verbose:
1590 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 else:
1592 logging.basicConfig(level=logging.WARNING)
1593 return options, args
1594 parser.parse_args = Parse
1595
1596 if argv:
1597 command = Command(argv[0])
1598 if command:
1599 # "fix" the usage and the description now that we know the subcommand.
1600 GenUsage(parser, argv[0])
1601 try:
1602 return command(parser, argv[1:])
1603 except urllib2.HTTPError, e:
1604 if e.code != 500:
1605 raise
1606 DieWithError(
1607 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1608 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1609
1610 # Not a known command. Default to help.
1611 GenUsage(parser, 'help')
1612 return CMDhelp(parser, argv)
1613
1614
1615if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001616 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001617 sys.exit(main(sys.argv[1:]))