blob: 2a81861d56f83303b0d2e96e3fb233860c42dd35 [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.
5#
6# Wrapper script around Rietveld's upload.py that groups files into
7# changelists.
8
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
[email protected]207fdf32009-04-28 19:57:0120import xml.dom.minidom
[email protected]fb2b8eb2009-04-23 21:03:4221
[email protected]46a94102009-05-12 20:32:4322# gcl now depends on gclient.
[email protected]5f3eee32009-09-17 00:34:3023import gclient_scm
24import gclient_utils
[email protected]c1675e22009-04-27 20:30:4825
[email protected]98fc2b92009-05-21 14:11:5126__version__ = '1.1.1'
[email protected]c1675e22009-04-27 20:30:4827
28
[email protected]fb2b8eb2009-04-23 21:03:4229CODEREVIEW_SETTINGS = {
30 # Default values.
31 "CODE_REVIEW_SERVER": "codereview.chromium.org",
32 "CC_LIST": "[email protected]",
33 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
34}
35
[email protected]fb2b8eb2009-04-23 21:03:4236# globals that store the root of the current repository and the directory where
37# we store information about changelists.
[email protected]98fc2b92009-05-21 14:11:5138REPOSITORY_ROOT = ""
[email protected]fb2b8eb2009-04-23 21:03:4239
40# Filename where we store repository specific information for gcl.
41CODEREVIEW_SETTINGS_FILE = "codereview.settings"
42
43# Warning message when the change appears to be missing tests.
44MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
45
[email protected]98fc2b92009-05-21 14:11:5146# Global cache of files cached in GetCacheDir().
47FILES_CACHE = {}
[email protected]fb2b8eb2009-04-23 21:03:4248
49
[email protected]207fdf32009-04-28 19:57:0150### SVN Functions
51
[email protected]fb2b8eb2009-04-23 21:03:4252def IsSVNMoved(filename):
53 """Determine if a file has been added through svn mv"""
[email protected]5f3eee32009-09-17 00:34:3054 info = gclient_scm.CaptureSVNInfo(filename)
[email protected]fb2b8eb2009-04-23 21:03:4255 return (info.get('Copied From URL') and
56 info.get('Copied From Rev') and
57 info.get('Schedule') == 'add')
58
59
[email protected]fb2b8eb2009-04-23 21:03:4260def GetSVNFileProperty(file, property_name):
61 """Returns the value of an SVN property for the given file.
62
63 Args:
64 file: The file to check
65 property_name: The name of the SVN property, e.g. "svn:mime-type"
66
67 Returns:
68 The value of the property, which will be the empty string if the property
69 is not set on the file. If the file is not under version control, the
70 empty string is also returned.
71 """
72 output = RunShell(["svn", "propget", property_name, file])
73 if (output.startswith("svn: ") and
74 output.endswith("is not under version control")):
75 return ""
76 else:
77 return output
78
79
[email protected]207fdf32009-04-28 19:57:0180def UnknownFiles(extra_args):
81 """Runs svn status and prints unknown files.
82
83 Any args in |extra_args| are passed to the tool to support giving alternate
84 code locations.
85 """
[email protected]5f3eee32009-09-17 00:34:3086 return [item[1] for item in gclient_scm.CaptureSVNStatus(extra_args)
[email protected]4810a962009-05-12 21:03:3487 if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0188
89
[email protected]fb2b8eb2009-04-23 21:03:4290def GetRepositoryRoot():
91 """Returns the top level directory of the current repository.
92
93 The directory is returned as an absolute path.
94 """
[email protected]98fc2b92009-05-21 14:11:5195 global REPOSITORY_ROOT
96 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:3097 infos = gclient_scm.CaptureSVNInfo(os.getcwd(), print_error=False)
[email protected]46a94102009-05-12 20:32:4398 cur_dir_repo_root = infos.get("Repository Root")
[email protected]fb2b8eb2009-04-23 21:03:4299 if not cur_dir_repo_root:
[email protected]5f3eee32009-09-17 00:34:30100 raise gclient_utils.Error("gcl run outside of repository")
[email protected]fb2b8eb2009-04-23 21:03:42101
[email protected]98fc2b92009-05-21 14:11:51102 REPOSITORY_ROOT = os.getcwd()
[email protected]fb2b8eb2009-04-23 21:03:42103 while True:
[email protected]98fc2b92009-05-21 14:11:51104 parent = os.path.dirname(REPOSITORY_ROOT)
[email protected]5f3eee32009-09-17 00:34:30105 if (gclient_scm.CaptureSVNInfo(parent, print_error=False).get(
[email protected]8c3ccf32009-05-20 18:28:37106 "Repository Root") != cur_dir_repo_root):
[email protected]fb2b8eb2009-04-23 21:03:42107 break
[email protected]98fc2b92009-05-21 14:11:51108 REPOSITORY_ROOT = parent
109 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:42110
111
112def GetInfoDir():
113 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:52114 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
115
116
117def GetChangesDir():
118 """Returns the directory where gcl change files are stored."""
119 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:42120
121
[email protected]98fc2b92009-05-21 14:11:51122def GetCacheDir():
123 """Returns the directory where gcl change files are stored."""
124 return os.path.join(GetInfoDir(), 'cache')
125
126
127def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
128 """Retrieves a file from the repository and caches it in GetCacheDir() for
129 max_age seconds.
130
131 use_root: If False, look up the arborescence for the first match, otherwise go
132 directory to the root repository.
133
134 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:02135 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:51136 """
137 global FILES_CACHE
138 if filename not in FILES_CACHE:
139 # Don't try to look up twice.
140 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28141 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02142 try:
143 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]5f3eee32009-09-17 00:34:30144 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:02145 return None
[email protected]98fc2b92009-05-21 14:11:51146 if (not os.path.exists(cached_file) or
147 os.stat(cached_file).st_mtime > max_age):
[email protected]5f3eee32009-09-17 00:34:30148 dir_info = gclient_scm.CaptureSVNInfo(".")
[email protected]9b613272009-04-24 01:28:28149 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51150 if use_root:
151 url_path = repo_root
152 else:
153 url_path = dir_info["URL"]
154 content = ""
[email protected]9b613272009-04-24 01:28:28155 while True:
[email protected]bb816382009-10-29 01:38:02156 # First, look for a locally modified version of codereview.settings.
157 content, rc = RunShellWithReturnCode(["svn", "status", filename])
158 if not rc and content.startswith('M'):
159 content = ReadFile(filename)
160 rc = 0
161 else:
162 # Then look in the repository
163 svn_path = url_path + "/" + filename
164 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
165
[email protected]9b613272009-04-24 01:28:28166 if not rc:
[email protected]98fc2b92009-05-21 14:11:51167 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28168 break
169 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51170 content = ""
[email protected]9b613272009-04-24 01:28:28171 if url_path == repo_root:
172 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51173 break
[email protected]9b613272009-04-24 01:28:28174 # Go up one level to try again.
175 url_path = os.path.dirname(url_path)
[email protected]9b613272009-04-24 01:28:28176 # Write a cached version even if there isn't a file, so we don't try to
177 # fetch it each time.
[email protected]98fc2b92009-05-21 14:11:51178 WriteFile(cached_file, content)
179 else:
180 content = ReadFile(cached_settings_file)
181 # Keep the content cached in memory.
182 FILES_CACHE[filename] = content
183 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28184
[email protected]98fc2b92009-05-21 14:11:51185
186def GetCodeReviewSetting(key):
187 """Returns a value for the given key for this repository."""
188 # Use '__just_initialized' as a flag to determine if the settings were
189 # already initialized.
190 global CODEREVIEW_SETTINGS
191 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47192 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
193 if settings_file:
194 for line in settings_file.splitlines():
195 if not line or line.startswith("#"):
196 continue
197 k, v = line.split(": ", 1)
198 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51199 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42200 return CODEREVIEW_SETTINGS.get(key, "")
201
202
[email protected]fb2b8eb2009-04-23 21:03:42203def Warn(msg):
204 ErrorExit(msg, exit=False)
205
206
207def ErrorExit(msg, exit=True):
208 """Print an error message to stderr and optionally exit."""
209 print >>sys.stderr, msg
210 if exit:
211 sys.exit(1)
212
213
214def RunShellWithReturnCode(command, print_output=False):
215 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02216 # Use a shell for subcommands on Windows to get a PATH search, and because svn
217 # may be a batch file.
218 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42219 p = subprocess.Popen(command, stdout=subprocess.PIPE,
220 stderr=subprocess.STDOUT, shell=use_shell,
221 universal_newlines=True)
222 if print_output:
223 output_array = []
224 while True:
225 line = p.stdout.readline()
226 if not line:
227 break
228 if print_output:
229 print line.strip('\n')
230 output_array.append(line)
231 output = "".join(output_array)
232 else:
233 output = p.stdout.read()
234 p.wait()
235 p.stdout.close()
236 return output, p.returncode
237
238
239def RunShell(command, print_output=False):
240 """Executes a command and returns the output."""
241 return RunShellWithReturnCode(command, print_output)[0]
242
243
[email protected]c1675e22009-04-27 20:30:48244def ReadFile(filename, flags='r'):
[email protected]fb2b8eb2009-04-23 21:03:42245 """Returns the contents of a file."""
[email protected]c1675e22009-04-27 20:30:48246 file = open(filename, flags)
[email protected]fb2b8eb2009-04-23 21:03:42247 result = file.read()
248 file.close()
249 return result
250
251
252def WriteFile(filename, contents):
253 """Overwrites the file with the given contents."""
254 file = open(filename, 'w')
255 file.write(contents)
256 file.close()
257
258
[email protected]51ee0072009-06-08 19:20:05259def FilterFlag(args, flag):
260 """Returns True if the flag is present in args list.
261
262 The flag is removed from args if present.
263 """
264 if flag in args:
265 args.remove(flag)
266 return True
267 return False
268
269
[email protected]be0d1ca2009-05-12 19:23:02270class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42271 """Holds information about a changelist.
272
[email protected]32ba2602009-06-06 18:44:48273 name: change name.
274 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
275 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42276 description: the description.
277 files: a list of 2 tuple containing (status, filename) of changed files,
278 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08279 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42280 """
[email protected]32ba2602009-06-06 18:44:48281
282 _SEPARATOR = "\n-----\n"
283 # The info files have the following format:
284 # issue_id, patchset\n (, patchset is optional)
285 # _SEPARATOR\n
286 # filepath1\n
287 # filepath2\n
288 # .
289 # .
290 # filepathn\n
291 # _SEPARATOR\n
292 # description
293
[email protected]8d5c9a52009-06-12 15:59:08294 def __init__(self, name, issue, patchset, description, files, local_root):
[email protected]fb2b8eb2009-04-23 21:03:42295 self.name = name
[email protected]32ba2602009-06-06 18:44:48296 self.issue = int(issue)
297 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42298 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02299 if files is None:
300 files = []
[email protected]17f59f22009-06-12 13:27:24301 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42302 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08303 self._local_root = local_root
[email protected]fb2b8eb2009-04-23 21:03:42304
[email protected]17f59f22009-06-12 13:27:24305 def GetFileNames(self):
306 """Returns the list of file names included in this change."""
307 return [file[1] for file in self._files]
308
309 def GetFiles(self):
310 """Returns the list of files included in this change with their status."""
311 return self._files
312
313 def GetLocalRoot(self):
314 """Returns the local repository checkout root directory."""
315 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42316
[email protected]f0dfba32009-08-07 22:03:37317 def Exists(self):
318 """Returns True if this change already exists (i.e., is not new)."""
319 return (self.issue or self.description or self._files)
320
[email protected]fb2b8eb2009-04-23 21:03:42321 def _NonDeletedFileList(self):
322 """Returns a list of files in this change, not including deleted files."""
[email protected]17f59f22009-06-12 13:27:24323 return [file[1] for file in self.GetFiles()
324 if not file[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42325
326 def _AddedFileList(self):
327 """Returns a list of files added in this change."""
[email protected]17f59f22009-06-12 13:27:24328 return [file[1] for file in self.GetFiles() if file[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42329
330 def Save(self):
331 """Writes the changelist information to disk."""
[email protected]32ba2602009-06-06 18:44:48332 data = ChangeInfo._SEPARATOR.join([
333 "%d, %d" % (self.issue, self.patchset),
[email protected]17f59f22009-06-12 13:27:24334 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48335 self.description])
[email protected]fb2b8eb2009-04-23 21:03:42336 WriteFile(GetChangelistInfoFile(self.name), data)
337
338 def Delete(self):
339 """Removes the changelist information from disk."""
340 os.remove(GetChangelistInfoFile(self.name))
341
342 def CloseIssue(self):
343 """Closes the Rietveld issue for this changelist."""
344 data = [("description", self.description),]
345 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10346 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42347
348 def UpdateRietveldDescription(self):
349 """Sets the description for an issue on Rietveld."""
350 data = [("description", self.description),]
351 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10352 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42353
354 def MissingTests(self):
355 """Returns True if the change looks like it needs unit tests but has none.
356
357 A change needs unit tests if it contains any new source files or methods.
358 """
359 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
360 # Ignore third_party entirely.
361 files = [file for file in self._NonDeletedFileList()
362 if file.find("third_party") == -1]
363 added_files = [file for file in self._AddedFileList()
364 if file.find("third_party") == -1]
365
366 # If the change is entirely in third_party, we're done.
367 if len(files) == 0:
368 return False
369
370 # Any new or modified test files?
371 # A test file's name ends with "test.*" or "tests.*".
372 test_files = [test for test in files
373 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
374 if len(test_files) > 0:
375 return False
376
377 # Any new source files?
378 source_files = [file for file in added_files
379 if os.path.splitext(file)[1] in SOURCE_SUFFIXES]
380 if len(source_files) > 0:
381 return True
382
383 # Do the long test, checking the files for new methods.
384 return self._HasNewMethod()
385
386 def _HasNewMethod(self):
387 """Returns True if the changeset contains any new functions, or if a
388 function signature has been changed.
389
390 A function is identified by starting flush left, containing a "(" before
391 the next flush-left line, and either ending with "{" before the next
392 flush-left line or being followed by an unindented "{".
393
394 Currently this returns True for new methods, new static functions, and
395 methods or functions whose signatures have been changed.
396
397 Inline methods added to header files won't be detected by this. That's
398 acceptable for purposes of determining if a unit test is needed, since
399 inline methods should be trivial.
400 """
401 # To check for methods added to source or header files, we need the diffs.
402 # We'll generate them all, since there aren't likely to be many files
403 # apart from source and headers; besides, we'll want them all if we're
404 # uploading anyway.
405 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24406 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42407
408 definition = ""
409 for line in self.patch.splitlines():
410 if not line.startswith("+"):
411 continue
412 line = line.strip("+").rstrip(" \t")
413 # Skip empty lines, comments, and preprocessor directives.
414 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
415 if line == "" or line.startswith("/") or line.startswith("#"):
416 continue
417
418 # A possible definition ending with "{" is complete, so check it.
419 if definition.endswith("{"):
420 if definition.find("(") != -1:
421 return True
422 definition = ""
423
424 # A { or an indented line, when we're in a definition, continues it.
425 if (definition != "" and
426 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
427 definition += line
428
429 # A flush-left line starts a new possible function definition.
430 elif not line.startswith(" ") and not line.startswith("\t"):
431 definition = line
432
433 return False
434
[email protected]32ba2602009-06-06 18:44:48435 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08436 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48437 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42438
[email protected]32ba2602009-06-06 18:44:48439 Args:
440 fail_on_not_found: if True, this function will quit the program if the
441 changelist doesn't exist.
442 update_status: if True, the svn status will be updated for all the files
443 and unchanged files will be removed.
444
445 Returns: a ChangeInfo object.
446 """
447 info_file = GetChangelistInfoFile(changename)
448 if not os.path.exists(info_file):
449 if fail_on_not_found:
450 ErrorExit("Changelist " + changename + " not found.")
[email protected]8d5c9a52009-06-12 15:59:08451 return ChangeInfo(changename, 0, 0, '', None, local_root)
[email protected]32ba2602009-06-06 18:44:48452 split_data = ReadFile(info_file).split(ChangeInfo._SEPARATOR, 2)
453 if len(split_data) != 3:
454 ErrorExit("Changelist file %s is corrupt" % info_file)
455 items = split_data[0].split(',')
456 issue = 0
457 patchset = 0
458 if items[0]:
459 issue = int(items[0])
460 if len(items) > 1:
461 patchset = int(items[1])
462 files = []
463 for line in split_data[1].splitlines():
464 status = line[:7]
465 file = line[7:]
466 files.append((status, file))
467 description = split_data[2]
468 save = False
469 if update_status:
470 for file in files:
[email protected]8d5c9a52009-06-12 15:59:08471 filename = os.path.join(local_root, file[1])
[email protected]5f3eee32009-09-17 00:34:30472 status_result = gclient_scm.CaptureSVNStatus(filename)
[email protected]32ba2602009-06-06 18:44:48473 if not status_result or not status_result[0][0]:
474 # File has been reverted.
475 save = True
476 files.remove(file)
477 continue
478 status = status_result[0][0]
479 if status != file[0]:
480 save = True
481 files[files.index(file)] = (status, file[1])
[email protected]8d5c9a52009-06-12 15:59:08482 change_info = ChangeInfo(changename, issue, patchset, description, files,
483 local_root)
[email protected]32ba2602009-06-06 18:44:48484 if save:
485 change_info.Save()
486 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42487
488
489def GetChangelistInfoFile(changename):
490 """Returns the file that stores information about a changelist."""
491 if not changename or re.search(r'[^\w-]', changename):
492 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52493 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42494
495
[email protected]8d5c9a52009-06-12 15:59:08496def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
497 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42498 """Loads many changes and merge their files list into one pseudo change.
499
500 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
501 """
502 changes = changenames.split(',')
[email protected]8d5c9a52009-06-12 15:59:08503 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root)
[email protected]fb2b8eb2009-04-23 21:03:42504 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08505 aggregate_change_info._files += ChangeInfo.Load(change,
506 local_root,
507 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24508 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42509 return aggregate_change_info
510
511
[email protected]fb2b8eb2009-04-23 21:03:42512def GetCLs():
513 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52514 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42515 if CODEREVIEW_SETTINGS_FILE in cls:
516 cls.remove(CODEREVIEW_SETTINGS_FILE)
517 return cls
518
519
520def GenerateChangeName():
521 """Generate a random changelist name."""
522 random.seed()
523 current_cl_names = GetCLs()
524 while True:
525 cl_name = (random.choice(string.ascii_lowercase) +
526 random.choice(string.digits) +
527 random.choice(string.ascii_lowercase) +
528 random.choice(string.digits))
529 if cl_name not in current_cl_names:
530 return cl_name
531
532
533def GetModifiedFiles():
534 """Returns a set that maps from changelist name to (status,filename) tuples.
535
536 Files not in a changelist have an empty changelist name. Filenames are in
537 relation to the top level directory of the current repository. Note that
538 only the current directory and subdirectories are scanned, in order to
539 improve performance while still being flexible.
540 """
541 files = {}
542
543 # Since the files are normalized to the root folder of the repositary, figure
544 # out what we need to add to the paths.
545 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
546
547 # Get a list of all files in changelists.
548 files_in_cl = {}
549 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08550 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
551 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24552 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42553 files_in_cl[filename] = change_info.name
554
555 # Get all the modified files.
[email protected]5f3eee32009-09-17 00:34:30556 status_result = gclient_scm.CaptureSVNStatus(None)
[email protected]207fdf32009-04-28 19:57:01557 for line in status_result:
558 status = line[0]
559 filename = line[1]
560 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42561 continue
[email protected]fb2b8eb2009-04-23 21:03:42562 if dir_prefix:
563 filename = os.path.join(dir_prefix, filename)
564 change_list_name = ""
565 if filename in files_in_cl:
566 change_list_name = files_in_cl[filename]
567 files.setdefault(change_list_name, []).append((status, filename))
568
569 return files
570
571
572def GetFilesNotInCL():
573 """Returns a list of tuples (status,filename) that aren't in any changelists.
574
575 See docstring of GetModifiedFiles for information about path of files and
576 which directories are scanned.
577 """
578 modified_files = GetModifiedFiles()
579 if "" not in modified_files:
580 return []
581 return modified_files[""]
582
583
584def SendToRietveld(request_path, payload=None,
585 content_type="application/octet-stream", timeout=None):
586 """Send a POST/GET to Rietveld. Returns the response body."""
587 def GetUserCredentials():
588 """Prompts the user for a username and password."""
589 email = upload.GetEmail()
590 password = getpass.getpass("Password for %s: " % email)
591 return email, password
592
593 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
594 rpc_server = upload.HttpRpcServer(server,
595 GetUserCredentials,
596 host_override=server,
597 save_cookies=True)
598 try:
599 return rpc_server.Send(request_path, payload, content_type, timeout)
600 except urllib2.URLError, e:
601 if timeout is None:
602 ErrorExit("Error accessing url %s" % request_path)
603 else:
604 return None
605
606
607def GetIssueDescription(issue):
608 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48609 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42610
611
[email protected]88c32d82009-10-12 18:24:05612def Opened(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42613 """Prints a list of modified files in the current directory down."""
614 files = GetModifiedFiles()
615 cl_keys = files.keys()
616 cl_keys.sort()
617 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05618 if not cl_name:
619 continue
620 note = ""
621 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
622 fail_on_not_found=True, update_status=False)
623 if len(change_info.GetFiles()) != len(files[cl_name]):
624 note = " (Note: this changelist contains files outside this directory)"
625 print "\n--- Changelist " + cl_name + note + ":"
[email protected]fb2b8eb2009-04-23 21:03:42626 for file in files[cl_name]:
627 print "".join(file)
[email protected]88c32d82009-10-12 18:24:05628 if show_unknown_files:
629 unknown_files = UnknownFiles([])
630 if (files.get('') or (show_unknown_files and len(unknown_files))):
631 print "\n--- Not in any changelist:"
632 for file in files.get('', []):
633 print "".join(file)
634 if show_unknown_files:
635 for file in unknown_files:
636 print "? %s" % file
[email protected]fb2b8eb2009-04-23 21:03:42637
638
639def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53640 if argv:
641 if argv[0] == 'try':
642 TryChange(None, ['--help'], swallow_exception=False)
643 return
644 if argv[0] == 'upload':
645 upload.RealMain(['upload.py', '--help'])
646 return
[email protected]fb2b8eb2009-04-23 21:03:42647
648 print (
649"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48650version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42651
652Basic commands:
653-----------------------------------------
654 gcl change change_name
655 Add/remove files to a changelist. Only scans the current directory and
656 subdirectories.
657
658 gcl upload change_name [-r [email protected],[email protected],...]
659 [--send_mail] [--no_try] [--no_presubmit]
[email protected]b2ab4942009-06-11 21:39:19660 [--no_watchlists]
[email protected]fb2b8eb2009-04-23 21:03:42661 Uploads the changelist to the server for review.
662
[email protected]3b217f52009-06-01 17:54:20663 gcl commit change_name [--no_presubmit]
[email protected]fb2b8eb2009-04-23 21:03:42664 Commits the changelist to the repository.
665
666 gcl lint change_name
667 Check all the files in the changelist for possible style violations.
668
669Advanced commands:
670-----------------------------------------
671 gcl delete change_name
672 Deletes a changelist.
673
674 gcl diff change_name
675 Diffs all files in the changelist.
676
677 gcl presubmit change_name
678 Runs presubmit checks without uploading the changelist.
679
680 gcl diff
681 Diffs all files in the current directory and subdirectories that aren't in
682 a changelist.
683
684 gcl changes
685 Lists all the the changelists and the files in them.
686
687 gcl nothave [optional directory]
688 Lists files unknown to Subversion.
689
690 gcl opened
691 Lists modified files in the current directory and subdirectories.
692
693 gcl settings
694 Print the code review settings for this directory.
695
696 gcl status
697 Lists modified and unknown files in the current directory and
698 subdirectories.
699
700 gcl try change_name
701 Sends the change to the tryserver so a trybot can do a test run on your
702 code. To send multiple changes as one path, use a comma-separated list
703 of changenames.
704 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53705
[email protected]bfd09ce2009-08-05 21:17:23706 gcl deleteempties
707 Deletes all changelists that have no files associated with them. Careful,
708 you can lose your descriptions.
709
[email protected]3bcc6ce2009-05-12 22:53:53710 gcl help [command]
711 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42712""")
713
714def GetEditor():
715 editor = os.environ.get("SVN_EDITOR")
716 if not editor:
717 editor = os.environ.get("EDITOR")
718
719 if not editor:
720 if sys.platform.startswith("win"):
721 editor = "notepad"
722 else:
723 editor = "vi"
724
725 return editor
726
727
728def GenerateDiff(files, root=None):
729 """Returns a string containing the diff for the given file list.
730
731 The files in the list should either be absolute paths or relative to the
732 given root. If no root directory is provided, the repository root will be
733 used.
734 """
735 previous_cwd = os.getcwd()
736 if root is None:
737 os.chdir(GetRepositoryRoot())
738 else:
739 os.chdir(root)
740
741 diff = []
742 for file in files:
743 # Use svn info output instead of os.path.isdir because the latter fails
744 # when the file is deleted.
[email protected]5f3eee32009-09-17 00:34:30745 if gclient_scm.CaptureSVNInfo(file).get("Node Kind") in ("dir",
746 "directory"):
[email protected]fb2b8eb2009-04-23 21:03:42747 continue
748 # If the user specified a custom diff command in their svn config file,
749 # then it'll be used when we do svn diff, which we don't want to happen
750 # since we want the unified diff. Using --diff-cmd=diff doesn't always
751 # work, since they can have another diff executable in their path that
752 # gives different line endings. So we use a bogus temp directory as the
753 # config directory, which gets around these problems.
754 if sys.platform.startswith("win"):
755 parent_dir = tempfile.gettempdir()
756 else:
757 parent_dir = sys.path[0] # tempdir is not secure.
758 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
759 if not os.path.exists(bogus_dir):
760 os.mkdir(bogus_dir)
761 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
762 if output:
763 diff.append(output)
[email protected]c3150202009-05-13 14:31:01764 elif IsSVNMoved(file):
765 # svn diff on a mv/cp'd file outputs nothing.
766 # We put in an empty Index entry so upload.py knows about them.
[email protected]fb2b8eb2009-04-23 21:03:42767 diff.append("\nIndex: %s\n" % file)
[email protected]c3150202009-05-13 14:31:01768 else:
769 # The file is not modified anymore. It should be removed from the set.
770 pass
[email protected]fb2b8eb2009-04-23 21:03:42771 os.chdir(previous_cwd)
772 return "".join(diff)
773
774
[email protected]51ee0072009-06-08 19:20:05775
776def OptionallyDoPresubmitChecks(change_info, committing, args):
777 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
778 return True
[email protected]b0dfd352009-06-10 14:12:54779 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05780
781
[email protected]fb2b8eb2009-04-23 21:03:42782def UploadCL(change_info, args):
[email protected]17f59f22009-06-12 13:27:24783 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42784 print "Nothing to upload, changelist is empty."
785 return
[email protected]51ee0072009-06-08 19:20:05786 if not OptionallyDoPresubmitChecks(change_info, False, args):
787 return
788 no_try = FilterFlag(args, "--no_try") or FilterFlag(args, "--no-try")
[email protected]b2ab4942009-06-11 21:39:19789 no_watchlists = FilterFlag(args, "--no_watchlists") or \
790 FilterFlag(args, "--no-watchlists")
[email protected]fb2b8eb2009-04-23 21:03:42791
792 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05793 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42794 args.append("--send_mail")
795
796 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05797 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42798
[email protected]003c2692009-05-20 13:08:08799 # Disable try when the server is overridden.
800 server_1 = re.compile(r"^-s\b.*")
801 server_2 = re.compile(r"^--server\b.*")
802 for arg in args:
803 if server_1.match(arg) or server_2.match(arg):
804 no_try = True
805 break
[email protected]fb2b8eb2009-04-23 21:03:42806
807 upload_arg = ["upload.py", "-y"]
808 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
809 upload_arg.extend(args)
810
811 desc_file = ""
812 if change_info.issue: # Uploading a new patchset.
813 found_message = False
814 for arg in args:
815 if arg.startswith("--message") or arg.startswith("-m"):
816 found_message = True
817 break
818
819 if not found_message:
820 upload_arg.append("--message=''")
821
[email protected]32ba2602009-06-06 18:44:48822 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42823 else: # First time we upload.
824 handle, desc_file = tempfile.mkstemp(text=True)
825 os.write(handle, change_info.description)
826 os.close(handle)
827
[email protected]b2ab4942009-06-11 21:39:19828 # Watchlist processing -- CC people interested in this changeset
829 # http://dev.chromium.org/developers/contributing-code/watchlists
830 if not no_watchlists:
831 import watchlists
[email protected]17f59f22009-06-12 13:27:24832 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08833 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19834
[email protected]fb2b8eb2009-04-23 21:03:42835 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19836 if not no_watchlists and watchers:
837 # Filter out all empty elements and join by ','
838 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42839 if cc_list:
840 upload_arg.append("--cc=" + cc_list)
841 upload_arg.append("--description_file=" + desc_file + "")
842 if change_info.description:
843 subject = change_info.description[:77]
844 if subject.find("\r\n") != -1:
845 subject = subject[:subject.find("\r\n")]
846 if subject.find("\n") != -1:
847 subject = subject[:subject.find("\n")]
848 if len(change_info.description) > 77:
849 subject = subject + "..."
850 upload_arg.append("--message=" + subject)
851
852 # Change the current working directory before calling upload.py so that it
853 # shows the correct base.
854 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24855 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42856
857 # If we have a lot of files with long paths, then we won't be able to fit
858 # the command to "svn diff". Instead, we generate the diff manually for
859 # each file and concatenate them before passing it to upload.py.
860 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24861 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42862 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48863 if issue and patchset:
864 change_info.issue = int(issue)
865 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42866 change_info.Save()
867
868 if desc_file:
869 os.remove(desc_file)
870
871 # Do background work on Rietveld to lint the file so that the results are
872 # ready when the issue is viewed.
873 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
874
[email protected]57e78552009-09-11 23:04:30875 # Move back before considering try, so GetCodeReviewSettings is
876 # consistent.
877 os.chdir(previous_cwd)
878
[email protected]fb2b8eb2009-04-23 21:03:42879 # Once uploaded to Rietveld, send it to the try server.
880 if not no_try:
881 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
882 if try_on_upload and try_on_upload.lower() == 'true':
[email protected]32ba2602009-06-06 18:44:48883 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42884 if clobber:
[email protected]32ba2602009-06-06 18:44:48885 trychange_args.append('--clobber')
886 TryChange(change_info, trychange_args, swallow_exception=True)
[email protected]fb2b8eb2009-04-23 21:03:42887
[email protected]fb2b8eb2009-04-23 21:03:42888
889
890def PresubmitCL(change_info):
891 """Reports what presubmit checks on the change would report."""
[email protected]17f59f22009-06-12 13:27:24892 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42893 print "Nothing to presubmit check, changelist is empty."
894 return
895
896 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]b0dfd352009-06-10 14:12:54897 DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42898
[email protected]b0dfd352009-06-10 14:12:54899 print "\n*** Presubmit checks for COMMIT would report: ***"
900 DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42901
902
903def TryChange(change_info, args, swallow_exception):
904 """Create a diff file of change_info and send it to the try server."""
905 try:
906 import trychange
907 except ImportError:
908 if swallow_exception:
909 return
910 ErrorExit("You need to install trychange.py to use the try server.")
911
912 if change_info:
913 trychange_args = ['--name', change_info.name]
[email protected]32ba2602009-06-06 18:44:48914 if change_info.issue:
915 trychange_args.extend(["--issue", str(change_info.issue)])
916 if change_info.patchset:
917 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42918 trychange_args.extend(args)
919 trychange.TryChange(trychange_args,
[email protected]17f59f22009-06-12 13:27:24920 file_list=change_info.GetFileNames(),
[email protected]fb2b8eb2009-04-23 21:03:42921 swallow_exception=swallow_exception,
922 prog='gcl try')
923 else:
924 trychange.TryChange(args,
925 file_list=None,
926 swallow_exception=swallow_exception,
927 prog='gcl try')
928
929
930def Commit(change_info, args):
[email protected]17f59f22009-06-12 13:27:24931 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42932 print "Nothing to commit, changelist is empty."
933 return
[email protected]51ee0072009-06-08 19:20:05934 if not OptionallyDoPresubmitChecks(change_info, True, args):
935 return
[email protected]fb2b8eb2009-04-23 21:03:42936
[email protected]1bb04aa2009-06-01 17:52:11937 # We face a problem with svn here: Let's say change 'bleh' modifies
938 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
939 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
940 # The only fix is to use --non-recursive but that has its issues too:
941 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
942 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
943 # directory with child nodes". Yay...
944 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42945 filename = ''
946 if change_info.issue:
947 # Get the latest description from Rietveld.
948 change_info.description = GetIssueDescription(change_info.issue)
949
950 commit_message = change_info.description.replace('\r\n', '\n')
951 if change_info.issue:
[email protected]32ba2602009-06-06 18:44:48952 commit_message += ('\nReview URL: http://%s/%d' %
[email protected]fb2b8eb2009-04-23 21:03:42953 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
954 change_info.issue))
955
956 handle, commit_filename = tempfile.mkstemp(text=True)
957 os.write(handle, commit_message)
958 os.close(handle)
959
960 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24961 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42962 os.close(handle)
963
964 commit_cmd += ['--file=' + commit_filename]
965 commit_cmd += ['--targets=' + targets_filename]
966 # Change the current working directory before calling commit.
967 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24968 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42969 output = RunShell(commit_cmd, True)
970 os.remove(commit_filename)
971 os.remove(targets_filename)
972 if output.find("Committed revision") != -1:
973 change_info.Delete()
974
975 if change_info.issue:
976 revision = re.compile(".*?\nCommitted revision (\d+)",
977 re.DOTALL).match(output).group(1)
978 viewvc_url = GetCodeReviewSetting("VIEW_VC")
979 change_info.description = change_info.description + '\n'
980 if viewvc_url:
981 change_info.description += "\nCommitted: " + viewvc_url + revision
982 change_info.CloseIssue()
983 os.chdir(previous_cwd)
984
[email protected]2c8d4b22009-06-06 21:03:10985
[email protected]9ce98222009-10-19 20:24:17986def Change(change_info, args):
[email protected]fb2b8eb2009-04-23 21:03:42987 """Creates/edits a changelist."""
[email protected]9ce98222009-10-19 20:24:17988 silent = FilterFlag(args, "--silent")
989 if (len(args) == 1):
990 filename = args[0]
991 f = open(filename, 'rU')
992 override_description = f.read()
993 f.close()
994 else:
995 override_description = None
996
[email protected]fb2b8eb2009-04-23 21:03:42997 if change_info.issue:
998 try:
999 description = GetIssueDescription(change_info.issue)
1000 except urllib2.HTTPError, err:
1001 if err.code == 404:
1002 # The user deleted the issue in Rietveld, so forget the old issue id.
1003 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:101004 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:421005 change_info.Save()
1006 else:
1007 ErrorExit("Error getting the description from Rietveld: " + err)
1008 else:
[email protected]85532fc2009-06-04 22:36:531009 if override_description:
1010 description = override_description
1011 else:
1012 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:421013
1014 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:231015
[email protected]f0dfba32009-08-07 22:03:371016 # Edited files (as opposed to files with only changed properties) will have
1017 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:531018 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:371019 affected_files = [x for x in other_files if file_re.match(x[0])]
1020 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:421021
1022 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:241023 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:421024 "---Paths in this changelist (" + change_info.name + "):\n")
1025 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
1026 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:371027 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
1028
1029 if change_info.Exists():
1030 text += (separator2 +
1031 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
1032 else:
1033 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
1034 separator2)
1035 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:421036
1037 handle, filename = tempfile.mkstemp(text=True)
1038 os.write(handle, text)
1039 os.close(handle)
1040
[email protected]9ce98222009-10-19 20:24:171041 if not silent:
1042 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:421043
1044 result = ReadFile(filename)
1045 os.remove(filename)
1046
1047 if not result:
1048 return
1049
1050 split_result = result.split(separator1, 1)
1051 if len(split_result) != 2:
1052 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1053
1054 new_description = split_result[0]
1055 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531056 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421057 change_info.description = new_description
1058 if change_info.issue:
1059 # Update the Rietveld issue with the new description.
1060 change_info.UpdateRietveldDescription()
1061
1062 new_cl_files = []
1063 for line in cl_files_text.splitlines():
1064 if not len(line):
1065 continue
1066 if line.startswith("---"):
1067 break
1068 status = line[:7]
1069 file = line[7:]
1070 new_cl_files.append((status, file))
[email protected]bfd09ce2009-08-05 21:17:231071
1072 if (not len(change_info._files)) and (not change_info.issue) and \
1073 (not len(new_description) and (not new_cl_files)):
1074 ErrorExit("Empty changelist not saved")
1075
[email protected]17f59f22009-06-12 13:27:241076 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421077
1078 change_info.Save()
1079 print change_info.name + " changelist saved."
1080 if change_info.MissingTests():
1081 Warn("WARNING: " + MISSING_TEST_MSG)
1082
1083# We don't lint files in these path prefixes.
[email protected]a135e352009-09-15 04:59:401084IGNORE_PATHS = (os.path.join("webkit","api"),)
[email protected]fb2b8eb2009-04-23 21:03:421085
1086# Valid extensions for files we want to lint.
[email protected]bb816382009-10-29 01:38:021087LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
1088LINT_IGNORE_REGEX = r""
[email protected]fb2b8eb2009-04-23 21:03:421089
1090def Lint(change_info, args):
1091 """Runs cpplint.py on all the files in |change_info|"""
1092 try:
1093 import cpplint
1094 except ImportError:
1095 ErrorExit("You need to install cpplint.py to lint C++ files.")
1096
1097 # Change the current working directory before calling lint so that it
1098 # shows the correct base.
1099 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241100 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421101
1102 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241103 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421104
[email protected]bb816382009-10-29 01:38:021105 white_list = GetCodeReviewSetting("LINT_REGEX")
1106 if not white_list:
1107 white_list = LINT_REGEX
1108 white_regex = re.compile(white_list)
1109 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1110 if not black_list:
1111 black_list = LINT_IGNORE_REGEX
1112 black_regex = re.compile(black_list)
[email protected]fb2b8eb2009-04-23 21:03:421113 for file in filenames:
[email protected]bb816382009-10-29 01:38:021114 if white_regex.match(file):
1115 if black_regex.match(file):
1116 print "Ignoring file %s" % file
[email protected]fb2b8eb2009-04-23 21:03:421117 else:
1118 cpplint.ProcessFile(file, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021119 else:
1120 print "Skipping file %s" % file
[email protected]fb2b8eb2009-04-23 21:03:421121
1122 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1123 os.chdir(previous_cwd)
1124
1125
[email protected]b0dfd352009-06-10 14:12:541126def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421127 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1128 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481129 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151130 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411131 change = presubmit_support.SvnChange(change_info.name,
1132 change_info.description,
1133 change_info.GetLocalRoot(),
1134 change_info.GetFiles(),
1135 change_info.issue,
1136 change_info.patchset)
1137 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541138 committing=committing,
[email protected]1033acc2009-05-13 14:36:481139 verbose=False,
1140 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151141 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541142 default_presubmit=root_presubmit,
1143 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551144 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421145 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1146 return result
1147
1148
1149def Changes():
1150 """Print all the changelists and their files."""
1151 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081152 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421153 print "\n--- Changelist " + change_info.name + ":"
[email protected]17f59f22009-06-12 13:27:241154 for file in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421155 print "".join(file)
1156
1157
[email protected]bfd09ce2009-08-05 21:17:231158def DeleteEmptyChangeLists():
1159 """Delete all changelists that have no files."""
1160 print "\n--- Deleting:"
1161 for cl in GetCLs():
1162 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1163 if not len(change_info._files):
1164 print change_info.name
1165 change_info.Delete()
1166
1167
[email protected]fb2b8eb2009-04-23 21:03:421168def main(argv=None):
1169 if argv is None:
1170 argv = sys.argv
1171
1172 if len(argv) == 1:
1173 Help()
1174 return 0;
1175
[email protected]a05be0b2009-06-30 19:13:021176 try:
1177 # Create the directories where we store information about changelists if it
1178 # doesn't exist.
1179 if not os.path.exists(GetInfoDir()):
1180 os.mkdir(GetInfoDir())
1181 if not os.path.exists(GetChangesDir()):
1182 os.mkdir(GetChangesDir())
1183 # For smooth upgrade support, move the files in GetInfoDir() to
1184 # GetChangesDir().
1185 # TODO(maruel): Remove this code in August 2009.
1186 for file in os.listdir(unicode(GetInfoDir())):
1187 file_path = os.path.join(unicode(GetInfoDir()), file)
1188 if os.path.isfile(file_path) and file != CODEREVIEW_SETTINGS_FILE:
1189 shutil.move(file_path, GetChangesDir())
1190 if not os.path.exists(GetCacheDir()):
1191 os.mkdir(GetCacheDir())
[email protected]5f3eee32009-09-17 00:34:301192 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:021193 # Will throw an exception if not run in a svn checkout.
1194 pass
[email protected]fb2b8eb2009-04-23 21:03:421195
1196 # Commands that don't require an argument.
1197 command = argv[1]
[email protected]88c32d82009-10-12 18:24:051198 if command == "opened" or command == "status":
1199 Opened(command == "status")
[email protected]fb2b8eb2009-04-23 21:03:421200 return 0
1201 if command == "nothave":
[email protected]88c32d82009-10-12 18:24:051202 unknown_files = UnknownFiles(argv[2:])
1203 for file in unknown_files:
1204 print "? " + "".join(file)
[email protected]fb2b8eb2009-04-23 21:03:421205 return 0
1206 if command == "changes":
1207 Changes()
1208 return 0
1209 if command == "help":
1210 Help(argv[2:])
1211 return 0
1212 if command == "diff" and len(argv) == 2:
1213 files = GetFilesNotInCL()
1214 print GenerateDiff([x[1] for x in files])
1215 return 0
1216 if command == "settings":
1217 ignore = GetCodeReviewSetting("UNKNOWN");
[email protected]a005ccd2009-06-12 13:25:541218 del CODEREVIEW_SETTINGS['__just_initialized']
1219 print '\n'.join(("%s: %s" % (str(k), str(v))
1220 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
[email protected]fb2b8eb2009-04-23 21:03:421221 return 0
[email protected]bfd09ce2009-08-05 21:17:231222 if command == "deleteempties":
1223 DeleteEmptyChangeLists()
1224 return 0
[email protected]fb2b8eb2009-04-23 21:03:421225
1226 if len(argv) == 2:
1227 if command == "change":
1228 # Generate a random changelist name.
1229 changename = GenerateChangeName()
1230 else:
1231 ErrorExit("Need a changelist name.")
1232 else:
1233 changename = argv[2]
1234
1235 # When the command is 'try' and --patchset is used, the patch to try
1236 # is on the Rietveld server. 'change' creates a change so it's fine if the
1237 # change didn't exist. All other commands require an existing change.
1238 fail_on_not_found = command != "try" and command != "change"
1239 if command == "try" and changename.find(',') != -1:
[email protected]8d5c9a52009-06-12 15:59:081240 change_info = LoadChangelistInfoForMultiple(changename, GetRepositoryRoot(),
1241 True, True)
[email protected]fb2b8eb2009-04-23 21:03:421242 else:
[email protected]8d5c9a52009-06-12 15:59:081243 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(),
1244 fail_on_not_found, True)
[email protected]fb2b8eb2009-04-23 21:03:421245
1246 if command == "change":
[email protected]9ce98222009-10-19 20:24:171247 Change(change_info, argv[3:])
[email protected]fb2b8eb2009-04-23 21:03:421248 elif command == "lint":
1249 Lint(change_info, argv[3:])
1250 elif command == "upload":
1251 UploadCL(change_info, argv[3:])
1252 elif command == "presubmit":
1253 PresubmitCL(change_info)
1254 elif command in ("commit", "submit"):
1255 Commit(change_info, argv[3:])
1256 elif command == "delete":
1257 change_info.Delete()
1258 elif command == "try":
1259 # When the change contains no file, send the "changename" positional
1260 # argument to trychange.py.
[email protected]17f59f22009-06-12 13:27:241261 if change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421262 args = argv[3:]
1263 else:
1264 change_info = None
1265 args = argv[2:]
1266 TryChange(change_info, args, swallow_exception=False)
1267 else:
1268 # Everything else that is passed into gcl we redirect to svn, after adding
1269 # the files. This allows commands such as 'gcl diff xxx' to work.
1270 args =["svn", command]
1271 root = GetRepositoryRoot()
[email protected]17f59f22009-06-12 13:27:241272 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
[email protected]fb2b8eb2009-04-23 21:03:421273 RunShell(args, True)
1274 return 0
1275
1276
1277if __name__ == "__main__":
1278 sys.exit(main())