blob: d2f4b31f3fa320bac98c347d7bc67c32d7e3af18 [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:
cmp@chromium.orgdb5a0de2012-06-04 17:17:34 +00001022 # upload uses '-C' by default when generating the diff for upload.
1023 # Add another '-C' to trigger --find-copies-harder.
1024 issue, patchset = upload.RealMain(['upload'] + upload_args + args +
1025 ['--', '-C'])
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001026 except KeyboardInterrupt:
1027 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028 except:
1029 # If we got an exception after the user typed a description for their
1030 # change, back up the description before re-raising.
1031 if change_desc:
1032 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1033 print '\nGot exception while uploading -- saving description to %s\n' \
1034 % backup_path
1035 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001036 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 backup_file.close()
1038 raise
1039
1040 if not cl.GetIssue():
1041 cl.SetIssue(issue)
1042 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001043
1044 if options.use_commit_queue:
1045 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 return 0
1047
1048
ukai@chromium.orge8077812012-02-03 03:41:46 +00001049@usage('[args to "git diff"]')
1050def CMDupload(parser, args):
1051 """upload the current changelist to codereview"""
1052 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1053 help='bypass upload presubmit hook')
1054 parser.add_option('-f', action='store_true', dest='force',
1055 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001056 parser.add_option('-m', dest='message', help='message for patchset')
1057 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001058 parser.add_option('-r', '--reviewers',
1059 help='reviewer email addresses')
1060 parser.add_option('--cc',
1061 help='cc email addresses')
1062 parser.add_option('--send-mail', action='store_true',
1063 help='send email to reviewer immediately')
1064 parser.add_option("--emulate_svn_auto_props", action="store_true",
1065 dest="emulate_svn_auto_props",
1066 help="Emulate Subversion's auto properties feature.")
1067 parser.add_option("--desc_from_logs", action="store_true",
1068 dest="from_logs",
1069 help="""Squashes git commit logs into change description and
1070 uses message as subject""")
1071 parser.add_option('-c', '--use-commit-queue', action='store_true',
1072 help='tell the commit queue to commit this patchset')
1073 if settings.GetIsGerrit():
1074 parser.add_option('--target_branch', dest='target_branch', default='master',
1075 help='target branch to upload')
1076 (options, args) = parser.parse_args(args)
1077
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001078 # Print warning if the user used the -m/--message argument. This will soon
1079 # change to -t/--title.
1080 if options.message:
1081 print >> sys.stderr, (
1082 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1083 'In the near future, -m or --message will send a message instead.\n'
1084 'See http://goo.gl/JGg0Z for details.\n')
1085
ukai@chromium.orge8077812012-02-03 03:41:46 +00001086 # Make sure index is up-to-date before running diff-index.
1087 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1088 if RunGit(['diff-index', 'HEAD']):
1089 print 'Cannot upload with a dirty tree. You must commit locally first.'
1090 return 1
1091
1092 cl = Changelist()
1093 if args:
1094 # TODO(ukai): is it ok for gerrit case?
1095 base_branch = args[0]
1096 else:
1097 # Default to diffing against the "upstream" branch.
1098 base_branch = cl.GetUpstreamBranch()
1099 args = [base_branch + "..."]
1100
1101 if not options.bypass_hooks:
1102 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1103 may_prompt=not options.force,
1104 verbose=options.verbose,
1105 author=None)
1106 if not hook_results.should_continue():
1107 return 1
1108 if not options.reviewers and hook_results.reviewers:
1109 options.reviewers = hook_results.reviewers
1110
1111 # --no-ext-diff is broken in some versions of Git, so try to work around
1112 # this by overriding the environment (but there is still a problem if the
1113 # git config key "diff.external" is used).
1114 env = os.environ.copy()
1115 if 'GIT_EXTERNAL_DIFF' in env:
1116 del env['GIT_EXTERNAL_DIFF']
1117 subprocess2.call(
cmp@chromium.orgdb5a0de2012-06-04 17:17:34 +00001118 ['git', 'diff', '--no-ext-diff', '--stat', '-C', '-C'] + args, env=env)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001119
1120 if settings.GetIsGerrit():
1121 return GerritUpload(options, args, cl)
1122 return RietveldUpload(options, args, cl)
1123
1124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125def SendUpstream(parser, args, cmd):
1126 """Common code for CmdPush and CmdDCommit
1127
1128 Squashed commit into a single.
1129 Updates changelog with metadata (e.g. pointer to review).
1130 Pushes/dcommits the code upstream.
1131 Updates review and closes.
1132 """
1133 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1134 help='bypass upload presubmit hook')
1135 parser.add_option('-m', dest='message',
1136 help="override review description")
1137 parser.add_option('-f', action='store_true', dest='force',
1138 help="force yes to questions (don't prompt)")
1139 parser.add_option('-c', dest='contributor',
1140 help="external contributor for patch (appended to " +
1141 "description and used as author for git). Should be " +
1142 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 (options, args) = parser.parse_args(args)
1144 cl = Changelist()
1145
1146 if not args or cmd == 'push':
1147 # Default to merging against our best guess of the upstream branch.
1148 args = [cl.GetUpstreamBranch()]
1149
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001150 if options.contributor:
1151 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1152 print "Please provide contibutor as 'First Last <email@example.com>'"
1153 return 1
1154
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 base_branch = args[0]
1156
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001157 # Make sure index is up-to-date before running diff-index.
1158 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 if RunGit(['diff-index', 'HEAD']):
1160 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1161 return 1
1162
1163 # This rev-list syntax means "show all commits not in my branch that
1164 # are in base_branch".
1165 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1166 base_branch]).splitlines()
1167 if upstream_commits:
1168 print ('Base branch "%s" has %d commits '
1169 'not in this branch.' % (base_branch, len(upstream_commits)))
1170 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1171 return 1
1172
1173 if cmd == 'dcommit':
1174 # This is the revision `svn dcommit` will commit on top of.
1175 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1176 '--pretty=format:%H'])
1177 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1178 if extra_commits:
1179 print ('This branch has %d additional commits not upstreamed yet.'
1180 % len(extra_commits.splitlines()))
1181 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1182 'before attempting to %s.' % (base_branch, cmd))
1183 return 1
1184
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001185 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001186 author = None
1187 if options.contributor:
1188 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001189 hook_results = cl.RunHook(
1190 committing=True,
1191 upstream_branch=base_branch,
1192 may_prompt=not options.force,
1193 verbose=options.verbose,
1194 author=author)
1195 if not hook_results.should_continue():
1196 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197
1198 if cmd == 'dcommit':
1199 # Check the tree status if the tree status URL is set.
1200 status = GetTreeStatus()
1201 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001202 print('The tree is closed. Please wait for it to reopen. Use '
1203 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 return 1
1205 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001206 print('Unable to determine tree status. Please verify manually and '
1207 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001208 else:
1209 breakpad.SendStack(
1210 'GitClHooksBypassedCommit',
1211 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001212 (cl.GetRietveldServer(), cl.GetIssue()),
1213 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214
1215 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001216 if not description and cl.GetIssue():
1217 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001219 if not description:
1220 print 'No description set.'
1221 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1222 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001224 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226
1227 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 description += "\nPatch from %s." % options.contributor
1229 print 'Description:', repr(description)
1230
1231 branches = [base_branch, cl.GetBranchRef()]
1232 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001233 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001234 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
1236 # We want to squash all this branch's commits into one commit with the
1237 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001238 # We do this by doing a "reset --soft" to the base branch (which keeps
1239 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 MERGE_BRANCH = 'git-cl-commit'
1241 # Delete the merge branch if it already exists.
1242 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1243 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1244 RunGit(['branch', '-D', MERGE_BRANCH])
1245
1246 # We might be in a directory that's present in this branch but not in the
1247 # trunk. Move up to the top of the tree so that git commands that expect a
1248 # valid CWD won't fail after we check out the merge branch.
1249 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1250 if rel_base_path:
1251 os.chdir(rel_base_path)
1252
1253 # Stuff our change into the merge branch.
1254 # We wrap in a try...finally block so if anything goes wrong,
1255 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001256 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001258 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1259 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 if options.contributor:
1261 RunGit(['commit', '--author', options.contributor, '-m', description])
1262 else:
1263 RunGit(['commit', '-m', description])
1264 if cmd == 'push':
1265 # push the merge branch.
1266 remote, branch = cl.FetchUpstreamTuple()
1267 retcode, output = RunGitWithCode(
1268 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1269 logging.debug(output)
1270 else:
1271 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001272 retcode, output = RunGitWithCode(['svn', 'dcommit',
1273 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 finally:
1275 # And then swap back to the original branch and clean up.
1276 RunGit(['checkout', '-q', cl.GetBranch()])
1277 RunGit(['branch', '-D', MERGE_BRANCH])
1278
1279 if cl.GetIssue():
1280 if cmd == 'dcommit' and 'Committed r' in output:
1281 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1282 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001283 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1284 for l in output.splitlines(False))
1285 match = filter(None, match)
1286 if len(match) != 1:
1287 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1288 output)
1289 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 else:
1291 return 1
1292 viewvc_url = settings.GetViewVCUrl()
1293 if viewvc_url and revision:
1294 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1295 print ('Closing issue '
1296 '(you may be prompted for your codereview password)...')
1297 cl.CloseIssue()
1298 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001299
1300 if retcode == 0:
1301 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1302 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001303 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 return 0
1306
1307
1308@usage('[upstream branch to apply against]')
1309def CMDdcommit(parser, args):
1310 """commit the current changelist via git-svn"""
1311 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001312 message = """This doesn't appear to be an SVN repository.
1313If your project has a git mirror with an upstream SVN master, you probably need
1314to run 'git svn init', see your project's git mirror documentation.
1315If your project has a true writeable upstream repository, you probably want
1316to run 'git cl push' instead.
1317Choose wisely, if you get this wrong, your commit might appear to succeed but
1318will instead be silently ignored."""
1319 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001320 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 return SendUpstream(parser, args, 'dcommit')
1322
1323
1324@usage('[upstream branch to apply against]')
1325def CMDpush(parser, args):
1326 """commit the current changelist via git"""
1327 if settings.GetIsGitSvn():
1328 print('This appears to be an SVN repository.')
1329 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001330 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 return SendUpstream(parser, args, 'push')
1332
1333
1334@usage('<patch url or issue id>')
1335def CMDpatch(parser, args):
1336 """patch in a code review"""
1337 parser.add_option('-b', dest='newbranch',
1338 help='create a new branch off trunk for the patch')
1339 parser.add_option('-f', action='store_true', dest='force',
1340 help='with -b, clobber any existing branch')
1341 parser.add_option('--reject', action='store_true', dest='reject',
1342 help='allow failed patches and spew .rej files')
1343 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1344 help="don't commit after patch applies")
1345 (options, args) = parser.parse_args(args)
1346 if len(args) != 1:
1347 parser.print_help()
1348 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001349 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001351 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001352 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001353
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001354 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001356 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001357 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001359 # Assume it's a URL to the patch. Default to https.
1360 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001361 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001362 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363 DieWithError('Must pass an issue ID or full URL for '
1364 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001365 issue = match.group(1)
1366 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367
1368 if options.newbranch:
1369 if options.force:
1370 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001371 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 RunGit(['checkout', '-b', options.newbranch,
1373 Changelist().GetUpstreamBranch()])
1374
1375 # Switch up to the top-level directory, if necessary, in preparation for
1376 # applying the patch.
1377 top = RunGit(['rev-parse', '--show-cdup']).strip()
1378 if top:
1379 os.chdir(top)
1380
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 # Git patches have a/ at the beginning of source paths. We strip that out
1382 # with a sed script rather than the -p flag to patch so we can feed either
1383 # Git or svn-style patches into the same apply command.
1384 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001385 try:
1386 patch_data = subprocess2.check_output(
1387 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1388 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 DieWithError('Git patch mungling failed.')
1390 logging.info(patch_data)
1391 # We use "git apply" to apply the patch instead of "patch" so that we can
1392 # pick up file adds.
1393 # The --index flag means: also insert into the index (so we catch adds).
1394 cmd = ['git', 'apply', '--index', '-p0']
1395 if options.reject:
1396 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001397 try:
1398 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1399 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 DieWithError('Failed to apply the patch')
1401
1402 # If we had an issue, commit the current state and register the issue.
1403 if not options.nocommit:
1404 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1405 cl = Changelist()
1406 cl.SetIssue(issue)
1407 print "Committed patch."
1408 else:
1409 print "Patch applied to index."
1410 return 0
1411
1412
1413def CMDrebase(parser, args):
1414 """rebase current branch on top of svn repo"""
1415 # Provide a wrapper for git svn rebase to help avoid accidental
1416 # git svn dcommit.
1417 # It's the only command that doesn't use parser at all since we just defer
1418 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001419 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420
1421
1422def GetTreeStatus():
1423 """Fetches the tree status and returns either 'open', 'closed',
1424 'unknown' or 'unset'."""
1425 url = settings.GetTreeStatusUrl(error_ok=True)
1426 if url:
1427 status = urllib2.urlopen(url).read().lower()
1428 if status.find('closed') != -1 or status == '0':
1429 return 'closed'
1430 elif status.find('open') != -1 or status == '1':
1431 return 'open'
1432 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 return 'unset'
1434
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001435
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436def GetTreeStatusReason():
1437 """Fetches the tree status from a json url and returns the message
1438 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001439 url = settings.GetTreeStatusUrl()
1440 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 connection = urllib2.urlopen(json_url)
1442 status = json.loads(connection.read())
1443 connection.close()
1444 return status['message']
1445
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447def CMDtree(parser, args):
1448 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001449 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 status = GetTreeStatus()
1451 if 'unset' == status:
1452 print 'You must configure your tree status URL by running "git cl config".'
1453 return 2
1454
1455 print "The tree is %s" % status
1456 print
1457 print GetTreeStatusReason()
1458 if status != 'open':
1459 return 1
1460 return 0
1461
1462
1463def CMDupstream(parser, args):
1464 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001465 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001466 if args:
1467 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 cl = Changelist()
1469 print cl.GetUpstreamBranch()
1470 return 0
1471
1472
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001473def CMDset_commit(parser, args):
1474 """set the commit bit"""
1475 _, args = parser.parse_args(args)
1476 if args:
1477 parser.error('Unrecognized args: %s' % ' '.join(args))
1478 cl = Changelist()
1479 cl.SetFlag('commit', '1')
1480 return 0
1481
1482
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483def Command(name):
1484 return getattr(sys.modules[__name__], 'CMD' + name, None)
1485
1486
1487def CMDhelp(parser, args):
1488 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001489 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490 if len(args) == 1:
1491 return main(args + ['--help'])
1492 parser.print_help()
1493 return 0
1494
1495
1496def GenUsage(parser, command):
1497 """Modify an OptParse object with the function's documentation."""
1498 obj = Command(command)
1499 more = getattr(obj, 'usage_more', '')
1500 if command == 'help':
1501 command = '<command>'
1502 else:
1503 # OptParser.description prefer nicely non-formatted strings.
1504 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1505 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1506
1507
1508def main(argv):
1509 """Doesn't parse the arguments here, just find the right subcommand to
1510 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001511 if sys.hexversion < 0x02060000:
1512 print >> sys.stderr, (
1513 '\nYour python version %s is unsupported, please upgrade.\n' %
1514 sys.version.split(' ', 1)[0])
1515 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001516 # Reload settings.
1517 global settings
1518 settings = Settings()
1519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520 # Do it late so all commands are listed.
1521 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1522 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1523 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1524
1525 # Create the option parse and add --verbose support.
1526 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001527 parser.add_option(
1528 '-v', '--verbose', action='count', default=0,
1529 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530 old_parser_args = parser.parse_args
1531 def Parse(args):
1532 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001533 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001535 elif options.verbose:
1536 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537 else:
1538 logging.basicConfig(level=logging.WARNING)
1539 return options, args
1540 parser.parse_args = Parse
1541
1542 if argv:
1543 command = Command(argv[0])
1544 if command:
1545 # "fix" the usage and the description now that we know the subcommand.
1546 GenUsage(parser, argv[0])
1547 try:
1548 return command(parser, argv[1:])
1549 except urllib2.HTTPError, e:
1550 if e.code != 500:
1551 raise
1552 DieWithError(
1553 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1554 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1555
1556 # Not a known command. Default to help.
1557 GenUsage(parser, 'help')
1558 return CMDhelp(parser, argv)
1559
1560
1561if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001562 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 sys.exit(main(sys.argv[1:]))