blob: 42afb630d7fa6375e4aec355881c594f459abcd2 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# 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.orgd4b4e112011-09-09 11:44:10 +000010import errno
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +000015import subprocess
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000017import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000019import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import urllib2
21
22try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000023 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024except ImportError:
25 pass
26
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000027try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000028 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000029except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000030 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000031 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032 except ImportError:
33 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000034 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000035 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036
37
38from third_party import upload
39import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000040import fix_encoding
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000042import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import scm
44import watchlists
45
46
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +000047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DEFAULT_SERVER = 'http://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000049POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
51
maruel@chromium.org90541732011-04-01 17:54:18 +000052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000054 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 sys.exit(1)
56
57
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +000058def Popen(cmd, **kwargs):
59 """Wrapper for subprocess.Popen() that logs and watch for cygwin issues"""
60 logging.debug('Popen: ' + ' '.join(cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 try:
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +000062 return subprocess.Popen(cmd, **kwargs)
63 except OSError, e:
64 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 DieWithError(
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +000066 'Visit '
67 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
68 'learn how to fix this error; you need to rebase your cygwin dlls')
69 raise
70
71
72def RunCommand(cmd, error_ok=False, error_message=None,
73 redirect_stdout=True, swallow_stderr=False, **kwargs):
74 if redirect_stdout:
75 stdout = subprocess.PIPE
76 else:
77 stdout = None
78 if swallow_stderr:
79 stderr = subprocess.PIPE
80 else:
81 stderr = None
82 proc = Popen(cmd, stdout=stdout, stderr=stderr, **kwargs)
83 output = proc.communicate()[0]
84 if not error_ok and proc.returncode != 0:
85 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) +
86 (error_message or output or ''))
87 return output
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088
89
90def RunGit(args, **kwargs):
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +000091 cmd = ['git'] + args
92 return RunCommand(cmd, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
95def RunGitWithCode(args):
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +000096 proc = Popen(['git'] + args, stdout=subprocess.PIPE)
97 output = proc.communicate()[0]
98 return proc.returncode, output
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099
100
101def usage(more):
102 def hook(fn):
103 fn.usage_more = more
104 return fn
105 return hook
106
107
maruel@chromium.org90541732011-04-01 17:54:18 +0000108def ask_for_data(prompt):
109 try:
110 return raw_input(prompt)
111 except KeyboardInterrupt:
112 # Hide the exception.
113 sys.exit(1)
114
115
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116def FixUrl(server):
117 """Fix a server url to defaults protocol to http:// if none is specified."""
118 if not server:
119 return server
120 if not re.match(r'[a-z]+\://.*', server):
121 return 'http://' + server
122 return server
123
124
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000125def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
126 """Return the corresponding git ref if |base_url| together with |glob_spec|
127 matches the full |url|.
128
129 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
130 """
131 fetch_suburl, as_ref = glob_spec.split(':')
132 if allow_wildcards:
133 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
134 if glob_match:
135 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
136 # "branches/{472,597,648}/src:refs/remotes/svn/*".
137 branch_re = re.escape(base_url)
138 if glob_match.group(1):
139 branch_re += '/' + re.escape(glob_match.group(1))
140 wildcard = glob_match.group(2)
141 if wildcard == '*':
142 branch_re += '([^/]*)'
143 else:
144 # Escape and replace surrounding braces with parentheses and commas
145 # with pipe symbols.
146 wildcard = re.escape(wildcard)
147 wildcard = re.sub('^\\\\{', '(', wildcard)
148 wildcard = re.sub('\\\\,', '|', wildcard)
149 wildcard = re.sub('\\\\}$', ')', wildcard)
150 branch_re += wildcard
151 if glob_match.group(3):
152 branch_re += re.escape(glob_match.group(3))
153 match = re.match(branch_re, url)
154 if match:
155 return re.sub('\*$', match.group(1), as_ref)
156
157 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
158 if fetch_suburl:
159 full_url = base_url + '/' + fetch_suburl
160 else:
161 full_url = base_url
162 if full_url == url:
163 return as_ref
164 return None
165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000166class Settings(object):
167 def __init__(self):
168 self.default_server = None
169 self.cc = None
170 self.root = None
171 self.is_git_svn = None
172 self.svn_branch = None
173 self.tree_status_url = None
174 self.viewvc_url = None
175 self.updated = False
176
177 def LazyUpdateIfNeeded(self):
178 """Updates the settings from a codereview.settings file, if available."""
179 if not self.updated:
180 cr_settings_file = FindCodereviewSettingsFile()
181 if cr_settings_file:
182 LoadCodereviewSettingsFromFile(cr_settings_file)
183 self.updated = True
184
185 def GetDefaultServerUrl(self, error_ok=False):
186 if not self.default_server:
187 self.LazyUpdateIfNeeded()
188 self.default_server = FixUrl(self._GetConfig('rietveld.server',
189 error_ok=True))
190 if error_ok:
191 return self.default_server
192 if not self.default_server:
193 error_message = ('Could not find settings file. You must configure '
194 'your review setup by running "git cl config".')
195 self.default_server = FixUrl(self._GetConfig(
196 'rietveld.server', error_message=error_message))
197 return self.default_server
198
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000199 def GetRoot(self):
200 if not self.root:
201 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
202 return self.root
203
204 def GetIsGitSvn(self):
205 """Return true if this repo looks like it's using git-svn."""
206 if self.is_git_svn is None:
207 # If you have any "svn-remote.*" config keys, we think you're using svn.
208 self.is_git_svn = RunGitWithCode(
209 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
210 return self.is_git_svn
211
212 def GetSVNBranch(self):
213 if self.svn_branch is None:
214 if not self.GetIsGitSvn():
215 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
216
217 # Try to figure out which remote branch we're based on.
218 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000219 # 1) iterate through our branch history and find the svn URL.
220 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000221
222 # regexp matching the git-svn line that contains the URL.
223 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
224
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000225 # We don't want to go through all of history, so read a line from the
226 # pipe at a time.
227 # The -100 is an arbitrary limit so we don't search forever.
228 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +0000229 proc = Popen(cmd, stdout=subprocess.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000230 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000231 for line in proc.stdout:
232 match = git_svn_re.match(line)
233 if match:
234 url = match.group(1)
235 proc.stdout.close() # Cut pipe.
236 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000237
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000238 if url:
239 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
240 remotes = RunGit(['config', '--get-regexp',
241 r'^svn-remote\..*\.url']).splitlines()
242 for remote in remotes:
243 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000244 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000245 remote = match.group(1)
246 base_url = match.group(2)
247 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000248 ['config', 'svn-remote.%s.fetch' % remote],
249 error_ok=True).strip()
250 if fetch_spec:
251 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
252 if self.svn_branch:
253 break
254 branch_spec = RunGit(
255 ['config', 'svn-remote.%s.branches' % remote],
256 error_ok=True).strip()
257 if branch_spec:
258 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
259 if self.svn_branch:
260 break
261 tag_spec = RunGit(
262 ['config', 'svn-remote.%s.tags' % remote],
263 error_ok=True).strip()
264 if tag_spec:
265 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
266 if self.svn_branch:
267 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268
269 if not self.svn_branch:
270 DieWithError('Can\'t guess svn branch -- try specifying it on the '
271 'command line')
272
273 return self.svn_branch
274
275 def GetTreeStatusUrl(self, error_ok=False):
276 if not self.tree_status_url:
277 error_message = ('You must configure your tree status URL by running '
278 '"git cl config".')
279 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
280 error_ok=error_ok,
281 error_message=error_message)
282 return self.tree_status_url
283
284 def GetViewVCUrl(self):
285 if not self.viewvc_url:
286 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
287 return self.viewvc_url
288
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000289 def GetDefaultCCList(self):
290 return self._GetConfig('rietveld.cc', error_ok=True)
291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292 def _GetConfig(self, param, **kwargs):
293 self.LazyUpdateIfNeeded()
294 return RunGit(['config', param], **kwargs).strip()
295
296
297settings = Settings()
298
299
300did_migrate_check = False
301def CheckForMigration():
302 """Migrate from the old issue format, if found.
303
304 We used to store the branch<->issue mapping in a file in .git, but it's
305 better to store it in the .git/config, since deleting a branch deletes that
306 branch's entry there.
307 """
308
309 # Don't run more than once.
310 global did_migrate_check
311 if did_migrate_check:
312 return
313
314 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
315 storepath = os.path.join(gitdir, 'cl-mapping')
316 if os.path.exists(storepath):
317 print "old-style git-cl mapping file (%s) found; migrating." % storepath
318 store = open(storepath, 'r')
319 for line in store:
320 branch, issue = line.strip().split()
321 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
322 issue])
323 store.close()
324 os.remove(storepath)
325 did_migrate_check = True
326
327
328def ShortBranchName(branch):
329 """Convert a name like 'refs/heads/foo' to just 'foo'."""
330 return branch.replace('refs/heads/', '')
331
332
333class Changelist(object):
334 def __init__(self, branchref=None):
335 # Poke settings so we get the "configure your server" message if necessary.
336 settings.GetDefaultServerUrl()
337 self.branchref = branchref
338 if self.branchref:
339 self.branch = ShortBranchName(self.branchref)
340 else:
341 self.branch = None
342 self.rietveld_server = None
343 self.upstream_branch = None
344 self.has_issue = False
345 self.issue = None
346 self.has_description = False
347 self.description = None
348 self.has_patchset = False
349 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000350 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000351 self.cc = None
352 self.watchers = ()
353
354 def GetCCList(self):
355 """Return the users cc'd on this CL.
356
357 Return is a string suitable for passing to gcl with the --cc flag.
358 """
359 if self.cc is None:
360 base_cc = settings .GetDefaultCCList()
361 more_cc = ','.join(self.watchers)
362 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
363 return self.cc
364
365 def SetWatchers(self, watchers):
366 """Set the list of email addresses that should be cc'd based on the changed
367 files in this CL.
368 """
369 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000370
371 def GetBranch(self):
372 """Returns the short branch name, e.g. 'master'."""
373 if not self.branch:
374 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
375 self.branch = ShortBranchName(self.branchref)
376 return self.branch
377
378 def GetBranchRef(self):
379 """Returns the full branch name, e.g. 'refs/heads/master'."""
380 self.GetBranch() # Poke the lazy loader.
381 return self.branchref
382
383 def FetchUpstreamTuple(self):
384 """Returns a tuple containg remote and remote ref,
385 e.g. 'origin', 'refs/heads/master'
386 """
387 remote = '.'
388 branch = self.GetBranch()
389 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
390 error_ok=True).strip()
391 if upstream_branch:
392 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
393 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000394 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
395 error_ok=True).strip()
396 if upstream_branch:
397 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000398 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000399 # Fall back on trying a git-svn upstream branch.
400 if settings.GetIsGitSvn():
401 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000403 # Else, try to guess the origin remote.
404 remote_branches = RunGit(['branch', '-r']).split()
405 if 'origin/master' in remote_branches:
406 # Fall back on origin/master if it exits.
407 remote = 'origin'
408 upstream_branch = 'refs/heads/master'
409 elif 'origin/trunk' in remote_branches:
410 # Fall back on origin/trunk if it exists. Generally a shared
411 # git-svn clone
412 remote = 'origin'
413 upstream_branch = 'refs/heads/trunk'
414 else:
415 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000416Either pass complete "git diff"-style arguments, like
417 git cl upload origin/master
418or verify this branch is set up to track another (via the --track argument to
419"git checkout -b ...").""")
420
421 return remote, upstream_branch
422
423 def GetUpstreamBranch(self):
424 if self.upstream_branch is None:
425 remote, upstream_branch = self.FetchUpstreamTuple()
426 if remote is not '.':
427 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
428 self.upstream_branch = upstream_branch
429 return self.upstream_branch
430
431 def GetRemoteUrl(self):
432 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
433
434 Returns None if there is no remote.
435 """
436 remote = self.FetchUpstreamTuple()[0]
437 if remote == '.':
438 return None
439 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
440
441 def GetIssue(self):
442 if not self.has_issue:
443 CheckForMigration()
444 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
445 if issue:
446 self.issue = issue
447 self.rietveld_server = FixUrl(RunGit(
448 ['config', self._RietveldServer()], error_ok=True).strip())
449 else:
450 self.issue = None
451 if not self.rietveld_server:
452 self.rietveld_server = settings.GetDefaultServerUrl()
453 self.has_issue = True
454 return self.issue
455
456 def GetRietveldServer(self):
457 self.GetIssue()
458 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.org27bb3872011-05-30 20:33:19 +0000467 self.description = self.RpcServer().get_description(
468 int(self.GetIssue())).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469 self.has_description = True
470 if pretty:
471 wrapper = textwrap.TextWrapper()
472 wrapper.initial_indent = wrapper.subsequent_indent = ' '
473 return wrapper.fill(self.description)
474 return self.description
475
476 def GetPatchset(self):
477 if not self.has_patchset:
478 patchset = RunGit(['config', self._PatchsetSetting()],
479 error_ok=True).strip()
480 if patchset:
481 self.patchset = patchset
482 else:
483 self.patchset = None
484 self.has_patchset = True
485 return self.patchset
486
487 def SetPatchset(self, patchset):
488 """Set this branch's patchset. If patchset=0, clears the patchset."""
489 if patchset:
490 RunGit(['config', self._PatchsetSetting(), str(patchset)])
491 else:
492 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +0000493 swallow_stderr=True, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 self.has_patchset = False
495
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000496 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000497 patchset = self.RpcServer().get_issue_properties(
498 int(issue), False)['patchsets'][-1]
499 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000500 '/download/issue%s_%s.diff' % (issue, patchset))
501
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000502 def SetIssue(self, issue):
503 """Set this branch's issue. If issue=0, clears the issue."""
504 if issue:
505 RunGit(['config', self._IssueSetting(), str(issue)])
506 if self.rietveld_server:
507 RunGit(['config', self._RietveldServer(), self.rietveld_server])
508 else:
509 RunGit(['config', '--unset', self._IssueSetting()])
510 self.SetPatchset(0)
511 self.has_issue = False
512
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000513 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000514 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000515 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
516 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000517
518 # We use the sha1 of HEAD as a name of this change.
519 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000520 # Need to pass a relative path for msysgit.
521 files = scm.GIT.CaptureStatus([root], upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000522
523 issue = ConvertToInteger(self.GetIssue())
524 patchset = ConvertToInteger(self.GetPatchset())
525 if issue:
526 description = self.GetDescription()
527 else:
528 # If the change was never uploaded, use the log messages of all commits
529 # up to the branch point, as git cl upload will prefill the description
530 # with these log messages.
531 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
532 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000533
534 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000535 author = RunGit(['config', 'user.email']).strip() or None
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000536 change = presubmit_support.GitChange(
537 name,
538 description,
539 absroot,
540 files,
541 issue,
542 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000543 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000544
545 # Apply watchlists on upload.
546 if not committing:
547 watchlist = watchlists.Watchlists(change.RepositoryRoot())
548 files = [f.LocalPath() for f in change.AffectedFiles()]
549 self.SetWatchers(watchlist.GetWatchersForPaths(files))
550
551 try:
552 output = presubmit_support.DoPresubmitChecks(change, committing,
553 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000554 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000555 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000556 except presubmit_support.PresubmitFailure, e:
557 DieWithError(
558 ('%s\nMaybe your depot_tools is out of date?\n'
559 'If all fails, contact maruel@') % e)
560
561 # TODO(dpranke): We should propagate the error out instead of calling
562 # exit().
563 if not output.should_continue():
564 sys.exit(1)
565
566 return output
567
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000568 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000569 """Updates the description and closes the issue."""
570 issue = int(self.GetIssue())
571 self.RpcServer().update_description(issue, self.description)
572 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000574 def SetFlag(self, flag, value):
575 """Patchset must match."""
576 if not self.GetPatchset():
577 DieWithError('The patchset needs to match. Send another patchset.')
578 try:
579 return self.RpcServer().set_flag(
580 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
581 except urllib2.HTTPError, e:
582 if e.code == 404:
583 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
584 if e.code == 403:
585 DieWithError(
586 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
587 'match?') % (self.GetIssue(), self.GetPatchset()))
588 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000590 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591 """Returns an upload.RpcServer() to access this review's rietveld instance.
592 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000593 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000594 self.GetIssue()
595 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000596 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597
598 def _IssueSetting(self):
599 """Return the git setting that stores this change's issue."""
600 return 'branch.%s.rietveldissue' % self.GetBranch()
601
602 def _PatchsetSetting(self):
603 """Return the git setting that stores this change's most recent patchset."""
604 return 'branch.%s.rietveldpatchset' % self.GetBranch()
605
606 def _RietveldServer(self):
607 """Returns the git setting that stores this change's rietveld server."""
608 return 'branch.%s.rietveldserver' % self.GetBranch()
609
610
611def GetCodereviewSettingsInteractively():
612 """Prompt the user for settings."""
613 server = settings.GetDefaultServerUrl(error_ok=True)
614 prompt = 'Rietveld server (host[:port])'
615 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000616 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617 if not server and not newserver:
618 newserver = DEFAULT_SERVER
619 if newserver and newserver != server:
620 RunGit(['config', 'rietveld.server', newserver])
621
622 def SetProperty(initial, caption, name):
623 prompt = caption
624 if initial:
625 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000626 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 if new_val == 'x':
628 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
629 elif new_val and new_val != initial:
630 RunGit(['config', 'rietveld.' + name, new_val])
631
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000632 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
634 'tree-status-url')
635 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
636
637 # TODO: configure a default branch to diff against, rather than this
638 # svn-based hackery.
639
640
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000641class ChangeDescription(object):
642 """Contains a parsed form of the change description."""
643 def __init__(self, subject, log_desc, reviewers):
644 self.subject = subject
645 self.log_desc = log_desc
646 self.reviewers = reviewers
647 self.description = self.log_desc
648
649 def Update(self):
650 initial_text = """# Enter a description of the change.
651# This will displayed on the codereview site.
652# The first line will also be used as the subject of the review.
653"""
654 initial_text += self.description
655 if 'R=' not in self.description and self.reviewers:
656 initial_text += '\nR=' + self.reviewers
657 if 'BUG=' not in self.description:
658 initial_text += '\nBUG='
659 if 'TEST=' not in self.description:
660 initial_text += '\nTEST='
661 self._ParseDescription(UserEditedLog(initial_text))
662
663 def _ParseDescription(self, description):
664 if not description:
665 self.description = description
666 return
667
668 parsed_lines = []
669 reviewers_regexp = re.compile('\s*R=(.+)')
670 reviewers = ''
671 subject = ''
672 for l in description.splitlines():
673 if not subject:
674 subject = l
675 matched_reviewers = reviewers_regexp.match(l)
676 if matched_reviewers:
677 reviewers = matched_reviewers.group(1)
678 parsed_lines.append(l)
679
680 self.description = '\n'.join(parsed_lines) + '\n'
681 self.subject = subject
682 self.reviewers = reviewers
683
684 def IsEmpty(self):
685 return not self.description
686
687
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688def FindCodereviewSettingsFile(filename='codereview.settings'):
689 """Finds the given file starting in the cwd and going up.
690
691 Only looks up to the top of the repository unless an
692 'inherit-review-settings-ok' file exists in the root of the repository.
693 """
694 inherit_ok_file = 'inherit-review-settings-ok'
695 cwd = os.getcwd()
696 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
697 if os.path.isfile(os.path.join(root, inherit_ok_file)):
698 root = '/'
699 while True:
700 if filename in os.listdir(cwd):
701 if os.path.isfile(os.path.join(cwd, filename)):
702 return open(os.path.join(cwd, filename))
703 if cwd == root:
704 break
705 cwd = os.path.dirname(cwd)
706
707
708def LoadCodereviewSettingsFromFile(fileobj):
709 """Parse a codereview.settings file and updates hooks."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710 keyvals = {}
711 for line in fileobj.read().splitlines():
712 if not line or line.startswith("#"):
713 continue
714 k, v = line.split(": ", 1)
715 keyvals[k] = v
716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 def SetProperty(name, setting, unset_error_ok=False):
718 fullname = 'rietveld.' + name
719 if setting in keyvals:
720 RunGit(['config', fullname, keyvals[setting]])
721 else:
722 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
723
724 SetProperty('server', 'CODE_REVIEW_SERVER')
725 # Only server setting is required. Other settings can be absent.
726 # In that case, we ignore errors raised during option deletion attempt.
727 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
728 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
729 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
730
731 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
732 #should be of the form
733 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
734 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
735 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
736 keyvals['ORIGIN_URL_CONFIG']])
737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738
739@usage('[repo root containing codereview.settings]')
740def CMDconfig(parser, args):
741 """edit configuration for this tree"""
742
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000743 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 if len(args) == 0:
745 GetCodereviewSettingsInteractively()
746 return 0
747
748 url = args[0]
749 if not url.endswith('codereview.settings'):
750 url = os.path.join(url, 'codereview.settings')
751
752 # Load code review settings and download hooks (if available).
753 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
754 return 0
755
756
757def CMDstatus(parser, args):
758 """show status of changelists"""
759 parser.add_option('--field',
760 help='print only specific field (desc|id|patch|url)')
761 (options, args) = parser.parse_args(args)
762
763 # TODO: maybe make show_branches a flag if necessary.
764 show_branches = not options.field
765
766 if show_branches:
767 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
768 if branches:
769 print 'Branches associated with reviews:'
770 for branch in sorted(branches.splitlines()):
771 cl = Changelist(branchref=branch)
772 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
773
774 cl = Changelist()
775 if options.field:
776 if options.field.startswith('desc'):
777 print cl.GetDescription()
778 elif options.field == 'id':
779 issueid = cl.GetIssue()
780 if issueid:
781 print issueid
782 elif options.field == 'patch':
783 patchset = cl.GetPatchset()
784 if patchset:
785 print patchset
786 elif options.field == 'url':
787 url = cl.GetIssueURL()
788 if url:
789 print url
790 else:
791 print
792 print 'Current branch:',
793 if not cl.GetIssue():
794 print 'no issue assigned.'
795 return 0
796 print cl.GetBranch()
797 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
798 print 'Issue description:'
799 print cl.GetDescription(pretty=True)
800 return 0
801
802
803@usage('[issue_number]')
804def CMDissue(parser, args):
805 """Set or display the current code review issue number.
806
807 Pass issue number 0 to clear the current issue.
808"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000809 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810
811 cl = Changelist()
812 if len(args) > 0:
813 try:
814 issue = int(args[0])
815 except ValueError:
816 DieWithError('Pass a number to set the issue or none to list it.\n'
817 'Maybe you want to run git cl status?')
818 cl.SetIssue(issue)
819 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
820 return 0
821
822
823def CreateDescriptionFromLog(args):
824 """Pulls out the commit log to use as a base for the CL description."""
825 log_args = []
826 if len(args) == 1 and not args[0].endswith('.'):
827 log_args = [args[0] + '..']
828 elif len(args) == 1 and args[0].endswith('...'):
829 log_args = [args[0][:-1]]
830 elif len(args) == 2:
831 log_args = [args[0] + '..' + args[1]]
832 else:
833 log_args = args[:] # Hope for the best!
834 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
835
836
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000837def UserEditedLog(starting_text):
838 """Given some starting text, let the user edit it and return the result."""
839 editor = os.getenv('EDITOR', 'vi')
840
841 (file_handle, filename) = tempfile.mkstemp()
842 fileobj = os.fdopen(file_handle, 'w')
843 fileobj.write(starting_text)
844 fileobj.close()
845
846 # Open up the default editor in the system to get the CL description.
847 try:
848 cmd = '%s %s' % (editor, filename)
849 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
850 # Msysgit requires the usage of 'env' to be present.
851 cmd = 'env ' + cmd
852 # shell=True to allow the shell to handle all forms of quotes in $EDITOR.
maruel@chromium.org2a471072011-05-10 17:29:23 +0000853 try:
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +0000854 subprocess.check_call(cmd, shell=True)
855 except subprocess.CalledProcessError, e:
maruel@chromium.org2a471072011-05-10 17:29:23 +0000856 DieWithError('Editor returned %d' % e.returncode)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000857 fileobj = open(filename)
858 text = fileobj.read()
859 fileobj.close()
860 finally:
861 os.remove(filename)
862
863 if not text:
864 return
865
866 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
867 return stripcomment_re.sub('', text).strip()
868
869
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000870def ConvertToInteger(inputval):
871 """Convert a string to integer, but returns either an int or None."""
872 try:
873 return int(inputval)
874 except (TypeError, ValueError):
875 return None
876
877
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878def CMDpresubmit(parser, args):
879 """run presubmit tests on the current changelist"""
880 parser.add_option('--upload', action='store_true',
881 help='Run upload hook instead of the push/dcommit hook')
882 (options, args) = parser.parse_args(args)
883
884 # Make sure index is up-to-date before running diff-index.
885 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
886 if RunGit(['diff-index', 'HEAD']):
887 # TODO(maruel): Is this really necessary?
888 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
889 return 1
890
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000891 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 if args:
893 base_branch = args[0]
894 else:
895 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000896 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000898 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000899 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000900 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000901 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902
903
904@usage('[args to "git diff"]')
905def CMDupload(parser, args):
906 """upload the current changelist to codereview"""
907 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
908 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000909 parser.add_option('-f', action='store_true', dest='force',
910 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911 parser.add_option('-m', dest='message', help='message for patch')
912 parser.add_option('-r', '--reviewers',
913 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000914 parser.add_option('--cc',
915 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 parser.add_option('--send-mail', action='store_true',
917 help='send email to reviewer immediately')
918 parser.add_option("--emulate_svn_auto_props", action="store_true",
919 dest="emulate_svn_auto_props",
920 help="Emulate Subversion's auto properties feature.")
921 parser.add_option("--desc_from_logs", action="store_true",
922 dest="from_logs",
923 help="""Squashes git commit logs into change description and
924 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000925 parser.add_option('-c', '--use-commit-queue', action='store_true',
926 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 (options, args) = parser.parse_args(args)
928
929 # Make sure index is up-to-date before running diff-index.
930 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
931 if RunGit(['diff-index', 'HEAD']):
932 print 'Cannot upload with a dirty tree. You must commit locally first.'
933 return 1
934
935 cl = Changelist()
936 if args:
937 base_branch = args[0]
938 else:
939 # Default to diffing against the "upstream" branch.
940 base_branch = cl.GetUpstreamBranch()
941 args = [base_branch + "..."]
942
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000943 if not options.bypass_hooks and not options.force:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000944 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000945 may_prompt=True,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000946 verbose=options.verbose,
947 author=None)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000948 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000949 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951
952 # --no-ext-diff is broken in some versions of Git, so try to work around
953 # this by overriding the environment (but there is still a problem if the
954 # git config key "diff.external" is used).
955 env = os.environ.copy()
956 if 'GIT_EXTERNAL_DIFF' in env:
957 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +0000958 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args,
959 env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960
961 upload_args = ['--assume_yes'] # Don't ask about untracked files.
962 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 if options.emulate_svn_auto_props:
964 upload_args.append('--emulate_svn_auto_props')
965 if options.send_mail:
966 if not options.reviewers:
967 DieWithError("Must specify reviewers to send email.")
968 upload_args.append('--send_mail')
969 if options.from_logs and not options.message:
970 print 'Must set message for subject line if using desc_from_logs'
971 return 1
972
973 change_desc = None
974
975 if cl.GetIssue():
976 if options.message:
977 upload_args.extend(['--message', options.message])
978 upload_args.extend(['--issue', cl.GetIssue()])
979 print ("This branch is associated with issue %s. "
980 "Adding patch to that issue." % cl.GetIssue())
981 else:
982 log_desc = CreateDescriptionFromLog(args)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000983 change_desc = ChangeDescription(options.message, log_desc,
984 options.reviewers)
985 if not options.from_logs:
986 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000987
988 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989 print "Description is empty; aborting."
990 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000991
992 upload_args.extend(['--message', change_desc.subject])
993 upload_args.extend(['--description', change_desc.description])
994 if change_desc.reviewers:
995 upload_args.extend(['--reviewers', change_desc.reviewers])
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000996 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000997 if cc:
998 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999
1000 # Include the upstream repo's URL in the change -- this is useful for
1001 # projects that have their source spread across multiple repos.
1002 remote_url = None
1003 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +00001004 # URL is dependent on the current directory.
1005 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006 if data:
1007 keys = dict(line.split(': ', 1) for line in data.splitlines()
1008 if ': ' in line)
1009 remote_url = keys.get('URL', None)
1010 else:
1011 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1012 remote_url = (cl.GetRemoteUrl() + '@'
1013 + cl.GetUpstreamBranch().split('/')[-1])
1014 if remote_url:
1015 upload_args.extend(['--base_url', remote_url])
1016
1017 try:
1018 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001019 except KeyboardInterrupt:
1020 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 except:
1022 # If we got an exception after the user typed a description for their
1023 # change, back up the description before re-raising.
1024 if change_desc:
1025 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1026 print '\nGot exception while uploading -- saving description to %s\n' \
1027 % backup_path
1028 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001029 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 backup_file.close()
1031 raise
1032
1033 if not cl.GetIssue():
1034 cl.SetIssue(issue)
1035 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001036
1037 if options.use_commit_queue:
1038 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 return 0
1040
1041
1042def SendUpstream(parser, args, cmd):
1043 """Common code for CmdPush and CmdDCommit
1044
1045 Squashed commit into a single.
1046 Updates changelog with metadata (e.g. pointer to review).
1047 Pushes/dcommits the code upstream.
1048 Updates review and closes.
1049 """
1050 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1051 help='bypass upload presubmit hook')
1052 parser.add_option('-m', dest='message',
1053 help="override review description")
1054 parser.add_option('-f', action='store_true', dest='force',
1055 help="force yes to questions (don't prompt)")
1056 parser.add_option('-c', dest='contributor',
1057 help="external contributor for patch (appended to " +
1058 "description and used as author for git). Should be " +
1059 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001060 (options, args) = parser.parse_args(args)
1061 cl = Changelist()
1062
1063 if not args or cmd == 'push':
1064 # Default to merging against our best guess of the upstream branch.
1065 args = [cl.GetUpstreamBranch()]
1066
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001067 if options.contributor:
1068 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1069 print "Please provide contibutor as 'First Last <email@example.com>'"
1070 return 1
1071
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 base_branch = args[0]
1073
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001074 # Make sure index is up-to-date before running diff-index.
1075 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076 if RunGit(['diff-index', 'HEAD']):
1077 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1078 return 1
1079
1080 # This rev-list syntax means "show all commits not in my branch that
1081 # are in base_branch".
1082 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1083 base_branch]).splitlines()
1084 if upstream_commits:
1085 print ('Base branch "%s" has %d commits '
1086 'not in this branch.' % (base_branch, len(upstream_commits)))
1087 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1088 return 1
1089
1090 if cmd == 'dcommit':
1091 # This is the revision `svn dcommit` will commit on top of.
1092 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1093 '--pretty=format:%H'])
1094 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1095 if extra_commits:
1096 print ('This branch has %d additional commits not upstreamed yet.'
1097 % len(extra_commits.splitlines()))
1098 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1099 'before attempting to %s.' % (base_branch, cmd))
1100 return 1
1101
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001102 if not options.bypass_hooks and not options.force:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001103 author = None
1104 if options.contributor:
1105 author = re.search(r'\<(.*)\>', options.contributor).group(1)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001106 cl.RunHook(committing=True, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001107 may_prompt=True, verbose=options.verbose,
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001108 author=author)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109
1110 if cmd == 'dcommit':
1111 # Check the tree status if the tree status URL is set.
1112 status = GetTreeStatus()
1113 if 'closed' == status:
1114 print ('The tree is closed. Please wait for it to reopen. Use '
1115 '"git cl dcommit -f" to commit on a closed tree.')
1116 return 1
1117 elif 'unknown' == status:
1118 print ('Unable to determine tree status. Please verify manually and '
1119 'use "git cl dcommit -f" to commit on a closed tree.')
1120
1121 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001122 if not description and cl.GetIssue():
1123 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001125 if not description:
1126 print 'No description set.'
1127 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1128 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001130 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132
1133 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 description += "\nPatch from %s." % options.contributor
1135 print 'Description:', repr(description)
1136
1137 branches = [base_branch, cl.GetBranchRef()]
1138 if not options.force:
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +00001139 subprocess.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001140 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141
1142 # We want to squash all this branch's commits into one commit with the
1143 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001144 # We do this by doing a "reset --soft" to the base branch (which keeps
1145 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 MERGE_BRANCH = 'git-cl-commit'
1147 # Delete the merge branch if it already exists.
1148 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1149 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1150 RunGit(['branch', '-D', MERGE_BRANCH])
1151
1152 # We might be in a directory that's present in this branch but not in the
1153 # trunk. Move up to the top of the tree so that git commands that expect a
1154 # valid CWD won't fail after we check out the merge branch.
1155 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1156 if rel_base_path:
1157 os.chdir(rel_base_path)
1158
1159 # Stuff our change into the merge branch.
1160 # We wrap in a try...finally block so if anything goes wrong,
1161 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001162 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001164 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1165 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 if options.contributor:
1167 RunGit(['commit', '--author', options.contributor, '-m', description])
1168 else:
1169 RunGit(['commit', '-m', description])
1170 if cmd == 'push':
1171 # push the merge branch.
1172 remote, branch = cl.FetchUpstreamTuple()
1173 retcode, output = RunGitWithCode(
1174 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1175 logging.debug(output)
1176 else:
1177 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001178 retcode, output = RunGitWithCode(['svn', 'dcommit',
1179 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 finally:
1181 # And then swap back to the original branch and clean up.
1182 RunGit(['checkout', '-q', cl.GetBranch()])
1183 RunGit(['branch', '-D', MERGE_BRANCH])
1184
1185 if cl.GetIssue():
1186 if cmd == 'dcommit' and 'Committed r' in output:
1187 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1188 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001189 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1190 for l in output.splitlines(False))
1191 match = filter(None, match)
1192 if len(match) != 1:
1193 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1194 output)
1195 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 else:
1197 return 1
1198 viewvc_url = settings.GetViewVCUrl()
1199 if viewvc_url and revision:
1200 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1201 print ('Closing issue '
1202 '(you may be prompted for your codereview password)...')
1203 cl.CloseIssue()
1204 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001205
1206 if retcode == 0:
1207 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1208 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001209 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001210
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 return 0
1212
1213
1214@usage('[upstream branch to apply against]')
1215def CMDdcommit(parser, args):
1216 """commit the current changelist via git-svn"""
1217 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001218 message = """This doesn't appear to be an SVN repository.
1219If your project has a git mirror with an upstream SVN master, you probably need
1220to run 'git svn init', see your project's git mirror documentation.
1221If your project has a true writeable upstream repository, you probably want
1222to run 'git cl push' instead.
1223Choose wisely, if you get this wrong, your commit might appear to succeed but
1224will instead be silently ignored."""
1225 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001226 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 return SendUpstream(parser, args, 'dcommit')
1228
1229
1230@usage('[upstream branch to apply against]')
1231def CMDpush(parser, args):
1232 """commit the current changelist via git"""
1233 if settings.GetIsGitSvn():
1234 print('This appears to be an SVN repository.')
1235 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001236 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 return SendUpstream(parser, args, 'push')
1238
1239
1240@usage('<patch url or issue id>')
1241def CMDpatch(parser, args):
1242 """patch in a code review"""
1243 parser.add_option('-b', dest='newbranch',
1244 help='create a new branch off trunk for the patch')
1245 parser.add_option('-f', action='store_true', dest='force',
1246 help='with -b, clobber any existing branch')
1247 parser.add_option('--reject', action='store_true', dest='reject',
1248 help='allow failed patches and spew .rej files')
1249 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1250 help="don't commit after patch applies")
1251 (options, args) = parser.parse_args(args)
1252 if len(args) != 1:
1253 parser.print_help()
1254 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001255 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001257 # TODO(maruel): Use apply_issue.py
1258
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001259 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001261 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001262 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 else:
1264 # Assume it's a URL to the patch. Default to http.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001265 issue_url = FixUrl(issue_arg)
1266 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001267 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 DieWithError('Must pass an issue ID or full URL for '
1269 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001270 issue = match.group(1)
1271 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272
1273 if options.newbranch:
1274 if options.force:
1275 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +00001276 swallow_stderr=True, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 RunGit(['checkout', '-b', options.newbranch,
1278 Changelist().GetUpstreamBranch()])
1279
1280 # Switch up to the top-level directory, if necessary, in preparation for
1281 # applying the patch.
1282 top = RunGit(['rev-parse', '--show-cdup']).strip()
1283 if top:
1284 os.chdir(top)
1285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 # Git patches have a/ at the beginning of source paths. We strip that out
1287 # with a sed script rather than the -p flag to patch so we can feed either
1288 # Git or svn-style patches into the same apply command.
1289 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +00001290 sed_proc = Popen(['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'],
1291 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
1292 patch_data = sed_proc.communicate(patch_data)[0]
1293 if sed_proc.returncode:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294 DieWithError('Git patch mungling failed.')
1295 logging.info(patch_data)
1296 # We use "git apply" to apply the patch instead of "patch" so that we can
1297 # pick up file adds.
1298 # The --index flag means: also insert into the index (so we catch adds).
1299 cmd = ['git', 'apply', '--index', '-p0']
1300 if options.reject:
1301 cmd.append('--reject')
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +00001302 patch_proc = Popen(cmd, stdin=subprocess.PIPE)
1303 patch_proc.communicate(patch_data)
1304 if patch_proc.returncode:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 DieWithError('Failed to apply the patch')
1306
1307 # If we had an issue, commit the current state and register the issue.
1308 if not options.nocommit:
1309 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1310 cl = Changelist()
1311 cl.SetIssue(issue)
1312 print "Committed patch."
1313 else:
1314 print "Patch applied to index."
1315 return 0
1316
1317
1318def CMDrebase(parser, args):
1319 """rebase current branch on top of svn repo"""
1320 # Provide a wrapper for git svn rebase to help avoid accidental
1321 # git svn dcommit.
1322 # It's the only command that doesn't use parser at all since we just defer
1323 # execution to git-svn.
maruel@chromium.orgd4b4e112011-09-09 11:44:10 +00001324 RunGit(['svn', 'rebase'] + args, redirect_stdout=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325 return 0
1326
1327
1328def GetTreeStatus():
1329 """Fetches the tree status and returns either 'open', 'closed',
1330 'unknown' or 'unset'."""
1331 url = settings.GetTreeStatusUrl(error_ok=True)
1332 if url:
1333 status = urllib2.urlopen(url).read().lower()
1334 if status.find('closed') != -1 or status == '0':
1335 return 'closed'
1336 elif status.find('open') != -1 or status == '1':
1337 return 'open'
1338 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 return 'unset'
1340
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001341
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342def GetTreeStatusReason():
1343 """Fetches the tree status from a json url and returns the message
1344 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001345 url = settings.GetTreeStatusUrl()
1346 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 connection = urllib2.urlopen(json_url)
1348 status = json.loads(connection.read())
1349 connection.close()
1350 return status['message']
1351
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353def CMDtree(parser, args):
1354 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001355 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 status = GetTreeStatus()
1357 if 'unset' == status:
1358 print 'You must configure your tree status URL by running "git cl config".'
1359 return 2
1360
1361 print "The tree is %s" % status
1362 print
1363 print GetTreeStatusReason()
1364 if status != 'open':
1365 return 1
1366 return 0
1367
1368
1369def CMDupstream(parser, args):
1370 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001371 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001372 if args:
1373 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 cl = Changelist()
1375 print cl.GetUpstreamBranch()
1376 return 0
1377
1378
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001379def CMDset_commit(parser, args):
1380 """set the commit bit"""
1381 _, args = parser.parse_args(args)
1382 if args:
1383 parser.error('Unrecognized args: %s' % ' '.join(args))
1384 cl = Changelist()
1385 cl.SetFlag('commit', '1')
1386 return 0
1387
1388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389def Command(name):
1390 return getattr(sys.modules[__name__], 'CMD' + name, None)
1391
1392
1393def CMDhelp(parser, args):
1394 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001395 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 if len(args) == 1:
1397 return main(args + ['--help'])
1398 parser.print_help()
1399 return 0
1400
1401
1402def GenUsage(parser, command):
1403 """Modify an OptParse object with the function's documentation."""
1404 obj = Command(command)
1405 more = getattr(obj, 'usage_more', '')
1406 if command == 'help':
1407 command = '<command>'
1408 else:
1409 # OptParser.description prefer nicely non-formatted strings.
1410 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1411 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1412
1413
1414def main(argv):
1415 """Doesn't parse the arguments here, just find the right subcommand to
1416 execute."""
1417 # Do it late so all commands are listed.
1418 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1419 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1420 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1421
1422 # Create the option parse and add --verbose support.
1423 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001424 parser.add_option(
1425 '-v', '--verbose', action='count', default=0,
1426 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 old_parser_args = parser.parse_args
1428 def Parse(args):
1429 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001430 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001432 elif options.verbose:
1433 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 else:
1435 logging.basicConfig(level=logging.WARNING)
1436 return options, args
1437 parser.parse_args = Parse
1438
1439 if argv:
1440 command = Command(argv[0])
1441 if command:
1442 # "fix" the usage and the description now that we know the subcommand.
1443 GenUsage(parser, argv[0])
1444 try:
1445 return command(parser, argv[1:])
1446 except urllib2.HTTPError, e:
1447 if e.code != 500:
1448 raise
1449 DieWithError(
1450 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1451 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1452
1453 # Not a known command. Default to help.
1454 GenUsage(parser, 'help')
1455 return CMDhelp(parser, argv)
1456
1457
1458if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001459 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 sys.exit(main(sys.argv[1:]))