blob: f78839b2de234e747dae00da704c75a951cc5f40 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000010import logging
11import optparse
12import os
13import re
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000016import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import urllib2
18
19try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000020 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021except ImportError:
22 pass
23
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000024try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000025 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000028 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 except ImportError:
30 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000031 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000032 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
34
35from third_party import upload
36import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000040import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
44
45
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000046DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
49
maruel@chromium.org90541732011-04-01 17:54:18 +000050
maruel@chromium.orgddd59412011-11-30 14:20:38 +000051# Initialized in main()
52settings = None
53
54
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000056 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057 sys.exit(1)
58
59
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 return subprocess2.check_output(args, shell=False, **kwargs)
63 except subprocess2.CalledProcessError, e:
64 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 'Command "%s" failed.\n%s' % (
67 ' '.join(args), error_message or e.stdout or ''))
68 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns stdout."""
73 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns return code and stdout."""
78 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
79 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def usage(more):
83 def hook(fn):
84 fn.usage_more = more
85 return fn
86 return hook
87
88
maruel@chromium.org90541732011-04-01 17:54:18 +000089def ask_for_data(prompt):
90 try:
91 return raw_input(prompt)
92 except KeyboardInterrupt:
93 # Hide the exception.
94 sys.exit(1)
95
96
bauerb@chromium.org866276c2011-03-18 20:09:31 +000097def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
98 """Return the corresponding git ref if |base_url| together with |glob_spec|
99 matches the full |url|.
100
101 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
102 """
103 fetch_suburl, as_ref = glob_spec.split(':')
104 if allow_wildcards:
105 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
106 if glob_match:
107 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
108 # "branches/{472,597,648}/src:refs/remotes/svn/*".
109 branch_re = re.escape(base_url)
110 if glob_match.group(1):
111 branch_re += '/' + re.escape(glob_match.group(1))
112 wildcard = glob_match.group(2)
113 if wildcard == '*':
114 branch_re += '([^/]*)'
115 else:
116 # Escape and replace surrounding braces with parentheses and commas
117 # with pipe symbols.
118 wildcard = re.escape(wildcard)
119 wildcard = re.sub('^\\\\{', '(', wildcard)
120 wildcard = re.sub('\\\\,', '|', wildcard)
121 wildcard = re.sub('\\\\}$', ')', wildcard)
122 branch_re += wildcard
123 if glob_match.group(3):
124 branch_re += re.escape(glob_match.group(3))
125 match = re.match(branch_re, url)
126 if match:
127 return re.sub('\*$', match.group(1), as_ref)
128
129 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
130 if fetch_suburl:
131 full_url = base_url + '/' + fetch_suburl
132 else:
133 full_url = base_url
134 if full_url == url:
135 return as_ref
136 return None
137
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000139class Settings(object):
140 def __init__(self):
141 self.default_server = None
142 self.cc = None
143 self.root = None
144 self.is_git_svn = None
145 self.svn_branch = None
146 self.tree_status_url = None
147 self.viewvc_url = None
148 self.updated = False
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000149 self.did_migrate_check = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000150 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000151
152 def LazyUpdateIfNeeded(self):
153 """Updates the settings from a codereview.settings file, if available."""
154 if not self.updated:
155 cr_settings_file = FindCodereviewSettingsFile()
156 if cr_settings_file:
157 LoadCodereviewSettingsFromFile(cr_settings_file)
158 self.updated = True
159
160 def GetDefaultServerUrl(self, error_ok=False):
161 if not self.default_server:
162 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000163 self.default_server = gclient_utils.UpgradeToHttps(
164 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000165 if error_ok:
166 return self.default_server
167 if not self.default_server:
168 error_message = ('Could not find settings file. You must configure '
169 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000170 self.default_server = gclient_utils.UpgradeToHttps(
171 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000172 return self.default_server
173
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000174 def GetRoot(self):
175 if not self.root:
176 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
177 return self.root
178
179 def GetIsGitSvn(self):
180 """Return true if this repo looks like it's using git-svn."""
181 if self.is_git_svn is None:
182 # If you have any "svn-remote.*" config keys, we think you're using svn.
183 self.is_git_svn = RunGitWithCode(
184 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
185 return self.is_git_svn
186
187 def GetSVNBranch(self):
188 if self.svn_branch is None:
189 if not self.GetIsGitSvn():
190 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
191
192 # Try to figure out which remote branch we're based on.
193 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000194 # 1) iterate through our branch history and find the svn URL.
195 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000196
197 # regexp matching the git-svn line that contains the URL.
198 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
199
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000200 # We don't want to go through all of history, so read a line from the
201 # pipe at a time.
202 # The -100 is an arbitrary limit so we don't search forever.
203 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000204 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000205 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000206 for line in proc.stdout:
207 match = git_svn_re.match(line)
208 if match:
209 url = match.group(1)
210 proc.stdout.close() # Cut pipe.
211 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000212
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000213 if url:
214 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
215 remotes = RunGit(['config', '--get-regexp',
216 r'^svn-remote\..*\.url']).splitlines()
217 for remote in remotes:
218 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000219 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000220 remote = match.group(1)
221 base_url = match.group(2)
222 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000223 ['config', 'svn-remote.%s.fetch' % remote],
224 error_ok=True).strip()
225 if fetch_spec:
226 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
227 if self.svn_branch:
228 break
229 branch_spec = RunGit(
230 ['config', 'svn-remote.%s.branches' % remote],
231 error_ok=True).strip()
232 if branch_spec:
233 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
234 if self.svn_branch:
235 break
236 tag_spec = RunGit(
237 ['config', 'svn-remote.%s.tags' % remote],
238 error_ok=True).strip()
239 if tag_spec:
240 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
241 if self.svn_branch:
242 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000243
244 if not self.svn_branch:
245 DieWithError('Can\'t guess svn branch -- try specifying it on the '
246 'command line')
247
248 return self.svn_branch
249
250 def GetTreeStatusUrl(self, error_ok=False):
251 if not self.tree_status_url:
252 error_message = ('You must configure your tree status URL by running '
253 '"git cl config".')
254 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
255 error_ok=error_ok,
256 error_message=error_message)
257 return self.tree_status_url
258
259 def GetViewVCUrl(self):
260 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000261 self.viewvc_url = gclient_utils.UpgradeToHttps(
262 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000263 return self.viewvc_url
264
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000265 def GetDefaultCCList(self):
266 return self._GetConfig('rietveld.cc', error_ok=True)
267
ukai@chromium.orge8077812012-02-03 03:41:46 +0000268 def GetIsGerrit(self):
269 """Return true if this repo is assosiated with gerrit code review system."""
270 if self.is_gerrit is None:
271 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
272 return self.is_gerrit
273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274 def _GetConfig(self, param, **kwargs):
275 self.LazyUpdateIfNeeded()
276 return RunGit(['config', param], **kwargs).strip()
277
278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000279def CheckForMigration():
280 """Migrate from the old issue format, if found.
281
282 We used to store the branch<->issue mapping in a file in .git, but it's
283 better to store it in the .git/config, since deleting a branch deletes that
284 branch's entry there.
285 """
286
287 # Don't run more than once.
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000288 if settings.did_migrate_check:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 return
290
291 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
292 storepath = os.path.join(gitdir, 'cl-mapping')
293 if os.path.exists(storepath):
294 print "old-style git-cl mapping file (%s) found; migrating." % storepath
295 store = open(storepath, 'r')
296 for line in store:
297 branch, issue = line.strip().split()
298 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
299 issue])
300 store.close()
301 os.remove(storepath)
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000302 settings.did_migrate_check = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303
304
305def ShortBranchName(branch):
306 """Convert a name like 'refs/heads/foo' to just 'foo'."""
307 return branch.replace('refs/heads/', '')
308
309
310class Changelist(object):
311 def __init__(self, branchref=None):
312 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000313 global settings
314 if not settings:
315 # Happens when git_cl.py is used as a utility library.
316 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000317 settings.GetDefaultServerUrl()
318 self.branchref = branchref
319 if self.branchref:
320 self.branch = ShortBranchName(self.branchref)
321 else:
322 self.branch = None
323 self.rietveld_server = None
324 self.upstream_branch = None
325 self.has_issue = False
326 self.issue = None
327 self.has_description = False
328 self.description = None
329 self.has_patchset = False
330 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000331 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000332 self.cc = None
333 self.watchers = ()
334
335 def GetCCList(self):
336 """Return the users cc'd on this CL.
337
338 Return is a string suitable for passing to gcl with the --cc flag.
339 """
340 if self.cc is None:
341 base_cc = settings .GetDefaultCCList()
342 more_cc = ','.join(self.watchers)
343 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
344 return self.cc
345
346 def SetWatchers(self, watchers):
347 """Set the list of email addresses that should be cc'd based on the changed
348 files in this CL.
349 """
350 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000351
352 def GetBranch(self):
353 """Returns the short branch name, e.g. 'master'."""
354 if not self.branch:
355 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
356 self.branch = ShortBranchName(self.branchref)
357 return self.branch
358
359 def GetBranchRef(self):
360 """Returns the full branch name, e.g. 'refs/heads/master'."""
361 self.GetBranch() # Poke the lazy loader.
362 return self.branchref
363
364 def FetchUpstreamTuple(self):
365 """Returns a tuple containg remote and remote ref,
366 e.g. 'origin', 'refs/heads/master'
367 """
368 remote = '.'
369 branch = self.GetBranch()
370 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
371 error_ok=True).strip()
372 if upstream_branch:
373 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
374 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000375 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
376 error_ok=True).strip()
377 if upstream_branch:
378 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000379 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000380 # Fall back on trying a git-svn upstream branch.
381 if settings.GetIsGitSvn():
382 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000383 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000384 # Else, try to guess the origin remote.
385 remote_branches = RunGit(['branch', '-r']).split()
386 if 'origin/master' in remote_branches:
387 # Fall back on origin/master if it exits.
388 remote = 'origin'
389 upstream_branch = 'refs/heads/master'
390 elif 'origin/trunk' in remote_branches:
391 # Fall back on origin/trunk if it exists. Generally a shared
392 # git-svn clone
393 remote = 'origin'
394 upstream_branch = 'refs/heads/trunk'
395 else:
396 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000397Either pass complete "git diff"-style arguments, like
398 git cl upload origin/master
399or verify this branch is set up to track another (via the --track argument to
400"git checkout -b ...").""")
401
402 return remote, upstream_branch
403
404 def GetUpstreamBranch(self):
405 if self.upstream_branch is None:
406 remote, upstream_branch = self.FetchUpstreamTuple()
407 if remote is not '.':
408 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
409 self.upstream_branch = upstream_branch
410 return self.upstream_branch
411
412 def GetRemoteUrl(self):
413 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
414
415 Returns None if there is no remote.
416 """
417 remote = self.FetchUpstreamTuple()[0]
418 if remote == '.':
419 return None
420 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
421
422 def GetIssue(self):
423 if not self.has_issue:
424 CheckForMigration()
425 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
426 if issue:
427 self.issue = issue
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000428 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000429 ['config', self._RietveldServer()], error_ok=True).strip())
430 else:
431 self.issue = None
432 if not self.rietveld_server:
433 self.rietveld_server = settings.GetDefaultServerUrl()
434 self.has_issue = True
435 return self.issue
436
437 def GetRietveldServer(self):
438 self.GetIssue()
439 return self.rietveld_server
440
441 def GetIssueURL(self):
442 """Get the URL for a particular issue."""
443 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
444
445 def GetDescription(self, pretty=False):
446 if not self.has_description:
447 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000448 issue = int(self.GetIssue())
449 try:
450 self.description = self.RpcServer().get_description(issue).strip()
451 except urllib2.HTTPError, e:
452 if e.code == 404:
453 DieWithError(
454 ('\nWhile fetching the description for issue %d, received a '
455 '404 (not found)\n'
456 'error. It is likely that you deleted this '
457 'issue on the server. If this is the\n'
458 'case, please run\n\n'
459 ' git cl issue 0\n\n'
460 'to clear the association with the deleted issue. Then run '
461 'this command again.') % issue)
462 else:
463 DieWithError(
464 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 self.has_description = True
466 if pretty:
467 wrapper = textwrap.TextWrapper()
468 wrapper.initial_indent = wrapper.subsequent_indent = ' '
469 return wrapper.fill(self.description)
470 return self.description
471
472 def GetPatchset(self):
473 if not self.has_patchset:
474 patchset = RunGit(['config', self._PatchsetSetting()],
475 error_ok=True).strip()
476 if patchset:
477 self.patchset = patchset
478 else:
479 self.patchset = None
480 self.has_patchset = True
481 return self.patchset
482
483 def SetPatchset(self, patchset):
484 """Set this branch's patchset. If patchset=0, clears the patchset."""
485 if patchset:
486 RunGit(['config', self._PatchsetSetting(), str(patchset)])
487 else:
488 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000489 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000490 self.has_patchset = False
491
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000492 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000493 patchset = self.RpcServer().get_issue_properties(
494 int(issue), False)['patchsets'][-1]
495 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000496 '/download/issue%s_%s.diff' % (issue, patchset))
497
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498 def SetIssue(self, issue):
499 """Set this branch's issue. If issue=0, clears the issue."""
500 if issue:
501 RunGit(['config', self._IssueSetting(), str(issue)])
502 if self.rietveld_server:
503 RunGit(['config', self._RietveldServer(), self.rietveld_server])
504 else:
505 RunGit(['config', '--unset', self._IssueSetting()])
506 self.SetPatchset(0)
507 self.has_issue = False
508
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000509 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000510 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
511 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000512
513 # We use the sha1 of HEAD as a name of this change.
514 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000515 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000516 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000517 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000518 except subprocess2.CalledProcessError:
519 DieWithError(
520 ('\nFailed to diff against upstream branch %s!\n\n'
521 'This branch probably doesn\'t exist anymore. To reset the\n'
522 'tracking branch, please run\n'
523 ' git branch --set-upstream %s trunk\n'
524 'replacing trunk with origin/master or the relevant branch') %
525 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000526
527 issue = ConvertToInteger(self.GetIssue())
528 patchset = ConvertToInteger(self.GetPatchset())
529 if issue:
530 description = self.GetDescription()
531 else:
532 # If the change was never uploaded, use the log messages of all commits
533 # up to the branch point, as git cl upload will prefill the description
534 # with these log messages.
535 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
536 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000537
538 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000539 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000540 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000541 name,
542 description,
543 absroot,
544 files,
545 issue,
546 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000547 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000548
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000549 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
550 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
551 change = self.GetChange(upstream_branch, author)
552
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000553 # Apply watchlists on upload.
554 if not committing:
555 watchlist = watchlists.Watchlists(change.RepositoryRoot())
556 files = [f.LocalPath() for f in change.AffectedFiles()]
557 self.SetWatchers(watchlist.GetWatchersForPaths(files))
558
559 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000560 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000561 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000562 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000563 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000564 except presubmit_support.PresubmitFailure, e:
565 DieWithError(
566 ('%s\nMaybe your depot_tools is out of date?\n'
567 'If all fails, contact maruel@') % e)
568
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000570 """Updates the description and closes the issue."""
571 issue = int(self.GetIssue())
572 self.RpcServer().update_description(issue, self.description)
573 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000575 def SetFlag(self, flag, value):
576 """Patchset must match."""
577 if not self.GetPatchset():
578 DieWithError('The patchset needs to match. Send another patchset.')
579 try:
580 return self.RpcServer().set_flag(
581 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
582 except urllib2.HTTPError, e:
583 if e.code == 404:
584 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
585 if e.code == 403:
586 DieWithError(
587 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
588 'match?') % (self.GetIssue(), self.GetPatchset()))
589 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000591 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000592 """Returns an upload.RpcServer() to access this review's rietveld instance.
593 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000594 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000595 self.GetIssue()
596 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000597 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598
599 def _IssueSetting(self):
600 """Return the git setting that stores this change's issue."""
601 return 'branch.%s.rietveldissue' % self.GetBranch()
602
603 def _PatchsetSetting(self):
604 """Return the git setting that stores this change's most recent patchset."""
605 return 'branch.%s.rietveldpatchset' % self.GetBranch()
606
607 def _RietveldServer(self):
608 """Returns the git setting that stores this change's rietveld server."""
609 return 'branch.%s.rietveldserver' % self.GetBranch()
610
611
612def GetCodereviewSettingsInteractively():
613 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000614 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 server = settings.GetDefaultServerUrl(error_ok=True)
616 prompt = 'Rietveld server (host[:port])'
617 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000618 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 if not server and not newserver:
620 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 if newserver:
622 newserver = gclient_utils.UpgradeToHttps(newserver)
623 if newserver != server:
624 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000626 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 prompt = caption
628 if initial:
629 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000630 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631 if new_val == 'x':
632 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000633 elif new_val:
634 if is_url:
635 new_val = gclient_utils.UpgradeToHttps(new_val)
636 if new_val != initial:
637 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000639 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000641 'tree-status-url', False)
642 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643
644 # TODO: configure a default branch to diff against, rather than this
645 # svn-based hackery.
646
647
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000648class ChangeDescription(object):
649 """Contains a parsed form of the change description."""
jam@chromium.org31083642012-01-27 03:14:45 +0000650 def __init__(self, subject, log_desc, reviewers):
651 self.subject = subject
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
jam@chromium.org31083642012-01-27 03:14:45 +0000656 def Update(self):
657 initial_text = """# 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"""
jam@chromium.org31083642012-01-27 03:14:45 +0000661 initial_text += 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):
jam@chromium.org31083642012-01-27 03:14:45 +0000665 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000666 if '\nBUG=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000667 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000668 if '\nTEST=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000669 initial_text += '\nTEST='
670 initial_text = initial_text.rstrip('\n') + '\n'
671 content = gclient_utils.RunEditor(initial_text, 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()
675 if not content:
676 DieWithError('No CL description, aborting')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000677 self.ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000678
ukai@chromium.orge8077812012-02-03 03:41:46 +0000679 def ParseDescription(self, description):
jam@chromium.org31083642012-01-27 03:14:45 +0000680 """Updates the list of reviewers and subject from the description."""
681 if not description:
682 self.description = description
683 return
684
685 self.description = description.strip('\n') + '\n'
686 self.subject = description.split('\n', 1)[0]
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000687 # Retrieves all reviewer lines
688 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
jam@chromium.org31083642012-01-27 03:14:45 +0000689 self.reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000690 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000691
692 def IsEmpty(self):
693 return not self.description
694
695
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696def FindCodereviewSettingsFile(filename='codereview.settings'):
697 """Finds the given file starting in the cwd and going up.
698
699 Only looks up to the top of the repository unless an
700 'inherit-review-settings-ok' file exists in the root of the repository.
701 """
702 inherit_ok_file = 'inherit-review-settings-ok'
703 cwd = os.getcwd()
704 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
705 if os.path.isfile(os.path.join(root, inherit_ok_file)):
706 root = '/'
707 while True:
708 if filename in os.listdir(cwd):
709 if os.path.isfile(os.path.join(cwd, filename)):
710 return open(os.path.join(cwd, filename))
711 if cwd == root:
712 break
713 cwd = os.path.dirname(cwd)
714
715
716def LoadCodereviewSettingsFromFile(fileobj):
717 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000718 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 def SetProperty(name, setting, unset_error_ok=False):
721 fullname = 'rietveld.' + name
722 if setting in keyvals:
723 RunGit(['config', fullname, keyvals[setting]])
724 else:
725 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
726
727 SetProperty('server', 'CODE_REVIEW_SERVER')
728 # Only server setting is required. Other settings can be absent.
729 # In that case, we ignore errors raised during option deletion attempt.
730 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
731 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
732 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
733
ukai@chromium.orge8077812012-02-03 03:41:46 +0000734 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
735 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
736 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
737 # Install the standard commit-msg hook.
738 RunCommand(['scp', '-p', '-P', keyvals['GERRIT_PORT'],
739 '%s:hooks/commit-msg' % keyvals['GERRIT_HOST'],
740 os.path.join(settings.GetRoot(),
741 '.git', 'hooks', 'commit-msg')])
742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
744 #should be of the form
745 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
746 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
747 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
748 keyvals['ORIGIN_URL_CONFIG']])
749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750
751@usage('[repo root containing codereview.settings]')
752def CMDconfig(parser, args):
753 """edit configuration for this tree"""
754
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000755 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000756 if len(args) == 0:
757 GetCodereviewSettingsInteractively()
758 return 0
759
760 url = args[0]
761 if not url.endswith('codereview.settings'):
762 url = os.path.join(url, 'codereview.settings')
763
764 # Load code review settings and download hooks (if available).
765 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
766 return 0
767
768
769def CMDstatus(parser, args):
770 """show status of changelists"""
771 parser.add_option('--field',
772 help='print only specific field (desc|id|patch|url)')
773 (options, args) = parser.parse_args(args)
774
775 # TODO: maybe make show_branches a flag if necessary.
776 show_branches = not options.field
777
778 if show_branches:
779 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
780 if branches:
781 print 'Branches associated with reviews:'
782 for branch in sorted(branches.splitlines()):
783 cl = Changelist(branchref=branch)
784 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
785
786 cl = Changelist()
787 if options.field:
788 if options.field.startswith('desc'):
789 print cl.GetDescription()
790 elif options.field == 'id':
791 issueid = cl.GetIssue()
792 if issueid:
793 print issueid
794 elif options.field == 'patch':
795 patchset = cl.GetPatchset()
796 if patchset:
797 print patchset
798 elif options.field == 'url':
799 url = cl.GetIssueURL()
800 if url:
801 print url
802 else:
803 print
804 print 'Current branch:',
805 if not cl.GetIssue():
806 print 'no issue assigned.'
807 return 0
808 print cl.GetBranch()
809 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
810 print 'Issue description:'
811 print cl.GetDescription(pretty=True)
812 return 0
813
814
815@usage('[issue_number]')
816def CMDissue(parser, args):
817 """Set or display the current code review issue number.
818
819 Pass issue number 0 to clear the current issue.
820"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000821 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
823 cl = Changelist()
824 if len(args) > 0:
825 try:
826 issue = int(args[0])
827 except ValueError:
828 DieWithError('Pass a number to set the issue or none to list it.\n'
829 'Maybe you want to run git cl status?')
830 cl.SetIssue(issue)
831 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
832 return 0
833
834
835def CreateDescriptionFromLog(args):
836 """Pulls out the commit log to use as a base for the CL description."""
837 log_args = []
838 if len(args) == 1 and not args[0].endswith('.'):
839 log_args = [args[0] + '..']
840 elif len(args) == 1 and args[0].endswith('...'):
841 log_args = [args[0][:-1]]
842 elif len(args) == 2:
843 log_args = [args[0] + '..' + args[1]]
844 else:
845 log_args = args[:] # Hope for the best!
846 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
847
848
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000849def ConvertToInteger(inputval):
850 """Convert a string to integer, but returns either an int or None."""
851 try:
852 return int(inputval)
853 except (TypeError, ValueError):
854 return None
855
856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857def CMDpresubmit(parser, args):
858 """run presubmit tests on the current changelist"""
859 parser.add_option('--upload', action='store_true',
860 help='Run upload hook instead of the push/dcommit hook')
861 (options, args) = parser.parse_args(args)
862
863 # Make sure index is up-to-date before running diff-index.
864 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
865 if RunGit(['diff-index', 'HEAD']):
866 # TODO(maruel): Is this really necessary?
867 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
868 return 1
869
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000870 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000871 if args:
872 base_branch = args[0]
873 else:
874 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000875 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000877 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000878 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000879 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000880 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881
882
ukai@chromium.orge8077812012-02-03 03:41:46 +0000883def GerritUpload(options, args, cl):
884 """upload the current branch to gerrit."""
885 # We assume the remote called "origin" is the one we want.
886 # It is probably not worthwhile to support different workflows.
887 remote = 'origin'
888 branch = 'master'
889 if options.target_branch:
890 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891
ukai@chromium.orge8077812012-02-03 03:41:46 +0000892 log_desc = CreateDescriptionFromLog(args)
893 if options.reviewers:
894 log_desc += '\nR=' + options.reviewers
895 change_desc = ChangeDescription(options.message, log_desc,
896 options.reviewers)
897 change_desc.ParseDescription(log_desc)
898 if change_desc.IsEmpty():
899 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900 return 1
901
ukai@chromium.orge8077812012-02-03 03:41:46 +0000902 receive_options = []
903 cc = cl.GetCCList().split(',')
904 if options.cc:
905 cc += options.cc.split(',')
906 cc = filter(None, cc)
907 if cc:
908 receive_options += ['--cc=' + email for email in cc]
909 if change_desc.reviewers:
910 reviewers = filter(None, change_desc.reviewers.split(','))
911 if reviewers:
912 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913
ukai@chromium.orge8077812012-02-03 03:41:46 +0000914 git_command = ['push']
915 if receive_options:
916 git_command.append('--receive-pack="git receive-pack %s"' %
917 ' '.join(receive_options))
918 git_command += [remote, 'HEAD:refs/for/' + branch]
919 RunGit(git_command)
920 # TODO(ukai): parse Change-Id: and set issue number?
921 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923
ukai@chromium.orge8077812012-02-03 03:41:46 +0000924def RietveldUpload(options, args, cl):
925 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 upload_args = ['--assume_yes'] # Don't ask about untracked files.
927 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 if options.emulate_svn_auto_props:
929 upload_args.append('--emulate_svn_auto_props')
jam@chromium.org31083642012-01-27 03:14:45 +0000930 if options.from_logs and not options.message:
931 print 'Must set message for subject line if using desc_from_logs'
932 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933
934 change_desc = None
935
936 if cl.GetIssue():
937 if options.message:
938 upload_args.extend(['--message', options.message])
939 upload_args.extend(['--issue', cl.GetIssue()])
940 print ("This branch is associated with issue %s. "
941 "Adding patch to that issue." % cl.GetIssue())
942 else:
jam@chromium.org31083642012-01-27 03:14:45 +0000943 log_desc = CreateDescriptionFromLog(args)
944 change_desc = ChangeDescription(options.message, log_desc,
945 options.reviewers)
946 if not options.from_logs:
947 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000948
949 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 print "Description is empty; aborting."
951 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000952
jam@chromium.org31083642012-01-27 03:14:45 +0000953 upload_args.extend(['--message', change_desc.subject])
954 upload_args.extend(['--description', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000955 if change_desc.reviewers:
956 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000957 if options.send_mail:
958 if not change_desc.reviewers:
959 DieWithError("Must specify reviewers to send email.")
960 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000961 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000962 if cc:
963 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000964
965 # Include the upstream repo's URL in the change -- this is useful for
966 # projects that have their source spread across multiple repos.
967 remote_url = None
968 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000969 # URL is dependent on the current directory.
970 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971 if data:
972 keys = dict(line.split(': ', 1) for line in data.splitlines()
973 if ': ' in line)
974 remote_url = keys.get('URL', None)
975 else:
976 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
977 remote_url = (cl.GetRemoteUrl() + '@'
978 + cl.GetUpstreamBranch().split('/')[-1])
979 if remote_url:
980 upload_args.extend(['--base_url', remote_url])
981
982 try:
983 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000984 except KeyboardInterrupt:
985 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986 except:
987 # If we got an exception after the user typed a description for their
988 # change, back up the description before re-raising.
989 if change_desc:
990 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
991 print '\nGot exception while uploading -- saving description to %s\n' \
992 % backup_path
993 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000994 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 backup_file.close()
996 raise
997
998 if not cl.GetIssue():
999 cl.SetIssue(issue)
1000 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001001
1002 if options.use_commit_queue:
1003 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004 return 0
1005
1006
ukai@chromium.orge8077812012-02-03 03:41:46 +00001007@usage('[args to "git diff"]')
1008def CMDupload(parser, args):
1009 """upload the current changelist to codereview"""
1010 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1011 help='bypass upload presubmit hook')
1012 parser.add_option('-f', action='store_true', dest='force',
1013 help="force yes to questions (don't prompt)")
1014 parser.add_option('-m', dest='message', help='message for patch')
1015 parser.add_option('-r', '--reviewers',
1016 help='reviewer email addresses')
1017 parser.add_option('--cc',
1018 help='cc email addresses')
1019 parser.add_option('--send-mail', action='store_true',
1020 help='send email to reviewer immediately')
1021 parser.add_option("--emulate_svn_auto_props", action="store_true",
1022 dest="emulate_svn_auto_props",
1023 help="Emulate Subversion's auto properties feature.")
1024 parser.add_option("--desc_from_logs", action="store_true",
1025 dest="from_logs",
1026 help="""Squashes git commit logs into change description and
1027 uses message as subject""")
1028 parser.add_option('-c', '--use-commit-queue', action='store_true',
1029 help='tell the commit queue to commit this patchset')
1030 if settings.GetIsGerrit():
1031 parser.add_option('--target_branch', dest='target_branch', default='master',
1032 help='target branch to upload')
1033 (options, args) = parser.parse_args(args)
1034
1035 # Make sure index is up-to-date before running diff-index.
1036 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1037 if RunGit(['diff-index', 'HEAD']):
1038 print 'Cannot upload with a dirty tree. You must commit locally first.'
1039 return 1
1040
1041 cl = Changelist()
1042 if args:
1043 # TODO(ukai): is it ok for gerrit case?
1044 base_branch = args[0]
1045 else:
1046 # Default to diffing against the "upstream" branch.
1047 base_branch = cl.GetUpstreamBranch()
1048 args = [base_branch + "..."]
1049
1050 if not options.bypass_hooks:
1051 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1052 may_prompt=not options.force,
1053 verbose=options.verbose,
1054 author=None)
1055 if not hook_results.should_continue():
1056 return 1
1057 if not options.reviewers and hook_results.reviewers:
1058 options.reviewers = hook_results.reviewers
1059
1060 # --no-ext-diff is broken in some versions of Git, so try to work around
1061 # this by overriding the environment (but there is still a problem if the
1062 # git config key "diff.external" is used).
1063 env = os.environ.copy()
1064 if 'GIT_EXTERNAL_DIFF' in env:
1065 del env['GIT_EXTERNAL_DIFF']
1066 subprocess2.call(
1067 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1068
1069 if settings.GetIsGerrit():
1070 return GerritUpload(options, args, cl)
1071 return RietveldUpload(options, args, cl)
1072
1073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074def SendUpstream(parser, args, cmd):
1075 """Common code for CmdPush and CmdDCommit
1076
1077 Squashed commit into a single.
1078 Updates changelog with metadata (e.g. pointer to review).
1079 Pushes/dcommits the code upstream.
1080 Updates review and closes.
1081 """
1082 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1083 help='bypass upload presubmit hook')
1084 parser.add_option('-m', dest='message',
1085 help="override review description")
1086 parser.add_option('-f', action='store_true', dest='force',
1087 help="force yes to questions (don't prompt)")
1088 parser.add_option('-c', dest='contributor',
1089 help="external contributor for patch (appended to " +
1090 "description and used as author for git). Should be " +
1091 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 (options, args) = parser.parse_args(args)
1093 cl = Changelist()
1094
1095 if not args or cmd == 'push':
1096 # Default to merging against our best guess of the upstream branch.
1097 args = [cl.GetUpstreamBranch()]
1098
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001099 if options.contributor:
1100 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1101 print "Please provide contibutor as 'First Last <email@example.com>'"
1102 return 1
1103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 base_branch = args[0]
1105
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001106 # Make sure index is up-to-date before running diff-index.
1107 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 if RunGit(['diff-index', 'HEAD']):
1109 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1110 return 1
1111
1112 # This rev-list syntax means "show all commits not in my branch that
1113 # are in base_branch".
1114 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1115 base_branch]).splitlines()
1116 if upstream_commits:
1117 print ('Base branch "%s" has %d commits '
1118 'not in this branch.' % (base_branch, len(upstream_commits)))
1119 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1120 return 1
1121
1122 if cmd == 'dcommit':
1123 # This is the revision `svn dcommit` will commit on top of.
1124 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1125 '--pretty=format:%H'])
1126 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1127 if extra_commits:
1128 print ('This branch has %d additional commits not upstreamed yet.'
1129 % len(extra_commits.splitlines()))
1130 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1131 'before attempting to %s.' % (base_branch, cmd))
1132 return 1
1133
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001134 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001135 author = None
1136 if options.contributor:
1137 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001138 hook_results = cl.RunHook(
1139 committing=True,
1140 upstream_branch=base_branch,
1141 may_prompt=not options.force,
1142 verbose=options.verbose,
1143 author=author)
1144 if not hook_results.should_continue():
1145 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146
1147 if cmd == 'dcommit':
1148 # Check the tree status if the tree status URL is set.
1149 status = GetTreeStatus()
1150 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001151 print('The tree is closed. Please wait for it to reopen. Use '
1152 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 return 1
1154 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001155 print('Unable to determine tree status. Please verify manually and '
1156 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001157 else:
1158 breakpad.SendStack(
1159 'GitClHooksBypassedCommit',
1160 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001161 (cl.GetRietveldServer(), cl.GetIssue()),
1162 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
1164 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001165 if not description and cl.GetIssue():
1166 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001168 if not description:
1169 print 'No description set.'
1170 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1171 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001173 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175
1176 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 description += "\nPatch from %s." % options.contributor
1178 print 'Description:', repr(description)
1179
1180 branches = [base_branch, cl.GetBranchRef()]
1181 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001182 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001183 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184
1185 # We want to squash all this branch's commits into one commit with the
1186 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001187 # We do this by doing a "reset --soft" to the base branch (which keeps
1188 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 MERGE_BRANCH = 'git-cl-commit'
1190 # Delete the merge branch if it already exists.
1191 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1192 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1193 RunGit(['branch', '-D', MERGE_BRANCH])
1194
1195 # We might be in a directory that's present in this branch but not in the
1196 # trunk. Move up to the top of the tree so that git commands that expect a
1197 # valid CWD won't fail after we check out the merge branch.
1198 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1199 if rel_base_path:
1200 os.chdir(rel_base_path)
1201
1202 # Stuff our change into the merge branch.
1203 # We wrap in a try...finally block so if anything goes wrong,
1204 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001205 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001207 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1208 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 if options.contributor:
1210 RunGit(['commit', '--author', options.contributor, '-m', description])
1211 else:
1212 RunGit(['commit', '-m', description])
1213 if cmd == 'push':
1214 # push the merge branch.
1215 remote, branch = cl.FetchUpstreamTuple()
1216 retcode, output = RunGitWithCode(
1217 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1218 logging.debug(output)
1219 else:
1220 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001221 retcode, output = RunGitWithCode(['svn', 'dcommit',
1222 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 finally:
1224 # And then swap back to the original branch and clean up.
1225 RunGit(['checkout', '-q', cl.GetBranch()])
1226 RunGit(['branch', '-D', MERGE_BRANCH])
1227
1228 if cl.GetIssue():
1229 if cmd == 'dcommit' and 'Committed r' in output:
1230 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1231 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001232 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1233 for l in output.splitlines(False))
1234 match = filter(None, match)
1235 if len(match) != 1:
1236 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1237 output)
1238 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 else:
1240 return 1
1241 viewvc_url = settings.GetViewVCUrl()
1242 if viewvc_url and revision:
1243 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1244 print ('Closing issue '
1245 '(you may be prompted for your codereview password)...')
1246 cl.CloseIssue()
1247 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001248
1249 if retcode == 0:
1250 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1251 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001252 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 return 0
1255
1256
1257@usage('[upstream branch to apply against]')
1258def CMDdcommit(parser, args):
1259 """commit the current changelist via git-svn"""
1260 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001261 message = """This doesn't appear to be an SVN repository.
1262If your project has a git mirror with an upstream SVN master, you probably need
1263to run 'git svn init', see your project's git mirror documentation.
1264If your project has a true writeable upstream repository, you probably want
1265to run 'git cl push' instead.
1266Choose wisely, if you get this wrong, your commit might appear to succeed but
1267will instead be silently ignored."""
1268 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001269 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 return SendUpstream(parser, args, 'dcommit')
1271
1272
1273@usage('[upstream branch to apply against]')
1274def CMDpush(parser, args):
1275 """commit the current changelist via git"""
1276 if settings.GetIsGitSvn():
1277 print('This appears to be an SVN repository.')
1278 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001279 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 return SendUpstream(parser, args, 'push')
1281
1282
1283@usage('<patch url or issue id>')
1284def CMDpatch(parser, args):
1285 """patch in a code review"""
1286 parser.add_option('-b', dest='newbranch',
1287 help='create a new branch off trunk for the patch')
1288 parser.add_option('-f', action='store_true', dest='force',
1289 help='with -b, clobber any existing branch')
1290 parser.add_option('--reject', action='store_true', dest='reject',
1291 help='allow failed patches and spew .rej files')
1292 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1293 help="don't commit after patch applies")
1294 (options, args) = parser.parse_args(args)
1295 if len(args) != 1:
1296 parser.print_help()
1297 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001298 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001300 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001301 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001302
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001303 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001305 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001306 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001308 # Assume it's a URL to the patch. Default to https.
1309 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001310 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001311 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 DieWithError('Must pass an issue ID or full URL for '
1313 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001314 issue = match.group(1)
1315 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316
1317 if options.newbranch:
1318 if options.force:
1319 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001320 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 RunGit(['checkout', '-b', options.newbranch,
1322 Changelist().GetUpstreamBranch()])
1323
1324 # Switch up to the top-level directory, if necessary, in preparation for
1325 # applying the patch.
1326 top = RunGit(['rev-parse', '--show-cdup']).strip()
1327 if top:
1328 os.chdir(top)
1329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 # Git patches have a/ at the beginning of source paths. We strip that out
1331 # with a sed script rather than the -p flag to patch so we can feed either
1332 # Git or svn-style patches into the same apply command.
1333 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001334 try:
1335 patch_data = subprocess2.check_output(
1336 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1337 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 DieWithError('Git patch mungling failed.')
1339 logging.info(patch_data)
1340 # We use "git apply" to apply the patch instead of "patch" so that we can
1341 # pick up file adds.
1342 # The --index flag means: also insert into the index (so we catch adds).
1343 cmd = ['git', 'apply', '--index', '-p0']
1344 if options.reject:
1345 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001346 try:
1347 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1348 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 DieWithError('Failed to apply the patch')
1350
1351 # If we had an issue, commit the current state and register the issue.
1352 if not options.nocommit:
1353 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1354 cl = Changelist()
1355 cl.SetIssue(issue)
1356 print "Committed patch."
1357 else:
1358 print "Patch applied to index."
1359 return 0
1360
1361
1362def CMDrebase(parser, args):
1363 """rebase current branch on top of svn repo"""
1364 # Provide a wrapper for git svn rebase to help avoid accidental
1365 # git svn dcommit.
1366 # It's the only command that doesn't use parser at all since we just defer
1367 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001368 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369
1370
1371def GetTreeStatus():
1372 """Fetches the tree status and returns either 'open', 'closed',
1373 'unknown' or 'unset'."""
1374 url = settings.GetTreeStatusUrl(error_ok=True)
1375 if url:
1376 status = urllib2.urlopen(url).read().lower()
1377 if status.find('closed') != -1 or status == '0':
1378 return 'closed'
1379 elif status.find('open') != -1 or status == '1':
1380 return 'open'
1381 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 return 'unset'
1383
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385def GetTreeStatusReason():
1386 """Fetches the tree status from a json url and returns the message
1387 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001388 url = settings.GetTreeStatusUrl()
1389 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 connection = urllib2.urlopen(json_url)
1391 status = json.loads(connection.read())
1392 connection.close()
1393 return status['message']
1394
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396def CMDtree(parser, args):
1397 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001398 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 status = GetTreeStatus()
1400 if 'unset' == status:
1401 print 'You must configure your tree status URL by running "git cl config".'
1402 return 2
1403
1404 print "The tree is %s" % status
1405 print
1406 print GetTreeStatusReason()
1407 if status != 'open':
1408 return 1
1409 return 0
1410
1411
1412def CMDupstream(parser, args):
1413 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001414 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001415 if args:
1416 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 cl = Changelist()
1418 print cl.GetUpstreamBranch()
1419 return 0
1420
1421
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001422def CMDset_commit(parser, args):
1423 """set the commit bit"""
1424 _, args = parser.parse_args(args)
1425 if args:
1426 parser.error('Unrecognized args: %s' % ' '.join(args))
1427 cl = Changelist()
1428 cl.SetFlag('commit', '1')
1429 return 0
1430
1431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432def Command(name):
1433 return getattr(sys.modules[__name__], 'CMD' + name, None)
1434
1435
1436def CMDhelp(parser, args):
1437 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001438 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 if len(args) == 1:
1440 return main(args + ['--help'])
1441 parser.print_help()
1442 return 0
1443
1444
1445def GenUsage(parser, command):
1446 """Modify an OptParse object with the function's documentation."""
1447 obj = Command(command)
1448 more = getattr(obj, 'usage_more', '')
1449 if command == 'help':
1450 command = '<command>'
1451 else:
1452 # OptParser.description prefer nicely non-formatted strings.
1453 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1454 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1455
1456
1457def main(argv):
1458 """Doesn't parse the arguments here, just find the right subcommand to
1459 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001460 # Reload settings.
1461 global settings
1462 settings = Settings()
1463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464 # Do it late so all commands are listed.
1465 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1466 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1467 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1468
1469 # Create the option parse and add --verbose support.
1470 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001471 parser.add_option(
1472 '-v', '--verbose', action='count', default=0,
1473 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 old_parser_args = parser.parse_args
1475 def Parse(args):
1476 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001477 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001479 elif options.verbose:
1480 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 else:
1482 logging.basicConfig(level=logging.WARNING)
1483 return options, args
1484 parser.parse_args = Parse
1485
1486 if argv:
1487 command = Command(argv[0])
1488 if command:
1489 # "fix" the usage and the description now that we know the subcommand.
1490 GenUsage(parser, argv[0])
1491 try:
1492 return command(parser, argv[1:])
1493 except urllib2.HTTPError, e:
1494 if e.code != 500:
1495 raise
1496 DieWithError(
1497 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1498 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1499
1500 # Not a known command. Default to help.
1501 GenUsage(parser, 'help')
1502 return CMDhelp(parser, argv)
1503
1504
1505if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001506 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507 sys.exit(main(sys.argv[1:]))