blob: 8b149319008445bfcd8c6054094e3ea0c846beec [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
18import upload
19import 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
61
[email protected]207fdf32009-04-28 19:57:0162def UnknownFiles(extra_args):
[email protected]5aeb7dd2009-11-17 18:09:0163 """Runs svn status and returns unknown files.
[email protected]207fdf32009-04-28 19:57:0164
65 Any args in |extra_args| are passed to the tool to support giving alternate
66 code locations.
67 """
[email protected]5aeb7dd2009-11-17 18:09:0168 return [item[1] for item in SVN.CaptureStatus(extra_args)
[email protected]4810a962009-05-12 21:03:3469 if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0170
71
[email protected]fb2b8eb2009-04-23 21:03:4272def GetRepositoryRoot():
73 """Returns the top level directory of the current repository.
74
75 The directory is returned as an absolute path.
76 """
[email protected]98fc2b92009-05-21 14:11:5177 global REPOSITORY_ROOT
78 if not REPOSITORY_ROOT:
[email protected]94b1ee92009-12-19 20:27:2079 REPOSITORY_ROOT = SVN.GetCheckoutRoot(os.getcwd())
80 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:3081 raise gclient_utils.Error("gcl run outside of repository")
[email protected]98fc2b92009-05-21 14:11:5182 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:4283
84
85def GetInfoDir():
86 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:5287 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
88
89
90def GetChangesDir():
91 """Returns the directory where gcl change files are stored."""
92 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:4293
94
[email protected]98fc2b92009-05-21 14:11:5195def GetCacheDir():
96 """Returns the directory where gcl change files are stored."""
97 return os.path.join(GetInfoDir(), 'cache')
98
99
100def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
101 """Retrieves a file from the repository and caches it in GetCacheDir() for
102 max_age seconds.
103
104 use_root: If False, look up the arborescence for the first match, otherwise go
105 directory to the root repository.
106
107 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:02108 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:51109 """
110 global FILES_CACHE
111 if filename not in FILES_CACHE:
112 # Don't try to look up twice.
113 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28114 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02115 try:
116 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]5f3eee32009-09-17 00:34:30117 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:02118 return None
[email protected]98fc2b92009-05-21 14:11:51119 if (not os.path.exists(cached_file) or
120 os.stat(cached_file).st_mtime > max_age):
[email protected]5aeb7dd2009-11-17 18:09:01121 dir_info = SVN.CaptureInfo(".")
[email protected]9b613272009-04-24 01:28:28122 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51123 if use_root:
124 url_path = repo_root
125 else:
126 url_path = dir_info["URL"]
127 content = ""
[email protected]9b613272009-04-24 01:28:28128 while True:
[email protected]fa44e4a2009-12-03 01:41:13129 # Look in the repository at the current level for the file.
130 svn_path = url_path + "/" + filename
131 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
[email protected]9b613272009-04-24 01:28:28132 if not rc:
[email protected]98fc2b92009-05-21 14:11:51133 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28134 break
135 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51136 content = ""
[email protected]9b613272009-04-24 01:28:28137 if url_path == repo_root:
138 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51139 break
[email protected]9b613272009-04-24 01:28:28140 # Go up one level to try again.
141 url_path = os.path.dirname(url_path)
[email protected]9b613272009-04-24 01:28:28142 # Write a cached version even if there isn't a file, so we don't try to
143 # fetch it each time.
[email protected]fc83c112009-12-18 15:14:10144 gclient_utils.FileWrite(cached_file, content)
[email protected]98fc2b92009-05-21 14:11:51145 else:
[email protected]0fca4f32009-12-18 15:14:34146 content = gclient_utils.FileRead(cached_file, 'r')
[email protected]98fc2b92009-05-21 14:11:51147 # Keep the content cached in memory.
148 FILES_CACHE[filename] = content
149 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28150
[email protected]98fc2b92009-05-21 14:11:51151
152def GetCodeReviewSetting(key):
153 """Returns a value for the given key for this repository."""
154 # Use '__just_initialized' as a flag to determine if the settings were
155 # already initialized.
156 global CODEREVIEW_SETTINGS
157 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47158 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
159 if settings_file:
160 for line in settings_file.splitlines():
161 if not line or line.startswith("#"):
162 continue
163 k, v = line.split(": ", 1)
164 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51165 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42166 return CODEREVIEW_SETTINGS.get(key, "")
167
168
[email protected]fb2b8eb2009-04-23 21:03:42169def Warn(msg):
170 ErrorExit(msg, exit=False)
171
172
173def ErrorExit(msg, exit=True):
174 """Print an error message to stderr and optionally exit."""
175 print >>sys.stderr, msg
176 if exit:
177 sys.exit(1)
178
179
180def RunShellWithReturnCode(command, print_output=False):
181 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02182 # Use a shell for subcommands on Windows to get a PATH search, and because svn
183 # may be a batch file.
184 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42185 p = subprocess.Popen(command, stdout=subprocess.PIPE,
186 stderr=subprocess.STDOUT, shell=use_shell,
187 universal_newlines=True)
188 if print_output:
189 output_array = []
190 while True:
191 line = p.stdout.readline()
192 if not line:
193 break
194 if print_output:
195 print line.strip('\n')
196 output_array.append(line)
197 output = "".join(output_array)
198 else:
199 output = p.stdout.read()
200 p.wait()
201 p.stdout.close()
202 return output, p.returncode
203
204
205def RunShell(command, print_output=False):
206 """Executes a command and returns the output."""
207 return RunShellWithReturnCode(command, print_output)[0]
208
209
[email protected]51ee0072009-06-08 19:20:05210def FilterFlag(args, flag):
211 """Returns True if the flag is present in args list.
212
213 The flag is removed from args if present.
214 """
215 if flag in args:
216 args.remove(flag)
217 return True
218 return False
219
220
[email protected]be0d1ca2009-05-12 19:23:02221class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42222 """Holds information about a changelist.
223
[email protected]32ba2602009-06-06 18:44:48224 name: change name.
225 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
226 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42227 description: the description.
228 files: a list of 2 tuple containing (status, filename) of changed files,
229 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08230 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42231 """
[email protected]32ba2602009-06-06 18:44:48232
233 _SEPARATOR = "\n-----\n"
234 # The info files have the following format:
235 # issue_id, patchset\n (, patchset is optional)
236 # _SEPARATOR\n
237 # filepath1\n
238 # filepath2\n
239 # .
240 # .
241 # filepathn\n
242 # _SEPARATOR\n
243 # description
244
[email protected]ea452b32009-11-22 20:04:31245 def __init__(self, name, issue, patchset, description, files, local_root,
246 needs_upload=False):
[email protected]fb2b8eb2009-04-23 21:03:42247 self.name = name
[email protected]32ba2602009-06-06 18:44:48248 self.issue = int(issue)
249 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42250 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02251 if files is None:
252 files = []
[email protected]17f59f22009-06-12 13:27:24253 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42254 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08255 self._local_root = local_root
[email protected]ea452b32009-11-22 20:04:31256 self.needs_upload = needs_upload
257
258 def NeedsUpload(self):
259 return self.needs_upload
[email protected]fb2b8eb2009-04-23 21:03:42260
[email protected]17f59f22009-06-12 13:27:24261 def GetFileNames(self):
262 """Returns the list of file names included in this change."""
[email protected]e3608df2009-11-10 20:22:57263 return [f[1] for f in self._files]
[email protected]17f59f22009-06-12 13:27:24264
265 def GetFiles(self):
266 """Returns the list of files included in this change with their status."""
267 return self._files
268
269 def GetLocalRoot(self):
270 """Returns the local repository checkout root directory."""
271 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42272
[email protected]f0dfba32009-08-07 22:03:37273 def Exists(self):
274 """Returns True if this change already exists (i.e., is not new)."""
275 return (self.issue or self.description or self._files)
276
[email protected]fb2b8eb2009-04-23 21:03:42277 def _NonDeletedFileList(self):
278 """Returns a list of files in this change, not including deleted files."""
[email protected]e3608df2009-11-10 20:22:57279 return [f[1] for f in self.GetFiles()
280 if not f[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42281
282 def _AddedFileList(self):
283 """Returns a list of files added in this change."""
[email protected]e3608df2009-11-10 20:22:57284 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42285
286 def Save(self):
287 """Writes the changelist information to disk."""
[email protected]ea452b32009-11-22 20:04:31288 if self.NeedsUpload():
289 needs_upload = "dirty"
290 else:
291 needs_upload = "clean"
[email protected]32ba2602009-06-06 18:44:48292 data = ChangeInfo._SEPARATOR.join([
[email protected]ea452b32009-11-22 20:04:31293 "%d, %d, %s" % (self.issue, self.patchset, needs_upload),
[email protected]17f59f22009-06-12 13:27:24294 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48295 self.description])
[email protected]fc83c112009-12-18 15:14:10296 gclient_utils.FileWrite(GetChangelistInfoFile(self.name), data)
[email protected]fb2b8eb2009-04-23 21:03:42297
298 def Delete(self):
299 """Removes the changelist information from disk."""
300 os.remove(GetChangelistInfoFile(self.name))
301
302 def CloseIssue(self):
303 """Closes the Rietveld issue for this changelist."""
304 data = [("description", self.description),]
305 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10306 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42307
308 def UpdateRietveldDescription(self):
309 """Sets the description for an issue on Rietveld."""
310 data = [("description", self.description),]
311 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10312 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42313
314 def MissingTests(self):
315 """Returns True if the change looks like it needs unit tests but has none.
316
317 A change needs unit tests if it contains any new source files or methods.
318 """
319 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
320 # Ignore third_party entirely.
[email protected]e3608df2009-11-10 20:22:57321 files = [f for f in self._NonDeletedFileList()
322 if f.find("third_party") == -1]
323 added_files = [f for f in self._AddedFileList()
324 if f.find("third_party") == -1]
[email protected]fb2b8eb2009-04-23 21:03:42325
326 # If the change is entirely in third_party, we're done.
327 if len(files) == 0:
328 return False
329
330 # Any new or modified test files?
331 # A test file's name ends with "test.*" or "tests.*".
332 test_files = [test for test in files
333 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
334 if len(test_files) > 0:
335 return False
336
337 # Any new source files?
[email protected]e3608df2009-11-10 20:22:57338 source_files = [item for item in added_files
339 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
[email protected]fb2b8eb2009-04-23 21:03:42340 if len(source_files) > 0:
341 return True
342
343 # Do the long test, checking the files for new methods.
344 return self._HasNewMethod()
345
346 def _HasNewMethod(self):
347 """Returns True if the changeset contains any new functions, or if a
348 function signature has been changed.
349
350 A function is identified by starting flush left, containing a "(" before
351 the next flush-left line, and either ending with "{" before the next
352 flush-left line or being followed by an unindented "{".
353
354 Currently this returns True for new methods, new static functions, and
355 methods or functions whose signatures have been changed.
356
357 Inline methods added to header files won't be detected by this. That's
358 acceptable for purposes of determining if a unit test is needed, since
359 inline methods should be trivial.
360 """
361 # To check for methods added to source or header files, we need the diffs.
362 # We'll generate them all, since there aren't likely to be many files
363 # apart from source and headers; besides, we'll want them all if we're
364 # uploading anyway.
365 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24366 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42367
368 definition = ""
369 for line in self.patch.splitlines():
370 if not line.startswith("+"):
371 continue
372 line = line.strip("+").rstrip(" \t")
373 # Skip empty lines, comments, and preprocessor directives.
374 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
375 if line == "" or line.startswith("/") or line.startswith("#"):
376 continue
377
378 # A possible definition ending with "{" is complete, so check it.
379 if definition.endswith("{"):
380 if definition.find("(") != -1:
381 return True
382 definition = ""
383
384 # A { or an indented line, when we're in a definition, continues it.
385 if (definition != "" and
386 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
387 definition += line
388
389 # A flush-left line starts a new possible function definition.
390 elif not line.startswith(" ") and not line.startswith("\t"):
391 definition = line
392
393 return False
394
[email protected]32ba2602009-06-06 18:44:48395 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08396 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48397 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42398
[email protected]32ba2602009-06-06 18:44:48399 Args:
400 fail_on_not_found: if True, this function will quit the program if the
401 changelist doesn't exist.
402 update_status: if True, the svn status will be updated for all the files
403 and unchanged files will be removed.
404
405 Returns: a ChangeInfo object.
406 """
407 info_file = GetChangelistInfoFile(changename)
408 if not os.path.exists(info_file):
409 if fail_on_not_found:
410 ErrorExit("Changelist " + changename + " not found.")
[email protected]ea452b32009-11-22 20:04:31411 return ChangeInfo(changename, 0, 0, '', None, local_root,
412 needs_upload=False)
[email protected]0fca4f32009-12-18 15:14:34413 split_data = gclient_utils.FileRead(info_file, 'r').split(
414 ChangeInfo._SEPARATOR, 2)
[email protected]32ba2602009-06-06 18:44:48415 if len(split_data) != 3:
416 ErrorExit("Changelist file %s is corrupt" % info_file)
[email protected]ea452b32009-11-22 20:04:31417 items = split_data[0].split(', ')
[email protected]32ba2602009-06-06 18:44:48418 issue = 0
419 patchset = 0
[email protected]ea452b32009-11-22 20:04:31420 needs_upload = False
[email protected]32ba2602009-06-06 18:44:48421 if items[0]:
422 issue = int(items[0])
423 if len(items) > 1:
424 patchset = int(items[1])
[email protected]ea452b32009-11-22 20:04:31425 if len(items) > 2:
426 needs_upload = (items[2] == "dirty")
[email protected]32ba2602009-06-06 18:44:48427 files = []
428 for line in split_data[1].splitlines():
429 status = line[:7]
[email protected]e3608df2009-11-10 20:22:57430 filename = line[7:]
431 files.append((status, filename))
[email protected]32ba2602009-06-06 18:44:48432 description = split_data[2]
433 save = False
434 if update_status:
[email protected]e3608df2009-11-10 20:22:57435 for item in files:
436 filename = os.path.join(local_root, item[1])
[email protected]5aeb7dd2009-11-17 18:09:01437 status_result = SVN.CaptureStatus(filename)
[email protected]32ba2602009-06-06 18:44:48438 if not status_result or not status_result[0][0]:
439 # File has been reverted.
440 save = True
[email protected]e3608df2009-11-10 20:22:57441 files.remove(item)
[email protected]32ba2602009-06-06 18:44:48442 continue
443 status = status_result[0][0]
[email protected]e3608df2009-11-10 20:22:57444 if status != item[0]:
[email protected]32ba2602009-06-06 18:44:48445 save = True
[email protected]e3608df2009-11-10 20:22:57446 files[files.index(item)] = (status, item[1])
[email protected]8d5c9a52009-06-12 15:59:08447 change_info = ChangeInfo(changename, issue, patchset, description, files,
[email protected]ea452b32009-11-22 20:04:31448 local_root, needs_upload)
[email protected]32ba2602009-06-06 18:44:48449 if save:
450 change_info.Save()
451 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42452
453
454def GetChangelistInfoFile(changename):
455 """Returns the file that stores information about a changelist."""
456 if not changename or re.search(r'[^\w-]', changename):
457 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52458 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42459
460
[email protected]8d5c9a52009-06-12 15:59:08461def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
462 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42463 """Loads many changes and merge their files list into one pseudo change.
464
465 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
466 """
467 changes = changenames.split(',')
[email protected]ea452b32009-11-22 20:04:31468 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root,
469 needs_upload=False)
[email protected]fb2b8eb2009-04-23 21:03:42470 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08471 aggregate_change_info._files += ChangeInfo.Load(change,
472 local_root,
473 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24474 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42475 return aggregate_change_info
476
477
[email protected]fb2b8eb2009-04-23 21:03:42478def GetCLs():
479 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52480 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42481 if CODEREVIEW_SETTINGS_FILE in cls:
482 cls.remove(CODEREVIEW_SETTINGS_FILE)
483 return cls
484
485
486def GenerateChangeName():
487 """Generate a random changelist name."""
488 random.seed()
489 current_cl_names = GetCLs()
490 while True:
491 cl_name = (random.choice(string.ascii_lowercase) +
492 random.choice(string.digits) +
493 random.choice(string.ascii_lowercase) +
494 random.choice(string.digits))
495 if cl_name not in current_cl_names:
496 return cl_name
497
498
499def GetModifiedFiles():
500 """Returns a set that maps from changelist name to (status,filename) tuples.
501
502 Files not in a changelist have an empty changelist name. Filenames are in
503 relation to the top level directory of the current repository. Note that
504 only the current directory and subdirectories are scanned, in order to
505 improve performance while still being flexible.
506 """
507 files = {}
508
509 # Since the files are normalized to the root folder of the repositary, figure
510 # out what we need to add to the paths.
511 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
512
513 # Get a list of all files in changelists.
514 files_in_cl = {}
515 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08516 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
517 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24518 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42519 files_in_cl[filename] = change_info.name
520
521 # Get all the modified files.
[email protected]5aeb7dd2009-11-17 18:09:01522 status_result = SVN.CaptureStatus(None)
[email protected]207fdf32009-04-28 19:57:01523 for line in status_result:
524 status = line[0]
525 filename = line[1]
526 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42527 continue
[email protected]fb2b8eb2009-04-23 21:03:42528 if dir_prefix:
529 filename = os.path.join(dir_prefix, filename)
530 change_list_name = ""
531 if filename in files_in_cl:
532 change_list_name = files_in_cl[filename]
533 files.setdefault(change_list_name, []).append((status, filename))
534
535 return files
536
537
538def GetFilesNotInCL():
539 """Returns a list of tuples (status,filename) that aren't in any changelists.
540
541 See docstring of GetModifiedFiles for information about path of files and
542 which directories are scanned.
543 """
544 modified_files = GetModifiedFiles()
545 if "" not in modified_files:
546 return []
547 return modified_files[""]
548
549
550def SendToRietveld(request_path, payload=None,
551 content_type="application/octet-stream", timeout=None):
552 """Send a POST/GET to Rietveld. Returns the response body."""
[email protected]3884d762009-11-25 00:01:22553 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
[email protected]fb2b8eb2009-04-23 21:03:42554 def GetUserCredentials():
555 """Prompts the user for a username and password."""
[email protected]3884d762009-11-25 00:01:22556 email = upload.GetEmail("Email (login for uploading to %s)" % server)
[email protected]fb2b8eb2009-04-23 21:03:42557 password = getpass.getpass("Password for %s: " % email)
558 return email, password
[email protected]fb2b8eb2009-04-23 21:03:42559 rpc_server = upload.HttpRpcServer(server,
560 GetUserCredentials,
561 host_override=server,
562 save_cookies=True)
563 try:
564 return rpc_server.Send(request_path, payload, content_type, timeout)
[email protected]e3608df2009-11-10 20:22:57565 except urllib2.URLError:
[email protected]fb2b8eb2009-04-23 21:03:42566 if timeout is None:
567 ErrorExit("Error accessing url %s" % request_path)
568 else:
569 return None
570
571
572def GetIssueDescription(issue):
573 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48574 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42575
576
[email protected]88c32d82009-10-12 18:24:05577def Opened(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42578 """Prints a list of modified files in the current directory down."""
579 files = GetModifiedFiles()
580 cl_keys = files.keys()
581 cl_keys.sort()
582 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05583 if not cl_name:
584 continue
585 note = ""
586 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
587 fail_on_not_found=True, update_status=False)
588 if len(change_info.GetFiles()) != len(files[cl_name]):
589 note = " (Note: this changelist contains files outside this directory)"
590 print "\n--- Changelist " + cl_name + note + ":"
[email protected]e3608df2009-11-10 20:22:57591 for filename in files[cl_name]:
592 print "".join(filename)
[email protected]88c32d82009-10-12 18:24:05593 if show_unknown_files:
594 unknown_files = UnknownFiles([])
595 if (files.get('') or (show_unknown_files and len(unknown_files))):
596 print "\n--- Not in any changelist:"
[email protected]e3608df2009-11-10 20:22:57597 for item in files.get('', []):
598 print "".join(item)
[email protected]88c32d82009-10-12 18:24:05599 if show_unknown_files:
[email protected]e3608df2009-11-10 20:22:57600 for filename in unknown_files:
601 print "? %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:42602
603
604def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53605 if argv:
606 if argv[0] == 'try':
607 TryChange(None, ['--help'], swallow_exception=False)
608 return
609 if argv[0] == 'upload':
610 upload.RealMain(['upload.py', '--help'])
611 return
[email protected]fb2b8eb2009-04-23 21:03:42612
613 print (
614"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48615version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42616
617Basic commands:
618-----------------------------------------
619 gcl change change_name
620 Add/remove files to a changelist. Only scans the current directory and
621 subdirectories.
622
623 gcl upload change_name [-r [email protected],[email protected],...]
624 [--send_mail] [--no_try] [--no_presubmit]
[email protected]b2ab4942009-06-11 21:39:19625 [--no_watchlists]
[email protected]fb2b8eb2009-04-23 21:03:42626 Uploads the changelist to the server for review.
627
[email protected]3b217f52009-06-01 17:54:20628 gcl commit change_name [--no_presubmit]
[email protected]fb2b8eb2009-04-23 21:03:42629 Commits the changelist to the repository.
630
631 gcl lint change_name
632 Check all the files in the changelist for possible style violations.
633
634Advanced commands:
635-----------------------------------------
636 gcl delete change_name
637 Deletes a changelist.
638
639 gcl diff change_name
640 Diffs all files in the changelist.
641
642 gcl presubmit change_name
643 Runs presubmit checks without uploading the changelist.
644
645 gcl diff
646 Diffs all files in the current directory and subdirectories that aren't in
647 a changelist.
648
649 gcl changes
650 Lists all the the changelists and the files in them.
651
652 gcl nothave [optional directory]
653 Lists files unknown to Subversion.
654
655 gcl opened
656 Lists modified files in the current directory and subdirectories.
657
658 gcl settings
659 Print the code review settings for this directory.
660
661 gcl status
662 Lists modified and unknown files in the current directory and
663 subdirectories.
664
665 gcl try change_name
666 Sends the change to the tryserver so a trybot can do a test run on your
667 code. To send multiple changes as one path, use a comma-separated list
668 of changenames.
669 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53670
[email protected]bfd09ce2009-08-05 21:17:23671 gcl deleteempties
672 Deletes all changelists that have no files associated with them. Careful,
673 you can lose your descriptions.
674
[email protected]3bcc6ce2009-05-12 22:53:53675 gcl help [command]
676 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42677""")
678
679def GetEditor():
680 editor = os.environ.get("SVN_EDITOR")
681 if not editor:
682 editor = os.environ.get("EDITOR")
683
684 if not editor:
685 if sys.platform.startswith("win"):
686 editor = "notepad"
687 else:
688 editor = "vi"
689
690 return editor
691
692
693def GenerateDiff(files, root=None):
[email protected]f2f9d552009-12-22 00:12:57694 return SVN.GenerateDiff(files, root=root)
[email protected]fb2b8eb2009-04-23 21:03:42695
[email protected]51ee0072009-06-08 19:20:05696
697def OptionallyDoPresubmitChecks(change_info, committing, args):
698 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
699 return True
[email protected]b0dfd352009-06-10 14:12:54700 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05701
702
[email protected]fb2b8eb2009-04-23 21:03:42703def UploadCL(change_info, args):
[email protected]17f59f22009-06-12 13:27:24704 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42705 print "Nothing to upload, changelist is empty."
706 return
[email protected]51ee0072009-06-08 19:20:05707 if not OptionallyDoPresubmitChecks(change_info, False, args):
708 return
709 no_try = FilterFlag(args, "--no_try") or FilterFlag(args, "--no-try")
[email protected]b2ab4942009-06-11 21:39:19710 no_watchlists = FilterFlag(args, "--no_watchlists") or \
711 FilterFlag(args, "--no-watchlists")
[email protected]fb2b8eb2009-04-23 21:03:42712
713 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05714 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42715 args.append("--send_mail")
716
717 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05718 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42719
[email protected]003c2692009-05-20 13:08:08720 # Disable try when the server is overridden.
721 server_1 = re.compile(r"^-s\b.*")
722 server_2 = re.compile(r"^--server\b.*")
723 for arg in args:
724 if server_1.match(arg) or server_2.match(arg):
725 no_try = True
726 break
[email protected]fb2b8eb2009-04-23 21:03:42727
728 upload_arg = ["upload.py", "-y"]
729 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
730 upload_arg.extend(args)
731
732 desc_file = ""
733 if change_info.issue: # Uploading a new patchset.
734 found_message = False
735 for arg in args:
736 if arg.startswith("--message") or arg.startswith("-m"):
737 found_message = True
738 break
739
740 if not found_message:
741 upload_arg.append("--message=''")
742
[email protected]32ba2602009-06-06 18:44:48743 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42744 else: # First time we upload.
745 handle, desc_file = tempfile.mkstemp(text=True)
746 os.write(handle, change_info.description)
747 os.close(handle)
748
[email protected]b2ab4942009-06-11 21:39:19749 # Watchlist processing -- CC people interested in this changeset
750 # http://dev.chromium.org/developers/contributing-code/watchlists
751 if not no_watchlists:
752 import watchlists
[email protected]17f59f22009-06-12 13:27:24753 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08754 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19755
[email protected]fb2b8eb2009-04-23 21:03:42756 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19757 if not no_watchlists and watchers:
758 # Filter out all empty elements and join by ','
759 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42760 if cc_list:
761 upload_arg.append("--cc=" + cc_list)
762 upload_arg.append("--description_file=" + desc_file + "")
763 if change_info.description:
764 subject = change_info.description[:77]
765 if subject.find("\r\n") != -1:
766 subject = subject[:subject.find("\r\n")]
767 if subject.find("\n") != -1:
768 subject = subject[:subject.find("\n")]
769 if len(change_info.description) > 77:
770 subject = subject + "..."
771 upload_arg.append("--message=" + subject)
772
773 # Change the current working directory before calling upload.py so that it
774 # shows the correct base.
775 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24776 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42777
778 # If we have a lot of files with long paths, then we won't be able to fit
779 # the command to "svn diff". Instead, we generate the diff manually for
780 # each file and concatenate them before passing it to upload.py.
781 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24782 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42783 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48784 if issue and patchset:
785 change_info.issue = int(issue)
786 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42787 change_info.Save()
788
789 if desc_file:
790 os.remove(desc_file)
791
792 # Do background work on Rietveld to lint the file so that the results are
793 # ready when the issue is viewed.
794 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
795
[email protected]57e78552009-09-11 23:04:30796 # Move back before considering try, so GetCodeReviewSettings is
797 # consistent.
798 os.chdir(previous_cwd)
799
[email protected]fb2b8eb2009-04-23 21:03:42800 # Once uploaded to Rietveld, send it to the try server.
801 if not no_try:
802 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
803 if try_on_upload and try_on_upload.lower() == 'true':
[email protected]32ba2602009-06-06 18:44:48804 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42805 if clobber:
[email protected]32ba2602009-06-06 18:44:48806 trychange_args.append('--clobber')
807 TryChange(change_info, trychange_args, swallow_exception=True)
[email protected]fb2b8eb2009-04-23 21:03:42808
[email protected]fb2b8eb2009-04-23 21:03:42809
810
811def PresubmitCL(change_info):
812 """Reports what presubmit checks on the change would report."""
[email protected]17f59f22009-06-12 13:27:24813 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42814 print "Nothing to presubmit check, changelist is empty."
815 return
816
817 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]b0dfd352009-06-10 14:12:54818 DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42819
[email protected]b0dfd352009-06-10 14:12:54820 print "\n*** Presubmit checks for COMMIT would report: ***"
821 DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42822
823
824def TryChange(change_info, args, swallow_exception):
825 """Create a diff file of change_info and send it to the try server."""
826 try:
827 import trychange
828 except ImportError:
829 if swallow_exception:
830 return
831 ErrorExit("You need to install trychange.py to use the try server.")
832
[email protected]18111352009-12-20 17:21:28833 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42834 if change_info:
[email protected]18111352009-12-20 17:21:28835 trychange_args.extend(['--name', change_info.name])
[email protected]32ba2602009-06-06 18:44:48836 if change_info.issue:
837 trychange_args.extend(["--issue", str(change_info.issue)])
838 if change_info.patchset:
839 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42840 trychange_args.extend(args)
[email protected]1227c7d2009-12-22 00:54:27841 file_list = change_info.GetFileNames()
[email protected]fb2b8eb2009-04-23 21:03:42842 else:
[email protected]18111352009-12-20 17:21:28843 trychange_args.extend(args)
[email protected]1227c7d2009-12-22 00:54:27844 file_list = None
845 trychange.TryChange(trychange_args,
846 file_list=file_list,
847 swallow_exception=swallow_exception,
848 prog='gcl try')
[email protected]fb2b8eb2009-04-23 21:03:42849
850
851def Commit(change_info, args):
[email protected]17f59f22009-06-12 13:27:24852 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42853 print "Nothing to commit, changelist is empty."
854 return
[email protected]51ee0072009-06-08 19:20:05855 if not OptionallyDoPresubmitChecks(change_info, True, args):
856 return
[email protected]fb2b8eb2009-04-23 21:03:42857
[email protected]1bb04aa2009-06-01 17:52:11858 # We face a problem with svn here: Let's say change 'bleh' modifies
859 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
860 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
861 # The only fix is to use --non-recursive but that has its issues too:
862 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
863 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
864 # directory with child nodes". Yay...
865 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42866 if change_info.issue:
867 # Get the latest description from Rietveld.
868 change_info.description = GetIssueDescription(change_info.issue)
869
870 commit_message = change_info.description.replace('\r\n', '\n')
871 if change_info.issue:
[email protected]32ba2602009-06-06 18:44:48872 commit_message += ('\nReview URL: http://%s/%d' %
[email protected]fb2b8eb2009-04-23 21:03:42873 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
874 change_info.issue))
875
876 handle, commit_filename = tempfile.mkstemp(text=True)
877 os.write(handle, commit_message)
878 os.close(handle)
879
880 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24881 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42882 os.close(handle)
883
884 commit_cmd += ['--file=' + commit_filename]
885 commit_cmd += ['--targets=' + targets_filename]
886 # Change the current working directory before calling commit.
887 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24888 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42889 output = RunShell(commit_cmd, True)
890 os.remove(commit_filename)
891 os.remove(targets_filename)
892 if output.find("Committed revision") != -1:
893 change_info.Delete()
894
895 if change_info.issue:
896 revision = re.compile(".*?\nCommitted revision (\d+)",
897 re.DOTALL).match(output).group(1)
898 viewvc_url = GetCodeReviewSetting("VIEW_VC")
899 change_info.description = change_info.description + '\n'
900 if viewvc_url:
901 change_info.description += "\nCommitted: " + viewvc_url + revision
902 change_info.CloseIssue()
903 os.chdir(previous_cwd)
904
[email protected]2c8d4b22009-06-06 21:03:10905
[email protected]9ce98222009-10-19 20:24:17906def Change(change_info, args):
[email protected]fb2b8eb2009-04-23 21:03:42907 """Creates/edits a changelist."""
[email protected]9ce98222009-10-19 20:24:17908 silent = FilterFlag(args, "--silent")
[email protected]d36b3ed2009-11-09 18:51:42909
910 # Verify the user is running the change command from a read-write checkout.
[email protected]5aeb7dd2009-11-17 18:09:01911 svn_info = SVN.CaptureInfo('.')
[email protected]d36b3ed2009-11-09 18:51:42912 if not svn_info:
913 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
914 "directory.")
[email protected]d36b3ed2009-11-09 18:51:42915
[email protected]9ce98222009-10-19 20:24:17916 if (len(args) == 1):
917 filename = args[0]
918 f = open(filename, 'rU')
919 override_description = f.read()
920 f.close()
921 else:
922 override_description = None
[email protected]5aeb7dd2009-11-17 18:09:01923
[email protected]ea452b32009-11-22 20:04:31924 if change_info.issue and not change_info.NeedsUpload():
[email protected]fb2b8eb2009-04-23 21:03:42925 try:
926 description = GetIssueDescription(change_info.issue)
927 except urllib2.HTTPError, err:
928 if err.code == 404:
929 # The user deleted the issue in Rietveld, so forget the old issue id.
930 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:10931 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:42932 change_info.Save()
933 else:
934 ErrorExit("Error getting the description from Rietveld: " + err)
935 else:
[email protected]85532fc2009-06-04 22:36:53936 if override_description:
937 description = override_description
938 else:
939 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:42940
941 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:23942
[email protected]f0dfba32009-08-07 22:03:37943 # Edited files (as opposed to files with only changed properties) will have
944 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:53945 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:37946 affected_files = [x for x in other_files if file_re.match(x[0])]
947 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:42948
949 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:24950 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:42951 "---Paths in this changelist (" + change_info.name + "):\n")
952 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
953 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:37954 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
955
956 if change_info.Exists():
957 text += (separator2 +
958 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
959 else:
960 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
961 separator2)
962 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:42963
964 handle, filename = tempfile.mkstemp(text=True)
965 os.write(handle, text)
966 os.close(handle)
967
[email protected]9ce98222009-10-19 20:24:17968 if not silent:
969 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:42970
[email protected]0fca4f32009-12-18 15:14:34971 result = gclient_utils.FileRead(filename, 'r')
[email protected]fb2b8eb2009-04-23 21:03:42972 os.remove(filename)
973
974 if not result:
975 return
976
977 split_result = result.split(separator1, 1)
978 if len(split_result) != 2:
979 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
980
[email protected]ea452b32009-11-22 20:04:31981 # Update the CL description if it has changed.
[email protected]fb2b8eb2009-04-23 21:03:42982 new_description = split_result[0]
983 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:53984 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:42985 change_info.description = new_description
[email protected]ea452b32009-11-22 20:04:31986 change_info.needs_upload = True
[email protected]fb2b8eb2009-04-23 21:03:42987
988 new_cl_files = []
989 for line in cl_files_text.splitlines():
990 if not len(line):
991 continue
992 if line.startswith("---"):
993 break
994 status = line[:7]
[email protected]e3608df2009-11-10 20:22:57995 filename = line[7:]
996 new_cl_files.append((status, filename))
[email protected]bfd09ce2009-08-05 21:17:23997
998 if (not len(change_info._files)) and (not change_info.issue) and \
999 (not len(new_description) and (not new_cl_files)):
1000 ErrorExit("Empty changelist not saved")
1001
[email protected]17f59f22009-06-12 13:27:241002 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421003 change_info.Save()
[email protected]53bcf152009-11-13 21:04:101004 if svn_info.get('URL', '').startswith('http:'):
1005 Warn("WARNING: Creating CL in a read-only checkout. You will not be "
1006 "able to commit it!")
1007
[email protected]fb2b8eb2009-04-23 21:03:421008 print change_info.name + " changelist saved."
1009 if change_info.MissingTests():
1010 Warn("WARNING: " + MISSING_TEST_MSG)
1011
[email protected]ea452b32009-11-22 20:04:311012 # Update the Rietveld issue.
1013 if change_info.issue and change_info.NeedsUpload():
1014 change_info.UpdateRietveldDescription()
1015 change_info.needs_upload = False
1016 change_info.Save()
1017
1018
[email protected]fb2b8eb2009-04-23 21:03:421019# Valid extensions for files we want to lint.
[email protected]e72bb632009-10-29 20:15:481020DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
1021DEFAULT_LINT_IGNORE_REGEX = r""
[email protected]fb2b8eb2009-04-23 21:03:421022
1023def Lint(change_info, args):
1024 """Runs cpplint.py on all the files in |change_info|"""
1025 try:
1026 import cpplint
1027 except ImportError:
1028 ErrorExit("You need to install cpplint.py to lint C++ files.")
1029
1030 # Change the current working directory before calling lint so that it
1031 # shows the correct base.
1032 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241033 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421034
1035 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241036 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421037
[email protected]bb816382009-10-29 01:38:021038 white_list = GetCodeReviewSetting("LINT_REGEX")
1039 if not white_list:
[email protected]e72bb632009-10-29 20:15:481040 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021041 white_regex = re.compile(white_list)
1042 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1043 if not black_list:
[email protected]e72bb632009-10-29 20:15:481044 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021045 black_regex = re.compile(black_list)
[email protected]e3608df2009-11-10 20:22:571046 for filename in filenames:
1047 if white_regex.match(filename):
1048 if black_regex.match(filename):
1049 print "Ignoring file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421050 else:
[email protected]e3608df2009-11-10 20:22:571051 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021052 else:
[email protected]e3608df2009-11-10 20:22:571053 print "Skipping file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421054
1055 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1056 os.chdir(previous_cwd)
1057
1058
[email protected]b0dfd352009-06-10 14:12:541059def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421060 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1061 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481062 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151063 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411064 change = presubmit_support.SvnChange(change_info.name,
1065 change_info.description,
1066 change_info.GetLocalRoot(),
1067 change_info.GetFiles(),
1068 change_info.issue,
1069 change_info.patchset)
1070 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541071 committing=committing,
[email protected]1033acc2009-05-13 14:36:481072 verbose=False,
1073 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151074 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541075 default_presubmit=root_presubmit,
1076 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551077 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421078 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1079 return result
1080
1081
1082def Changes():
1083 """Print all the changelists and their files."""
1084 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081085 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421086 print "\n--- Changelist " + change_info.name + ":"
[email protected]e3608df2009-11-10 20:22:571087 for filename in change_info.GetFiles():
1088 print "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421089
1090
[email protected]bfd09ce2009-08-05 21:17:231091def DeleteEmptyChangeLists():
1092 """Delete all changelists that have no files."""
1093 print "\n--- Deleting:"
1094 for cl in GetCLs():
1095 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1096 if not len(change_info._files):
1097 print change_info.name
1098 change_info.Delete()
1099
1100
[email protected]fb2b8eb2009-04-23 21:03:421101def main(argv=None):
1102 if argv is None:
1103 argv = sys.argv
1104
1105 if len(argv) == 1:
1106 Help()
1107 return 0;
1108
[email protected]a05be0b2009-06-30 19:13:021109 try:
1110 # Create the directories where we store information about changelists if it
1111 # doesn't exist.
1112 if not os.path.exists(GetInfoDir()):
1113 os.mkdir(GetInfoDir())
1114 if not os.path.exists(GetChangesDir()):
1115 os.mkdir(GetChangesDir())
[email protected]a05be0b2009-06-30 19:13:021116 if not os.path.exists(GetCacheDir()):
1117 os.mkdir(GetCacheDir())
[email protected]5f3eee32009-09-17 00:34:301118 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:021119 # Will throw an exception if not run in a svn checkout.
1120 pass
[email protected]fb2b8eb2009-04-23 21:03:421121
1122 # Commands that don't require an argument.
1123 command = argv[1]
[email protected]88c32d82009-10-12 18:24:051124 if command == "opened" or command == "status":
1125 Opened(command == "status")
[email protected]fb2b8eb2009-04-23 21:03:421126 return 0
1127 if command == "nothave":
[email protected]e3608df2009-11-10 20:22:571128 __pychecker__ = 'no-returnvalues'
1129 for filename in UnknownFiles(argv[2:]):
1130 print "? " + "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421131 return 0
1132 if command == "changes":
1133 Changes()
1134 return 0
1135 if command == "help":
1136 Help(argv[2:])
1137 return 0
1138 if command == "diff" and len(argv) == 2:
1139 files = GetFilesNotInCL()
1140 print GenerateDiff([x[1] for x in files])
1141 return 0
1142 if command == "settings":
[email protected]e3608df2009-11-10 20:22:571143 # Force load settings
1144 GetCodeReviewSetting("UNKNOWN");
[email protected]a005ccd2009-06-12 13:25:541145 del CODEREVIEW_SETTINGS['__just_initialized']
1146 print '\n'.join(("%s: %s" % (str(k), str(v))
1147 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
[email protected]fb2b8eb2009-04-23 21:03:421148 return 0
[email protected]bfd09ce2009-08-05 21:17:231149 if command == "deleteempties":
1150 DeleteEmptyChangeLists()
1151 return 0
[email protected]fb2b8eb2009-04-23 21:03:421152
[email protected]a31d6582009-11-10 13:55:131153 if command == "change":
1154 if len(argv) == 2:
[email protected]fb2b8eb2009-04-23 21:03:421155 # Generate a random changelist name.
1156 changename = GenerateChangeName()
[email protected]a31d6582009-11-10 13:55:131157 elif argv[2] == '--force':
1158 changename = GenerateChangeName()
1159 # argv[3:] is passed to Change() as |args| later. Change() should receive
1160 # |args| which includes '--force'.
1161 argv.insert(2, changename)
[email protected]fb2b8eb2009-04-23 21:03:421162 else:
[email protected]a31d6582009-11-10 13:55:131163 changename = argv[2]
1164 elif len(argv) == 2:
1165 ErrorExit("Need a changelist name.")
[email protected]fb2b8eb2009-04-23 21:03:421166 else:
1167 changename = argv[2]
1168
1169 # When the command is 'try' and --patchset is used, the patch to try
1170 # is on the Rietveld server. 'change' creates a change so it's fine if the
1171 # change didn't exist. All other commands require an existing change.
1172 fail_on_not_found = command != "try" and command != "change"
1173 if command == "try" and changename.find(',') != -1:
[email protected]8d5c9a52009-06-12 15:59:081174 change_info = LoadChangelistInfoForMultiple(changename, GetRepositoryRoot(),
1175 True, True)
[email protected]fb2b8eb2009-04-23 21:03:421176 else:
[email protected]8d5c9a52009-06-12 15:59:081177 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(),
1178 fail_on_not_found, True)
[email protected]fb2b8eb2009-04-23 21:03:421179
1180 if command == "change":
[email protected]9ce98222009-10-19 20:24:171181 Change(change_info, argv[3:])
[email protected]fb2b8eb2009-04-23 21:03:421182 elif command == "lint":
1183 Lint(change_info, argv[3:])
1184 elif command == "upload":
1185 UploadCL(change_info, argv[3:])
1186 elif command == "presubmit":
1187 PresubmitCL(change_info)
1188 elif command in ("commit", "submit"):
1189 Commit(change_info, argv[3:])
1190 elif command == "delete":
1191 change_info.Delete()
1192 elif command == "try":
1193 # When the change contains no file, send the "changename" positional
1194 # argument to trychange.py.
[email protected]17f59f22009-06-12 13:27:241195 if change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421196 args = argv[3:]
1197 else:
1198 change_info = None
1199 args = argv[2:]
1200 TryChange(change_info, args, swallow_exception=False)
1201 else:
1202 # Everything else that is passed into gcl we redirect to svn, after adding
1203 # the files. This allows commands such as 'gcl diff xxx' to work.
[email protected]f40fbb32009-11-10 13:54:311204 if command == "diff" and not change_info.GetFileNames():
1205 return 0
[email protected]fb2b8eb2009-04-23 21:03:421206 args =["svn", command]
1207 root = GetRepositoryRoot()
[email protected]17f59f22009-06-12 13:27:241208 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
[email protected]fb2b8eb2009-04-23 21:03:421209 RunShell(args, True)
1210 return 0
1211
1212
1213if __name__ == "__main__":
1214 sys.exit(main())