blob: 2c63d7330cafd947151134f864714f77e8e918b6 [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."""
72 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
73 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def usage(more):
77 def hook(fn):
78 fn.usage_more = more
79 return fn
80 return hook
81
82
maruel@chromium.org90541732011-04-01 17:54:18 +000083def ask_for_data(prompt):
84 try:
85 return raw_input(prompt)
86 except KeyboardInterrupt:
87 # Hide the exception.
88 sys.exit(1)
89
90
bauerb@chromium.org866276c2011-03-18 20:09:31 +000091def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
92 """Return the corresponding git ref if |base_url| together with |glob_spec|
93 matches the full |url|.
94
95 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
96 """
97 fetch_suburl, as_ref = glob_spec.split(':')
98 if allow_wildcards:
99 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
100 if glob_match:
101 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
102 # "branches/{472,597,648}/src:refs/remotes/svn/*".
103 branch_re = re.escape(base_url)
104 if glob_match.group(1):
105 branch_re += '/' + re.escape(glob_match.group(1))
106 wildcard = glob_match.group(2)
107 if wildcard == '*':
108 branch_re += '([^/]*)'
109 else:
110 # Escape and replace surrounding braces with parentheses and commas
111 # with pipe symbols.
112 wildcard = re.escape(wildcard)
113 wildcard = re.sub('^\\\\{', '(', wildcard)
114 wildcard = re.sub('\\\\,', '|', wildcard)
115 wildcard = re.sub('\\\\}$', ')', wildcard)
116 branch_re += wildcard
117 if glob_match.group(3):
118 branch_re += re.escape(glob_match.group(3))
119 match = re.match(branch_re, url)
120 if match:
121 return re.sub('\*$', match.group(1), as_ref)
122
123 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
124 if fetch_suburl:
125 full_url = base_url + '/' + fetch_suburl
126 else:
127 full_url = base_url
128 if full_url == url:
129 return as_ref
130 return None
131
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133class Settings(object):
134 def __init__(self):
135 self.default_server = None
136 self.cc = None
137 self.root = None
138 self.is_git_svn = None
139 self.svn_branch = None
140 self.tree_status_url = None
141 self.viewvc_url = None
142 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000143 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000144
145 def LazyUpdateIfNeeded(self):
146 """Updates the settings from a codereview.settings file, if available."""
147 if not self.updated:
148 cr_settings_file = FindCodereviewSettingsFile()
149 if cr_settings_file:
150 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000151 self.updated = True
152 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153 self.updated = True
154
155 def GetDefaultServerUrl(self, error_ok=False):
156 if not self.default_server:
157 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000158 self.default_server = gclient_utils.UpgradeToHttps(
159 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000160 if error_ok:
161 return self.default_server
162 if not self.default_server:
163 error_message = ('Could not find settings file. You must configure '
164 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000165 self.default_server = gclient_utils.UpgradeToHttps(
166 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000167 return self.default_server
168
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000169 def GetRoot(self):
170 if not self.root:
171 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
172 return self.root
173
174 def GetIsGitSvn(self):
175 """Return true if this repo looks like it's using git-svn."""
176 if self.is_git_svn is None:
177 # If you have any "svn-remote.*" config keys, we think you're using svn.
178 self.is_git_svn = RunGitWithCode(
179 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
180 return self.is_git_svn
181
182 def GetSVNBranch(self):
183 if self.svn_branch is None:
184 if not self.GetIsGitSvn():
185 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
186
187 # Try to figure out which remote branch we're based on.
188 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000189 # 1) iterate through our branch history and find the svn URL.
190 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000191
192 # regexp matching the git-svn line that contains the URL.
193 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
194
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000195 # We don't want to go through all of history, so read a line from the
196 # pipe at a time.
197 # The -100 is an arbitrary limit so we don't search forever.
198 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000199 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000200 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000201 for line in proc.stdout:
202 match = git_svn_re.match(line)
203 if match:
204 url = match.group(1)
205 proc.stdout.close() # Cut pipe.
206 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000207
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000208 if url:
209 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
210 remotes = RunGit(['config', '--get-regexp',
211 r'^svn-remote\..*\.url']).splitlines()
212 for remote in remotes:
213 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000214 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000215 remote = match.group(1)
216 base_url = match.group(2)
217 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000218 ['config', 'svn-remote.%s.fetch' % remote],
219 error_ok=True).strip()
220 if fetch_spec:
221 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
222 if self.svn_branch:
223 break
224 branch_spec = RunGit(
225 ['config', 'svn-remote.%s.branches' % remote],
226 error_ok=True).strip()
227 if branch_spec:
228 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
229 if self.svn_branch:
230 break
231 tag_spec = RunGit(
232 ['config', 'svn-remote.%s.tags' % remote],
233 error_ok=True).strip()
234 if tag_spec:
235 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
236 if self.svn_branch:
237 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000238
239 if not self.svn_branch:
240 DieWithError('Can\'t guess svn branch -- try specifying it on the '
241 'command line')
242
243 return self.svn_branch
244
245 def GetTreeStatusUrl(self, error_ok=False):
246 if not self.tree_status_url:
247 error_message = ('You must configure your tree status URL by running '
248 '"git cl config".')
249 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
250 error_ok=error_ok,
251 error_message=error_message)
252 return self.tree_status_url
253
254 def GetViewVCUrl(self):
255 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000256 self.viewvc_url = gclient_utils.UpgradeToHttps(
257 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000258 return self.viewvc_url
259
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000260 def GetDefaultCCList(self):
261 return self._GetConfig('rietveld.cc', error_ok=True)
262
ukai@chromium.orge8077812012-02-03 03:41:46 +0000263 def GetIsGerrit(self):
264 """Return true if this repo is assosiated with gerrit code review system."""
265 if self.is_gerrit is None:
266 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
267 return self.is_gerrit
268
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000269 def _GetConfig(self, param, **kwargs):
270 self.LazyUpdateIfNeeded()
271 return RunGit(['config', param], **kwargs).strip()
272
273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274def ShortBranchName(branch):
275 """Convert a name like 'refs/heads/foo' to just 'foo'."""
276 return branch.replace('refs/heads/', '')
277
278
279class Changelist(object):
280 def __init__(self, branchref=None):
281 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000282 global settings
283 if not settings:
284 # Happens when git_cl.py is used as a utility library.
285 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286 settings.GetDefaultServerUrl()
287 self.branchref = branchref
288 if self.branchref:
289 self.branch = ShortBranchName(self.branchref)
290 else:
291 self.branch = None
292 self.rietveld_server = None
293 self.upstream_branch = None
294 self.has_issue = False
295 self.issue = None
296 self.has_description = False
297 self.description = None
298 self.has_patchset = False
299 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000300 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000301 self.cc = None
302 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000303 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000304
305 def GetCCList(self):
306 """Return the users cc'd on this CL.
307
308 Return is a string suitable for passing to gcl with the --cc flag.
309 """
310 if self.cc is None:
311 base_cc = settings .GetDefaultCCList()
312 more_cc = ','.join(self.watchers)
313 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
314 return self.cc
315
316 def SetWatchers(self, watchers):
317 """Set the list of email addresses that should be cc'd based on the changed
318 files in this CL.
319 """
320 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321
322 def GetBranch(self):
323 """Returns the short branch name, e.g. 'master'."""
324 if not self.branch:
325 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
326 self.branch = ShortBranchName(self.branchref)
327 return self.branch
328
329 def GetBranchRef(self):
330 """Returns the full branch name, e.g. 'refs/heads/master'."""
331 self.GetBranch() # Poke the lazy loader.
332 return self.branchref
333
334 def FetchUpstreamTuple(self):
335 """Returns a tuple containg remote and remote ref,
336 e.g. 'origin', 'refs/heads/master'
337 """
338 remote = '.'
339 branch = self.GetBranch()
340 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
341 error_ok=True).strip()
342 if upstream_branch:
343 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
344 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000345 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
346 error_ok=True).strip()
347 if upstream_branch:
348 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000349 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000350 # Fall back on trying a git-svn upstream branch.
351 if settings.GetIsGitSvn():
352 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000353 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000354 # Else, try to guess the origin remote.
355 remote_branches = RunGit(['branch', '-r']).split()
356 if 'origin/master' in remote_branches:
357 # Fall back on origin/master if it exits.
358 remote = 'origin'
359 upstream_branch = 'refs/heads/master'
360 elif 'origin/trunk' in remote_branches:
361 # Fall back on origin/trunk if it exists. Generally a shared
362 # git-svn clone
363 remote = 'origin'
364 upstream_branch = 'refs/heads/trunk'
365 else:
366 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000367Either pass complete "git diff"-style arguments, like
368 git cl upload origin/master
369or verify this branch is set up to track another (via the --track argument to
370"git checkout -b ...").""")
371
372 return remote, upstream_branch
373
374 def GetUpstreamBranch(self):
375 if self.upstream_branch is None:
376 remote, upstream_branch = self.FetchUpstreamTuple()
377 if remote is not '.':
378 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
379 self.upstream_branch = upstream_branch
380 return self.upstream_branch
381
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000382 def GetRemote(self):
383 if not self._remote:
384 self._remote = self.FetchUpstreamTuple()[0]
385 if self._remote == '.':
386
387 remotes = RunGit(['remote'], error_ok=True).split()
388 if len(remotes) == 1:
389 self._remote, = remotes
390 elif 'origin' in remotes:
391 self._remote = 'origin'
392 logging.warning('Could not determine which remote this change is '
393 'associated with, so defaulting to "%s". This may '
394 'not be what you want. You may prevent this message '
395 'by running "git svn info" as documented here: %s',
396 self._remote,
397 GIT_INSTRUCTIONS_URL)
398 else:
399 logging.warn('Could not determine which remote this change is '
400 'associated with. You may prevent this message by '
401 'running "git svn info" as documented here: %s',
402 GIT_INSTRUCTIONS_URL)
403 return self._remote
404
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000405 def GetGitBaseUrlFromConfig(self):
406 """Return the configured base URL from branch.<branchname>.baseurl.
407
408 Returns None if it is not set.
409 """
410 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
411 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000412
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000413 def GetRemoteUrl(self):
414 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
415
416 Returns None if there is no remote.
417 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000418 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
420
421 def GetIssue(self):
422 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000423 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
424 if issue:
425 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000426 else:
427 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 self.has_issue = True
429 return self.issue
430
431 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000432 if not self.rietveld_server:
433 # If we're on a branch then get the server potentially associated
434 # with that branch.
435 if self.GetIssue():
436 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
437 ['config', self._RietveldServer()], error_ok=True).strip())
438 if not self.rietveld_server:
439 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 return self.rietveld_server
441
442 def GetIssueURL(self):
443 """Get the URL for a particular issue."""
444 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
445
446 def GetDescription(self, pretty=False):
447 if not self.has_description:
448 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000449 issue = int(self.GetIssue())
450 try:
451 self.description = self.RpcServer().get_description(issue).strip()
452 except urllib2.HTTPError, e:
453 if e.code == 404:
454 DieWithError(
455 ('\nWhile fetching the description for issue %d, received a '
456 '404 (not found)\n'
457 'error. It is likely that you deleted this '
458 'issue on the server. If this is the\n'
459 'case, please run\n\n'
460 ' git cl issue 0\n\n'
461 'to clear the association with the deleted issue. Then run '
462 'this command again.') % issue)
463 else:
464 DieWithError(
465 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 self.has_description = True
467 if pretty:
468 wrapper = textwrap.TextWrapper()
469 wrapper.initial_indent = wrapper.subsequent_indent = ' '
470 return wrapper.fill(self.description)
471 return self.description
472
473 def GetPatchset(self):
474 if not self.has_patchset:
475 patchset = RunGit(['config', self._PatchsetSetting()],
476 error_ok=True).strip()
477 if patchset:
478 self.patchset = patchset
479 else:
480 self.patchset = None
481 self.has_patchset = True
482 return self.patchset
483
484 def SetPatchset(self, patchset):
485 """Set this branch's patchset. If patchset=0, clears the patchset."""
486 if patchset:
487 RunGit(['config', self._PatchsetSetting(), str(patchset)])
488 else:
489 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000490 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 self.has_patchset = False
492
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000493 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000494 patchset = self.RpcServer().get_issue_properties(
495 int(issue), False)['patchsets'][-1]
496 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000497 '/download/issue%s_%s.diff' % (issue, patchset))
498
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000499 def SetIssue(self, issue):
500 """Set this branch's issue. If issue=0, clears the issue."""
501 if issue:
502 RunGit(['config', self._IssueSetting(), str(issue)])
503 if self.rietveld_server:
504 RunGit(['config', self._RietveldServer(), self.rietveld_server])
505 else:
506 RunGit(['config', '--unset', self._IssueSetting()])
507 self.SetPatchset(0)
508 self.has_issue = False
509
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000510 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000511 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
512 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000513
514 # We use the sha1 of HEAD as a name of this change.
515 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000516 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000517 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000518 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000519 except subprocess2.CalledProcessError:
520 DieWithError(
521 ('\nFailed to diff against upstream branch %s!\n\n'
522 'This branch probably doesn\'t exist anymore. To reset the\n'
523 'tracking branch, please run\n'
524 ' git branch --set-upstream %s trunk\n'
525 'replacing trunk with origin/master or the relevant branch') %
526 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000527
528 issue = ConvertToInteger(self.GetIssue())
529 patchset = ConvertToInteger(self.GetPatchset())
530 if issue:
531 description = self.GetDescription()
532 else:
533 # If the change was never uploaded, use the log messages of all commits
534 # up to the branch point, as git cl upload will prefill the description
535 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000536 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
537 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000538
539 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000540 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000541 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000542 name,
543 description,
544 absroot,
545 files,
546 issue,
547 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000548 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000549
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000550 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
551 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
552 change = self.GetChange(upstream_branch, author)
553
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000554 # Apply watchlists on upload.
555 if not committing:
556 watchlist = watchlists.Watchlists(change.RepositoryRoot())
557 files = [f.LocalPath() for f in change.AffectedFiles()]
558 self.SetWatchers(watchlist.GetWatchersForPaths(files))
559
560 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000561 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000562 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000563 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000564 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000565 except presubmit_support.PresubmitFailure, e:
566 DieWithError(
567 ('%s\nMaybe your depot_tools is out of date?\n'
568 'If all fails, contact maruel@') % e)
569
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000571 """Updates the description and closes the issue."""
572 issue = int(self.GetIssue())
573 self.RpcServer().update_description(issue, self.description)
574 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000576 def SetFlag(self, flag, value):
577 """Patchset must match."""
578 if not self.GetPatchset():
579 DieWithError('The patchset needs to match. Send another patchset.')
580 try:
581 return self.RpcServer().set_flag(
582 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
583 except urllib2.HTTPError, e:
584 if e.code == 404:
585 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
586 if e.code == 403:
587 DieWithError(
588 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
589 'match?') % (self.GetIssue(), self.GetPatchset()))
590 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000592 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000593 """Returns an upload.RpcServer() to access this review's rietveld instance.
594 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000595 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000596 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
597 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000598 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599
600 def _IssueSetting(self):
601 """Return the git setting that stores this change's issue."""
602 return 'branch.%s.rietveldissue' % self.GetBranch()
603
604 def _PatchsetSetting(self):
605 """Return the git setting that stores this change's most recent patchset."""
606 return 'branch.%s.rietveldpatchset' % self.GetBranch()
607
608 def _RietveldServer(self):
609 """Returns the git setting that stores this change's rietveld server."""
610 return 'branch.%s.rietveldserver' % self.GetBranch()
611
612
613def GetCodereviewSettingsInteractively():
614 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000615 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 server = settings.GetDefaultServerUrl(error_ok=True)
617 prompt = 'Rietveld server (host[:port])'
618 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000619 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 if not server and not newserver:
621 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000622 if newserver:
623 newserver = gclient_utils.UpgradeToHttps(newserver)
624 if newserver != server:
625 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000627 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 prompt = caption
629 if initial:
630 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000631 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632 if new_val == 'x':
633 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000634 elif new_val:
635 if is_url:
636 new_val = gclient_utils.UpgradeToHttps(new_val)
637 if new_val != initial:
638 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000640 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000642 'tree-status-url', False)
643 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644
645 # TODO: configure a default branch to diff against, rather than this
646 # svn-based hackery.
647
648
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000649class ChangeDescription(object):
650 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000651 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000652 self.log_desc = log_desc
653 self.reviewers = reviewers
654 self.description = self.log_desc
655
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000656 def Prompt(self):
657 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000658# This will displayed on the codereview site.
659# The first line will also be used as the subject of the review.
660"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000661 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000662 if ('\nR=' not in self.description and
663 '\nTBR=' not in self.description and
664 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000665 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000666 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000667 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000668 if '\nTEST=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000669 content += '\nTEST='
670 content = content.rstrip('\n') + '\n'
671 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000672 if not content:
673 DieWithError('Running editor failed')
674 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000675 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000676 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000677 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000678
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000679 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000680 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000681 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000682 # Retrieves all reviewer lines
683 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000684 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000685 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000686 if reviewers:
687 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000688
689 def IsEmpty(self):
690 return not self.description
691
692
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000693def FindCodereviewSettingsFile(filename='codereview.settings'):
694 """Finds the given file starting in the cwd and going up.
695
696 Only looks up to the top of the repository unless an
697 'inherit-review-settings-ok' file exists in the root of the repository.
698 """
699 inherit_ok_file = 'inherit-review-settings-ok'
700 cwd = os.getcwd()
701 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
702 if os.path.isfile(os.path.join(root, inherit_ok_file)):
703 root = '/'
704 while True:
705 if filename in os.listdir(cwd):
706 if os.path.isfile(os.path.join(cwd, filename)):
707 return open(os.path.join(cwd, filename))
708 if cwd == root:
709 break
710 cwd = os.path.dirname(cwd)
711
712
713def LoadCodereviewSettingsFromFile(fileobj):
714 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000715 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 def SetProperty(name, setting, unset_error_ok=False):
718 fullname = 'rietveld.' + name
719 if setting in keyvals:
720 RunGit(['config', fullname, keyvals[setting]])
721 else:
722 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
723
724 SetProperty('server', 'CODE_REVIEW_SERVER')
725 # Only server setting is required. Other settings can be absent.
726 # In that case, we ignore errors raised during option deletion attempt.
727 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
728 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
729 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
730
ukai@chromium.orge8077812012-02-03 03:41:46 +0000731 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
732 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
733 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
736 #should be of the form
737 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
738 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
739 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
740 keyvals['ORIGIN_URL_CONFIG']])
741
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000743def DownloadHooks(force):
744 """downloads hooks
745
746 Args:
747 force: True to update hooks. False to install hooks if not present.
748 """
749 if not settings.GetIsGerrit():
750 return
751 server_url = settings.GetDefaultServerUrl()
752 src = '%s/tools/hooks/commit-msg' % server_url
753 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
754 if not os.access(dst, os.X_OK):
755 if os.path.exists(dst):
756 if not force:
757 return
758 os.remove(dst)
759 try:
760 urllib.urlretrieve(src, dst)
761 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
762 except Exception:
763 if os.path.exists(dst):
764 os.remove(dst)
765 DieWithError('\nFailed to download hooks from %s' % src)
766
767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768@usage('[repo root containing codereview.settings]')
769def CMDconfig(parser, args):
770 """edit configuration for this tree"""
771
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000772 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 if len(args) == 0:
774 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000775 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 return 0
777
778 url = args[0]
779 if not url.endswith('codereview.settings'):
780 url = os.path.join(url, 'codereview.settings')
781
782 # Load code review settings and download hooks (if available).
783 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000784 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 return 0
786
787
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000788def CMDbaseurl(parser, args):
789 """get or set base-url for this branch"""
790 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
791 branch = ShortBranchName(branchref)
792 _, args = parser.parse_args(args)
793 if not args:
794 print("Current base-url:")
795 return RunGit(['config', 'branch.%s.base-url' % branch],
796 error_ok=False).strip()
797 else:
798 print("Setting base-url to %s" % args[0])
799 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
800 error_ok=False).strip()
801
802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803def CMDstatus(parser, args):
804 """show status of changelists"""
805 parser.add_option('--field',
806 help='print only specific field (desc|id|patch|url)')
807 (options, args) = parser.parse_args(args)
808
809 # TODO: maybe make show_branches a flag if necessary.
810 show_branches = not options.field
811
812 if show_branches:
813 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
814 if branches:
815 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000816 changes = (Changelist(branchref=b) for b in branches.splitlines())
817 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
818 alignment = max(5, max(len(b) for b in branches))
819 for branch in sorted(branches):
820 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
822 cl = Changelist()
823 if options.field:
824 if options.field.startswith('desc'):
825 print cl.GetDescription()
826 elif options.field == 'id':
827 issueid = cl.GetIssue()
828 if issueid:
829 print issueid
830 elif options.field == 'patch':
831 patchset = cl.GetPatchset()
832 if patchset:
833 print patchset
834 elif options.field == 'url':
835 url = cl.GetIssueURL()
836 if url:
837 print url
838 else:
839 print
840 print 'Current branch:',
841 if not cl.GetIssue():
842 print 'no issue assigned.'
843 return 0
844 print cl.GetBranch()
845 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
846 print 'Issue description:'
847 print cl.GetDescription(pretty=True)
848 return 0
849
850
851@usage('[issue_number]')
852def CMDissue(parser, args):
853 """Set or display the current code review issue number.
854
855 Pass issue number 0 to clear the current issue.
856"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000857 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858
859 cl = Changelist()
860 if len(args) > 0:
861 try:
862 issue = int(args[0])
863 except ValueError:
864 DieWithError('Pass a number to set the issue or none to list it.\n'
865 'Maybe you want to run git cl status?')
866 cl.SetIssue(issue)
867 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
868 return 0
869
870
871def CreateDescriptionFromLog(args):
872 """Pulls out the commit log to use as a base for the CL description."""
873 log_args = []
874 if len(args) == 1 and not args[0].endswith('.'):
875 log_args = [args[0] + '..']
876 elif len(args) == 1 and args[0].endswith('...'):
877 log_args = [args[0][:-1]]
878 elif len(args) == 2:
879 log_args = [args[0] + '..' + args[1]]
880 else:
881 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000882 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883
884
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000885def ConvertToInteger(inputval):
886 """Convert a string to integer, but returns either an int or None."""
887 try:
888 return int(inputval)
889 except (TypeError, ValueError):
890 return None
891
892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893def CMDpresubmit(parser, args):
894 """run presubmit tests on the current changelist"""
895 parser.add_option('--upload', action='store_true',
896 help='Run upload hook instead of the push/dcommit hook')
897 (options, args) = parser.parse_args(args)
898
899 # Make sure index is up-to-date before running diff-index.
900 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
901 if RunGit(['diff-index', 'HEAD']):
902 # TODO(maruel): Is this really necessary?
903 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
904 return 1
905
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000906 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 if args:
908 base_branch = args[0]
909 else:
910 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000911 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000913 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000914 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000915 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000916 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917
918
ukai@chromium.orge8077812012-02-03 03:41:46 +0000919def GerritUpload(options, args, cl):
920 """upload the current branch to gerrit."""
921 # We assume the remote called "origin" is the one we want.
922 # It is probably not worthwhile to support different workflows.
923 remote = 'origin'
924 branch = 'master'
925 if options.target_branch:
926 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000928 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000929 if options.reviewers:
930 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000931 change_desc = ChangeDescription(log_desc, options.reviewers)
932 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000933 if change_desc.IsEmpty():
934 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935 return 1
936
ukai@chromium.orge8077812012-02-03 03:41:46 +0000937 receive_options = []
938 cc = cl.GetCCList().split(',')
939 if options.cc:
940 cc += options.cc.split(',')
941 cc = filter(None, cc)
942 if cc:
943 receive_options += ['--cc=' + email for email in cc]
944 if change_desc.reviewers:
945 reviewers = filter(None, change_desc.reviewers.split(','))
946 if reviewers:
947 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948
ukai@chromium.orge8077812012-02-03 03:41:46 +0000949 git_command = ['push']
950 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000951 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000952 ' '.join(receive_options))
953 git_command += [remote, 'HEAD:refs/for/' + branch]
954 RunGit(git_command)
955 # TODO(ukai): parse Change-Id: and set issue number?
956 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000957
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958
ukai@chromium.orge8077812012-02-03 03:41:46 +0000959def RietveldUpload(options, args, cl):
960 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 upload_args = ['--assume_yes'] # Don't ask about untracked files.
962 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 if options.emulate_svn_auto_props:
964 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965
966 change_desc = None
967
968 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +0000969 if options.title:
970 upload_args.extend(['--title', options.title])
971 elif options.message:
972 # TODO(rogerta): for now, the -m option will also set the --title option
973 # for upload.py. Soon this will be changed to set the --message option.
974 # Will wait until people are used to typing -t instead of -m.
975 upload_args.extend(['--title', options.message])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976 upload_args.extend(['--issue', cl.GetIssue()])
977 print ("This branch is associated with issue %s. "
978 "Adding patch to that issue." % cl.GetIssue())
979 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +0000980 if options.title:
981 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000982 message = options.message or CreateDescriptionFromLog(args)
983 change_desc = ChangeDescription(message, options.reviewers)
984 if not options.force:
985 change_desc.Prompt()
986 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000987
988 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989 print "Description is empty; aborting."
990 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000991
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000992 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000993 if change_desc.reviewers:
994 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000995 if options.send_mail:
996 if not change_desc.reviewers:
997 DieWithError("Must specify reviewers to send email.")
998 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000999 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001000 if cc:
1001 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002
1003 # Include the upstream repo's URL in the change -- this is useful for
1004 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001005 remote_url = cl.GetGitBaseUrlFromConfig()
1006 if not remote_url:
1007 if settings.GetIsGitSvn():
1008 # URL is dependent on the current directory.
1009 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1010 if data:
1011 keys = dict(line.split(': ', 1) for line in data.splitlines()
1012 if ': ' in line)
1013 remote_url = keys.get('URL', None)
1014 else:
1015 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1016 remote_url = (cl.GetRemoteUrl() + '@'
1017 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 if remote_url:
1019 upload_args.extend(['--base_url', remote_url])
1020
1021 try:
1022 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001023 except KeyboardInterrupt:
1024 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 except:
1026 # If we got an exception after the user typed a description for their
1027 # change, back up the description before re-raising.
1028 if change_desc:
1029 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1030 print '\nGot exception while uploading -- saving description to %s\n' \
1031 % backup_path
1032 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001033 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 backup_file.close()
1035 raise
1036
1037 if not cl.GetIssue():
1038 cl.SetIssue(issue)
1039 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001040
1041 if options.use_commit_queue:
1042 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 return 0
1044
1045
ukai@chromium.orge8077812012-02-03 03:41:46 +00001046@usage('[args to "git diff"]')
1047def CMDupload(parser, args):
1048 """upload the current changelist to codereview"""
1049 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1050 help='bypass upload presubmit hook')
1051 parser.add_option('-f', action='store_true', dest='force',
1052 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001053 parser.add_option('-m', dest='message', help='message for patchset')
1054 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001055 parser.add_option('-r', '--reviewers',
1056 help='reviewer email addresses')
1057 parser.add_option('--cc',
1058 help='cc email addresses')
1059 parser.add_option('--send-mail', action='store_true',
1060 help='send email to reviewer immediately')
1061 parser.add_option("--emulate_svn_auto_props", action="store_true",
1062 dest="emulate_svn_auto_props",
1063 help="Emulate Subversion's auto properties feature.")
1064 parser.add_option("--desc_from_logs", action="store_true",
1065 dest="from_logs",
1066 help="""Squashes git commit logs into change description and
1067 uses message as subject""")
1068 parser.add_option('-c', '--use-commit-queue', action='store_true',
1069 help='tell the commit queue to commit this patchset')
1070 if settings.GetIsGerrit():
1071 parser.add_option('--target_branch', dest='target_branch', default='master',
1072 help='target branch to upload')
1073 (options, args) = parser.parse_args(args)
1074
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001075 # Print warning if the user used the -m/--message argument. This will soon
1076 # change to -t/--title.
1077 if options.message:
1078 print >> sys.stderr, (
1079 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1080 'In the near future, -m or --message will send a message instead.\n'
1081 'See http://goo.gl/JGg0Z for details.\n')
1082
ukai@chromium.orge8077812012-02-03 03:41:46 +00001083 # Make sure index is up-to-date before running diff-index.
1084 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1085 if RunGit(['diff-index', 'HEAD']):
1086 print 'Cannot upload with a dirty tree. You must commit locally first.'
1087 return 1
1088
1089 cl = Changelist()
1090 if args:
1091 # TODO(ukai): is it ok for gerrit case?
1092 base_branch = args[0]
1093 else:
1094 # Default to diffing against the "upstream" branch.
1095 base_branch = cl.GetUpstreamBranch()
1096 args = [base_branch + "..."]
1097
1098 if not options.bypass_hooks:
1099 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1100 may_prompt=not options.force,
1101 verbose=options.verbose,
1102 author=None)
1103 if not hook_results.should_continue():
1104 return 1
1105 if not options.reviewers and hook_results.reviewers:
1106 options.reviewers = hook_results.reviewers
1107
1108 # --no-ext-diff is broken in some versions of Git, so try to work around
1109 # this by overriding the environment (but there is still a problem if the
1110 # git config key "diff.external" is used).
1111 env = os.environ.copy()
1112 if 'GIT_EXTERNAL_DIFF' in env:
1113 del env['GIT_EXTERNAL_DIFF']
1114 subprocess2.call(
1115 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1116
1117 if settings.GetIsGerrit():
1118 return GerritUpload(options, args, cl)
1119 return RietveldUpload(options, args, cl)
1120
1121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122def SendUpstream(parser, args, cmd):
1123 """Common code for CmdPush and CmdDCommit
1124
1125 Squashed commit into a single.
1126 Updates changelog with metadata (e.g. pointer to review).
1127 Pushes/dcommits the code upstream.
1128 Updates review and closes.
1129 """
1130 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1131 help='bypass upload presubmit hook')
1132 parser.add_option('-m', dest='message',
1133 help="override review description")
1134 parser.add_option('-f', action='store_true', dest='force',
1135 help="force yes to questions (don't prompt)")
1136 parser.add_option('-c', dest='contributor',
1137 help="external contributor for patch (appended to " +
1138 "description and used as author for git). Should be " +
1139 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 (options, args) = parser.parse_args(args)
1141 cl = Changelist()
1142
1143 if not args or cmd == 'push':
1144 # Default to merging against our best guess of the upstream branch.
1145 args = [cl.GetUpstreamBranch()]
1146
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001147 if options.contributor:
1148 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1149 print "Please provide contibutor as 'First Last <email@example.com>'"
1150 return 1
1151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 base_branch = args[0]
1153
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001154 # Make sure index is up-to-date before running diff-index.
1155 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 if RunGit(['diff-index', 'HEAD']):
1157 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1158 return 1
1159
1160 # This rev-list syntax means "show all commits not in my branch that
1161 # are in base_branch".
1162 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1163 base_branch]).splitlines()
1164 if upstream_commits:
1165 print ('Base branch "%s" has %d commits '
1166 'not in this branch.' % (base_branch, len(upstream_commits)))
1167 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1168 return 1
1169
1170 if cmd == 'dcommit':
1171 # This is the revision `svn dcommit` will commit on top of.
1172 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1173 '--pretty=format:%H'])
1174 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1175 if extra_commits:
1176 print ('This branch has %d additional commits not upstreamed yet.'
1177 % len(extra_commits.splitlines()))
1178 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1179 'before attempting to %s.' % (base_branch, cmd))
1180 return 1
1181
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001182 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001183 author = None
1184 if options.contributor:
1185 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001186 hook_results = cl.RunHook(
1187 committing=True,
1188 upstream_branch=base_branch,
1189 may_prompt=not options.force,
1190 verbose=options.verbose,
1191 author=author)
1192 if not hook_results.should_continue():
1193 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194
1195 if cmd == 'dcommit':
1196 # Check the tree status if the tree status URL is set.
1197 status = GetTreeStatus()
1198 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001199 print('The tree is closed. Please wait for it to reopen. Use '
1200 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 return 1
1202 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001203 print('Unable to determine tree status. Please verify manually and '
1204 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001205 else:
1206 breakpad.SendStack(
1207 'GitClHooksBypassedCommit',
1208 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001209 (cl.GetRietveldServer(), cl.GetIssue()),
1210 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211
1212 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001213 if not description and cl.GetIssue():
1214 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001216 if not description:
1217 print 'No description set.'
1218 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1219 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001221 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223
1224 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 description += "\nPatch from %s." % options.contributor
1226 print 'Description:', repr(description)
1227
1228 branches = [base_branch, cl.GetBranchRef()]
1229 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001230 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001231 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
1233 # We want to squash all this branch's commits into one commit with the
1234 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001235 # We do this by doing a "reset --soft" to the base branch (which keeps
1236 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 MERGE_BRANCH = 'git-cl-commit'
1238 # Delete the merge branch if it already exists.
1239 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1240 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1241 RunGit(['branch', '-D', MERGE_BRANCH])
1242
1243 # We might be in a directory that's present in this branch but not in the
1244 # trunk. Move up to the top of the tree so that git commands that expect a
1245 # valid CWD won't fail after we check out the merge branch.
1246 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1247 if rel_base_path:
1248 os.chdir(rel_base_path)
1249
1250 # Stuff our change into the merge branch.
1251 # We wrap in a try...finally block so if anything goes wrong,
1252 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001253 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001255 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1256 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 if options.contributor:
1258 RunGit(['commit', '--author', options.contributor, '-m', description])
1259 else:
1260 RunGit(['commit', '-m', description])
1261 if cmd == 'push':
1262 # push the merge branch.
1263 remote, branch = cl.FetchUpstreamTuple()
1264 retcode, output = RunGitWithCode(
1265 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1266 logging.debug(output)
1267 else:
1268 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001269 retcode, output = RunGitWithCode(['svn', 'dcommit',
1270 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 finally:
1272 # And then swap back to the original branch and clean up.
1273 RunGit(['checkout', '-q', cl.GetBranch()])
1274 RunGit(['branch', '-D', MERGE_BRANCH])
1275
1276 if cl.GetIssue():
1277 if cmd == 'dcommit' and 'Committed r' in output:
1278 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1279 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001280 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1281 for l in output.splitlines(False))
1282 match = filter(None, match)
1283 if len(match) != 1:
1284 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1285 output)
1286 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 else:
1288 return 1
1289 viewvc_url = settings.GetViewVCUrl()
1290 if viewvc_url and revision:
1291 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1292 print ('Closing issue '
1293 '(you may be prompted for your codereview password)...')
1294 cl.CloseIssue()
1295 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001296
1297 if retcode == 0:
1298 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1299 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001300 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 return 0
1303
1304
1305@usage('[upstream branch to apply against]')
1306def CMDdcommit(parser, args):
1307 """commit the current changelist via git-svn"""
1308 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001309 message = """This doesn't appear to be an SVN repository.
1310If your project has a git mirror with an upstream SVN master, you probably need
1311to run 'git svn init', see your project's git mirror documentation.
1312If your project has a true writeable upstream repository, you probably want
1313to run 'git cl push' instead.
1314Choose wisely, if you get this wrong, your commit might appear to succeed but
1315will instead be silently ignored."""
1316 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001317 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 return SendUpstream(parser, args, 'dcommit')
1319
1320
1321@usage('[upstream branch to apply against]')
1322def CMDpush(parser, args):
1323 """commit the current changelist via git"""
1324 if settings.GetIsGitSvn():
1325 print('This appears to be an SVN repository.')
1326 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001327 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 return SendUpstream(parser, args, 'push')
1329
1330
1331@usage('<patch url or issue id>')
1332def CMDpatch(parser, args):
1333 """patch in a code review"""
1334 parser.add_option('-b', dest='newbranch',
1335 help='create a new branch off trunk for the patch')
1336 parser.add_option('-f', action='store_true', dest='force',
1337 help='with -b, clobber any existing branch')
1338 parser.add_option('--reject', action='store_true', dest='reject',
1339 help='allow failed patches and spew .rej files')
1340 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1341 help="don't commit after patch applies")
1342 (options, args) = parser.parse_args(args)
1343 if len(args) != 1:
1344 parser.print_help()
1345 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001346 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001348 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001349 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001350
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001351 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001353 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001354 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001356 # Assume it's a URL to the patch. Default to https.
1357 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001358 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001359 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 DieWithError('Must pass an issue ID or full URL for '
1361 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001362 issue = match.group(1)
1363 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364
1365 if options.newbranch:
1366 if options.force:
1367 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001368 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 RunGit(['checkout', '-b', options.newbranch,
1370 Changelist().GetUpstreamBranch()])
1371
1372 # Switch up to the top-level directory, if necessary, in preparation for
1373 # applying the patch.
1374 top = RunGit(['rev-parse', '--show-cdup']).strip()
1375 if top:
1376 os.chdir(top)
1377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 # Git patches have a/ at the beginning of source paths. We strip that out
1379 # with a sed script rather than the -p flag to patch so we can feed either
1380 # Git or svn-style patches into the same apply command.
1381 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001382 try:
1383 patch_data = subprocess2.check_output(
1384 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1385 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386 DieWithError('Git patch mungling failed.')
1387 logging.info(patch_data)
1388 # We use "git apply" to apply the patch instead of "patch" so that we can
1389 # pick up file adds.
1390 # The --index flag means: also insert into the index (so we catch adds).
1391 cmd = ['git', 'apply', '--index', '-p0']
1392 if options.reject:
1393 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001394 try:
1395 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1396 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 DieWithError('Failed to apply the patch')
1398
1399 # If we had an issue, commit the current state and register the issue.
1400 if not options.nocommit:
1401 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1402 cl = Changelist()
1403 cl.SetIssue(issue)
1404 print "Committed patch."
1405 else:
1406 print "Patch applied to index."
1407 return 0
1408
1409
1410def CMDrebase(parser, args):
1411 """rebase current branch on top of svn repo"""
1412 # Provide a wrapper for git svn rebase to help avoid accidental
1413 # git svn dcommit.
1414 # It's the only command that doesn't use parser at all since we just defer
1415 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001416 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417
1418
1419def GetTreeStatus():
1420 """Fetches the tree status and returns either 'open', 'closed',
1421 'unknown' or 'unset'."""
1422 url = settings.GetTreeStatusUrl(error_ok=True)
1423 if url:
1424 status = urllib2.urlopen(url).read().lower()
1425 if status.find('closed') != -1 or status == '0':
1426 return 'closed'
1427 elif status.find('open') != -1 or status == '1':
1428 return 'open'
1429 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 return 'unset'
1431
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433def GetTreeStatusReason():
1434 """Fetches the tree status from a json url and returns the message
1435 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001436 url = settings.GetTreeStatusUrl()
1437 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 connection = urllib2.urlopen(json_url)
1439 status = json.loads(connection.read())
1440 connection.close()
1441 return status['message']
1442
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444def CMDtree(parser, args):
1445 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001446 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 status = GetTreeStatus()
1448 if 'unset' == status:
1449 print 'You must configure your tree status URL by running "git cl config".'
1450 return 2
1451
1452 print "The tree is %s" % status
1453 print
1454 print GetTreeStatusReason()
1455 if status != 'open':
1456 return 1
1457 return 0
1458
1459
1460def CMDupstream(parser, args):
1461 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001462 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001463 if args:
1464 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 cl = Changelist()
1466 print cl.GetUpstreamBranch()
1467 return 0
1468
1469
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001470def CMDset_commit(parser, args):
1471 """set the commit bit"""
1472 _, args = parser.parse_args(args)
1473 if args:
1474 parser.error('Unrecognized args: %s' % ' '.join(args))
1475 cl = Changelist()
1476 cl.SetFlag('commit', '1')
1477 return 0
1478
1479
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480def Command(name):
1481 return getattr(sys.modules[__name__], 'CMD' + name, None)
1482
1483
1484def CMDhelp(parser, args):
1485 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001486 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 if len(args) == 1:
1488 return main(args + ['--help'])
1489 parser.print_help()
1490 return 0
1491
1492
1493def GenUsage(parser, command):
1494 """Modify an OptParse object with the function's documentation."""
1495 obj = Command(command)
1496 more = getattr(obj, 'usage_more', '')
1497 if command == 'help':
1498 command = '<command>'
1499 else:
1500 # OptParser.description prefer nicely non-formatted strings.
1501 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1502 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1503
1504
1505def main(argv):
1506 """Doesn't parse the arguments here, just find the right subcommand to
1507 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001508 if sys.hexversion < 0x02060000:
1509 print >> sys.stderr, (
1510 '\nYour python version %s is unsupported, please upgrade.\n' %
1511 sys.version.split(' ', 1)[0])
1512 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001513 # Reload settings.
1514 global settings
1515 settings = Settings()
1516
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001517 # Do it late so all commands are listed.
1518 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1519 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1520 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1521
1522 # Create the option parse and add --verbose support.
1523 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001524 parser.add_option(
1525 '-v', '--verbose', action='count', default=0,
1526 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 old_parser_args = parser.parse_args
1528 def Parse(args):
1529 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001530 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001532 elif options.verbose:
1533 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534 else:
1535 logging.basicConfig(level=logging.WARNING)
1536 return options, args
1537 parser.parse_args = Parse
1538
1539 if argv:
1540 command = Command(argv[0])
1541 if command:
1542 # "fix" the usage and the description now that we know the subcommand.
1543 GenUsage(parser, argv[0])
1544 try:
1545 return command(parser, argv[1:])
1546 except urllib2.HTTPError, e:
1547 if e.code != 500:
1548 raise
1549 DieWithError(
1550 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1551 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1552
1553 # Not a known command. Default to help.
1554 GenUsage(parser, 'help')
1555 return CMDhelp(parser, argv)
1556
1557
1558if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001559 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560 sys.exit(main(sys.argv[1:]))