blob: 4258f400eb05b8b439d62b381f6c0ffe816af618 [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]5aeb7dd2009-11-17 18:09:0127__version__ = '1.1.2'
[email protected]c1675e22009-04-27 20:30:4828
29
[email protected]fb2b8eb2009-04-23 21:03:4230CODEREVIEW_SETTINGS = {
31 # Default values.
32 "CODE_REVIEW_SERVER": "codereview.chromium.org",
33 "CC_LIST": "[email protected]",
34 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
35}
36
[email protected]fb2b8eb2009-04-23 21:03:4237# globals that store the root of the current repository and the directory where
38# we store information about changelists.
[email protected]98fc2b92009-05-21 14:11:5139REPOSITORY_ROOT = ""
[email protected]fb2b8eb2009-04-23 21:03:4240
41# Filename where we store repository specific information for gcl.
42CODEREVIEW_SETTINGS_FILE = "codereview.settings"
43
44# Warning message when the change appears to be missing tests.
45MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
46
[email protected]98fc2b92009-05-21 14:11:5147# Global cache of files cached in GetCacheDir().
48FILES_CACHE = {}
[email protected]fb2b8eb2009-04-23 21:03:4249
50
[email protected]207fdf32009-04-28 19:57:0151def UnknownFiles(extra_args):
[email protected]5aeb7dd2009-11-17 18:09:0152 """Runs svn status and returns unknown files.
[email protected]207fdf32009-04-28 19:57:0153
54 Any args in |extra_args| are passed to the tool to support giving alternate
55 code locations.
56 """
[email protected]5aeb7dd2009-11-17 18:09:0157 return [item[1] for item in SVN.CaptureStatus(extra_args)
[email protected]4810a962009-05-12 21:03:3458 if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0159
60
[email protected]fb2b8eb2009-04-23 21:03:4261def GetRepositoryRoot():
62 """Returns the top level directory of the current repository.
63
64 The directory is returned as an absolute path.
65 """
[email protected]98fc2b92009-05-21 14:11:5166 global REPOSITORY_ROOT
67 if not REPOSITORY_ROOT:
[email protected]94b1ee92009-12-19 20:27:2068 REPOSITORY_ROOT = SVN.GetCheckoutRoot(os.getcwd())
69 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:3070 raise gclient_utils.Error("gcl run outside of repository")
[email protected]98fc2b92009-05-21 14:11:5171 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:4272
73
74def GetInfoDir():
75 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:5276 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
77
78
79def GetChangesDir():
80 """Returns the directory where gcl change files are stored."""
81 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:4282
83
[email protected]98fc2b92009-05-21 14:11:5184def GetCacheDir():
85 """Returns the directory where gcl change files are stored."""
86 return os.path.join(GetInfoDir(), 'cache')
87
88
89def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
90 """Retrieves a file from the repository and caches it in GetCacheDir() for
91 max_age seconds.
92
93 use_root: If False, look up the arborescence for the first match, otherwise go
94 directory to the root repository.
95
96 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:0297 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:5198 """
99 global FILES_CACHE
100 if filename not in FILES_CACHE:
101 # Don't try to look up twice.
102 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28103 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02104 try:
105 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]5f3eee32009-09-17 00:34:30106 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:02107 return None
[email protected]98fc2b92009-05-21 14:11:51108 if (not os.path.exists(cached_file) or
109 os.stat(cached_file).st_mtime > max_age):
[email protected]5aeb7dd2009-11-17 18:09:01110 dir_info = SVN.CaptureInfo(".")
[email protected]9b613272009-04-24 01:28:28111 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51112 if use_root:
113 url_path = repo_root
114 else:
115 url_path = dir_info["URL"]
116 content = ""
[email protected]9b613272009-04-24 01:28:28117 while True:
[email protected]fa44e4a2009-12-03 01:41:13118 # Look in the repository at the current level for the file.
119 svn_path = url_path + "/" + filename
120 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
[email protected]9b613272009-04-24 01:28:28121 if not rc:
[email protected]98fc2b92009-05-21 14:11:51122 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28123 break
124 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51125 content = ""
[email protected]9b613272009-04-24 01:28:28126 if url_path == repo_root:
127 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51128 break
[email protected]9b613272009-04-24 01:28:28129 # Go up one level to try again.
130 url_path = os.path.dirname(url_path)
[email protected]9b613272009-04-24 01:28:28131 # Write a cached version even if there isn't a file, so we don't try to
132 # fetch it each time.
[email protected]fc83c112009-12-18 15:14:10133 gclient_utils.FileWrite(cached_file, content)
[email protected]98fc2b92009-05-21 14:11:51134 else:
[email protected]0fca4f32009-12-18 15:14:34135 content = gclient_utils.FileRead(cached_file, 'r')
[email protected]98fc2b92009-05-21 14:11:51136 # Keep the content cached in memory.
137 FILES_CACHE[filename] = content
138 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28139
[email protected]98fc2b92009-05-21 14:11:51140
141def GetCodeReviewSetting(key):
142 """Returns a value for the given key for this repository."""
143 # Use '__just_initialized' as a flag to determine if the settings were
144 # already initialized.
145 global CODEREVIEW_SETTINGS
146 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47147 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
148 if settings_file:
149 for line in settings_file.splitlines():
150 if not line or line.startswith("#"):
151 continue
152 k, v = line.split(": ", 1)
153 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51154 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42155 return CODEREVIEW_SETTINGS.get(key, "")
156
157
[email protected]fb2b8eb2009-04-23 21:03:42158def Warn(msg):
159 ErrorExit(msg, exit=False)
160
161
162def ErrorExit(msg, exit=True):
163 """Print an error message to stderr and optionally exit."""
164 print >>sys.stderr, msg
165 if exit:
166 sys.exit(1)
167
168
169def RunShellWithReturnCode(command, print_output=False):
170 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02171 # Use a shell for subcommands on Windows to get a PATH search, and because svn
172 # may be a batch file.
173 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42174 p = subprocess.Popen(command, stdout=subprocess.PIPE,
175 stderr=subprocess.STDOUT, shell=use_shell,
176 universal_newlines=True)
177 if print_output:
178 output_array = []
179 while True:
180 line = p.stdout.readline()
181 if not line:
182 break
183 if print_output:
184 print line.strip('\n')
185 output_array.append(line)
186 output = "".join(output_array)
187 else:
188 output = p.stdout.read()
189 p.wait()
190 p.stdout.close()
191 return output, p.returncode
192
193
194def RunShell(command, print_output=False):
195 """Executes a command and returns the output."""
196 return RunShellWithReturnCode(command, print_output)[0]
197
198
[email protected]51ee0072009-06-08 19:20:05199def FilterFlag(args, flag):
200 """Returns True if the flag is present in args list.
201
202 The flag is removed from args if present.
203 """
204 if flag in args:
205 args.remove(flag)
206 return True
207 return False
208
209
[email protected]be0d1ca2009-05-12 19:23:02210class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42211 """Holds information about a changelist.
212
[email protected]32ba2602009-06-06 18:44:48213 name: change name.
214 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
215 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42216 description: the description.
217 files: a list of 2 tuple containing (status, filename) of changed files,
218 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08219 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42220 """
[email protected]32ba2602009-06-06 18:44:48221
222 _SEPARATOR = "\n-----\n"
223 # The info files have the following format:
224 # issue_id, patchset\n (, patchset is optional)
225 # _SEPARATOR\n
226 # filepath1\n
227 # filepath2\n
228 # .
229 # .
230 # filepathn\n
231 # _SEPARATOR\n
232 # description
233
[email protected]ea452b32009-11-22 20:04:31234 def __init__(self, name, issue, patchset, description, files, local_root,
235 needs_upload=False):
[email protected]fb2b8eb2009-04-23 21:03:42236 self.name = name
[email protected]32ba2602009-06-06 18:44:48237 self.issue = int(issue)
238 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42239 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02240 if files is None:
241 files = []
[email protected]17f59f22009-06-12 13:27:24242 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42243 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08244 self._local_root = local_root
[email protected]ea452b32009-11-22 20:04:31245 self.needs_upload = needs_upload
246
247 def NeedsUpload(self):
248 return self.needs_upload
[email protected]fb2b8eb2009-04-23 21:03:42249
[email protected]17f59f22009-06-12 13:27:24250 def GetFileNames(self):
251 """Returns the list of file names included in this change."""
[email protected]e3608df2009-11-10 20:22:57252 return [f[1] for f in self._files]
[email protected]17f59f22009-06-12 13:27:24253
254 def GetFiles(self):
255 """Returns the list of files included in this change with their status."""
256 return self._files
257
258 def GetLocalRoot(self):
259 """Returns the local repository checkout root directory."""
260 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42261
[email protected]f0dfba32009-08-07 22:03:37262 def Exists(self):
263 """Returns True if this change already exists (i.e., is not new)."""
264 return (self.issue or self.description or self._files)
265
[email protected]fb2b8eb2009-04-23 21:03:42266 def _NonDeletedFileList(self):
267 """Returns a list of files in this change, not including deleted files."""
[email protected]e3608df2009-11-10 20:22:57268 return [f[1] for f in self.GetFiles()
269 if not f[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42270
271 def _AddedFileList(self):
272 """Returns a list of files added in this change."""
[email protected]e3608df2009-11-10 20:22:57273 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42274
275 def Save(self):
276 """Writes the changelist information to disk."""
[email protected]ea452b32009-11-22 20:04:31277 if self.NeedsUpload():
278 needs_upload = "dirty"
279 else:
280 needs_upload = "clean"
[email protected]32ba2602009-06-06 18:44:48281 data = ChangeInfo._SEPARATOR.join([
[email protected]ea452b32009-11-22 20:04:31282 "%d, %d, %s" % (self.issue, self.patchset, needs_upload),
[email protected]17f59f22009-06-12 13:27:24283 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48284 self.description])
[email protected]fc83c112009-12-18 15:14:10285 gclient_utils.FileWrite(GetChangelistInfoFile(self.name), data)
[email protected]fb2b8eb2009-04-23 21:03:42286
287 def Delete(self):
288 """Removes the changelist information from disk."""
289 os.remove(GetChangelistInfoFile(self.name))
290
291 def CloseIssue(self):
292 """Closes the Rietveld issue for this changelist."""
293 data = [("description", self.description),]
294 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10295 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42296
297 def UpdateRietveldDescription(self):
298 """Sets the description for an issue on Rietveld."""
299 data = [("description", self.description),]
300 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10301 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42302
303 def MissingTests(self):
304 """Returns True if the change looks like it needs unit tests but has none.
305
306 A change needs unit tests if it contains any new source files or methods.
307 """
308 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
309 # Ignore third_party entirely.
[email protected]e3608df2009-11-10 20:22:57310 files = [f for f in self._NonDeletedFileList()
311 if f.find("third_party") == -1]
312 added_files = [f for f in self._AddedFileList()
313 if f.find("third_party") == -1]
[email protected]fb2b8eb2009-04-23 21:03:42314
315 # If the change is entirely in third_party, we're done.
316 if len(files) == 0:
317 return False
318
319 # Any new or modified test files?
320 # A test file's name ends with "test.*" or "tests.*".
321 test_files = [test for test in files
322 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
323 if len(test_files) > 0:
324 return False
325
326 # Any new source files?
[email protected]e3608df2009-11-10 20:22:57327 source_files = [item for item in added_files
328 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
[email protected]fb2b8eb2009-04-23 21:03:42329 if len(source_files) > 0:
330 return True
331
332 # Do the long test, checking the files for new methods.
333 return self._HasNewMethod()
334
335 def _HasNewMethod(self):
336 """Returns True if the changeset contains any new functions, or if a
337 function signature has been changed.
338
339 A function is identified by starting flush left, containing a "(" before
340 the next flush-left line, and either ending with "{" before the next
341 flush-left line or being followed by an unindented "{".
342
343 Currently this returns True for new methods, new static functions, and
344 methods or functions whose signatures have been changed.
345
346 Inline methods added to header files won't be detected by this. That's
347 acceptable for purposes of determining if a unit test is needed, since
348 inline methods should be trivial.
349 """
350 # To check for methods added to source or header files, we need the diffs.
351 # We'll generate them all, since there aren't likely to be many files
352 # apart from source and headers; besides, we'll want them all if we're
353 # uploading anyway.
354 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24355 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42356
357 definition = ""
358 for line in self.patch.splitlines():
359 if not line.startswith("+"):
360 continue
361 line = line.strip("+").rstrip(" \t")
362 # Skip empty lines, comments, and preprocessor directives.
363 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
364 if line == "" or line.startswith("/") or line.startswith("#"):
365 continue
366
367 # A possible definition ending with "{" is complete, so check it.
368 if definition.endswith("{"):
369 if definition.find("(") != -1:
370 return True
371 definition = ""
372
373 # A { or an indented line, when we're in a definition, continues it.
374 if (definition != "" and
375 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
376 definition += line
377
378 # A flush-left line starts a new possible function definition.
379 elif not line.startswith(" ") and not line.startswith("\t"):
380 definition = line
381
382 return False
383
[email protected]32ba2602009-06-06 18:44:48384 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08385 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48386 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42387
[email protected]32ba2602009-06-06 18:44:48388 Args:
389 fail_on_not_found: if True, this function will quit the program if the
390 changelist doesn't exist.
391 update_status: if True, the svn status will be updated for all the files
392 and unchanged files will be removed.
393
394 Returns: a ChangeInfo object.
395 """
396 info_file = GetChangelistInfoFile(changename)
397 if not os.path.exists(info_file):
398 if fail_on_not_found:
399 ErrorExit("Changelist " + changename + " not found.")
[email protected]ea452b32009-11-22 20:04:31400 return ChangeInfo(changename, 0, 0, '', None, local_root,
401 needs_upload=False)
[email protected]0fca4f32009-12-18 15:14:34402 split_data = gclient_utils.FileRead(info_file, 'r').split(
403 ChangeInfo._SEPARATOR, 2)
[email protected]32ba2602009-06-06 18:44:48404 if len(split_data) != 3:
405 ErrorExit("Changelist file %s is corrupt" % info_file)
[email protected]ea452b32009-11-22 20:04:31406 items = split_data[0].split(', ')
[email protected]32ba2602009-06-06 18:44:48407 issue = 0
408 patchset = 0
[email protected]ea452b32009-11-22 20:04:31409 needs_upload = False
[email protected]32ba2602009-06-06 18:44:48410 if items[0]:
411 issue = int(items[0])
412 if len(items) > 1:
413 patchset = int(items[1])
[email protected]ea452b32009-11-22 20:04:31414 if len(items) > 2:
415 needs_upload = (items[2] == "dirty")
[email protected]32ba2602009-06-06 18:44:48416 files = []
417 for line in split_data[1].splitlines():
418 status = line[:7]
[email protected]e3608df2009-11-10 20:22:57419 filename = line[7:]
420 files.append((status, filename))
[email protected]32ba2602009-06-06 18:44:48421 description = split_data[2]
422 save = False
423 if update_status:
[email protected]e3608df2009-11-10 20:22:57424 for item in files:
425 filename = os.path.join(local_root, item[1])
[email protected]5aeb7dd2009-11-17 18:09:01426 status_result = SVN.CaptureStatus(filename)
[email protected]32ba2602009-06-06 18:44:48427 if not status_result or not status_result[0][0]:
428 # File has been reverted.
429 save = True
[email protected]e3608df2009-11-10 20:22:57430 files.remove(item)
[email protected]32ba2602009-06-06 18:44:48431 continue
432 status = status_result[0][0]
[email protected]e3608df2009-11-10 20:22:57433 if status != item[0]:
[email protected]32ba2602009-06-06 18:44:48434 save = True
[email protected]e3608df2009-11-10 20:22:57435 files[files.index(item)] = (status, item[1])
[email protected]8d5c9a52009-06-12 15:59:08436 change_info = ChangeInfo(changename, issue, patchset, description, files,
[email protected]ea452b32009-11-22 20:04:31437 local_root, needs_upload)
[email protected]32ba2602009-06-06 18:44:48438 if save:
439 change_info.Save()
440 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42441
442
443def GetChangelistInfoFile(changename):
444 """Returns the file that stores information about a changelist."""
445 if not changename or re.search(r'[^\w-]', changename):
446 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52447 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42448
449
[email protected]8d5c9a52009-06-12 15:59:08450def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
451 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42452 """Loads many changes and merge their files list into one pseudo change.
453
454 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
455 """
456 changes = changenames.split(',')
[email protected]ea452b32009-11-22 20:04:31457 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root,
458 needs_upload=False)
[email protected]fb2b8eb2009-04-23 21:03:42459 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08460 aggregate_change_info._files += ChangeInfo.Load(change,
461 local_root,
462 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24463 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42464 return aggregate_change_info
465
466
[email protected]fb2b8eb2009-04-23 21:03:42467def GetCLs():
468 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52469 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42470 if CODEREVIEW_SETTINGS_FILE in cls:
471 cls.remove(CODEREVIEW_SETTINGS_FILE)
472 return cls
473
474
475def GenerateChangeName():
476 """Generate a random changelist name."""
477 random.seed()
478 current_cl_names = GetCLs()
479 while True:
480 cl_name = (random.choice(string.ascii_lowercase) +
481 random.choice(string.digits) +
482 random.choice(string.ascii_lowercase) +
483 random.choice(string.digits))
484 if cl_name not in current_cl_names:
485 return cl_name
486
487
488def GetModifiedFiles():
489 """Returns a set that maps from changelist name to (status,filename) tuples.
490
491 Files not in a changelist have an empty changelist name. Filenames are in
492 relation to the top level directory of the current repository. Note that
493 only the current directory and subdirectories are scanned, in order to
494 improve performance while still being flexible.
495 """
496 files = {}
497
498 # Since the files are normalized to the root folder of the repositary, figure
499 # out what we need to add to the paths.
500 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
501
502 # Get a list of all files in changelists.
503 files_in_cl = {}
504 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08505 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
506 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24507 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42508 files_in_cl[filename] = change_info.name
509
510 # Get all the modified files.
[email protected]5aeb7dd2009-11-17 18:09:01511 status_result = SVN.CaptureStatus(None)
[email protected]207fdf32009-04-28 19:57:01512 for line in status_result:
513 status = line[0]
514 filename = line[1]
515 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42516 continue
[email protected]fb2b8eb2009-04-23 21:03:42517 if dir_prefix:
518 filename = os.path.join(dir_prefix, filename)
519 change_list_name = ""
520 if filename in files_in_cl:
521 change_list_name = files_in_cl[filename]
522 files.setdefault(change_list_name, []).append((status, filename))
523
524 return files
525
526
527def GetFilesNotInCL():
528 """Returns a list of tuples (status,filename) that aren't in any changelists.
529
530 See docstring of GetModifiedFiles for information about path of files and
531 which directories are scanned.
532 """
533 modified_files = GetModifiedFiles()
534 if "" not in modified_files:
535 return []
536 return modified_files[""]
537
538
539def SendToRietveld(request_path, payload=None,
540 content_type="application/octet-stream", timeout=None):
541 """Send a POST/GET to Rietveld. Returns the response body."""
[email protected]3884d762009-11-25 00:01:22542 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
[email protected]fb2b8eb2009-04-23 21:03:42543 def GetUserCredentials():
544 """Prompts the user for a username and password."""
[email protected]3884d762009-11-25 00:01:22545 email = upload.GetEmail("Email (login for uploading to %s)" % server)
[email protected]fb2b8eb2009-04-23 21:03:42546 password = getpass.getpass("Password for %s: " % email)
547 return email, password
[email protected]fb2b8eb2009-04-23 21:03:42548 rpc_server = upload.HttpRpcServer(server,
549 GetUserCredentials,
550 host_override=server,
551 save_cookies=True)
552 try:
553 return rpc_server.Send(request_path, payload, content_type, timeout)
[email protected]e3608df2009-11-10 20:22:57554 except urllib2.URLError:
[email protected]fb2b8eb2009-04-23 21:03:42555 if timeout is None:
556 ErrorExit("Error accessing url %s" % request_path)
557 else:
558 return None
559
560
561def GetIssueDescription(issue):
562 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48563 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42564
565
[email protected]88c32d82009-10-12 18:24:05566def Opened(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42567 """Prints a list of modified files in the current directory down."""
568 files = GetModifiedFiles()
569 cl_keys = files.keys()
570 cl_keys.sort()
571 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05572 if not cl_name:
573 continue
574 note = ""
575 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
576 fail_on_not_found=True, update_status=False)
577 if len(change_info.GetFiles()) != len(files[cl_name]):
578 note = " (Note: this changelist contains files outside this directory)"
579 print "\n--- Changelist " + cl_name + note + ":"
[email protected]e3608df2009-11-10 20:22:57580 for filename in files[cl_name]:
581 print "".join(filename)
[email protected]88c32d82009-10-12 18:24:05582 if show_unknown_files:
583 unknown_files = UnknownFiles([])
584 if (files.get('') or (show_unknown_files and len(unknown_files))):
585 print "\n--- Not in any changelist:"
[email protected]e3608df2009-11-10 20:22:57586 for item in files.get('', []):
587 print "".join(item)
[email protected]88c32d82009-10-12 18:24:05588 if show_unknown_files:
[email protected]e3608df2009-11-10 20:22:57589 for filename in unknown_files:
590 print "? %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:42591
592
593def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53594 if argv:
595 if argv[0] == 'try':
596 TryChange(None, ['--help'], swallow_exception=False)
597 return
598 if argv[0] == 'upload':
599 upload.RealMain(['upload.py', '--help'])
600 return
[email protected]fb2b8eb2009-04-23 21:03:42601
602 print (
603"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48604version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42605
606Basic commands:
607-----------------------------------------
608 gcl change change_name
609 Add/remove files to a changelist. Only scans the current directory and
610 subdirectories.
611
612 gcl upload change_name [-r [email protected],[email protected],...]
613 [--send_mail] [--no_try] [--no_presubmit]
[email protected]b2ab4942009-06-11 21:39:19614 [--no_watchlists]
[email protected]fb2b8eb2009-04-23 21:03:42615 Uploads the changelist to the server for review.
616
[email protected]3b217f52009-06-01 17:54:20617 gcl commit change_name [--no_presubmit]
[email protected]fb2b8eb2009-04-23 21:03:42618 Commits the changelist to the repository.
619
620 gcl lint change_name
621 Check all the files in the changelist for possible style violations.
622
623Advanced commands:
624-----------------------------------------
625 gcl delete change_name
626 Deletes a changelist.
627
628 gcl diff change_name
629 Diffs all files in the changelist.
630
631 gcl presubmit change_name
632 Runs presubmit checks without uploading the changelist.
633
634 gcl diff
635 Diffs all files in the current directory and subdirectories that aren't in
636 a changelist.
637
638 gcl changes
639 Lists all the the changelists and the files in them.
640
641 gcl nothave [optional directory]
642 Lists files unknown to Subversion.
643
644 gcl opened
645 Lists modified files in the current directory and subdirectories.
646
647 gcl settings
648 Print the code review settings for this directory.
649
650 gcl status
651 Lists modified and unknown files in the current directory and
652 subdirectories.
653
654 gcl try change_name
655 Sends the change to the tryserver so a trybot can do a test run on your
656 code. To send multiple changes as one path, use a comma-separated list
657 of changenames.
658 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53659
[email protected]bfd09ce2009-08-05 21:17:23660 gcl deleteempties
661 Deletes all changelists that have no files associated with them. Careful,
662 you can lose your descriptions.
663
[email protected]3bcc6ce2009-05-12 22:53:53664 gcl help [command]
665 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42666""")
667
668def GetEditor():
669 editor = os.environ.get("SVN_EDITOR")
670 if not editor:
671 editor = os.environ.get("EDITOR")
672
673 if not editor:
674 if sys.platform.startswith("win"):
675 editor = "notepad"
676 else:
677 editor = "vi"
678
679 return editor
680
681
682def GenerateDiff(files, root=None):
683 """Returns a string containing the diff for the given file list.
684
685 The files in the list should either be absolute paths or relative to the
686 given root. If no root directory is provided, the repository root will be
687 used.
688 """
689 previous_cwd = os.getcwd()
690 if root is None:
691 os.chdir(GetRepositoryRoot())
692 else:
693 os.chdir(root)
694
[email protected]63ebf042009-12-08 20:43:44695 # If the user specified a custom diff command in their svn config file,
696 # then it'll be used when we do svn diff, which we don't want to happen
697 # since we want the unified diff. Using --diff-cmd=diff doesn't always
698 # work, since they can have another diff executable in their path that
699 # gives different line endings. So we use a bogus temp directory as the
700 # config directory, which gets around these problems.
701 bogus_dir = tempfile.mkdtemp()
[email protected]fb2b8eb2009-04-23 21:03:42702 diff = []
[email protected]e3608df2009-11-10 20:22:57703 for filename in files:
[email protected]5aeb7dd2009-11-17 18:09:01704 # TODO(maruel): Use SVN.DiffItem().
[email protected]fb2b8eb2009-04-23 21:03:42705 # Use svn info output instead of os.path.isdir because the latter fails
706 # when the file is deleted.
[email protected]5aeb7dd2009-11-17 18:09:01707 if SVN.CaptureInfo(filename).get('Node Kind') == 'directory':
[email protected]fb2b8eb2009-04-23 21:03:42708 continue
[email protected]e3608df2009-11-10 20:22:57709 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, filename])
[email protected]fb2b8eb2009-04-23 21:03:42710 if output:
711 diff.append(output)
[email protected]5aeb7dd2009-11-17 18:09:01712 elif SVN.IsMoved(filename):
[email protected]c3150202009-05-13 14:31:01713 # svn diff on a mv/cp'd file outputs nothing.
714 # We put in an empty Index entry so upload.py knows about them.
[email protected]e3608df2009-11-10 20:22:57715 diff.append("\nIndex: %s\n" % filename)
[email protected]c3150202009-05-13 14:31:01716 else:
717 # The file is not modified anymore. It should be removed from the set.
718 pass
[email protected]63ebf042009-12-08 20:43:44719 shutil.rmtree(bogus_dir)
[email protected]fb2b8eb2009-04-23 21:03:42720 os.chdir(previous_cwd)
721 return "".join(diff)
722
723
[email protected]51ee0072009-06-08 19:20:05724
725def OptionallyDoPresubmitChecks(change_info, committing, args):
726 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
727 return True
[email protected]b0dfd352009-06-10 14:12:54728 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05729
730
[email protected]fb2b8eb2009-04-23 21:03:42731def UploadCL(change_info, args):
[email protected]17f59f22009-06-12 13:27:24732 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42733 print "Nothing to upload, changelist is empty."
734 return
[email protected]51ee0072009-06-08 19:20:05735 if not OptionallyDoPresubmitChecks(change_info, False, args):
736 return
737 no_try = FilterFlag(args, "--no_try") or FilterFlag(args, "--no-try")
[email protected]b2ab4942009-06-11 21:39:19738 no_watchlists = FilterFlag(args, "--no_watchlists") or \
739 FilterFlag(args, "--no-watchlists")
[email protected]fb2b8eb2009-04-23 21:03:42740
741 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05742 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42743 args.append("--send_mail")
744
745 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05746 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42747
[email protected]003c2692009-05-20 13:08:08748 # Disable try when the server is overridden.
749 server_1 = re.compile(r"^-s\b.*")
750 server_2 = re.compile(r"^--server\b.*")
751 for arg in args:
752 if server_1.match(arg) or server_2.match(arg):
753 no_try = True
754 break
[email protected]fb2b8eb2009-04-23 21:03:42755
756 upload_arg = ["upload.py", "-y"]
757 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
758 upload_arg.extend(args)
759
760 desc_file = ""
761 if change_info.issue: # Uploading a new patchset.
762 found_message = False
763 for arg in args:
764 if arg.startswith("--message") or arg.startswith("-m"):
765 found_message = True
766 break
767
768 if not found_message:
769 upload_arg.append("--message=''")
770
[email protected]32ba2602009-06-06 18:44:48771 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42772 else: # First time we upload.
773 handle, desc_file = tempfile.mkstemp(text=True)
774 os.write(handle, change_info.description)
775 os.close(handle)
776
[email protected]b2ab4942009-06-11 21:39:19777 # Watchlist processing -- CC people interested in this changeset
778 # http://dev.chromium.org/developers/contributing-code/watchlists
779 if not no_watchlists:
780 import watchlists
[email protected]17f59f22009-06-12 13:27:24781 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08782 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19783
[email protected]fb2b8eb2009-04-23 21:03:42784 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19785 if not no_watchlists and watchers:
786 # Filter out all empty elements and join by ','
787 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42788 if cc_list:
789 upload_arg.append("--cc=" + cc_list)
790 upload_arg.append("--description_file=" + desc_file + "")
791 if change_info.description:
792 subject = change_info.description[:77]
793 if subject.find("\r\n") != -1:
794 subject = subject[:subject.find("\r\n")]
795 if subject.find("\n") != -1:
796 subject = subject[:subject.find("\n")]
797 if len(change_info.description) > 77:
798 subject = subject + "..."
799 upload_arg.append("--message=" + subject)
800
801 # Change the current working directory before calling upload.py so that it
802 # shows the correct base.
803 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24804 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42805
806 # If we have a lot of files with long paths, then we won't be able to fit
807 # the command to "svn diff". Instead, we generate the diff manually for
808 # each file and concatenate them before passing it to upload.py.
809 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24810 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42811 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48812 if issue and patchset:
813 change_info.issue = int(issue)
814 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42815 change_info.Save()
816
817 if desc_file:
818 os.remove(desc_file)
819
820 # Do background work on Rietveld to lint the file so that the results are
821 # ready when the issue is viewed.
822 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
823
[email protected]57e78552009-09-11 23:04:30824 # Move back before considering try, so GetCodeReviewSettings is
825 # consistent.
826 os.chdir(previous_cwd)
827
[email protected]fb2b8eb2009-04-23 21:03:42828 # Once uploaded to Rietveld, send it to the try server.
829 if not no_try:
830 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
831 if try_on_upload and try_on_upload.lower() == 'true':
[email protected]32ba2602009-06-06 18:44:48832 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42833 if clobber:
[email protected]32ba2602009-06-06 18:44:48834 trychange_args.append('--clobber')
835 TryChange(change_info, trychange_args, swallow_exception=True)
[email protected]fb2b8eb2009-04-23 21:03:42836
[email protected]fb2b8eb2009-04-23 21:03:42837
838
839def PresubmitCL(change_info):
840 """Reports what presubmit checks on the change would report."""
[email protected]17f59f22009-06-12 13:27:24841 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42842 print "Nothing to presubmit check, changelist is empty."
843 return
844
845 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]b0dfd352009-06-10 14:12:54846 DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42847
[email protected]b0dfd352009-06-10 14:12:54848 print "\n*** Presubmit checks for COMMIT would report: ***"
849 DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42850
851
852def TryChange(change_info, args, swallow_exception):
853 """Create a diff file of change_info and send it to the try server."""
854 try:
855 import trychange
856 except ImportError:
857 if swallow_exception:
858 return
859 ErrorExit("You need to install trychange.py to use the try server.")
860
861 if change_info:
862 trychange_args = ['--name', change_info.name]
[email protected]32ba2602009-06-06 18:44:48863 if change_info.issue:
864 trychange_args.extend(["--issue", str(change_info.issue)])
865 if change_info.patchset:
866 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42867 trychange_args.extend(args)
868 trychange.TryChange(trychange_args,
[email protected]17f59f22009-06-12 13:27:24869 file_list=change_info.GetFileNames(),
[email protected]fb2b8eb2009-04-23 21:03:42870 swallow_exception=swallow_exception,
871 prog='gcl try')
872 else:
873 trychange.TryChange(args,
874 file_list=None,
875 swallow_exception=swallow_exception,
876 prog='gcl try')
877
878
879def Commit(change_info, args):
[email protected]17f59f22009-06-12 13:27:24880 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42881 print "Nothing to commit, changelist is empty."
882 return
[email protected]51ee0072009-06-08 19:20:05883 if not OptionallyDoPresubmitChecks(change_info, True, args):
884 return
[email protected]fb2b8eb2009-04-23 21:03:42885
[email protected]1bb04aa2009-06-01 17:52:11886 # We face a problem with svn here: Let's say change 'bleh' modifies
887 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
888 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
889 # The only fix is to use --non-recursive but that has its issues too:
890 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
891 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
892 # directory with child nodes". Yay...
893 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42894 if change_info.issue:
895 # Get the latest description from Rietveld.
896 change_info.description = GetIssueDescription(change_info.issue)
897
898 commit_message = change_info.description.replace('\r\n', '\n')
899 if change_info.issue:
[email protected]32ba2602009-06-06 18:44:48900 commit_message += ('\nReview URL: http://%s/%d' %
[email protected]fb2b8eb2009-04-23 21:03:42901 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
902 change_info.issue))
903
904 handle, commit_filename = tempfile.mkstemp(text=True)
905 os.write(handle, commit_message)
906 os.close(handle)
907
908 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24909 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42910 os.close(handle)
911
912 commit_cmd += ['--file=' + commit_filename]
913 commit_cmd += ['--targets=' + targets_filename]
914 # Change the current working directory before calling commit.
915 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24916 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42917 output = RunShell(commit_cmd, True)
918 os.remove(commit_filename)
919 os.remove(targets_filename)
920 if output.find("Committed revision") != -1:
921 change_info.Delete()
922
923 if change_info.issue:
924 revision = re.compile(".*?\nCommitted revision (\d+)",
925 re.DOTALL).match(output).group(1)
926 viewvc_url = GetCodeReviewSetting("VIEW_VC")
927 change_info.description = change_info.description + '\n'
928 if viewvc_url:
929 change_info.description += "\nCommitted: " + viewvc_url + revision
930 change_info.CloseIssue()
931 os.chdir(previous_cwd)
932
[email protected]2c8d4b22009-06-06 21:03:10933
[email protected]9ce98222009-10-19 20:24:17934def Change(change_info, args):
[email protected]fb2b8eb2009-04-23 21:03:42935 """Creates/edits a changelist."""
[email protected]9ce98222009-10-19 20:24:17936 silent = FilterFlag(args, "--silent")
[email protected]d36b3ed2009-11-09 18:51:42937
938 # Verify the user is running the change command from a read-write checkout.
[email protected]5aeb7dd2009-11-17 18:09:01939 svn_info = SVN.CaptureInfo('.')
[email protected]d36b3ed2009-11-09 18:51:42940 if not svn_info:
941 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
942 "directory.")
[email protected]d36b3ed2009-11-09 18:51:42943
[email protected]9ce98222009-10-19 20:24:17944 if (len(args) == 1):
945 filename = args[0]
946 f = open(filename, 'rU')
947 override_description = f.read()
948 f.close()
949 else:
950 override_description = None
[email protected]5aeb7dd2009-11-17 18:09:01951
[email protected]ea452b32009-11-22 20:04:31952 if change_info.issue and not change_info.NeedsUpload():
[email protected]fb2b8eb2009-04-23 21:03:42953 try:
954 description = GetIssueDescription(change_info.issue)
955 except urllib2.HTTPError, err:
956 if err.code == 404:
957 # The user deleted the issue in Rietveld, so forget the old issue id.
958 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:10959 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:42960 change_info.Save()
961 else:
962 ErrorExit("Error getting the description from Rietveld: " + err)
963 else:
[email protected]85532fc2009-06-04 22:36:53964 if override_description:
965 description = override_description
966 else:
967 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:42968
969 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:23970
[email protected]f0dfba32009-08-07 22:03:37971 # Edited files (as opposed to files with only changed properties) will have
972 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:53973 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:37974 affected_files = [x for x in other_files if file_re.match(x[0])]
975 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:42976
977 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:24978 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:42979 "---Paths in this changelist (" + change_info.name + "):\n")
980 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
981 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:37982 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
983
984 if change_info.Exists():
985 text += (separator2 +
986 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
987 else:
988 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
989 separator2)
990 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:42991
992 handle, filename = tempfile.mkstemp(text=True)
993 os.write(handle, text)
994 os.close(handle)
995
[email protected]9ce98222009-10-19 20:24:17996 if not silent:
997 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:42998
[email protected]0fca4f32009-12-18 15:14:34999 result = gclient_utils.FileRead(filename, 'r')
[email protected]fb2b8eb2009-04-23 21:03:421000 os.remove(filename)
1001
1002 if not result:
1003 return
1004
1005 split_result = result.split(separator1, 1)
1006 if len(split_result) != 2:
1007 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1008
[email protected]ea452b32009-11-22 20:04:311009 # Update the CL description if it has changed.
[email protected]fb2b8eb2009-04-23 21:03:421010 new_description = split_result[0]
1011 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531012 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421013 change_info.description = new_description
[email protected]ea452b32009-11-22 20:04:311014 change_info.needs_upload = True
[email protected]fb2b8eb2009-04-23 21:03:421015
1016 new_cl_files = []
1017 for line in cl_files_text.splitlines():
1018 if not len(line):
1019 continue
1020 if line.startswith("---"):
1021 break
1022 status = line[:7]
[email protected]e3608df2009-11-10 20:22:571023 filename = line[7:]
1024 new_cl_files.append((status, filename))
[email protected]bfd09ce2009-08-05 21:17:231025
1026 if (not len(change_info._files)) and (not change_info.issue) and \
1027 (not len(new_description) and (not new_cl_files)):
1028 ErrorExit("Empty changelist not saved")
1029
[email protected]17f59f22009-06-12 13:27:241030 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421031 change_info.Save()
[email protected]53bcf152009-11-13 21:04:101032 if svn_info.get('URL', '').startswith('http:'):
1033 Warn("WARNING: Creating CL in a read-only checkout. You will not be "
1034 "able to commit it!")
1035
[email protected]fb2b8eb2009-04-23 21:03:421036 print change_info.name + " changelist saved."
1037 if change_info.MissingTests():
1038 Warn("WARNING: " + MISSING_TEST_MSG)
1039
[email protected]ea452b32009-11-22 20:04:311040 # Update the Rietveld issue.
1041 if change_info.issue and change_info.NeedsUpload():
1042 change_info.UpdateRietveldDescription()
1043 change_info.needs_upload = False
1044 change_info.Save()
1045
1046
[email protected]fb2b8eb2009-04-23 21:03:421047# Valid extensions for files we want to lint.
[email protected]e72bb632009-10-29 20:15:481048DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
1049DEFAULT_LINT_IGNORE_REGEX = r""
[email protected]fb2b8eb2009-04-23 21:03:421050
1051def Lint(change_info, args):
1052 """Runs cpplint.py on all the files in |change_info|"""
1053 try:
1054 import cpplint
1055 except ImportError:
1056 ErrorExit("You need to install cpplint.py to lint C++ files.")
1057
1058 # Change the current working directory before calling lint so that it
1059 # shows the correct base.
1060 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241061 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421062
1063 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241064 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421065
[email protected]bb816382009-10-29 01:38:021066 white_list = GetCodeReviewSetting("LINT_REGEX")
1067 if not white_list:
[email protected]e72bb632009-10-29 20:15:481068 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021069 white_regex = re.compile(white_list)
1070 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1071 if not black_list:
[email protected]e72bb632009-10-29 20:15:481072 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021073 black_regex = re.compile(black_list)
[email protected]e3608df2009-11-10 20:22:571074 for filename in filenames:
1075 if white_regex.match(filename):
1076 if black_regex.match(filename):
1077 print "Ignoring file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421078 else:
[email protected]e3608df2009-11-10 20:22:571079 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021080 else:
[email protected]e3608df2009-11-10 20:22:571081 print "Skipping file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421082
1083 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1084 os.chdir(previous_cwd)
1085
1086
[email protected]b0dfd352009-06-10 14:12:541087def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421088 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1089 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481090 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151091 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411092 change = presubmit_support.SvnChange(change_info.name,
1093 change_info.description,
1094 change_info.GetLocalRoot(),
1095 change_info.GetFiles(),
1096 change_info.issue,
1097 change_info.patchset)
1098 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541099 committing=committing,
[email protected]1033acc2009-05-13 14:36:481100 verbose=False,
1101 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151102 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541103 default_presubmit=root_presubmit,
1104 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551105 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421106 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1107 return result
1108
1109
1110def Changes():
1111 """Print all the changelists and their files."""
1112 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081113 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421114 print "\n--- Changelist " + change_info.name + ":"
[email protected]e3608df2009-11-10 20:22:571115 for filename in change_info.GetFiles():
1116 print "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421117
1118
[email protected]bfd09ce2009-08-05 21:17:231119def DeleteEmptyChangeLists():
1120 """Delete all changelists that have no files."""
1121 print "\n--- Deleting:"
1122 for cl in GetCLs():
1123 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1124 if not len(change_info._files):
1125 print change_info.name
1126 change_info.Delete()
1127
1128
[email protected]fb2b8eb2009-04-23 21:03:421129def main(argv=None):
1130 if argv is None:
1131 argv = sys.argv
1132
1133 if len(argv) == 1:
1134 Help()
1135 return 0;
1136
[email protected]a05be0b2009-06-30 19:13:021137 try:
1138 # Create the directories where we store information about changelists if it
1139 # doesn't exist.
1140 if not os.path.exists(GetInfoDir()):
1141 os.mkdir(GetInfoDir())
1142 if not os.path.exists(GetChangesDir()):
1143 os.mkdir(GetChangesDir())
1144 # For smooth upgrade support, move the files in GetInfoDir() to
1145 # GetChangesDir().
1146 # TODO(maruel): Remove this code in August 2009.
[email protected]e3608df2009-11-10 20:22:571147 for filename in os.listdir(unicode(GetInfoDir())):
1148 file_path = os.path.join(unicode(GetInfoDir()), filename)
1149 if os.path.isfile(file_path) and filename != CODEREVIEW_SETTINGS_FILE:
[email protected]a05be0b2009-06-30 19:13:021150 shutil.move(file_path, GetChangesDir())
1151 if not os.path.exists(GetCacheDir()):
1152 os.mkdir(GetCacheDir())
[email protected]5f3eee32009-09-17 00:34:301153 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:021154 # Will throw an exception if not run in a svn checkout.
1155 pass
[email protected]fb2b8eb2009-04-23 21:03:421156
1157 # Commands that don't require an argument.
1158 command = argv[1]
[email protected]88c32d82009-10-12 18:24:051159 if command == "opened" or command == "status":
1160 Opened(command == "status")
[email protected]fb2b8eb2009-04-23 21:03:421161 return 0
1162 if command == "nothave":
[email protected]e3608df2009-11-10 20:22:571163 __pychecker__ = 'no-returnvalues'
1164 for filename in UnknownFiles(argv[2:]):
1165 print "? " + "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421166 return 0
1167 if command == "changes":
1168 Changes()
1169 return 0
1170 if command == "help":
1171 Help(argv[2:])
1172 return 0
1173 if command == "diff" and len(argv) == 2:
1174 files = GetFilesNotInCL()
1175 print GenerateDiff([x[1] for x in files])
1176 return 0
1177 if command == "settings":
[email protected]e3608df2009-11-10 20:22:571178 # Force load settings
1179 GetCodeReviewSetting("UNKNOWN");
[email protected]a005ccd2009-06-12 13:25:541180 del CODEREVIEW_SETTINGS['__just_initialized']
1181 print '\n'.join(("%s: %s" % (str(k), str(v))
1182 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
[email protected]fb2b8eb2009-04-23 21:03:421183 return 0
[email protected]bfd09ce2009-08-05 21:17:231184 if command == "deleteempties":
1185 DeleteEmptyChangeLists()
1186 return 0
[email protected]fb2b8eb2009-04-23 21:03:421187
[email protected]a31d6582009-11-10 13:55:131188 if command == "change":
1189 if len(argv) == 2:
[email protected]fb2b8eb2009-04-23 21:03:421190 # Generate a random changelist name.
1191 changename = GenerateChangeName()
[email protected]a31d6582009-11-10 13:55:131192 elif argv[2] == '--force':
1193 changename = GenerateChangeName()
1194 # argv[3:] is passed to Change() as |args| later. Change() should receive
1195 # |args| which includes '--force'.
1196 argv.insert(2, changename)
[email protected]fb2b8eb2009-04-23 21:03:421197 else:
[email protected]a31d6582009-11-10 13:55:131198 changename = argv[2]
1199 elif len(argv) == 2:
1200 ErrorExit("Need a changelist name.")
[email protected]fb2b8eb2009-04-23 21:03:421201 else:
1202 changename = argv[2]
1203
1204 # When the command is 'try' and --patchset is used, the patch to try
1205 # is on the Rietveld server. 'change' creates a change so it's fine if the
1206 # change didn't exist. All other commands require an existing change.
1207 fail_on_not_found = command != "try" and command != "change"
1208 if command == "try" and changename.find(',') != -1:
[email protected]8d5c9a52009-06-12 15:59:081209 change_info = LoadChangelistInfoForMultiple(changename, GetRepositoryRoot(),
1210 True, True)
[email protected]fb2b8eb2009-04-23 21:03:421211 else:
[email protected]8d5c9a52009-06-12 15:59:081212 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(),
1213 fail_on_not_found, True)
[email protected]fb2b8eb2009-04-23 21:03:421214
1215 if command == "change":
[email protected]9ce98222009-10-19 20:24:171216 Change(change_info, argv[3:])
[email protected]fb2b8eb2009-04-23 21:03:421217 elif command == "lint":
1218 Lint(change_info, argv[3:])
1219 elif command == "upload":
1220 UploadCL(change_info, argv[3:])
1221 elif command == "presubmit":
1222 PresubmitCL(change_info)
1223 elif command in ("commit", "submit"):
1224 Commit(change_info, argv[3:])
1225 elif command == "delete":
1226 change_info.Delete()
1227 elif command == "try":
1228 # When the change contains no file, send the "changename" positional
1229 # argument to trychange.py.
[email protected]17f59f22009-06-12 13:27:241230 if change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421231 args = argv[3:]
1232 else:
1233 change_info = None
1234 args = argv[2:]
1235 TryChange(change_info, args, swallow_exception=False)
1236 else:
1237 # Everything else that is passed into gcl we redirect to svn, after adding
1238 # the files. This allows commands such as 'gcl diff xxx' to work.
[email protected]f40fbb32009-11-10 13:54:311239 if command == "diff" and not change_info.GetFileNames():
1240 return 0
[email protected]fb2b8eb2009-04-23 21:03:421241 args =["svn", command]
1242 root = GetRepositoryRoot()
[email protected]17f59f22009-06-12 13:27:241243 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
[email protected]fb2b8eb2009-04-23 21:03:421244 RunShell(args, True)
1245 return 0
1246
1247
1248if __name__ == "__main__":
1249 sys.exit(main())