blob: 194a817846dec236ed72f843b0361ec47bc03261 [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
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000871def CMDcomments(parser, args):
872 """show review comments of the current changelist"""
873 (_, args) = parser.parse_args(args)
874 if args:
875 parser.error('Unsupported argument: %s' % args)
876
877 cl = Changelist()
878 if cl.GetIssue():
879 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
880 for message in sorted(data['messages'], key=lambda x: x['date']):
881 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
882 if message['text'].strip():
883 print '\n'.join(' ' + l for l in message['text'].splitlines())
884 return 0
885
886
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887def CreateDescriptionFromLog(args):
888 """Pulls out the commit log to use as a base for the CL description."""
889 log_args = []
890 if len(args) == 1 and not args[0].endswith('.'):
891 log_args = [args[0] + '..']
892 elif len(args) == 1 and args[0].endswith('...'):
893 log_args = [args[0][:-1]]
894 elif len(args) == 2:
895 log_args = [args[0] + '..' + args[1]]
896 else:
897 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000898 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899
900
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000901def ConvertToInteger(inputval):
902 """Convert a string to integer, but returns either an int or None."""
903 try:
904 return int(inputval)
905 except (TypeError, ValueError):
906 return None
907
908
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909def CMDpresubmit(parser, args):
910 """run presubmit tests on the current changelist"""
911 parser.add_option('--upload', action='store_true',
912 help='Run upload hook instead of the push/dcommit hook')
913 (options, args) = parser.parse_args(args)
914
915 # Make sure index is up-to-date before running diff-index.
916 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
917 if RunGit(['diff-index', 'HEAD']):
918 # TODO(maruel): Is this really necessary?
919 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
920 return 1
921
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000922 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923 if args:
924 base_branch = args[0]
925 else:
926 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000927 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000929 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000930 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000931 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000932 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933
934
ukai@chromium.orge8077812012-02-03 03:41:46 +0000935def GerritUpload(options, args, cl):
936 """upload the current branch to gerrit."""
937 # We assume the remote called "origin" is the one we want.
938 # It is probably not worthwhile to support different workflows.
939 remote = 'origin'
940 branch = 'master'
941 if options.target_branch:
942 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000944 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000945 if options.reviewers:
946 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000947 change_desc = ChangeDescription(log_desc, options.reviewers)
948 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000949 if change_desc.IsEmpty():
950 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951 return 1
952
ukai@chromium.orge8077812012-02-03 03:41:46 +0000953 receive_options = []
954 cc = cl.GetCCList().split(',')
955 if options.cc:
956 cc += options.cc.split(',')
957 cc = filter(None, cc)
958 if cc:
959 receive_options += ['--cc=' + email for email in cc]
960 if change_desc.reviewers:
961 reviewers = filter(None, change_desc.reviewers.split(','))
962 if reviewers:
963 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000964
ukai@chromium.orge8077812012-02-03 03:41:46 +0000965 git_command = ['push']
966 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000967 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000968 ' '.join(receive_options))
969 git_command += [remote, 'HEAD:refs/for/' + branch]
970 RunGit(git_command)
971 # TODO(ukai): parse Change-Id: and set issue number?
972 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974
ukai@chromium.orge8077812012-02-03 03:41:46 +0000975def RietveldUpload(options, args, cl):
976 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 upload_args = ['--assume_yes'] # Don't ask about untracked files.
978 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979 if options.emulate_svn_auto_props:
980 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981
982 change_desc = None
983
984 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +0000985 if options.title:
986 upload_args.extend(['--title', options.title])
987 elif options.message:
988 # TODO(rogerta): for now, the -m option will also set the --title option
989 # for upload.py. Soon this will be changed to set the --message option.
990 # Will wait until people are used to typing -t instead of -m.
991 upload_args.extend(['--title', options.message])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992 upload_args.extend(['--issue', cl.GetIssue()])
993 print ("This branch is associated with issue %s. "
994 "Adding patch to that issue." % cl.GetIssue())
995 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +0000996 if options.title:
997 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000998 message = options.message or CreateDescriptionFromLog(args)
999 change_desc = ChangeDescription(message, options.reviewers)
1000 if not options.force:
1001 change_desc.Prompt()
1002 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001003
1004 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 print "Description is empty; aborting."
1006 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001007
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001008 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001009 if change_desc.reviewers:
1010 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001011 if options.send_mail:
1012 if not change_desc.reviewers:
1013 DieWithError("Must specify reviewers to send email.")
1014 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001015 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001016 if cc:
1017 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018
1019 # Include the upstream repo's URL in the change -- this is useful for
1020 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001021 remote_url = cl.GetGitBaseUrlFromConfig()
1022 if not remote_url:
1023 if settings.GetIsGitSvn():
1024 # URL is dependent on the current directory.
1025 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1026 if data:
1027 keys = dict(line.split(': ', 1) for line in data.splitlines()
1028 if ': ' in line)
1029 remote_url = keys.get('URL', None)
1030 else:
1031 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1032 remote_url = (cl.GetRemoteUrl() + '@'
1033 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 if remote_url:
1035 upload_args.extend(['--base_url', remote_url])
1036
1037 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001038 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001039 except KeyboardInterrupt:
1040 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041 except:
1042 # If we got an exception after the user typed a description for their
1043 # change, back up the description before re-raising.
1044 if change_desc:
1045 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1046 print '\nGot exception while uploading -- saving description to %s\n' \
1047 % backup_path
1048 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001049 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 backup_file.close()
1051 raise
1052
1053 if not cl.GetIssue():
1054 cl.SetIssue(issue)
1055 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001056
1057 if options.use_commit_queue:
1058 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 return 0
1060
1061
ukai@chromium.orge8077812012-02-03 03:41:46 +00001062@usage('[args to "git diff"]')
1063def CMDupload(parser, args):
1064 """upload the current changelist to codereview"""
1065 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1066 help='bypass upload presubmit hook')
1067 parser.add_option('-f', action='store_true', dest='force',
1068 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001069 parser.add_option('-m', dest='message', help='message for patchset')
1070 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001071 parser.add_option('-r', '--reviewers',
1072 help='reviewer email addresses')
1073 parser.add_option('--cc',
1074 help='cc email addresses')
1075 parser.add_option('--send-mail', action='store_true',
1076 help='send email to reviewer immediately')
1077 parser.add_option("--emulate_svn_auto_props", action="store_true",
1078 dest="emulate_svn_auto_props",
1079 help="Emulate Subversion's auto properties feature.")
1080 parser.add_option("--desc_from_logs", action="store_true",
1081 dest="from_logs",
1082 help="""Squashes git commit logs into change description and
1083 uses message as subject""")
1084 parser.add_option('-c', '--use-commit-queue', action='store_true',
1085 help='tell the commit queue to commit this patchset')
1086 if settings.GetIsGerrit():
1087 parser.add_option('--target_branch', dest='target_branch', default='master',
1088 help='target branch to upload')
1089 (options, args) = parser.parse_args(args)
1090
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001091 # Print warning if the user used the -m/--message argument. This will soon
1092 # change to -t/--title.
1093 if options.message:
1094 print >> sys.stderr, (
1095 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1096 'In the near future, -m or --message will send a message instead.\n'
1097 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001098
ukai@chromium.orge8077812012-02-03 03:41:46 +00001099 # Make sure index is up-to-date before running diff-index.
1100 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1101 if RunGit(['diff-index', 'HEAD']):
1102 print 'Cannot upload with a dirty tree. You must commit locally first.'
1103 return 1
1104
1105 cl = Changelist()
1106 if args:
1107 # TODO(ukai): is it ok for gerrit case?
1108 base_branch = args[0]
1109 else:
1110 # Default to diffing against the "upstream" branch.
1111 base_branch = cl.GetUpstreamBranch()
1112 args = [base_branch + "..."]
1113
1114 if not options.bypass_hooks:
1115 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1116 may_prompt=not options.force,
1117 verbose=options.verbose,
1118 author=None)
1119 if not hook_results.should_continue():
1120 return 1
1121 if not options.reviewers and hook_results.reviewers:
1122 options.reviewers = hook_results.reviewers
1123
1124 # --no-ext-diff is broken in some versions of Git, so try to work around
1125 # this by overriding the environment (but there is still a problem if the
1126 # git config key "diff.external" is used).
1127 env = os.environ.copy()
1128 if 'GIT_EXTERNAL_DIFF' in env:
1129 del env['GIT_EXTERNAL_DIFF']
1130 subprocess2.call(
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001131 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001132
1133 if settings.GetIsGerrit():
1134 return GerritUpload(options, args, cl)
1135 return RietveldUpload(options, args, cl)
1136
1137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138def SendUpstream(parser, args, cmd):
1139 """Common code for CmdPush and CmdDCommit
1140
1141 Squashed commit into a single.
1142 Updates changelog with metadata (e.g. pointer to review).
1143 Pushes/dcommits the code upstream.
1144 Updates review and closes.
1145 """
1146 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1147 help='bypass upload presubmit hook')
1148 parser.add_option('-m', dest='message',
1149 help="override review description")
1150 parser.add_option('-f', action='store_true', dest='force',
1151 help="force yes to questions (don't prompt)")
1152 parser.add_option('-c', dest='contributor',
1153 help="external contributor for patch (appended to " +
1154 "description and used as author for git). Should be " +
1155 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 (options, args) = parser.parse_args(args)
1157 cl = Changelist()
1158
1159 if not args or cmd == 'push':
1160 # Default to merging against our best guess of the upstream branch.
1161 args = [cl.GetUpstreamBranch()]
1162
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001163 if options.contributor:
1164 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1165 print "Please provide contibutor as 'First Last <email@example.com>'"
1166 return 1
1167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 base_branch = args[0]
1169
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001170 # Make sure index is up-to-date before running diff-index.
1171 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 if RunGit(['diff-index', 'HEAD']):
1173 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1174 return 1
1175
1176 # This rev-list syntax means "show all commits not in my branch that
1177 # are in base_branch".
1178 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1179 base_branch]).splitlines()
1180 if upstream_commits:
1181 print ('Base branch "%s" has %d commits '
1182 'not in this branch.' % (base_branch, len(upstream_commits)))
1183 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1184 return 1
1185
1186 if cmd == 'dcommit':
1187 # This is the revision `svn dcommit` will commit on top of.
1188 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1189 '--pretty=format:%H'])
1190 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1191 if extra_commits:
1192 print ('This branch has %d additional commits not upstreamed yet.'
1193 % len(extra_commits.splitlines()))
1194 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1195 'before attempting to %s.' % (base_branch, cmd))
1196 return 1
1197
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001198 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001199 author = None
1200 if options.contributor:
1201 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001202 hook_results = cl.RunHook(
1203 committing=True,
1204 upstream_branch=base_branch,
1205 may_prompt=not options.force,
1206 verbose=options.verbose,
1207 author=author)
1208 if not hook_results.should_continue():
1209 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210
1211 if cmd == 'dcommit':
1212 # Check the tree status if the tree status URL is set.
1213 status = GetTreeStatus()
1214 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001215 print('The tree is closed. Please wait for it to reopen. Use '
1216 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 return 1
1218 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001219 print('Unable to determine tree status. Please verify manually and '
1220 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001221 else:
1222 breakpad.SendStack(
1223 'GitClHooksBypassedCommit',
1224 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001225 (cl.GetRietveldServer(), cl.GetIssue()),
1226 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
1228 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001229 if not description and cl.GetIssue():
1230 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001232 if not description:
1233 print 'No description set.'
1234 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1235 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001237 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
1240 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 description += "\nPatch from %s." % options.contributor
1242 print 'Description:', repr(description)
1243
1244 branches = [base_branch, cl.GetBranchRef()]
1245 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001246 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001247 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248
1249 # We want to squash all this branch's commits into one commit with the
1250 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001251 # We do this by doing a "reset --soft" to the base branch (which keeps
1252 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 MERGE_BRANCH = 'git-cl-commit'
1254 # Delete the merge branch if it already exists.
1255 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1256 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1257 RunGit(['branch', '-D', MERGE_BRANCH])
1258
1259 # We might be in a directory that's present in this branch but not in the
1260 # trunk. Move up to the top of the tree so that git commands that expect a
1261 # valid CWD won't fail after we check out the merge branch.
1262 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1263 if rel_base_path:
1264 os.chdir(rel_base_path)
1265
1266 # Stuff our change into the merge branch.
1267 # We wrap in a try...finally block so if anything goes wrong,
1268 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001269 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001271 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1272 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 if options.contributor:
1274 RunGit(['commit', '--author', options.contributor, '-m', description])
1275 else:
1276 RunGit(['commit', '-m', description])
1277 if cmd == 'push':
1278 # push the merge branch.
1279 remote, branch = cl.FetchUpstreamTuple()
1280 retcode, output = RunGitWithCode(
1281 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1282 logging.debug(output)
1283 else:
1284 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001285 retcode, output = RunGitWithCode(['svn', 'dcommit',
1286 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 finally:
1288 # And then swap back to the original branch and clean up.
1289 RunGit(['checkout', '-q', cl.GetBranch()])
1290 RunGit(['branch', '-D', MERGE_BRANCH])
1291
1292 if cl.GetIssue():
1293 if cmd == 'dcommit' and 'Committed r' in output:
1294 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1295 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001296 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1297 for l in output.splitlines(False))
1298 match = filter(None, match)
1299 if len(match) != 1:
1300 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1301 output)
1302 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 else:
1304 return 1
1305 viewvc_url = settings.GetViewVCUrl()
1306 if viewvc_url and revision:
1307 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1308 print ('Closing issue '
1309 '(you may be prompted for your codereview password)...')
1310 cl.CloseIssue()
1311 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001312
1313 if retcode == 0:
1314 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1315 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001316 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001317
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 return 0
1319
1320
1321@usage('[upstream branch to apply against]')
1322def CMDdcommit(parser, args):
1323 """commit the current changelist via git-svn"""
1324 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001325 message = """This doesn't appear to be an SVN repository.
1326If your project has a git mirror with an upstream SVN master, you probably need
1327to run 'git svn init', see your project's git mirror documentation.
1328If your project has a true writeable upstream repository, you probably want
1329to run 'git cl push' instead.
1330Choose wisely, if you get this wrong, your commit might appear to succeed but
1331will instead be silently ignored."""
1332 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001333 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 return SendUpstream(parser, args, 'dcommit')
1335
1336
1337@usage('[upstream branch to apply against]')
1338def CMDpush(parser, args):
1339 """commit the current changelist via git"""
1340 if settings.GetIsGitSvn():
1341 print('This appears to be an SVN repository.')
1342 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001343 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 return SendUpstream(parser, args, 'push')
1345
1346
1347@usage('<patch url or issue id>')
1348def CMDpatch(parser, args):
1349 """patch in a code review"""
1350 parser.add_option('-b', dest='newbranch',
1351 help='create a new branch off trunk for the patch')
1352 parser.add_option('-f', action='store_true', dest='force',
1353 help='with -b, clobber any existing branch')
1354 parser.add_option('--reject', action='store_true', dest='reject',
1355 help='allow failed patches and spew .rej files')
1356 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1357 help="don't commit after patch applies")
1358 (options, args) = parser.parse_args(args)
1359 if len(args) != 1:
1360 parser.print_help()
1361 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001362 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001364 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001365 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001366
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001367 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001369 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001370 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001372 # Assume it's a URL to the patch. Default to https.
1373 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001374 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001375 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 DieWithError('Must pass an issue ID or full URL for '
1377 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001378 issue = match.group(1)
1379 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
1381 if options.newbranch:
1382 if options.force:
1383 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001384 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385 RunGit(['checkout', '-b', options.newbranch,
1386 Changelist().GetUpstreamBranch()])
1387
1388 # Switch up to the top-level directory, if necessary, in preparation for
1389 # applying the patch.
1390 top = RunGit(['rev-parse', '--show-cdup']).strip()
1391 if top:
1392 os.chdir(top)
1393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 # Git patches have a/ at the beginning of source paths. We strip that out
1395 # with a sed script rather than the -p flag to patch so we can feed either
1396 # Git or svn-style patches into the same apply command.
1397 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001398 try:
1399 patch_data = subprocess2.check_output(
1400 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1401 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 DieWithError('Git patch mungling failed.')
1403 logging.info(patch_data)
1404 # We use "git apply" to apply the patch instead of "patch" so that we can
1405 # pick up file adds.
1406 # The --index flag means: also insert into the index (so we catch adds).
1407 cmd = ['git', 'apply', '--index', '-p0']
1408 if options.reject:
1409 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001410 try:
1411 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1412 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 DieWithError('Failed to apply the patch')
1414
1415 # If we had an issue, commit the current state and register the issue.
1416 if not options.nocommit:
1417 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1418 cl = Changelist()
1419 cl.SetIssue(issue)
1420 print "Committed patch."
1421 else:
1422 print "Patch applied to index."
1423 return 0
1424
1425
1426def CMDrebase(parser, args):
1427 """rebase current branch on top of svn repo"""
1428 # Provide a wrapper for git svn rebase to help avoid accidental
1429 # git svn dcommit.
1430 # It's the only command that doesn't use parser at all since we just defer
1431 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001432 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433
1434
1435def GetTreeStatus():
1436 """Fetches the tree status and returns either 'open', 'closed',
1437 'unknown' or 'unset'."""
1438 url = settings.GetTreeStatusUrl(error_ok=True)
1439 if url:
1440 status = urllib2.urlopen(url).read().lower()
1441 if status.find('closed') != -1 or status == '0':
1442 return 'closed'
1443 elif status.find('open') != -1 or status == '1':
1444 return 'open'
1445 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 return 'unset'
1447
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449def GetTreeStatusReason():
1450 """Fetches the tree status from a json url and returns the message
1451 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001452 url = settings.GetTreeStatusUrl()
1453 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 connection = urllib2.urlopen(json_url)
1455 status = json.loads(connection.read())
1456 connection.close()
1457 return status['message']
1458
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001459
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460def CMDtree(parser, args):
1461 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001462 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 status = GetTreeStatus()
1464 if 'unset' == status:
1465 print 'You must configure your tree status URL by running "git cl config".'
1466 return 2
1467
1468 print "The tree is %s" % status
1469 print
1470 print GetTreeStatusReason()
1471 if status != 'open':
1472 return 1
1473 return 0
1474
1475
1476def CMDupstream(parser, args):
1477 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001478 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001479 if args:
1480 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 cl = Changelist()
1482 print cl.GetUpstreamBranch()
1483 return 0
1484
1485
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001486def CMDset_commit(parser, args):
1487 """set the commit bit"""
1488 _, args = parser.parse_args(args)
1489 if args:
1490 parser.error('Unrecognized args: %s' % ' '.join(args))
1491 cl = Changelist()
1492 cl.SetFlag('commit', '1')
1493 return 0
1494
1495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496def Command(name):
1497 return getattr(sys.modules[__name__], 'CMD' + name, None)
1498
1499
1500def CMDhelp(parser, args):
1501 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001502 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503 if len(args) == 1:
1504 return main(args + ['--help'])
1505 parser.print_help()
1506 return 0
1507
1508
1509def GenUsage(parser, command):
1510 """Modify an OptParse object with the function's documentation."""
1511 obj = Command(command)
1512 more = getattr(obj, 'usage_more', '')
1513 if command == 'help':
1514 command = '<command>'
1515 else:
1516 # OptParser.description prefer nicely non-formatted strings.
1517 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1518 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1519
1520
1521def main(argv):
1522 """Doesn't parse the arguments here, just find the right subcommand to
1523 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001524 if sys.hexversion < 0x02060000:
1525 print >> sys.stderr, (
1526 '\nYour python version %s is unsupported, please upgrade.\n' %
1527 sys.version.split(' ', 1)[0])
1528 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001529 # Reload settings.
1530 global settings
1531 settings = Settings()
1532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 # Do it late so all commands are listed.
1534 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1535 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1536 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1537
1538 # Create the option parse and add --verbose support.
1539 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001540 parser.add_option(
1541 '-v', '--verbose', action='count', default=0,
1542 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543 old_parser_args = parser.parse_args
1544 def Parse(args):
1545 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001546 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001548 elif options.verbose:
1549 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 else:
1551 logging.basicConfig(level=logging.WARNING)
1552 return options, args
1553 parser.parse_args = Parse
1554
1555 if argv:
1556 command = Command(argv[0])
1557 if command:
1558 # "fix" the usage and the description now that we know the subcommand.
1559 GenUsage(parser, argv[0])
1560 try:
1561 return command(parser, argv[1:])
1562 except urllib2.HTTPError, e:
1563 if e.code != 500:
1564 raise
1565 DieWithError(
1566 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1567 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1568
1569 # Not a known command. Default to help.
1570 GenUsage(parser, 'help')
1571 return CMDhelp(parser, argv)
1572
1573
1574if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001575 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576 sys.exit(main(sys.argv[1:]))