blob: 539f136b69550da5749e91008a90691fc79d1df8 [file] [log] [blame]
[email protected]fb2b8eb2009-04-23 21:03:421#!/usr/bin/python
2# Copyright (c) 2006-2009 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.
[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
12import os
13import random
14import re
[email protected]fb2b8eb2009-04-23 21:03:4215import string
16import subprocess
17import sys
18import tempfile
[email protected]2f6a0d82010-05-12 00:03:3019import time
[email protected]ba551772010-02-03 18:21:4220from third_party import upload
[email protected]fb2b8eb2009-04-23 21:03:4221import urllib2
22
[email protected]4c22d722010-05-14 19:01:2223__pychecker__ = 'unusednames=breakpad'
[email protected]ada4c652009-12-03 15:32:0124import breakpad
[email protected]4c22d722010-05-14 19:01:2225__pychecker__ = ''
[email protected]ada4c652009-12-03 15:32:0126
[email protected]46a94102009-05-12 20:32:4327# gcl now depends on gclient.
[email protected]5aeb7dd2009-11-17 18:09:0128from scm import SVN
[email protected]5f3eee32009-09-17 00:34:3029import gclient_utils
[email protected]c1675e22009-04-27 20:30:4830
[email protected]62fd6932010-05-27 13:13:2331__version__ = '1.2'
[email protected]c1675e22009-04-27 20:30:4832
33
[email protected]fb2b8eb2009-04-23 21:03:4234CODEREVIEW_SETTINGS = {
[email protected]172b6e72010-01-26 00:35:0335 # Ideally, we want to set |CODE_REVIEW_SERVER| to a generic server like
36 # codereview.appspot.com and remove |CC_LIST| and |VIEW_VC|. In practice, we
37 # need these settings so developers making changes in directories such as
38 # Chromium's src/third_party/WebKit will send change lists to the correct
39 # server.
40 #
41 # To make gcl send reviews to a different server, check in a file named
42 # "codereview.settings" (see |CODEREVIEW_SETTINGS_FILE| below) to your
43 # project's base directory and add the following line to codereview.settings:
44 # CODE_REVIEW_SERVER: codereview.yourserver.org
45 #
[email protected]fb2b8eb2009-04-23 21:03:4246 # Default values.
[email protected]172b6e72010-01-26 00:35:0347 "CODE_REVIEW_SERVER": "codereview.chromium.org",
48 "CC_LIST": "[email protected]",
49 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
[email protected]fb2b8eb2009-04-23 21:03:4250}
51
[email protected]fb2b8eb2009-04-23 21:03:4252# globals that store the root of the current repository and the directory where
53# we store information about changelists.
[email protected]98fc2b92009-05-21 14:11:5154REPOSITORY_ROOT = ""
[email protected]fb2b8eb2009-04-23 21:03:4255
56# Filename where we store repository specific information for gcl.
57CODEREVIEW_SETTINGS_FILE = "codereview.settings"
58
59# Warning message when the change appears to be missing tests.
60MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
61
[email protected]98fc2b92009-05-21 14:11:5162# Global cache of files cached in GetCacheDir().
63FILES_CACHE = {}
[email protected]fb2b8eb2009-04-23 21:03:4264
[email protected]4c22d722010-05-14 19:01:2265# Valid extensions for files we want to lint.
66DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
67DEFAULT_LINT_IGNORE_REGEX = r"$^"
68
69
[email protected]e5299012010-04-07 18:02:2670def CheckHomeForFile(filename):
71 """Checks the users home dir for the existence of the given file. Returns
72 the path to the file if it's there, or None if it is not.
73 """
74 home_vars = ['HOME']
75 if sys.platform in ('cygwin', 'win32'):
76 home_vars.append('USERPROFILE')
77 for home_var in home_vars:
78 home = os.getenv(home_var)
79 if home != None:
80 full_path = os.path.join(home, filename)
81 if os.path.exists(full_path):
82 return full_path
83 return None
[email protected]fb2b8eb2009-04-23 21:03:4284
[email protected]35fe9ad2010-05-25 23:59:5485
[email protected]62fd6932010-05-27 13:13:2386def UnknownFiles():
87 """Runs svn status and returns unknown files."""
88 return [item[1] for item in SVN.CaptureStatus([]) if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0189
90
[email protected]fb2b8eb2009-04-23 21:03:4291def GetRepositoryRoot():
92 """Returns the top level directory of the current repository.
93
94 The directory is returned as an absolute path.
95 """
[email protected]98fc2b92009-05-21 14:11:5196 global REPOSITORY_ROOT
97 if not REPOSITORY_ROOT:
[email protected]94b1ee92009-12-19 20:27:2098 REPOSITORY_ROOT = SVN.GetCheckoutRoot(os.getcwd())
99 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:30100 raise gclient_utils.Error("gcl run outside of repository")
[email protected]98fc2b92009-05-21 14:11:51101 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:42102
103
104def GetInfoDir():
105 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:52106 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
107
108
109def GetChangesDir():
110 """Returns the directory where gcl change files are stored."""
111 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:42112
113
[email protected]98fc2b92009-05-21 14:11:51114def GetCacheDir():
115 """Returns the directory where gcl change files are stored."""
116 return os.path.join(GetInfoDir(), 'cache')
117
118
119def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
120 """Retrieves a file from the repository and caches it in GetCacheDir() for
121 max_age seconds.
122
123 use_root: If False, look up the arborescence for the first match, otherwise go
124 directory to the root repository.
125
126 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:02127 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:51128 """
[email protected]98fc2b92009-05-21 14:11:51129 if filename not in FILES_CACHE:
130 # Don't try to look up twice.
131 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28132 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02133 try:
134 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]5f3eee32009-09-17 00:34:30135 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:02136 return None
[email protected]98fc2b92009-05-21 14:11:51137 if (not os.path.exists(cached_file) or
[email protected]2f6a0d82010-05-12 00:03:30138 (time.time() - os.stat(cached_file).st_mtime) > max_age):
[email protected]5aeb7dd2009-11-17 18:09:01139 dir_info = SVN.CaptureInfo(".")
[email protected]9b613272009-04-24 01:28:28140 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51141 if use_root:
142 url_path = repo_root
143 else:
144 url_path = dir_info["URL"]
145 content = ""
[email protected]9b613272009-04-24 01:28:28146 while True:
[email protected]fa44e4a2009-12-03 01:41:13147 # Look in the repository at the current level for the file.
148 svn_path = url_path + "/" + filename
149 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
[email protected]9b613272009-04-24 01:28:28150 if not rc:
[email protected]98fc2b92009-05-21 14:11:51151 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28152 break
153 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51154 content = ""
[email protected]9b613272009-04-24 01:28:28155 if url_path == repo_root:
156 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51157 break
[email protected]9b613272009-04-24 01:28:28158 # Go up one level to try again.
159 url_path = os.path.dirname(url_path)
[email protected]9b613272009-04-24 01:28:28160 # Write a cached version even if there isn't a file, so we don't try to
161 # fetch it each time.
[email protected]fc83c112009-12-18 15:14:10162 gclient_utils.FileWrite(cached_file, content)
[email protected]98fc2b92009-05-21 14:11:51163 else:
[email protected]0fca4f32009-12-18 15:14:34164 content = gclient_utils.FileRead(cached_file, 'r')
[email protected]98fc2b92009-05-21 14:11:51165 # Keep the content cached in memory.
166 FILES_CACHE[filename] = content
167 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28168
[email protected]98fc2b92009-05-21 14:11:51169
170def GetCodeReviewSetting(key):
171 """Returns a value for the given key for this repository."""
172 # Use '__just_initialized' as a flag to determine if the settings were
173 # already initialized.
[email protected]98fc2b92009-05-21 14:11:51174 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47175 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
176 if settings_file:
177 for line in settings_file.splitlines():
[email protected]807c4462010-07-10 00:45:28178 if not line or line.startswith('#'):
[email protected]b0442182009-06-05 14:20:47179 continue
[email protected]807c4462010-07-10 00:45:28180 if not ':' in line:
181 raise gclient_utils.Error(
182 '%s is invalid, please fix. It\'s content:\n\n%s' %
183 (CODEREVIEW_SETTINGS_FILE, settings_file))
184 k, v = line.split(': ', 1)
[email protected]b0442182009-06-05 14:20:47185 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51186 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42187 return CODEREVIEW_SETTINGS.get(key, "")
188
189
[email protected]fb2b8eb2009-04-23 21:03:42190def Warn(msg):
[email protected]6e29d572010-06-04 17:32:20191 print >> sys.stderr, msg
[email protected]223b7192010-06-04 18:52:58192
193
194def ErrorExit(msg):
195 print >> sys.stderr, msg
196 sys.exit(1)
[email protected]fb2b8eb2009-04-23 21:03:42197
198
199def RunShellWithReturnCode(command, print_output=False):
200 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02201 # Use a shell for subcommands on Windows to get a PATH search, and because svn
202 # may be a batch file.
203 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42204 p = subprocess.Popen(command, stdout=subprocess.PIPE,
205 stderr=subprocess.STDOUT, shell=use_shell,
206 universal_newlines=True)
207 if print_output:
208 output_array = []
209 while True:
210 line = p.stdout.readline()
211 if not line:
212 break
213 if print_output:
214 print line.strip('\n')
215 output_array.append(line)
216 output = "".join(output_array)
217 else:
218 output = p.stdout.read()
219 p.wait()
220 p.stdout.close()
221 return output, p.returncode
222
223
224def RunShell(command, print_output=False):
225 """Executes a command and returns the output."""
226 return RunShellWithReturnCode(command, print_output)[0]
227
228
[email protected]51ee0072009-06-08 19:20:05229def FilterFlag(args, flag):
230 """Returns True if the flag is present in args list.
231
232 The flag is removed from args if present.
233 """
234 if flag in args:
235 args.remove(flag)
236 return True
237 return False
238
239
[email protected]be0d1ca2009-05-12 19:23:02240class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42241 """Holds information about a changelist.
242
[email protected]32ba2602009-06-06 18:44:48243 name: change name.
244 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
245 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42246 description: the description.
247 files: a list of 2 tuple containing (status, filename) of changed files,
248 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08249 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42250 """
[email protected]32ba2602009-06-06 18:44:48251
252 _SEPARATOR = "\n-----\n"
253 # The info files have the following format:
254 # issue_id, patchset\n (, patchset is optional)
255 # _SEPARATOR\n
256 # filepath1\n
257 # filepath2\n
258 # .
259 # .
260 # filepathn\n
261 # _SEPARATOR\n
262 # description
263
[email protected]ea452b32009-11-22 20:04:31264 def __init__(self, name, issue, patchset, description, files, local_root,
265 needs_upload=False):
[email protected]fb2b8eb2009-04-23 21:03:42266 self.name = name
[email protected]32ba2602009-06-06 18:44:48267 self.issue = int(issue)
268 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42269 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02270 if files is None:
271 files = []
[email protected]17f59f22009-06-12 13:27:24272 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42273 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08274 self._local_root = local_root
[email protected]ea452b32009-11-22 20:04:31275 self.needs_upload = needs_upload
276
277 def NeedsUpload(self):
278 return self.needs_upload
[email protected]fb2b8eb2009-04-23 21:03:42279
[email protected]17f59f22009-06-12 13:27:24280 def GetFileNames(self):
281 """Returns the list of file names included in this change."""
[email protected]e3608df2009-11-10 20:22:57282 return [f[1] for f in self._files]
[email protected]17f59f22009-06-12 13:27:24283
284 def GetFiles(self):
285 """Returns the list of files included in this change with their status."""
286 return self._files
287
288 def GetLocalRoot(self):
289 """Returns the local repository checkout root directory."""
290 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42291
[email protected]f0dfba32009-08-07 22:03:37292 def Exists(self):
293 """Returns True if this change already exists (i.e., is not new)."""
294 return (self.issue or self.description or self._files)
295
[email protected]fb2b8eb2009-04-23 21:03:42296 def _NonDeletedFileList(self):
297 """Returns a list of files in this change, not including deleted files."""
[email protected]e3608df2009-11-10 20:22:57298 return [f[1] for f in self.GetFiles()
299 if not f[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42300
301 def _AddedFileList(self):
302 """Returns a list of files added in this change."""
[email protected]e3608df2009-11-10 20:22:57303 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42304
305 def Save(self):
306 """Writes the changelist information to disk."""
[email protected]ea452b32009-11-22 20:04:31307 if self.NeedsUpload():
308 needs_upload = "dirty"
309 else:
310 needs_upload = "clean"
[email protected]32ba2602009-06-06 18:44:48311 data = ChangeInfo._SEPARATOR.join([
[email protected]ea452b32009-11-22 20:04:31312 "%d, %d, %s" % (self.issue, self.patchset, needs_upload),
[email protected]17f59f22009-06-12 13:27:24313 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48314 self.description])
[email protected]fc83c112009-12-18 15:14:10315 gclient_utils.FileWrite(GetChangelistInfoFile(self.name), data)
[email protected]fb2b8eb2009-04-23 21:03:42316
317 def Delete(self):
318 """Removes the changelist information from disk."""
319 os.remove(GetChangelistInfoFile(self.name))
320
321 def CloseIssue(self):
322 """Closes the Rietveld issue for this changelist."""
323 data = [("description", self.description),]
324 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10325 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42326
327 def UpdateRietveldDescription(self):
328 """Sets the description for an issue on Rietveld."""
329 data = [("description", self.description),]
330 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10331 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42332
333 def MissingTests(self):
334 """Returns True if the change looks like it needs unit tests but has none.
335
336 A change needs unit tests if it contains any new source files or methods.
337 """
338 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
339 # Ignore third_party entirely.
[email protected]e3608df2009-11-10 20:22:57340 files = [f for f in self._NonDeletedFileList()
341 if f.find("third_party") == -1]
342 added_files = [f for f in self._AddedFileList()
343 if f.find("third_party") == -1]
[email protected]fb2b8eb2009-04-23 21:03:42344
345 # If the change is entirely in third_party, we're done.
346 if len(files) == 0:
347 return False
348
349 # Any new or modified test files?
350 # A test file's name ends with "test.*" or "tests.*".
351 test_files = [test for test in files
352 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
353 if len(test_files) > 0:
354 return False
355
356 # Any new source files?
[email protected]e3608df2009-11-10 20:22:57357 source_files = [item for item in added_files
358 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
[email protected]fb2b8eb2009-04-23 21:03:42359 if len(source_files) > 0:
360 return True
361
362 # Do the long test, checking the files for new methods.
363 return self._HasNewMethod()
364
365 def _HasNewMethod(self):
366 """Returns True if the changeset contains any new functions, or if a
367 function signature has been changed.
368
369 A function is identified by starting flush left, containing a "(" before
370 the next flush-left line, and either ending with "{" before the next
371 flush-left line or being followed by an unindented "{".
372
373 Currently this returns True for new methods, new static functions, and
374 methods or functions whose signatures have been changed.
375
376 Inline methods added to header files won't be detected by this. That's
377 acceptable for purposes of determining if a unit test is needed, since
378 inline methods should be trivial.
379 """
380 # To check for methods added to source or header files, we need the diffs.
381 # We'll generate them all, since there aren't likely to be many files
382 # apart from source and headers; besides, we'll want them all if we're
383 # uploading anyway.
384 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24385 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42386
387 definition = ""
388 for line in self.patch.splitlines():
389 if not line.startswith("+"):
390 continue
391 line = line.strip("+").rstrip(" \t")
392 # Skip empty lines, comments, and preprocessor directives.
393 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
394 if line == "" or line.startswith("/") or line.startswith("#"):
395 continue
396
397 # A possible definition ending with "{" is complete, so check it.
398 if definition.endswith("{"):
399 if definition.find("(") != -1:
400 return True
401 definition = ""
402
403 # A { or an indented line, when we're in a definition, continues it.
404 if (definition != "" and
405 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
406 definition += line
407
408 # A flush-left line starts a new possible function definition.
409 elif not line.startswith(" ") and not line.startswith("\t"):
410 definition = line
411
412 return False
413
[email protected]32ba2602009-06-06 18:44:48414 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08415 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48416 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42417
[email protected]32ba2602009-06-06 18:44:48418 Args:
419 fail_on_not_found: if True, this function will quit the program if the
420 changelist doesn't exist.
421 update_status: if True, the svn status will be updated for all the files
422 and unchanged files will be removed.
423
424 Returns: a ChangeInfo object.
425 """
426 info_file = GetChangelistInfoFile(changename)
427 if not os.path.exists(info_file):
428 if fail_on_not_found:
429 ErrorExit("Changelist " + changename + " not found.")
[email protected]ea452b32009-11-22 20:04:31430 return ChangeInfo(changename, 0, 0, '', None, local_root,
431 needs_upload=False)
[email protected]0fca4f32009-12-18 15:14:34432 split_data = gclient_utils.FileRead(info_file, 'r').split(
433 ChangeInfo._SEPARATOR, 2)
[email protected]32ba2602009-06-06 18:44:48434 if len(split_data) != 3:
435 ErrorExit("Changelist file %s is corrupt" % info_file)
[email protected]ea452b32009-11-22 20:04:31436 items = split_data[0].split(', ')
[email protected]32ba2602009-06-06 18:44:48437 issue = 0
438 patchset = 0
[email protected]ea452b32009-11-22 20:04:31439 needs_upload = False
[email protected]32ba2602009-06-06 18:44:48440 if items[0]:
441 issue = int(items[0])
442 if len(items) > 1:
443 patchset = int(items[1])
[email protected]ea452b32009-11-22 20:04:31444 if len(items) > 2:
445 needs_upload = (items[2] == "dirty")
[email protected]32ba2602009-06-06 18:44:48446 files = []
447 for line in split_data[1].splitlines():
448 status = line[:7]
[email protected]e3608df2009-11-10 20:22:57449 filename = line[7:]
450 files.append((status, filename))
[email protected]32ba2602009-06-06 18:44:48451 description = split_data[2]
452 save = False
453 if update_status:
[email protected]e3608df2009-11-10 20:22:57454 for item in files:
455 filename = os.path.join(local_root, item[1])
[email protected]5aeb7dd2009-11-17 18:09:01456 status_result = SVN.CaptureStatus(filename)
[email protected]32ba2602009-06-06 18:44:48457 if not status_result or not status_result[0][0]:
458 # File has been reverted.
459 save = True
[email protected]e3608df2009-11-10 20:22:57460 files.remove(item)
[email protected]32ba2602009-06-06 18:44:48461 continue
462 status = status_result[0][0]
[email protected]e3608df2009-11-10 20:22:57463 if status != item[0]:
[email protected]32ba2602009-06-06 18:44:48464 save = True
[email protected]e3608df2009-11-10 20:22:57465 files[files.index(item)] = (status, item[1])
[email protected]8d5c9a52009-06-12 15:59:08466 change_info = ChangeInfo(changename, issue, patchset, description, files,
[email protected]ea452b32009-11-22 20:04:31467 local_root, needs_upload)
[email protected]32ba2602009-06-06 18:44:48468 if save:
469 change_info.Save()
470 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42471
472
473def GetChangelistInfoFile(changename):
474 """Returns the file that stores information about a changelist."""
475 if not changename or re.search(r'[^\w-]', changename):
476 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52477 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42478
479
[email protected]8d5c9a52009-06-12 15:59:08480def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
481 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42482 """Loads many changes and merge their files list into one pseudo change.
483
484 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
485 """
486 changes = changenames.split(',')
[email protected]ea452b32009-11-22 20:04:31487 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root,
488 needs_upload=False)
[email protected]fb2b8eb2009-04-23 21:03:42489 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08490 aggregate_change_info._files += ChangeInfo.Load(change,
491 local_root,
492 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24493 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42494 return aggregate_change_info
495
496
[email protected]fb2b8eb2009-04-23 21:03:42497def GetCLs():
498 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52499 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42500 if CODEREVIEW_SETTINGS_FILE in cls:
501 cls.remove(CODEREVIEW_SETTINGS_FILE)
502 return cls
503
504
505def GenerateChangeName():
506 """Generate a random changelist name."""
507 random.seed()
508 current_cl_names = GetCLs()
509 while True:
510 cl_name = (random.choice(string.ascii_lowercase) +
511 random.choice(string.digits) +
512 random.choice(string.ascii_lowercase) +
513 random.choice(string.digits))
514 if cl_name not in current_cl_names:
515 return cl_name
516
517
518def GetModifiedFiles():
519 """Returns a set that maps from changelist name to (status,filename) tuples.
520
521 Files not in a changelist have an empty changelist name. Filenames are in
522 relation to the top level directory of the current repository. Note that
523 only the current directory and subdirectories are scanned, in order to
524 improve performance while still being flexible.
525 """
526 files = {}
527
528 # Since the files are normalized to the root folder of the repositary, figure
529 # out what we need to add to the paths.
530 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
531
532 # Get a list of all files in changelists.
533 files_in_cl = {}
534 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08535 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
536 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24537 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42538 files_in_cl[filename] = change_info.name
539
540 # Get all the modified files.
[email protected]5aeb7dd2009-11-17 18:09:01541 status_result = SVN.CaptureStatus(None)
[email protected]207fdf32009-04-28 19:57:01542 for line in status_result:
543 status = line[0]
544 filename = line[1]
545 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42546 continue
[email protected]fb2b8eb2009-04-23 21:03:42547 if dir_prefix:
548 filename = os.path.join(dir_prefix, filename)
549 change_list_name = ""
550 if filename in files_in_cl:
551 change_list_name = files_in_cl[filename]
552 files.setdefault(change_list_name, []).append((status, filename))
553
554 return files
555
556
557def GetFilesNotInCL():
558 """Returns a list of tuples (status,filename) that aren't in any changelists.
559
560 See docstring of GetModifiedFiles for information about path of files and
561 which directories are scanned.
562 """
563 modified_files = GetModifiedFiles()
564 if "" not in modified_files:
565 return []
566 return modified_files[""]
567
568
569def SendToRietveld(request_path, payload=None,
570 content_type="application/octet-stream", timeout=None):
571 """Send a POST/GET to Rietveld. Returns the response body."""
[email protected]3884d762009-11-25 00:01:22572 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
[email protected]fb2b8eb2009-04-23 21:03:42573 def GetUserCredentials():
574 """Prompts the user for a username and password."""
[email protected]3884d762009-11-25 00:01:22575 email = upload.GetEmail("Email (login for uploading to %s)" % server)
[email protected]fb2b8eb2009-04-23 21:03:42576 password = getpass.getpass("Password for %s: " % email)
577 return email, password
[email protected]fb2b8eb2009-04-23 21:03:42578 rpc_server = upload.HttpRpcServer(server,
579 GetUserCredentials,
[email protected]fb2b8eb2009-04-23 21:03:42580 save_cookies=True)
581 try:
582 return rpc_server.Send(request_path, payload, content_type, timeout)
[email protected]e3608df2009-11-10 20:22:57583 except urllib2.URLError:
[email protected]fb2b8eb2009-04-23 21:03:42584 if timeout is None:
585 ErrorExit("Error accessing url %s" % request_path)
586 else:
587 return None
588
589
590def GetIssueDescription(issue):
591 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48592 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42593
594
[email protected]4c22d722010-05-14 19:01:22595def ListFiles(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42596 files = GetModifiedFiles()
597 cl_keys = files.keys()
598 cl_keys.sort()
599 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05600 if not cl_name:
601 continue
602 note = ""
603 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
604 fail_on_not_found=True, update_status=False)
605 if len(change_info.GetFiles()) != len(files[cl_name]):
606 note = " (Note: this changelist contains files outside this directory)"
607 print "\n--- Changelist " + cl_name + note + ":"
[email protected]e3608df2009-11-10 20:22:57608 for filename in files[cl_name]:
609 print "".join(filename)
[email protected]88c32d82009-10-12 18:24:05610 if show_unknown_files:
[email protected]62fd6932010-05-27 13:13:23611 unknown_files = UnknownFiles()
[email protected]88c32d82009-10-12 18:24:05612 if (files.get('') or (show_unknown_files and len(unknown_files))):
613 print "\n--- Not in any changelist:"
[email protected]e3608df2009-11-10 20:22:57614 for item in files.get('', []):
615 print "".join(item)
[email protected]88c32d82009-10-12 18:24:05616 if show_unknown_files:
[email protected]e3608df2009-11-10 20:22:57617 for filename in unknown_files:
618 print "? %s" % filename
[email protected]4c22d722010-05-14 19:01:22619 return 0
[email protected]fb2b8eb2009-04-23 21:03:42620
621
[email protected]fb2b8eb2009-04-23 21:03:42622def GetEditor():
623 editor = os.environ.get("SVN_EDITOR")
624 if not editor:
625 editor = os.environ.get("EDITOR")
626
627 if not editor:
628 if sys.platform.startswith("win"):
629 editor = "notepad"
630 else:
631 editor = "vi"
632
633 return editor
634
635
636def GenerateDiff(files, root=None):
[email protected]f2f9d552009-12-22 00:12:57637 return SVN.GenerateDiff(files, root=root)
[email protected]fb2b8eb2009-04-23 21:03:42638
[email protected]51ee0072009-06-08 19:20:05639
640def OptionallyDoPresubmitChecks(change_info, committing, args):
641 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
642 return True
[email protected]b0dfd352009-06-10 14:12:54643 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05644
645
[email protected]62fd6932010-05-27 13:13:23646def defer_attributes(a, b):
647 """Copy attributes from an object (like a function) to another."""
648 for x in dir(a):
649 if not getattr(b, x, None):
650 setattr(b, x, getattr(a, x))
651
652
[email protected]35fe9ad2010-05-25 23:59:54653def need_change(function):
654 """Converts args -> change_info."""
655 def hook(args):
656 if not len(args) == 1:
657 ErrorExit("You need to pass a change list name")
658 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(), True, True)
659 return function(change_info)
[email protected]62fd6932010-05-27 13:13:23660 defer_attributes(function, hook)
661 hook.need_change = True
662 hook.no_args = True
[email protected]35fe9ad2010-05-25 23:59:54663 return hook
664
665
[email protected]62fd6932010-05-27 13:13:23666def need_change_and_args(function):
667 """Converts args -> change_info."""
668 def hook(args):
[email protected]e56fe822010-05-28 20:36:57669 if not args:
670 ErrorExit("You need to pass a change list name")
[email protected]62fd6932010-05-27 13:13:23671 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
672 return function(change_info, args)
673 defer_attributes(function, hook)
674 hook.need_change = True
675 return hook
676
677
678def no_args(function):
679 """Make sure no args are passed."""
680 def hook(args):
681 if args:
682 ErrorExit("Doesn't support arguments")
683 return function()
684 defer_attributes(function, hook)
685 hook.no_args = True
686 return hook
687
688
689def attrs(**kwargs):
690 """Decorate a function with new attributes."""
691 def decorate(function):
692 for k in kwargs:
693 setattr(function, k, kwargs[k])
694 return function
695 return decorate
696
697
698@no_args
699def CMDopened():
700 """Lists modified files in the current directory down."""
701 return ListFiles(False)
702
703
704@no_args
705def CMDstatus():
706 """Lists modified and unknown files in the current directory down."""
707 return ListFiles(True)
708
709
710@need_change_and_args
[email protected]5db5ba52010-07-20 15:50:47711@attrs(usage='[--no_presubmit] [--clobber] [--no_watchlists]')
[email protected]62fd6932010-05-27 13:13:23712def CMDupload(change_info, args):
713 """Uploads the changelist to the server for review.
714
[email protected]5db5ba52010-07-20 15:50:47715 This does not submit a try job; use gcl try to submit a try job.
[email protected]62fd6932010-05-27 13:13:23716 """
[email protected]17f59f22009-06-12 13:27:24717 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42718 print "Nothing to upload, changelist is empty."
[email protected]35fe9ad2010-05-25 23:59:54719 return 0
[email protected]51ee0072009-06-08 19:20:05720 if not OptionallyDoPresubmitChecks(change_info, False, args):
[email protected]35fe9ad2010-05-25 23:59:54721 return 1
[email protected]62fd6932010-05-27 13:13:23722 no_watchlists = (FilterFlag(args, "--no_watchlists") or
723 FilterFlag(args, "--no-watchlists"))
[email protected]fb2b8eb2009-04-23 21:03:42724
725 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05726 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42727 args.append("--send_mail")
728
729 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05730 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42731
[email protected]fb2b8eb2009-04-23 21:03:42732 upload_arg = ["upload.py", "-y"]
733 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
734 upload_arg.extend(args)
735
736 desc_file = ""
737 if change_info.issue: # Uploading a new patchset.
738 found_message = False
739 for arg in args:
740 if arg.startswith("--message") or arg.startswith("-m"):
741 found_message = True
742 break
743
744 if not found_message:
745 upload_arg.append("--message=''")
746
[email protected]32ba2602009-06-06 18:44:48747 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42748 else: # First time we upload.
749 handle, desc_file = tempfile.mkstemp(text=True)
750 os.write(handle, change_info.description)
751 os.close(handle)
752
[email protected]b2ab4942009-06-11 21:39:19753 # Watchlist processing -- CC people interested in this changeset
754 # http://dev.chromium.org/developers/contributing-code/watchlists
755 if not no_watchlists:
756 import watchlists
[email protected]17f59f22009-06-12 13:27:24757 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08758 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19759
[email protected]fb2b8eb2009-04-23 21:03:42760 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19761 if not no_watchlists and watchers:
[email protected]6e29d572010-06-04 17:32:20762 # Filter out all empty elements and join by ','
763 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42764 if cc_list:
765 upload_arg.append("--cc=" + cc_list)
766 upload_arg.append("--description_file=" + desc_file + "")
767 if change_info.description:
768 subject = change_info.description[:77]
769 if subject.find("\r\n") != -1:
770 subject = subject[:subject.find("\r\n")]
771 if subject.find("\n") != -1:
772 subject = subject[:subject.find("\n")]
773 if len(change_info.description) > 77:
774 subject = subject + "..."
775 upload_arg.append("--message=" + subject)
776
[email protected]83b6e4b2010-03-09 03:16:14777 if GetCodeReviewSetting("PRIVATE") == "True":
778 upload_arg.append("--private")
779
[email protected]fb2b8eb2009-04-23 21:03:42780 # Change the current working directory before calling upload.py so that it
781 # shows the correct base.
782 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24783 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42784
785 # If we have a lot of files with long paths, then we won't be able to fit
786 # the command to "svn diff". Instead, we generate the diff manually for
787 # each file and concatenate them before passing it to upload.py.
788 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24789 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42790 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48791 if issue and patchset:
792 change_info.issue = int(issue)
793 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42794 change_info.Save()
795
796 if desc_file:
797 os.remove(desc_file)
798
799 # Do background work on Rietveld to lint the file so that the results are
800 # ready when the issue is viewed.
801 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
802
[email protected]57e78552009-09-11 23:04:30803 # Move back before considering try, so GetCodeReviewSettings is
804 # consistent.
805 os.chdir(previous_cwd)
806
[email protected]35fe9ad2010-05-25 23:59:54807 return 0
[email protected]fb2b8eb2009-04-23 21:03:42808
[email protected]fb2b8eb2009-04-23 21:03:42809
[email protected]35fe9ad2010-05-25 23:59:54810@need_change
811def CMDpresubmit(change_info):
[email protected]62fd6932010-05-27 13:13:23812 """Runs presubmit checks on the change.
813
814 The actual presubmit code is implemented in presubmit_support.py and looks
815 for PRESUBMIT.py files."""
[email protected]17f59f22009-06-12 13:27:24816 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42817 print "Nothing to presubmit check, changelist is empty."
[email protected]4c22d722010-05-14 19:01:22818 return 0
[email protected]fb2b8eb2009-04-23 21:03:42819
820 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]4c22d722010-05-14 19:01:22821 result = DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42822
[email protected]b0dfd352009-06-10 14:12:54823 print "\n*** Presubmit checks for COMMIT would report: ***"
[email protected]4c22d722010-05-14 19:01:22824 result &= DoPresubmitChecks(change_info, True, False)
825 return not result
[email protected]fb2b8eb2009-04-23 21:03:42826
827
828def TryChange(change_info, args, swallow_exception):
829 """Create a diff file of change_info and send it to the try server."""
830 try:
831 import trychange
832 except ImportError:
833 if swallow_exception:
[email protected]35fe9ad2010-05-25 23:59:54834 return 1
[email protected]fb2b8eb2009-04-23 21:03:42835 ErrorExit("You need to install trychange.py to use the try server.")
836
[email protected]18111352009-12-20 17:21:28837 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42838 if change_info:
[email protected]18111352009-12-20 17:21:28839 trychange_args.extend(['--name', change_info.name])
[email protected]32ba2602009-06-06 18:44:48840 if change_info.issue:
841 trychange_args.extend(["--issue", str(change_info.issue)])
842 if change_info.patchset:
843 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42844 trychange_args.extend(args)
[email protected]1227c7d2009-12-22 00:54:27845 file_list = change_info.GetFileNames()
[email protected]fb2b8eb2009-04-23 21:03:42846 else:
[email protected]18111352009-12-20 17:21:28847 trychange_args.extend(args)
[email protected]1227c7d2009-12-22 00:54:27848 file_list = None
[email protected]d0891922010-05-31 18:33:16849 return trychange.TryChange(
850 trychange_args,
851 file_list=file_list,
852 swallow_exception=swallow_exception,
853 prog='gcl try',
854 extra_epilog='\n'
855 'When called from gcl, use the format gcl try <change_name>.\n')
[email protected]fb2b8eb2009-04-23 21:03:42856
857
[email protected]62fd6932010-05-27 13:13:23858@need_change_and_args
859@attrs(usage='[--no_presubmit]')
[email protected]4357af22010-05-27 15:42:34860def CMDcommit(change_info, args):
[email protected]62fd6932010-05-27 13:13:23861 """Commits the changelist to the repository."""
[email protected]17f59f22009-06-12 13:27:24862 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42863 print "Nothing to commit, changelist is empty."
[email protected]4c22d722010-05-14 19:01:22864 return 1
[email protected]51ee0072009-06-08 19:20:05865 if not OptionallyDoPresubmitChecks(change_info, True, args):
[email protected]4c22d722010-05-14 19:01:22866 return 1
[email protected]fb2b8eb2009-04-23 21:03:42867
[email protected]1bb04aa2009-06-01 17:52:11868 # We face a problem with svn here: Let's say change 'bleh' modifies
869 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
870 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
871 # The only fix is to use --non-recursive but that has its issues too:
872 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
873 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
874 # directory with child nodes". Yay...
875 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42876 if change_info.issue:
877 # Get the latest description from Rietveld.
878 change_info.description = GetIssueDescription(change_info.issue)
879
880 commit_message = change_info.description.replace('\r\n', '\n')
881 if change_info.issue:
[email protected]fcff9272010-04-29 23:56:19882 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
883 if not server.startswith("http://") and not server.startswith("https://"):
884 server = "http://" + server
885 commit_message += ('\nReview URL: %s/%d' % (server, change_info.issue))
[email protected]fb2b8eb2009-04-23 21:03:42886
887 handle, commit_filename = tempfile.mkstemp(text=True)
888 os.write(handle, commit_message)
889 os.close(handle)
890
891 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24892 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42893 os.close(handle)
894
895 commit_cmd += ['--file=' + commit_filename]
896 commit_cmd += ['--targets=' + targets_filename]
897 # Change the current working directory before calling commit.
898 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24899 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42900 output = RunShell(commit_cmd, True)
901 os.remove(commit_filename)
902 os.remove(targets_filename)
903 if output.find("Committed revision") != -1:
904 change_info.Delete()
905
906 if change_info.issue:
907 revision = re.compile(".*?\nCommitted revision (\d+)",
908 re.DOTALL).match(output).group(1)
909 viewvc_url = GetCodeReviewSetting("VIEW_VC")
910 change_info.description = change_info.description + '\n'
911 if viewvc_url:
912 change_info.description += "\nCommitted: " + viewvc_url + revision
913 change_info.CloseIssue()
914 os.chdir(previous_cwd)
[email protected]4c22d722010-05-14 19:01:22915 return 0
[email protected]fb2b8eb2009-04-23 21:03:42916
[email protected]2c8d4b22009-06-06 21:03:10917
[email protected]35fe9ad2010-05-25 23:59:54918def CMDchange(args):
[email protected]62fd6932010-05-27 13:13:23919 """Creates or edits a changelist.
920
921 Only scans the current directory and subdirectories."""
[email protected]35fe9ad2010-05-25 23:59:54922 if len(args) == 0:
923 # Generate a random changelist name.
924 changename = GenerateChangeName()
925 elif args[0] == '--force':
926 changename = GenerateChangeName()
927 else:
928 changename = args[0]
929 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(), False, True)
[email protected]9ce98222009-10-19 20:24:17930 silent = FilterFlag(args, "--silent")
[email protected]d36b3ed2009-11-09 18:51:42931
932 # Verify the user is running the change command from a read-write checkout.
[email protected]5aeb7dd2009-11-17 18:09:01933 svn_info = SVN.CaptureInfo('.')
[email protected]d36b3ed2009-11-09 18:51:42934 if not svn_info:
935 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
936 "directory.")
[email protected]d36b3ed2009-11-09 18:51:42937
[email protected]35fe9ad2010-05-25 23:59:54938 if len(args) == 2:
939 f = open(args[1], 'rU')
[email protected]9ce98222009-10-19 20:24:17940 override_description = f.read()
941 f.close()
942 else:
943 override_description = None
[email protected]5aeb7dd2009-11-17 18:09:01944
[email protected]ea452b32009-11-22 20:04:31945 if change_info.issue and not change_info.NeedsUpload():
[email protected]fb2b8eb2009-04-23 21:03:42946 try:
947 description = GetIssueDescription(change_info.issue)
948 except urllib2.HTTPError, err:
949 if err.code == 404:
950 # The user deleted the issue in Rietveld, so forget the old issue id.
951 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:10952 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:42953 change_info.Save()
954 else:
955 ErrorExit("Error getting the description from Rietveld: " + err)
956 else:
[email protected]85532fc2009-06-04 22:36:53957 if override_description:
958 description = override_description
959 else:
960 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:42961
962 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:23963
[email protected]f0dfba32009-08-07 22:03:37964 # Edited files (as opposed to files with only changed properties) will have
965 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:53966 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:37967 affected_files = [x for x in other_files if file_re.match(x[0])]
968 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:42969
970 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:24971 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:42972 "---Paths in this changelist (" + change_info.name + "):\n")
973 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
974 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:37975 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
976
977 if change_info.Exists():
978 text += (separator2 +
979 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
980 else:
981 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
982 separator2)
983 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:42984
985 handle, filename = tempfile.mkstemp(text=True)
986 os.write(handle, text)
987 os.close(handle)
988
[email protected]9ce98222009-10-19 20:24:17989 if not silent:
990 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:42991
[email protected]0fca4f32009-12-18 15:14:34992 result = gclient_utils.FileRead(filename, 'r')
[email protected]fb2b8eb2009-04-23 21:03:42993 os.remove(filename)
994
995 if not result:
[email protected]4c22d722010-05-14 19:01:22996 return 0
[email protected]fb2b8eb2009-04-23 21:03:42997
998 split_result = result.split(separator1, 1)
999 if len(split_result) != 2:
1000 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1001
[email protected]ea452b32009-11-22 20:04:311002 # Update the CL description if it has changed.
[email protected]fb2b8eb2009-04-23 21:03:421003 new_description = split_result[0]
1004 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531005 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421006 change_info.description = new_description
[email protected]ea452b32009-11-22 20:04:311007 change_info.needs_upload = True
[email protected]fb2b8eb2009-04-23 21:03:421008
1009 new_cl_files = []
1010 for line in cl_files_text.splitlines():
1011 if not len(line):
1012 continue
1013 if line.startswith("---"):
1014 break
1015 status = line[:7]
[email protected]e3608df2009-11-10 20:22:571016 filename = line[7:]
1017 new_cl_files.append((status, filename))
[email protected]bfd09ce2009-08-05 21:17:231018
1019 if (not len(change_info._files)) and (not change_info.issue) and \
1020 (not len(new_description) and (not new_cl_files)):
1021 ErrorExit("Empty changelist not saved")
1022
[email protected]17f59f22009-06-12 13:27:241023 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421024 change_info.Save()
[email protected]53bcf152009-11-13 21:04:101025 if svn_info.get('URL', '').startswith('http:'):
1026 Warn("WARNING: Creating CL in a read-only checkout. You will not be "
1027 "able to commit it!")
1028
[email protected]fb2b8eb2009-04-23 21:03:421029 print change_info.name + " changelist saved."
1030 if change_info.MissingTests():
1031 Warn("WARNING: " + MISSING_TEST_MSG)
1032
[email protected]ea452b32009-11-22 20:04:311033 # Update the Rietveld issue.
1034 if change_info.issue and change_info.NeedsUpload():
1035 change_info.UpdateRietveldDescription()
1036 change_info.needs_upload = False
1037 change_info.Save()
[email protected]4c22d722010-05-14 19:01:221038 return 0
[email protected]ea452b32009-11-22 20:04:311039
1040
[email protected]62fd6932010-05-27 13:13:231041@need_change_and_args
1042def CMDlint(change_info, args):
1043 """Runs cpplint.py on all the files in the change list.
1044
1045 Checks all the files in the changelist for possible style violations.
1046 """
[email protected]fb2b8eb2009-04-23 21:03:421047 try:
1048 import cpplint
1049 except ImportError:
1050 ErrorExit("You need to install cpplint.py to lint C++ files.")
[email protected]fb2b8eb2009-04-23 21:03:421051 # Change the current working directory before calling lint so that it
1052 # shows the correct base.
1053 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241054 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421055
1056 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241057 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421058
[email protected]bb816382009-10-29 01:38:021059 white_list = GetCodeReviewSetting("LINT_REGEX")
1060 if not white_list:
[email protected]e72bb632009-10-29 20:15:481061 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021062 white_regex = re.compile(white_list)
1063 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1064 if not black_list:
[email protected]e72bb632009-10-29 20:15:481065 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021066 black_regex = re.compile(black_list)
[email protected]e3608df2009-11-10 20:22:571067 for filename in filenames:
1068 if white_regex.match(filename):
1069 if black_regex.match(filename):
1070 print "Ignoring file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421071 else:
[email protected]e3608df2009-11-10 20:22:571072 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021073 else:
[email protected]e3608df2009-11-10 20:22:571074 print "Skipping file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421075
1076 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1077 os.chdir(previous_cwd)
[email protected]4c22d722010-05-14 19:01:221078 return 1
[email protected]fb2b8eb2009-04-23 21:03:421079
1080
[email protected]b0dfd352009-06-10 14:12:541081def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421082 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1083 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481084 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151085 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411086 change = presubmit_support.SvnChange(change_info.name,
1087 change_info.description,
1088 change_info.GetLocalRoot(),
1089 change_info.GetFiles(),
1090 change_info.issue,
1091 change_info.patchset)
1092 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541093 committing=committing,
[email protected]1033acc2009-05-13 14:36:481094 verbose=False,
1095 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151096 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541097 default_presubmit=root_presubmit,
1098 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551099 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421100 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1101 return result
1102
1103
[email protected]62fd6932010-05-27 13:13:231104@no_args
1105def CMDchanges():
[email protected]4c22d722010-05-14 19:01:221106 """Lists all the changelists and their files."""
[email protected]fb2b8eb2009-04-23 21:03:421107 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081108 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421109 print "\n--- Changelist " + change_info.name + ":"
[email protected]e3608df2009-11-10 20:22:571110 for filename in change_info.GetFiles():
1111 print "".join(filename)
[email protected]4c22d722010-05-14 19:01:221112 return 0
[email protected]fb2b8eb2009-04-23 21:03:421113
1114
[email protected]62fd6932010-05-27 13:13:231115@no_args
1116def CMDdeleteempties():
[email protected]bfd09ce2009-08-05 21:17:231117 """Delete all changelists that have no files."""
1118 print "\n--- Deleting:"
1119 for cl in GetCLs():
1120 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1121 if not len(change_info._files):
1122 print change_info.name
1123 change_info.Delete()
[email protected]4c22d722010-05-14 19:01:221124 return 0
1125
1126
[email protected]62fd6932010-05-27 13:13:231127@no_args
1128def CMDnothave():
[email protected]4c22d722010-05-14 19:01:221129 """Lists files unknown to Subversion."""
[email protected]62fd6932010-05-27 13:13:231130 for filename in UnknownFiles():
[email protected]4c22d722010-05-14 19:01:221131 print "? " + "".join(filename)
1132 return 0
1133
1134
[email protected]62fd6932010-05-27 13:13:231135@attrs(usage='<svn options>')
[email protected]35fe9ad2010-05-25 23:59:541136def CMDdiff(args):
[email protected]62fd6932010-05-27 13:13:231137 """Diffs all files in the changelist or all files that aren't in a CL."""
[email protected]35fe9ad2010-05-25 23:59:541138 files = None
1139 if args:
[email protected]62fd6932010-05-27 13:13:231140 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
[email protected]35fe9ad2010-05-25 23:59:541141 files = change_info.GetFileNames()
1142 else:
[email protected]707c1482010-06-02 19:52:421143 files = [f[1] for f in GetFilesNotInCL()]
[email protected]38729702010-06-01 23:42:031144
1145 root = GetRepositoryRoot()
1146 cmd = ['svn', 'diff']
1147 cmd.extend([os.path.join(root, x) for x in files])
1148 cmd.extend(args)
1149 return RunShellWithReturnCode(cmd, print_output=True)[1]
[email protected]4c22d722010-05-14 19:01:221150
1151
[email protected]62fd6932010-05-27 13:13:231152@no_args
1153def CMDsettings():
1154 """Prints code review settings for this checkout."""
[email protected]4c22d722010-05-14 19:01:221155 # Force load settings
[email protected]6e29d572010-06-04 17:32:201156 GetCodeReviewSetting("UNKNOWN")
[email protected]4c22d722010-05-14 19:01:221157 del CODEREVIEW_SETTINGS['__just_initialized']
1158 print '\n'.join(("%s: %s" % (str(k), str(v))
1159 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
1160 return 0
1161
1162
[email protected]35fe9ad2010-05-25 23:59:541163@need_change
1164def CMDdescription(change_info):
[email protected]4c22d722010-05-14 19:01:221165 """Prints the description of the specified change to stdout."""
[email protected]4c22d722010-05-14 19:01:221166 print change_info.description
1167 return 0
1168
1169
[email protected]35fe9ad2010-05-25 23:59:541170@need_change
1171def CMDdelete(change_info):
[email protected]4c22d722010-05-14 19:01:221172 """Deletes a changelist."""
[email protected]4c22d722010-05-14 19:01:221173 change_info.Delete()
1174 return 0
1175
1176
[email protected]35fe9ad2010-05-25 23:59:541177def CMDtry(args):
[email protected]62fd6932010-05-27 13:13:231178 """Sends the change to the tryserver to do a test run on your code.
[email protected]4c22d722010-05-14 19:01:221179
1180 To send multiple changes as one path, use a comma-separated list of
1181 changenames. Use 'gcl help try' for more information!"""
1182 # When the change contains no file, send the "changename" positional
1183 # argument to trychange.py.
[email protected]35fe9ad2010-05-25 23:59:541184 # When the command is 'try' and --patchset is used, the patch to try
1185 # is on the Rietveld server.
1186 if not args:
1187 ErrorExit("You need to pass a change list name")
1188 if args[0].find(',') != -1:
1189 change_info = LoadChangelistInfoForMultiple(args[0], GetRepositoryRoot(),
1190 True, True)
1191 else:
1192 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(),
1193 False, True)
[email protected]4c22d722010-05-14 19:01:221194 if change_info.GetFiles():
[email protected]35fe9ad2010-05-25 23:59:541195 args = args[1:]
[email protected]4c22d722010-05-14 19:01:221196 else:
1197 change_info = None
[email protected]35fe9ad2010-05-25 23:59:541198 return TryChange(change_info, args, swallow_exception=False)
[email protected]4c22d722010-05-14 19:01:221199
1200
[email protected]62fd6932010-05-27 13:13:231201@attrs(usage='<old-name> <new-name>')
[email protected]35fe9ad2010-05-25 23:59:541202def CMDrename(args):
[email protected]4c22d722010-05-14 19:01:221203 """Renames an existing change."""
[email protected]35fe9ad2010-05-25 23:59:541204 if len(args) != 2:
[email protected]4c22d722010-05-14 19:01:221205 ErrorExit("Usage: gcl rename <old-name> <new-name>.")
[email protected]35fe9ad2010-05-25 23:59:541206 src, dst = args
[email protected]4c22d722010-05-14 19:01:221207 src_file = GetChangelistInfoFile(src)
1208 if not os.path.isfile(src_file):
1209 ErrorExit("Change '%s' does not exist." % src)
1210 dst_file = GetChangelistInfoFile(dst)
1211 if os.path.isfile(dst_file):
1212 ErrorExit("Change '%s' already exists; pick a new name." % dst)
1213 os.rename(src_file, dst_file)
1214 print "Change '%s' renamed '%s'." % (src, dst)
1215 return 0
[email protected]bfd09ce2009-08-05 21:17:231216
1217
[email protected]35fe9ad2010-05-25 23:59:541218def CMDpassthru(args):
[email protected]62fd6932010-05-27 13:13:231219 """Everything else that is passed into gcl we redirect to svn.
1220
1221 It assumes a change list name is passed and is converted with the files names.
1222 """
[email protected]35fe9ad2010-05-25 23:59:541223 args = ["svn", args[0]]
1224 if len(args) > 1:
1225 root = GetRepositoryRoot()
1226 change_info = ChangeInfo.Load(args[1], root, True, True)
1227 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
1228 return RunShellWithReturnCode(args, print_output=True)[1]
1229
1230
[email protected]62fd6932010-05-27 13:13:231231def Command(name):
1232 return getattr(sys.modules[__name__], 'CMD' + name, None)
1233
1234
1235def GenUsage(command):
1236 """Modify an OptParse object with the function's documentation."""
1237 obj = Command(command)
1238 display = command
1239 more = getattr(obj, 'usage', '')
1240 if command == 'help':
1241 display = '<command>'
1242 need_change = ''
1243 if getattr(obj, 'need_change', None):
1244 need_change = ' <change_list>'
1245 options = ' [options]'
1246 if getattr(obj, 'no_args', None):
1247 options = ''
1248 res = 'Usage: gcl %s%s%s %s\n\n' % (display, need_change, options, more)
1249 res += re.sub('\n ', '\n', obj.__doc__)
1250 return res
1251
1252
1253def CMDhelp(args):
1254 """Prints this help or help for the given command."""
1255 if args and 'CMD' + args[0] in dir(sys.modules[__name__]):
1256 print GenUsage(args[0])
1257
1258 # These commands defer to external tools so give this info too.
1259 if args[0] == 'try':
1260 TryChange(None, ['--help'], swallow_exception=False)
1261 if args[0] == 'upload':
1262 upload.RealMain(['upload.py', '--help'])
1263 return 0
1264
1265 print GenUsage('help')
1266 print sys.modules[__name__].__doc__
1267 print 'version ' + __version__ + '\n'
1268
1269 print('Commands are:\n' + '\n'.join([
1270 ' %-12s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1271 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1272 return 0
1273
1274
[email protected]35fe9ad2010-05-25 23:59:541275def main(argv):
[email protected]c68f9cb2010-06-17 20:34:181276 if not argv:
1277 argv = ['help']
1278 command = Command(argv[0])
1279 # Help can be run from anywhere.
1280 if command == CMDhelp:
1281 return command(argv[1:])
1282
[email protected]a05be0b2009-06-30 19:13:021283 try:
[email protected]62fd6932010-05-27 13:13:231284 GetRepositoryRoot()
[email protected]5f3eee32009-09-17 00:34:301285 except gclient_utils.Error:
[email protected]62fd6932010-05-27 13:13:231286 print('To use gcl, you need to be in a subversion checkout.')
1287 return 1
1288
1289 # Create the directories where we store information about changelists if it
1290 # doesn't exist.
[email protected]807c4462010-07-10 00:45:281291 try:
1292 if not os.path.exists(GetInfoDir()):
1293 os.mkdir(GetInfoDir())
1294 if not os.path.exists(GetChangesDir()):
1295 os.mkdir(GetChangesDir())
1296 if not os.path.exists(GetCacheDir()):
1297 os.mkdir(GetCacheDir())
[email protected]fb2b8eb2009-04-23 21:03:421298
[email protected]807c4462010-07-10 00:45:281299 if command:
1300 return command(argv[1:])
1301 # Unknown command, try to pass that to svn
1302 return CMDpassthru(argv)
1303 except gclient_utils.Error, e:
1304 print('Got an exception')
1305 print(str(e))
[email protected]fb2b8eb2009-04-23 21:03:421306
1307if __name__ == "__main__":
[email protected]35fe9ad2010-05-25 23:59:541308 sys.exit(main(sys.argv[1:]))