blob: 9946999c639b4bfd50edd3ab5011b9f97e18a400 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000010import logging
11import optparse
12import os
13import re
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import sys
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000015import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000017import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import urllib2
19
20try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000021 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022except ImportError:
23 pass
24
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000025try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000027except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000028 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000029 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000030 except ImportError:
31 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000032 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000033 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034
35
36from third_party import upload
37import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000040import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
44
45
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000046DEFAULT_SERVER = 'http://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
49
maruel@chromium.org90541732011-04-01 17:54:18 +000050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000052 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053 sys.exit(1)
54
55
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000056def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058 return subprocess2.check_output(args, shell=False, **kwargs)
59 except subprocess2.CalledProcessError, e:
60 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 'Command "%s" failed.\n%s' % (
63 ' '.join(args), error_message or e.stdout or ''))
64 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065
66
67def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000068 """Returns stdout."""
69 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070
71
72def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000073 """Returns return code and stdout."""
74 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
75 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
77
78def usage(more):
79 def hook(fn):
80 fn.usage_more = more
81 return fn
82 return hook
83
84
maruel@chromium.org90541732011-04-01 17:54:18 +000085def ask_for_data(prompt):
86 try:
87 return raw_input(prompt)
88 except KeyboardInterrupt:
89 # Hide the exception.
90 sys.exit(1)
91
92
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093def FixUrl(server):
94 """Fix a server url to defaults protocol to http:// if none is specified."""
95 if not server:
96 return server
97 if not re.match(r'[a-z]+\://.*', server):
98 return 'http://' + server
99 return server
100
101
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000102def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
103 """Return the corresponding git ref if |base_url| together with |glob_spec|
104 matches the full |url|.
105
106 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
107 """
108 fetch_suburl, as_ref = glob_spec.split(':')
109 if allow_wildcards:
110 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
111 if glob_match:
112 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
113 # "branches/{472,597,648}/src:refs/remotes/svn/*".
114 branch_re = re.escape(base_url)
115 if glob_match.group(1):
116 branch_re += '/' + re.escape(glob_match.group(1))
117 wildcard = glob_match.group(2)
118 if wildcard == '*':
119 branch_re += '([^/]*)'
120 else:
121 # Escape and replace surrounding braces with parentheses and commas
122 # with pipe symbols.
123 wildcard = re.escape(wildcard)
124 wildcard = re.sub('^\\\\{', '(', wildcard)
125 wildcard = re.sub('\\\\,', '|', wildcard)
126 wildcard = re.sub('\\\\}$', ')', wildcard)
127 branch_re += wildcard
128 if glob_match.group(3):
129 branch_re += re.escape(glob_match.group(3))
130 match = re.match(branch_re, url)
131 if match:
132 return re.sub('\*$', match.group(1), as_ref)
133
134 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
135 if fetch_suburl:
136 full_url = base_url + '/' + fetch_suburl
137 else:
138 full_url = base_url
139 if full_url == url:
140 return as_ref
141 return None
142
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000143
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000144class Settings(object):
145 def __init__(self):
146 self.default_server = None
147 self.cc = None
148 self.root = None
149 self.is_git_svn = None
150 self.svn_branch = None
151 self.tree_status_url = None
152 self.viewvc_url = None
153 self.updated = False
154
155 def LazyUpdateIfNeeded(self):
156 """Updates the settings from a codereview.settings file, if available."""
157 if not self.updated:
158 cr_settings_file = FindCodereviewSettingsFile()
159 if cr_settings_file:
160 LoadCodereviewSettingsFromFile(cr_settings_file)
161 self.updated = True
162
163 def GetDefaultServerUrl(self, error_ok=False):
164 if not self.default_server:
165 self.LazyUpdateIfNeeded()
166 self.default_server = FixUrl(self._GetConfig('rietveld.server',
167 error_ok=True))
168 if error_ok:
169 return self.default_server
170 if not self.default_server:
171 error_message = ('Could not find settings file. You must configure '
172 'your review setup by running "git cl config".')
173 self.default_server = FixUrl(self._GetConfig(
174 'rietveld.server', error_message=error_message))
175 return self.default_server
176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000177 def GetRoot(self):
178 if not self.root:
179 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
180 return self.root
181
182 def GetIsGitSvn(self):
183 """Return true if this repo looks like it's using git-svn."""
184 if self.is_git_svn is None:
185 # If you have any "svn-remote.*" config keys, we think you're using svn.
186 self.is_git_svn = RunGitWithCode(
187 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
188 return self.is_git_svn
189
190 def GetSVNBranch(self):
191 if self.svn_branch is None:
192 if not self.GetIsGitSvn():
193 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
194
195 # Try to figure out which remote branch we're based on.
196 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000197 # 1) iterate through our branch history and find the svn URL.
198 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000199
200 # regexp matching the git-svn line that contains the URL.
201 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
202
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000203 # We don't want to go through all of history, so read a line from the
204 # pipe at a time.
205 # The -100 is an arbitrary limit so we don't search forever.
206 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000207 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000208 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000209 for line in proc.stdout:
210 match = git_svn_re.match(line)
211 if match:
212 url = match.group(1)
213 proc.stdout.close() # Cut pipe.
214 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000215
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000216 if url:
217 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
218 remotes = RunGit(['config', '--get-regexp',
219 r'^svn-remote\..*\.url']).splitlines()
220 for remote in remotes:
221 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000222 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000223 remote = match.group(1)
224 base_url = match.group(2)
225 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000226 ['config', 'svn-remote.%s.fetch' % remote],
227 error_ok=True).strip()
228 if fetch_spec:
229 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
230 if self.svn_branch:
231 break
232 branch_spec = RunGit(
233 ['config', 'svn-remote.%s.branches' % remote],
234 error_ok=True).strip()
235 if branch_spec:
236 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
237 if self.svn_branch:
238 break
239 tag_spec = RunGit(
240 ['config', 'svn-remote.%s.tags' % remote],
241 error_ok=True).strip()
242 if tag_spec:
243 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
244 if self.svn_branch:
245 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000246
247 if not self.svn_branch:
248 DieWithError('Can\'t guess svn branch -- try specifying it on the '
249 'command line')
250
251 return self.svn_branch
252
253 def GetTreeStatusUrl(self, error_ok=False):
254 if not self.tree_status_url:
255 error_message = ('You must configure your tree status URL by running '
256 '"git cl config".')
257 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
258 error_ok=error_ok,
259 error_message=error_message)
260 return self.tree_status_url
261
262 def GetViewVCUrl(self):
263 if not self.viewvc_url:
264 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
265 return self.viewvc_url
266
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000267 def GetDefaultCCList(self):
268 return self._GetConfig('rietveld.cc', error_ok=True)
269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000270 def _GetConfig(self, param, **kwargs):
271 self.LazyUpdateIfNeeded()
272 return RunGit(['config', param], **kwargs).strip()
273
274
275settings = Settings()
276
277
278did_migrate_check = False
279def CheckForMigration():
280 """Migrate from the old issue format, if found.
281
282 We used to store the branch<->issue mapping in a file in .git, but it's
283 better to store it in the .git/config, since deleting a branch deletes that
284 branch's entry there.
285 """
286
287 # Don't run more than once.
288 global did_migrate_check
289 if did_migrate_check:
290 return
291
292 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
293 storepath = os.path.join(gitdir, 'cl-mapping')
294 if os.path.exists(storepath):
295 print "old-style git-cl mapping file (%s) found; migrating." % storepath
296 store = open(storepath, 'r')
297 for line in store:
298 branch, issue = line.strip().split()
299 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
300 issue])
301 store.close()
302 os.remove(storepath)
303 did_migrate_check = True
304
305
306def ShortBranchName(branch):
307 """Convert a name like 'refs/heads/foo' to just 'foo'."""
308 return branch.replace('refs/heads/', '')
309
310
311class Changelist(object):
312 def __init__(self, branchref=None):
313 # Poke settings so we get the "configure your server" message if necessary.
314 settings.GetDefaultServerUrl()
315 self.branchref = branchref
316 if self.branchref:
317 self.branch = ShortBranchName(self.branchref)
318 else:
319 self.branch = None
320 self.rietveld_server = None
321 self.upstream_branch = None
322 self.has_issue = False
323 self.issue = None
324 self.has_description = False
325 self.description = None
326 self.has_patchset = False
327 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000328 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000329 self.cc = None
330 self.watchers = ()
331
332 def GetCCList(self):
333 """Return the users cc'd on this CL.
334
335 Return is a string suitable for passing to gcl with the --cc flag.
336 """
337 if self.cc is None:
338 base_cc = settings .GetDefaultCCList()
339 more_cc = ','.join(self.watchers)
340 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
341 return self.cc
342
343 def SetWatchers(self, watchers):
344 """Set the list of email addresses that should be cc'd based on the changed
345 files in this CL.
346 """
347 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348
349 def GetBranch(self):
350 """Returns the short branch name, e.g. 'master'."""
351 if not self.branch:
352 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
353 self.branch = ShortBranchName(self.branchref)
354 return self.branch
355
356 def GetBranchRef(self):
357 """Returns the full branch name, e.g. 'refs/heads/master'."""
358 self.GetBranch() # Poke the lazy loader.
359 return self.branchref
360
361 def FetchUpstreamTuple(self):
362 """Returns a tuple containg remote and remote ref,
363 e.g. 'origin', 'refs/heads/master'
364 """
365 remote = '.'
366 branch = self.GetBranch()
367 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
368 error_ok=True).strip()
369 if upstream_branch:
370 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
371 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000372 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
373 error_ok=True).strip()
374 if upstream_branch:
375 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000376 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000377 # Fall back on trying a git-svn upstream branch.
378 if settings.GetIsGitSvn():
379 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000380 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000381 # Else, try to guess the origin remote.
382 remote_branches = RunGit(['branch', '-r']).split()
383 if 'origin/master' in remote_branches:
384 # Fall back on origin/master if it exits.
385 remote = 'origin'
386 upstream_branch = 'refs/heads/master'
387 elif 'origin/trunk' in remote_branches:
388 # Fall back on origin/trunk if it exists. Generally a shared
389 # git-svn clone
390 remote = 'origin'
391 upstream_branch = 'refs/heads/trunk'
392 else:
393 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000394Either pass complete "git diff"-style arguments, like
395 git cl upload origin/master
396or verify this branch is set up to track another (via the --track argument to
397"git checkout -b ...").""")
398
399 return remote, upstream_branch
400
401 def GetUpstreamBranch(self):
402 if self.upstream_branch is None:
403 remote, upstream_branch = self.FetchUpstreamTuple()
404 if remote is not '.':
405 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
406 self.upstream_branch = upstream_branch
407 return self.upstream_branch
408
409 def GetRemoteUrl(self):
410 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
411
412 Returns None if there is no remote.
413 """
414 remote = self.FetchUpstreamTuple()[0]
415 if remote == '.':
416 return None
417 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
418
419 def GetIssue(self):
420 if not self.has_issue:
421 CheckForMigration()
422 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
423 if issue:
424 self.issue = issue
425 self.rietveld_server = FixUrl(RunGit(
426 ['config', self._RietveldServer()], error_ok=True).strip())
427 else:
428 self.issue = None
429 if not self.rietveld_server:
430 self.rietveld_server = settings.GetDefaultServerUrl()
431 self.has_issue = True
432 return self.issue
433
434 def GetRietveldServer(self):
435 self.GetIssue()
436 return self.rietveld_server
437
438 def GetIssueURL(self):
439 """Get the URL for a particular issue."""
440 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
441
442 def GetDescription(self, pretty=False):
443 if not self.has_description:
444 if self.GetIssue():
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000445 self.description = self.RpcServer().get_description(
446 int(self.GetIssue())).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000447 self.has_description = True
448 if pretty:
449 wrapper = textwrap.TextWrapper()
450 wrapper.initial_indent = wrapper.subsequent_indent = ' '
451 return wrapper.fill(self.description)
452 return self.description
453
454 def GetPatchset(self):
455 if not self.has_patchset:
456 patchset = RunGit(['config', self._PatchsetSetting()],
457 error_ok=True).strip()
458 if patchset:
459 self.patchset = patchset
460 else:
461 self.patchset = None
462 self.has_patchset = True
463 return self.patchset
464
465 def SetPatchset(self, patchset):
466 """Set this branch's patchset. If patchset=0, clears the patchset."""
467 if patchset:
468 RunGit(['config', self._PatchsetSetting(), str(patchset)])
469 else:
470 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000471 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472 self.has_patchset = False
473
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000474 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000475 patchset = self.RpcServer().get_issue_properties(
476 int(issue), False)['patchsets'][-1]
477 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000478 '/download/issue%s_%s.diff' % (issue, patchset))
479
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480 def SetIssue(self, issue):
481 """Set this branch's issue. If issue=0, clears the issue."""
482 if issue:
483 RunGit(['config', self._IssueSetting(), str(issue)])
484 if self.rietveld_server:
485 RunGit(['config', self._RietveldServer(), self.rietveld_server])
486 else:
487 RunGit(['config', '--unset', self._IssueSetting()])
488 self.SetPatchset(0)
489 self.has_issue = False
490
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000491 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000492 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
493 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000494
495 # We use the sha1 of HEAD as a name of this change.
496 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000497 # Need to pass a relative path for msysgit.
498 files = scm.GIT.CaptureStatus([root], upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000499
500 issue = ConvertToInteger(self.GetIssue())
501 patchset = ConvertToInteger(self.GetPatchset())
502 if issue:
503 description = self.GetDescription()
504 else:
505 # If the change was never uploaded, use the log messages of all commits
506 # up to the branch point, as git cl upload will prefill the description
507 # with these log messages.
508 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
509 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000510
511 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000512 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000513 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000514 name,
515 description,
516 absroot,
517 files,
518 issue,
519 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000520 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000521
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000522 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
523 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
524 change = self.GetChange(upstream_branch, author)
525
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000526 # Apply watchlists on upload.
527 if not committing:
528 watchlist = watchlists.Watchlists(change.RepositoryRoot())
529 files = [f.LocalPath() for f in change.AffectedFiles()]
530 self.SetWatchers(watchlist.GetWatchersForPaths(files))
531
532 try:
533 output = presubmit_support.DoPresubmitChecks(change, committing,
534 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000535 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000536 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000537 except presubmit_support.PresubmitFailure, e:
538 DieWithError(
539 ('%s\nMaybe your depot_tools is out of date?\n'
540 'If all fails, contact maruel@') % e)
541
542 # TODO(dpranke): We should propagate the error out instead of calling
543 # exit().
544 if not output.should_continue():
545 sys.exit(1)
546
547 return output
548
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000550 """Updates the description and closes the issue."""
551 issue = int(self.GetIssue())
552 self.RpcServer().update_description(issue, self.description)
553 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000554
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000555 def SetFlag(self, flag, value):
556 """Patchset must match."""
557 if not self.GetPatchset():
558 DieWithError('The patchset needs to match. Send another patchset.')
559 try:
560 return self.RpcServer().set_flag(
561 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
562 except urllib2.HTTPError, e:
563 if e.code == 404:
564 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
565 if e.code == 403:
566 DieWithError(
567 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
568 'match?') % (self.GetIssue(), self.GetPatchset()))
569 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000571 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000572 """Returns an upload.RpcServer() to access this review's rietveld instance.
573 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000574 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000575 self.GetIssue()
576 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000577 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578
579 def _IssueSetting(self):
580 """Return the git setting that stores this change's issue."""
581 return 'branch.%s.rietveldissue' % self.GetBranch()
582
583 def _PatchsetSetting(self):
584 """Return the git setting that stores this change's most recent patchset."""
585 return 'branch.%s.rietveldpatchset' % self.GetBranch()
586
587 def _RietveldServer(self):
588 """Returns the git setting that stores this change's rietveld server."""
589 return 'branch.%s.rietveldserver' % self.GetBranch()
590
591
592def GetCodereviewSettingsInteractively():
593 """Prompt the user for settings."""
594 server = settings.GetDefaultServerUrl(error_ok=True)
595 prompt = 'Rietveld server (host[:port])'
596 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000597 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598 if not server and not newserver:
599 newserver = DEFAULT_SERVER
600 if newserver and newserver != server:
601 RunGit(['config', 'rietveld.server', newserver])
602
603 def SetProperty(initial, caption, name):
604 prompt = caption
605 if initial:
606 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000607 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 if new_val == 'x':
609 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
610 elif new_val and new_val != initial:
611 RunGit(['config', 'rietveld.' + name, new_val])
612
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000613 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
615 'tree-status-url')
616 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
617
618 # TODO: configure a default branch to diff against, rather than this
619 # svn-based hackery.
620
621
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000622class ChangeDescription(object):
623 """Contains a parsed form of the change description."""
624 def __init__(self, subject, log_desc, reviewers):
625 self.subject = subject
626 self.log_desc = log_desc
627 self.reviewers = reviewers
628 self.description = self.log_desc
629
630 def Update(self):
631 initial_text = """# Enter a description of the change.
632# This will displayed on the codereview site.
633# The first line will also be used as the subject of the review.
634"""
635 initial_text += self.description
636 if 'R=' not in self.description and self.reviewers:
637 initial_text += '\nR=' + self.reviewers
638 if 'BUG=' not in self.description:
639 initial_text += '\nBUG='
640 if 'TEST=' not in self.description:
641 initial_text += '\nTEST='
642 self._ParseDescription(UserEditedLog(initial_text))
643
644 def _ParseDescription(self, description):
645 if not description:
646 self.description = description
647 return
648
649 parsed_lines = []
650 reviewers_regexp = re.compile('\s*R=(.+)')
651 reviewers = ''
652 subject = ''
653 for l in description.splitlines():
654 if not subject:
655 subject = l
656 matched_reviewers = reviewers_regexp.match(l)
657 if matched_reviewers:
658 reviewers = matched_reviewers.group(1)
659 parsed_lines.append(l)
660
661 self.description = '\n'.join(parsed_lines) + '\n'
662 self.subject = subject
663 self.reviewers = reviewers
664
665 def IsEmpty(self):
666 return not self.description
667
668
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669def FindCodereviewSettingsFile(filename='codereview.settings'):
670 """Finds the given file starting in the cwd and going up.
671
672 Only looks up to the top of the repository unless an
673 'inherit-review-settings-ok' file exists in the root of the repository.
674 """
675 inherit_ok_file = 'inherit-review-settings-ok'
676 cwd = os.getcwd()
677 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
678 if os.path.isfile(os.path.join(root, inherit_ok_file)):
679 root = '/'
680 while True:
681 if filename in os.listdir(cwd):
682 if os.path.isfile(os.path.join(cwd, filename)):
683 return open(os.path.join(cwd, filename))
684 if cwd == root:
685 break
686 cwd = os.path.dirname(cwd)
687
688
689def LoadCodereviewSettingsFromFile(fileobj):
690 """Parse a codereview.settings file and updates hooks."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 keyvals = {}
692 for line in fileobj.read().splitlines():
693 if not line or line.startswith("#"):
694 continue
695 k, v = line.split(": ", 1)
696 keyvals[k] = v
697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698 def SetProperty(name, setting, unset_error_ok=False):
699 fullname = 'rietveld.' + name
700 if setting in keyvals:
701 RunGit(['config', fullname, keyvals[setting]])
702 else:
703 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
704
705 SetProperty('server', 'CODE_REVIEW_SERVER')
706 # Only server setting is required. Other settings can be absent.
707 # In that case, we ignore errors raised during option deletion attempt.
708 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
709 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
710 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
711
712 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
713 #should be of the form
714 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
715 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
716 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
717 keyvals['ORIGIN_URL_CONFIG']])
718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719
720@usage('[repo root containing codereview.settings]')
721def CMDconfig(parser, args):
722 """edit configuration for this tree"""
723
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000724 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 if len(args) == 0:
726 GetCodereviewSettingsInteractively()
727 return 0
728
729 url = args[0]
730 if not url.endswith('codereview.settings'):
731 url = os.path.join(url, 'codereview.settings')
732
733 # Load code review settings and download hooks (if available).
734 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
735 return 0
736
737
738def CMDstatus(parser, args):
739 """show status of changelists"""
740 parser.add_option('--field',
741 help='print only specific field (desc|id|patch|url)')
742 (options, args) = parser.parse_args(args)
743
744 # TODO: maybe make show_branches a flag if necessary.
745 show_branches = not options.field
746
747 if show_branches:
748 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
749 if branches:
750 print 'Branches associated with reviews:'
751 for branch in sorted(branches.splitlines()):
752 cl = Changelist(branchref=branch)
753 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
754
755 cl = Changelist()
756 if options.field:
757 if options.field.startswith('desc'):
758 print cl.GetDescription()
759 elif options.field == 'id':
760 issueid = cl.GetIssue()
761 if issueid:
762 print issueid
763 elif options.field == 'patch':
764 patchset = cl.GetPatchset()
765 if patchset:
766 print patchset
767 elif options.field == 'url':
768 url = cl.GetIssueURL()
769 if url:
770 print url
771 else:
772 print
773 print 'Current branch:',
774 if not cl.GetIssue():
775 print 'no issue assigned.'
776 return 0
777 print cl.GetBranch()
778 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
779 print 'Issue description:'
780 print cl.GetDescription(pretty=True)
781 return 0
782
783
784@usage('[issue_number]')
785def CMDissue(parser, args):
786 """Set or display the current code review issue number.
787
788 Pass issue number 0 to clear the current issue.
789"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000790 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791
792 cl = Changelist()
793 if len(args) > 0:
794 try:
795 issue = int(args[0])
796 except ValueError:
797 DieWithError('Pass a number to set the issue or none to list it.\n'
798 'Maybe you want to run git cl status?')
799 cl.SetIssue(issue)
800 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
801 return 0
802
803
804def CreateDescriptionFromLog(args):
805 """Pulls out the commit log to use as a base for the CL description."""
806 log_args = []
807 if len(args) == 1 and not args[0].endswith('.'):
808 log_args = [args[0] + '..']
809 elif len(args) == 1 and args[0].endswith('...'):
810 log_args = [args[0][:-1]]
811 elif len(args) == 2:
812 log_args = [args[0] + '..' + args[1]]
813 else:
814 log_args = args[:] # Hope for the best!
815 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
816
817
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000818def UserEditedLog(starting_text):
819 """Given some starting text, let the user edit it and return the result."""
820 editor = os.getenv('EDITOR', 'vi')
821
822 (file_handle, filename) = tempfile.mkstemp()
823 fileobj = os.fdopen(file_handle, 'w')
824 fileobj.write(starting_text)
825 fileobj.close()
826
827 # Open up the default editor in the system to get the CL description.
828 try:
829 cmd = '%s %s' % (editor, filename)
830 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
831 # Msysgit requires the usage of 'env' to be present.
832 cmd = 'env ' + cmd
833 # shell=True to allow the shell to handle all forms of quotes in $EDITOR.
maruel@chromium.org2a471072011-05-10 17:29:23 +0000834 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000835 subprocess2.check_call(cmd, shell=True)
836 except subprocess2.CalledProcessError, e:
maruel@chromium.org2a471072011-05-10 17:29:23 +0000837 DieWithError('Editor returned %d' % e.returncode)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000838 fileobj = open(filename)
839 text = fileobj.read()
840 fileobj.close()
841 finally:
842 os.remove(filename)
843
844 if not text:
845 return
846
847 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
848 return stripcomment_re.sub('', text).strip()
849
850
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000851def ConvertToInteger(inputval):
852 """Convert a string to integer, but returns either an int or None."""
853 try:
854 return int(inputval)
855 except (TypeError, ValueError):
856 return None
857
858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859def CMDpresubmit(parser, args):
860 """run presubmit tests on the current changelist"""
861 parser.add_option('--upload', action='store_true',
862 help='Run upload hook instead of the push/dcommit hook')
863 (options, args) = parser.parse_args(args)
864
865 # Make sure index is up-to-date before running diff-index.
866 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
867 if RunGit(['diff-index', 'HEAD']):
868 # TODO(maruel): Is this really necessary?
869 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
870 return 1
871
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000872 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 if args:
874 base_branch = args[0]
875 else:
876 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000877 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000879 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000880 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000881 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000882 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883
884
885@usage('[args to "git diff"]')
886def CMDupload(parser, args):
887 """upload the current changelist to codereview"""
888 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
889 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000890 parser.add_option('-f', action='store_true', dest='force',
891 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 parser.add_option('-m', dest='message', help='message for patch')
893 parser.add_option('-r', '--reviewers',
894 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000895 parser.add_option('--cc',
896 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 parser.add_option('--send-mail', action='store_true',
898 help='send email to reviewer immediately')
899 parser.add_option("--emulate_svn_auto_props", action="store_true",
900 dest="emulate_svn_auto_props",
901 help="Emulate Subversion's auto properties feature.")
902 parser.add_option("--desc_from_logs", action="store_true",
903 dest="from_logs",
904 help="""Squashes git commit logs into change description and
905 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000906 parser.add_option('-c', '--use-commit-queue', action='store_true',
907 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908 (options, args) = parser.parse_args(args)
909
910 # Make sure index is up-to-date before running diff-index.
911 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
912 if RunGit(['diff-index', 'HEAD']):
913 print 'Cannot upload with a dirty tree. You must commit locally first.'
914 return 1
915
916 cl = Changelist()
917 if args:
918 base_branch = args[0]
919 else:
920 # Default to diffing against the "upstream" branch.
921 base_branch = cl.GetUpstreamBranch()
922 args = [base_branch + "..."]
923
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000924 if not options.bypass_hooks and not options.force:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000925 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000926 may_prompt=True,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000927 verbose=options.verbose,
928 author=None)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000929 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000930 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933 # --no-ext-diff is broken in some versions of Git, so try to work around
934 # this by overriding the environment (but there is still a problem if the
935 # git config key "diff.external" is used).
936 env = os.environ.copy()
937 if 'GIT_EXTERNAL_DIFF' in env:
938 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000939 subprocess2.call(
940 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941
942 upload_args = ['--assume_yes'] # Don't ask about untracked files.
943 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944 if options.emulate_svn_auto_props:
945 upload_args.append('--emulate_svn_auto_props')
946 if options.send_mail:
947 if not options.reviewers:
948 DieWithError("Must specify reviewers to send email.")
949 upload_args.append('--send_mail')
950 if options.from_logs and not options.message:
951 print 'Must set message for subject line if using desc_from_logs'
952 return 1
953
954 change_desc = None
955
956 if cl.GetIssue():
957 if options.message:
958 upload_args.extend(['--message', options.message])
959 upload_args.extend(['--issue', cl.GetIssue()])
960 print ("This branch is associated with issue %s. "
961 "Adding patch to that issue." % cl.GetIssue())
962 else:
963 log_desc = CreateDescriptionFromLog(args)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000964 change_desc = ChangeDescription(options.message, log_desc,
965 options.reviewers)
966 if not options.from_logs:
967 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000968
969 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 print "Description is empty; aborting."
971 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000972
973 upload_args.extend(['--message', change_desc.subject])
974 upload_args.extend(['--description', change_desc.description])
975 if change_desc.reviewers:
976 upload_args.extend(['--reviewers', change_desc.reviewers])
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000977 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000978 if cc:
979 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980
981 # Include the upstream repo's URL in the change -- this is useful for
982 # projects that have their source spread across multiple repos.
983 remote_url = None
984 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000985 # URL is dependent on the current directory.
986 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 if data:
988 keys = dict(line.split(': ', 1) for line in data.splitlines()
989 if ': ' in line)
990 remote_url = keys.get('URL', None)
991 else:
992 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
993 remote_url = (cl.GetRemoteUrl() + '@'
994 + cl.GetUpstreamBranch().split('/')[-1])
995 if remote_url:
996 upload_args.extend(['--base_url', remote_url])
997
998 try:
999 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001000 except KeyboardInterrupt:
1001 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 except:
1003 # If we got an exception after the user typed a description for their
1004 # change, back up the description before re-raising.
1005 if change_desc:
1006 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1007 print '\nGot exception while uploading -- saving description to %s\n' \
1008 % backup_path
1009 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001010 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 backup_file.close()
1012 raise
1013
1014 if not cl.GetIssue():
1015 cl.SetIssue(issue)
1016 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001017
1018 if options.use_commit_queue:
1019 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 return 0
1021
1022
1023def SendUpstream(parser, args, cmd):
1024 """Common code for CmdPush and CmdDCommit
1025
1026 Squashed commit into a single.
1027 Updates changelog with metadata (e.g. pointer to review).
1028 Pushes/dcommits the code upstream.
1029 Updates review and closes.
1030 """
1031 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1032 help='bypass upload presubmit hook')
1033 parser.add_option('-m', dest='message',
1034 help="override review description")
1035 parser.add_option('-f', action='store_true', dest='force',
1036 help="force yes to questions (don't prompt)")
1037 parser.add_option('-c', dest='contributor',
1038 help="external contributor for patch (appended to " +
1039 "description and used as author for git). Should be " +
1040 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041 (options, args) = parser.parse_args(args)
1042 cl = Changelist()
1043
1044 if not args or cmd == 'push':
1045 # Default to merging against our best guess of the upstream branch.
1046 args = [cl.GetUpstreamBranch()]
1047
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001048 if options.contributor:
1049 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1050 print "Please provide contibutor as 'First Last <email@example.com>'"
1051 return 1
1052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 base_branch = args[0]
1054
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001055 # Make sure index is up-to-date before running diff-index.
1056 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 if RunGit(['diff-index', 'HEAD']):
1058 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1059 return 1
1060
1061 # This rev-list syntax means "show all commits not in my branch that
1062 # are in base_branch".
1063 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1064 base_branch]).splitlines()
1065 if upstream_commits:
1066 print ('Base branch "%s" has %d commits '
1067 'not in this branch.' % (base_branch, len(upstream_commits)))
1068 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1069 return 1
1070
1071 if cmd == 'dcommit':
1072 # This is the revision `svn dcommit` will commit on top of.
1073 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1074 '--pretty=format:%H'])
1075 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1076 if extra_commits:
1077 print ('This branch has %d additional commits not upstreamed yet.'
1078 % len(extra_commits.splitlines()))
1079 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1080 'before attempting to %s.' % (base_branch, cmd))
1081 return 1
1082
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001083 if not options.bypass_hooks and not options.force:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001084 author = None
1085 if options.contributor:
1086 author = re.search(r'\<(.*)\>', options.contributor).group(1)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001087 cl.RunHook(committing=True, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001088 may_prompt=True, verbose=options.verbose,
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001089 author=author)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090
1091 if cmd == 'dcommit':
1092 # Check the tree status if the tree status URL is set.
1093 status = GetTreeStatus()
1094 if 'closed' == status:
1095 print ('The tree is closed. Please wait for it to reopen. Use '
1096 '"git cl dcommit -f" to commit on a closed tree.')
1097 return 1
1098 elif 'unknown' == status:
1099 print ('Unable to determine tree status. Please verify manually and '
1100 'use "git cl dcommit -f" to commit on a closed tree.')
1101
1102 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001103 if not description and cl.GetIssue():
1104 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001106 if not description:
1107 print 'No description set.'
1108 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1109 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001111 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113
1114 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 description += "\nPatch from %s." % options.contributor
1116 print 'Description:', repr(description)
1117
1118 branches = [base_branch, cl.GetBranchRef()]
1119 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001120 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001121 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122
1123 # We want to squash all this branch's commits into one commit with the
1124 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001125 # We do this by doing a "reset --soft" to the base branch (which keeps
1126 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 MERGE_BRANCH = 'git-cl-commit'
1128 # Delete the merge branch if it already exists.
1129 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1130 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1131 RunGit(['branch', '-D', MERGE_BRANCH])
1132
1133 # We might be in a directory that's present in this branch but not in the
1134 # trunk. Move up to the top of the tree so that git commands that expect a
1135 # valid CWD won't fail after we check out the merge branch.
1136 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1137 if rel_base_path:
1138 os.chdir(rel_base_path)
1139
1140 # Stuff our change into the merge branch.
1141 # We wrap in a try...finally block so if anything goes wrong,
1142 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001143 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001145 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1146 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 if options.contributor:
1148 RunGit(['commit', '--author', options.contributor, '-m', description])
1149 else:
1150 RunGit(['commit', '-m', description])
1151 if cmd == 'push':
1152 # push the merge branch.
1153 remote, branch = cl.FetchUpstreamTuple()
1154 retcode, output = RunGitWithCode(
1155 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1156 logging.debug(output)
1157 else:
1158 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001159 retcode, output = RunGitWithCode(['svn', 'dcommit',
1160 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 finally:
1162 # And then swap back to the original branch and clean up.
1163 RunGit(['checkout', '-q', cl.GetBranch()])
1164 RunGit(['branch', '-D', MERGE_BRANCH])
1165
1166 if cl.GetIssue():
1167 if cmd == 'dcommit' and 'Committed r' in output:
1168 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1169 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001170 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1171 for l in output.splitlines(False))
1172 match = filter(None, match)
1173 if len(match) != 1:
1174 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1175 output)
1176 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 else:
1178 return 1
1179 viewvc_url = settings.GetViewVCUrl()
1180 if viewvc_url and revision:
1181 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1182 print ('Closing issue '
1183 '(you may be prompted for your codereview password)...')
1184 cl.CloseIssue()
1185 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001186
1187 if retcode == 0:
1188 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1189 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001190 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 return 0
1193
1194
1195@usage('[upstream branch to apply against]')
1196def CMDdcommit(parser, args):
1197 """commit the current changelist via git-svn"""
1198 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001199 message = """This doesn't appear to be an SVN repository.
1200If your project has a git mirror with an upstream SVN master, you probably need
1201to run 'git svn init', see your project's git mirror documentation.
1202If your project has a true writeable upstream repository, you probably want
1203to run 'git cl push' instead.
1204Choose wisely, if you get this wrong, your commit might appear to succeed but
1205will instead be silently ignored."""
1206 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001207 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 return SendUpstream(parser, args, 'dcommit')
1209
1210
1211@usage('[upstream branch to apply against]')
1212def CMDpush(parser, args):
1213 """commit the current changelist via git"""
1214 if settings.GetIsGitSvn():
1215 print('This appears to be an SVN repository.')
1216 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001217 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 return SendUpstream(parser, args, 'push')
1219
1220
1221@usage('<patch url or issue id>')
1222def CMDpatch(parser, args):
1223 """patch in a code review"""
1224 parser.add_option('-b', dest='newbranch',
1225 help='create a new branch off trunk for the patch')
1226 parser.add_option('-f', action='store_true', dest='force',
1227 help='with -b, clobber any existing branch')
1228 parser.add_option('--reject', action='store_true', dest='reject',
1229 help='allow failed patches and spew .rej files')
1230 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1231 help="don't commit after patch applies")
1232 (options, args) = parser.parse_args(args)
1233 if len(args) != 1:
1234 parser.print_help()
1235 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001236 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001238 # TODO(maruel): Use apply_issue.py
1239
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001240 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001242 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001243 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 else:
1245 # Assume it's a URL to the patch. Default to http.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001246 issue_url = FixUrl(issue_arg)
1247 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001248 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 DieWithError('Must pass an issue ID or full URL for '
1250 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001251 issue = match.group(1)
1252 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253
1254 if options.newbranch:
1255 if options.force:
1256 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001257 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 RunGit(['checkout', '-b', options.newbranch,
1259 Changelist().GetUpstreamBranch()])
1260
1261 # Switch up to the top-level directory, if necessary, in preparation for
1262 # applying the patch.
1263 top = RunGit(['rev-parse', '--show-cdup']).strip()
1264 if top:
1265 os.chdir(top)
1266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 # Git patches have a/ at the beginning of source paths. We strip that out
1268 # with a sed script rather than the -p flag to patch so we can feed either
1269 # Git or svn-style patches into the same apply command.
1270 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001271 try:
1272 patch_data = subprocess2.check_output(
1273 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1274 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 DieWithError('Git patch mungling failed.')
1276 logging.info(patch_data)
1277 # We use "git apply" to apply the patch instead of "patch" so that we can
1278 # pick up file adds.
1279 # The --index flag means: also insert into the index (so we catch adds).
1280 cmd = ['git', 'apply', '--index', '-p0']
1281 if options.reject:
1282 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001283 try:
1284 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1285 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 DieWithError('Failed to apply the patch')
1287
1288 # If we had an issue, commit the current state and register the issue.
1289 if not options.nocommit:
1290 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1291 cl = Changelist()
1292 cl.SetIssue(issue)
1293 print "Committed patch."
1294 else:
1295 print "Patch applied to index."
1296 return 0
1297
1298
1299def CMDrebase(parser, args):
1300 """rebase current branch on top of svn repo"""
1301 # Provide a wrapper for git svn rebase to help avoid accidental
1302 # git svn dcommit.
1303 # It's the only command that doesn't use parser at all since we just defer
1304 # execution to git-svn.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001305 subprocess2.check_call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 return 0
1307
1308
1309def GetTreeStatus():
1310 """Fetches the tree status and returns either 'open', 'closed',
1311 'unknown' or 'unset'."""
1312 url = settings.GetTreeStatusUrl(error_ok=True)
1313 if url:
1314 status = urllib2.urlopen(url).read().lower()
1315 if status.find('closed') != -1 or status == '0':
1316 return 'closed'
1317 elif status.find('open') != -1 or status == '1':
1318 return 'open'
1319 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 return 'unset'
1321
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323def GetTreeStatusReason():
1324 """Fetches the tree status from a json url and returns the message
1325 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001326 url = settings.GetTreeStatusUrl()
1327 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 connection = urllib2.urlopen(json_url)
1329 status = json.loads(connection.read())
1330 connection.close()
1331 return status['message']
1332
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334def CMDtree(parser, args):
1335 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001336 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 status = GetTreeStatus()
1338 if 'unset' == status:
1339 print 'You must configure your tree status URL by running "git cl config".'
1340 return 2
1341
1342 print "The tree is %s" % status
1343 print
1344 print GetTreeStatusReason()
1345 if status != 'open':
1346 return 1
1347 return 0
1348
1349
1350def CMDupstream(parser, args):
1351 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001352 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001353 if args:
1354 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 cl = Changelist()
1356 print cl.GetUpstreamBranch()
1357 return 0
1358
1359
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001360def CMDset_commit(parser, args):
1361 """set the commit bit"""
1362 _, args = parser.parse_args(args)
1363 if args:
1364 parser.error('Unrecognized args: %s' % ' '.join(args))
1365 cl = Changelist()
1366 cl.SetFlag('commit', '1')
1367 return 0
1368
1369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370def Command(name):
1371 return getattr(sys.modules[__name__], 'CMD' + name, None)
1372
1373
1374def CMDhelp(parser, args):
1375 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001376 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 if len(args) == 1:
1378 return main(args + ['--help'])
1379 parser.print_help()
1380 return 0
1381
1382
1383def GenUsage(parser, command):
1384 """Modify an OptParse object with the function's documentation."""
1385 obj = Command(command)
1386 more = getattr(obj, 'usage_more', '')
1387 if command == 'help':
1388 command = '<command>'
1389 else:
1390 # OptParser.description prefer nicely non-formatted strings.
1391 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1392 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1393
1394
1395def main(argv):
1396 """Doesn't parse the arguments here, just find the right subcommand to
1397 execute."""
1398 # Do it late so all commands are listed.
1399 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1400 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1401 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1402
1403 # Create the option parse and add --verbose support.
1404 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001405 parser.add_option(
1406 '-v', '--verbose', action='count', default=0,
1407 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 old_parser_args = parser.parse_args
1409 def Parse(args):
1410 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001411 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001413 elif options.verbose:
1414 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 else:
1416 logging.basicConfig(level=logging.WARNING)
1417 return options, args
1418 parser.parse_args = Parse
1419
1420 if argv:
1421 command = Command(argv[0])
1422 if command:
1423 # "fix" the usage and the description now that we know the subcommand.
1424 GenUsage(parser, argv[0])
1425 try:
1426 return command(parser, argv[1:])
1427 except urllib2.HTTPError, e:
1428 if e.code != 500:
1429 raise
1430 DieWithError(
1431 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1432 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1433
1434 # Not a known command. Default to help.
1435 GenUsage(parser, 'help')
1436 return CMDhelp(parser, argv)
1437
1438
1439if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001440 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 sys.exit(main(sys.argv[1:]))