blob: e250c8e5e4dfcf884891629f46fbf7d217fd0cbd [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042
maruel@chromium.org90541732011-04-01 17:54:18 +000043
maruel@chromium.orgddd59412011-11-30 14:20:38 +000044# Initialized in main()
45settings = None
46
47
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000049 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050 sys.exit(1)
51
52
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000055 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000056 except subprocess2.CalledProcessError, e:
57 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059 'Command "%s" failed.\n%s' % (
60 ' '.join(args), error_message or e.stdout or ''))
61 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062
63
64def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 """Returns stdout."""
66 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
68
69def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000071 try:
72 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
73 return code, out[0]
74 except ValueError:
75 # When the subprocess fails, it returns None. That triggers a ValueError
76 # when trying to unpack the return value into (out, code).
77 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
79
80def usage(more):
81 def hook(fn):
82 fn.usage_more = more
83 return fn
84 return hook
85
86
maruel@chromium.org90541732011-04-01 17:54:18 +000087def ask_for_data(prompt):
88 try:
89 return raw_input(prompt)
90 except KeyboardInterrupt:
91 # Hide the exception.
92 sys.exit(1)
93
94
bauerb@chromium.org866276c2011-03-18 20:09:31 +000095def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
96 """Return the corresponding git ref if |base_url| together with |glob_spec|
97 matches the full |url|.
98
99 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
100 """
101 fetch_suburl, as_ref = glob_spec.split(':')
102 if allow_wildcards:
103 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
104 if glob_match:
105 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
106 # "branches/{472,597,648}/src:refs/remotes/svn/*".
107 branch_re = re.escape(base_url)
108 if glob_match.group(1):
109 branch_re += '/' + re.escape(glob_match.group(1))
110 wildcard = glob_match.group(2)
111 if wildcard == '*':
112 branch_re += '([^/]*)'
113 else:
114 # Escape and replace surrounding braces with parentheses and commas
115 # with pipe symbols.
116 wildcard = re.escape(wildcard)
117 wildcard = re.sub('^\\\\{', '(', wildcard)
118 wildcard = re.sub('\\\\,', '|', wildcard)
119 wildcard = re.sub('\\\\}$', ')', wildcard)
120 branch_re += wildcard
121 if glob_match.group(3):
122 branch_re += re.escape(glob_match.group(3))
123 match = re.match(branch_re, url)
124 if match:
125 return re.sub('\*$', match.group(1), as_ref)
126
127 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
128 if fetch_suburl:
129 full_url = base_url + '/' + fetch_suburl
130 else:
131 full_url = base_url
132 if full_url == url:
133 return as_ref
134 return None
135
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000137def print_stats(args):
138 """Prints statistics about the change to the user."""
139 # --no-ext-diff is broken in some versions of Git, so try to work around
140 # this by overriding the environment (but there is still a problem if the
141 # git config key "diff.external" is used).
142 env = os.environ.copy()
143 if 'GIT_EXTERNAL_DIFF' in env:
144 del env['GIT_EXTERNAL_DIFF']
145 return subprocess2.call(
146 ['git', 'diff', '--no-ext-diff', '--stat', '--find-copies-harder'] + args,
147 env=env)
148
149
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150class Settings(object):
151 def __init__(self):
152 self.default_server = None
153 self.cc = None
154 self.root = None
155 self.is_git_svn = None
156 self.svn_branch = None
157 self.tree_status_url = None
158 self.viewvc_url = None
159 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000160 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000161
162 def LazyUpdateIfNeeded(self):
163 """Updates the settings from a codereview.settings file, if available."""
164 if not self.updated:
165 cr_settings_file = FindCodereviewSettingsFile()
166 if cr_settings_file:
167 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000168 self.updated = True
169 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000170 self.updated = True
171
172 def GetDefaultServerUrl(self, error_ok=False):
173 if not self.default_server:
174 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000175 self.default_server = gclient_utils.UpgradeToHttps(
176 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000177 if error_ok:
178 return self.default_server
179 if not self.default_server:
180 error_message = ('Could not find settings file. You must configure '
181 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000182 self.default_server = gclient_utils.UpgradeToHttps(
183 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000184 return self.default_server
185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000186 def GetRoot(self):
187 if not self.root:
188 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
189 return self.root
190
191 def GetIsGitSvn(self):
192 """Return true if this repo looks like it's using git-svn."""
193 if self.is_git_svn is None:
194 # If you have any "svn-remote.*" config keys, we think you're using svn.
195 self.is_git_svn = RunGitWithCode(
196 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
197 return self.is_git_svn
198
199 def GetSVNBranch(self):
200 if self.svn_branch is None:
201 if not self.GetIsGitSvn():
202 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
203
204 # Try to figure out which remote branch we're based on.
205 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000206 # 1) iterate through our branch history and find the svn URL.
207 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000208
209 # regexp matching the git-svn line that contains the URL.
210 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
211
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000212 # We don't want to go through all of history, so read a line from the
213 # pipe at a time.
214 # The -100 is an arbitrary limit so we don't search forever.
215 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000216 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000217 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000218 for line in proc.stdout:
219 match = git_svn_re.match(line)
220 if match:
221 url = match.group(1)
222 proc.stdout.close() # Cut pipe.
223 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000224
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000225 if url:
226 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
227 remotes = RunGit(['config', '--get-regexp',
228 r'^svn-remote\..*\.url']).splitlines()
229 for remote in remotes:
230 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000231 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000232 remote = match.group(1)
233 base_url = match.group(2)
234 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000235 ['config', 'svn-remote.%s.fetch' % remote],
236 error_ok=True).strip()
237 if fetch_spec:
238 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
239 if self.svn_branch:
240 break
241 branch_spec = RunGit(
242 ['config', 'svn-remote.%s.branches' % remote],
243 error_ok=True).strip()
244 if branch_spec:
245 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
246 if self.svn_branch:
247 break
248 tag_spec = RunGit(
249 ['config', 'svn-remote.%s.tags' % remote],
250 error_ok=True).strip()
251 if tag_spec:
252 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
253 if self.svn_branch:
254 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000255
256 if not self.svn_branch:
257 DieWithError('Can\'t guess svn branch -- try specifying it on the '
258 'command line')
259
260 return self.svn_branch
261
262 def GetTreeStatusUrl(self, error_ok=False):
263 if not self.tree_status_url:
264 error_message = ('You must configure your tree status URL by running '
265 '"git cl config".')
266 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
267 error_ok=error_ok,
268 error_message=error_message)
269 return self.tree_status_url
270
271 def GetViewVCUrl(self):
272 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000273 self.viewvc_url = gclient_utils.UpgradeToHttps(
274 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 return self.viewvc_url
276
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000277 def GetDefaultCCList(self):
278 return self._GetConfig('rietveld.cc', error_ok=True)
279
ukai@chromium.orge8077812012-02-03 03:41:46 +0000280 def GetIsGerrit(self):
281 """Return true if this repo is assosiated with gerrit code review system."""
282 if self.is_gerrit is None:
283 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
284 return self.is_gerrit
285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286 def _GetConfig(self, param, **kwargs):
287 self.LazyUpdateIfNeeded()
288 return RunGit(['config', param], **kwargs).strip()
289
290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291def ShortBranchName(branch):
292 """Convert a name like 'refs/heads/foo' to just 'foo'."""
293 return branch.replace('refs/heads/', '')
294
295
296class Changelist(object):
297 def __init__(self, branchref=None):
298 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000299 global settings
300 if not settings:
301 # Happens when git_cl.py is used as a utility library.
302 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303 settings.GetDefaultServerUrl()
304 self.branchref = branchref
305 if self.branchref:
306 self.branch = ShortBranchName(self.branchref)
307 else:
308 self.branch = None
309 self.rietveld_server = None
310 self.upstream_branch = None
311 self.has_issue = False
312 self.issue = None
313 self.has_description = False
314 self.description = None
315 self.has_patchset = False
316 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000317 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000318 self.cc = None
319 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000320 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000321
322 def GetCCList(self):
323 """Return the users cc'd on this CL.
324
325 Return is a string suitable for passing to gcl with the --cc flag.
326 """
327 if self.cc is None:
328 base_cc = settings .GetDefaultCCList()
329 more_cc = ','.join(self.watchers)
330 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
331 return self.cc
332
333 def SetWatchers(self, watchers):
334 """Set the list of email addresses that should be cc'd based on the changed
335 files in this CL.
336 """
337 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000338
339 def GetBranch(self):
340 """Returns the short branch name, e.g. 'master'."""
341 if not self.branch:
342 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
343 self.branch = ShortBranchName(self.branchref)
344 return self.branch
345
346 def GetBranchRef(self):
347 """Returns the full branch name, e.g. 'refs/heads/master'."""
348 self.GetBranch() # Poke the lazy loader.
349 return self.branchref
350
351 def FetchUpstreamTuple(self):
352 """Returns a tuple containg remote and remote ref,
353 e.g. 'origin', 'refs/heads/master'
354 """
355 remote = '.'
356 branch = self.GetBranch()
357 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
358 error_ok=True).strip()
359 if upstream_branch:
360 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
361 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
363 error_ok=True).strip()
364 if upstream_branch:
365 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000367 # Fall back on trying a git-svn upstream branch.
368 if settings.GetIsGitSvn():
369 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000370 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000371 # Else, try to guess the origin remote.
372 remote_branches = RunGit(['branch', '-r']).split()
373 if 'origin/master' in remote_branches:
374 # Fall back on origin/master if it exits.
375 remote = 'origin'
376 upstream_branch = 'refs/heads/master'
377 elif 'origin/trunk' in remote_branches:
378 # Fall back on origin/trunk if it exists. Generally a shared
379 # git-svn clone
380 remote = 'origin'
381 upstream_branch = 'refs/heads/trunk'
382 else:
383 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000384Either pass complete "git diff"-style arguments, like
385 git cl upload origin/master
386or verify this branch is set up to track another (via the --track argument to
387"git checkout -b ...").""")
388
389 return remote, upstream_branch
390
391 def GetUpstreamBranch(self):
392 if self.upstream_branch is None:
393 remote, upstream_branch = self.FetchUpstreamTuple()
394 if remote is not '.':
395 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
396 self.upstream_branch = upstream_branch
397 return self.upstream_branch
398
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000399 def GetRemote(self):
400 if not self._remote:
401 self._remote = self.FetchUpstreamTuple()[0]
402 if self._remote == '.':
403
404 remotes = RunGit(['remote'], error_ok=True).split()
405 if len(remotes) == 1:
406 self._remote, = remotes
407 elif 'origin' in remotes:
408 self._remote = 'origin'
409 logging.warning('Could not determine which remote this change is '
410 'associated with, so defaulting to "%s". This may '
411 'not be what you want. You may prevent this message '
412 'by running "git svn info" as documented here: %s',
413 self._remote,
414 GIT_INSTRUCTIONS_URL)
415 else:
416 logging.warn('Could not determine which remote this change is '
417 'associated with. You may prevent this message by '
418 'running "git svn info" as documented here: %s',
419 GIT_INSTRUCTIONS_URL)
420 return self._remote
421
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000422 def GetGitBaseUrlFromConfig(self):
423 """Return the configured base URL from branch.<branchname>.baseurl.
424
425 Returns None if it is not set.
426 """
427 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
428 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 def GetRemoteUrl(self):
431 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
432
433 Returns None if there is no remote.
434 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000435 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
437
438 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000439 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
442 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000443 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 else:
445 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 self.has_issue = True
447 return self.issue
448
449 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000450 if not self.rietveld_server:
451 # If we're on a branch then get the server potentially associated
452 # with that branch.
453 if self.GetIssue():
454 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
455 ['config', self._RietveldServer()], error_ok=True).strip())
456 if not self.rietveld_server:
457 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 return self.rietveld_server
459
460 def GetIssueURL(self):
461 """Get the URL for a particular issue."""
462 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
463
464 def GetDescription(self, pretty=False):
465 if not self.has_description:
466 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000467 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000468 try:
469 self.description = self.RpcServer().get_description(issue).strip()
470 except urllib2.HTTPError, e:
471 if e.code == 404:
472 DieWithError(
473 ('\nWhile fetching the description for issue %d, received a '
474 '404 (not found)\n'
475 'error. It is likely that you deleted this '
476 'issue on the server. If this is the\n'
477 'case, please run\n\n'
478 ' git cl issue 0\n\n'
479 'to clear the association with the deleted issue. Then run '
480 'this command again.') % issue)
481 else:
482 DieWithError(
483 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 self.has_description = True
485 if pretty:
486 wrapper = textwrap.TextWrapper()
487 wrapper.initial_indent = wrapper.subsequent_indent = ' '
488 return wrapper.fill(self.description)
489 return self.description
490
491 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000492 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 if not self.has_patchset:
494 patchset = RunGit(['config', self._PatchsetSetting()],
495 error_ok=True).strip()
496 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000497 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498 else:
499 self.patchset = None
500 self.has_patchset = True
501 return self.patchset
502
503 def SetPatchset(self, patchset):
504 """Set this branch's patchset. If patchset=0, clears the patchset."""
505 if patchset:
506 RunGit(['config', self._PatchsetSetting(), str(patchset)])
507 else:
508 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000509 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000510 self.has_patchset = False
511
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000512 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000513 patchset = self.RpcServer().get_issue_properties(
514 int(issue), False)['patchsets'][-1]
515 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000516 '/download/issue%s_%s.diff' % (issue, patchset))
517
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000518 def SetIssue(self, issue):
519 """Set this branch's issue. If issue=0, clears the issue."""
520 if issue:
521 RunGit(['config', self._IssueSetting(), str(issue)])
522 if self.rietveld_server:
523 RunGit(['config', self._RietveldServer(), self.rietveld_server])
524 else:
525 RunGit(['config', '--unset', self._IssueSetting()])
526 self.SetPatchset(0)
527 self.has_issue = False
528
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000529 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000530 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
531 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000532
533 # We use the sha1 of HEAD as a name of this change.
534 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000535 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000536 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000537 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000538 except subprocess2.CalledProcessError:
539 DieWithError(
540 ('\nFailed to diff against upstream branch %s!\n\n'
541 'This branch probably doesn\'t exist anymore. To reset the\n'
542 'tracking branch, please run\n'
543 ' git branch --set-upstream %s trunk\n'
544 'replacing trunk with origin/master or the relevant branch') %
545 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000546
maruel@chromium.org52424302012-08-29 15:14:30 +0000547 issue = self.GetIssue()
548 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000549 if issue:
550 description = self.GetDescription()
551 else:
552 # If the change was never uploaded, use the log messages of all commits
553 # up to the branch point, as git cl upload will prefill the description
554 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000555 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
556 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000557
558 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000559 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000560 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000561 name,
562 description,
563 absroot,
564 files,
565 issue,
566 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000567 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000568
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000569 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
570 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
571 change = self.GetChange(upstream_branch, author)
572
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000573 # Apply watchlists on upload.
574 if not committing:
575 watchlist = watchlists.Watchlists(change.RepositoryRoot())
576 files = [f.LocalPath() for f in change.AffectedFiles()]
577 self.SetWatchers(watchlist.GetWatchersForPaths(files))
578
579 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000580 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000581 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000582 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000583 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000584 except presubmit_support.PresubmitFailure, e:
585 DieWithError(
586 ('%s\nMaybe your depot_tools is out of date?\n'
587 'If all fails, contact maruel@') % e)
588
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000590 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000591 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000592 self.RpcServer().update_description(issue, self.description)
593 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000595 def SetFlag(self, flag, value):
596 """Patchset must match."""
597 if not self.GetPatchset():
598 DieWithError('The patchset needs to match. Send another patchset.')
599 try:
600 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000601 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000602 except urllib2.HTTPError, e:
603 if e.code == 404:
604 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
605 if e.code == 403:
606 DieWithError(
607 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
608 'match?') % (self.GetIssue(), self.GetPatchset()))
609 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000611 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 """Returns an upload.RpcServer() to access this review's rietveld instance.
613 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000614 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000615 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
616 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000617 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618
619 def _IssueSetting(self):
620 """Return the git setting that stores this change's issue."""
621 return 'branch.%s.rietveldissue' % self.GetBranch()
622
623 def _PatchsetSetting(self):
624 """Return the git setting that stores this change's most recent patchset."""
625 return 'branch.%s.rietveldpatchset' % self.GetBranch()
626
627 def _RietveldServer(self):
628 """Returns the git setting that stores this change's rietveld server."""
629 return 'branch.%s.rietveldserver' % self.GetBranch()
630
631
632def GetCodereviewSettingsInteractively():
633 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000634 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 server = settings.GetDefaultServerUrl(error_ok=True)
636 prompt = 'Rietveld server (host[:port])'
637 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000638 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 if not server and not newserver:
640 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000641 if newserver:
642 newserver = gclient_utils.UpgradeToHttps(newserver)
643 if newserver != server:
644 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000646 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 prompt = caption
648 if initial:
649 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000650 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 if new_val == 'x':
652 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000653 elif new_val:
654 if is_url:
655 new_val = gclient_utils.UpgradeToHttps(new_val)
656 if new_val != initial:
657 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000659 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000661 'tree-status-url', False)
662 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000663
664 # TODO: configure a default branch to diff against, rather than this
665 # svn-based hackery.
666
667
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000668class ChangeDescription(object):
669 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000670 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000671 self.log_desc = log_desc
672 self.reviewers = reviewers
673 self.description = self.log_desc
674
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000675 def Prompt(self):
676 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000677# This will displayed on the codereview site.
678# The first line will also be used as the subject of the review.
679"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000680 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000681 if ('\nR=' not in self.description and
682 '\nTBR=' not in self.description and
683 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000684 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000685 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000686 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000687 content = content.rstrip('\n') + '\n'
688 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000689 if not content:
690 DieWithError('Running editor failed')
691 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000692 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000693 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000694 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000695
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000696 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000697 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000698 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000699 # Retrieves all reviewer lines
700 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000701 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000702 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000703 if reviewers:
704 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000705
706 def IsEmpty(self):
707 return not self.description
708
709
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710def FindCodereviewSettingsFile(filename='codereview.settings'):
711 """Finds the given file starting in the cwd and going up.
712
713 Only looks up to the top of the repository unless an
714 'inherit-review-settings-ok' file exists in the root of the repository.
715 """
716 inherit_ok_file = 'inherit-review-settings-ok'
717 cwd = os.getcwd()
718 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
719 if os.path.isfile(os.path.join(root, inherit_ok_file)):
720 root = '/'
721 while True:
722 if filename in os.listdir(cwd):
723 if os.path.isfile(os.path.join(cwd, filename)):
724 return open(os.path.join(cwd, filename))
725 if cwd == root:
726 break
727 cwd = os.path.dirname(cwd)
728
729
730def LoadCodereviewSettingsFromFile(fileobj):
731 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000732 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 def SetProperty(name, setting, unset_error_ok=False):
735 fullname = 'rietveld.' + name
736 if setting in keyvals:
737 RunGit(['config', fullname, keyvals[setting]])
738 else:
739 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
740
741 SetProperty('server', 'CODE_REVIEW_SERVER')
742 # Only server setting is required. Other settings can be absent.
743 # In that case, we ignore errors raised during option deletion attempt.
744 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
745 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
746 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
747
ukai@chromium.orge8077812012-02-03 03:41:46 +0000748 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
749 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
750 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000751
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
753 #should be of the form
754 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
755 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
756 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
757 keyvals['ORIGIN_URL_CONFIG']])
758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000759
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000760def urlretrieve(source, destination):
761 """urllib is broken for SSL connections via a proxy therefore we
762 can't use urllib.urlretrieve()."""
763 with open(destination, 'w') as f:
764 f.write(urllib2.urlopen(source).read())
765
766
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000767def DownloadHooks(force):
768 """downloads hooks
769
770 Args:
771 force: True to update hooks. False to install hooks if not present.
772 """
773 if not settings.GetIsGerrit():
774 return
775 server_url = settings.GetDefaultServerUrl()
776 src = '%s/tools/hooks/commit-msg' % server_url
777 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
778 if not os.access(dst, os.X_OK):
779 if os.path.exists(dst):
780 if not force:
781 return
782 os.remove(dst)
783 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000784 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000785 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
786 except Exception:
787 if os.path.exists(dst):
788 os.remove(dst)
789 DieWithError('\nFailed to download hooks from %s' % src)
790
791
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792@usage('[repo root containing codereview.settings]')
793def CMDconfig(parser, args):
794 """edit configuration for this tree"""
795
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000796 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 if len(args) == 0:
798 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000799 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 return 0
801
802 url = args[0]
803 if not url.endswith('codereview.settings'):
804 url = os.path.join(url, 'codereview.settings')
805
806 # Load code review settings and download hooks (if available).
807 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000808 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 return 0
810
811
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000812def CMDbaseurl(parser, args):
813 """get or set base-url for this branch"""
814 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
815 branch = ShortBranchName(branchref)
816 _, args = parser.parse_args(args)
817 if not args:
818 print("Current base-url:")
819 return RunGit(['config', 'branch.%s.base-url' % branch],
820 error_ok=False).strip()
821 else:
822 print("Setting base-url to %s" % args[0])
823 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
824 error_ok=False).strip()
825
826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827def CMDstatus(parser, args):
828 """show status of changelists"""
829 parser.add_option('--field',
830 help='print only specific field (desc|id|patch|url)')
831 (options, args) = parser.parse_args(args)
832
833 # TODO: maybe make show_branches a flag if necessary.
834 show_branches = not options.field
835
836 if show_branches:
837 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
838 if branches:
839 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000840 changes = (Changelist(branchref=b) for b in branches.splitlines())
841 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
842 alignment = max(5, max(len(b) for b in branches))
843 for branch in sorted(branches):
844 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
846 cl = Changelist()
847 if options.field:
848 if options.field.startswith('desc'):
849 print cl.GetDescription()
850 elif options.field == 'id':
851 issueid = cl.GetIssue()
852 if issueid:
853 print issueid
854 elif options.field == 'patch':
855 patchset = cl.GetPatchset()
856 if patchset:
857 print patchset
858 elif options.field == 'url':
859 url = cl.GetIssueURL()
860 if url:
861 print url
862 else:
863 print
864 print 'Current branch:',
865 if not cl.GetIssue():
866 print 'no issue assigned.'
867 return 0
868 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000869 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 print 'Issue description:'
871 print cl.GetDescription(pretty=True)
872 return 0
873
874
875@usage('[issue_number]')
876def CMDissue(parser, args):
877 """Set or display the current code review issue number.
878
879 Pass issue number 0 to clear the current issue.
880"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000881 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882
883 cl = Changelist()
884 if len(args) > 0:
885 try:
886 issue = int(args[0])
887 except ValueError:
888 DieWithError('Pass a number to set the issue or none to list it.\n'
889 'Maybe you want to run git cl status?')
890 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +0000891 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 return 0
893
894
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000895def CMDcomments(parser, args):
896 """show review comments of the current changelist"""
897 (_, args) = parser.parse_args(args)
898 if args:
899 parser.error('Unsupported argument: %s' % args)
900
901 cl = Changelist()
902 if cl.GetIssue():
903 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
904 for message in sorted(data['messages'], key=lambda x: x['date']):
905 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
906 if message['text'].strip():
907 print '\n'.join(' ' + l for l in message['text'].splitlines())
908 return 0
909
910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911def CreateDescriptionFromLog(args):
912 """Pulls out the commit log to use as a base for the CL description."""
913 log_args = []
914 if len(args) == 1 and not args[0].endswith('.'):
915 log_args = [args[0] + '..']
916 elif len(args) == 1 and args[0].endswith('...'):
917 log_args = [args[0][:-1]]
918 elif len(args) == 2:
919 log_args = [args[0] + '..' + args[1]]
920 else:
921 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000922 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923
924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925def CMDpresubmit(parser, args):
926 """run presubmit tests on the current changelist"""
927 parser.add_option('--upload', action='store_true',
928 help='Run upload hook instead of the push/dcommit hook')
929 (options, args) = parser.parse_args(args)
930
931 # Make sure index is up-to-date before running diff-index.
932 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
933 if RunGit(['diff-index', 'HEAD']):
934 # TODO(maruel): Is this really necessary?
935 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
936 return 1
937
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000938 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 if args:
940 base_branch = args[0]
941 else:
942 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000943 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000945 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000946 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000947 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000948 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949
950
ukai@chromium.orge8077812012-02-03 03:41:46 +0000951def GerritUpload(options, args, cl):
952 """upload the current branch to gerrit."""
953 # We assume the remote called "origin" is the one we want.
954 # It is probably not worthwhile to support different workflows.
955 remote = 'origin'
956 branch = 'master'
957 if options.target_branch:
958 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000960 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000961 if options.reviewers:
962 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000963 change_desc = ChangeDescription(log_desc, options.reviewers)
964 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000965 if change_desc.IsEmpty():
966 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 return 1
968
ukai@chromium.orge8077812012-02-03 03:41:46 +0000969 receive_options = []
970 cc = cl.GetCCList().split(',')
971 if options.cc:
972 cc += options.cc.split(',')
973 cc = filter(None, cc)
974 if cc:
975 receive_options += ['--cc=' + email for email in cc]
976 if change_desc.reviewers:
977 reviewers = filter(None, change_desc.reviewers.split(','))
978 if reviewers:
979 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980
ukai@chromium.orge8077812012-02-03 03:41:46 +0000981 git_command = ['push']
982 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000983 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000984 ' '.join(receive_options))
985 git_command += [remote, 'HEAD:refs/for/' + branch]
986 RunGit(git_command)
987 # TODO(ukai): parse Change-Id: and set issue number?
988 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000989
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000990
ukai@chromium.orge8077812012-02-03 03:41:46 +0000991def RietveldUpload(options, args, cl):
992 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993 upload_args = ['--assume_yes'] # Don't ask about untracked files.
994 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 if options.emulate_svn_auto_props:
996 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997
998 change_desc = None
999
1000 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001001 if options.title:
1002 upload_args.extend(['--title', options.title])
1003 elif options.message:
1004 # TODO(rogerta): for now, the -m option will also set the --title option
1005 # for upload.py. Soon this will be changed to set the --message option.
1006 # Will wait until people are used to typing -t instead of -m.
1007 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001008 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009 print ("This branch is associated with issue %s. "
1010 "Adding patch to that issue." % cl.GetIssue())
1011 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001012 if options.title:
1013 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001014 message = options.message or CreateDescriptionFromLog(args)
1015 change_desc = ChangeDescription(message, options.reviewers)
1016 if not options.force:
1017 change_desc.Prompt()
1018 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001019
1020 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 print "Description is empty; aborting."
1022 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001023
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001024 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001025 if change_desc.reviewers:
1026 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001027 if options.send_mail:
1028 if not change_desc.reviewers:
1029 DieWithError("Must specify reviewers to send email.")
1030 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001031 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001032 if cc:
1033 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034
1035 # Include the upstream repo's URL in the change -- this is useful for
1036 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001037 remote_url = cl.GetGitBaseUrlFromConfig()
1038 if not remote_url:
1039 if settings.GetIsGitSvn():
1040 # URL is dependent on the current directory.
1041 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1042 if data:
1043 keys = dict(line.split(': ', 1) for line in data.splitlines()
1044 if ': ' in line)
1045 remote_url = keys.get('URL', None)
1046 else:
1047 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1048 remote_url = (cl.GetRemoteUrl() + '@'
1049 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 if remote_url:
1051 upload_args.extend(['--base_url', remote_url])
1052
1053 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001054 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001055 except KeyboardInterrupt:
1056 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 except:
1058 # If we got an exception after the user typed a description for their
1059 # change, back up the description before re-raising.
1060 if change_desc:
1061 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1062 print '\nGot exception while uploading -- saving description to %s\n' \
1063 % backup_path
1064 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001065 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 backup_file.close()
1067 raise
1068
1069 if not cl.GetIssue():
1070 cl.SetIssue(issue)
1071 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001072
1073 if options.use_commit_queue:
1074 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 return 0
1076
1077
ukai@chromium.orge8077812012-02-03 03:41:46 +00001078@usage('[args to "git diff"]')
1079def CMDupload(parser, args):
1080 """upload the current changelist to codereview"""
1081 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1082 help='bypass upload presubmit hook')
1083 parser.add_option('-f', action='store_true', dest='force',
1084 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001085 parser.add_option('-m', dest='message', help='message for patchset')
1086 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001087 parser.add_option('-r', '--reviewers',
1088 help='reviewer email addresses')
1089 parser.add_option('--cc',
1090 help='cc email addresses')
1091 parser.add_option('--send-mail', action='store_true',
1092 help='send email to reviewer immediately')
1093 parser.add_option("--emulate_svn_auto_props", action="store_true",
1094 dest="emulate_svn_auto_props",
1095 help="Emulate Subversion's auto properties feature.")
1096 parser.add_option("--desc_from_logs", action="store_true",
1097 dest="from_logs",
1098 help="""Squashes git commit logs into change description and
1099 uses message as subject""")
1100 parser.add_option('-c', '--use-commit-queue', action='store_true',
1101 help='tell the commit queue to commit this patchset')
1102 if settings.GetIsGerrit():
1103 parser.add_option('--target_branch', dest='target_branch', default='master',
1104 help='target branch to upload')
1105 (options, args) = parser.parse_args(args)
1106
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001107 # Print warning if the user used the -m/--message argument. This will soon
1108 # change to -t/--title.
1109 if options.message:
1110 print >> sys.stderr, (
1111 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1112 'In the near future, -m or --message will send a message instead.\n'
1113 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001114
ukai@chromium.orge8077812012-02-03 03:41:46 +00001115 # Make sure index is up-to-date before running diff-index.
1116 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1117 if RunGit(['diff-index', 'HEAD']):
1118 print 'Cannot upload with a dirty tree. You must commit locally first.'
1119 return 1
1120
1121 cl = Changelist()
1122 if args:
1123 # TODO(ukai): is it ok for gerrit case?
1124 base_branch = args[0]
1125 else:
1126 # Default to diffing against the "upstream" branch.
1127 base_branch = cl.GetUpstreamBranch()
1128 args = [base_branch + "..."]
1129
1130 if not options.bypass_hooks:
1131 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1132 may_prompt=not options.force,
1133 verbose=options.verbose,
1134 author=None)
1135 if not hook_results.should_continue():
1136 return 1
1137 if not options.reviewers and hook_results.reviewers:
1138 options.reviewers = hook_results.reviewers
1139
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001140 print_stats(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001141 if settings.GetIsGerrit():
1142 return GerritUpload(options, args, cl)
1143 return RietveldUpload(options, args, cl)
1144
1145
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001146def IsSubmoduleMergeCommit(ref):
1147 # When submodules are added to the repo, we expect there to be a single
1148 # non-git-svn merge commit at remote HEAD with a signature comment.
1149 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001150 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001151 return RunGit(cmd) != ''
1152
1153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154def SendUpstream(parser, args, cmd):
1155 """Common code for CmdPush and CmdDCommit
1156
1157 Squashed commit into a single.
1158 Updates changelog with metadata (e.g. pointer to review).
1159 Pushes/dcommits the code upstream.
1160 Updates review and closes.
1161 """
1162 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1163 help='bypass upload presubmit hook')
1164 parser.add_option('-m', dest='message',
1165 help="override review description")
1166 parser.add_option('-f', action='store_true', dest='force',
1167 help="force yes to questions (don't prompt)")
1168 parser.add_option('-c', dest='contributor',
1169 help="external contributor for patch (appended to " +
1170 "description and used as author for git). Should be " +
1171 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 (options, args) = parser.parse_args(args)
1173 cl = Changelist()
1174
1175 if not args or cmd == 'push':
1176 # Default to merging against our best guess of the upstream branch.
1177 args = [cl.GetUpstreamBranch()]
1178
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001179 if options.contributor:
1180 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1181 print "Please provide contibutor as 'First Last <email@example.com>'"
1182 return 1
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001185 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001187 # Make sure index is up-to-date before running diff-index.
1188 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 if RunGit(['diff-index', 'HEAD']):
1190 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1191 return 1
1192
1193 # This rev-list syntax means "show all commits not in my branch that
1194 # are in base_branch".
1195 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1196 base_branch]).splitlines()
1197 if upstream_commits:
1198 print ('Base branch "%s" has %d commits '
1199 'not in this branch.' % (base_branch, len(upstream_commits)))
1200 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1201 return 1
1202
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001203 # This is the revision `svn dcommit` will commit on top of.
1204 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1205 '--pretty=format:%H'])
1206
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001208 # If the base_head is a submodule merge commit, the first parent of the
1209 # base_head should be a git-svn commit, which is what we're interested in.
1210 base_svn_head = base_branch
1211 if base_has_submodules:
1212 base_svn_head += '^1'
1213
1214 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if extra_commits:
1216 print ('This branch has %d additional commits not upstreamed yet.'
1217 % len(extra_commits.splitlines()))
1218 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1219 'before attempting to %s.' % (base_branch, cmd))
1220 return 1
1221
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001222 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001223 author = None
1224 if options.contributor:
1225 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001226 hook_results = cl.RunHook(
1227 committing=True,
1228 upstream_branch=base_branch,
1229 may_prompt=not options.force,
1230 verbose=options.verbose,
1231 author=author)
1232 if not hook_results.should_continue():
1233 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
1235 if cmd == 'dcommit':
1236 # Check the tree status if the tree status URL is set.
1237 status = GetTreeStatus()
1238 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001239 print('The tree is closed. Please wait for it to reopen. Use '
1240 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 return 1
1242 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001243 print('Unable to determine tree status. Please verify manually and '
1244 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001245 else:
1246 breakpad.SendStack(
1247 'GitClHooksBypassedCommit',
1248 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001249 (cl.GetRietveldServer(), cl.GetIssue()),
1250 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251
1252 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001253 if not description and cl.GetIssue():
1254 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001256 if not description:
1257 print 'No description set.'
1258 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1259 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001261 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
1264 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 description += "\nPatch from %s." % options.contributor
1266 print 'Description:', repr(description)
1267
1268 branches = [base_branch, cl.GetBranchRef()]
1269 if not options.force:
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001270 print_stats(branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001271 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001273 # We want to squash all this branch's commits into one commit with the proper
1274 # description. We do this by doing a "reset --soft" to the base branch (which
1275 # keeps the working copy the same), then dcommitting that. If origin/master
1276 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1277 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001279 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1280 # Delete the branches if they exist.
1281 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1282 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1283 result = RunGitWithCode(showref_cmd)
1284 if result[0] == 0:
1285 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286
1287 # We might be in a directory that's present in this branch but not in the
1288 # trunk. Move up to the top of the tree so that git commands that expect a
1289 # valid CWD won't fail after we check out the merge branch.
1290 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1291 if rel_base_path:
1292 os.chdir(rel_base_path)
1293
1294 # Stuff our change into the merge branch.
1295 # We wrap in a try...finally block so if anything goes wrong,
1296 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001297 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001299 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1300 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if options.contributor:
1302 RunGit(['commit', '--author', options.contributor, '-m', description])
1303 else:
1304 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001305 if base_has_submodules:
1306 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1307 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1308 RunGit(['checkout', CHERRY_PICK_BRANCH])
1309 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 if cmd == 'push':
1311 # push the merge branch.
1312 remote, branch = cl.FetchUpstreamTuple()
1313 retcode, output = RunGitWithCode(
1314 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1315 logging.debug(output)
1316 else:
1317 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001318 retcode, output = RunGitWithCode(['svn', 'dcommit',
1319 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 finally:
1321 # And then swap back to the original branch and clean up.
1322 RunGit(['checkout', '-q', cl.GetBranch()])
1323 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001324 if base_has_submodules:
1325 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326
1327 if cl.GetIssue():
1328 if cmd == 'dcommit' and 'Committed r' in output:
1329 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1330 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001331 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1332 for l in output.splitlines(False))
1333 match = filter(None, match)
1334 if len(match) != 1:
1335 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1336 output)
1337 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 else:
1339 return 1
1340 viewvc_url = settings.GetViewVCUrl()
1341 if viewvc_url and revision:
1342 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1343 print ('Closing issue '
1344 '(you may be prompted for your codereview password)...')
1345 cl.CloseIssue()
1346 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001347
1348 if retcode == 0:
1349 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1350 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001351 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 return 0
1354
1355
1356@usage('[upstream branch to apply against]')
1357def CMDdcommit(parser, args):
1358 """commit the current changelist via git-svn"""
1359 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001360 message = """This doesn't appear to be an SVN repository.
1361If your project has a git mirror with an upstream SVN master, you probably need
1362to run 'git svn init', see your project's git mirror documentation.
1363If your project has a true writeable upstream repository, you probably want
1364to run 'git cl push' instead.
1365Choose wisely, if you get this wrong, your commit might appear to succeed but
1366will instead be silently ignored."""
1367 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001368 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 return SendUpstream(parser, args, 'dcommit')
1370
1371
1372@usage('[upstream branch to apply against]')
1373def CMDpush(parser, args):
1374 """commit the current changelist via git"""
1375 if settings.GetIsGitSvn():
1376 print('This appears to be an SVN repository.')
1377 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001378 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return SendUpstream(parser, args, 'push')
1380
1381
1382@usage('<patch url or issue id>')
1383def CMDpatch(parser, args):
1384 """patch in a code review"""
1385 parser.add_option('-b', dest='newbranch',
1386 help='create a new branch off trunk for the patch')
1387 parser.add_option('-f', action='store_true', dest='force',
1388 help='with -b, clobber any existing branch')
1389 parser.add_option('--reject', action='store_true', dest='reject',
1390 help='allow failed patches and spew .rej files')
1391 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1392 help="don't commit after patch applies")
1393 (options, args) = parser.parse_args(args)
1394 if len(args) != 1:
1395 parser.print_help()
1396 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001397 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001399 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001400 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001401
maruel@chromium.org52424302012-08-29 15:14:30 +00001402 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001404 issue = int(issue_arg)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001405 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001407 # Assume it's a URL to the patch. Default to https.
1408 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001409 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001410 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 DieWithError('Must pass an issue ID or full URL for '
1412 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001413 issue = int(match.group(1))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001414 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
1416 if options.newbranch:
1417 if options.force:
1418 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001419 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 RunGit(['checkout', '-b', options.newbranch,
1421 Changelist().GetUpstreamBranch()])
1422
1423 # Switch up to the top-level directory, if necessary, in preparation for
1424 # applying the patch.
1425 top = RunGit(['rev-parse', '--show-cdup']).strip()
1426 if top:
1427 os.chdir(top)
1428
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 # Git patches have a/ at the beginning of source paths. We strip that out
1430 # with a sed script rather than the -p flag to patch so we can feed either
1431 # Git or svn-style patches into the same apply command.
1432 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001433 try:
1434 patch_data = subprocess2.check_output(
1435 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1436 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 DieWithError('Git patch mungling failed.')
1438 logging.info(patch_data)
1439 # We use "git apply" to apply the patch instead of "patch" so that we can
1440 # pick up file adds.
1441 # The --index flag means: also insert into the index (so we catch adds).
1442 cmd = ['git', 'apply', '--index', '-p0']
1443 if options.reject:
1444 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001445 try:
1446 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1447 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448 DieWithError('Failed to apply the patch')
1449
1450 # If we had an issue, commit the current state and register the issue.
1451 if not options.nocommit:
1452 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1453 cl = Changelist()
1454 cl.SetIssue(issue)
1455 print "Committed patch."
1456 else:
1457 print "Patch applied to index."
1458 return 0
1459
1460
1461def CMDrebase(parser, args):
1462 """rebase current branch on top of svn repo"""
1463 # Provide a wrapper for git svn rebase to help avoid accidental
1464 # git svn dcommit.
1465 # It's the only command that doesn't use parser at all since we just defer
1466 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001467 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
1469
1470def GetTreeStatus():
1471 """Fetches the tree status and returns either 'open', 'closed',
1472 'unknown' or 'unset'."""
1473 url = settings.GetTreeStatusUrl(error_ok=True)
1474 if url:
1475 status = urllib2.urlopen(url).read().lower()
1476 if status.find('closed') != -1 or status == '0':
1477 return 'closed'
1478 elif status.find('open') != -1 or status == '1':
1479 return 'open'
1480 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 return 'unset'
1482
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001483
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001484def GetTreeStatusReason():
1485 """Fetches the tree status from a json url and returns the message
1486 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001487 url = settings.GetTreeStatusUrl()
1488 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489 connection = urllib2.urlopen(json_url)
1490 status = json.loads(connection.read())
1491 connection.close()
1492 return status['message']
1493
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495def CMDtree(parser, args):
1496 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001497 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 status = GetTreeStatus()
1499 if 'unset' == status:
1500 print 'You must configure your tree status URL by running "git cl config".'
1501 return 2
1502
1503 print "The tree is %s" % status
1504 print
1505 print GetTreeStatusReason()
1506 if status != 'open':
1507 return 1
1508 return 0
1509
1510
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001511@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001513 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001514 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001515 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001516 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001517 return 0
1518
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001520 if args:
1521 # One arg means set upstream branch.
1522 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1523 cl = Changelist()
1524 print "Upstream branch set to " + cl.GetUpstreamBranch()
1525 else:
1526 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 return 0
1528
1529
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001530def CMDset_commit(parser, args):
1531 """set the commit bit"""
1532 _, args = parser.parse_args(args)
1533 if args:
1534 parser.error('Unrecognized args: %s' % ' '.join(args))
1535 cl = Changelist()
1536 cl.SetFlag('commit', '1')
1537 return 0
1538
1539
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540def Command(name):
1541 return getattr(sys.modules[__name__], 'CMD' + name, None)
1542
1543
1544def CMDhelp(parser, args):
1545 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001546 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 if len(args) == 1:
1548 return main(args + ['--help'])
1549 parser.print_help()
1550 return 0
1551
1552
1553def GenUsage(parser, command):
1554 """Modify an OptParse object with the function's documentation."""
1555 obj = Command(command)
1556 more = getattr(obj, 'usage_more', '')
1557 if command == 'help':
1558 command = '<command>'
1559 else:
1560 # OptParser.description prefer nicely non-formatted strings.
1561 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1562 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1563
1564
1565def main(argv):
1566 """Doesn't parse the arguments here, just find the right subcommand to
1567 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001568 if sys.hexversion < 0x02060000:
1569 print >> sys.stderr, (
1570 '\nYour python version %s is unsupported, please upgrade.\n' %
1571 sys.version.split(' ', 1)[0])
1572 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001573 # Reload settings.
1574 global settings
1575 settings = Settings()
1576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 # Do it late so all commands are listed.
1578 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1579 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1580 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1581
1582 # Create the option parse and add --verbose support.
1583 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001584 parser.add_option(
1585 '-v', '--verbose', action='count', default=0,
1586 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587 old_parser_args = parser.parse_args
1588 def Parse(args):
1589 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001590 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001592 elif options.verbose:
1593 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001594 else:
1595 logging.basicConfig(level=logging.WARNING)
1596 return options, args
1597 parser.parse_args = Parse
1598
1599 if argv:
1600 command = Command(argv[0])
1601 if command:
1602 # "fix" the usage and the description now that we know the subcommand.
1603 GenUsage(parser, argv[0])
1604 try:
1605 return command(parser, argv[1:])
1606 except urllib2.HTTPError, e:
1607 if e.code != 500:
1608 raise
1609 DieWithError(
1610 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1611 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1612
1613 # Not a known command. Default to help.
1614 GenUsage(parser, 'help')
1615 return CMDhelp(parser, argv)
1616
1617
1618if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001619 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001620 sys.exit(main(sys.argv[1:]))