blob: 6af8bcbe53e5233fe6bf01c8e420dd226256ba55 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
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
maruel@chromium.orgddd59412011-11-30 14:20:38 +000051# Initialized in main()
52settings = None
53
54
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000056 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057 sys.exit(1)
58
59
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 return subprocess2.check_output(args, shell=False, **kwargs)
63 except subprocess2.CalledProcessError, e:
64 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 'Command "%s" failed.\n%s' % (
67 ' '.join(args), error_message or e.stdout or ''))
68 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns stdout."""
73 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns return code and stdout."""
78 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
79 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def usage(more):
83 def hook(fn):
84 fn.usage_more = more
85 return fn
86 return hook
87
88
maruel@chromium.org90541732011-04-01 17:54:18 +000089def ask_for_data(prompt):
90 try:
91 return raw_input(prompt)
92 except KeyboardInterrupt:
93 # Hide the exception.
94 sys.exit(1)
95
96
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097def FixUrl(server):
98 """Fix a server url to defaults protocol to http:// if none is specified."""
99 if not server:
100 return server
101 if not re.match(r'[a-z]+\://.*', server):
102 return 'http://' + server
103 return server
104
105
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000106def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
107 """Return the corresponding git ref if |base_url| together with |glob_spec|
108 matches the full |url|.
109
110 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
111 """
112 fetch_suburl, as_ref = glob_spec.split(':')
113 if allow_wildcards:
114 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
115 if glob_match:
116 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
117 # "branches/{472,597,648}/src:refs/remotes/svn/*".
118 branch_re = re.escape(base_url)
119 if glob_match.group(1):
120 branch_re += '/' + re.escape(glob_match.group(1))
121 wildcard = glob_match.group(2)
122 if wildcard == '*':
123 branch_re += '([^/]*)'
124 else:
125 # Escape and replace surrounding braces with parentheses and commas
126 # with pipe symbols.
127 wildcard = re.escape(wildcard)
128 wildcard = re.sub('^\\\\{', '(', wildcard)
129 wildcard = re.sub('\\\\,', '|', wildcard)
130 wildcard = re.sub('\\\\}$', ')', wildcard)
131 branch_re += wildcard
132 if glob_match.group(3):
133 branch_re += re.escape(glob_match.group(3))
134 match = re.match(branch_re, url)
135 if match:
136 return re.sub('\*$', match.group(1), as_ref)
137
138 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
139 if fetch_suburl:
140 full_url = base_url + '/' + fetch_suburl
141 else:
142 full_url = base_url
143 if full_url == url:
144 return as_ref
145 return None
146
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000148class Settings(object):
149 def __init__(self):
150 self.default_server = None
151 self.cc = None
152 self.root = None
153 self.is_git_svn = None
154 self.svn_branch = None
155 self.tree_status_url = None
156 self.viewvc_url = None
157 self.updated = False
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000158 self.did_migrate_check = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000159
160 def LazyUpdateIfNeeded(self):
161 """Updates the settings from a codereview.settings file, if available."""
162 if not self.updated:
163 cr_settings_file = FindCodereviewSettingsFile()
164 if cr_settings_file:
165 LoadCodereviewSettingsFromFile(cr_settings_file)
166 self.updated = True
167
168 def GetDefaultServerUrl(self, error_ok=False):
169 if not self.default_server:
170 self.LazyUpdateIfNeeded()
171 self.default_server = FixUrl(self._GetConfig('rietveld.server',
172 error_ok=True))
173 if error_ok:
174 return self.default_server
175 if not self.default_server:
176 error_message = ('Could not find settings file. You must configure '
177 'your review setup by running "git cl config".')
178 self.default_server = FixUrl(self._GetConfig(
179 'rietveld.server', error_message=error_message))
180 return self.default_server
181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000182 def GetRoot(self):
183 if not self.root:
184 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
185 return self.root
186
187 def GetIsGitSvn(self):
188 """Return true if this repo looks like it's using git-svn."""
189 if self.is_git_svn is None:
190 # If you have any "svn-remote.*" config keys, we think you're using svn.
191 self.is_git_svn = RunGitWithCode(
192 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
193 return self.is_git_svn
194
195 def GetSVNBranch(self):
196 if self.svn_branch is None:
197 if not self.GetIsGitSvn():
198 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
199
200 # Try to figure out which remote branch we're based on.
201 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000202 # 1) iterate through our branch history and find the svn URL.
203 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000204
205 # regexp matching the git-svn line that contains the URL.
206 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
207
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000208 # We don't want to go through all of history, so read a line from the
209 # pipe at a time.
210 # The -100 is an arbitrary limit so we don't search forever.
211 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000212 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000213 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000214 for line in proc.stdout:
215 match = git_svn_re.match(line)
216 if match:
217 url = match.group(1)
218 proc.stdout.close() # Cut pipe.
219 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000220
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000221 if url:
222 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
223 remotes = RunGit(['config', '--get-regexp',
224 r'^svn-remote\..*\.url']).splitlines()
225 for remote in remotes:
226 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000227 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000228 remote = match.group(1)
229 base_url = match.group(2)
230 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000231 ['config', 'svn-remote.%s.fetch' % remote],
232 error_ok=True).strip()
233 if fetch_spec:
234 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
235 if self.svn_branch:
236 break
237 branch_spec = RunGit(
238 ['config', 'svn-remote.%s.branches' % remote],
239 error_ok=True).strip()
240 if branch_spec:
241 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
242 if self.svn_branch:
243 break
244 tag_spec = RunGit(
245 ['config', 'svn-remote.%s.tags' % remote],
246 error_ok=True).strip()
247 if tag_spec:
248 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
249 if self.svn_branch:
250 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000251
252 if not self.svn_branch:
253 DieWithError('Can\'t guess svn branch -- try specifying it on the '
254 'command line')
255
256 return self.svn_branch
257
258 def GetTreeStatusUrl(self, error_ok=False):
259 if not self.tree_status_url:
260 error_message = ('You must configure your tree status URL by running '
261 '"git cl config".')
262 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
263 error_ok=error_ok,
264 error_message=error_message)
265 return self.tree_status_url
266
267 def GetViewVCUrl(self):
268 if not self.viewvc_url:
269 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
270 return self.viewvc_url
271
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000272 def GetDefaultCCList(self):
273 return self._GetConfig('rietveld.cc', error_ok=True)
274
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 def _GetConfig(self, param, **kwargs):
276 self.LazyUpdateIfNeeded()
277 return RunGit(['config', param], **kwargs).strip()
278
279
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000280def CheckForMigration():
281 """Migrate from the old issue format, if found.
282
283 We used to store the branch<->issue mapping in a file in .git, but it's
284 better to store it in the .git/config, since deleting a branch deletes that
285 branch's entry there.
286 """
287
288 # Don't run more than once.
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000289 if settings.did_migrate_check:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 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)
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000303 settings.did_migrate_check = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000304
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.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000314 global settings
315 if not settings:
316 # Happens when git_cl.py is used as a utility library.
317 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000318 settings.GetDefaultServerUrl()
319 self.branchref = branchref
320 if self.branchref:
321 self.branch = ShortBranchName(self.branchref)
322 else:
323 self.branch = None
324 self.rietveld_server = None
325 self.upstream_branch = None
326 self.has_issue = False
327 self.issue = None
328 self.has_description = False
329 self.description = None
330 self.has_patchset = False
331 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000332 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000333 self.cc = None
334 self.watchers = ()
335
336 def GetCCList(self):
337 """Return the users cc'd on this CL.
338
339 Return is a string suitable for passing to gcl with the --cc flag.
340 """
341 if self.cc is None:
342 base_cc = settings .GetDefaultCCList()
343 more_cc = ','.join(self.watchers)
344 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
345 return self.cc
346
347 def SetWatchers(self, watchers):
348 """Set the list of email addresses that should be cc'd based on the changed
349 files in this CL.
350 """
351 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000352
353 def GetBranch(self):
354 """Returns the short branch name, e.g. 'master'."""
355 if not self.branch:
356 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
357 self.branch = ShortBranchName(self.branchref)
358 return self.branch
359
360 def GetBranchRef(self):
361 """Returns the full branch name, e.g. 'refs/heads/master'."""
362 self.GetBranch() # Poke the lazy loader.
363 return self.branchref
364
365 def FetchUpstreamTuple(self):
366 """Returns a tuple containg remote and remote ref,
367 e.g. 'origin', 'refs/heads/master'
368 """
369 remote = '.'
370 branch = self.GetBranch()
371 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
372 error_ok=True).strip()
373 if upstream_branch:
374 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
375 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000376 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
377 error_ok=True).strip()
378 if upstream_branch:
379 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000380 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000381 # Fall back on trying a git-svn upstream branch.
382 if settings.GetIsGitSvn():
383 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000384 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000385 # Else, try to guess the origin remote.
386 remote_branches = RunGit(['branch', '-r']).split()
387 if 'origin/master' in remote_branches:
388 # Fall back on origin/master if it exits.
389 remote = 'origin'
390 upstream_branch = 'refs/heads/master'
391 elif 'origin/trunk' in remote_branches:
392 # Fall back on origin/trunk if it exists. Generally a shared
393 # git-svn clone
394 remote = 'origin'
395 upstream_branch = 'refs/heads/trunk'
396 else:
397 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000398Either pass complete "git diff"-style arguments, like
399 git cl upload origin/master
400or verify this branch is set up to track another (via the --track argument to
401"git checkout -b ...").""")
402
403 return remote, upstream_branch
404
405 def GetUpstreamBranch(self):
406 if self.upstream_branch is None:
407 remote, upstream_branch = self.FetchUpstreamTuple()
408 if remote is not '.':
409 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
410 self.upstream_branch = upstream_branch
411 return self.upstream_branch
412
413 def GetRemoteUrl(self):
414 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
415
416 Returns None if there is no remote.
417 """
418 remote = self.FetchUpstreamTuple()[0]
419 if remote == '.':
420 return None
421 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
422
423 def GetIssue(self):
424 if not self.has_issue:
425 CheckForMigration()
426 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
427 if issue:
428 self.issue = issue
429 self.rietveld_server = FixUrl(RunGit(
430 ['config', self._RietveldServer()], error_ok=True).strip())
431 else:
432 self.issue = None
433 if not self.rietveld_server:
434 self.rietveld_server = settings.GetDefaultServerUrl()
435 self.has_issue = True
436 return self.issue
437
438 def GetRietveldServer(self):
439 self.GetIssue()
440 return self.rietveld_server
441
442 def GetIssueURL(self):
443 """Get the URL for a particular issue."""
444 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
445
446 def GetDescription(self, pretty=False):
447 if not self.has_description:
448 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000449 issue = int(self.GetIssue())
450 try:
451 self.description = self.RpcServer().get_description(issue).strip()
452 except urllib2.HTTPError, e:
453 if e.code == 404:
454 DieWithError(
455 ('\nWhile fetching the description for issue %d, received a '
456 '404 (not found)\n'
457 'error. It is likely that you deleted this '
458 'issue on the server. If this is the\n'
459 'case, please run\n\n'
460 ' git cl issue 0\n\n'
461 'to clear the association with the deleted issue. Then run '
462 'this command again.') % issue)
463 else:
464 DieWithError(
465 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 self.has_description = True
467 if pretty:
468 wrapper = textwrap.TextWrapper()
469 wrapper.initial_indent = wrapper.subsequent_indent = ' '
470 return wrapper.fill(self.description)
471 return self.description
472
473 def GetPatchset(self):
474 if not self.has_patchset:
475 patchset = RunGit(['config', self._PatchsetSetting()],
476 error_ok=True).strip()
477 if patchset:
478 self.patchset = patchset
479 else:
480 self.patchset = None
481 self.has_patchset = True
482 return self.patchset
483
484 def SetPatchset(self, patchset):
485 """Set this branch's patchset. If patchset=0, clears the patchset."""
486 if patchset:
487 RunGit(['config', self._PatchsetSetting(), str(patchset)])
488 else:
489 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000490 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 self.has_patchset = False
492
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000493 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000494 patchset = self.RpcServer().get_issue_properties(
495 int(issue), False)['patchsets'][-1]
496 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000497 '/download/issue%s_%s.diff' % (issue, patchset))
498
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000499 def SetIssue(self, issue):
500 """Set this branch's issue. If issue=0, clears the issue."""
501 if issue:
502 RunGit(['config', self._IssueSetting(), str(issue)])
503 if self.rietveld_server:
504 RunGit(['config', self._RietveldServer(), self.rietveld_server])
505 else:
506 RunGit(['config', '--unset', self._IssueSetting()])
507 self.SetPatchset(0)
508 self.has_issue = False
509
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000510 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000511 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
512 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000513
514 # We use the sha1 of HEAD as a name of this change.
515 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000516 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000517 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000518 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000519 except subprocess2.CalledProcessError:
520 DieWithError(
521 ('\nFailed to diff against upstream branch %s!\n\n'
522 'This branch probably doesn\'t exist anymore. To reset the\n'
523 'tracking branch, please run\n'
524 ' git branch --set-upstream %s trunk\n'
525 'replacing trunk with origin/master or the relevant branch') %
526 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000527
528 issue = ConvertToInteger(self.GetIssue())
529 patchset = ConvertToInteger(self.GetPatchset())
530 if issue:
531 description = self.GetDescription()
532 else:
533 # If the change was never uploaded, use the log messages of all commits
534 # up to the branch point, as git cl upload will prefill the description
535 # with these log messages.
536 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
537 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000538
539 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000540 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000541 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000542 name,
543 description,
544 absroot,
545 files,
546 issue,
547 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000548 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000549
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000550 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
551 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
552 change = self.GetChange(upstream_branch, author)
553
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000554 # Apply watchlists on upload.
555 if not committing:
556 watchlist = watchlists.Watchlists(change.RepositoryRoot())
557 files = [f.LocalPath() for f in change.AffectedFiles()]
558 self.SetWatchers(watchlist.GetWatchersForPaths(files))
559
560 try:
561 output = presubmit_support.DoPresubmitChecks(change, committing,
562 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000563 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000564 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000565 except presubmit_support.PresubmitFailure, e:
566 DieWithError(
567 ('%s\nMaybe your depot_tools is out of date?\n'
568 'If all fails, contact maruel@') % e)
569
570 # TODO(dpranke): We should propagate the error out instead of calling
571 # exit().
572 if not output.should_continue():
573 sys.exit(1)
574
575 return output
576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000577 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000578 """Updates the description and closes the issue."""
579 issue = int(self.GetIssue())
580 self.RpcServer().update_description(issue, self.description)
581 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000583 def SetFlag(self, flag, value):
584 """Patchset must match."""
585 if not self.GetPatchset():
586 DieWithError('The patchset needs to match. Send another patchset.')
587 try:
588 return self.RpcServer().set_flag(
589 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
590 except urllib2.HTTPError, e:
591 if e.code == 404:
592 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
593 if e.code == 403:
594 DieWithError(
595 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
596 'match?') % (self.GetIssue(), self.GetPatchset()))
597 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000599 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 """Returns an upload.RpcServer() to access this review's rietveld instance.
601 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000602 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000603 self.GetIssue()
604 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000605 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606
607 def _IssueSetting(self):
608 """Return the git setting that stores this change's issue."""
609 return 'branch.%s.rietveldissue' % self.GetBranch()
610
611 def _PatchsetSetting(self):
612 """Return the git setting that stores this change's most recent patchset."""
613 return 'branch.%s.rietveldpatchset' % self.GetBranch()
614
615 def _RietveldServer(self):
616 """Returns the git setting that stores this change's rietveld server."""
617 return 'branch.%s.rietveldserver' % self.GetBranch()
618
619
620def GetCodereviewSettingsInteractively():
621 """Prompt the user for settings."""
622 server = settings.GetDefaultServerUrl(error_ok=True)
623 prompt = 'Rietveld server (host[:port])'
624 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000625 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 if not server and not newserver:
627 newserver = DEFAULT_SERVER
628 if newserver and newserver != server:
629 RunGit(['config', 'rietveld.server', newserver])
630
631 def SetProperty(initial, caption, name):
632 prompt = caption
633 if initial:
634 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000635 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000636 if new_val == 'x':
637 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
638 elif new_val and new_val != initial:
639 RunGit(['config', 'rietveld.' + name, new_val])
640
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000641 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000642 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
643 'tree-status-url')
644 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
645
646 # TODO: configure a default branch to diff against, rather than this
647 # svn-based hackery.
648
649
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000650class ChangeDescription(object):
651 """Contains a parsed form of the change description."""
652 def __init__(self, subject, log_desc, reviewers):
653 self.subject = subject
654 self.log_desc = log_desc
655 self.reviewers = reviewers
656 self.description = self.log_desc
657
658 def Update(self):
659 initial_text = """# Enter a description of the change.
660# This will displayed on the codereview site.
661# The first line will also be used as the subject of the review.
662"""
663 initial_text += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000664 if ('\nR=' not in self.description and
665 '\nTBR=' not in self.description and
666 self.reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000667 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000668 if '\nBUG=' not in self.description:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000669 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000670 if '\nTEST=' not in self.description:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000671 initial_text += '\nTEST='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000672 initial_text = initial_text.rstrip('\n') + '\n'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000673 content = gclient_utils.RunEditor(initial_text, True)
674 if not content:
675 DieWithError('Running editor failed')
676 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
677 if not content:
678 DieWithError('No CL description, aborting')
679 self._ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000680
681 def _ParseDescription(self, description):
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000682 """Updates the list of reviewers and subject from the description."""
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000683 if not description:
684 self.description = description
685 return
686
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000687 self.description = description.strip('\n') + '\n'
688 self.subject = description.split('\n', 1)[0]
689 # Retrieves all reviewer lines
690 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
691 self.reviewers = ','.join(
692 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000693
694 def IsEmpty(self):
695 return not self.description
696
697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698def FindCodereviewSettingsFile(filename='codereview.settings'):
699 """Finds the given file starting in the cwd and going up.
700
701 Only looks up to the top of the repository unless an
702 'inherit-review-settings-ok' file exists in the root of the repository.
703 """
704 inherit_ok_file = 'inherit-review-settings-ok'
705 cwd = os.getcwd()
706 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
707 if os.path.isfile(os.path.join(root, inherit_ok_file)):
708 root = '/'
709 while True:
710 if filename in os.listdir(cwd):
711 if os.path.isfile(os.path.join(cwd, filename)):
712 return open(os.path.join(cwd, filename))
713 if cwd == root:
714 break
715 cwd = os.path.dirname(cwd)
716
717
718def LoadCodereviewSettingsFromFile(fileobj):
719 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000720 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 def SetProperty(name, setting, unset_error_ok=False):
723 fullname = 'rietveld.' + name
724 if setting in keyvals:
725 RunGit(['config', fullname, keyvals[setting]])
726 else:
727 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
728
729 SetProperty('server', 'CODE_REVIEW_SERVER')
730 # Only server setting is required. Other settings can be absent.
731 # In that case, we ignore errors raised during option deletion attempt.
732 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
733 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
734 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
735
736 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
737 #should be of the form
738 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
739 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
740 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
741 keyvals['ORIGIN_URL_CONFIG']])
742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743
744@usage('[repo root containing codereview.settings]')
745def CMDconfig(parser, args):
746 """edit configuration for this tree"""
747
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000748 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749 if len(args) == 0:
750 GetCodereviewSettingsInteractively()
751 return 0
752
753 url = args[0]
754 if not url.endswith('codereview.settings'):
755 url = os.path.join(url, 'codereview.settings')
756
757 # Load code review settings and download hooks (if available).
758 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
759 return 0
760
761
762def CMDstatus(parser, args):
763 """show status of changelists"""
764 parser.add_option('--field',
765 help='print only specific field (desc|id|patch|url)')
766 (options, args) = parser.parse_args(args)
767
768 # TODO: maybe make show_branches a flag if necessary.
769 show_branches = not options.field
770
771 if show_branches:
772 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
773 if branches:
774 print 'Branches associated with reviews:'
775 for branch in sorted(branches.splitlines()):
776 cl = Changelist(branchref=branch)
777 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
778
779 cl = Changelist()
780 if options.field:
781 if options.field.startswith('desc'):
782 print cl.GetDescription()
783 elif options.field == 'id':
784 issueid = cl.GetIssue()
785 if issueid:
786 print issueid
787 elif options.field == 'patch':
788 patchset = cl.GetPatchset()
789 if patchset:
790 print patchset
791 elif options.field == 'url':
792 url = cl.GetIssueURL()
793 if url:
794 print url
795 else:
796 print
797 print 'Current branch:',
798 if not cl.GetIssue():
799 print 'no issue assigned.'
800 return 0
801 print cl.GetBranch()
802 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
803 print 'Issue description:'
804 print cl.GetDescription(pretty=True)
805 return 0
806
807
808@usage('[issue_number]')
809def CMDissue(parser, args):
810 """Set or display the current code review issue number.
811
812 Pass issue number 0 to clear the current issue.
813"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000814 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815
816 cl = Changelist()
817 if len(args) > 0:
818 try:
819 issue = int(args[0])
820 except ValueError:
821 DieWithError('Pass a number to set the issue or none to list it.\n'
822 'Maybe you want to run git cl status?')
823 cl.SetIssue(issue)
824 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
825 return 0
826
827
828def CreateDescriptionFromLog(args):
829 """Pulls out the commit log to use as a base for the CL description."""
830 log_args = []
831 if len(args) == 1 and not args[0].endswith('.'):
832 log_args = [args[0] + '..']
833 elif len(args) == 1 and args[0].endswith('...'):
834 log_args = [args[0][:-1]]
835 elif len(args) == 2:
836 log_args = [args[0] + '..' + args[1]]
837 else:
838 log_args = args[:] # Hope for the best!
839 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
840
841
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000842def ConvertToInteger(inputval):
843 """Convert a string to integer, but returns either an int or None."""
844 try:
845 return int(inputval)
846 except (TypeError, ValueError):
847 return None
848
849
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850def CMDpresubmit(parser, args):
851 """run presubmit tests on the current changelist"""
852 parser.add_option('--upload', action='store_true',
853 help='Run upload hook instead of the push/dcommit hook')
854 (options, args) = parser.parse_args(args)
855
856 # Make sure index is up-to-date before running diff-index.
857 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
858 if RunGit(['diff-index', 'HEAD']):
859 # TODO(maruel): Is this really necessary?
860 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
861 return 1
862
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000863 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 if args:
865 base_branch = args[0]
866 else:
867 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000868 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000870 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000871 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000872 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000873 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874
875
876@usage('[args to "git diff"]')
877def CMDupload(parser, args):
878 """upload the current changelist to codereview"""
879 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
880 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000881 parser.add_option('-f', action='store_true', dest='force',
882 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 parser.add_option('-m', dest='message', help='message for patch')
884 parser.add_option('-r', '--reviewers',
885 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000886 parser.add_option('--cc',
887 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888 parser.add_option('--send-mail', action='store_true',
889 help='send email to reviewer immediately')
890 parser.add_option("--emulate_svn_auto_props", action="store_true",
891 dest="emulate_svn_auto_props",
892 help="Emulate Subversion's auto properties feature.")
893 parser.add_option("--desc_from_logs", action="store_true",
894 dest="from_logs",
895 help="""Squashes git commit logs into change description and
896 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000897 parser.add_option('-c', '--use-commit-queue', action='store_true',
898 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899 (options, args) = parser.parse_args(args)
900
901 # Make sure index is up-to-date before running diff-index.
902 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
903 if RunGit(['diff-index', 'HEAD']):
904 print 'Cannot upload with a dirty tree. You must commit locally first.'
905 return 1
906
907 cl = Changelist()
908 if args:
909 base_branch = args[0]
910 else:
911 # Default to diffing against the "upstream" branch.
912 base_branch = cl.GetUpstreamBranch()
913 args = [base_branch + "..."]
914
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000915 if not options.bypass_hooks and not options.force:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000916 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000917 may_prompt=True,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000918 verbose=options.verbose,
919 author=None)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000920 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000921 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923
924 # --no-ext-diff is broken in some versions of Git, so try to work around
925 # this by overriding the environment (but there is still a problem if the
926 # git config key "diff.external" is used).
927 env = os.environ.copy()
928 if 'GIT_EXTERNAL_DIFF' in env:
929 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000930 subprocess2.call(
931 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933 upload_args = ['--assume_yes'] # Don't ask about untracked files.
934 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935 if options.emulate_svn_auto_props:
936 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937 if options.from_logs and not options.message:
938 print 'Must set message for subject line if using desc_from_logs'
939 return 1
940
941 change_desc = None
942
943 if cl.GetIssue():
944 if options.message:
945 upload_args.extend(['--message', options.message])
946 upload_args.extend(['--issue', cl.GetIssue()])
947 print ("This branch is associated with issue %s. "
948 "Adding patch to that issue." % cl.GetIssue())
949 else:
950 log_desc = CreateDescriptionFromLog(args)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000951 change_desc = ChangeDescription(options.message, log_desc,
952 options.reviewers)
953 if not options.from_logs:
954 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000955
956 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 print "Description is empty; aborting."
958 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000959
960 upload_args.extend(['--message', change_desc.subject])
961 upload_args.extend(['--description', change_desc.description])
962 if change_desc.reviewers:
963 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000964 if options.send_mail:
965 if not change_desc.reviewers:
966 DieWithError("Must specify reviewers to send email.")
967 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000968 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000969 if cc:
970 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971
972 # Include the upstream repo's URL in the change -- this is useful for
973 # projects that have their source spread across multiple repos.
974 remote_url = None
975 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000976 # URL is dependent on the current directory.
977 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978 if data:
979 keys = dict(line.split(': ', 1) for line in data.splitlines()
980 if ': ' in line)
981 remote_url = keys.get('URL', None)
982 else:
983 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
984 remote_url = (cl.GetRemoteUrl() + '@'
985 + cl.GetUpstreamBranch().split('/')[-1])
986 if remote_url:
987 upload_args.extend(['--base_url', remote_url])
988
989 try:
990 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000991 except KeyboardInterrupt:
992 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993 except:
994 # If we got an exception after the user typed a description for their
995 # change, back up the description before re-raising.
996 if change_desc:
997 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
998 print '\nGot exception while uploading -- saving description to %s\n' \
999 % backup_path
1000 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001001 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 backup_file.close()
1003 raise
1004
1005 if not cl.GetIssue():
1006 cl.SetIssue(issue)
1007 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001008
1009 if options.use_commit_queue:
1010 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 return 0
1012
1013
1014def SendUpstream(parser, args, cmd):
1015 """Common code for CmdPush and CmdDCommit
1016
1017 Squashed commit into a single.
1018 Updates changelog with metadata (e.g. pointer to review).
1019 Pushes/dcommits the code upstream.
1020 Updates review and closes.
1021 """
1022 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1023 help='bypass upload presubmit hook')
1024 parser.add_option('-m', dest='message',
1025 help="override review description")
1026 parser.add_option('-f', action='store_true', dest='force',
1027 help="force yes to questions (don't prompt)")
1028 parser.add_option('-c', dest='contributor',
1029 help="external contributor for patch (appended to " +
1030 "description and used as author for git). Should be " +
1031 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032 (options, args) = parser.parse_args(args)
1033 cl = Changelist()
1034
1035 if not args or cmd == 'push':
1036 # Default to merging against our best guess of the upstream branch.
1037 args = [cl.GetUpstreamBranch()]
1038
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001039 if options.contributor:
1040 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1041 print "Please provide contibutor as 'First Last <email@example.com>'"
1042 return 1
1043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 base_branch = args[0]
1045
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001046 # Make sure index is up-to-date before running diff-index.
1047 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 if RunGit(['diff-index', 'HEAD']):
1049 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1050 return 1
1051
1052 # This rev-list syntax means "show all commits not in my branch that
1053 # are in base_branch".
1054 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1055 base_branch]).splitlines()
1056 if upstream_commits:
1057 print ('Base branch "%s" has %d commits '
1058 'not in this branch.' % (base_branch, len(upstream_commits)))
1059 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1060 return 1
1061
1062 if cmd == 'dcommit':
1063 # This is the revision `svn dcommit` will commit on top of.
1064 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1065 '--pretty=format:%H'])
1066 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1067 if extra_commits:
1068 print ('This branch has %d additional commits not upstreamed yet.'
1069 % len(extra_commits.splitlines()))
1070 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1071 'before attempting to %s.' % (base_branch, cmd))
1072 return 1
1073
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001074 if not options.bypass_hooks and not options.force:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001075 author = None
1076 if options.contributor:
1077 author = re.search(r'\<(.*)\>', options.contributor).group(1)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001078 cl.RunHook(committing=True, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001079 may_prompt=True, verbose=options.verbose,
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001080 author=author)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082 if cmd == 'dcommit':
1083 # Check the tree status if the tree status URL is set.
1084 status = GetTreeStatus()
1085 if 'closed' == status:
1086 print ('The tree is closed. Please wait for it to reopen. Use '
1087 '"git cl dcommit -f" to commit on a closed tree.')
1088 return 1
1089 elif 'unknown' == status:
1090 print ('Unable to determine tree status. Please verify manually and '
1091 'use "git cl dcommit -f" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001092 else:
1093 breakpad.SendStack(
1094 'GitClHooksBypassedCommit',
1095 'Issue %s/%s bypassed hook when committing' %
1096 (cl.GetRietveldServer(), cl.GetIssue()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097
1098 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001099 if not description and cl.GetIssue():
1100 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001102 if not description:
1103 print 'No description set.'
1104 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1105 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001107 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109
1110 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 description += "\nPatch from %s." % options.contributor
1112 print 'Description:', repr(description)
1113
1114 branches = [base_branch, cl.GetBranchRef()]
1115 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001116 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001117 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118
1119 # We want to squash all this branch's commits into one commit with the
1120 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001121 # We do this by doing a "reset --soft" to the base branch (which keeps
1122 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 MERGE_BRANCH = 'git-cl-commit'
1124 # Delete the merge branch if it already exists.
1125 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1126 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1127 RunGit(['branch', '-D', MERGE_BRANCH])
1128
1129 # We might be in a directory that's present in this branch but not in the
1130 # trunk. Move up to the top of the tree so that git commands that expect a
1131 # valid CWD won't fail after we check out the merge branch.
1132 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1133 if rel_base_path:
1134 os.chdir(rel_base_path)
1135
1136 # Stuff our change into the merge branch.
1137 # We wrap in a try...finally block so if anything goes wrong,
1138 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001139 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001141 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1142 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 if options.contributor:
1144 RunGit(['commit', '--author', options.contributor, '-m', description])
1145 else:
1146 RunGit(['commit', '-m', description])
1147 if cmd == 'push':
1148 # push the merge branch.
1149 remote, branch = cl.FetchUpstreamTuple()
1150 retcode, output = RunGitWithCode(
1151 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1152 logging.debug(output)
1153 else:
1154 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001155 retcode, output = RunGitWithCode(['svn', 'dcommit',
1156 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 finally:
1158 # And then swap back to the original branch and clean up.
1159 RunGit(['checkout', '-q', cl.GetBranch()])
1160 RunGit(['branch', '-D', MERGE_BRANCH])
1161
1162 if cl.GetIssue():
1163 if cmd == 'dcommit' and 'Committed r' in output:
1164 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1165 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001166 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1167 for l in output.splitlines(False))
1168 match = filter(None, match)
1169 if len(match) != 1:
1170 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1171 output)
1172 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 else:
1174 return 1
1175 viewvc_url = settings.GetViewVCUrl()
1176 if viewvc_url and revision:
1177 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1178 print ('Closing issue '
1179 '(you may be prompted for your codereview password)...')
1180 cl.CloseIssue()
1181 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001182
1183 if retcode == 0:
1184 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1185 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001186 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001187
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 return 0
1189
1190
1191@usage('[upstream branch to apply against]')
1192def CMDdcommit(parser, args):
1193 """commit the current changelist via git-svn"""
1194 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001195 message = """This doesn't appear to be an SVN repository.
1196If your project has a git mirror with an upstream SVN master, you probably need
1197to run 'git svn init', see your project's git mirror documentation.
1198If your project has a true writeable upstream repository, you probably want
1199to run 'git cl push' instead.
1200Choose wisely, if you get this wrong, your commit might appear to succeed but
1201will instead be silently ignored."""
1202 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001203 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 return SendUpstream(parser, args, 'dcommit')
1205
1206
1207@usage('[upstream branch to apply against]')
1208def CMDpush(parser, args):
1209 """commit the current changelist via git"""
1210 if settings.GetIsGitSvn():
1211 print('This appears to be an SVN repository.')
1212 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001213 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 return SendUpstream(parser, args, 'push')
1215
1216
1217@usage('<patch url or issue id>')
1218def CMDpatch(parser, args):
1219 """patch in a code review"""
1220 parser.add_option('-b', dest='newbranch',
1221 help='create a new branch off trunk for the patch')
1222 parser.add_option('-f', action='store_true', dest='force',
1223 help='with -b, clobber any existing branch')
1224 parser.add_option('--reject', action='store_true', dest='reject',
1225 help='allow failed patches and spew .rej files')
1226 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1227 help="don't commit after patch applies")
1228 (options, args) = parser.parse_args(args)
1229 if len(args) != 1:
1230 parser.print_help()
1231 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001232 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001234 # TODO(maruel): Use apply_issue.py
1235
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001236 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001238 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001239 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 else:
1241 # Assume it's a URL to the patch. Default to http.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001242 issue_url = FixUrl(issue_arg)
1243 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001244 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 DieWithError('Must pass an issue ID or full URL for '
1246 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001247 issue = match.group(1)
1248 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249
1250 if options.newbranch:
1251 if options.force:
1252 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001253 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 RunGit(['checkout', '-b', options.newbranch,
1255 Changelist().GetUpstreamBranch()])
1256
1257 # Switch up to the top-level directory, if necessary, in preparation for
1258 # applying the patch.
1259 top = RunGit(['rev-parse', '--show-cdup']).strip()
1260 if top:
1261 os.chdir(top)
1262
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 # Git patches have a/ at the beginning of source paths. We strip that out
1264 # with a sed script rather than the -p flag to patch so we can feed either
1265 # Git or svn-style patches into the same apply command.
1266 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001267 try:
1268 patch_data = subprocess2.check_output(
1269 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1270 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 DieWithError('Git patch mungling failed.')
1272 logging.info(patch_data)
1273 # We use "git apply" to apply the patch instead of "patch" so that we can
1274 # pick up file adds.
1275 # The --index flag means: also insert into the index (so we catch adds).
1276 cmd = ['git', 'apply', '--index', '-p0']
1277 if options.reject:
1278 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001279 try:
1280 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1281 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 DieWithError('Failed to apply the patch')
1283
1284 # If we had an issue, commit the current state and register the issue.
1285 if not options.nocommit:
1286 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1287 cl = Changelist()
1288 cl.SetIssue(issue)
1289 print "Committed patch."
1290 else:
1291 print "Patch applied to index."
1292 return 0
1293
1294
1295def CMDrebase(parser, args):
1296 """rebase current branch on top of svn repo"""
1297 # Provide a wrapper for git svn rebase to help avoid accidental
1298 # git svn dcommit.
1299 # It's the only command that doesn't use parser at all since we just defer
1300 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001301 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302
1303
1304def GetTreeStatus():
1305 """Fetches the tree status and returns either 'open', 'closed',
1306 'unknown' or 'unset'."""
1307 url = settings.GetTreeStatusUrl(error_ok=True)
1308 if url:
1309 status = urllib2.urlopen(url).read().lower()
1310 if status.find('closed') != -1 or status == '0':
1311 return 'closed'
1312 elif status.find('open') != -1 or status == '1':
1313 return 'open'
1314 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315 return 'unset'
1316
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001317
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318def GetTreeStatusReason():
1319 """Fetches the tree status from a json url and returns the message
1320 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001321 url = settings.GetTreeStatusUrl()
1322 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323 connection = urllib2.urlopen(json_url)
1324 status = json.loads(connection.read())
1325 connection.close()
1326 return status['message']
1327
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001328
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329def CMDtree(parser, args):
1330 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001331 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 status = GetTreeStatus()
1333 if 'unset' == status:
1334 print 'You must configure your tree status URL by running "git cl config".'
1335 return 2
1336
1337 print "The tree is %s" % status
1338 print
1339 print GetTreeStatusReason()
1340 if status != 'open':
1341 return 1
1342 return 0
1343
1344
1345def CMDupstream(parser, args):
1346 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001347 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001348 if args:
1349 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350 cl = Changelist()
1351 print cl.GetUpstreamBranch()
1352 return 0
1353
1354
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001355def CMDset_commit(parser, args):
1356 """set the commit bit"""
1357 _, args = parser.parse_args(args)
1358 if args:
1359 parser.error('Unrecognized args: %s' % ' '.join(args))
1360 cl = Changelist()
1361 cl.SetFlag('commit', '1')
1362 return 0
1363
1364
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365def Command(name):
1366 return getattr(sys.modules[__name__], 'CMD' + name, None)
1367
1368
1369def CMDhelp(parser, args):
1370 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001371 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 if len(args) == 1:
1373 return main(args + ['--help'])
1374 parser.print_help()
1375 return 0
1376
1377
1378def GenUsage(parser, command):
1379 """Modify an OptParse object with the function's documentation."""
1380 obj = Command(command)
1381 more = getattr(obj, 'usage_more', '')
1382 if command == 'help':
1383 command = '<command>'
1384 else:
1385 # OptParser.description prefer nicely non-formatted strings.
1386 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1387 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1388
1389
1390def main(argv):
1391 """Doesn't parse the arguments here, just find the right subcommand to
1392 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001393 # Reload settings.
1394 global settings
1395 settings = Settings()
1396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 # Do it late so all commands are listed.
1398 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1399 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1400 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1401
1402 # Create the option parse and add --verbose support.
1403 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001404 parser.add_option(
1405 '-v', '--verbose', action='count', default=0,
1406 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 old_parser_args = parser.parse_args
1408 def Parse(args):
1409 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001410 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001412 elif options.verbose:
1413 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 else:
1415 logging.basicConfig(level=logging.WARNING)
1416 return options, args
1417 parser.parse_args = Parse
1418
1419 if argv:
1420 command = Command(argv[0])
1421 if command:
1422 # "fix" the usage and the description now that we know the subcommand.
1423 GenUsage(parser, argv[0])
1424 try:
1425 return command(parser, argv[1:])
1426 except urllib2.HTTPError, e:
1427 if e.code != 500:
1428 raise
1429 DieWithError(
1430 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1431 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1432
1433 # Not a known command. Default to help.
1434 GenUsage(parser, 'help')
1435 return CMDhelp(parser, argv)
1436
1437
1438if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001439 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 sys.exit(main(sys.argv[1:]))