blob: db5371c4a48c65da6fc6563c2fe6dc0dc3f84e61 [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]e72bb632009-10-29 20:15:48148 local_dir = os.path.dirname(os.path.abspath(filename))
149 local_base = os.path.basename(filename)
[email protected]5f3eee32009-09-17 00:34:30150 dir_info = gclient_scm.CaptureSVNInfo(".")
[email protected]9b613272009-04-24 01:28:28151 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51152 if use_root:
153 url_path = repo_root
154 else:
155 url_path = dir_info["URL"]
156 content = ""
[email protected]9b613272009-04-24 01:28:28157 while True:
[email protected]e72bb632009-10-29 20:15:48158 # First, look for a locally modified version of the file.
159 local_path = os.path.join(local_dir, local_base)
160 content, rc = RunShellWithReturnCode(["svn", "status", local_path])
[email protected]bb816382009-10-29 01:38:02161 if not rc and content.startswith('M'):
[email protected]e72bb632009-10-29 20:15:48162 content = ReadFile(local_path)
[email protected]bb816382009-10-29 01:38:02163 rc = 0
164 else:
[email protected]e72bb632009-10-29 20:15:48165 # Then look in the repository.
[email protected]bb816382009-10-29 01:38:02166 svn_path = url_path + "/" + filename
167 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
168
[email protected]9b613272009-04-24 01:28:28169 if not rc:
[email protected]98fc2b92009-05-21 14:11:51170 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28171 break
172 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51173 content = ""
[email protected]9b613272009-04-24 01:28:28174 if url_path == repo_root:
175 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51176 break
[email protected]9b613272009-04-24 01:28:28177 # Go up one level to try again.
178 url_path = os.path.dirname(url_path)
[email protected]e72bb632009-10-29 20:15:48179 local_dir = os.path.dirname(local_dir)
[email protected]9b613272009-04-24 01:28:28180 # Write a cached version even if there isn't a file, so we don't try to
181 # fetch it each time.
[email protected]98fc2b92009-05-21 14:11:51182 WriteFile(cached_file, content)
183 else:
184 content = ReadFile(cached_settings_file)
185 # Keep the content cached in memory.
186 FILES_CACHE[filename] = content
187 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28188
[email protected]98fc2b92009-05-21 14:11:51189
190def GetCodeReviewSetting(key):
191 """Returns a value for the given key for this repository."""
192 # Use '__just_initialized' as a flag to determine if the settings were
193 # already initialized.
194 global CODEREVIEW_SETTINGS
195 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47196 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
197 if settings_file:
198 for line in settings_file.splitlines():
199 if not line or line.startswith("#"):
200 continue
201 k, v = line.split(": ", 1)
202 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51203 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42204 return CODEREVIEW_SETTINGS.get(key, "")
205
206
[email protected]fb2b8eb2009-04-23 21:03:42207def Warn(msg):
208 ErrorExit(msg, exit=False)
209
210
211def ErrorExit(msg, exit=True):
212 """Print an error message to stderr and optionally exit."""
213 print >>sys.stderr, msg
214 if exit:
215 sys.exit(1)
216
217
218def RunShellWithReturnCode(command, print_output=False):
219 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02220 # Use a shell for subcommands on Windows to get a PATH search, and because svn
221 # may be a batch file.
222 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42223 p = subprocess.Popen(command, stdout=subprocess.PIPE,
224 stderr=subprocess.STDOUT, shell=use_shell,
225 universal_newlines=True)
226 if print_output:
227 output_array = []
228 while True:
229 line = p.stdout.readline()
230 if not line:
231 break
232 if print_output:
233 print line.strip('\n')
234 output_array.append(line)
235 output = "".join(output_array)
236 else:
237 output = p.stdout.read()
238 p.wait()
239 p.stdout.close()
240 return output, p.returncode
241
242
243def RunShell(command, print_output=False):
244 """Executes a command and returns the output."""
245 return RunShellWithReturnCode(command, print_output)[0]
246
247
[email protected]c1675e22009-04-27 20:30:48248def ReadFile(filename, flags='r'):
[email protected]fb2b8eb2009-04-23 21:03:42249 """Returns the contents of a file."""
[email protected]c1675e22009-04-27 20:30:48250 file = open(filename, flags)
[email protected]fb2b8eb2009-04-23 21:03:42251 result = file.read()
252 file.close()
253 return result
254
255
256def WriteFile(filename, contents):
257 """Overwrites the file with the given contents."""
258 file = open(filename, 'w')
259 file.write(contents)
260 file.close()
261
262
[email protected]51ee0072009-06-08 19:20:05263def FilterFlag(args, flag):
264 """Returns True if the flag is present in args list.
265
266 The flag is removed from args if present.
267 """
268 if flag in args:
269 args.remove(flag)
270 return True
271 return False
272
273
[email protected]be0d1ca2009-05-12 19:23:02274class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42275 """Holds information about a changelist.
276
[email protected]32ba2602009-06-06 18:44:48277 name: change name.
278 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
279 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42280 description: the description.
281 files: a list of 2 tuple containing (status, filename) of changed files,
282 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08283 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42284 """
[email protected]32ba2602009-06-06 18:44:48285
286 _SEPARATOR = "\n-----\n"
287 # The info files have the following format:
288 # issue_id, patchset\n (, patchset is optional)
289 # _SEPARATOR\n
290 # filepath1\n
291 # filepath2\n
292 # .
293 # .
294 # filepathn\n
295 # _SEPARATOR\n
296 # description
297
[email protected]8d5c9a52009-06-12 15:59:08298 def __init__(self, name, issue, patchset, description, files, local_root):
[email protected]fb2b8eb2009-04-23 21:03:42299 self.name = name
[email protected]32ba2602009-06-06 18:44:48300 self.issue = int(issue)
301 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42302 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02303 if files is None:
304 files = []
[email protected]17f59f22009-06-12 13:27:24305 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42306 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08307 self._local_root = local_root
[email protected]fb2b8eb2009-04-23 21:03:42308
[email protected]17f59f22009-06-12 13:27:24309 def GetFileNames(self):
310 """Returns the list of file names included in this change."""
311 return [file[1] for file in self._files]
312
313 def GetFiles(self):
314 """Returns the list of files included in this change with their status."""
315 return self._files
316
317 def GetLocalRoot(self):
318 """Returns the local repository checkout root directory."""
319 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42320
[email protected]f0dfba32009-08-07 22:03:37321 def Exists(self):
322 """Returns True if this change already exists (i.e., is not new)."""
323 return (self.issue or self.description or self._files)
324
[email protected]fb2b8eb2009-04-23 21:03:42325 def _NonDeletedFileList(self):
326 """Returns a list of files in this change, not including deleted files."""
[email protected]17f59f22009-06-12 13:27:24327 return [file[1] for file in self.GetFiles()
328 if not file[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42329
330 def _AddedFileList(self):
331 """Returns a list of files added in this change."""
[email protected]17f59f22009-06-12 13:27:24332 return [file[1] for file in self.GetFiles() if file[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42333
334 def Save(self):
335 """Writes the changelist information to disk."""
[email protected]32ba2602009-06-06 18:44:48336 data = ChangeInfo._SEPARATOR.join([
337 "%d, %d" % (self.issue, self.patchset),
[email protected]17f59f22009-06-12 13:27:24338 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48339 self.description])
[email protected]fb2b8eb2009-04-23 21:03:42340 WriteFile(GetChangelistInfoFile(self.name), data)
341
342 def Delete(self):
343 """Removes the changelist information from disk."""
344 os.remove(GetChangelistInfoFile(self.name))
345
346 def CloseIssue(self):
347 """Closes the Rietveld issue for this changelist."""
348 data = [("description", self.description),]
349 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10350 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42351
352 def UpdateRietveldDescription(self):
353 """Sets the description for an issue on Rietveld."""
354 data = [("description", self.description),]
355 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10356 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42357
358 def MissingTests(self):
359 """Returns True if the change looks like it needs unit tests but has none.
360
361 A change needs unit tests if it contains any new source files or methods.
362 """
363 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
364 # Ignore third_party entirely.
365 files = [file for file in self._NonDeletedFileList()
366 if file.find("third_party") == -1]
367 added_files = [file for file in self._AddedFileList()
368 if file.find("third_party") == -1]
369
370 # If the change is entirely in third_party, we're done.
371 if len(files) == 0:
372 return False
373
374 # Any new or modified test files?
375 # A test file's name ends with "test.*" or "tests.*".
376 test_files = [test for test in files
377 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
378 if len(test_files) > 0:
379 return False
380
381 # Any new source files?
382 source_files = [file for file in added_files
383 if os.path.splitext(file)[1] in SOURCE_SUFFIXES]
384 if len(source_files) > 0:
385 return True
386
387 # Do the long test, checking the files for new methods.
388 return self._HasNewMethod()
389
390 def _HasNewMethod(self):
391 """Returns True if the changeset contains any new functions, or if a
392 function signature has been changed.
393
394 A function is identified by starting flush left, containing a "(" before
395 the next flush-left line, and either ending with "{" before the next
396 flush-left line or being followed by an unindented "{".
397
398 Currently this returns True for new methods, new static functions, and
399 methods or functions whose signatures have been changed.
400
401 Inline methods added to header files won't be detected by this. That's
402 acceptable for purposes of determining if a unit test is needed, since
403 inline methods should be trivial.
404 """
405 # To check for methods added to source or header files, we need the diffs.
406 # We'll generate them all, since there aren't likely to be many files
407 # apart from source and headers; besides, we'll want them all if we're
408 # uploading anyway.
409 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24410 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42411
412 definition = ""
413 for line in self.patch.splitlines():
414 if not line.startswith("+"):
415 continue
416 line = line.strip("+").rstrip(" \t")
417 # Skip empty lines, comments, and preprocessor directives.
418 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
419 if line == "" or line.startswith("/") or line.startswith("#"):
420 continue
421
422 # A possible definition ending with "{" is complete, so check it.
423 if definition.endswith("{"):
424 if definition.find("(") != -1:
425 return True
426 definition = ""
427
428 # A { or an indented line, when we're in a definition, continues it.
429 if (definition != "" and
430 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
431 definition += line
432
433 # A flush-left line starts a new possible function definition.
434 elif not line.startswith(" ") and not line.startswith("\t"):
435 definition = line
436
437 return False
438
[email protected]32ba2602009-06-06 18:44:48439 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08440 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48441 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42442
[email protected]32ba2602009-06-06 18:44:48443 Args:
444 fail_on_not_found: if True, this function will quit the program if the
445 changelist doesn't exist.
446 update_status: if True, the svn status will be updated for all the files
447 and unchanged files will be removed.
448
449 Returns: a ChangeInfo object.
450 """
451 info_file = GetChangelistInfoFile(changename)
452 if not os.path.exists(info_file):
453 if fail_on_not_found:
454 ErrorExit("Changelist " + changename + " not found.")
[email protected]8d5c9a52009-06-12 15:59:08455 return ChangeInfo(changename, 0, 0, '', None, local_root)
[email protected]32ba2602009-06-06 18:44:48456 split_data = ReadFile(info_file).split(ChangeInfo._SEPARATOR, 2)
457 if len(split_data) != 3:
458 ErrorExit("Changelist file %s is corrupt" % info_file)
459 items = split_data[0].split(',')
460 issue = 0
461 patchset = 0
462 if items[0]:
463 issue = int(items[0])
464 if len(items) > 1:
465 patchset = int(items[1])
466 files = []
467 for line in split_data[1].splitlines():
468 status = line[:7]
469 file = line[7:]
470 files.append((status, file))
471 description = split_data[2]
472 save = False
473 if update_status:
474 for file in files:
[email protected]8d5c9a52009-06-12 15:59:08475 filename = os.path.join(local_root, file[1])
[email protected]5f3eee32009-09-17 00:34:30476 status_result = gclient_scm.CaptureSVNStatus(filename)
[email protected]32ba2602009-06-06 18:44:48477 if not status_result or not status_result[0][0]:
478 # File has been reverted.
479 save = True
480 files.remove(file)
481 continue
482 status = status_result[0][0]
483 if status != file[0]:
484 save = True
485 files[files.index(file)] = (status, file[1])
[email protected]8d5c9a52009-06-12 15:59:08486 change_info = ChangeInfo(changename, issue, patchset, description, files,
487 local_root)
[email protected]32ba2602009-06-06 18:44:48488 if save:
489 change_info.Save()
490 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42491
492
493def GetChangelistInfoFile(changename):
494 """Returns the file that stores information about a changelist."""
495 if not changename or re.search(r'[^\w-]', changename):
496 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52497 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42498
499
[email protected]8d5c9a52009-06-12 15:59:08500def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
501 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42502 """Loads many changes and merge their files list into one pseudo change.
503
504 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
505 """
506 changes = changenames.split(',')
[email protected]8d5c9a52009-06-12 15:59:08507 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root)
[email protected]fb2b8eb2009-04-23 21:03:42508 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08509 aggregate_change_info._files += ChangeInfo.Load(change,
510 local_root,
511 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24512 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42513 return aggregate_change_info
514
515
[email protected]fb2b8eb2009-04-23 21:03:42516def GetCLs():
517 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52518 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42519 if CODEREVIEW_SETTINGS_FILE in cls:
520 cls.remove(CODEREVIEW_SETTINGS_FILE)
521 return cls
522
523
524def GenerateChangeName():
525 """Generate a random changelist name."""
526 random.seed()
527 current_cl_names = GetCLs()
528 while True:
529 cl_name = (random.choice(string.ascii_lowercase) +
530 random.choice(string.digits) +
531 random.choice(string.ascii_lowercase) +
532 random.choice(string.digits))
533 if cl_name not in current_cl_names:
534 return cl_name
535
536
537def GetModifiedFiles():
538 """Returns a set that maps from changelist name to (status,filename) tuples.
539
540 Files not in a changelist have an empty changelist name. Filenames are in
541 relation to the top level directory of the current repository. Note that
542 only the current directory and subdirectories are scanned, in order to
543 improve performance while still being flexible.
544 """
545 files = {}
546
547 # Since the files are normalized to the root folder of the repositary, figure
548 # out what we need to add to the paths.
549 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
550
551 # Get a list of all files in changelists.
552 files_in_cl = {}
553 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08554 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
555 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24556 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42557 files_in_cl[filename] = change_info.name
558
559 # Get all the modified files.
[email protected]5f3eee32009-09-17 00:34:30560 status_result = gclient_scm.CaptureSVNStatus(None)
[email protected]207fdf32009-04-28 19:57:01561 for line in status_result:
562 status = line[0]
563 filename = line[1]
564 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42565 continue
[email protected]fb2b8eb2009-04-23 21:03:42566 if dir_prefix:
567 filename = os.path.join(dir_prefix, filename)
568 change_list_name = ""
569 if filename in files_in_cl:
570 change_list_name = files_in_cl[filename]
571 files.setdefault(change_list_name, []).append((status, filename))
572
573 return files
574
575
576def GetFilesNotInCL():
577 """Returns a list of tuples (status,filename) that aren't in any changelists.
578
579 See docstring of GetModifiedFiles for information about path of files and
580 which directories are scanned.
581 """
582 modified_files = GetModifiedFiles()
583 if "" not in modified_files:
584 return []
585 return modified_files[""]
586
587
588def SendToRietveld(request_path, payload=None,
589 content_type="application/octet-stream", timeout=None):
590 """Send a POST/GET to Rietveld. Returns the response body."""
591 def GetUserCredentials():
592 """Prompts the user for a username and password."""
593 email = upload.GetEmail()
594 password = getpass.getpass("Password for %s: " % email)
595 return email, password
596
597 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
598 rpc_server = upload.HttpRpcServer(server,
599 GetUserCredentials,
600 host_override=server,
601 save_cookies=True)
602 try:
603 return rpc_server.Send(request_path, payload, content_type, timeout)
604 except urllib2.URLError, e:
605 if timeout is None:
606 ErrorExit("Error accessing url %s" % request_path)
607 else:
608 return None
609
610
611def GetIssueDescription(issue):
612 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48613 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42614
615
[email protected]88c32d82009-10-12 18:24:05616def Opened(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42617 """Prints a list of modified files in the current directory down."""
618 files = GetModifiedFiles()
619 cl_keys = files.keys()
620 cl_keys.sort()
621 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05622 if not cl_name:
623 continue
624 note = ""
625 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
626 fail_on_not_found=True, update_status=False)
627 if len(change_info.GetFiles()) != len(files[cl_name]):
628 note = " (Note: this changelist contains files outside this directory)"
629 print "\n--- Changelist " + cl_name + note + ":"
[email protected]fb2b8eb2009-04-23 21:03:42630 for file in files[cl_name]:
631 print "".join(file)
[email protected]88c32d82009-10-12 18:24:05632 if show_unknown_files:
633 unknown_files = UnknownFiles([])
634 if (files.get('') or (show_unknown_files and len(unknown_files))):
635 print "\n--- Not in any changelist:"
636 for file in files.get('', []):
637 print "".join(file)
638 if show_unknown_files:
639 for file in unknown_files:
640 print "? %s" % file
[email protected]fb2b8eb2009-04-23 21:03:42641
642
643def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53644 if argv:
645 if argv[0] == 'try':
646 TryChange(None, ['--help'], swallow_exception=False)
647 return
648 if argv[0] == 'upload':
649 upload.RealMain(['upload.py', '--help'])
650 return
[email protected]fb2b8eb2009-04-23 21:03:42651
652 print (
653"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48654version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42655
656Basic commands:
657-----------------------------------------
658 gcl change change_name
659 Add/remove files to a changelist. Only scans the current directory and
660 subdirectories.
661
662 gcl upload change_name [-r [email protected],[email protected],...]
663 [--send_mail] [--no_try] [--no_presubmit]
[email protected]b2ab4942009-06-11 21:39:19664 [--no_watchlists]
[email protected]fb2b8eb2009-04-23 21:03:42665 Uploads the changelist to the server for review.
666
[email protected]3b217f52009-06-01 17:54:20667 gcl commit change_name [--no_presubmit]
[email protected]fb2b8eb2009-04-23 21:03:42668 Commits the changelist to the repository.
669
670 gcl lint change_name
671 Check all the files in the changelist for possible style violations.
672
673Advanced commands:
674-----------------------------------------
675 gcl delete change_name
676 Deletes a changelist.
677
678 gcl diff change_name
679 Diffs all files in the changelist.
680
681 gcl presubmit change_name
682 Runs presubmit checks without uploading the changelist.
683
684 gcl diff
685 Diffs all files in the current directory and subdirectories that aren't in
686 a changelist.
687
688 gcl changes
689 Lists all the the changelists and the files in them.
690
691 gcl nothave [optional directory]
692 Lists files unknown to Subversion.
693
694 gcl opened
695 Lists modified files in the current directory and subdirectories.
696
697 gcl settings
698 Print the code review settings for this directory.
699
700 gcl status
701 Lists modified and unknown files in the current directory and
702 subdirectories.
703
704 gcl try change_name
705 Sends the change to the tryserver so a trybot can do a test run on your
706 code. To send multiple changes as one path, use a comma-separated list
707 of changenames.
708 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53709
[email protected]bfd09ce2009-08-05 21:17:23710 gcl deleteempties
711 Deletes all changelists that have no files associated with them. Careful,
712 you can lose your descriptions.
713
[email protected]3bcc6ce2009-05-12 22:53:53714 gcl help [command]
715 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42716""")
717
718def GetEditor():
719 editor = os.environ.get("SVN_EDITOR")
720 if not editor:
721 editor = os.environ.get("EDITOR")
722
723 if not editor:
724 if sys.platform.startswith("win"):
725 editor = "notepad"
726 else:
727 editor = "vi"
728
729 return editor
730
731
732def GenerateDiff(files, root=None):
733 """Returns a string containing the diff for the given file list.
734
735 The files in the list should either be absolute paths or relative to the
736 given root. If no root directory is provided, the repository root will be
737 used.
738 """
739 previous_cwd = os.getcwd()
740 if root is None:
741 os.chdir(GetRepositoryRoot())
742 else:
743 os.chdir(root)
744
745 diff = []
746 for file in files:
747 # Use svn info output instead of os.path.isdir because the latter fails
748 # when the file is deleted.
[email protected]5f3eee32009-09-17 00:34:30749 if gclient_scm.CaptureSVNInfo(file).get("Node Kind") in ("dir",
750 "directory"):
[email protected]fb2b8eb2009-04-23 21:03:42751 continue
752 # If the user specified a custom diff command in their svn config file,
753 # then it'll be used when we do svn diff, which we don't want to happen
754 # since we want the unified diff. Using --diff-cmd=diff doesn't always
755 # work, since they can have another diff executable in their path that
756 # gives different line endings. So we use a bogus temp directory as the
757 # config directory, which gets around these problems.
758 if sys.platform.startswith("win"):
759 parent_dir = tempfile.gettempdir()
760 else:
761 parent_dir = sys.path[0] # tempdir is not secure.
762 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
763 if not os.path.exists(bogus_dir):
764 os.mkdir(bogus_dir)
765 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
766 if output:
767 diff.append(output)
[email protected]c3150202009-05-13 14:31:01768 elif IsSVNMoved(file):
769 # svn diff on a mv/cp'd file outputs nothing.
770 # We put in an empty Index entry so upload.py knows about them.
[email protected]fb2b8eb2009-04-23 21:03:42771 diff.append("\nIndex: %s\n" % file)
[email protected]c3150202009-05-13 14:31:01772 else:
773 # The file is not modified anymore. It should be removed from the set.
774 pass
[email protected]fb2b8eb2009-04-23 21:03:42775 os.chdir(previous_cwd)
776 return "".join(diff)
777
778
[email protected]51ee0072009-06-08 19:20:05779
780def OptionallyDoPresubmitChecks(change_info, committing, args):
781 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
782 return True
[email protected]b0dfd352009-06-10 14:12:54783 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05784
785
[email protected]fb2b8eb2009-04-23 21:03:42786def UploadCL(change_info, args):
[email protected]17f59f22009-06-12 13:27:24787 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42788 print "Nothing to upload, changelist is empty."
789 return
[email protected]51ee0072009-06-08 19:20:05790 if not OptionallyDoPresubmitChecks(change_info, False, args):
791 return
792 no_try = FilterFlag(args, "--no_try") or FilterFlag(args, "--no-try")
[email protected]b2ab4942009-06-11 21:39:19793 no_watchlists = FilterFlag(args, "--no_watchlists") or \
794 FilterFlag(args, "--no-watchlists")
[email protected]fb2b8eb2009-04-23 21:03:42795
796 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05797 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42798 args.append("--send_mail")
799
800 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05801 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42802
[email protected]003c2692009-05-20 13:08:08803 # Disable try when the server is overridden.
804 server_1 = re.compile(r"^-s\b.*")
805 server_2 = re.compile(r"^--server\b.*")
806 for arg in args:
807 if server_1.match(arg) or server_2.match(arg):
808 no_try = True
809 break
[email protected]fb2b8eb2009-04-23 21:03:42810
811 upload_arg = ["upload.py", "-y"]
812 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
813 upload_arg.extend(args)
814
815 desc_file = ""
816 if change_info.issue: # Uploading a new patchset.
817 found_message = False
818 for arg in args:
819 if arg.startswith("--message") or arg.startswith("-m"):
820 found_message = True
821 break
822
823 if not found_message:
824 upload_arg.append("--message=''")
825
[email protected]32ba2602009-06-06 18:44:48826 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42827 else: # First time we upload.
828 handle, desc_file = tempfile.mkstemp(text=True)
829 os.write(handle, change_info.description)
830 os.close(handle)
831
[email protected]b2ab4942009-06-11 21:39:19832 # Watchlist processing -- CC people interested in this changeset
833 # http://dev.chromium.org/developers/contributing-code/watchlists
834 if not no_watchlists:
835 import watchlists
[email protected]17f59f22009-06-12 13:27:24836 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08837 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19838
[email protected]fb2b8eb2009-04-23 21:03:42839 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19840 if not no_watchlists and watchers:
841 # Filter out all empty elements and join by ','
842 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42843 if cc_list:
844 upload_arg.append("--cc=" + cc_list)
845 upload_arg.append("--description_file=" + desc_file + "")
846 if change_info.description:
847 subject = change_info.description[:77]
848 if subject.find("\r\n") != -1:
849 subject = subject[:subject.find("\r\n")]
850 if subject.find("\n") != -1:
851 subject = subject[:subject.find("\n")]
852 if len(change_info.description) > 77:
853 subject = subject + "..."
854 upload_arg.append("--message=" + subject)
855
856 # Change the current working directory before calling upload.py so that it
857 # shows the correct base.
858 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24859 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42860
861 # If we have a lot of files with long paths, then we won't be able to fit
862 # the command to "svn diff". Instead, we generate the diff manually for
863 # each file and concatenate them before passing it to upload.py.
864 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24865 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42866 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48867 if issue and patchset:
868 change_info.issue = int(issue)
869 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42870 change_info.Save()
871
872 if desc_file:
873 os.remove(desc_file)
874
875 # Do background work on Rietveld to lint the file so that the results are
876 # ready when the issue is viewed.
877 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
878
[email protected]57e78552009-09-11 23:04:30879 # Move back before considering try, so GetCodeReviewSettings is
880 # consistent.
881 os.chdir(previous_cwd)
882
[email protected]fb2b8eb2009-04-23 21:03:42883 # Once uploaded to Rietveld, send it to the try server.
884 if not no_try:
885 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
886 if try_on_upload and try_on_upload.lower() == 'true':
[email protected]32ba2602009-06-06 18:44:48887 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42888 if clobber:
[email protected]32ba2602009-06-06 18:44:48889 trychange_args.append('--clobber')
890 TryChange(change_info, trychange_args, swallow_exception=True)
[email protected]fb2b8eb2009-04-23 21:03:42891
[email protected]fb2b8eb2009-04-23 21:03:42892
893
894def PresubmitCL(change_info):
895 """Reports what presubmit checks on the change would report."""
[email protected]17f59f22009-06-12 13:27:24896 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42897 print "Nothing to presubmit check, changelist is empty."
898 return
899
900 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]b0dfd352009-06-10 14:12:54901 DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42902
[email protected]b0dfd352009-06-10 14:12:54903 print "\n*** Presubmit checks for COMMIT would report: ***"
904 DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42905
906
907def TryChange(change_info, args, swallow_exception):
908 """Create a diff file of change_info and send it to the try server."""
909 try:
910 import trychange
911 except ImportError:
912 if swallow_exception:
913 return
914 ErrorExit("You need to install trychange.py to use the try server.")
915
916 if change_info:
917 trychange_args = ['--name', change_info.name]
[email protected]32ba2602009-06-06 18:44:48918 if change_info.issue:
919 trychange_args.extend(["--issue", str(change_info.issue)])
920 if change_info.patchset:
921 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42922 trychange_args.extend(args)
923 trychange.TryChange(trychange_args,
[email protected]17f59f22009-06-12 13:27:24924 file_list=change_info.GetFileNames(),
[email protected]fb2b8eb2009-04-23 21:03:42925 swallow_exception=swallow_exception,
926 prog='gcl try')
927 else:
928 trychange.TryChange(args,
929 file_list=None,
930 swallow_exception=swallow_exception,
931 prog='gcl try')
932
933
934def Commit(change_info, args):
[email protected]17f59f22009-06-12 13:27:24935 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42936 print "Nothing to commit, changelist is empty."
937 return
[email protected]51ee0072009-06-08 19:20:05938 if not OptionallyDoPresubmitChecks(change_info, True, args):
939 return
[email protected]fb2b8eb2009-04-23 21:03:42940
[email protected]1bb04aa2009-06-01 17:52:11941 # We face a problem with svn here: Let's say change 'bleh' modifies
942 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
943 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
944 # The only fix is to use --non-recursive but that has its issues too:
945 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
946 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
947 # directory with child nodes". Yay...
948 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42949 filename = ''
950 if change_info.issue:
951 # Get the latest description from Rietveld.
952 change_info.description = GetIssueDescription(change_info.issue)
953
954 commit_message = change_info.description.replace('\r\n', '\n')
955 if change_info.issue:
[email protected]32ba2602009-06-06 18:44:48956 commit_message += ('\nReview URL: http://%s/%d' %
[email protected]fb2b8eb2009-04-23 21:03:42957 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
958 change_info.issue))
959
960 handle, commit_filename = tempfile.mkstemp(text=True)
961 os.write(handle, commit_message)
962 os.close(handle)
963
964 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24965 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42966 os.close(handle)
967
968 commit_cmd += ['--file=' + commit_filename]
969 commit_cmd += ['--targets=' + targets_filename]
970 # Change the current working directory before calling commit.
971 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24972 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42973 output = RunShell(commit_cmd, True)
974 os.remove(commit_filename)
975 os.remove(targets_filename)
976 if output.find("Committed revision") != -1:
977 change_info.Delete()
978
979 if change_info.issue:
980 revision = re.compile(".*?\nCommitted revision (\d+)",
981 re.DOTALL).match(output).group(1)
982 viewvc_url = GetCodeReviewSetting("VIEW_VC")
983 change_info.description = change_info.description + '\n'
984 if viewvc_url:
985 change_info.description += "\nCommitted: " + viewvc_url + revision
986 change_info.CloseIssue()
987 os.chdir(previous_cwd)
988
[email protected]2c8d4b22009-06-06 21:03:10989
[email protected]9ce98222009-10-19 20:24:17990def Change(change_info, args):
[email protected]fb2b8eb2009-04-23 21:03:42991 """Creates/edits a changelist."""
[email protected]9ce98222009-10-19 20:24:17992 silent = FilterFlag(args, "--silent")
993 if (len(args) == 1):
994 filename = args[0]
995 f = open(filename, 'rU')
996 override_description = f.read()
997 f.close()
998 else:
999 override_description = None
1000
[email protected]fb2b8eb2009-04-23 21:03:421001 if change_info.issue:
1002 try:
1003 description = GetIssueDescription(change_info.issue)
1004 except urllib2.HTTPError, err:
1005 if err.code == 404:
1006 # The user deleted the issue in Rietveld, so forget the old issue id.
1007 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:101008 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:421009 change_info.Save()
1010 else:
1011 ErrorExit("Error getting the description from Rietveld: " + err)
1012 else:
[email protected]85532fc2009-06-04 22:36:531013 if override_description:
1014 description = override_description
1015 else:
1016 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:421017
1018 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:231019
[email protected]f0dfba32009-08-07 22:03:371020 # Edited files (as opposed to files with only changed properties) will have
1021 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:531022 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:371023 affected_files = [x for x in other_files if file_re.match(x[0])]
1024 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:421025
1026 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:241027 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:421028 "---Paths in this changelist (" + change_info.name + "):\n")
1029 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
1030 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:371031 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
1032
1033 if change_info.Exists():
1034 text += (separator2 +
1035 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
1036 else:
1037 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
1038 separator2)
1039 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:421040
1041 handle, filename = tempfile.mkstemp(text=True)
1042 os.write(handle, text)
1043 os.close(handle)
1044
[email protected]9ce98222009-10-19 20:24:171045 if not silent:
1046 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:421047
1048 result = ReadFile(filename)
1049 os.remove(filename)
1050
1051 if not result:
1052 return
1053
1054 split_result = result.split(separator1, 1)
1055 if len(split_result) != 2:
1056 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1057
1058 new_description = split_result[0]
1059 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531060 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421061 change_info.description = new_description
1062 if change_info.issue:
1063 # Update the Rietveld issue with the new description.
1064 change_info.UpdateRietveldDescription()
1065
1066 new_cl_files = []
1067 for line in cl_files_text.splitlines():
1068 if not len(line):
1069 continue
1070 if line.startswith("---"):
1071 break
1072 status = line[:7]
1073 file = line[7:]
1074 new_cl_files.append((status, file))
[email protected]bfd09ce2009-08-05 21:17:231075
1076 if (not len(change_info._files)) and (not change_info.issue) and \
1077 (not len(new_description) and (not new_cl_files)):
1078 ErrorExit("Empty changelist not saved")
1079
[email protected]17f59f22009-06-12 13:27:241080 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421081
1082 change_info.Save()
1083 print change_info.name + " changelist saved."
1084 if change_info.MissingTests():
1085 Warn("WARNING: " + MISSING_TEST_MSG)
1086
[email protected]fb2b8eb2009-04-23 21:03:421087# Valid extensions for files we want to lint.
[email protected]e72bb632009-10-29 20:15:481088DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
1089DEFAULT_LINT_IGNORE_REGEX = r""
[email protected]fb2b8eb2009-04-23 21:03:421090
1091def Lint(change_info, args):
1092 """Runs cpplint.py on all the files in |change_info|"""
1093 try:
1094 import cpplint
1095 except ImportError:
1096 ErrorExit("You need to install cpplint.py to lint C++ files.")
1097
1098 # Change the current working directory before calling lint so that it
1099 # shows the correct base.
1100 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241101 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421102
1103 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241104 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421105
[email protected]bb816382009-10-29 01:38:021106 white_list = GetCodeReviewSetting("LINT_REGEX")
1107 if not white_list:
[email protected]e72bb632009-10-29 20:15:481108 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021109 white_regex = re.compile(white_list)
1110 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1111 if not black_list:
[email protected]e72bb632009-10-29 20:15:481112 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021113 black_regex = re.compile(black_list)
[email protected]fb2b8eb2009-04-23 21:03:421114 for file in filenames:
[email protected]bb816382009-10-29 01:38:021115 if white_regex.match(file):
1116 if black_regex.match(file):
1117 print "Ignoring file %s" % file
[email protected]fb2b8eb2009-04-23 21:03:421118 else:
1119 cpplint.ProcessFile(file, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021120 else:
1121 print "Skipping file %s" % file
[email protected]fb2b8eb2009-04-23 21:03:421122
1123 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1124 os.chdir(previous_cwd)
1125
1126
[email protected]b0dfd352009-06-10 14:12:541127def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421128 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1129 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481130 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151131 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411132 change = presubmit_support.SvnChange(change_info.name,
1133 change_info.description,
1134 change_info.GetLocalRoot(),
1135 change_info.GetFiles(),
1136 change_info.issue,
1137 change_info.patchset)
1138 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541139 committing=committing,
[email protected]1033acc2009-05-13 14:36:481140 verbose=False,
1141 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151142 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541143 default_presubmit=root_presubmit,
1144 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551145 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421146 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1147 return result
1148
1149
1150def Changes():
1151 """Print all the changelists and their files."""
1152 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081153 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421154 print "\n--- Changelist " + change_info.name + ":"
[email protected]17f59f22009-06-12 13:27:241155 for file in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421156 print "".join(file)
1157
1158
[email protected]bfd09ce2009-08-05 21:17:231159def DeleteEmptyChangeLists():
1160 """Delete all changelists that have no files."""
1161 print "\n--- Deleting:"
1162 for cl in GetCLs():
1163 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1164 if not len(change_info._files):
1165 print change_info.name
1166 change_info.Delete()
1167
1168
[email protected]fb2b8eb2009-04-23 21:03:421169def main(argv=None):
1170 if argv is None:
1171 argv = sys.argv
1172
1173 if len(argv) == 1:
1174 Help()
1175 return 0;
1176
[email protected]a05be0b2009-06-30 19:13:021177 try:
1178 # Create the directories where we store information about changelists if it
1179 # doesn't exist.
1180 if not os.path.exists(GetInfoDir()):
1181 os.mkdir(GetInfoDir())
1182 if not os.path.exists(GetChangesDir()):
1183 os.mkdir(GetChangesDir())
1184 # For smooth upgrade support, move the files in GetInfoDir() to
1185 # GetChangesDir().
1186 # TODO(maruel): Remove this code in August 2009.
1187 for file in os.listdir(unicode(GetInfoDir())):
1188 file_path = os.path.join(unicode(GetInfoDir()), file)
1189 if os.path.isfile(file_path) and file != CODEREVIEW_SETTINGS_FILE:
1190 shutil.move(file_path, GetChangesDir())
1191 if not os.path.exists(GetCacheDir()):
1192 os.mkdir(GetCacheDir())
[email protected]5f3eee32009-09-17 00:34:301193 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:021194 # Will throw an exception if not run in a svn checkout.
1195 pass
[email protected]fb2b8eb2009-04-23 21:03:421196
1197 # Commands that don't require an argument.
1198 command = argv[1]
[email protected]88c32d82009-10-12 18:24:051199 if command == "opened" or command == "status":
1200 Opened(command == "status")
[email protected]fb2b8eb2009-04-23 21:03:421201 return 0
1202 if command == "nothave":
[email protected]88c32d82009-10-12 18:24:051203 unknown_files = UnknownFiles(argv[2:])
1204 for file in unknown_files:
1205 print "? " + "".join(file)
[email protected]fb2b8eb2009-04-23 21:03:421206 return 0
1207 if command == "changes":
1208 Changes()
1209 return 0
1210 if command == "help":
1211 Help(argv[2:])
1212 return 0
1213 if command == "diff" and len(argv) == 2:
1214 files = GetFilesNotInCL()
1215 print GenerateDiff([x[1] for x in files])
1216 return 0
1217 if command == "settings":
1218 ignore = GetCodeReviewSetting("UNKNOWN");
[email protected]a005ccd2009-06-12 13:25:541219 del CODEREVIEW_SETTINGS['__just_initialized']
1220 print '\n'.join(("%s: %s" % (str(k), str(v))
1221 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
[email protected]fb2b8eb2009-04-23 21:03:421222 return 0
[email protected]bfd09ce2009-08-05 21:17:231223 if command == "deleteempties":
1224 DeleteEmptyChangeLists()
1225 return 0
[email protected]fb2b8eb2009-04-23 21:03:421226
1227 if len(argv) == 2:
1228 if command == "change":
1229 # Generate a random changelist name.
1230 changename = GenerateChangeName()
1231 else:
1232 ErrorExit("Need a changelist name.")
1233 else:
1234 changename = argv[2]
1235
1236 # When the command is 'try' and --patchset is used, the patch to try
1237 # is on the Rietveld server. 'change' creates a change so it's fine if the
1238 # change didn't exist. All other commands require an existing change.
1239 fail_on_not_found = command != "try" and command != "change"
1240 if command == "try" and changename.find(',') != -1:
[email protected]8d5c9a52009-06-12 15:59:081241 change_info = LoadChangelistInfoForMultiple(changename, GetRepositoryRoot(),
1242 True, True)
[email protected]fb2b8eb2009-04-23 21:03:421243 else:
[email protected]8d5c9a52009-06-12 15:59:081244 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(),
1245 fail_on_not_found, True)
[email protected]fb2b8eb2009-04-23 21:03:421246
1247 if command == "change":
[email protected]9ce98222009-10-19 20:24:171248 Change(change_info, argv[3:])
[email protected]fb2b8eb2009-04-23 21:03:421249 elif command == "lint":
1250 Lint(change_info, argv[3:])
1251 elif command == "upload":
1252 UploadCL(change_info, argv[3:])
1253 elif command == "presubmit":
1254 PresubmitCL(change_info)
1255 elif command in ("commit", "submit"):
1256 Commit(change_info, argv[3:])
1257 elif command == "delete":
1258 change_info.Delete()
1259 elif command == "try":
1260 # When the change contains no file, send the "changename" positional
1261 # argument to trychange.py.
[email protected]17f59f22009-06-12 13:27:241262 if change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421263 args = argv[3:]
1264 else:
1265 change_info = None
1266 args = argv[2:]
1267 TryChange(change_info, args, swallow_exception=False)
1268 else:
1269 # Everything else that is passed into gcl we redirect to svn, after adding
1270 # the files. This allows commands such as 'gcl diff xxx' to work.
1271 args =["svn", command]
1272 root = GetRepositoryRoot()
[email protected]17f59f22009-06-12 13:27:241273 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
[email protected]fb2b8eb2009-04-23 21:03:421274 RunShell(args, True)
1275 return 0
1276
1277
1278if __name__ == "__main__":
1279 sys.exit(main())