blob: 804f322f70f67ad39fb6119f301ab76ac72b32c0 [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
6"""Wrapper script around Rietveld's upload.py that groups files into
7changelists."""
[email protected]fb2b8eb2009-04-23 21:03:428
9import getpass
10import os
11import random
12import re
[email protected]374d65e2009-05-21 14:00:5213import shutil
[email protected]fb2b8eb2009-04-23 21:03:4214import string
15import subprocess
16import sys
17import tempfile
[email protected]ba551772010-02-03 18:21:4218from third_party import upload
[email protected]fb2b8eb2009-04-23 21:03:4219import urllib2
20
[email protected]ada4c652009-12-03 15:32:0121import breakpad
22
[email protected]46a94102009-05-12 20:32:4323# gcl now depends on gclient.
[email protected]5aeb7dd2009-11-17 18:09:0124from scm import SVN
[email protected]5f3eee32009-09-17 00:34:3025import gclient_utils
[email protected]c1675e22009-04-27 20:30:4826
[email protected]18111352009-12-20 17:21:2827__version__ = '1.1.3'
[email protected]c1675e22009-04-27 20:30:4828
29
[email protected]fb2b8eb2009-04-23 21:03:4230CODEREVIEW_SETTINGS = {
[email protected]172b6e72010-01-26 00:35:0331 # Ideally, we want to set |CODE_REVIEW_SERVER| to a generic server like
32 # codereview.appspot.com and remove |CC_LIST| and |VIEW_VC|. In practice, we
33 # need these settings so developers making changes in directories such as
34 # Chromium's src/third_party/WebKit will send change lists to the correct
35 # server.
36 #
37 # To make gcl send reviews to a different server, check in a file named
38 # "codereview.settings" (see |CODEREVIEW_SETTINGS_FILE| below) to your
39 # project's base directory and add the following line to codereview.settings:
40 # CODE_REVIEW_SERVER: codereview.yourserver.org
41 #
[email protected]fb2b8eb2009-04-23 21:03:4242 # Default values.
[email protected]172b6e72010-01-26 00:35:0343 "CODE_REVIEW_SERVER": "codereview.chromium.org",
44 "CC_LIST": "[email protected]",
45 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
[email protected]fb2b8eb2009-04-23 21:03:4246}
47
[email protected]fb2b8eb2009-04-23 21:03:4248# globals that store the root of the current repository and the directory where
49# we store information about changelists.
[email protected]98fc2b92009-05-21 14:11:5150REPOSITORY_ROOT = ""
[email protected]fb2b8eb2009-04-23 21:03:4251
52# Filename where we store repository specific information for gcl.
53CODEREVIEW_SETTINGS_FILE = "codereview.settings"
54
55# Warning message when the change appears to be missing tests.
56MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
57
[email protected]98fc2b92009-05-21 14:11:5158# Global cache of files cached in GetCacheDir().
59FILES_CACHE = {}
[email protected]fb2b8eb2009-04-23 21:03:4260
[email protected]e5299012010-04-07 18:02:2661def CheckHomeForFile(filename):
62 """Checks the users home dir for the existence of the given file. Returns
63 the path to the file if it's there, or None if it is not.
64 """
65 home_vars = ['HOME']
66 if sys.platform in ('cygwin', 'win32'):
67 home_vars.append('USERPROFILE')
68 for home_var in home_vars:
69 home = os.getenv(home_var)
70 if home != None:
71 full_path = os.path.join(home, filename)
72 if os.path.exists(full_path):
73 return full_path
74 return None
[email protected]fb2b8eb2009-04-23 21:03:4275
[email protected]207fdf32009-04-28 19:57:0176def UnknownFiles(extra_args):
[email protected]5aeb7dd2009-11-17 18:09:0177 """Runs svn status and returns unknown files.
[email protected]207fdf32009-04-28 19:57:0178
79 Any args in |extra_args| are passed to the tool to support giving alternate
80 code locations.
81 """
[email protected]5aeb7dd2009-11-17 18:09:0182 return [item[1] for item in SVN.CaptureStatus(extra_args)
[email protected]4810a962009-05-12 21:03:3483 if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0184
85
[email protected]fb2b8eb2009-04-23 21:03:4286def GetRepositoryRoot():
87 """Returns the top level directory of the current repository.
88
89 The directory is returned as an absolute path.
90 """
[email protected]98fc2b92009-05-21 14:11:5191 global REPOSITORY_ROOT
92 if not REPOSITORY_ROOT:
[email protected]94b1ee92009-12-19 20:27:2093 REPOSITORY_ROOT = SVN.GetCheckoutRoot(os.getcwd())
94 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:3095 raise gclient_utils.Error("gcl run outside of repository")
[email protected]98fc2b92009-05-21 14:11:5196 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:4297
98
99def GetInfoDir():
100 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:52101 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
102
103
104def GetChangesDir():
105 """Returns the directory where gcl change files are stored."""
106 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:42107
108
[email protected]98fc2b92009-05-21 14:11:51109def GetCacheDir():
110 """Returns the directory where gcl change files are stored."""
111 return os.path.join(GetInfoDir(), 'cache')
112
113
114def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
115 """Retrieves a file from the repository and caches it in GetCacheDir() for
116 max_age seconds.
117
118 use_root: If False, look up the arborescence for the first match, otherwise go
119 directory to the root repository.
120
121 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:02122 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:51123 """
124 global FILES_CACHE
125 if filename not in FILES_CACHE:
126 # Don't try to look up twice.
127 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28128 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02129 try:
130 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]5f3eee32009-09-17 00:34:30131 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:02132 return None
[email protected]98fc2b92009-05-21 14:11:51133 if (not os.path.exists(cached_file) or
134 os.stat(cached_file).st_mtime > max_age):
[email protected]5aeb7dd2009-11-17 18:09:01135 dir_info = SVN.CaptureInfo(".")
[email protected]9b613272009-04-24 01:28:28136 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51137 if use_root:
138 url_path = repo_root
139 else:
140 url_path = dir_info["URL"]
141 content = ""
[email protected]9b613272009-04-24 01:28:28142 while True:
[email protected]fa44e4a2009-12-03 01:41:13143 # Look in the repository at the current level for the file.
144 svn_path = url_path + "/" + filename
145 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
[email protected]9b613272009-04-24 01:28:28146 if not rc:
[email protected]98fc2b92009-05-21 14:11:51147 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28148 break
149 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51150 content = ""
[email protected]9b613272009-04-24 01:28:28151 if url_path == repo_root:
152 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51153 break
[email protected]9b613272009-04-24 01:28:28154 # Go up one level to try again.
155 url_path = os.path.dirname(url_path)
[email protected]9b613272009-04-24 01:28:28156 # Write a cached version even if there isn't a file, so we don't try to
157 # fetch it each time.
[email protected]fc83c112009-12-18 15:14:10158 gclient_utils.FileWrite(cached_file, content)
[email protected]98fc2b92009-05-21 14:11:51159 else:
[email protected]0fca4f32009-12-18 15:14:34160 content = gclient_utils.FileRead(cached_file, 'r')
[email protected]98fc2b92009-05-21 14:11:51161 # Keep the content cached in memory.
162 FILES_CACHE[filename] = content
163 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28164
[email protected]98fc2b92009-05-21 14:11:51165
166def GetCodeReviewSetting(key):
167 """Returns a value for the given key for this repository."""
168 # Use '__just_initialized' as a flag to determine if the settings were
169 # already initialized.
170 global CODEREVIEW_SETTINGS
171 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47172 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
173 if settings_file:
174 for line in settings_file.splitlines():
175 if not line or line.startswith("#"):
176 continue
177 k, v = line.split(": ", 1)
178 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51179 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42180 return CODEREVIEW_SETTINGS.get(key, "")
181
182
[email protected]fb2b8eb2009-04-23 21:03:42183def Warn(msg):
184 ErrorExit(msg, exit=False)
185
186
187def ErrorExit(msg, exit=True):
188 """Print an error message to stderr and optionally exit."""
189 print >>sys.stderr, msg
190 if exit:
191 sys.exit(1)
192
193
194def RunShellWithReturnCode(command, print_output=False):
195 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02196 # Use a shell for subcommands on Windows to get a PATH search, and because svn
197 # may be a batch file.
198 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42199 p = subprocess.Popen(command, stdout=subprocess.PIPE,
200 stderr=subprocess.STDOUT, shell=use_shell,
201 universal_newlines=True)
202 if print_output:
203 output_array = []
204 while True:
205 line = p.stdout.readline()
206 if not line:
207 break
208 if print_output:
209 print line.strip('\n')
210 output_array.append(line)
211 output = "".join(output_array)
212 else:
213 output = p.stdout.read()
214 p.wait()
215 p.stdout.close()
216 return output, p.returncode
217
218
219def RunShell(command, print_output=False):
220 """Executes a command and returns the output."""
221 return RunShellWithReturnCode(command, print_output)[0]
222
223
[email protected]51ee0072009-06-08 19:20:05224def FilterFlag(args, flag):
225 """Returns True if the flag is present in args list.
226
227 The flag is removed from args if present.
228 """
229 if flag in args:
230 args.remove(flag)
231 return True
232 return False
233
234
[email protected]be0d1ca2009-05-12 19:23:02235class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42236 """Holds information about a changelist.
237
[email protected]32ba2602009-06-06 18:44:48238 name: change name.
239 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
240 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42241 description: the description.
242 files: a list of 2 tuple containing (status, filename) of changed files,
243 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08244 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42245 """
[email protected]32ba2602009-06-06 18:44:48246
247 _SEPARATOR = "\n-----\n"
248 # The info files have the following format:
249 # issue_id, patchset\n (, patchset is optional)
250 # _SEPARATOR\n
251 # filepath1\n
252 # filepath2\n
253 # .
254 # .
255 # filepathn\n
256 # _SEPARATOR\n
257 # description
258
[email protected]ea452b32009-11-22 20:04:31259 def __init__(self, name, issue, patchset, description, files, local_root,
260 needs_upload=False):
[email protected]fb2b8eb2009-04-23 21:03:42261 self.name = name
[email protected]32ba2602009-06-06 18:44:48262 self.issue = int(issue)
263 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42264 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02265 if files is None:
266 files = []
[email protected]17f59f22009-06-12 13:27:24267 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42268 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08269 self._local_root = local_root
[email protected]ea452b32009-11-22 20:04:31270 self.needs_upload = needs_upload
271
272 def NeedsUpload(self):
273 return self.needs_upload
[email protected]fb2b8eb2009-04-23 21:03:42274
[email protected]17f59f22009-06-12 13:27:24275 def GetFileNames(self):
276 """Returns the list of file names included in this change."""
[email protected]e3608df2009-11-10 20:22:57277 return [f[1] for f in self._files]
[email protected]17f59f22009-06-12 13:27:24278
279 def GetFiles(self):
280 """Returns the list of files included in this change with their status."""
281 return self._files
282
283 def GetLocalRoot(self):
284 """Returns the local repository checkout root directory."""
285 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42286
[email protected]f0dfba32009-08-07 22:03:37287 def Exists(self):
288 """Returns True if this change already exists (i.e., is not new)."""
289 return (self.issue or self.description or self._files)
290
[email protected]fb2b8eb2009-04-23 21:03:42291 def _NonDeletedFileList(self):
292 """Returns a list of files in this change, not including deleted files."""
[email protected]e3608df2009-11-10 20:22:57293 return [f[1] for f in self.GetFiles()
294 if not f[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42295
296 def _AddedFileList(self):
297 """Returns a list of files added in this change."""
[email protected]e3608df2009-11-10 20:22:57298 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42299
300 def Save(self):
301 """Writes the changelist information to disk."""
[email protected]ea452b32009-11-22 20:04:31302 if self.NeedsUpload():
303 needs_upload = "dirty"
304 else:
305 needs_upload = "clean"
[email protected]32ba2602009-06-06 18:44:48306 data = ChangeInfo._SEPARATOR.join([
[email protected]ea452b32009-11-22 20:04:31307 "%d, %d, %s" % (self.issue, self.patchset, needs_upload),
[email protected]17f59f22009-06-12 13:27:24308 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48309 self.description])
[email protected]fc83c112009-12-18 15:14:10310 gclient_utils.FileWrite(GetChangelistInfoFile(self.name), data)
[email protected]fb2b8eb2009-04-23 21:03:42311
312 def Delete(self):
313 """Removes the changelist information from disk."""
314 os.remove(GetChangelistInfoFile(self.name))
315
316 def CloseIssue(self):
317 """Closes the Rietveld issue for this changelist."""
318 data = [("description", self.description),]
319 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10320 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42321
322 def UpdateRietveldDescription(self):
323 """Sets the description for an issue on Rietveld."""
324 data = [("description", self.description),]
325 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10326 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42327
328 def MissingTests(self):
329 """Returns True if the change looks like it needs unit tests but has none.
330
331 A change needs unit tests if it contains any new source files or methods.
332 """
333 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
334 # Ignore third_party entirely.
[email protected]e3608df2009-11-10 20:22:57335 files = [f for f in self._NonDeletedFileList()
336 if f.find("third_party") == -1]
337 added_files = [f for f in self._AddedFileList()
338 if f.find("third_party") == -1]
[email protected]fb2b8eb2009-04-23 21:03:42339
340 # If the change is entirely in third_party, we're done.
341 if len(files) == 0:
342 return False
343
344 # Any new or modified test files?
345 # A test file's name ends with "test.*" or "tests.*".
346 test_files = [test for test in files
347 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
348 if len(test_files) > 0:
349 return False
350
351 # Any new source files?
[email protected]e3608df2009-11-10 20:22:57352 source_files = [item for item in added_files
353 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
[email protected]fb2b8eb2009-04-23 21:03:42354 if len(source_files) > 0:
355 return True
356
357 # Do the long test, checking the files for new methods.
358 return self._HasNewMethod()
359
360 def _HasNewMethod(self):
361 """Returns True if the changeset contains any new functions, or if a
362 function signature has been changed.
363
364 A function is identified by starting flush left, containing a "(" before
365 the next flush-left line, and either ending with "{" before the next
366 flush-left line or being followed by an unindented "{".
367
368 Currently this returns True for new methods, new static functions, and
369 methods or functions whose signatures have been changed.
370
371 Inline methods added to header files won't be detected by this. That's
372 acceptable for purposes of determining if a unit test is needed, since
373 inline methods should be trivial.
374 """
375 # To check for methods added to source or header files, we need the diffs.
376 # We'll generate them all, since there aren't likely to be many files
377 # apart from source and headers; besides, we'll want them all if we're
378 # uploading anyway.
379 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24380 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42381
382 definition = ""
383 for line in self.patch.splitlines():
384 if not line.startswith("+"):
385 continue
386 line = line.strip("+").rstrip(" \t")
387 # Skip empty lines, comments, and preprocessor directives.
388 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
389 if line == "" or line.startswith("/") or line.startswith("#"):
390 continue
391
392 # A possible definition ending with "{" is complete, so check it.
393 if definition.endswith("{"):
394 if definition.find("(") != -1:
395 return True
396 definition = ""
397
398 # A { or an indented line, when we're in a definition, continues it.
399 if (definition != "" and
400 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
401 definition += line
402
403 # A flush-left line starts a new possible function definition.
404 elif not line.startswith(" ") and not line.startswith("\t"):
405 definition = line
406
407 return False
408
[email protected]32ba2602009-06-06 18:44:48409 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08410 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48411 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42412
[email protected]32ba2602009-06-06 18:44:48413 Args:
414 fail_on_not_found: if True, this function will quit the program if the
415 changelist doesn't exist.
416 update_status: if True, the svn status will be updated for all the files
417 and unchanged files will be removed.
418
419 Returns: a ChangeInfo object.
420 """
421 info_file = GetChangelistInfoFile(changename)
422 if not os.path.exists(info_file):
423 if fail_on_not_found:
424 ErrorExit("Changelist " + changename + " not found.")
[email protected]ea452b32009-11-22 20:04:31425 return ChangeInfo(changename, 0, 0, '', None, local_root,
426 needs_upload=False)
[email protected]0fca4f32009-12-18 15:14:34427 split_data = gclient_utils.FileRead(info_file, 'r').split(
428 ChangeInfo._SEPARATOR, 2)
[email protected]32ba2602009-06-06 18:44:48429 if len(split_data) != 3:
430 ErrorExit("Changelist file %s is corrupt" % info_file)
[email protected]ea452b32009-11-22 20:04:31431 items = split_data[0].split(', ')
[email protected]32ba2602009-06-06 18:44:48432 issue = 0
433 patchset = 0
[email protected]ea452b32009-11-22 20:04:31434 needs_upload = False
[email protected]32ba2602009-06-06 18:44:48435 if items[0]:
436 issue = int(items[0])
437 if len(items) > 1:
438 patchset = int(items[1])
[email protected]ea452b32009-11-22 20:04:31439 if len(items) > 2:
440 needs_upload = (items[2] == "dirty")
[email protected]32ba2602009-06-06 18:44:48441 files = []
442 for line in split_data[1].splitlines():
443 status = line[:7]
[email protected]e3608df2009-11-10 20:22:57444 filename = line[7:]
445 files.append((status, filename))
[email protected]32ba2602009-06-06 18:44:48446 description = split_data[2]
447 save = False
448 if update_status:
[email protected]e3608df2009-11-10 20:22:57449 for item in files:
450 filename = os.path.join(local_root, item[1])
[email protected]5aeb7dd2009-11-17 18:09:01451 status_result = SVN.CaptureStatus(filename)
[email protected]32ba2602009-06-06 18:44:48452 if not status_result or not status_result[0][0]:
453 # File has been reverted.
454 save = True
[email protected]e3608df2009-11-10 20:22:57455 files.remove(item)
[email protected]32ba2602009-06-06 18:44:48456 continue
457 status = status_result[0][0]
[email protected]e3608df2009-11-10 20:22:57458 if status != item[0]:
[email protected]32ba2602009-06-06 18:44:48459 save = True
[email protected]e3608df2009-11-10 20:22:57460 files[files.index(item)] = (status, item[1])
[email protected]8d5c9a52009-06-12 15:59:08461 change_info = ChangeInfo(changename, issue, patchset, description, files,
[email protected]ea452b32009-11-22 20:04:31462 local_root, needs_upload)
[email protected]32ba2602009-06-06 18:44:48463 if save:
464 change_info.Save()
465 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42466
467
468def GetChangelistInfoFile(changename):
469 """Returns the file that stores information about a changelist."""
470 if not changename or re.search(r'[^\w-]', changename):
471 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52472 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42473
474
[email protected]8d5c9a52009-06-12 15:59:08475def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
476 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42477 """Loads many changes and merge their files list into one pseudo change.
478
479 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
480 """
481 changes = changenames.split(',')
[email protected]ea452b32009-11-22 20:04:31482 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root,
483 needs_upload=False)
[email protected]fb2b8eb2009-04-23 21:03:42484 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08485 aggregate_change_info._files += ChangeInfo.Load(change,
486 local_root,
487 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24488 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42489 return aggregate_change_info
490
491
[email protected]fb2b8eb2009-04-23 21:03:42492def GetCLs():
493 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52494 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42495 if CODEREVIEW_SETTINGS_FILE in cls:
496 cls.remove(CODEREVIEW_SETTINGS_FILE)
497 return cls
498
499
500def GenerateChangeName():
501 """Generate a random changelist name."""
502 random.seed()
503 current_cl_names = GetCLs()
504 while True:
505 cl_name = (random.choice(string.ascii_lowercase) +
506 random.choice(string.digits) +
507 random.choice(string.ascii_lowercase) +
508 random.choice(string.digits))
509 if cl_name not in current_cl_names:
510 return cl_name
511
512
513def GetModifiedFiles():
514 """Returns a set that maps from changelist name to (status,filename) tuples.
515
516 Files not in a changelist have an empty changelist name. Filenames are in
517 relation to the top level directory of the current repository. Note that
518 only the current directory and subdirectories are scanned, in order to
519 improve performance while still being flexible.
520 """
521 files = {}
522
523 # Since the files are normalized to the root folder of the repositary, figure
524 # out what we need to add to the paths.
525 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
526
527 # Get a list of all files in changelists.
528 files_in_cl = {}
529 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08530 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
531 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24532 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42533 files_in_cl[filename] = change_info.name
534
535 # Get all the modified files.
[email protected]5aeb7dd2009-11-17 18:09:01536 status_result = SVN.CaptureStatus(None)
[email protected]207fdf32009-04-28 19:57:01537 for line in status_result:
538 status = line[0]
539 filename = line[1]
540 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42541 continue
[email protected]fb2b8eb2009-04-23 21:03:42542 if dir_prefix:
543 filename = os.path.join(dir_prefix, filename)
544 change_list_name = ""
545 if filename in files_in_cl:
546 change_list_name = files_in_cl[filename]
547 files.setdefault(change_list_name, []).append((status, filename))
548
549 return files
550
551
552def GetFilesNotInCL():
553 """Returns a list of tuples (status,filename) that aren't in any changelists.
554
555 See docstring of GetModifiedFiles for information about path of files and
556 which directories are scanned.
557 """
558 modified_files = GetModifiedFiles()
559 if "" not in modified_files:
560 return []
561 return modified_files[""]
562
563
564def SendToRietveld(request_path, payload=None,
565 content_type="application/octet-stream", timeout=None):
566 """Send a POST/GET to Rietveld. Returns the response body."""
[email protected]3884d762009-11-25 00:01:22567 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
[email protected]fb2b8eb2009-04-23 21:03:42568 def GetUserCredentials():
569 """Prompts the user for a username and password."""
[email protected]3884d762009-11-25 00:01:22570 email = upload.GetEmail("Email (login for uploading to %s)" % server)
[email protected]fb2b8eb2009-04-23 21:03:42571 password = getpass.getpass("Password for %s: " % email)
572 return email, password
[email protected]fb2b8eb2009-04-23 21:03:42573 rpc_server = upload.HttpRpcServer(server,
574 GetUserCredentials,
575 host_override=server,
576 save_cookies=True)
577 try:
578 return rpc_server.Send(request_path, payload, content_type, timeout)
[email protected]e3608df2009-11-10 20:22:57579 except urllib2.URLError:
[email protected]fb2b8eb2009-04-23 21:03:42580 if timeout is None:
581 ErrorExit("Error accessing url %s" % request_path)
582 else:
583 return None
584
585
586def GetIssueDescription(issue):
587 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48588 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42589
590
[email protected]88c32d82009-10-12 18:24:05591def Opened(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42592 """Prints a list of modified files in the current directory down."""
593 files = GetModifiedFiles()
594 cl_keys = files.keys()
595 cl_keys.sort()
596 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05597 if not cl_name:
598 continue
599 note = ""
600 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
601 fail_on_not_found=True, update_status=False)
602 if len(change_info.GetFiles()) != len(files[cl_name]):
603 note = " (Note: this changelist contains files outside this directory)"
604 print "\n--- Changelist " + cl_name + note + ":"
[email protected]e3608df2009-11-10 20:22:57605 for filename in files[cl_name]:
606 print "".join(filename)
[email protected]88c32d82009-10-12 18:24:05607 if show_unknown_files:
608 unknown_files = UnknownFiles([])
609 if (files.get('') or (show_unknown_files and len(unknown_files))):
610 print "\n--- Not in any changelist:"
[email protected]e3608df2009-11-10 20:22:57611 for item in files.get('', []):
612 print "".join(item)
[email protected]88c32d82009-10-12 18:24:05613 if show_unknown_files:
[email protected]e3608df2009-11-10 20:22:57614 for filename in unknown_files:
615 print "? %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:42616
617
618def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53619 if argv:
620 if argv[0] == 'try':
621 TryChange(None, ['--help'], swallow_exception=False)
622 return
623 if argv[0] == 'upload':
624 upload.RealMain(['upload.py', '--help'])
625 return
[email protected]fb2b8eb2009-04-23 21:03:42626
627 print (
628"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48629version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42630
631Basic commands:
632-----------------------------------------
633 gcl change change_name
634 Add/remove files to a changelist. Only scans the current directory and
635 subdirectories.
636
637 gcl upload change_name [-r [email protected],[email protected],...]
638 [--send_mail] [--no_try] [--no_presubmit]
[email protected]b2ab4942009-06-11 21:39:19639 [--no_watchlists]
[email protected]fb2b8eb2009-04-23 21:03:42640 Uploads the changelist to the server for review.
[email protected]e5299012010-04-07 18:02:26641 (You can create the file '.gcl_upload_no_try' in your home dir to
642 skip the automatic tries.)
[email protected]fb2b8eb2009-04-23 21:03:42643
[email protected]3b217f52009-06-01 17:54:20644 gcl commit change_name [--no_presubmit]
[email protected]fb2b8eb2009-04-23 21:03:42645 Commits the changelist to the repository.
646
647 gcl lint change_name
648 Check all the files in the changelist for possible style violations.
649
650Advanced commands:
651-----------------------------------------
652 gcl delete change_name
653 Deletes a changelist.
654
655 gcl diff change_name
656 Diffs all files in the changelist.
657
658 gcl presubmit change_name
659 Runs presubmit checks without uploading the changelist.
660
661 gcl diff
662 Diffs all files in the current directory and subdirectories that aren't in
663 a changelist.
664
665 gcl changes
666 Lists all the the changelists and the files in them.
667
668 gcl nothave [optional directory]
669 Lists files unknown to Subversion.
670
671 gcl opened
672 Lists modified files in the current directory and subdirectories.
673
674 gcl settings
675 Print the code review settings for this directory.
676
677 gcl status
678 Lists modified and unknown files in the current directory and
679 subdirectories.
680
681 gcl try change_name
682 Sends the change to the tryserver so a trybot can do a test run on your
683 code. To send multiple changes as one path, use a comma-separated list
684 of changenames.
685 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53686
[email protected]bfd09ce2009-08-05 21:17:23687 gcl deleteempties
688 Deletes all changelists that have no files associated with them. Careful,
689 you can lose your descriptions.
690
[email protected]3bcc6ce2009-05-12 22:53:53691 gcl help [command]
692 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42693""")
694
695def GetEditor():
696 editor = os.environ.get("SVN_EDITOR")
697 if not editor:
698 editor = os.environ.get("EDITOR")
699
700 if not editor:
701 if sys.platform.startswith("win"):
702 editor = "notepad"
703 else:
704 editor = "vi"
705
706 return editor
707
708
709def GenerateDiff(files, root=None):
[email protected]f2f9d552009-12-22 00:12:57710 return SVN.GenerateDiff(files, root=root)
[email protected]fb2b8eb2009-04-23 21:03:42711
[email protected]51ee0072009-06-08 19:20:05712
713def OptionallyDoPresubmitChecks(change_info, committing, args):
714 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
715 return True
[email protected]b0dfd352009-06-10 14:12:54716 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05717
718
[email protected]fb2b8eb2009-04-23 21:03:42719def UploadCL(change_info, args):
[email protected]17f59f22009-06-12 13:27:24720 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42721 print "Nothing to upload, changelist is empty."
722 return
[email protected]51ee0072009-06-08 19:20:05723 if not OptionallyDoPresubmitChecks(change_info, False, args):
724 return
[email protected]e5299012010-04-07 18:02:26725 # Might want to support GetInfoDir()/GetRepositoryRoot() like
726 # CheckHomeForFile() so the skip of tries can be per tree basis instead
727 # of user global.
728 no_try = FilterFlag(args, "--no_try") or \
729 FilterFlag(args, "--no-try") or \
730 not (CheckHomeForFile(".gcl_upload_no_try") is None)
[email protected]b2ab4942009-06-11 21:39:19731 no_watchlists = FilterFlag(args, "--no_watchlists") or \
732 FilterFlag(args, "--no-watchlists")
[email protected]fb2b8eb2009-04-23 21:03:42733
734 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05735 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42736 args.append("--send_mail")
737
738 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05739 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42740
[email protected]003c2692009-05-20 13:08:08741 # Disable try when the server is overridden.
742 server_1 = re.compile(r"^-s\b.*")
743 server_2 = re.compile(r"^--server\b.*")
744 for arg in args:
745 if server_1.match(arg) or server_2.match(arg):
746 no_try = True
747 break
[email protected]fb2b8eb2009-04-23 21:03:42748
749 upload_arg = ["upload.py", "-y"]
750 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
751 upload_arg.extend(args)
752
753 desc_file = ""
754 if change_info.issue: # Uploading a new patchset.
755 found_message = False
756 for arg in args:
757 if arg.startswith("--message") or arg.startswith("-m"):
758 found_message = True
759 break
760
761 if not found_message:
762 upload_arg.append("--message=''")
763
[email protected]32ba2602009-06-06 18:44:48764 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42765 else: # First time we upload.
766 handle, desc_file = tempfile.mkstemp(text=True)
767 os.write(handle, change_info.description)
768 os.close(handle)
769
[email protected]b2ab4942009-06-11 21:39:19770 # Watchlist processing -- CC people interested in this changeset
771 # http://dev.chromium.org/developers/contributing-code/watchlists
772 if not no_watchlists:
773 import watchlists
[email protected]17f59f22009-06-12 13:27:24774 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08775 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19776
[email protected]fb2b8eb2009-04-23 21:03:42777 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19778 if not no_watchlists and watchers:
779 # Filter out all empty elements and join by ','
780 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42781 if cc_list:
782 upload_arg.append("--cc=" + cc_list)
783 upload_arg.append("--description_file=" + desc_file + "")
784 if change_info.description:
785 subject = change_info.description[:77]
786 if subject.find("\r\n") != -1:
787 subject = subject[:subject.find("\r\n")]
788 if subject.find("\n") != -1:
789 subject = subject[:subject.find("\n")]
790 if len(change_info.description) > 77:
791 subject = subject + "..."
792 upload_arg.append("--message=" + subject)
793
[email protected]83b6e4b2010-03-09 03:16:14794 if GetCodeReviewSetting("PRIVATE") == "True":
795 upload_arg.append("--private")
796
[email protected]fb2b8eb2009-04-23 21:03:42797 # Change the current working directory before calling upload.py so that it
798 # shows the correct base.
799 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24800 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42801
802 # If we have a lot of files with long paths, then we won't be able to fit
803 # the command to "svn diff". Instead, we generate the diff manually for
804 # each file and concatenate them before passing it to upload.py.
805 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24806 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42807 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48808 if issue and patchset:
809 change_info.issue = int(issue)
810 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42811 change_info.Save()
812
813 if desc_file:
814 os.remove(desc_file)
815
816 # Do background work on Rietveld to lint the file so that the results are
817 # ready when the issue is viewed.
818 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
819
[email protected]57e78552009-09-11 23:04:30820 # Move back before considering try, so GetCodeReviewSettings is
821 # consistent.
822 os.chdir(previous_cwd)
823
[email protected]fb2b8eb2009-04-23 21:03:42824 # Once uploaded to Rietveld, send it to the try server.
825 if not no_try:
826 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
827 if try_on_upload and try_on_upload.lower() == 'true':
[email protected]32ba2602009-06-06 18:44:48828 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42829 if clobber:
[email protected]32ba2602009-06-06 18:44:48830 trychange_args.append('--clobber')
831 TryChange(change_info, trychange_args, swallow_exception=True)
[email protected]fb2b8eb2009-04-23 21:03:42832
[email protected]fb2b8eb2009-04-23 21:03:42833
834
835def PresubmitCL(change_info):
836 """Reports what presubmit checks on the change would report."""
[email protected]17f59f22009-06-12 13:27:24837 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42838 print "Nothing to presubmit check, changelist is empty."
839 return
840
841 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]b0dfd352009-06-10 14:12:54842 DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42843
[email protected]b0dfd352009-06-10 14:12:54844 print "\n*** Presubmit checks for COMMIT would report: ***"
845 DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42846
847
848def TryChange(change_info, args, swallow_exception):
849 """Create a diff file of change_info and send it to the try server."""
850 try:
851 import trychange
852 except ImportError:
853 if swallow_exception:
854 return
855 ErrorExit("You need to install trychange.py to use the try server.")
856
[email protected]18111352009-12-20 17:21:28857 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42858 if change_info:
[email protected]18111352009-12-20 17:21:28859 trychange_args.extend(['--name', change_info.name])
[email protected]32ba2602009-06-06 18:44:48860 if change_info.issue:
861 trychange_args.extend(["--issue", str(change_info.issue)])
862 if change_info.patchset:
863 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42864 trychange_args.extend(args)
[email protected]1227c7d2009-12-22 00:54:27865 file_list = change_info.GetFileNames()
[email protected]fb2b8eb2009-04-23 21:03:42866 else:
[email protected]18111352009-12-20 17:21:28867 trychange_args.extend(args)
[email protected]1227c7d2009-12-22 00:54:27868 file_list = None
869 trychange.TryChange(trychange_args,
870 file_list=file_list,
871 swallow_exception=swallow_exception,
872 prog='gcl try')
[email protected]fb2b8eb2009-04-23 21:03:42873
874
875def Commit(change_info, args):
[email protected]17f59f22009-06-12 13:27:24876 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42877 print "Nothing to commit, changelist is empty."
878 return
[email protected]51ee0072009-06-08 19:20:05879 if not OptionallyDoPresubmitChecks(change_info, True, args):
880 return
[email protected]fb2b8eb2009-04-23 21:03:42881
[email protected]1bb04aa2009-06-01 17:52:11882 # We face a problem with svn here: Let's say change 'bleh' modifies
883 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
884 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
885 # The only fix is to use --non-recursive but that has its issues too:
886 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
887 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
888 # directory with child nodes". Yay...
889 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42890 if change_info.issue:
891 # Get the latest description from Rietveld.
892 change_info.description = GetIssueDescription(change_info.issue)
893
894 commit_message = change_info.description.replace('\r\n', '\n')
895 if change_info.issue:
[email protected]32ba2602009-06-06 18:44:48896 commit_message += ('\nReview URL: http://%s/%d' %
[email protected]fb2b8eb2009-04-23 21:03:42897 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
898 change_info.issue))
899
900 handle, commit_filename = tempfile.mkstemp(text=True)
901 os.write(handle, commit_message)
902 os.close(handle)
903
904 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24905 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42906 os.close(handle)
907
908 commit_cmd += ['--file=' + commit_filename]
909 commit_cmd += ['--targets=' + targets_filename]
910 # Change the current working directory before calling commit.
911 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24912 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42913 output = RunShell(commit_cmd, True)
914 os.remove(commit_filename)
915 os.remove(targets_filename)
916 if output.find("Committed revision") != -1:
917 change_info.Delete()
918
919 if change_info.issue:
920 revision = re.compile(".*?\nCommitted revision (\d+)",
921 re.DOTALL).match(output).group(1)
922 viewvc_url = GetCodeReviewSetting("VIEW_VC")
923 change_info.description = change_info.description + '\n'
924 if viewvc_url:
925 change_info.description += "\nCommitted: " + viewvc_url + revision
926 change_info.CloseIssue()
927 os.chdir(previous_cwd)
928
[email protected]2c8d4b22009-06-06 21:03:10929
[email protected]9ce98222009-10-19 20:24:17930def Change(change_info, args):
[email protected]fb2b8eb2009-04-23 21:03:42931 """Creates/edits a changelist."""
[email protected]9ce98222009-10-19 20:24:17932 silent = FilterFlag(args, "--silent")
[email protected]d36b3ed2009-11-09 18:51:42933
934 # Verify the user is running the change command from a read-write checkout.
[email protected]5aeb7dd2009-11-17 18:09:01935 svn_info = SVN.CaptureInfo('.')
[email protected]d36b3ed2009-11-09 18:51:42936 if not svn_info:
937 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
938 "directory.")
[email protected]d36b3ed2009-11-09 18:51:42939
[email protected]9ce98222009-10-19 20:24:17940 if (len(args) == 1):
941 filename = args[0]
942 f = open(filename, 'rU')
943 override_description = f.read()
944 f.close()
945 else:
946 override_description = None
[email protected]5aeb7dd2009-11-17 18:09:01947
[email protected]ea452b32009-11-22 20:04:31948 if change_info.issue and not change_info.NeedsUpload():
[email protected]fb2b8eb2009-04-23 21:03:42949 try:
950 description = GetIssueDescription(change_info.issue)
951 except urllib2.HTTPError, err:
952 if err.code == 404:
953 # The user deleted the issue in Rietveld, so forget the old issue id.
954 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:10955 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:42956 change_info.Save()
957 else:
958 ErrorExit("Error getting the description from Rietveld: " + err)
959 else:
[email protected]85532fc2009-06-04 22:36:53960 if override_description:
961 description = override_description
962 else:
963 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:42964
965 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:23966
[email protected]f0dfba32009-08-07 22:03:37967 # Edited files (as opposed to files with only changed properties) will have
968 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:53969 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:37970 affected_files = [x for x in other_files if file_re.match(x[0])]
971 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:42972
973 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:24974 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:42975 "---Paths in this changelist (" + change_info.name + "):\n")
976 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
977 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:37978 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
979
980 if change_info.Exists():
981 text += (separator2 +
982 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
983 else:
984 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
985 separator2)
986 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:42987
988 handle, filename = tempfile.mkstemp(text=True)
989 os.write(handle, text)
990 os.close(handle)
991
[email protected]9ce98222009-10-19 20:24:17992 if not silent:
993 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:42994
[email protected]0fca4f32009-12-18 15:14:34995 result = gclient_utils.FileRead(filename, 'r')
[email protected]fb2b8eb2009-04-23 21:03:42996 os.remove(filename)
997
998 if not result:
999 return
1000
1001 split_result = result.split(separator1, 1)
1002 if len(split_result) != 2:
1003 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1004
[email protected]ea452b32009-11-22 20:04:311005 # Update the CL description if it has changed.
[email protected]fb2b8eb2009-04-23 21:03:421006 new_description = split_result[0]
1007 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531008 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421009 change_info.description = new_description
[email protected]ea452b32009-11-22 20:04:311010 change_info.needs_upload = True
[email protected]fb2b8eb2009-04-23 21:03:421011
1012 new_cl_files = []
1013 for line in cl_files_text.splitlines():
1014 if not len(line):
1015 continue
1016 if line.startswith("---"):
1017 break
1018 status = line[:7]
[email protected]e3608df2009-11-10 20:22:571019 filename = line[7:]
1020 new_cl_files.append((status, filename))
[email protected]bfd09ce2009-08-05 21:17:231021
1022 if (not len(change_info._files)) and (not change_info.issue) and \
1023 (not len(new_description) and (not new_cl_files)):
1024 ErrorExit("Empty changelist not saved")
1025
[email protected]17f59f22009-06-12 13:27:241026 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421027 change_info.Save()
[email protected]53bcf152009-11-13 21:04:101028 if svn_info.get('URL', '').startswith('http:'):
1029 Warn("WARNING: Creating CL in a read-only checkout. You will not be "
1030 "able to commit it!")
1031
[email protected]fb2b8eb2009-04-23 21:03:421032 print change_info.name + " changelist saved."
1033 if change_info.MissingTests():
1034 Warn("WARNING: " + MISSING_TEST_MSG)
1035
[email protected]ea452b32009-11-22 20:04:311036 # Update the Rietveld issue.
1037 if change_info.issue and change_info.NeedsUpload():
1038 change_info.UpdateRietveldDescription()
1039 change_info.needs_upload = False
1040 change_info.Save()
1041
1042
[email protected]fb2b8eb2009-04-23 21:03:421043# Valid extensions for files we want to lint.
[email protected]e72bb632009-10-29 20:15:481044DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
[email protected]296df662010-02-12 22:38:371045DEFAULT_LINT_IGNORE_REGEX = r"$^"
[email protected]fb2b8eb2009-04-23 21:03:421046
1047def Lint(change_info, args):
1048 """Runs cpplint.py on all the files in |change_info|"""
1049 try:
1050 import cpplint
1051 except ImportError:
1052 ErrorExit("You need to install cpplint.py to lint C++ files.")
1053
1054 # Change the current working directory before calling lint so that it
1055 # shows the correct base.
1056 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241057 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421058
1059 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241060 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421061
[email protected]bb816382009-10-29 01:38:021062 white_list = GetCodeReviewSetting("LINT_REGEX")
1063 if not white_list:
[email protected]e72bb632009-10-29 20:15:481064 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021065 white_regex = re.compile(white_list)
1066 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1067 if not black_list:
[email protected]e72bb632009-10-29 20:15:481068 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021069 black_regex = re.compile(black_list)
[email protected]e3608df2009-11-10 20:22:571070 for filename in filenames:
1071 if white_regex.match(filename):
1072 if black_regex.match(filename):
1073 print "Ignoring file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421074 else:
[email protected]e3608df2009-11-10 20:22:571075 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021076 else:
[email protected]e3608df2009-11-10 20:22:571077 print "Skipping file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421078
1079 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1080 os.chdir(previous_cwd)
1081
1082
[email protected]b0dfd352009-06-10 14:12:541083def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421084 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1085 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481086 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151087 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411088 change = presubmit_support.SvnChange(change_info.name,
1089 change_info.description,
1090 change_info.GetLocalRoot(),
1091 change_info.GetFiles(),
1092 change_info.issue,
1093 change_info.patchset)
1094 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541095 committing=committing,
[email protected]1033acc2009-05-13 14:36:481096 verbose=False,
1097 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151098 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541099 default_presubmit=root_presubmit,
1100 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551101 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421102 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1103 return result
1104
1105
1106def Changes():
1107 """Print all the changelists and their files."""
1108 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081109 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421110 print "\n--- Changelist " + change_info.name + ":"
[email protected]e3608df2009-11-10 20:22:571111 for filename in change_info.GetFiles():
1112 print "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421113
1114
[email protected]bfd09ce2009-08-05 21:17:231115def DeleteEmptyChangeLists():
1116 """Delete all changelists that have no files."""
1117 print "\n--- Deleting:"
1118 for cl in GetCLs():
1119 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1120 if not len(change_info._files):
1121 print change_info.name
1122 change_info.Delete()
1123
1124
[email protected]fb2b8eb2009-04-23 21:03:421125def main(argv=None):
1126 if argv is None:
1127 argv = sys.argv
1128
1129 if len(argv) == 1:
1130 Help()
1131 return 0;
1132
[email protected]a05be0b2009-06-30 19:13:021133 try:
1134 # Create the directories where we store information about changelists if it
1135 # doesn't exist.
1136 if not os.path.exists(GetInfoDir()):
1137 os.mkdir(GetInfoDir())
1138 if not os.path.exists(GetChangesDir()):
1139 os.mkdir(GetChangesDir())
[email protected]a05be0b2009-06-30 19:13:021140 if not os.path.exists(GetCacheDir()):
1141 os.mkdir(GetCacheDir())
[email protected]5f3eee32009-09-17 00:34:301142 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:021143 # Will throw an exception if not run in a svn checkout.
1144 pass
[email protected]fb2b8eb2009-04-23 21:03:421145
1146 # Commands that don't require an argument.
1147 command = argv[1]
[email protected]88c32d82009-10-12 18:24:051148 if command == "opened" or command == "status":
1149 Opened(command == "status")
[email protected]fb2b8eb2009-04-23 21:03:421150 return 0
1151 if command == "nothave":
[email protected]e3608df2009-11-10 20:22:571152 __pychecker__ = 'no-returnvalues'
1153 for filename in UnknownFiles(argv[2:]):
1154 print "? " + "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421155 return 0
1156 if command == "changes":
1157 Changes()
1158 return 0
1159 if command == "help":
1160 Help(argv[2:])
1161 return 0
1162 if command == "diff" and len(argv) == 2:
1163 files = GetFilesNotInCL()
1164 print GenerateDiff([x[1] for x in files])
1165 return 0
1166 if command == "settings":
[email protected]e3608df2009-11-10 20:22:571167 # Force load settings
1168 GetCodeReviewSetting("UNKNOWN");
[email protected]a005ccd2009-06-12 13:25:541169 del CODEREVIEW_SETTINGS['__just_initialized']
1170 print '\n'.join(("%s: %s" % (str(k), str(v))
1171 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
[email protected]fb2b8eb2009-04-23 21:03:421172 return 0
[email protected]bfd09ce2009-08-05 21:17:231173 if command == "deleteempties":
1174 DeleteEmptyChangeLists()
1175 return 0
[email protected]fb2b8eb2009-04-23 21:03:421176
[email protected]a31d6582009-11-10 13:55:131177 if command == "change":
1178 if len(argv) == 2:
[email protected]fb2b8eb2009-04-23 21:03:421179 # Generate a random changelist name.
1180 changename = GenerateChangeName()
[email protected]a31d6582009-11-10 13:55:131181 elif argv[2] == '--force':
1182 changename = GenerateChangeName()
1183 # argv[3:] is passed to Change() as |args| later. Change() should receive
1184 # |args| which includes '--force'.
1185 argv.insert(2, changename)
[email protected]fb2b8eb2009-04-23 21:03:421186 else:
[email protected]a31d6582009-11-10 13:55:131187 changename = argv[2]
1188 elif len(argv) == 2:
1189 ErrorExit("Need a changelist name.")
[email protected]fb2b8eb2009-04-23 21:03:421190 else:
1191 changename = argv[2]
1192
1193 # When the command is 'try' and --patchset is used, the patch to try
1194 # is on the Rietveld server. 'change' creates a change so it's fine if the
1195 # change didn't exist. All other commands require an existing change.
1196 fail_on_not_found = command != "try" and command != "change"
1197 if command == "try" and changename.find(',') != -1:
[email protected]8d5c9a52009-06-12 15:59:081198 change_info = LoadChangelistInfoForMultiple(changename, GetRepositoryRoot(),
1199 True, True)
[email protected]fb2b8eb2009-04-23 21:03:421200 else:
[email protected]8d5c9a52009-06-12 15:59:081201 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(),
1202 fail_on_not_found, True)
[email protected]fb2b8eb2009-04-23 21:03:421203
1204 if command == "change":
[email protected]9ce98222009-10-19 20:24:171205 Change(change_info, argv[3:])
[email protected]fb2b8eb2009-04-23 21:03:421206 elif command == "lint":
1207 Lint(change_info, argv[3:])
1208 elif command == "upload":
1209 UploadCL(change_info, argv[3:])
1210 elif command == "presubmit":
1211 PresubmitCL(change_info)
1212 elif command in ("commit", "submit"):
1213 Commit(change_info, argv[3:])
1214 elif command == "delete":
1215 change_info.Delete()
1216 elif command == "try":
1217 # When the change contains no file, send the "changename" positional
1218 # argument to trychange.py.
[email protected]17f59f22009-06-12 13:27:241219 if change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421220 args = argv[3:]
1221 else:
1222 change_info = None
1223 args = argv[2:]
1224 TryChange(change_info, args, swallow_exception=False)
1225 else:
1226 # Everything else that is passed into gcl we redirect to svn, after adding
1227 # the files. This allows commands such as 'gcl diff xxx' to work.
[email protected]f40fbb32009-11-10 13:54:311228 if command == "diff" and not change_info.GetFileNames():
1229 return 0
[email protected]fb2b8eb2009-04-23 21:03:421230 args =["svn", command]
1231 root = GetRepositoryRoot()
[email protected]17f59f22009-06-12 13:27:241232 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
[email protected]fb2b8eb2009-04-23 21:03:421233 RunShell(args, True)
1234 return 0
1235
1236
1237if __name__ == "__main__":
1238 sys.exit(main())