blob: bca8760d14b797f1917269ff90c5a1170049f224 [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')
sbc@chromium.org495ad152012-09-04 23:07:42 +0000929 parser.add_option('--force', action='store_true',
930 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931 (options, args) = parser.parse_args(args)
932
933 # Make sure index is up-to-date before running diff-index.
934 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
sbc@chromium.org495ad152012-09-04 23:07:42 +0000935 if not options.force and RunGit(['diff-index', 'HEAD']):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 # TODO(maruel): Is this really necessary?
sbc@chromium.org495ad152012-09-04 23:07:42 +0000937 print ('Cannot presubmit with a dirty tree.\n'
938 'You must commit locally first (or use --force).')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 return 1
940
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000941 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942 if args:
943 base_branch = args[0]
944 else:
945 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000946 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000948 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000949 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000950 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000951 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952
953
ukai@chromium.orge8077812012-02-03 03:41:46 +0000954def GerritUpload(options, args, cl):
955 """upload the current branch to gerrit."""
956 # We assume the remote called "origin" is the one we want.
957 # It is probably not worthwhile to support different workflows.
958 remote = 'origin'
959 branch = 'master'
960 if options.target_branch:
961 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000963 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000964 if options.reviewers:
965 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000966 change_desc = ChangeDescription(log_desc, options.reviewers)
967 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000968 if change_desc.IsEmpty():
969 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 return 1
971
ukai@chromium.orge8077812012-02-03 03:41:46 +0000972 receive_options = []
973 cc = cl.GetCCList().split(',')
974 if options.cc:
975 cc += options.cc.split(',')
976 cc = filter(None, cc)
977 if cc:
978 receive_options += ['--cc=' + email for email in cc]
979 if change_desc.reviewers:
980 reviewers = filter(None, change_desc.reviewers.split(','))
981 if reviewers:
982 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983
ukai@chromium.orge8077812012-02-03 03:41:46 +0000984 git_command = ['push']
985 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000986 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000987 ' '.join(receive_options))
988 git_command += [remote, 'HEAD:refs/for/' + branch]
989 RunGit(git_command)
990 # TODO(ukai): parse Change-Id: and set issue number?
991 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000992
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993
ukai@chromium.orge8077812012-02-03 03:41:46 +0000994def RietveldUpload(options, args, cl):
995 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 upload_args = ['--assume_yes'] # Don't ask about untracked files.
997 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 if options.emulate_svn_auto_props:
999 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000
1001 change_desc = None
1002
1003 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001004 if options.title:
1005 upload_args.extend(['--title', options.title])
1006 elif options.message:
1007 # TODO(rogerta): for now, the -m option will also set the --title option
1008 # for upload.py. Soon this will be changed to set the --message option.
1009 # Will wait until people are used to typing -t instead of -m.
1010 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001011 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012 print ("This branch is associated with issue %s. "
1013 "Adding patch to that issue." % cl.GetIssue())
1014 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001015 if options.title:
1016 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001017 message = options.message or CreateDescriptionFromLog(args)
1018 change_desc = ChangeDescription(message, options.reviewers)
1019 if not options.force:
1020 change_desc.Prompt()
1021 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001022
1023 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 print "Description is empty; aborting."
1025 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001026
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001027 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001028 if change_desc.reviewers:
1029 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001030 if options.send_mail:
1031 if not change_desc.reviewers:
1032 DieWithError("Must specify reviewers to send email.")
1033 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001034 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001035 if cc:
1036 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 # Include the upstream repo's URL in the change -- this is useful for
1039 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001040 remote_url = cl.GetGitBaseUrlFromConfig()
1041 if not remote_url:
1042 if settings.GetIsGitSvn():
1043 # URL is dependent on the current directory.
1044 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1045 if data:
1046 keys = dict(line.split(': ', 1) for line in data.splitlines()
1047 if ': ' in line)
1048 remote_url = keys.get('URL', None)
1049 else:
1050 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1051 remote_url = (cl.GetRemoteUrl() + '@'
1052 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 if remote_url:
1054 upload_args.extend(['--base_url', remote_url])
1055
1056 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001057 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001058 except KeyboardInterrupt:
1059 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001060 except:
1061 # If we got an exception after the user typed a description for their
1062 # change, back up the description before re-raising.
1063 if change_desc:
1064 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1065 print '\nGot exception while uploading -- saving description to %s\n' \
1066 % backup_path
1067 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001068 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069 backup_file.close()
1070 raise
1071
1072 if not cl.GetIssue():
1073 cl.SetIssue(issue)
1074 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001075
1076 if options.use_commit_queue:
1077 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 return 0
1079
1080
ukai@chromium.orge8077812012-02-03 03:41:46 +00001081@usage('[args to "git diff"]')
1082def CMDupload(parser, args):
1083 """upload the current changelist to codereview"""
1084 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1085 help='bypass upload presubmit hook')
1086 parser.add_option('-f', action='store_true', dest='force',
1087 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001088 parser.add_option('-m', dest='message', help='message for patchset')
1089 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001090 parser.add_option('-r', '--reviewers',
1091 help='reviewer email addresses')
1092 parser.add_option('--cc',
1093 help='cc email addresses')
1094 parser.add_option('--send-mail', action='store_true',
1095 help='send email to reviewer immediately')
1096 parser.add_option("--emulate_svn_auto_props", action="store_true",
1097 dest="emulate_svn_auto_props",
1098 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001099 parser.add_option('-c', '--use-commit-queue', action='store_true',
1100 help='tell the commit queue to commit this patchset')
1101 if settings.GetIsGerrit():
1102 parser.add_option('--target_branch', dest='target_branch', default='master',
1103 help='target branch to upload')
1104 (options, args) = parser.parse_args(args)
1105
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001106 # Print warning if the user used the -m/--message argument. This will soon
1107 # change to -t/--title.
1108 if options.message:
1109 print >> sys.stderr, (
1110 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1111 'In the near future, -m or --message will send a message instead.\n'
1112 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001113
ukai@chromium.orge8077812012-02-03 03:41:46 +00001114 # Make sure index is up-to-date before running diff-index.
1115 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1116 if RunGit(['diff-index', 'HEAD']):
1117 print 'Cannot upload with a dirty tree. You must commit locally first.'
1118 return 1
1119
1120 cl = Changelist()
1121 if args:
1122 # TODO(ukai): is it ok for gerrit case?
1123 base_branch = args[0]
1124 else:
1125 # Default to diffing against the "upstream" branch.
1126 base_branch = cl.GetUpstreamBranch()
1127 args = [base_branch + "..."]
1128
1129 if not options.bypass_hooks:
1130 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1131 may_prompt=not options.force,
1132 verbose=options.verbose,
1133 author=None)
1134 if not hook_results.should_continue():
1135 return 1
1136 if not options.reviewers and hook_results.reviewers:
1137 options.reviewers = hook_results.reviewers
1138
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001139 print_stats(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001140 if settings.GetIsGerrit():
1141 return GerritUpload(options, args, cl)
1142 return RietveldUpload(options, args, cl)
1143
1144
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001145def IsSubmoduleMergeCommit(ref):
1146 # When submodules are added to the repo, we expect there to be a single
1147 # non-git-svn merge commit at remote HEAD with a signature comment.
1148 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001149 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001150 return RunGit(cmd) != ''
1151
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153def SendUpstream(parser, args, cmd):
1154 """Common code for CmdPush and CmdDCommit
1155
1156 Squashed commit into a single.
1157 Updates changelog with metadata (e.g. pointer to review).
1158 Pushes/dcommits the code upstream.
1159 Updates review and closes.
1160 """
1161 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1162 help='bypass upload presubmit hook')
1163 parser.add_option('-m', dest='message',
1164 help="override review description")
1165 parser.add_option('-f', action='store_true', dest='force',
1166 help="force yes to questions (don't prompt)")
1167 parser.add_option('-c', dest='contributor',
1168 help="external contributor for patch (appended to " +
1169 "description and used as author for git). Should be " +
1170 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 (options, args) = parser.parse_args(args)
1172 cl = Changelist()
1173
1174 if not args or cmd == 'push':
1175 # Default to merging against our best guess of the upstream branch.
1176 args = [cl.GetUpstreamBranch()]
1177
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001178 if options.contributor:
1179 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1180 print "Please provide contibutor as 'First Last <email@example.com>'"
1181 return 1
1182
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001184 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001186 # Make sure index is up-to-date before running diff-index.
1187 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 if RunGit(['diff-index', 'HEAD']):
1189 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1190 return 1
1191
1192 # This rev-list syntax means "show all commits not in my branch that
1193 # are in base_branch".
1194 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1195 base_branch]).splitlines()
1196 if upstream_commits:
1197 print ('Base branch "%s" has %d commits '
1198 'not in this branch.' % (base_branch, len(upstream_commits)))
1199 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1200 return 1
1201
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001202 # This is the revision `svn dcommit` will commit on top of.
1203 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1204 '--pretty=format:%H'])
1205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001207 # If the base_head is a submodule merge commit, the first parent of the
1208 # base_head should be a git-svn commit, which is what we're interested in.
1209 base_svn_head = base_branch
1210 if base_has_submodules:
1211 base_svn_head += '^1'
1212
1213 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 if extra_commits:
1215 print ('This branch has %d additional commits not upstreamed yet.'
1216 % len(extra_commits.splitlines()))
1217 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1218 'before attempting to %s.' % (base_branch, cmd))
1219 return 1
1220
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001221 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001222 author = None
1223 if options.contributor:
1224 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001225 hook_results = cl.RunHook(
1226 committing=True,
1227 upstream_branch=base_branch,
1228 may_prompt=not options.force,
1229 verbose=options.verbose,
1230 author=author)
1231 if not hook_results.should_continue():
1232 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
1234 if cmd == 'dcommit':
1235 # Check the tree status if the tree status URL is set.
1236 status = GetTreeStatus()
1237 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001238 print('The tree is closed. Please wait for it to reopen. Use '
1239 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 return 1
1241 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001242 print('Unable to determine tree status. Please verify manually and '
1243 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001244 else:
1245 breakpad.SendStack(
1246 'GitClHooksBypassedCommit',
1247 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001248 (cl.GetRietveldServer(), cl.GetIssue()),
1249 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250
1251 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001252 if not description and cl.GetIssue():
1253 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001255 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001256 if not cl.GetIssue() and options.bypass_hooks:
1257 description = CreateDescriptionFromLog([base_branch])
1258 else:
1259 print 'No description set.'
1260 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1261 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001263 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265
1266 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 description += "\nPatch from %s." % options.contributor
1268 print 'Description:', repr(description)
1269
1270 branches = [base_branch, cl.GetBranchRef()]
1271 if not options.force:
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001272 print_stats(branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001273 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001275 # We want to squash all this branch's commits into one commit with the proper
1276 # description. We do this by doing a "reset --soft" to the base branch (which
1277 # keeps the working copy the same), then dcommitting that. If origin/master
1278 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1279 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001281 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1282 # Delete the branches if they exist.
1283 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1284 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1285 result = RunGitWithCode(showref_cmd)
1286 if result[0] == 0:
1287 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
1289 # We might be in a directory that's present in this branch but not in the
1290 # trunk. Move up to the top of the tree so that git commands that expect a
1291 # valid CWD won't fail after we check out the merge branch.
1292 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1293 if rel_base_path:
1294 os.chdir(rel_base_path)
1295
1296 # Stuff our change into the merge branch.
1297 # We wrap in a try...finally block so if anything goes wrong,
1298 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001299 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001301 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1302 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 if options.contributor:
1304 RunGit(['commit', '--author', options.contributor, '-m', description])
1305 else:
1306 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001307 if base_has_submodules:
1308 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1309 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1310 RunGit(['checkout', CHERRY_PICK_BRANCH])
1311 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 if cmd == 'push':
1313 # push the merge branch.
1314 remote, branch = cl.FetchUpstreamTuple()
1315 retcode, output = RunGitWithCode(
1316 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1317 logging.debug(output)
1318 else:
1319 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001320 retcode, output = RunGitWithCode(['svn', 'dcommit',
1321 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 finally:
1323 # And then swap back to the original branch and clean up.
1324 RunGit(['checkout', '-q', cl.GetBranch()])
1325 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001326 if base_has_submodules:
1327 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328
1329 if cl.GetIssue():
1330 if cmd == 'dcommit' and 'Committed r' in output:
1331 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1332 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001333 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1334 for l in output.splitlines(False))
1335 match = filter(None, match)
1336 if len(match) != 1:
1337 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1338 output)
1339 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 else:
1341 return 1
1342 viewvc_url = settings.GetViewVCUrl()
1343 if viewvc_url and revision:
1344 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1345 print ('Closing issue '
1346 '(you may be prompted for your codereview password)...')
1347 cl.CloseIssue()
1348 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001349
1350 if retcode == 0:
1351 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1352 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001353 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 return 0
1356
1357
1358@usage('[upstream branch to apply against]')
1359def CMDdcommit(parser, args):
1360 """commit the current changelist via git-svn"""
1361 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001362 message = """This doesn't appear to be an SVN repository.
1363If your project has a git mirror with an upstream SVN master, you probably need
1364to run 'git svn init', see your project's git mirror documentation.
1365If your project has a true writeable upstream repository, you probably want
1366to run 'git cl push' instead.
1367Choose wisely, if you get this wrong, your commit might appear to succeed but
1368will instead be silently ignored."""
1369 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001370 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 return SendUpstream(parser, args, 'dcommit')
1372
1373
1374@usage('[upstream branch to apply against]')
1375def CMDpush(parser, args):
1376 """commit the current changelist via git"""
1377 if settings.GetIsGitSvn():
1378 print('This appears to be an SVN repository.')
1379 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001380 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 return SendUpstream(parser, args, 'push')
1382
1383
1384@usage('<patch url or issue id>')
1385def CMDpatch(parser, args):
1386 """patch in a code review"""
1387 parser.add_option('-b', dest='newbranch',
1388 help='create a new branch off trunk for the patch')
1389 parser.add_option('-f', action='store_true', dest='force',
1390 help='with -b, clobber any existing branch')
1391 parser.add_option('--reject', action='store_true', dest='reject',
1392 help='allow failed patches and spew .rej files')
1393 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1394 help="don't commit after patch applies")
1395 (options, args) = parser.parse_args(args)
1396 if len(args) != 1:
1397 parser.print_help()
1398 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001399 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001401 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001402 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001403
maruel@chromium.org52424302012-08-29 15:14:30 +00001404 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001406 issue = int(issue_arg)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001407 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001409 # Assume it's a URL to the patch. Default to https.
1410 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001411 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001412 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 DieWithError('Must pass an issue ID or full URL for '
1414 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001415 issue = int(match.group(1))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001416 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417
1418 if options.newbranch:
1419 if options.force:
1420 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001421 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 RunGit(['checkout', '-b', options.newbranch,
1423 Changelist().GetUpstreamBranch()])
1424
1425 # Switch up to the top-level directory, if necessary, in preparation for
1426 # applying the patch.
1427 top = RunGit(['rev-parse', '--show-cdup']).strip()
1428 if top:
1429 os.chdir(top)
1430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 # Git patches have a/ at the beginning of source paths. We strip that out
1432 # with a sed script rather than the -p flag to patch so we can feed either
1433 # Git or svn-style patches into the same apply command.
1434 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001435 try:
1436 patch_data = subprocess2.check_output(
1437 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1438 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 DieWithError('Git patch mungling failed.')
1440 logging.info(patch_data)
1441 # We use "git apply" to apply the patch instead of "patch" so that we can
1442 # pick up file adds.
1443 # The --index flag means: also insert into the index (so we catch adds).
1444 cmd = ['git', 'apply', '--index', '-p0']
1445 if options.reject:
1446 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001447 try:
1448 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1449 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 DieWithError('Failed to apply the patch')
1451
1452 # If we had an issue, commit the current state and register the issue.
1453 if not options.nocommit:
1454 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1455 cl = Changelist()
1456 cl.SetIssue(issue)
1457 print "Committed patch."
1458 else:
1459 print "Patch applied to index."
1460 return 0
1461
1462
1463def CMDrebase(parser, args):
1464 """rebase current branch on top of svn repo"""
1465 # Provide a wrapper for git svn rebase to help avoid accidental
1466 # git svn dcommit.
1467 # It's the only command that doesn't use parser at all since we just defer
1468 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001469 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470
1471
1472def GetTreeStatus():
1473 """Fetches the tree status and returns either 'open', 'closed',
1474 'unknown' or 'unset'."""
1475 url = settings.GetTreeStatusUrl(error_ok=True)
1476 if url:
1477 status = urllib2.urlopen(url).read().lower()
1478 if status.find('closed') != -1 or status == '0':
1479 return 'closed'
1480 elif status.find('open') != -1 or status == '1':
1481 return 'open'
1482 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483 return 'unset'
1484
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001485
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486def GetTreeStatusReason():
1487 """Fetches the tree status from a json url and returns the message
1488 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001489 url = settings.GetTreeStatusUrl()
1490 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491 connection = urllib2.urlopen(json_url)
1492 status = json.loads(connection.read())
1493 connection.close()
1494 return status['message']
1495
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001496
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497def CMDtree(parser, args):
1498 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001499 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 status = GetTreeStatus()
1501 if 'unset' == status:
1502 print 'You must configure your tree status URL by running "git cl config".'
1503 return 2
1504
1505 print "The tree is %s" % status
1506 print
1507 print GetTreeStatusReason()
1508 if status != 'open':
1509 return 1
1510 return 0
1511
1512
maruel@chromium.org15192402012-09-06 12:38:29 +00001513def CMDtry(parser, args):
1514 """Triggers a try job through Rietveld."""
1515 group = optparse.OptionGroup(parser, "Try job options")
1516 group.add_option(
1517 "-b", "--bot", action="append",
1518 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1519 "times to specify multiple builders. ex: "
1520 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1521 "the try server waterfall for the builders name and the tests "
1522 "available. Can also be used to specify gtest_filter, e.g. "
1523 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1524 group.add_option(
1525 "-r", "--revision",
1526 help="Revision to use for the try job; default: the "
1527 "revision will be determined by the try server; see "
1528 "its waterfall for more info")
1529 group.add_option(
1530 "-c", "--clobber", action="store_true", default=False,
1531 help="Force a clobber before building; e.g. don't do an "
1532 "incremental build")
1533 group.add_option(
1534 "--project",
1535 help="Override which project to use. Projects are defined "
1536 "server-side to define what default bot set to use")
1537 group.add_option(
1538 "-t", "--testfilter", action="append", default=[],
1539 help=("Apply a testfilter to all the selected builders. Unless the "
1540 "builders configurations are similar, use multiple "
1541 "--bot <builder>:<test> arguments."))
1542 group.add_option(
1543 "-n", "--name", help="Try job name; default to current branch name")
1544 parser.add_option_group(group)
1545 options, args = parser.parse_args(args)
1546
1547 if args:
1548 parser.error('Unknown arguments: %s' % args)
1549
1550 cl = Changelist()
1551 if not cl.GetIssue():
1552 parser.error('Need to upload first')
1553
1554 if not options.name:
1555 options.name = cl.GetBranch()
1556
1557 # Process --bot and --testfilter.
1558 if not options.bot:
1559 # Get try slaves from PRESUBMIT.py files if not specified.
1560 change = cl.GetChange(cl.GetUpstreamBranch(), None)
1561 options.bot = presubmit_support.DoGetTrySlaves(
1562 change,
1563 change.LocalPaths(),
1564 settings.GetRoot(),
1565 None,
1566 None,
1567 options.verbose,
1568 sys.stdout)
1569 if not options.bot:
1570 parser.error('No default try builder to try, use --bot')
1571
1572 builders_and_tests = {}
1573 for bot in options.bot:
1574 if ':' in bot:
1575 builder, tests = bot.split(':', 1)
1576 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1577 elif ',' in bot:
1578 parser.error('Specify one bot per --bot flag')
1579 else:
1580 builders_and_tests.setdefault(bot, []).append('defaulttests')
1581
1582 if options.testfilter:
1583 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1584 builders_and_tests = dict(
1585 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1586 if t != ['compile'])
1587
1588 patchset = cl.GetPatchset()
1589 if not cl.GetPatchset():
1590 properties = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
1591 patchset = properties['patchsets'][-1]
1592
1593 cl.RpcServer().trigger_try_jobs(
1594 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1595 builders_and_tests)
1596 return 0
1597
1598
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001599@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001601 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001602 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001603 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001604 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001605 return 0
1606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001608 if args:
1609 # One arg means set upstream branch.
1610 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1611 cl = Changelist()
1612 print "Upstream branch set to " + cl.GetUpstreamBranch()
1613 else:
1614 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001615 return 0
1616
1617
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001618def CMDset_commit(parser, args):
1619 """set the commit bit"""
1620 _, args = parser.parse_args(args)
1621 if args:
1622 parser.error('Unrecognized args: %s' % ' '.join(args))
1623 cl = Changelist()
1624 cl.SetFlag('commit', '1')
1625 return 0
1626
1627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628def Command(name):
1629 return getattr(sys.modules[__name__], 'CMD' + name, None)
1630
1631
1632def CMDhelp(parser, args):
1633 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001634 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001635 if len(args) == 1:
1636 return main(args + ['--help'])
1637 parser.print_help()
1638 return 0
1639
1640
1641def GenUsage(parser, command):
1642 """Modify an OptParse object with the function's documentation."""
1643 obj = Command(command)
1644 more = getattr(obj, 'usage_more', '')
1645 if command == 'help':
1646 command = '<command>'
1647 else:
1648 # OptParser.description prefer nicely non-formatted strings.
1649 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1650 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1651
1652
1653def main(argv):
1654 """Doesn't parse the arguments here, just find the right subcommand to
1655 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001656 if sys.hexversion < 0x02060000:
1657 print >> sys.stderr, (
1658 '\nYour python version %s is unsupported, please upgrade.\n' %
1659 sys.version.split(' ', 1)[0])
1660 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001661 # Reload settings.
1662 global settings
1663 settings = Settings()
1664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665 # Do it late so all commands are listed.
1666 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1667 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1668 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1669
1670 # Create the option parse and add --verbose support.
1671 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001672 parser.add_option(
1673 '-v', '--verbose', action='count', default=0,
1674 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675 old_parser_args = parser.parse_args
1676 def Parse(args):
1677 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001678 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001679 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001680 elif options.verbose:
1681 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 else:
1683 logging.basicConfig(level=logging.WARNING)
1684 return options, args
1685 parser.parse_args = Parse
1686
1687 if argv:
1688 command = Command(argv[0])
1689 if command:
1690 # "fix" the usage and the description now that we know the subcommand.
1691 GenUsage(parser, argv[0])
1692 try:
1693 return command(parser, argv[1:])
1694 except urllib2.HTTPError, e:
1695 if e.code != 500:
1696 raise
1697 DieWithError(
1698 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1699 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1700
1701 # Not a known command. Default to help.
1702 GenUsage(parser, 'help')
1703 return CMDhelp(parser, argv)
1704
1705
1706if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001707 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 sys.exit(main(sys.argv[1:]))