blob: b3374d9d642d8c0a4490e65b85669491e3862c97 [file] [log] [blame]
[email protected]fb2b8eb2009-04-23 21:03:421#!/usr/bin/python
[email protected]8a8ea022010-11-01 13:27:322# Copyright (c) 2010 The Chromium Authors. All rights reserved.
[email protected]fb2b8eb2009-04-23 21:03:423# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
[email protected]ebbf9472009-11-10 20:26:305
[email protected]62fd6932010-05-27 13:13:236"""\
7Wrapper script around Rietveld's upload.py that simplifies working with groups
8of files.
9"""
[email protected]fb2b8eb2009-04-23 21:03:4210
11import getpass
[email protected]8e13a092010-11-02 19:06:0612import optparse
[email protected]fb2b8eb2009-04-23 21:03:4213import os
14import random
15import re
16import string
17import subprocess
18import sys
19import tempfile
[email protected]2f6a0d82010-05-12 00:03:3020import time
[email protected]ba551772010-02-03 18:21:4221from third_party import upload
[email protected]fb2b8eb2009-04-23 21:03:4222import urllib2
23
[email protected]fa3843e2010-11-01 13:34:3724try:
25 import simplejson as json
26except ImportError:
27 try:
28 import json
29 # Some versions of python2.5 have an incomplete json module. Check to make
30 # sure loads exists.
31 # pylint: disable=W0104
32 json.loads
33 except (ImportError, AttributeError):
34 # Import the one included in depot_tools.
35 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
36 import simplejson as json
37
[email protected]cb2985f2010-11-03 14:08:3138import breakpad # pylint: disable=W0611
[email protected]ada4c652009-12-03 15:32:0139
[email protected]46a94102009-05-12 20:32:4340# gcl now depends on gclient.
[email protected]5aeb7dd2009-11-17 18:09:0141from scm import SVN
[email protected]5f3eee32009-09-17 00:34:3042import gclient_utils
[email protected]c1675e22009-04-27 20:30:4843
[email protected]62fd6932010-05-27 13:13:2344__version__ = '1.2'
[email protected]c1675e22009-04-27 20:30:4845
46
[email protected]fb2b8eb2009-04-23 21:03:4247CODEREVIEW_SETTINGS = {
[email protected]b8260242010-08-19 17:03:1648 # To make gcl send reviews to a server, check in a file named
[email protected]172b6e72010-01-26 00:35:0349 # "codereview.settings" (see |CODEREVIEW_SETTINGS_FILE| below) to your
50 # project's base directory and add the following line to codereview.settings:
51 # CODE_REVIEW_SERVER: codereview.yourserver.org
[email protected]fb2b8eb2009-04-23 21:03:4252}
53
[email protected]fb2b8eb2009-04-23 21:03:4254# globals that store the root of the current repository and the directory where
55# we store information about changelists.
[email protected]98fc2b92009-05-21 14:11:5156REPOSITORY_ROOT = ""
[email protected]fb2b8eb2009-04-23 21:03:4257
58# Filename where we store repository specific information for gcl.
59CODEREVIEW_SETTINGS_FILE = "codereview.settings"
[email protected]b8260242010-08-19 17:03:1660CODEREVIEW_SETTINGS_FILE_NOT_FOUND = (
61 'No %s file found. Please add one.' % CODEREVIEW_SETTINGS_FILE)
[email protected]fb2b8eb2009-04-23 21:03:4262
63# Warning message when the change appears to be missing tests.
64MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
65
[email protected]98fc2b92009-05-21 14:11:5166# Global cache of files cached in GetCacheDir().
67FILES_CACHE = {}
[email protected]fb2b8eb2009-04-23 21:03:4268
[email protected]4c22d722010-05-14 19:01:2269# Valid extensions for files we want to lint.
70DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
71DEFAULT_LINT_IGNORE_REGEX = r"$^"
72
73
[email protected]e5299012010-04-07 18:02:2674def CheckHomeForFile(filename):
75 """Checks the users home dir for the existence of the given file. Returns
76 the path to the file if it's there, or None if it is not.
77 """
78 home_vars = ['HOME']
79 if sys.platform in ('cygwin', 'win32'):
80 home_vars.append('USERPROFILE')
81 for home_var in home_vars:
82 home = os.getenv(home_var)
83 if home != None:
84 full_path = os.path.join(home, filename)
85 if os.path.exists(full_path):
86 return full_path
87 return None
[email protected]fb2b8eb2009-04-23 21:03:4288
[email protected]35fe9ad2010-05-25 23:59:5489
[email protected]62fd6932010-05-27 13:13:2390def UnknownFiles():
91 """Runs svn status and returns unknown files."""
92 return [item[1] for item in SVN.CaptureStatus([]) if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0193
94
[email protected]fb2b8eb2009-04-23 21:03:4295def GetRepositoryRoot():
96 """Returns the top level directory of the current repository.
97
98 The directory is returned as an absolute path.
99 """
[email protected]98fc2b92009-05-21 14:11:51100 global REPOSITORY_ROOT
101 if not REPOSITORY_ROOT:
[email protected]94b1ee92009-12-19 20:27:20102 REPOSITORY_ROOT = SVN.GetCheckoutRoot(os.getcwd())
103 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:30104 raise gclient_utils.Error("gcl run outside of repository")
[email protected]98fc2b92009-05-21 14:11:51105 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:42106
107
108def GetInfoDir():
109 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:52110 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
111
112
113def GetChangesDir():
114 """Returns the directory where gcl change files are stored."""
115 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:42116
117
[email protected]98fc2b92009-05-21 14:11:51118def GetCacheDir():
119 """Returns the directory where gcl change files are stored."""
120 return os.path.join(GetInfoDir(), 'cache')
121
122
123def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
124 """Retrieves a file from the repository and caches it in GetCacheDir() for
125 max_age seconds.
126
127 use_root: If False, look up the arborescence for the first match, otherwise go
128 directory to the root repository.
129
130 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:02131 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:51132 """
[email protected]98fc2b92009-05-21 14:11:51133 if filename not in FILES_CACHE:
134 # Don't try to look up twice.
135 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28136 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02137 try:
138 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]5f3eee32009-09-17 00:34:30139 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:02140 return None
[email protected]98fc2b92009-05-21 14:11:51141 if (not os.path.exists(cached_file) or
[email protected]2f6a0d82010-05-12 00:03:30142 (time.time() - os.stat(cached_file).st_mtime) > max_age):
[email protected]b3b494f2010-08-31 20:40:08143 dir_info = SVN.CaptureInfo('.')
144 repo_root = dir_info['Repository Root']
[email protected]98fc2b92009-05-21 14:11:51145 if use_root:
146 url_path = repo_root
147 else:
[email protected]b3b494f2010-08-31 20:40:08148 url_path = dir_info['URL']
[email protected]9b613272009-04-24 01:28:28149 while True:
[email protected]fa44e4a2009-12-03 01:41:13150 # Look in the repository at the current level for the file.
[email protected]b6ee1672010-08-19 17:06:07151 for _ in range(5):
[email protected]b3b494f2010-08-31 20:40:08152 content = None
[email protected]b6ee1672010-08-19 17:06:07153 try:
154 # Take advantage of the fact that svn won't output to stderr in case
155 # of success but will do in case of failure so don't mind putting
156 # stderr into content_array.
157 content_array = []
[email protected]b3b494f2010-08-31 20:40:08158 svn_path = url_path + '/' + filename
[email protected]17368002010-09-01 23:36:32159 args = ['svn', 'cat', svn_path]
[email protected]02913a52010-08-25 20:50:50160 if sys.platform != 'darwin':
161 # MacOSX 10.5.2 has a bug with svn 1.4.4 that will trigger the
162 # 'Can\'t get username or password' and can be fixed easily.
163 # The fix doesn't work if the user upgraded to svn 1.6.x. Bleh.
164 # I don't have time to fix their broken stuff.
165 args.append('--non-interactive')
[email protected]17d01792010-09-01 18:07:10166 gclient_utils.CheckCallAndFilter(
167 args, cwd='.', filter_fn=content_array.append)
[email protected]b6ee1672010-08-19 17:06:07168 # Exit the loop if the file was found. Override content.
169 content = '\n'.join(content_array)
170 break
[email protected]2b9aa8e2010-08-25 20:01:42171 except gclient_utils.Error:
[email protected]b6ee1672010-08-19 17:06:07172 if content_array[0].startswith(
173 'svn: Can\'t get username or password'):
174 ErrorExit('Your svn credentials expired. Please run svn update '
175 'to fix the cached credentials')
[email protected]3c842982010-08-20 17:26:49176 if content_array[0].startswith('svn: Can\'t get password'):
177 ErrorExit('If are using a Mac and svn --version shows 1.4.x, '
178 'please hack gcl.py to remove --non-interactive usage, it\'s'
179 'a bug on your installed copy')
[email protected]b6ee1672010-08-19 17:06:07180 if not content_array[0].startswith('svn: File not found:'):
181 # Try again.
182 continue
183 if content:
[email protected]9b613272009-04-24 01:28:28184 break
[email protected]9b613272009-04-24 01:28:28185 if url_path == repo_root:
186 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51187 break
[email protected]9b613272009-04-24 01:28:28188 # Go up one level to try again.
189 url_path = os.path.dirname(url_path)
[email protected]b3b494f2010-08-31 20:40:08190 if content is not None or filename != CODEREVIEW_SETTINGS_FILE:
191 # Write a cached version even if there isn't a file, so we don't try to
192 # fetch it each time. codereview.settings must always be present so do
193 # not cache negative.
194 gclient_utils.FileWrite(cached_file, content or '')
[email protected]98fc2b92009-05-21 14:11:51195 else:
[email protected]0fca4f32009-12-18 15:14:34196 content = gclient_utils.FileRead(cached_file, 'r')
[email protected]98fc2b92009-05-21 14:11:51197 # Keep the content cached in memory.
198 FILES_CACHE[filename] = content
199 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28200
[email protected]98fc2b92009-05-21 14:11:51201
202def GetCodeReviewSetting(key):
203 """Returns a value for the given key for this repository."""
204 # Use '__just_initialized' as a flag to determine if the settings were
205 # already initialized.
[email protected]98fc2b92009-05-21 14:11:51206 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47207 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
208 if settings_file:
209 for line in settings_file.splitlines():
[email protected]807c4462010-07-10 00:45:28210 if not line or line.startswith('#'):
[email protected]b0442182009-06-05 14:20:47211 continue
[email protected]807c4462010-07-10 00:45:28212 if not ':' in line:
213 raise gclient_utils.Error(
214 '%s is invalid, please fix. It\'s content:\n\n%s' %
215 (CODEREVIEW_SETTINGS_FILE, settings_file))
[email protected]dd218e52010-08-23 13:02:41216 k, v = line.split(':', 1)
217 CODEREVIEW_SETTINGS[k.strip()] = v.strip()
[email protected]98fc2b92009-05-21 14:11:51218 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42219 return CODEREVIEW_SETTINGS.get(key, "")
220
221
[email protected]fb2b8eb2009-04-23 21:03:42222def Warn(msg):
[email protected]6e29d572010-06-04 17:32:20223 print >> sys.stderr, msg
[email protected]223b7192010-06-04 18:52:58224
225
226def ErrorExit(msg):
227 print >> sys.stderr, msg
228 sys.exit(1)
[email protected]fb2b8eb2009-04-23 21:03:42229
230
231def RunShellWithReturnCode(command, print_output=False):
232 """Executes a command and returns the output and the return code."""
[email protected]3a292682010-08-23 18:54:55233 p = gclient_utils.Popen(command, stdout=subprocess.PIPE,
234 stderr=subprocess.STDOUT, universal_newlines=True)
[email protected]fb2b8eb2009-04-23 21:03:42235 if print_output:
236 output_array = []
237 while True:
238 line = p.stdout.readline()
239 if not line:
240 break
241 if print_output:
242 print line.strip('\n')
243 output_array.append(line)
244 output = "".join(output_array)
245 else:
246 output = p.stdout.read()
247 p.wait()
248 p.stdout.close()
249 return output, p.returncode
250
251
252def RunShell(command, print_output=False):
253 """Executes a command and returns the output."""
254 return RunShellWithReturnCode(command, print_output)[0]
255
256
[email protected]51ee0072009-06-08 19:20:05257def FilterFlag(args, flag):
258 """Returns True if the flag is present in args list.
259
260 The flag is removed from args if present.
261 """
262 if flag in args:
263 args.remove(flag)
264 return True
265 return False
266
267
[email protected]be0d1ca2009-05-12 19:23:02268class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42269 """Holds information about a changelist.
270
[email protected]32ba2602009-06-06 18:44:48271 name: change name.
272 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
273 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42274 description: the description.
275 files: a list of 2 tuple containing (status, filename) of changed files,
276 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08277 local_root: Local root directory
[email protected]bf1fdca2010-11-01 18:05:36278 rietveld: rietveld server for this change
[email protected]fb2b8eb2009-04-23 21:03:42279 """
[email protected]bf1fdca2010-11-01 18:05:36280 # Kept for unit test support. This is for the old format, it's deprecated.
[email protected]32ba2602009-06-06 18:44:48281 _SEPARATOR = "\n-----\n"
[email protected]32ba2602009-06-06 18:44:48282
[email protected]ea452b32009-11-22 20:04:31283 def __init__(self, name, issue, patchset, description, files, local_root,
[email protected]bf1fdca2010-11-01 18:05:36284 rietveld, needs_upload=False):
[email protected]fb2b8eb2009-04-23 21:03:42285 self.name = name
[email protected]32ba2602009-06-06 18:44:48286 self.issue = int(issue)
287 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42288 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02289 if files is None:
290 files = []
[email protected]17f59f22009-06-12 13:27:24291 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42292 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08293 self._local_root = local_root
[email protected]ea452b32009-11-22 20:04:31294 self.needs_upload = needs_upload
[email protected]bf1fdca2010-11-01 18:05:36295 self.rietveld = rietveld
296 if not self.rietveld:
297 # Set the default value.
298 self.rietveld = GetCodeReviewSetting('CODE_REVIEW_SERVER')
[email protected]ea452b32009-11-22 20:04:31299
300 def NeedsUpload(self):
301 return self.needs_upload
[email protected]fb2b8eb2009-04-23 21:03:42302
[email protected]17f59f22009-06-12 13:27:24303 def GetFileNames(self):
304 """Returns the list of file names included in this change."""
[email protected]e3608df2009-11-10 20:22:57305 return [f[1] for f in self._files]
[email protected]17f59f22009-06-12 13:27:24306
307 def GetFiles(self):
308 """Returns the list of files included in this change with their status."""
309 return self._files
310
311 def GetLocalRoot(self):
312 """Returns the local repository checkout root directory."""
313 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42314
[email protected]f0dfba32009-08-07 22:03:37315 def Exists(self):
316 """Returns True if this change already exists (i.e., is not new)."""
317 return (self.issue or self.description or self._files)
318
[email protected]fb2b8eb2009-04-23 21:03:42319 def _NonDeletedFileList(self):
320 """Returns a list of files in this change, not including deleted files."""
[email protected]e3608df2009-11-10 20:22:57321 return [f[1] for f in self.GetFiles()
322 if not f[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42323
324 def _AddedFileList(self):
325 """Returns a list of files added in this change."""
[email protected]e3608df2009-11-10 20:22:57326 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42327
328 def Save(self):
329 """Writes the changelist information to disk."""
[email protected]fa3843e2010-11-01 13:34:37330 data = json.dumps({
331 'issue': self.issue,
332 'patchset': self.patchset,
333 'needs_upload': self.NeedsUpload(),
334 'files': self.GetFiles(),
335 'description': self.description,
[email protected]bf1fdca2010-11-01 18:05:36336 'rietveld': self.rietveld,
[email protected]fa3843e2010-11-01 13:34:37337 }, sort_keys=True, indent=2)
[email protected]fc83c112009-12-18 15:14:10338 gclient_utils.FileWrite(GetChangelistInfoFile(self.name), data)
[email protected]fb2b8eb2009-04-23 21:03:42339
340 def Delete(self):
341 """Removes the changelist information from disk."""
342 os.remove(GetChangelistInfoFile(self.name))
343
344 def CloseIssue(self):
345 """Closes the Rietveld issue for this changelist."""
346 data = [("description", self.description),]
347 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]bf1fdca2010-11-01 18:05:36348 self.SendToRietveld('/%d/close' % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42349
350 def UpdateRietveldDescription(self):
351 """Sets the description for an issue on Rietveld."""
352 data = [("description", self.description),]
353 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]bf1fdca2010-11-01 18:05:36354 self.SendToRietveld('/%d/description' % self.issue, body, ctype)
355
356 def GetIssueDescription(self):
357 """Returns the issue description from Rietveld."""
358 return self.SendToRietveld('/%d/description' % self.issue)
359
360 def PrimeLint(self):
361 """Do background work on Rietveld to lint the file so that the results are
362 ready when the issue is viewed."""
363 if self.issue and self.patchset:
364 self.SendToRietveld('/lint/issue%s_%s' % (self.issue, self.patchset),
365 timeout=1)
366
367 def SendToRietveld(self, request_path, payload=None,
368 content_type="application/octet-stream", timeout=None):
369 """Send a POST/GET to Rietveld. Returns the response body."""
370 if not self.rietveld:
371 ErrorExit(CODEREVIEW_SETTINGS_FILE_NOT_FOUND)
372 def GetUserCredentials():
373 """Prompts the user for a username and password."""
374 email = upload.GetEmail('Email (login for uploading to %s)' %
375 self.rietveld)
376 password = getpass.getpass('Password for %s: ' % email)
377 return email, password
378 rpc_server = upload.HttpRpcServer(self.rietveld,
379 GetUserCredentials,
380 save_cookies=True)
381 try:
382 return rpc_server.Send(request_path, payload, content_type, timeout)
383 except urllib2.URLError:
384 if timeout is None:
385 ErrorExit('Error accessing url %s' % request_path)
386 else:
387 return None
[email protected]fb2b8eb2009-04-23 21:03:42388
389 def MissingTests(self):
390 """Returns True if the change looks like it needs unit tests but has none.
391
392 A change needs unit tests if it contains any new source files or methods.
393 """
394 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
395 # Ignore third_party entirely.
[email protected]e3608df2009-11-10 20:22:57396 files = [f for f in self._NonDeletedFileList()
397 if f.find("third_party") == -1]
398 added_files = [f for f in self._AddedFileList()
399 if f.find("third_party") == -1]
[email protected]fb2b8eb2009-04-23 21:03:42400
401 # If the change is entirely in third_party, we're done.
402 if len(files) == 0:
403 return False
404
405 # Any new or modified test files?
406 # A test file's name ends with "test.*" or "tests.*".
407 test_files = [test for test in files
408 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
409 if len(test_files) > 0:
410 return False
411
412 # Any new source files?
[email protected]e3608df2009-11-10 20:22:57413 source_files = [item for item in added_files
414 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
[email protected]fb2b8eb2009-04-23 21:03:42415 if len(source_files) > 0:
416 return True
417
418 # Do the long test, checking the files for new methods.
419 return self._HasNewMethod()
420
421 def _HasNewMethod(self):
422 """Returns True if the changeset contains any new functions, or if a
423 function signature has been changed.
424
425 A function is identified by starting flush left, containing a "(" before
426 the next flush-left line, and either ending with "{" before the next
427 flush-left line or being followed by an unindented "{".
428
429 Currently this returns True for new methods, new static functions, and
430 methods or functions whose signatures have been changed.
431
432 Inline methods added to header files won't be detected by this. That's
433 acceptable for purposes of determining if a unit test is needed, since
434 inline methods should be trivial.
435 """
436 # To check for methods added to source or header files, we need the diffs.
437 # We'll generate them all, since there aren't likely to be many files
438 # apart from source and headers; besides, we'll want them all if we're
439 # uploading anyway.
440 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24441 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42442
443 definition = ""
444 for line in self.patch.splitlines():
445 if not line.startswith("+"):
446 continue
447 line = line.strip("+").rstrip(" \t")
448 # Skip empty lines, comments, and preprocessor directives.
449 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
450 if line == "" or line.startswith("/") or line.startswith("#"):
451 continue
452
453 # A possible definition ending with "{" is complete, so check it.
454 if definition.endswith("{"):
455 if definition.find("(") != -1:
456 return True
457 definition = ""
458
459 # A { or an indented line, when we're in a definition, continues it.
460 if (definition != "" and
461 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
462 definition += line
463
464 # A flush-left line starts a new possible function definition.
465 elif not line.startswith(" ") and not line.startswith("\t"):
466 definition = line
467
468 return False
469
[email protected]32ba2602009-06-06 18:44:48470 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08471 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48472 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42473
[email protected]32ba2602009-06-06 18:44:48474 Args:
475 fail_on_not_found: if True, this function will quit the program if the
476 changelist doesn't exist.
477 update_status: if True, the svn status will be updated for all the files
478 and unchanged files will be removed.
479
480 Returns: a ChangeInfo object.
481 """
482 info_file = GetChangelistInfoFile(changename)
483 if not os.path.exists(info_file):
484 if fail_on_not_found:
485 ErrorExit("Changelist " + changename + " not found.")
[email protected]bf1fdca2010-11-01 18:05:36486 return ChangeInfo(changename, 0, 0, '', None, local_root, rietveld=None,
[email protected]ea452b32009-11-22 20:04:31487 needs_upload=False)
[email protected]8a8ea022010-11-01 13:27:32488 content = gclient_utils.FileRead(info_file, 'r')
489 save = False
490 try:
[email protected]fa3843e2010-11-01 13:34:37491 values = ChangeInfo._LoadNewFormat(content)
[email protected]8a8ea022010-11-01 13:27:32492 except ValueError:
[email protected]fa3843e2010-11-01 13:34:37493 try:
494 values = ChangeInfo._LoadOldFormat(content)
495 save = True
496 except ValueError:
497 ErrorExit(
498 ('Changelist file %s is corrupt.\n'
499 'Either run "gcl delete %s" or manually edit the file') % (
500 info_file, changename))
[email protected]8a8ea022010-11-01 13:27:32501 files = values['files']
[email protected]32ba2602009-06-06 18:44:48502 if update_status:
[email protected]79b7ef02010-11-01 13:25:13503 for item in files[:]:
[email protected]e3608df2009-11-10 20:22:57504 filename = os.path.join(local_root, item[1])
[email protected]5aeb7dd2009-11-17 18:09:01505 status_result = SVN.CaptureStatus(filename)
[email protected]32ba2602009-06-06 18:44:48506 if not status_result or not status_result[0][0]:
507 # File has been reverted.
508 save = True
[email protected]e3608df2009-11-10 20:22:57509 files.remove(item)
[email protected]32ba2602009-06-06 18:44:48510 continue
511 status = status_result[0][0]
[email protected]e3608df2009-11-10 20:22:57512 if status != item[0]:
[email protected]32ba2602009-06-06 18:44:48513 save = True
[email protected]e3608df2009-11-10 20:22:57514 files[files.index(item)] = (status, item[1])
[email protected]8a8ea022010-11-01 13:27:32515 change_info = ChangeInfo(changename, values['issue'], values['patchset'],
516 values['description'], files,
[email protected]bf1fdca2010-11-01 18:05:36517 local_root, values.get('rietveld'),
518 values['needs_upload'])
[email protected]32ba2602009-06-06 18:44:48519 if save:
520 change_info.Save()
521 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42522
[email protected]8a8ea022010-11-01 13:27:32523 @staticmethod
524 def _LoadOldFormat(content):
[email protected]bf1fdca2010-11-01 18:05:36525 # The info files have the following format:
526 # issue_id, patchset\n (, patchset is optional)
527 # _SEPARATOR\n
528 # filepath1\n
529 # filepath2\n
530 # .
531 # .
532 # filepathn\n
533 # _SEPARATOR\n
534 # description
[email protected]8a8ea022010-11-01 13:27:32535 split_data = content.split(ChangeInfo._SEPARATOR, 2)
536 if len(split_data) != 3:
537 raise ValueError('Bad change format')
538 values = {
539 'issue': 0,
540 'patchset': 0,
541 'needs_upload': False,
542 'files': [],
543 }
544 items = split_data[0].split(', ')
545 if items[0]:
546 values['issue'] = int(items[0])
547 if len(items) > 1:
548 values['patchset'] = int(items[1])
549 if len(items) > 2:
550 values['needs_upload'] = (items[2] == "dirty")
551 for line in split_data[1].splitlines():
552 status = line[:7]
553 filename = line[7:]
554 values['files'].append((status, filename))
555 values['description'] = split_data[2]
556 return values
557
[email protected]fa3843e2010-11-01 13:34:37558 @staticmethod
559 def _LoadNewFormat(content):
560 return json.loads(content)
561
[email protected]fb2b8eb2009-04-23 21:03:42562
563def GetChangelistInfoFile(changename):
564 """Returns the file that stores information about a changelist."""
565 if not changename or re.search(r'[^\w-]', changename):
566 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52567 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42568
569
[email protected]8d5c9a52009-06-12 15:59:08570def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
571 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42572 """Loads many changes and merge their files list into one pseudo change.
573
574 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
575 """
576 changes = changenames.split(',')
[email protected]ea452b32009-11-22 20:04:31577 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root,
[email protected]bf1fdca2010-11-01 18:05:36578 rietveld=None, needs_upload=False)
[email protected]fb2b8eb2009-04-23 21:03:42579 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08580 aggregate_change_info._files += ChangeInfo.Load(change,
581 local_root,
582 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24583 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42584 return aggregate_change_info
585
586
[email protected]fb2b8eb2009-04-23 21:03:42587def GetCLs():
588 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52589 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42590 if CODEREVIEW_SETTINGS_FILE in cls:
591 cls.remove(CODEREVIEW_SETTINGS_FILE)
592 return cls
593
594
595def GenerateChangeName():
596 """Generate a random changelist name."""
597 random.seed()
598 current_cl_names = GetCLs()
599 while True:
600 cl_name = (random.choice(string.ascii_lowercase) +
601 random.choice(string.digits) +
602 random.choice(string.ascii_lowercase) +
603 random.choice(string.digits))
604 if cl_name not in current_cl_names:
605 return cl_name
606
607
608def GetModifiedFiles():
609 """Returns a set that maps from changelist name to (status,filename) tuples.
610
611 Files not in a changelist have an empty changelist name. Filenames are in
612 relation to the top level directory of the current repository. Note that
613 only the current directory and subdirectories are scanned, in order to
614 improve performance while still being flexible.
615 """
616 files = {}
617
618 # Since the files are normalized to the root folder of the repositary, figure
619 # out what we need to add to the paths.
620 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
621
622 # Get a list of all files in changelists.
623 files_in_cl = {}
624 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08625 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
626 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24627 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42628 files_in_cl[filename] = change_info.name
629
630 # Get all the modified files.
[email protected]5aeb7dd2009-11-17 18:09:01631 status_result = SVN.CaptureStatus(None)
[email protected]207fdf32009-04-28 19:57:01632 for line in status_result:
633 status = line[0]
634 filename = line[1]
635 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42636 continue
[email protected]fb2b8eb2009-04-23 21:03:42637 if dir_prefix:
638 filename = os.path.join(dir_prefix, filename)
639 change_list_name = ""
640 if filename in files_in_cl:
641 change_list_name = files_in_cl[filename]
642 files.setdefault(change_list_name, []).append((status, filename))
643
644 return files
645
646
647def GetFilesNotInCL():
648 """Returns a list of tuples (status,filename) that aren't in any changelists.
649
650 See docstring of GetModifiedFiles for information about path of files and
651 which directories are scanned.
652 """
653 modified_files = GetModifiedFiles()
654 if "" not in modified_files:
655 return []
656 return modified_files[""]
657
658
[email protected]4c22d722010-05-14 19:01:22659def ListFiles(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42660 files = GetModifiedFiles()
661 cl_keys = files.keys()
662 cl_keys.sort()
663 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05664 if not cl_name:
665 continue
666 note = ""
667 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
668 fail_on_not_found=True, update_status=False)
669 if len(change_info.GetFiles()) != len(files[cl_name]):
670 note = " (Note: this changelist contains files outside this directory)"
671 print "\n--- Changelist " + cl_name + note + ":"
[email protected]e3608df2009-11-10 20:22:57672 for filename in files[cl_name]:
673 print "".join(filename)
[email protected]88c32d82009-10-12 18:24:05674 if show_unknown_files:
[email protected]62fd6932010-05-27 13:13:23675 unknown_files = UnknownFiles()
[email protected]88c32d82009-10-12 18:24:05676 if (files.get('') or (show_unknown_files and len(unknown_files))):
677 print "\n--- Not in any changelist:"
[email protected]e3608df2009-11-10 20:22:57678 for item in files.get('', []):
679 print "".join(item)
[email protected]88c32d82009-10-12 18:24:05680 if show_unknown_files:
[email protected]e3608df2009-11-10 20:22:57681 for filename in unknown_files:
682 print "? %s" % filename
[email protected]4c22d722010-05-14 19:01:22683 return 0
[email protected]fb2b8eb2009-04-23 21:03:42684
685
[email protected]fb2b8eb2009-04-23 21:03:42686def GetEditor():
687 editor = os.environ.get("SVN_EDITOR")
688 if not editor:
689 editor = os.environ.get("EDITOR")
690
691 if not editor:
692 if sys.platform.startswith("win"):
693 editor = "notepad"
694 else:
695 editor = "vi"
696
697 return editor
698
699
700def GenerateDiff(files, root=None):
[email protected]f2f9d552009-12-22 00:12:57701 return SVN.GenerateDiff(files, root=root)
[email protected]fb2b8eb2009-04-23 21:03:42702
[email protected]51ee0072009-06-08 19:20:05703
704def OptionallyDoPresubmitChecks(change_info, committing, args):
705 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
706 return True
[email protected]b0dfd352009-06-10 14:12:54707 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05708
709
[email protected]62fd6932010-05-27 13:13:23710def defer_attributes(a, b):
711 """Copy attributes from an object (like a function) to another."""
712 for x in dir(a):
713 if not getattr(b, x, None):
714 setattr(b, x, getattr(a, x))
715
716
[email protected]35fe9ad2010-05-25 23:59:54717def need_change(function):
718 """Converts args -> change_info."""
[email protected]3a174252010-10-29 15:54:51719 # pylint: disable=W0612,W0621
[email protected]35fe9ad2010-05-25 23:59:54720 def hook(args):
721 if not len(args) == 1:
722 ErrorExit("You need to pass a change list name")
723 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(), True, True)
724 return function(change_info)
[email protected]62fd6932010-05-27 13:13:23725 defer_attributes(function, hook)
726 hook.need_change = True
727 hook.no_args = True
[email protected]35fe9ad2010-05-25 23:59:54728 return hook
729
730
[email protected]62fd6932010-05-27 13:13:23731def need_change_and_args(function):
732 """Converts args -> change_info."""
[email protected]3a174252010-10-29 15:54:51733 # pylint: disable=W0612,W0621
[email protected]62fd6932010-05-27 13:13:23734 def hook(args):
[email protected]e56fe822010-05-28 20:36:57735 if not args:
736 ErrorExit("You need to pass a change list name")
[email protected]62fd6932010-05-27 13:13:23737 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
738 return function(change_info, args)
739 defer_attributes(function, hook)
740 hook.need_change = True
741 return hook
742
743
744def no_args(function):
745 """Make sure no args are passed."""
[email protected]3a174252010-10-29 15:54:51746 # pylint: disable=W0612,W0621
[email protected]62fd6932010-05-27 13:13:23747 def hook(args):
748 if args:
749 ErrorExit("Doesn't support arguments")
750 return function()
751 defer_attributes(function, hook)
752 hook.no_args = True
753 return hook
754
755
756def attrs(**kwargs):
757 """Decorate a function with new attributes."""
758 def decorate(function):
759 for k in kwargs:
760 setattr(function, k, kwargs[k])
761 return function
762 return decorate
763
764
765@no_args
766def CMDopened():
767 """Lists modified files in the current directory down."""
768 return ListFiles(False)
769
770
771@no_args
772def CMDstatus():
773 """Lists modified and unknown files in the current directory down."""
774 return ListFiles(True)
775
776
777@need_change_and_args
[email protected]2b9aa8e2010-08-25 20:01:42778@attrs(usage='[--no_presubmit] [--no_watchlists]')
[email protected]62fd6932010-05-27 13:13:23779def CMDupload(change_info, args):
780 """Uploads the changelist to the server for review.
781
[email protected]5db5ba52010-07-20 15:50:47782 This does not submit a try job; use gcl try to submit a try job.
[email protected]62fd6932010-05-27 13:13:23783 """
[email protected]10ccd112010-08-24 16:48:42784 if '-s' in args or '--server' in args:
785 ErrorExit('Don\'t use the -s flag, fix codereview.settings instead')
[email protected]17f59f22009-06-12 13:27:24786 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42787 print "Nothing to upload, changelist is empty."
[email protected]35fe9ad2010-05-25 23:59:54788 return 0
[email protected]51ee0072009-06-08 19:20:05789 if not OptionallyDoPresubmitChecks(change_info, False, args):
[email protected]35fe9ad2010-05-25 23:59:54790 return 1
[email protected]62fd6932010-05-27 13:13:23791 no_watchlists = (FilterFlag(args, "--no_watchlists") or
792 FilterFlag(args, "--no-watchlists"))
[email protected]fb2b8eb2009-04-23 21:03:42793
794 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05795 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42796 args.append("--send_mail")
797
[email protected]fb2b8eb2009-04-23 21:03:42798 upload_arg = ["upload.py", "-y"]
[email protected]bf1fdca2010-11-01 18:05:36799 upload_arg.append("--server=%s" % change_info.rietveld)
[email protected]fb2b8eb2009-04-23 21:03:42800 upload_arg.extend(args)
801
802 desc_file = ""
803 if change_info.issue: # Uploading a new patchset.
804 found_message = False
805 for arg in args:
806 if arg.startswith("--message") or arg.startswith("-m"):
807 found_message = True
808 break
809
810 if not found_message:
811 upload_arg.append("--message=''")
812
[email protected]32ba2602009-06-06 18:44:48813 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42814 else: # First time we upload.
815 handle, desc_file = tempfile.mkstemp(text=True)
816 os.write(handle, change_info.description)
817 os.close(handle)
818
[email protected]b2ab4942009-06-11 21:39:19819 # Watchlist processing -- CC people interested in this changeset
820 # http://dev.chromium.org/developers/contributing-code/watchlists
821 if not no_watchlists:
822 import watchlists
[email protected]17f59f22009-06-12 13:27:24823 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08824 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19825
[email protected]fb2b8eb2009-04-23 21:03:42826 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19827 if not no_watchlists and watchers:
[email protected]6e29d572010-06-04 17:32:20828 # Filter out all empty elements and join by ','
829 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42830 if cc_list:
831 upload_arg.append("--cc=" + cc_list)
832 upload_arg.append("--description_file=" + desc_file + "")
833 if change_info.description:
834 subject = change_info.description[:77]
835 if subject.find("\r\n") != -1:
836 subject = subject[:subject.find("\r\n")]
837 if subject.find("\n") != -1:
838 subject = subject[:subject.find("\n")]
839 if len(change_info.description) > 77:
840 subject = subject + "..."
841 upload_arg.append("--message=" + subject)
842
[email protected]83b6e4b2010-03-09 03:16:14843 if GetCodeReviewSetting("PRIVATE") == "True":
844 upload_arg.append("--private")
845
[email protected]fb2b8eb2009-04-23 21:03:42846 # Change the current working directory before calling upload.py so that it
847 # shows the correct base.
848 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24849 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42850 # If we have a lot of files with long paths, then we won't be able to fit
851 # the command to "svn diff". Instead, we generate the diff manually for
852 # each file and concatenate them before passing it to upload.py.
853 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24854 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42855 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48856 if issue and patchset:
857 change_info.issue = int(issue)
858 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42859 change_info.Save()
860
861 if desc_file:
862 os.remove(desc_file)
[email protected]bf1fdca2010-11-01 18:05:36863 change_info.PrimeLint()
[email protected]57e78552009-09-11 23:04:30864 os.chdir(previous_cwd)
[email protected]02287952010-07-23 22:36:58865 print "*** Upload does not submit a try; use gcl try to submit a try. ***"
[email protected]35fe9ad2010-05-25 23:59:54866 return 0
[email protected]fb2b8eb2009-04-23 21:03:42867
[email protected]fb2b8eb2009-04-23 21:03:42868
[email protected]8e13a092010-11-02 19:06:06869@need_change_and_args
870@attrs(usage='[--upload]')
871def CMDpresubmit(change_info, args):
[email protected]62fd6932010-05-27 13:13:23872 """Runs presubmit checks on the change.
873
874 The actual presubmit code is implemented in presubmit_support.py and looks
875 for PRESUBMIT.py files."""
[email protected]17f59f22009-06-12 13:27:24876 if not change_info.GetFiles():
[email protected]8e13a092010-11-02 19:06:06877 print('Nothing to presubmit check, changelist is empty.')
[email protected]4c22d722010-05-14 19:01:22878 return 0
[email protected]8e13a092010-11-02 19:06:06879 parser = optparse.OptionParser()
880 parser.add_option('--upload', action='store_true')
881 options, args = parser.parse_args(args)
882 if args:
883 parser.error('Unrecognized args: %s' % args)
884 if options.upload:
885 print('*** Presubmit checks for UPLOAD would report: ***')
886 return not DoPresubmitChecks(change_info, False, False)
887 else:
888 print('*** Presubmit checks for COMMIT would report: ***')
889 return not DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42890
891
892def TryChange(change_info, args, swallow_exception):
893 """Create a diff file of change_info and send it to the try server."""
894 try:
895 import trychange
896 except ImportError:
897 if swallow_exception:
[email protected]35fe9ad2010-05-25 23:59:54898 return 1
[email protected]fb2b8eb2009-04-23 21:03:42899 ErrorExit("You need to install trychange.py to use the try server.")
900
[email protected]18111352009-12-20 17:21:28901 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42902 if change_info:
[email protected]18111352009-12-20 17:21:28903 trychange_args.extend(['--name', change_info.name])
[email protected]32ba2602009-06-06 18:44:48904 if change_info.issue:
905 trychange_args.extend(["--issue", str(change_info.issue)])
906 if change_info.patchset:
907 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42908 trychange_args.extend(args)
[email protected]1227c7d2009-12-22 00:54:27909 file_list = change_info.GetFileNames()
[email protected]fb2b8eb2009-04-23 21:03:42910 else:
[email protected]18111352009-12-20 17:21:28911 trychange_args.extend(args)
[email protected]09c0dba2010-10-14 14:32:22912 file_list = []
[email protected]d0891922010-05-31 18:33:16913 return trychange.TryChange(
914 trychange_args,
915 file_list=file_list,
916 swallow_exception=swallow_exception,
917 prog='gcl try',
918 extra_epilog='\n'
919 'When called from gcl, use the format gcl try <change_name>.\n')
[email protected]fb2b8eb2009-04-23 21:03:42920
921
[email protected]62fd6932010-05-27 13:13:23922@need_change_and_args
923@attrs(usage='[--no_presubmit]')
[email protected]4357af22010-05-27 15:42:34924def CMDcommit(change_info, args):
[email protected]62fd6932010-05-27 13:13:23925 """Commits the changelist to the repository."""
[email protected]17f59f22009-06-12 13:27:24926 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42927 print "Nothing to commit, changelist is empty."
[email protected]4c22d722010-05-14 19:01:22928 return 1
[email protected]51ee0072009-06-08 19:20:05929 if not OptionallyDoPresubmitChecks(change_info, True, args):
[email protected]4c22d722010-05-14 19:01:22930 return 1
[email protected]fb2b8eb2009-04-23 21:03:42931
[email protected]1bb04aa2009-06-01 17:52:11932 # We face a problem with svn here: Let's say change 'bleh' modifies
933 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
934 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
935 # The only fix is to use --non-recursive but that has its issues too:
936 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
937 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
938 # directory with child nodes". Yay...
939 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42940 if change_info.issue:
941 # Get the latest description from Rietveld.
[email protected]bf1fdca2010-11-01 18:05:36942 change_info.description = change_info.GetIssueDescription()
[email protected]fb2b8eb2009-04-23 21:03:42943
944 commit_message = change_info.description.replace('\r\n', '\n')
945 if change_info.issue:
[email protected]bf1fdca2010-11-01 18:05:36946 server = change_info.rietveld
[email protected]fcff9272010-04-29 23:56:19947 if not server.startswith("http://") and not server.startswith("https://"):
948 server = "http://" + server
949 commit_message += ('\nReview URL: %s/%d' % (server, change_info.issue))
[email protected]fb2b8eb2009-04-23 21:03:42950
951 handle, commit_filename = tempfile.mkstemp(text=True)
952 os.write(handle, commit_message)
953 os.close(handle)
954
955 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24956 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42957 os.close(handle)
958
959 commit_cmd += ['--file=' + commit_filename]
960 commit_cmd += ['--targets=' + targets_filename]
961 # Change the current working directory before calling commit.
962 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24963 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42964 output = RunShell(commit_cmd, True)
965 os.remove(commit_filename)
966 os.remove(targets_filename)
967 if output.find("Committed revision") != -1:
968 change_info.Delete()
969
970 if change_info.issue:
971 revision = re.compile(".*?\nCommitted revision (\d+)",
972 re.DOTALL).match(output).group(1)
973 viewvc_url = GetCodeReviewSetting("VIEW_VC")
974 change_info.description = change_info.description + '\n'
975 if viewvc_url:
976 change_info.description += "\nCommitted: " + viewvc_url + revision
977 change_info.CloseIssue()
978 os.chdir(previous_cwd)
[email protected]4c22d722010-05-14 19:01:22979 return 0
[email protected]fb2b8eb2009-04-23 21:03:42980
[email protected]2c8d4b22009-06-06 21:03:10981
[email protected]35fe9ad2010-05-25 23:59:54982def CMDchange(args):
[email protected]62fd6932010-05-27 13:13:23983 """Creates or edits a changelist.
984
985 Only scans the current directory and subdirectories."""
[email protected]35fe9ad2010-05-25 23:59:54986 if len(args) == 0:
987 # Generate a random changelist name.
988 changename = GenerateChangeName()
989 elif args[0] == '--force':
990 changename = GenerateChangeName()
991 else:
992 changename = args[0]
993 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(), False, True)
[email protected]9ce98222009-10-19 20:24:17994 silent = FilterFlag(args, "--silent")
[email protected]d36b3ed2009-11-09 18:51:42995
996 # Verify the user is running the change command from a read-write checkout.
[email protected]5aeb7dd2009-11-17 18:09:01997 svn_info = SVN.CaptureInfo('.')
[email protected]d36b3ed2009-11-09 18:51:42998 if not svn_info:
999 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
1000 "directory.")
[email protected]d36b3ed2009-11-09 18:51:421001
[email protected]35fe9ad2010-05-25 23:59:541002 if len(args) == 2:
[email protected]c8f3cf82010-09-09 20:00:121003 if not os.path.isfile(args[1]):
1004 ErrorExit('The change "%s" doesn\'t exist.' % args[1])
[email protected]35fe9ad2010-05-25 23:59:541005 f = open(args[1], 'rU')
[email protected]9ce98222009-10-19 20:24:171006 override_description = f.read()
1007 f.close()
1008 else:
1009 override_description = None
[email protected]5aeb7dd2009-11-17 18:09:011010
[email protected]ea452b32009-11-22 20:04:311011 if change_info.issue and not change_info.NeedsUpload():
[email protected]fb2b8eb2009-04-23 21:03:421012 try:
[email protected]bf1fdca2010-11-01 18:05:361013 description = change_info.GetIssueDescription()
[email protected]fb2b8eb2009-04-23 21:03:421014 except urllib2.HTTPError, err:
1015 if err.code == 404:
1016 # The user deleted the issue in Rietveld, so forget the old issue id.
1017 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:101018 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:421019 change_info.Save()
1020 else:
1021 ErrorExit("Error getting the description from Rietveld: " + err)
1022 else:
[email protected]85532fc2009-06-04 22:36:531023 if override_description:
1024 description = override_description
1025 else:
1026 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:421027
1028 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:231029
[email protected]f0dfba32009-08-07 22:03:371030 # Edited files (as opposed to files with only changed properties) will have
1031 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:531032 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:371033 affected_files = [x for x in other_files if file_re.match(x[0])]
1034 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:421035
1036 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:241037 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:421038 "---Paths in this changelist (" + change_info.name + "):\n")
1039 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
1040 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:371041 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
1042
1043 if change_info.Exists():
1044 text += (separator2 +
1045 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
1046 else:
1047 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
1048 separator2)
1049 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:421050
1051 handle, filename = tempfile.mkstemp(text=True)
1052 os.write(handle, text)
1053 os.close(handle)
1054
[email protected]9ce98222009-10-19 20:24:171055 if not silent:
1056 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:421057
[email protected]0fca4f32009-12-18 15:14:341058 result = gclient_utils.FileRead(filename, 'r')
[email protected]fb2b8eb2009-04-23 21:03:421059 os.remove(filename)
1060
1061 if not result:
[email protected]4c22d722010-05-14 19:01:221062 return 0
[email protected]fb2b8eb2009-04-23 21:03:421063
1064 split_result = result.split(separator1, 1)
1065 if len(split_result) != 2:
1066 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1067
[email protected]ea452b32009-11-22 20:04:311068 # Update the CL description if it has changed.
[email protected]fb2b8eb2009-04-23 21:03:421069 new_description = split_result[0]
1070 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531071 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421072 change_info.description = new_description
[email protected]ea452b32009-11-22 20:04:311073 change_info.needs_upload = True
[email protected]fb2b8eb2009-04-23 21:03:421074
1075 new_cl_files = []
1076 for line in cl_files_text.splitlines():
1077 if not len(line):
1078 continue
1079 if line.startswith("---"):
1080 break
1081 status = line[:7]
[email protected]e3608df2009-11-10 20:22:571082 filename = line[7:]
1083 new_cl_files.append((status, filename))
[email protected]bfd09ce2009-08-05 21:17:231084
[email protected]3a174252010-10-29 15:54:511085 if (not len(change_info.GetFiles()) and not change_info.issue and
1086 not len(new_description) and not new_cl_files):
[email protected]bfd09ce2009-08-05 21:17:231087 ErrorExit("Empty changelist not saved")
1088
[email protected]17f59f22009-06-12 13:27:241089 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421090 change_info.Save()
[email protected]53bcf152009-11-13 21:04:101091 if svn_info.get('URL', '').startswith('http:'):
1092 Warn("WARNING: Creating CL in a read-only checkout. You will not be "
1093 "able to commit it!")
1094
[email protected]fb2b8eb2009-04-23 21:03:421095 print change_info.name + " changelist saved."
1096 if change_info.MissingTests():
1097 Warn("WARNING: " + MISSING_TEST_MSG)
1098
[email protected]ea452b32009-11-22 20:04:311099 # Update the Rietveld issue.
1100 if change_info.issue and change_info.NeedsUpload():
1101 change_info.UpdateRietveldDescription()
1102 change_info.needs_upload = False
1103 change_info.Save()
[email protected]4c22d722010-05-14 19:01:221104 return 0
[email protected]ea452b32009-11-22 20:04:311105
1106
[email protected]62fd6932010-05-27 13:13:231107@need_change_and_args
1108def CMDlint(change_info, args):
1109 """Runs cpplint.py on all the files in the change list.
1110
1111 Checks all the files in the changelist for possible style violations.
1112 """
[email protected]fb2b8eb2009-04-23 21:03:421113 try:
1114 import cpplint
1115 except ImportError:
1116 ErrorExit("You need to install cpplint.py to lint C++ files.")
[email protected]fb2b8eb2009-04-23 21:03:421117 # Change the current working directory before calling lint so that it
1118 # shows the correct base.
1119 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241120 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421121 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241122 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421123
[email protected]bb816382009-10-29 01:38:021124 white_list = GetCodeReviewSetting("LINT_REGEX")
1125 if not white_list:
[email protected]e72bb632009-10-29 20:15:481126 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021127 white_regex = re.compile(white_list)
1128 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1129 if not black_list:
[email protected]e72bb632009-10-29 20:15:481130 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021131 black_regex = re.compile(black_list)
[email protected]b17b55b2010-11-03 14:42:371132 # Access to a protected member _XX of a client class
1133 # pylint: disable=W0212
[email protected]e3608df2009-11-10 20:22:571134 for filename in filenames:
1135 if white_regex.match(filename):
1136 if black_regex.match(filename):
1137 print "Ignoring file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421138 else:
[email protected]e3608df2009-11-10 20:22:571139 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021140 else:
[email protected]e3608df2009-11-10 20:22:571141 print "Skipping file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421142
1143 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1144 os.chdir(previous_cwd)
[email protected]4c22d722010-05-14 19:01:221145 return 1
[email protected]fb2b8eb2009-04-23 21:03:421146
1147
[email protected]b0dfd352009-06-10 14:12:541148def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421149 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1150 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481151 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151152 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411153 change = presubmit_support.SvnChange(change_info.name,
1154 change_info.description,
1155 change_info.GetLocalRoot(),
1156 change_info.GetFiles(),
1157 change_info.issue,
1158 change_info.patchset)
1159 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541160 committing=committing,
[email protected]1033acc2009-05-13 14:36:481161 verbose=False,
1162 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151163 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541164 default_presubmit=root_presubmit,
1165 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551166 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421167 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1168 return result
1169
1170
[email protected]62fd6932010-05-27 13:13:231171@no_args
1172def CMDchanges():
[email protected]4c22d722010-05-14 19:01:221173 """Lists all the changelists and their files."""
[email protected]fb2b8eb2009-04-23 21:03:421174 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081175 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421176 print "\n--- Changelist " + change_info.name + ":"
[email protected]e3608df2009-11-10 20:22:571177 for filename in change_info.GetFiles():
1178 print "".join(filename)
[email protected]4c22d722010-05-14 19:01:221179 return 0
[email protected]fb2b8eb2009-04-23 21:03:421180
1181
[email protected]62fd6932010-05-27 13:13:231182@no_args
1183def CMDdeleteempties():
[email protected]bfd09ce2009-08-05 21:17:231184 """Delete all changelists that have no files."""
1185 print "\n--- Deleting:"
1186 for cl in GetCLs():
1187 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]3a174252010-10-29 15:54:511188 if not len(change_info.GetFiles()):
[email protected]bfd09ce2009-08-05 21:17:231189 print change_info.name
1190 change_info.Delete()
[email protected]4c22d722010-05-14 19:01:221191 return 0
1192
1193
[email protected]62fd6932010-05-27 13:13:231194@no_args
1195def CMDnothave():
[email protected]4c22d722010-05-14 19:01:221196 """Lists files unknown to Subversion."""
[email protected]62fd6932010-05-27 13:13:231197 for filename in UnknownFiles():
[email protected]4c22d722010-05-14 19:01:221198 print "? " + "".join(filename)
1199 return 0
1200
1201
[email protected]62fd6932010-05-27 13:13:231202@attrs(usage='<svn options>')
[email protected]35fe9ad2010-05-25 23:59:541203def CMDdiff(args):
[email protected]62fd6932010-05-27 13:13:231204 """Diffs all files in the changelist or all files that aren't in a CL."""
[email protected]35fe9ad2010-05-25 23:59:541205 files = None
1206 if args:
[email protected]62fd6932010-05-27 13:13:231207 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
[email protected]35fe9ad2010-05-25 23:59:541208 files = change_info.GetFileNames()
1209 else:
[email protected]707c1482010-06-02 19:52:421210 files = [f[1] for f in GetFilesNotInCL()]
[email protected]38729702010-06-01 23:42:031211
1212 root = GetRepositoryRoot()
1213 cmd = ['svn', 'diff']
1214 cmd.extend([os.path.join(root, x) for x in files])
1215 cmd.extend(args)
1216 return RunShellWithReturnCode(cmd, print_output=True)[1]
[email protected]4c22d722010-05-14 19:01:221217
1218
[email protected]62fd6932010-05-27 13:13:231219@no_args
1220def CMDsettings():
1221 """Prints code review settings for this checkout."""
[email protected]4c22d722010-05-14 19:01:221222 # Force load settings
[email protected]6e29d572010-06-04 17:32:201223 GetCodeReviewSetting("UNKNOWN")
[email protected]4c22d722010-05-14 19:01:221224 del CODEREVIEW_SETTINGS['__just_initialized']
1225 print '\n'.join(("%s: %s" % (str(k), str(v))
1226 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
1227 return 0
1228
1229
[email protected]35fe9ad2010-05-25 23:59:541230@need_change
1231def CMDdescription(change_info):
[email protected]4c22d722010-05-14 19:01:221232 """Prints the description of the specified change to stdout."""
[email protected]4c22d722010-05-14 19:01:221233 print change_info.description
1234 return 0
1235
1236
[email protected]79b7ef02010-11-01 13:25:131237def CMDdelete(args):
[email protected]4c22d722010-05-14 19:01:221238 """Deletes a changelist."""
[email protected]79b7ef02010-11-01 13:25:131239 if not len(args) == 1:
1240 ErrorExit('You need to pass a change list name')
1241 os.remove(GetChangelistInfoFile(args[0]))
[email protected]4c22d722010-05-14 19:01:221242 return 0
1243
1244
[email protected]35fe9ad2010-05-25 23:59:541245def CMDtry(args):
[email protected]62fd6932010-05-27 13:13:231246 """Sends the change to the tryserver to do a test run on your code.
[email protected]4c22d722010-05-14 19:01:221247
1248 To send multiple changes as one path, use a comma-separated list of
1249 changenames. Use 'gcl help try' for more information!"""
1250 # When the change contains no file, send the "changename" positional
1251 # argument to trychange.py.
[email protected]35fe9ad2010-05-25 23:59:541252 # When the command is 'try' and --patchset is used, the patch to try
1253 # is on the Rietveld server.
1254 if not args:
1255 ErrorExit("You need to pass a change list name")
1256 if args[0].find(',') != -1:
1257 change_info = LoadChangelistInfoForMultiple(args[0], GetRepositoryRoot(),
1258 True, True)
1259 else:
1260 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(),
1261 False, True)
[email protected]4c22d722010-05-14 19:01:221262 if change_info.GetFiles():
[email protected]35fe9ad2010-05-25 23:59:541263 args = args[1:]
[email protected]4c22d722010-05-14 19:01:221264 else:
1265 change_info = None
[email protected]35fe9ad2010-05-25 23:59:541266 return TryChange(change_info, args, swallow_exception=False)
[email protected]4c22d722010-05-14 19:01:221267
1268
[email protected]62fd6932010-05-27 13:13:231269@attrs(usage='<old-name> <new-name>')
[email protected]35fe9ad2010-05-25 23:59:541270def CMDrename(args):
[email protected]4c22d722010-05-14 19:01:221271 """Renames an existing change."""
[email protected]35fe9ad2010-05-25 23:59:541272 if len(args) != 2:
[email protected]4c22d722010-05-14 19:01:221273 ErrorExit("Usage: gcl rename <old-name> <new-name>.")
[email protected]35fe9ad2010-05-25 23:59:541274 src, dst = args
[email protected]4c22d722010-05-14 19:01:221275 src_file = GetChangelistInfoFile(src)
1276 if not os.path.isfile(src_file):
1277 ErrorExit("Change '%s' does not exist." % src)
1278 dst_file = GetChangelistInfoFile(dst)
1279 if os.path.isfile(dst_file):
1280 ErrorExit("Change '%s' already exists; pick a new name." % dst)
1281 os.rename(src_file, dst_file)
1282 print "Change '%s' renamed '%s'." % (src, dst)
1283 return 0
[email protected]bfd09ce2009-08-05 21:17:231284
1285
[email protected]35fe9ad2010-05-25 23:59:541286def CMDpassthru(args):
[email protected]62fd6932010-05-27 13:13:231287 """Everything else that is passed into gcl we redirect to svn.
1288
1289 It assumes a change list name is passed and is converted with the files names.
1290 """
[email protected]35fe9ad2010-05-25 23:59:541291 args = ["svn", args[0]]
1292 if len(args) > 1:
1293 root = GetRepositoryRoot()
1294 change_info = ChangeInfo.Load(args[1], root, True, True)
1295 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
1296 return RunShellWithReturnCode(args, print_output=True)[1]
1297
1298
[email protected]62fd6932010-05-27 13:13:231299def Command(name):
1300 return getattr(sys.modules[__name__], 'CMD' + name, None)
1301
1302
1303def GenUsage(command):
1304 """Modify an OptParse object with the function's documentation."""
1305 obj = Command(command)
1306 display = command
1307 more = getattr(obj, 'usage', '')
1308 if command == 'help':
1309 display = '<command>'
[email protected]3a174252010-10-29 15:54:511310 need_change_val = ''
[email protected]62fd6932010-05-27 13:13:231311 if getattr(obj, 'need_change', None):
[email protected]3a174252010-10-29 15:54:511312 need_change_val = ' <change_list>'
[email protected]62fd6932010-05-27 13:13:231313 options = ' [options]'
1314 if getattr(obj, 'no_args', None):
1315 options = ''
[email protected]3a174252010-10-29 15:54:511316 res = 'Usage: gcl %s%s%s %s\n\n' % (display, need_change_val, options, more)
[email protected]62fd6932010-05-27 13:13:231317 res += re.sub('\n ', '\n', obj.__doc__)
1318 return res
1319
1320
1321def CMDhelp(args):
1322 """Prints this help or help for the given command."""
1323 if args and 'CMD' + args[0] in dir(sys.modules[__name__]):
1324 print GenUsage(args[0])
1325
1326 # These commands defer to external tools so give this info too.
1327 if args[0] == 'try':
1328 TryChange(None, ['--help'], swallow_exception=False)
1329 if args[0] == 'upload':
1330 upload.RealMain(['upload.py', '--help'])
1331 return 0
1332
1333 print GenUsage('help')
1334 print sys.modules[__name__].__doc__
1335 print 'version ' + __version__ + '\n'
1336
1337 print('Commands are:\n' + '\n'.join([
1338 ' %-12s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1339 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1340 return 0
1341
1342
[email protected]35fe9ad2010-05-25 23:59:541343def main(argv):
[email protected]c68f9cb2010-06-17 20:34:181344 if not argv:
1345 argv = ['help']
1346 command = Command(argv[0])
1347 # Help can be run from anywhere.
1348 if command == CMDhelp:
1349 return command(argv[1:])
1350
[email protected]a05be0b2009-06-30 19:13:021351 try:
[email protected]62fd6932010-05-27 13:13:231352 GetRepositoryRoot()
[email protected]5f3eee32009-09-17 00:34:301353 except gclient_utils.Error:
[email protected]58c19382010-09-22 19:53:591354 print >> sys.stderr, 'To use gcl, you need to be in a subversion checkout.'
[email protected]62fd6932010-05-27 13:13:231355 return 1
1356
1357 # Create the directories where we store information about changelists if it
1358 # doesn't exist.
[email protected]807c4462010-07-10 00:45:281359 try:
1360 if not os.path.exists(GetInfoDir()):
1361 os.mkdir(GetInfoDir())
1362 if not os.path.exists(GetChangesDir()):
1363 os.mkdir(GetChangesDir())
1364 if not os.path.exists(GetCacheDir()):
1365 os.mkdir(GetCacheDir())
[email protected]fb2b8eb2009-04-23 21:03:421366
[email protected]807c4462010-07-10 00:45:281367 if command:
1368 return command(argv[1:])
1369 # Unknown command, try to pass that to svn
1370 return CMDpassthru(argv)
1371 except gclient_utils.Error, e:
[email protected]58c19382010-09-22 19:53:591372 print >> sys.stderr, 'Got an exception'
1373 print >> sys.stderr, str(e)
1374 return 1
[email protected]42c7f662010-10-06 23:52:461375 except upload.ClientLoginError, e:
1376 print >> sys.stderr, 'Got an exception logging in to Rietveld'
1377 print >> sys.stderr, str(e)
[email protected]58c19382010-09-22 19:53:591378 except urllib2.HTTPError, e:
1379 if e.code != 500:
1380 raise
1381 print >> sys.stderr, (
1382 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
[email protected]7633e472010-09-22 20:03:141383 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))
[email protected]58c19382010-09-22 19:53:591384 return 1
1385
[email protected]fb2b8eb2009-04-23 21:03:421386
1387if __name__ == "__main__":
[email protected]35fe9ad2010-05-25 23:59:541388 sys.exit(main(sys.argv[1:]))