blob: a8669a37360b31e4db51071c4928bf6ddd820bf8 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000010import logging
11import optparse
12import os
13import re
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000016import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import urllib2
18
19try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000020 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021except ImportError:
22 pass
23
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000024try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000025 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000028 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 except ImportError:
30 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000031 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000032 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
34
35from third_party import upload
36import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000040import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
44
45
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000046DEFAULT_SERVER = 'http://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
49
maruel@chromium.org90541732011-04-01 17:54:18 +000050
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():
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000449 self.description = self.RpcServer().get_description(
450 int(self.GetIssue())).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000451 self.has_description = True
452 if pretty:
453 wrapper = textwrap.TextWrapper()
454 wrapper.initial_indent = wrapper.subsequent_indent = ' '
455 return wrapper.fill(self.description)
456 return self.description
457
458 def GetPatchset(self):
459 if not self.has_patchset:
460 patchset = RunGit(['config', self._PatchsetSetting()],
461 error_ok=True).strip()
462 if patchset:
463 self.patchset = patchset
464 else:
465 self.patchset = None
466 self.has_patchset = True
467 return self.patchset
468
469 def SetPatchset(self, patchset):
470 """Set this branch's patchset. If patchset=0, clears the patchset."""
471 if patchset:
472 RunGit(['config', self._PatchsetSetting(), str(patchset)])
473 else:
474 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000475 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 self.has_patchset = False
477
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000478 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000479 patchset = self.RpcServer().get_issue_properties(
480 int(issue), False)['patchsets'][-1]
481 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000482 '/download/issue%s_%s.diff' % (issue, patchset))
483
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 def SetIssue(self, issue):
485 """Set this branch's issue. If issue=0, clears the issue."""
486 if issue:
487 RunGit(['config', self._IssueSetting(), str(issue)])
488 if self.rietveld_server:
489 RunGit(['config', self._RietveldServer(), self.rietveld_server])
490 else:
491 RunGit(['config', '--unset', self._IssueSetting()])
492 self.SetPatchset(0)
493 self.has_issue = False
494
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000495 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000496 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
497 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000498
499 # We use the sha1 of HEAD as a name of this change.
500 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000501 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000502 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000503 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000504 except subprocess2.CalledProcessError:
505 DieWithError(
506 ('\nFailed to diff against upstream branch %s!\n\n'
507 'This branch probably doesn\'t exist anymore. To reset the\n'
508 'tracking branch, please run\n'
509 ' git branch --set-upstream %s trunk\n'
510 'replacing trunk with origin/master or the relevant branch') %
511 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000512
513 issue = ConvertToInteger(self.GetIssue())
514 patchset = ConvertToInteger(self.GetPatchset())
515 if issue:
516 description = self.GetDescription()
517 else:
518 # If the change was never uploaded, use the log messages of all commits
519 # up to the branch point, as git cl upload will prefill the description
520 # with these log messages.
521 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
522 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000523
524 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000525 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000526 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000527 name,
528 description,
529 absroot,
530 files,
531 issue,
532 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000533 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000534
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000535 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
536 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
537 change = self.GetChange(upstream_branch, author)
538
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000539 # Apply watchlists on upload.
540 if not committing:
541 watchlist = watchlists.Watchlists(change.RepositoryRoot())
542 files = [f.LocalPath() for f in change.AffectedFiles()]
543 self.SetWatchers(watchlist.GetWatchersForPaths(files))
544
545 try:
546 output = presubmit_support.DoPresubmitChecks(change, committing,
547 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000548 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000549 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000550 except presubmit_support.PresubmitFailure, e:
551 DieWithError(
552 ('%s\nMaybe your depot_tools is out of date?\n'
553 'If all fails, contact maruel@') % e)
554
555 # TODO(dpranke): We should propagate the error out instead of calling
556 # exit().
557 if not output.should_continue():
558 sys.exit(1)
559
560 return output
561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000563 """Updates the description and closes the issue."""
564 issue = int(self.GetIssue())
565 self.RpcServer().update_description(issue, self.description)
566 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000567
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000568 def SetFlag(self, flag, value):
569 """Patchset must match."""
570 if not self.GetPatchset():
571 DieWithError('The patchset needs to match. Send another patchset.')
572 try:
573 return self.RpcServer().set_flag(
574 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
575 except urllib2.HTTPError, e:
576 if e.code == 404:
577 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
578 if e.code == 403:
579 DieWithError(
580 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
581 'match?') % (self.GetIssue(), self.GetPatchset()))
582 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000584 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 """Returns an upload.RpcServer() to access this review's rietveld instance.
586 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000587 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000588 self.GetIssue()
589 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000590 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591
592 def _IssueSetting(self):
593 """Return the git setting that stores this change's issue."""
594 return 'branch.%s.rietveldissue' % self.GetBranch()
595
596 def _PatchsetSetting(self):
597 """Return the git setting that stores this change's most recent patchset."""
598 return 'branch.%s.rietveldpatchset' % self.GetBranch()
599
600 def _RietveldServer(self):
601 """Returns the git setting that stores this change's rietveld server."""
602 return 'branch.%s.rietveldserver' % self.GetBranch()
603
604
605def GetCodereviewSettingsInteractively():
606 """Prompt the user for settings."""
607 server = settings.GetDefaultServerUrl(error_ok=True)
608 prompt = 'Rietveld server (host[:port])'
609 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000610 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 if not server and not newserver:
612 newserver = DEFAULT_SERVER
613 if newserver and newserver != server:
614 RunGit(['config', 'rietveld.server', newserver])
615
616 def SetProperty(initial, caption, name):
617 prompt = caption
618 if initial:
619 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000620 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 if new_val == 'x':
622 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
623 elif new_val and new_val != initial:
624 RunGit(['config', 'rietveld.' + name, new_val])
625
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000626 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
628 'tree-status-url')
629 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
630
631 # TODO: configure a default branch to diff against, rather than this
632 # svn-based hackery.
633
634
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000635class ChangeDescription(object):
636 """Contains a parsed form of the change description."""
637 def __init__(self, subject, log_desc, reviewers):
638 self.subject = subject
639 self.log_desc = log_desc
640 self.reviewers = reviewers
641 self.description = self.log_desc
642
643 def Update(self):
644 initial_text = """# Enter a description of the change.
645# This will displayed on the codereview site.
646# The first line will also be used as the subject of the review.
647"""
648 initial_text += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000649 if ('\nR=' not in self.description and
650 '\nTBR=' not in self.description and
651 self.reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000652 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000653 if '\nBUG=' not in self.description:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000654 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000655 if '\nTEST=' not in self.description:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000656 initial_text += '\nTEST='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000657 initial_text = initial_text.rstrip('\n') + '\n'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000658 content = gclient_utils.RunEditor(initial_text, True)
659 if not content:
660 DieWithError('Running editor failed')
661 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
662 if not content:
663 DieWithError('No CL description, aborting')
664 self._ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000665
666 def _ParseDescription(self, description):
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000667 """Updates the list of reviewers and subject from the description."""
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000668 if not description:
669 self.description = description
670 return
671
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000672 self.description = description.strip('\n') + '\n'
673 self.subject = description.split('\n', 1)[0]
674 # Retrieves all reviewer lines
675 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
676 self.reviewers = ','.join(
677 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000678
679 def IsEmpty(self):
680 return not self.description
681
682
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683def FindCodereviewSettingsFile(filename='codereview.settings'):
684 """Finds the given file starting in the cwd and going up.
685
686 Only looks up to the top of the repository unless an
687 'inherit-review-settings-ok' file exists in the root of the repository.
688 """
689 inherit_ok_file = 'inherit-review-settings-ok'
690 cwd = os.getcwd()
691 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
692 if os.path.isfile(os.path.join(root, inherit_ok_file)):
693 root = '/'
694 while True:
695 if filename in os.listdir(cwd):
696 if os.path.isfile(os.path.join(cwd, filename)):
697 return open(os.path.join(cwd, filename))
698 if cwd == root:
699 break
700 cwd = os.path.dirname(cwd)
701
702
703def LoadCodereviewSettingsFromFile(fileobj):
704 """Parse a codereview.settings file and updates hooks."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000705 keyvals = {}
706 for line in fileobj.read().splitlines():
707 if not line or line.startswith("#"):
708 continue
709 k, v = line.split(": ", 1)
710 keyvals[k] = v
711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 def SetProperty(name, setting, unset_error_ok=False):
713 fullname = 'rietveld.' + name
714 if setting in keyvals:
715 RunGit(['config', fullname, keyvals[setting]])
716 else:
717 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
718
719 SetProperty('server', 'CODE_REVIEW_SERVER')
720 # Only server setting is required. Other settings can be absent.
721 # In that case, we ignore errors raised during option deletion attempt.
722 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
723 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
724 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
725
726 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
727 #should be of the form
728 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
729 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
730 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
731 keyvals['ORIGIN_URL_CONFIG']])
732
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
734@usage('[repo root containing codereview.settings]')
735def CMDconfig(parser, args):
736 """edit configuration for this tree"""
737
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000738 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 if len(args) == 0:
740 GetCodereviewSettingsInteractively()
741 return 0
742
743 url = args[0]
744 if not url.endswith('codereview.settings'):
745 url = os.path.join(url, 'codereview.settings')
746
747 # Load code review settings and download hooks (if available).
748 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
749 return 0
750
751
752def CMDstatus(parser, args):
753 """show status of changelists"""
754 parser.add_option('--field',
755 help='print only specific field (desc|id|patch|url)')
756 (options, args) = parser.parse_args(args)
757
758 # TODO: maybe make show_branches a flag if necessary.
759 show_branches = not options.field
760
761 if show_branches:
762 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
763 if branches:
764 print 'Branches associated with reviews:'
765 for branch in sorted(branches.splitlines()):
766 cl = Changelist(branchref=branch)
767 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
768
769 cl = Changelist()
770 if options.field:
771 if options.field.startswith('desc'):
772 print cl.GetDescription()
773 elif options.field == 'id':
774 issueid = cl.GetIssue()
775 if issueid:
776 print issueid
777 elif options.field == 'patch':
778 patchset = cl.GetPatchset()
779 if patchset:
780 print patchset
781 elif options.field == 'url':
782 url = cl.GetIssueURL()
783 if url:
784 print url
785 else:
786 print
787 print 'Current branch:',
788 if not cl.GetIssue():
789 print 'no issue assigned.'
790 return 0
791 print cl.GetBranch()
792 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
793 print 'Issue description:'
794 print cl.GetDescription(pretty=True)
795 return 0
796
797
798@usage('[issue_number]')
799def CMDissue(parser, args):
800 """Set or display the current code review issue number.
801
802 Pass issue number 0 to clear the current issue.
803"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000804 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805
806 cl = Changelist()
807 if len(args) > 0:
808 try:
809 issue = int(args[0])
810 except ValueError:
811 DieWithError('Pass a number to set the issue or none to list it.\n'
812 'Maybe you want to run git cl status?')
813 cl.SetIssue(issue)
814 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
815 return 0
816
817
818def CreateDescriptionFromLog(args):
819 """Pulls out the commit log to use as a base for the CL description."""
820 log_args = []
821 if len(args) == 1 and not args[0].endswith('.'):
822 log_args = [args[0] + '..']
823 elif len(args) == 1 and args[0].endswith('...'):
824 log_args = [args[0][:-1]]
825 elif len(args) == 2:
826 log_args = [args[0] + '..' + args[1]]
827 else:
828 log_args = args[:] # Hope for the best!
829 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
830
831
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000832def ConvertToInteger(inputval):
833 """Convert a string to integer, but returns either an int or None."""
834 try:
835 return int(inputval)
836 except (TypeError, ValueError):
837 return None
838
839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840def CMDpresubmit(parser, args):
841 """run presubmit tests on the current changelist"""
842 parser.add_option('--upload', action='store_true',
843 help='Run upload hook instead of the push/dcommit hook')
844 (options, args) = parser.parse_args(args)
845
846 # Make sure index is up-to-date before running diff-index.
847 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
848 if RunGit(['diff-index', 'HEAD']):
849 # TODO(maruel): Is this really necessary?
850 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
851 return 1
852
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000853 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 if args:
855 base_branch = args[0]
856 else:
857 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000858 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000860 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000861 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000862 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000863 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864
865
866@usage('[args to "git diff"]')
867def CMDupload(parser, args):
868 """upload the current changelist to codereview"""
869 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
870 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000871 parser.add_option('-f', action='store_true', dest='force',
872 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 parser.add_option('-m', dest='message', help='message for patch')
874 parser.add_option('-r', '--reviewers',
875 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000876 parser.add_option('--cc',
877 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 parser.add_option('--send-mail', action='store_true',
879 help='send email to reviewer immediately')
880 parser.add_option("--emulate_svn_auto_props", action="store_true",
881 dest="emulate_svn_auto_props",
882 help="Emulate Subversion's auto properties feature.")
883 parser.add_option("--desc_from_logs", action="store_true",
884 dest="from_logs",
885 help="""Squashes git commit logs into change description and
886 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000887 parser.add_option('-c', '--use-commit-queue', action='store_true',
888 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 (options, args) = parser.parse_args(args)
890
891 # Make sure index is up-to-date before running diff-index.
892 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
893 if RunGit(['diff-index', 'HEAD']):
894 print 'Cannot upload with a dirty tree. You must commit locally first.'
895 return 1
896
897 cl = Changelist()
898 if args:
899 base_branch = args[0]
900 else:
901 # Default to diffing against the "upstream" branch.
902 base_branch = cl.GetUpstreamBranch()
903 args = [base_branch + "..."]
904
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000905 if not options.bypass_hooks and not options.force:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000906 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000907 may_prompt=True,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000908 verbose=options.verbose,
909 author=None)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000910 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000911 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913
914 # --no-ext-diff is broken in some versions of Git, so try to work around
915 # this by overriding the environment (but there is still a problem if the
916 # git config key "diff.external" is used).
917 env = os.environ.copy()
918 if 'GIT_EXTERNAL_DIFF' in env:
919 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000920 subprocess2.call(
921 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922
923 upload_args = ['--assume_yes'] # Don't ask about untracked files.
924 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 if options.emulate_svn_auto_props:
926 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 if options.from_logs and not options.message:
928 print 'Must set message for subject line if using desc_from_logs'
929 return 1
930
931 change_desc = None
932
933 if cl.GetIssue():
934 if options.message:
935 upload_args.extend(['--message', options.message])
936 upload_args.extend(['--issue', cl.GetIssue()])
937 print ("This branch is associated with issue %s. "
938 "Adding patch to that issue." % cl.GetIssue())
939 else:
940 log_desc = CreateDescriptionFromLog(args)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000941 change_desc = ChangeDescription(options.message, log_desc,
942 options.reviewers)
943 if not options.from_logs:
944 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000945
946 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 print "Description is empty; aborting."
948 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000949
950 upload_args.extend(['--message', change_desc.subject])
951 upload_args.extend(['--description', change_desc.description])
952 if change_desc.reviewers:
953 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000954 if options.send_mail:
955 if not change_desc.reviewers:
956 DieWithError("Must specify reviewers to send email.")
957 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000958 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000959 if cc:
960 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961
962 # Include the upstream repo's URL in the change -- this is useful for
963 # projects that have their source spread across multiple repos.
964 remote_url = None
965 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000966 # URL is dependent on the current directory.
967 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 if data:
969 keys = dict(line.split(': ', 1) for line in data.splitlines()
970 if ': ' in line)
971 remote_url = keys.get('URL', None)
972 else:
973 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
974 remote_url = (cl.GetRemoteUrl() + '@'
975 + cl.GetUpstreamBranch().split('/')[-1])
976 if remote_url:
977 upload_args.extend(['--base_url', remote_url])
978
979 try:
980 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000981 except KeyboardInterrupt:
982 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983 except:
984 # If we got an exception after the user typed a description for their
985 # change, back up the description before re-raising.
986 if change_desc:
987 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
988 print '\nGot exception while uploading -- saving description to %s\n' \
989 % backup_path
990 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000991 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992 backup_file.close()
993 raise
994
995 if not cl.GetIssue():
996 cl.SetIssue(issue)
997 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000998
999 if options.use_commit_queue:
1000 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001 return 0
1002
1003
1004def SendUpstream(parser, args, cmd):
1005 """Common code for CmdPush and CmdDCommit
1006
1007 Squashed commit into a single.
1008 Updates changelog with metadata (e.g. pointer to review).
1009 Pushes/dcommits the code upstream.
1010 Updates review and closes.
1011 """
1012 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1013 help='bypass upload presubmit hook')
1014 parser.add_option('-m', dest='message',
1015 help="override review description")
1016 parser.add_option('-f', action='store_true', dest='force',
1017 help="force yes to questions (don't prompt)")
1018 parser.add_option('-c', dest='contributor',
1019 help="external contributor for patch (appended to " +
1020 "description and used as author for git). Should be " +
1021 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 (options, args) = parser.parse_args(args)
1023 cl = Changelist()
1024
1025 if not args or cmd == 'push':
1026 # Default to merging against our best guess of the upstream branch.
1027 args = [cl.GetUpstreamBranch()]
1028
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001029 if options.contributor:
1030 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1031 print "Please provide contibutor as 'First Last <email@example.com>'"
1032 return 1
1033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 base_branch = args[0]
1035
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001036 # Make sure index is up-to-date before running diff-index.
1037 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 if RunGit(['diff-index', 'HEAD']):
1039 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1040 return 1
1041
1042 # This rev-list syntax means "show all commits not in my branch that
1043 # are in base_branch".
1044 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1045 base_branch]).splitlines()
1046 if upstream_commits:
1047 print ('Base branch "%s" has %d commits '
1048 'not in this branch.' % (base_branch, len(upstream_commits)))
1049 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1050 return 1
1051
1052 if cmd == 'dcommit':
1053 # This is the revision `svn dcommit` will commit on top of.
1054 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1055 '--pretty=format:%H'])
1056 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1057 if extra_commits:
1058 print ('This branch has %d additional commits not upstreamed yet.'
1059 % len(extra_commits.splitlines()))
1060 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1061 'before attempting to %s.' % (base_branch, cmd))
1062 return 1
1063
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001064 if not options.bypass_hooks and not options.force:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001065 author = None
1066 if options.contributor:
1067 author = re.search(r'\<(.*)\>', options.contributor).group(1)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001068 cl.RunHook(committing=True, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001069 may_prompt=True, verbose=options.verbose,
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001070 author=author)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071
1072 if cmd == 'dcommit':
1073 # Check the tree status if the tree status URL is set.
1074 status = GetTreeStatus()
1075 if 'closed' == status:
1076 print ('The tree is closed. Please wait for it to reopen. Use '
1077 '"git cl dcommit -f" to commit on a closed tree.')
1078 return 1
1079 elif 'unknown' == status:
1080 print ('Unable to determine tree status. Please verify manually and '
1081 'use "git cl dcommit -f" to commit on a closed tree.')
1082
1083 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001084 if not description and cl.GetIssue():
1085 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001087 if not description:
1088 print 'No description set.'
1089 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1090 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001092 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094
1095 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 description += "\nPatch from %s." % options.contributor
1097 print 'Description:', repr(description)
1098
1099 branches = [base_branch, cl.GetBranchRef()]
1100 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001101 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001102 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103
1104 # We want to squash all this branch's commits into one commit with the
1105 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001106 # We do this by doing a "reset --soft" to the base branch (which keeps
1107 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 MERGE_BRANCH = 'git-cl-commit'
1109 # Delete the merge branch if it already exists.
1110 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1111 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1112 RunGit(['branch', '-D', MERGE_BRANCH])
1113
1114 # We might be in a directory that's present in this branch but not in the
1115 # trunk. Move up to the top of the tree so that git commands that expect a
1116 # valid CWD won't fail after we check out the merge branch.
1117 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1118 if rel_base_path:
1119 os.chdir(rel_base_path)
1120
1121 # Stuff our change into the merge branch.
1122 # We wrap in a try...finally block so if anything goes wrong,
1123 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001124 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001126 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1127 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 if options.contributor:
1129 RunGit(['commit', '--author', options.contributor, '-m', description])
1130 else:
1131 RunGit(['commit', '-m', description])
1132 if cmd == 'push':
1133 # push the merge branch.
1134 remote, branch = cl.FetchUpstreamTuple()
1135 retcode, output = RunGitWithCode(
1136 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1137 logging.debug(output)
1138 else:
1139 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001140 retcode, output = RunGitWithCode(['svn', 'dcommit',
1141 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 finally:
1143 # And then swap back to the original branch and clean up.
1144 RunGit(['checkout', '-q', cl.GetBranch()])
1145 RunGit(['branch', '-D', MERGE_BRANCH])
1146
1147 if cl.GetIssue():
1148 if cmd == 'dcommit' and 'Committed r' in output:
1149 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1150 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001151 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1152 for l in output.splitlines(False))
1153 match = filter(None, match)
1154 if len(match) != 1:
1155 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1156 output)
1157 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 else:
1159 return 1
1160 viewvc_url = settings.GetViewVCUrl()
1161 if viewvc_url and revision:
1162 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1163 print ('Closing issue '
1164 '(you may be prompted for your codereview password)...')
1165 cl.CloseIssue()
1166 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001167
1168 if retcode == 0:
1169 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1170 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001171 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 return 0
1174
1175
1176@usage('[upstream branch to apply against]')
1177def CMDdcommit(parser, args):
1178 """commit the current changelist via git-svn"""
1179 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001180 message = """This doesn't appear to be an SVN repository.
1181If your project has a git mirror with an upstream SVN master, you probably need
1182to run 'git svn init', see your project's git mirror documentation.
1183If your project has a true writeable upstream repository, you probably want
1184to run 'git cl push' instead.
1185Choose wisely, if you get this wrong, your commit might appear to succeed but
1186will instead be silently ignored."""
1187 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001188 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 return SendUpstream(parser, args, 'dcommit')
1190
1191
1192@usage('[upstream branch to apply against]')
1193def CMDpush(parser, args):
1194 """commit the current changelist via git"""
1195 if settings.GetIsGitSvn():
1196 print('This appears to be an SVN repository.')
1197 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001198 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199 return SendUpstream(parser, args, 'push')
1200
1201
1202@usage('<patch url or issue id>')
1203def CMDpatch(parser, args):
1204 """patch in a code review"""
1205 parser.add_option('-b', dest='newbranch',
1206 help='create a new branch off trunk for the patch')
1207 parser.add_option('-f', action='store_true', dest='force',
1208 help='with -b, clobber any existing branch')
1209 parser.add_option('--reject', action='store_true', dest='reject',
1210 help='allow failed patches and spew .rej files')
1211 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1212 help="don't commit after patch applies")
1213 (options, args) = parser.parse_args(args)
1214 if len(args) != 1:
1215 parser.print_help()
1216 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001217 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001219 # TODO(maruel): Use apply_issue.py
1220
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001221 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001223 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001224 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 else:
1226 # Assume it's a URL to the patch. Default to http.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001227 issue_url = FixUrl(issue_arg)
1228 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001229 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 DieWithError('Must pass an issue ID or full URL for '
1231 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001232 issue = match.group(1)
1233 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
1235 if options.newbranch:
1236 if options.force:
1237 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001238 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 RunGit(['checkout', '-b', options.newbranch,
1240 Changelist().GetUpstreamBranch()])
1241
1242 # Switch up to the top-level directory, if necessary, in preparation for
1243 # applying the patch.
1244 top = RunGit(['rev-parse', '--show-cdup']).strip()
1245 if top:
1246 os.chdir(top)
1247
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 # Git patches have a/ at the beginning of source paths. We strip that out
1249 # with a sed script rather than the -p flag to patch so we can feed either
1250 # Git or svn-style patches into the same apply command.
1251 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001252 try:
1253 patch_data = subprocess2.check_output(
1254 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1255 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 DieWithError('Git patch mungling failed.')
1257 logging.info(patch_data)
1258 # We use "git apply" to apply the patch instead of "patch" so that we can
1259 # pick up file adds.
1260 # The --index flag means: also insert into the index (so we catch adds).
1261 cmd = ['git', 'apply', '--index', '-p0']
1262 if options.reject:
1263 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001264 try:
1265 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1266 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 DieWithError('Failed to apply the patch')
1268
1269 # If we had an issue, commit the current state and register the issue.
1270 if not options.nocommit:
1271 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1272 cl = Changelist()
1273 cl.SetIssue(issue)
1274 print "Committed patch."
1275 else:
1276 print "Patch applied to index."
1277 return 0
1278
1279
1280def CMDrebase(parser, args):
1281 """rebase current branch on top of svn repo"""
1282 # Provide a wrapper for git svn rebase to help avoid accidental
1283 # git svn dcommit.
1284 # It's the only command that doesn't use parser at all since we just defer
1285 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001286 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287
1288
1289def GetTreeStatus():
1290 """Fetches the tree status and returns either 'open', 'closed',
1291 'unknown' or 'unset'."""
1292 url = settings.GetTreeStatusUrl(error_ok=True)
1293 if url:
1294 status = urllib2.urlopen(url).read().lower()
1295 if status.find('closed') != -1 or status == '0':
1296 return 'closed'
1297 elif status.find('open') != -1 or status == '1':
1298 return 'open'
1299 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 return 'unset'
1301
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303def GetTreeStatusReason():
1304 """Fetches the tree status from a json url and returns the message
1305 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001306 url = settings.GetTreeStatusUrl()
1307 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 connection = urllib2.urlopen(json_url)
1309 status = json.loads(connection.read())
1310 connection.close()
1311 return status['message']
1312
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001313
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314def CMDtree(parser, args):
1315 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001316 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 status = GetTreeStatus()
1318 if 'unset' == status:
1319 print 'You must configure your tree status URL by running "git cl config".'
1320 return 2
1321
1322 print "The tree is %s" % status
1323 print
1324 print GetTreeStatusReason()
1325 if status != 'open':
1326 return 1
1327 return 0
1328
1329
1330def CMDupstream(parser, args):
1331 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001332 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001333 if args:
1334 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 cl = Changelist()
1336 print cl.GetUpstreamBranch()
1337 return 0
1338
1339
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001340def CMDset_commit(parser, args):
1341 """set the commit bit"""
1342 _, args = parser.parse_args(args)
1343 if args:
1344 parser.error('Unrecognized args: %s' % ' '.join(args))
1345 cl = Changelist()
1346 cl.SetFlag('commit', '1')
1347 return 0
1348
1349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350def Command(name):
1351 return getattr(sys.modules[__name__], 'CMD' + name, None)
1352
1353
1354def CMDhelp(parser, args):
1355 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001356 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 if len(args) == 1:
1358 return main(args + ['--help'])
1359 parser.print_help()
1360 return 0
1361
1362
1363def GenUsage(parser, command):
1364 """Modify an OptParse object with the function's documentation."""
1365 obj = Command(command)
1366 more = getattr(obj, 'usage_more', '')
1367 if command == 'help':
1368 command = '<command>'
1369 else:
1370 # OptParser.description prefer nicely non-formatted strings.
1371 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1372 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1373
1374
1375def main(argv):
1376 """Doesn't parse the arguments here, just find the right subcommand to
1377 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001378 # Reload settings.
1379 global settings
1380 settings = Settings()
1381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 # Do it late so all commands are listed.
1383 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1384 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1385 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1386
1387 # Create the option parse and add --verbose support.
1388 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001389 parser.add_option(
1390 '-v', '--verbose', action='count', default=0,
1391 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 old_parser_args = parser.parse_args
1393 def Parse(args):
1394 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001395 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001397 elif options.verbose:
1398 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 else:
1400 logging.basicConfig(level=logging.WARNING)
1401 return options, args
1402 parser.parse_args = Parse
1403
1404 if argv:
1405 command = Command(argv[0])
1406 if command:
1407 # "fix" the usage and the description now that we know the subcommand.
1408 GenUsage(parser, argv[0])
1409 try:
1410 return command(parser, argv[1:])
1411 except urllib2.HTTPError, e:
1412 if e.code != 500:
1413 raise
1414 DieWithError(
1415 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1416 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1417
1418 # Not a known command. Default to help.
1419 GenUsage(parser, 'help')
1420 return CMDhelp(parser, argv)
1421
1422
1423if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001424 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 sys.exit(main(sys.argv[1:]))