blob: 1300267a7e79a0c78a2d8c3b45a4c96e40310fb1 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000016import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import urllib2
18
19try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000020 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021except ImportError:
22 pass
23
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000024try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000025 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000028 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 except ImportError:
30 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000031 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000032 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
34
35from third_party import upload
36import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000040import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
44
45
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.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000498 try:
499 files = scm.GIT.CaptureStatus([root], upstream_branch)
500 except subprocess2.CalledProcessError:
501 DieWithError(
502 ('\nFailed to diff against upstream branch %s!\n\n'
503 'This branch probably doesn\'t exist anymore. To reset the\n'
504 'tracking branch, please run\n'
505 ' git branch --set-upstream %s trunk\n'
506 'replacing trunk with origin/master or the relevant branch') %
507 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000508
509 issue = ConvertToInteger(self.GetIssue())
510 patchset = ConvertToInteger(self.GetPatchset())
511 if issue:
512 description = self.GetDescription()
513 else:
514 # If the change was never uploaded, use the log messages of all commits
515 # up to the branch point, as git cl upload will prefill the description
516 # with these log messages.
517 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
518 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000519
520 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000521 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000522 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000523 name,
524 description,
525 absroot,
526 files,
527 issue,
528 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000529 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000530
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000531 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
532 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
533 change = self.GetChange(upstream_branch, author)
534
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000535 # Apply watchlists on upload.
536 if not committing:
537 watchlist = watchlists.Watchlists(change.RepositoryRoot())
538 files = [f.LocalPath() for f in change.AffectedFiles()]
539 self.SetWatchers(watchlist.GetWatchersForPaths(files))
540
541 try:
542 output = presubmit_support.DoPresubmitChecks(change, committing,
543 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000544 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000545 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000546 except presubmit_support.PresubmitFailure, e:
547 DieWithError(
548 ('%s\nMaybe your depot_tools is out of date?\n'
549 'If all fails, contact maruel@') % e)
550
551 # TODO(dpranke): We should propagate the error out instead of calling
552 # exit().
553 if not output.should_continue():
554 sys.exit(1)
555
556 return output
557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000558 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000559 """Updates the description and closes the issue."""
560 issue = int(self.GetIssue())
561 self.RpcServer().update_description(issue, self.description)
562 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000563
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000564 def SetFlag(self, flag, value):
565 """Patchset must match."""
566 if not self.GetPatchset():
567 DieWithError('The patchset needs to match. Send another patchset.')
568 try:
569 return self.RpcServer().set_flag(
570 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
571 except urllib2.HTTPError, e:
572 if e.code == 404:
573 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
574 if e.code == 403:
575 DieWithError(
576 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
577 'match?') % (self.GetIssue(), self.GetPatchset()))
578 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000580 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581 """Returns an upload.RpcServer() to access this review's rietveld instance.
582 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000583 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000584 self.GetIssue()
585 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000586 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587
588 def _IssueSetting(self):
589 """Return the git setting that stores this change's issue."""
590 return 'branch.%s.rietveldissue' % self.GetBranch()
591
592 def _PatchsetSetting(self):
593 """Return the git setting that stores this change's most recent patchset."""
594 return 'branch.%s.rietveldpatchset' % self.GetBranch()
595
596 def _RietveldServer(self):
597 """Returns the git setting that stores this change's rietveld server."""
598 return 'branch.%s.rietveldserver' % self.GetBranch()
599
600
601def GetCodereviewSettingsInteractively():
602 """Prompt the user for settings."""
603 server = settings.GetDefaultServerUrl(error_ok=True)
604 prompt = 'Rietveld server (host[:port])'
605 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000606 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 if not server and not newserver:
608 newserver = DEFAULT_SERVER
609 if newserver and newserver != server:
610 RunGit(['config', 'rietveld.server', newserver])
611
612 def SetProperty(initial, caption, name):
613 prompt = caption
614 if initial:
615 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000616 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617 if new_val == 'x':
618 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
619 elif new_val and new_val != initial:
620 RunGit(['config', 'rietveld.' + name, new_val])
621
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000622 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
624 'tree-status-url')
625 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
626
627 # TODO: configure a default branch to diff against, rather than this
628 # svn-based hackery.
629
630
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000631class ChangeDescription(object):
632 """Contains a parsed form of the change description."""
633 def __init__(self, subject, log_desc, reviewers):
634 self.subject = subject
635 self.log_desc = log_desc
636 self.reviewers = reviewers
637 self.description = self.log_desc
638
639 def Update(self):
640 initial_text = """# Enter a description of the change.
641# This will displayed on the codereview site.
642# The first line will also be used as the subject of the review.
643"""
644 initial_text += self.description
645 if 'R=' not in self.description and self.reviewers:
646 initial_text += '\nR=' + self.reviewers
647 if 'BUG=' not in self.description:
648 initial_text += '\nBUG='
649 if 'TEST=' not in self.description:
650 initial_text += '\nTEST='
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000651 content = gclient_utils.RunEditor(initial_text, True)
652 if not content:
653 DieWithError('Running editor failed')
654 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
655 if not content:
656 DieWithError('No CL description, aborting')
657 self._ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000658
659 def _ParseDescription(self, description):
660 if not description:
661 self.description = description
662 return
663
664 parsed_lines = []
665 reviewers_regexp = re.compile('\s*R=(.+)')
666 reviewers = ''
667 subject = ''
668 for l in description.splitlines():
669 if not subject:
670 subject = l
671 matched_reviewers = reviewers_regexp.match(l)
672 if matched_reviewers:
673 reviewers = matched_reviewers.group(1)
674 parsed_lines.append(l)
675
676 self.description = '\n'.join(parsed_lines) + '\n'
677 self.subject = subject
678 self.reviewers = reviewers
679
680 def IsEmpty(self):
681 return not self.description
682
683
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684def FindCodereviewSettingsFile(filename='codereview.settings'):
685 """Finds the given file starting in the cwd and going up.
686
687 Only looks up to the top of the repository unless an
688 'inherit-review-settings-ok' file exists in the root of the repository.
689 """
690 inherit_ok_file = 'inherit-review-settings-ok'
691 cwd = os.getcwd()
692 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
693 if os.path.isfile(os.path.join(root, inherit_ok_file)):
694 root = '/'
695 while True:
696 if filename in os.listdir(cwd):
697 if os.path.isfile(os.path.join(cwd, filename)):
698 return open(os.path.join(cwd, filename))
699 if cwd == root:
700 break
701 cwd = os.path.dirname(cwd)
702
703
704def LoadCodereviewSettingsFromFile(fileobj):
705 """Parse a codereview.settings file and updates hooks."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706 keyvals = {}
707 for line in fileobj.read().splitlines():
708 if not line or line.startswith("#"):
709 continue
710 k, v = line.split(": ", 1)
711 keyvals[k] = v
712
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713 def SetProperty(name, setting, unset_error_ok=False):
714 fullname = 'rietveld.' + name
715 if setting in keyvals:
716 RunGit(['config', fullname, keyvals[setting]])
717 else:
718 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
719
720 SetProperty('server', 'CODE_REVIEW_SERVER')
721 # Only server setting is required. Other settings can be absent.
722 # In that case, we ignore errors raised during option deletion attempt.
723 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
724 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
725 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
726
727 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
728 #should be of the form
729 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
730 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
731 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
732 keyvals['ORIGIN_URL_CONFIG']])
733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734
735@usage('[repo root containing codereview.settings]')
736def CMDconfig(parser, args):
737 """edit configuration for this tree"""
738
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000739 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 if len(args) == 0:
741 GetCodereviewSettingsInteractively()
742 return 0
743
744 url = args[0]
745 if not url.endswith('codereview.settings'):
746 url = os.path.join(url, 'codereview.settings')
747
748 # Load code review settings and download hooks (if available).
749 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
750 return 0
751
752
753def CMDstatus(parser, args):
754 """show status of changelists"""
755 parser.add_option('--field',
756 help='print only specific field (desc|id|patch|url)')
757 (options, args) = parser.parse_args(args)
758
759 # TODO: maybe make show_branches a flag if necessary.
760 show_branches = not options.field
761
762 if show_branches:
763 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
764 if branches:
765 print 'Branches associated with reviews:'
766 for branch in sorted(branches.splitlines()):
767 cl = Changelist(branchref=branch)
768 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
769
770 cl = Changelist()
771 if options.field:
772 if options.field.startswith('desc'):
773 print cl.GetDescription()
774 elif options.field == 'id':
775 issueid = cl.GetIssue()
776 if issueid:
777 print issueid
778 elif options.field == 'patch':
779 patchset = cl.GetPatchset()
780 if patchset:
781 print patchset
782 elif options.field == 'url':
783 url = cl.GetIssueURL()
784 if url:
785 print url
786 else:
787 print
788 print 'Current branch:',
789 if not cl.GetIssue():
790 print 'no issue assigned.'
791 return 0
792 print cl.GetBranch()
793 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
794 print 'Issue description:'
795 print cl.GetDescription(pretty=True)
796 return 0
797
798
799@usage('[issue_number]')
800def CMDissue(parser, args):
801 """Set or display the current code review issue number.
802
803 Pass issue number 0 to clear the current issue.
804"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000805 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806
807 cl = Changelist()
808 if len(args) > 0:
809 try:
810 issue = int(args[0])
811 except ValueError:
812 DieWithError('Pass a number to set the issue or none to list it.\n'
813 'Maybe you want to run git cl status?')
814 cl.SetIssue(issue)
815 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
816 return 0
817
818
819def CreateDescriptionFromLog(args):
820 """Pulls out the commit log to use as a base for the CL description."""
821 log_args = []
822 if len(args) == 1 and not args[0].endswith('.'):
823 log_args = [args[0] + '..']
824 elif len(args) == 1 and args[0].endswith('...'):
825 log_args = [args[0][:-1]]
826 elif len(args) == 2:
827 log_args = [args[0] + '..' + args[1]]
828 else:
829 log_args = args[:] # Hope for the best!
830 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
831
832
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000833def ConvertToInteger(inputval):
834 """Convert a string to integer, but returns either an int or None."""
835 try:
836 return int(inputval)
837 except (TypeError, ValueError):
838 return None
839
840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841def CMDpresubmit(parser, args):
842 """run presubmit tests on the current changelist"""
843 parser.add_option('--upload', action='store_true',
844 help='Run upload hook instead of the push/dcommit hook')
845 (options, args) = parser.parse_args(args)
846
847 # Make sure index is up-to-date before running diff-index.
848 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
849 if RunGit(['diff-index', 'HEAD']):
850 # TODO(maruel): Is this really necessary?
851 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
852 return 1
853
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000854 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 if args:
856 base_branch = args[0]
857 else:
858 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000859 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000861 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000862 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000863 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000864 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865
866
867@usage('[args to "git diff"]')
868def CMDupload(parser, args):
869 """upload the current changelist to codereview"""
870 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
871 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000872 parser.add_option('-f', action='store_true', dest='force',
873 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 parser.add_option('-m', dest='message', help='message for patch')
875 parser.add_option('-r', '--reviewers',
876 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000877 parser.add_option('--cc',
878 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 parser.add_option('--send-mail', action='store_true',
880 help='send email to reviewer immediately')
881 parser.add_option("--emulate_svn_auto_props", action="store_true",
882 dest="emulate_svn_auto_props",
883 help="Emulate Subversion's auto properties feature.")
884 parser.add_option("--desc_from_logs", action="store_true",
885 dest="from_logs",
886 help="""Squashes git commit logs into change description and
887 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000888 parser.add_option('-c', '--use-commit-queue', action='store_true',
889 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 (options, args) = parser.parse_args(args)
891
892 # Make sure index is up-to-date before running diff-index.
893 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
894 if RunGit(['diff-index', 'HEAD']):
895 print 'Cannot upload with a dirty tree. You must commit locally first.'
896 return 1
897
898 cl = Changelist()
899 if args:
900 base_branch = args[0]
901 else:
902 # Default to diffing against the "upstream" branch.
903 base_branch = cl.GetUpstreamBranch()
904 args = [base_branch + "..."]
905
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000906 if not options.bypass_hooks and not options.force:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000907 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000908 may_prompt=True,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000909 verbose=options.verbose,
910 author=None)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000911 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000912 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914
915 # --no-ext-diff is broken in some versions of Git, so try to work around
916 # this by overriding the environment (but there is still a problem if the
917 # git config key "diff.external" is used).
918 env = os.environ.copy()
919 if 'GIT_EXTERNAL_DIFF' in env:
920 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000921 subprocess2.call(
922 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923
924 upload_args = ['--assume_yes'] # Don't ask about untracked files.
925 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 if options.emulate_svn_auto_props:
927 upload_args.append('--emulate_svn_auto_props')
928 if options.send_mail:
929 if not options.reviewers:
930 DieWithError("Must specify reviewers to send email.")
931 upload_args.append('--send_mail')
932 if options.from_logs and not options.message:
933 print 'Must set message for subject line if using desc_from_logs'
934 return 1
935
936 change_desc = None
937
938 if cl.GetIssue():
939 if options.message:
940 upload_args.extend(['--message', options.message])
941 upload_args.extend(['--issue', cl.GetIssue()])
942 print ("This branch is associated with issue %s. "
943 "Adding patch to that issue." % cl.GetIssue())
944 else:
945 log_desc = CreateDescriptionFromLog(args)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000946 change_desc = ChangeDescription(options.message, log_desc,
947 options.reviewers)
948 if not options.from_logs:
949 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000950
951 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952 print "Description is empty; aborting."
953 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000954
955 upload_args.extend(['--message', change_desc.subject])
956 upload_args.extend(['--description', change_desc.description])
957 if change_desc.reviewers:
958 upload_args.extend(['--reviewers', change_desc.reviewers])
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000959 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000960 if cc:
961 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962
963 # Include the upstream repo's URL in the change -- this is useful for
964 # projects that have their source spread across multiple repos.
965 remote_url = None
966 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000967 # URL is dependent on the current directory.
968 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 if data:
970 keys = dict(line.split(': ', 1) for line in data.splitlines()
971 if ': ' in line)
972 remote_url = keys.get('URL', None)
973 else:
974 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
975 remote_url = (cl.GetRemoteUrl() + '@'
976 + cl.GetUpstreamBranch().split('/')[-1])
977 if remote_url:
978 upload_args.extend(['--base_url', remote_url])
979
980 try:
981 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000982 except KeyboardInterrupt:
983 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984 except:
985 # If we got an exception after the user typed a description for their
986 # change, back up the description before re-raising.
987 if change_desc:
988 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
989 print '\nGot exception while uploading -- saving description to %s\n' \
990 % backup_path
991 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000992 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993 backup_file.close()
994 raise
995
996 if not cl.GetIssue():
997 cl.SetIssue(issue)
998 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000999
1000 if options.use_commit_queue:
1001 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 return 0
1003
1004
1005def SendUpstream(parser, args, cmd):
1006 """Common code for CmdPush and CmdDCommit
1007
1008 Squashed commit into a single.
1009 Updates changelog with metadata (e.g. pointer to review).
1010 Pushes/dcommits the code upstream.
1011 Updates review and closes.
1012 """
1013 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1014 help='bypass upload presubmit hook')
1015 parser.add_option('-m', dest='message',
1016 help="override review description")
1017 parser.add_option('-f', action='store_true', dest='force',
1018 help="force yes to questions (don't prompt)")
1019 parser.add_option('-c', dest='contributor',
1020 help="external contributor for patch (appended to " +
1021 "description and used as author for git). Should be " +
1022 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023 (options, args) = parser.parse_args(args)
1024 cl = Changelist()
1025
1026 if not args or cmd == 'push':
1027 # Default to merging against our best guess of the upstream branch.
1028 args = [cl.GetUpstreamBranch()]
1029
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001030 if options.contributor:
1031 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1032 print "Please provide contibutor as 'First Last <email@example.com>'"
1033 return 1
1034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001035 base_branch = args[0]
1036
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001037 # Make sure index is up-to-date before running diff-index.
1038 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if RunGit(['diff-index', 'HEAD']):
1040 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1041 return 1
1042
1043 # This rev-list syntax means "show all commits not in my branch that
1044 # are in base_branch".
1045 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1046 base_branch]).splitlines()
1047 if upstream_commits:
1048 print ('Base branch "%s" has %d commits '
1049 'not in this branch.' % (base_branch, len(upstream_commits)))
1050 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1051 return 1
1052
1053 if cmd == 'dcommit':
1054 # This is the revision `svn dcommit` will commit on top of.
1055 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1056 '--pretty=format:%H'])
1057 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1058 if extra_commits:
1059 print ('This branch has %d additional commits not upstreamed yet.'
1060 % len(extra_commits.splitlines()))
1061 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1062 'before attempting to %s.' % (base_branch, cmd))
1063 return 1
1064
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001065 if not options.bypass_hooks and not options.force:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001066 author = None
1067 if options.contributor:
1068 author = re.search(r'\<(.*)\>', options.contributor).group(1)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001069 cl.RunHook(committing=True, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001070 may_prompt=True, verbose=options.verbose,
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001071 author=author)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072
1073 if cmd == 'dcommit':
1074 # Check the tree status if the tree status URL is set.
1075 status = GetTreeStatus()
1076 if 'closed' == status:
1077 print ('The tree is closed. Please wait for it to reopen. Use '
1078 '"git cl dcommit -f" to commit on a closed tree.')
1079 return 1
1080 elif 'unknown' == status:
1081 print ('Unable to determine tree status. Please verify manually and '
1082 'use "git cl dcommit -f" to commit on a closed tree.')
1083
1084 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001085 if not description and cl.GetIssue():
1086 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001088 if not description:
1089 print 'No description set.'
1090 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1091 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001093 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095
1096 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 description += "\nPatch from %s." % options.contributor
1098 print 'Description:', repr(description)
1099
1100 branches = [base_branch, cl.GetBranchRef()]
1101 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001102 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001103 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104
1105 # We want to squash all this branch's commits into one commit with the
1106 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001107 # We do this by doing a "reset --soft" to the base branch (which keeps
1108 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 MERGE_BRANCH = 'git-cl-commit'
1110 # Delete the merge branch if it already exists.
1111 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1112 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1113 RunGit(['branch', '-D', MERGE_BRANCH])
1114
1115 # We might be in a directory that's present in this branch but not in the
1116 # trunk. Move up to the top of the tree so that git commands that expect a
1117 # valid CWD won't fail after we check out the merge branch.
1118 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1119 if rel_base_path:
1120 os.chdir(rel_base_path)
1121
1122 # Stuff our change into the merge branch.
1123 # We wrap in a try...finally block so if anything goes wrong,
1124 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001125 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001127 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1128 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 if options.contributor:
1130 RunGit(['commit', '--author', options.contributor, '-m', description])
1131 else:
1132 RunGit(['commit', '-m', description])
1133 if cmd == 'push':
1134 # push the merge branch.
1135 remote, branch = cl.FetchUpstreamTuple()
1136 retcode, output = RunGitWithCode(
1137 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1138 logging.debug(output)
1139 else:
1140 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001141 retcode, output = RunGitWithCode(['svn', 'dcommit',
1142 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 finally:
1144 # And then swap back to the original branch and clean up.
1145 RunGit(['checkout', '-q', cl.GetBranch()])
1146 RunGit(['branch', '-D', MERGE_BRANCH])
1147
1148 if cl.GetIssue():
1149 if cmd == 'dcommit' and 'Committed r' in output:
1150 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1151 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001152 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1153 for l in output.splitlines(False))
1154 match = filter(None, match)
1155 if len(match) != 1:
1156 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1157 output)
1158 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 else:
1160 return 1
1161 viewvc_url = settings.GetViewVCUrl()
1162 if viewvc_url and revision:
1163 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1164 print ('Closing issue '
1165 '(you may be prompted for your codereview password)...')
1166 cl.CloseIssue()
1167 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001168
1169 if retcode == 0:
1170 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1171 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001172 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001173
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174 return 0
1175
1176
1177@usage('[upstream branch to apply against]')
1178def CMDdcommit(parser, args):
1179 """commit the current changelist via git-svn"""
1180 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001181 message = """This doesn't appear to be an SVN repository.
1182If your project has a git mirror with an upstream SVN master, you probably need
1183to run 'git svn init', see your project's git mirror documentation.
1184If your project has a true writeable upstream repository, you probably want
1185to run 'git cl push' instead.
1186Choose wisely, if you get this wrong, your commit might appear to succeed but
1187will instead be silently ignored."""
1188 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001189 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 return SendUpstream(parser, args, 'dcommit')
1191
1192
1193@usage('[upstream branch to apply against]')
1194def CMDpush(parser, args):
1195 """commit the current changelist via git"""
1196 if settings.GetIsGitSvn():
1197 print('This appears to be an SVN repository.')
1198 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001199 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200 return SendUpstream(parser, args, 'push')
1201
1202
1203@usage('<patch url or issue id>')
1204def CMDpatch(parser, args):
1205 """patch in a code review"""
1206 parser.add_option('-b', dest='newbranch',
1207 help='create a new branch off trunk for the patch')
1208 parser.add_option('-f', action='store_true', dest='force',
1209 help='with -b, clobber any existing branch')
1210 parser.add_option('--reject', action='store_true', dest='reject',
1211 help='allow failed patches and spew .rej files')
1212 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1213 help="don't commit after patch applies")
1214 (options, args) = parser.parse_args(args)
1215 if len(args) != 1:
1216 parser.print_help()
1217 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001218 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001220 # TODO(maruel): Use apply_issue.py
1221
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001222 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001224 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001225 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 else:
1227 # Assume it's a URL to the patch. Default to http.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001228 issue_url = FixUrl(issue_arg)
1229 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001230 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 DieWithError('Must pass an issue ID or full URL for '
1232 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001233 issue = match.group(1)
1234 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
1236 if options.newbranch:
1237 if options.force:
1238 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001239 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 RunGit(['checkout', '-b', options.newbranch,
1241 Changelist().GetUpstreamBranch()])
1242
1243 # Switch up to the top-level directory, if necessary, in preparation for
1244 # applying the patch.
1245 top = RunGit(['rev-parse', '--show-cdup']).strip()
1246 if top:
1247 os.chdir(top)
1248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 # Git patches have a/ at the beginning of source paths. We strip that out
1250 # with a sed script rather than the -p flag to patch so we can feed either
1251 # Git or svn-style patches into the same apply command.
1252 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001253 try:
1254 patch_data = subprocess2.check_output(
1255 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1256 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 DieWithError('Git patch mungling failed.')
1258 logging.info(patch_data)
1259 # We use "git apply" to apply the patch instead of "patch" so that we can
1260 # pick up file adds.
1261 # The --index flag means: also insert into the index (so we catch adds).
1262 cmd = ['git', 'apply', '--index', '-p0']
1263 if options.reject:
1264 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001265 try:
1266 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1267 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 DieWithError('Failed to apply the patch')
1269
1270 # If we had an issue, commit the current state and register the issue.
1271 if not options.nocommit:
1272 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1273 cl = Changelist()
1274 cl.SetIssue(issue)
1275 print "Committed patch."
1276 else:
1277 print "Patch applied to index."
1278 return 0
1279
1280
1281def CMDrebase(parser, args):
1282 """rebase current branch on top of svn repo"""
1283 # Provide a wrapper for git svn rebase to help avoid accidental
1284 # git svn dcommit.
1285 # It's the only command that doesn't use parser at all since we just defer
1286 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001287 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
1289
1290def GetTreeStatus():
1291 """Fetches the tree status and returns either 'open', 'closed',
1292 'unknown' or 'unset'."""
1293 url = settings.GetTreeStatusUrl(error_ok=True)
1294 if url:
1295 status = urllib2.urlopen(url).read().lower()
1296 if status.find('closed') != -1 or status == '0':
1297 return 'closed'
1298 elif status.find('open') != -1 or status == '1':
1299 return 'open'
1300 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 return 'unset'
1302
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304def GetTreeStatusReason():
1305 """Fetches the tree status from a json url and returns the message
1306 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001307 url = settings.GetTreeStatusUrl()
1308 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309 connection = urllib2.urlopen(json_url)
1310 status = json.loads(connection.read())
1311 connection.close()
1312 return status['message']
1313
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315def CMDtree(parser, args):
1316 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001317 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 status = GetTreeStatus()
1319 if 'unset' == status:
1320 print 'You must configure your tree status URL by running "git cl config".'
1321 return 2
1322
1323 print "The tree is %s" % status
1324 print
1325 print GetTreeStatusReason()
1326 if status != 'open':
1327 return 1
1328 return 0
1329
1330
1331def CMDupstream(parser, args):
1332 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001333 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001334 if args:
1335 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 cl = Changelist()
1337 print cl.GetUpstreamBranch()
1338 return 0
1339
1340
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001341def CMDset_commit(parser, args):
1342 """set the commit bit"""
1343 _, args = parser.parse_args(args)
1344 if args:
1345 parser.error('Unrecognized args: %s' % ' '.join(args))
1346 cl = Changelist()
1347 cl.SetFlag('commit', '1')
1348 return 0
1349
1350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351def Command(name):
1352 return getattr(sys.modules[__name__], 'CMD' + name, None)
1353
1354
1355def CMDhelp(parser, args):
1356 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001357 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 if len(args) == 1:
1359 return main(args + ['--help'])
1360 parser.print_help()
1361 return 0
1362
1363
1364def GenUsage(parser, command):
1365 """Modify an OptParse object with the function's documentation."""
1366 obj = Command(command)
1367 more = getattr(obj, 'usage_more', '')
1368 if command == 'help':
1369 command = '<command>'
1370 else:
1371 # OptParser.description prefer nicely non-formatted strings.
1372 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1373 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1374
1375
1376def main(argv):
1377 """Doesn't parse the arguments here, just find the right subcommand to
1378 execute."""
1379 # Do it late so all commands are listed.
1380 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1381 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1382 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1383
1384 # Create the option parse and add --verbose support.
1385 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001386 parser.add_option(
1387 '-v', '--verbose', action='count', default=0,
1388 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 old_parser_args = parser.parse_args
1390 def Parse(args):
1391 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001392 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001394 elif options.verbose:
1395 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 else:
1397 logging.basicConfig(level=logging.WARNING)
1398 return options, args
1399 parser.parse_args = Parse
1400
1401 if argv:
1402 command = Command(argv[0])
1403 if command:
1404 # "fix" the usage and the description now that we know the subcommand.
1405 GenUsage(parser, argv[0])
1406 try:
1407 return command(parser, argv[1:])
1408 except urllib2.HTTPError, e:
1409 if e.code != 500:
1410 raise
1411 DieWithError(
1412 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1413 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1414
1415 # Not a known command. Default to help.
1416 GenUsage(parser, 'help')
1417 return CMDhelp(parser, argv)
1418
1419
1420if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001421 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 sys.exit(main(sys.argv[1:]))