blob: eb93403f7af2249590a9eda02dfd23d1c488804b [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."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 keyvals = {}
721 for line in fileobj.read().splitlines():
722 if not line or line.startswith("#"):
723 continue
724 k, v = line.split(": ", 1)
725 keyvals[k] = v
726
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 def SetProperty(name, setting, unset_error_ok=False):
728 fullname = 'rietveld.' + name
729 if setting in keyvals:
730 RunGit(['config', fullname, keyvals[setting]])
731 else:
732 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
733
734 SetProperty('server', 'CODE_REVIEW_SERVER')
735 # Only server setting is required. Other settings can be absent.
736 # In that case, we ignore errors raised during option deletion attempt.
737 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
738 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
739 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
740
741 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
742 #should be of the form
743 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
744 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
745 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
746 keyvals['ORIGIN_URL_CONFIG']])
747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748
749@usage('[repo root containing codereview.settings]')
750def CMDconfig(parser, args):
751 """edit configuration for this tree"""
752
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000753 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 if len(args) == 0:
755 GetCodereviewSettingsInteractively()
756 return 0
757
758 url = args[0]
759 if not url.endswith('codereview.settings'):
760 url = os.path.join(url, 'codereview.settings')
761
762 # Load code review settings and download hooks (if available).
763 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
764 return 0
765
766
767def CMDstatus(parser, args):
768 """show status of changelists"""
769 parser.add_option('--field',
770 help='print only specific field (desc|id|patch|url)')
771 (options, args) = parser.parse_args(args)
772
773 # TODO: maybe make show_branches a flag if necessary.
774 show_branches = not options.field
775
776 if show_branches:
777 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
778 if branches:
779 print 'Branches associated with reviews:'
780 for branch in sorted(branches.splitlines()):
781 cl = Changelist(branchref=branch)
782 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
783
784 cl = Changelist()
785 if options.field:
786 if options.field.startswith('desc'):
787 print cl.GetDescription()
788 elif options.field == 'id':
789 issueid = cl.GetIssue()
790 if issueid:
791 print issueid
792 elif options.field == 'patch':
793 patchset = cl.GetPatchset()
794 if patchset:
795 print patchset
796 elif options.field == 'url':
797 url = cl.GetIssueURL()
798 if url:
799 print url
800 else:
801 print
802 print 'Current branch:',
803 if not cl.GetIssue():
804 print 'no issue assigned.'
805 return 0
806 print cl.GetBranch()
807 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
808 print 'Issue description:'
809 print cl.GetDescription(pretty=True)
810 return 0
811
812
813@usage('[issue_number]')
814def CMDissue(parser, args):
815 """Set or display the current code review issue number.
816
817 Pass issue number 0 to clear the current issue.
818"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000819 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820
821 cl = Changelist()
822 if len(args) > 0:
823 try:
824 issue = int(args[0])
825 except ValueError:
826 DieWithError('Pass a number to set the issue or none to list it.\n'
827 'Maybe you want to run git cl status?')
828 cl.SetIssue(issue)
829 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
830 return 0
831
832
833def CreateDescriptionFromLog(args):
834 """Pulls out the commit log to use as a base for the CL description."""
835 log_args = []
836 if len(args) == 1 and not args[0].endswith('.'):
837 log_args = [args[0] + '..']
838 elif len(args) == 1 and args[0].endswith('...'):
839 log_args = [args[0][:-1]]
840 elif len(args) == 2:
841 log_args = [args[0] + '..' + args[1]]
842 else:
843 log_args = args[:] # Hope for the best!
844 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
845
846
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000847def ConvertToInteger(inputval):
848 """Convert a string to integer, but returns either an int or None."""
849 try:
850 return int(inputval)
851 except (TypeError, ValueError):
852 return None
853
854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855def CMDpresubmit(parser, args):
856 """run presubmit tests on the current changelist"""
857 parser.add_option('--upload', action='store_true',
858 help='Run upload hook instead of the push/dcommit hook')
859 (options, args) = parser.parse_args(args)
860
861 # Make sure index is up-to-date before running diff-index.
862 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
863 if RunGit(['diff-index', 'HEAD']):
864 # TODO(maruel): Is this really necessary?
865 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
866 return 1
867
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000868 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 if args:
870 base_branch = args[0]
871 else:
872 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000873 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000875 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000876 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000877 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000878 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879
880
881@usage('[args to "git diff"]')
882def CMDupload(parser, args):
883 """upload the current changelist to codereview"""
884 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
885 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000886 parser.add_option('-f', action='store_true', dest='force',
887 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888 parser.add_option('-m', dest='message', help='message for patch')
889 parser.add_option('-r', '--reviewers',
890 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000891 parser.add_option('--cc',
892 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893 parser.add_option('--send-mail', action='store_true',
894 help='send email to reviewer immediately')
895 parser.add_option("--emulate_svn_auto_props", action="store_true",
896 dest="emulate_svn_auto_props",
897 help="Emulate Subversion's auto properties feature.")
898 parser.add_option("--desc_from_logs", action="store_true",
899 dest="from_logs",
900 help="""Squashes git commit logs into change description and
901 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000902 parser.add_option('-c', '--use-commit-queue', action='store_true',
903 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904 (options, args) = parser.parse_args(args)
905
906 # Make sure index is up-to-date before running diff-index.
907 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
908 if RunGit(['diff-index', 'HEAD']):
909 print 'Cannot upload with a dirty tree. You must commit locally first.'
910 return 1
911
912 cl = Changelist()
913 if args:
914 base_branch = args[0]
915 else:
916 # Default to diffing against the "upstream" branch.
917 base_branch = cl.GetUpstreamBranch()
918 args = [base_branch + "..."]
919
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000920 if not options.bypass_hooks and not options.force:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000921 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000922 may_prompt=True,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000923 verbose=options.verbose,
924 author=None)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000925 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000926 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928
929 # --no-ext-diff is broken in some versions of Git, so try to work around
930 # this by overriding the environment (but there is still a problem if the
931 # git config key "diff.external" is used).
932 env = os.environ.copy()
933 if 'GIT_EXTERNAL_DIFF' in env:
934 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000935 subprocess2.call(
936 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937
938 upload_args = ['--assume_yes'] # Don't ask about untracked files.
939 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 if options.emulate_svn_auto_props:
941 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942 if options.from_logs and not options.message:
943 print 'Must set message for subject line if using desc_from_logs'
944 return 1
945
946 change_desc = None
947
948 if cl.GetIssue():
949 if options.message:
950 upload_args.extend(['--message', options.message])
951 upload_args.extend(['--issue', cl.GetIssue()])
952 print ("This branch is associated with issue %s. "
953 "Adding patch to that issue." % cl.GetIssue())
954 else:
955 log_desc = CreateDescriptionFromLog(args)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000956 change_desc = ChangeDescription(options.message, log_desc,
957 options.reviewers)
958 if not options.from_logs:
959 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000960
961 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962 print "Description is empty; aborting."
963 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000964
965 upload_args.extend(['--message', change_desc.subject])
966 upload_args.extend(['--description', change_desc.description])
967 if change_desc.reviewers:
968 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000969 if options.send_mail:
970 if not change_desc.reviewers:
971 DieWithError("Must specify reviewers to send email.")
972 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000973 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000974 if cc:
975 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976
977 # Include the upstream repo's URL in the change -- this is useful for
978 # projects that have their source spread across multiple repos.
979 remote_url = None
980 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000981 # URL is dependent on the current directory.
982 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983 if data:
984 keys = dict(line.split(': ', 1) for line in data.splitlines()
985 if ': ' in line)
986 remote_url = keys.get('URL', None)
987 else:
988 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
989 remote_url = (cl.GetRemoteUrl() + '@'
990 + cl.GetUpstreamBranch().split('/')[-1])
991 if remote_url:
992 upload_args.extend(['--base_url', remote_url])
993
994 try:
995 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000996 except KeyboardInterrupt:
997 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 except:
999 # If we got an exception after the user typed a description for their
1000 # change, back up the description before re-raising.
1001 if change_desc:
1002 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1003 print '\nGot exception while uploading -- saving description to %s\n' \
1004 % backup_path
1005 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001006 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 backup_file.close()
1008 raise
1009
1010 if not cl.GetIssue():
1011 cl.SetIssue(issue)
1012 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001013
1014 if options.use_commit_queue:
1015 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 return 0
1017
1018
1019def SendUpstream(parser, args, cmd):
1020 """Common code for CmdPush and CmdDCommit
1021
1022 Squashed commit into a single.
1023 Updates changelog with metadata (e.g. pointer to review).
1024 Pushes/dcommits the code upstream.
1025 Updates review and closes.
1026 """
1027 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1028 help='bypass upload presubmit hook')
1029 parser.add_option('-m', dest='message',
1030 help="override review description")
1031 parser.add_option('-f', action='store_true', dest='force',
1032 help="force yes to questions (don't prompt)")
1033 parser.add_option('-c', dest='contributor',
1034 help="external contributor for patch (appended to " +
1035 "description and used as author for git). Should be " +
1036 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 (options, args) = parser.parse_args(args)
1038 cl = Changelist()
1039
1040 if not args or cmd == 'push':
1041 # Default to merging against our best guess of the upstream branch.
1042 args = [cl.GetUpstreamBranch()]
1043
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001044 if options.contributor:
1045 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1046 print "Please provide contibutor as 'First Last <email@example.com>'"
1047 return 1
1048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 base_branch = args[0]
1050
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001051 # Make sure index is up-to-date before running diff-index.
1052 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 if RunGit(['diff-index', 'HEAD']):
1054 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1055 return 1
1056
1057 # This rev-list syntax means "show all commits not in my branch that
1058 # are in base_branch".
1059 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1060 base_branch]).splitlines()
1061 if upstream_commits:
1062 print ('Base branch "%s" has %d commits '
1063 'not in this branch.' % (base_branch, len(upstream_commits)))
1064 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1065 return 1
1066
1067 if cmd == 'dcommit':
1068 # This is the revision `svn dcommit` will commit on top of.
1069 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1070 '--pretty=format:%H'])
1071 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1072 if extra_commits:
1073 print ('This branch has %d additional commits not upstreamed yet.'
1074 % len(extra_commits.splitlines()))
1075 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1076 'before attempting to %s.' % (base_branch, cmd))
1077 return 1
1078
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001079 if not options.bypass_hooks and not options.force:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001080 author = None
1081 if options.contributor:
1082 author = re.search(r'\<(.*)\>', options.contributor).group(1)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001083 cl.RunHook(committing=True, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001084 may_prompt=True, verbose=options.verbose,
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001085 author=author)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086
1087 if cmd == 'dcommit':
1088 # Check the tree status if the tree status URL is set.
1089 status = GetTreeStatus()
1090 if 'closed' == status:
1091 print ('The tree is closed. Please wait for it to reopen. Use '
1092 '"git cl dcommit -f" to commit on a closed tree.')
1093 return 1
1094 elif 'unknown' == status:
1095 print ('Unable to determine tree status. Please verify manually and '
1096 'use "git cl dcommit -f" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001097 else:
1098 breakpad.SendStack(
1099 'GitClHooksBypassedCommit',
1100 'Issue %s/%s bypassed hook when committing' %
1101 (cl.GetRietveldServer(), cl.GetIssue()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102
1103 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001104 if not description and cl.GetIssue():
1105 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001107 if not description:
1108 print 'No description set.'
1109 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1110 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001112 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114
1115 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 description += "\nPatch from %s." % options.contributor
1117 print 'Description:', repr(description)
1118
1119 branches = [base_branch, cl.GetBranchRef()]
1120 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001121 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001122 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123
1124 # We want to squash all this branch's commits into one commit with the
1125 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001126 # We do this by doing a "reset --soft" to the base branch (which keeps
1127 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 MERGE_BRANCH = 'git-cl-commit'
1129 # Delete the merge branch if it already exists.
1130 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1131 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1132 RunGit(['branch', '-D', MERGE_BRANCH])
1133
1134 # We might be in a directory that's present in this branch but not in the
1135 # trunk. Move up to the top of the tree so that git commands that expect a
1136 # valid CWD won't fail after we check out the merge branch.
1137 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1138 if rel_base_path:
1139 os.chdir(rel_base_path)
1140
1141 # Stuff our change into the merge branch.
1142 # We wrap in a try...finally block so if anything goes wrong,
1143 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001144 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001146 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1147 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 if options.contributor:
1149 RunGit(['commit', '--author', options.contributor, '-m', description])
1150 else:
1151 RunGit(['commit', '-m', description])
1152 if cmd == 'push':
1153 # push the merge branch.
1154 remote, branch = cl.FetchUpstreamTuple()
1155 retcode, output = RunGitWithCode(
1156 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1157 logging.debug(output)
1158 else:
1159 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001160 retcode, output = RunGitWithCode(['svn', 'dcommit',
1161 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 finally:
1163 # And then swap back to the original branch and clean up.
1164 RunGit(['checkout', '-q', cl.GetBranch()])
1165 RunGit(['branch', '-D', MERGE_BRANCH])
1166
1167 if cl.GetIssue():
1168 if cmd == 'dcommit' and 'Committed r' in output:
1169 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1170 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001171 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1172 for l in output.splitlines(False))
1173 match = filter(None, match)
1174 if len(match) != 1:
1175 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1176 output)
1177 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 else:
1179 return 1
1180 viewvc_url = settings.GetViewVCUrl()
1181 if viewvc_url and revision:
1182 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1183 print ('Closing issue '
1184 '(you may be prompted for your codereview password)...')
1185 cl.CloseIssue()
1186 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001187
1188 if retcode == 0:
1189 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1190 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001191 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 return 0
1194
1195
1196@usage('[upstream branch to apply against]')
1197def CMDdcommit(parser, args):
1198 """commit the current changelist via git-svn"""
1199 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001200 message = """This doesn't appear to be an SVN repository.
1201If your project has a git mirror with an upstream SVN master, you probably need
1202to run 'git svn init', see your project's git mirror documentation.
1203If your project has a true writeable upstream repository, you probably want
1204to run 'git cl push' instead.
1205Choose wisely, if you get this wrong, your commit might appear to succeed but
1206will instead be silently ignored."""
1207 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001208 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 return SendUpstream(parser, args, 'dcommit')
1210
1211
1212@usage('[upstream branch to apply against]')
1213def CMDpush(parser, args):
1214 """commit the current changelist via git"""
1215 if settings.GetIsGitSvn():
1216 print('This appears to be an SVN repository.')
1217 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001218 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 return SendUpstream(parser, args, 'push')
1220
1221
1222@usage('<patch url or issue id>')
1223def CMDpatch(parser, args):
1224 """patch in a code review"""
1225 parser.add_option('-b', dest='newbranch',
1226 help='create a new branch off trunk for the patch')
1227 parser.add_option('-f', action='store_true', dest='force',
1228 help='with -b, clobber any existing branch')
1229 parser.add_option('--reject', action='store_true', dest='reject',
1230 help='allow failed patches and spew .rej files')
1231 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1232 help="don't commit after patch applies")
1233 (options, args) = parser.parse_args(args)
1234 if len(args) != 1:
1235 parser.print_help()
1236 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001237 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001239 # TODO(maruel): Use apply_issue.py
1240
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001241 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001243 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001244 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
1246 # Assume it's a URL to the patch. Default to http.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001247 issue_url = FixUrl(issue_arg)
1248 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001249 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 DieWithError('Must pass an issue ID or full URL for '
1251 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001252 issue = match.group(1)
1253 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254
1255 if options.newbranch:
1256 if options.force:
1257 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001258 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 RunGit(['checkout', '-b', options.newbranch,
1260 Changelist().GetUpstreamBranch()])
1261
1262 # Switch up to the top-level directory, if necessary, in preparation for
1263 # applying the patch.
1264 top = RunGit(['rev-parse', '--show-cdup']).strip()
1265 if top:
1266 os.chdir(top)
1267
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 # Git patches have a/ at the beginning of source paths. We strip that out
1269 # with a sed script rather than the -p flag to patch so we can feed either
1270 # Git or svn-style patches into the same apply command.
1271 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001272 try:
1273 patch_data = subprocess2.check_output(
1274 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1275 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 DieWithError('Git patch mungling failed.')
1277 logging.info(patch_data)
1278 # We use "git apply" to apply the patch instead of "patch" so that we can
1279 # pick up file adds.
1280 # The --index flag means: also insert into the index (so we catch adds).
1281 cmd = ['git', 'apply', '--index', '-p0']
1282 if options.reject:
1283 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001284 try:
1285 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1286 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 DieWithError('Failed to apply the patch')
1288
1289 # If we had an issue, commit the current state and register the issue.
1290 if not options.nocommit:
1291 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1292 cl = Changelist()
1293 cl.SetIssue(issue)
1294 print "Committed patch."
1295 else:
1296 print "Patch applied to index."
1297 return 0
1298
1299
1300def CMDrebase(parser, args):
1301 """rebase current branch on top of svn repo"""
1302 # Provide a wrapper for git svn rebase to help avoid accidental
1303 # git svn dcommit.
1304 # It's the only command that doesn't use parser at all since we just defer
1305 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001306 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307
1308
1309def GetTreeStatus():
1310 """Fetches the tree status and returns either 'open', 'closed',
1311 'unknown' or 'unset'."""
1312 url = settings.GetTreeStatusUrl(error_ok=True)
1313 if url:
1314 status = urllib2.urlopen(url).read().lower()
1315 if status.find('closed') != -1 or status == '0':
1316 return 'closed'
1317 elif status.find('open') != -1 or status == '1':
1318 return 'open'
1319 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 return 'unset'
1321
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323def GetTreeStatusReason():
1324 """Fetches the tree status from a json url and returns the message
1325 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001326 url = settings.GetTreeStatusUrl()
1327 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 connection = urllib2.urlopen(json_url)
1329 status = json.loads(connection.read())
1330 connection.close()
1331 return status['message']
1332
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334def CMDtree(parser, args):
1335 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001336 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 status = GetTreeStatus()
1338 if 'unset' == status:
1339 print 'You must configure your tree status URL by running "git cl config".'
1340 return 2
1341
1342 print "The tree is %s" % status
1343 print
1344 print GetTreeStatusReason()
1345 if status != 'open':
1346 return 1
1347 return 0
1348
1349
1350def CMDupstream(parser, args):
1351 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001352 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001353 if args:
1354 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 cl = Changelist()
1356 print cl.GetUpstreamBranch()
1357 return 0
1358
1359
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001360def CMDset_commit(parser, args):
1361 """set the commit bit"""
1362 _, args = parser.parse_args(args)
1363 if args:
1364 parser.error('Unrecognized args: %s' % ' '.join(args))
1365 cl = Changelist()
1366 cl.SetFlag('commit', '1')
1367 return 0
1368
1369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370def Command(name):
1371 return getattr(sys.modules[__name__], 'CMD' + name, None)
1372
1373
1374def CMDhelp(parser, args):
1375 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001376 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 if len(args) == 1:
1378 return main(args + ['--help'])
1379 parser.print_help()
1380 return 0
1381
1382
1383def GenUsage(parser, command):
1384 """Modify an OptParse object with the function's documentation."""
1385 obj = Command(command)
1386 more = getattr(obj, 'usage_more', '')
1387 if command == 'help':
1388 command = '<command>'
1389 else:
1390 # OptParser.description prefer nicely non-formatted strings.
1391 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1392 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1393
1394
1395def main(argv):
1396 """Doesn't parse the arguments here, just find the right subcommand to
1397 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001398 # Reload settings.
1399 global settings
1400 settings = Settings()
1401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 # Do it late so all commands are listed.
1403 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1404 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1405 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1406
1407 # Create the option parse and add --verbose support.
1408 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001409 parser.add_option(
1410 '-v', '--verbose', action='count', default=0,
1411 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 old_parser_args = parser.parse_args
1413 def Parse(args):
1414 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001415 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001417 elif options.verbose:
1418 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 else:
1420 logging.basicConfig(level=logging.WARNING)
1421 return options, args
1422 parser.parse_args = Parse
1423
1424 if argv:
1425 command = Command(argv[0])
1426 if command:
1427 # "fix" the usage and the description now that we know the subcommand.
1428 GenUsage(parser, argv[0])
1429 try:
1430 return command(parser, argv[1:])
1431 except urllib2.HTTPError, e:
1432 if e.code != 500:
1433 raise
1434 DieWithError(
1435 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1436 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1437
1438 # Not a known command. Default to help.
1439 GenUsage(parser, 'help')
1440 return CMDhelp(parser, argv)
1441
1442
1443if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001444 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445 sys.exit(main(sys.argv[1:]))