blob: c8a1be75d1ed26f5a63d84022d997408958eedc6 [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
[email protected]fb2b8eb2009-04-23 21:03:4220
[email protected]46a94102009-05-12 20:32:4321# gcl now depends on gclient.
[email protected]5f3eee32009-09-17 00:34:3022import gclient_scm
23import gclient_utils
[email protected]c1675e22009-04-27 20:30:4824
[email protected]98fc2b92009-05-21 14:11:5125__version__ = '1.1.1'
[email protected]c1675e22009-04-27 20:30:4826
27
[email protected]fb2b8eb2009-04-23 21:03:4228CODEREVIEW_SETTINGS = {
29 # Default values.
30 "CODE_REVIEW_SERVER": "codereview.chromium.org",
31 "CC_LIST": "[email protected]",
32 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
33}
34
[email protected]fb2b8eb2009-04-23 21:03:4235# globals that store the root of the current repository and the directory where
36# we store information about changelists.
[email protected]98fc2b92009-05-21 14:11:5137REPOSITORY_ROOT = ""
[email protected]fb2b8eb2009-04-23 21:03:4238
39# Filename where we store repository specific information for gcl.
40CODEREVIEW_SETTINGS_FILE = "codereview.settings"
41
42# Warning message when the change appears to be missing tests.
43MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
44
[email protected]98fc2b92009-05-21 14:11:5145# Global cache of files cached in GetCacheDir().
46FILES_CACHE = {}
[email protected]fb2b8eb2009-04-23 21:03:4247
48
[email protected]207fdf32009-04-28 19:57:0149### SVN Functions
50
[email protected]fb2b8eb2009-04-23 21:03:4251def IsSVNMoved(filename):
52 """Determine if a file has been added through svn mv"""
[email protected]5f3eee32009-09-17 00:34:3053 info = gclient_scm.CaptureSVNInfo(filename)
[email protected]fb2b8eb2009-04-23 21:03:4254 return (info.get('Copied From URL') and
55 info.get('Copied From Rev') and
56 info.get('Schedule') == 'add')
57
58
[email protected]fb2b8eb2009-04-23 21:03:4259def GetSVNFileProperty(file, property_name):
60 """Returns the value of an SVN property for the given file.
61
62 Args:
63 file: The file to check
64 property_name: The name of the SVN property, e.g. "svn:mime-type"
65
66 Returns:
67 The value of the property, which will be the empty string if the property
68 is not set on the file. If the file is not under version control, the
69 empty string is also returned.
70 """
71 output = RunShell(["svn", "propget", property_name, file])
72 if (output.startswith("svn: ") and
73 output.endswith("is not under version control")):
74 return ""
75 else:
76 return output
77
78
[email protected]207fdf32009-04-28 19:57:0179def UnknownFiles(extra_args):
80 """Runs svn status and prints unknown files.
81
82 Any args in |extra_args| are passed to the tool to support giving alternate
83 code locations.
84 """
[email protected]5f3eee32009-09-17 00:34:3085 return [item[1] for item in gclient_scm.CaptureSVNStatus(extra_args)
[email protected]4810a962009-05-12 21:03:3486 if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0187
88
[email protected]fb2b8eb2009-04-23 21:03:4289def GetRepositoryRoot():
90 """Returns the top level directory of the current repository.
91
92 The directory is returned as an absolute path.
93 """
[email protected]98fc2b92009-05-21 14:11:5194 global REPOSITORY_ROOT
95 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:3096 infos = gclient_scm.CaptureSVNInfo(os.getcwd(), print_error=False)
[email protected]46a94102009-05-12 20:32:4397 cur_dir_repo_root = infos.get("Repository Root")
[email protected]fb2b8eb2009-04-23 21:03:4298 if not cur_dir_repo_root:
[email protected]5f3eee32009-09-17 00:34:3099 raise gclient_utils.Error("gcl run outside of repository")
[email protected]fb2b8eb2009-04-23 21:03:42100
[email protected]98fc2b92009-05-21 14:11:51101 REPOSITORY_ROOT = os.getcwd()
[email protected]fb2b8eb2009-04-23 21:03:42102 while True:
[email protected]98fc2b92009-05-21 14:11:51103 parent = os.path.dirname(REPOSITORY_ROOT)
[email protected]5f3eee32009-09-17 00:34:30104 if (gclient_scm.CaptureSVNInfo(parent, print_error=False).get(
[email protected]8c3ccf32009-05-20 18:28:37105 "Repository Root") != cur_dir_repo_root):
[email protected]fb2b8eb2009-04-23 21:03:42106 break
[email protected]98fc2b92009-05-21 14:11:51107 REPOSITORY_ROOT = parent
108 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:42109
110
111def GetInfoDir():
112 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:52113 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
114
115
116def GetChangesDir():
117 """Returns the directory where gcl change files are stored."""
118 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:42119
120
[email protected]98fc2b92009-05-21 14:11:51121def GetCacheDir():
122 """Returns the directory where gcl change files are stored."""
123 return os.path.join(GetInfoDir(), 'cache')
124
125
126def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
127 """Retrieves a file from the repository and caches it in GetCacheDir() for
128 max_age seconds.
129
130 use_root: If False, look up the arborescence for the first match, otherwise go
131 directory to the root repository.
132
133 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:02134 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:51135 """
136 global FILES_CACHE
137 if filename not in FILES_CACHE:
138 # Don't try to look up twice.
139 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28140 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02141 try:
142 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]5f3eee32009-09-17 00:34:30143 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:02144 return None
[email protected]98fc2b92009-05-21 14:11:51145 if (not os.path.exists(cached_file) or
146 os.stat(cached_file).st_mtime > max_age):
[email protected]e72bb632009-10-29 20:15:48147 local_dir = os.path.dirname(os.path.abspath(filename))
148 local_base = os.path.basename(filename)
[email protected]5f3eee32009-09-17 00:34:30149 dir_info = gclient_scm.CaptureSVNInfo(".")
[email protected]9b613272009-04-24 01:28:28150 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51151 if use_root:
152 url_path = repo_root
153 else:
154 url_path = dir_info["URL"]
155 content = ""
[email protected]9b613272009-04-24 01:28:28156 while True:
[email protected]83e99432009-11-10 19:55:24157 # First, look for a locally modified version of the file if we can.
158 r = ""
159 if not use_root:
160 local_path = os.path.join(local_dir, local_base)
161 r = gclient_scm.CaptureSVNStatus((local_path,))
[email protected]cef0db92009-11-04 01:04:57162 rc = -1
163 if r:
[email protected]e3608df2009-11-10 20:22:57164 status = r[0][0]
[email protected]cef0db92009-11-04 01:04:57165 rc = 0
166 if not rc and status[0] in ('A','M'):
[email protected]e72bb632009-10-29 20:15:48167 content = ReadFile(local_path)
[email protected]bb816382009-10-29 01:38:02168 rc = 0
169 else:
[email protected]83e99432009-11-10 19:55:24170 # Look in the repository if we didn't find something local.
[email protected]bb816382009-10-29 01:38:02171 svn_path = url_path + "/" + filename
172 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
173
[email protected]9b613272009-04-24 01:28:28174 if not rc:
[email protected]98fc2b92009-05-21 14:11:51175 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28176 break
177 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51178 content = ""
[email protected]9b613272009-04-24 01:28:28179 if url_path == repo_root:
180 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51181 break
[email protected]9b613272009-04-24 01:28:28182 # Go up one level to try again.
183 url_path = os.path.dirname(url_path)
[email protected]e72bb632009-10-29 20:15:48184 local_dir = os.path.dirname(local_dir)
[email protected]9b613272009-04-24 01:28:28185 # Write a cached version even if there isn't a file, so we don't try to
186 # fetch it each time.
[email protected]98fc2b92009-05-21 14:11:51187 WriteFile(cached_file, content)
188 else:
[email protected]e3608df2009-11-10 20:22:57189 content = ReadFile(cached_file)
[email protected]98fc2b92009-05-21 14:11:51190 # Keep the content cached in memory.
191 FILES_CACHE[filename] = content
192 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28193
[email protected]98fc2b92009-05-21 14:11:51194
195def GetCodeReviewSetting(key):
196 """Returns a value for the given key for this repository."""
197 # Use '__just_initialized' as a flag to determine if the settings were
198 # already initialized.
199 global CODEREVIEW_SETTINGS
200 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47201 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
202 if settings_file:
203 for line in settings_file.splitlines():
204 if not line or line.startswith("#"):
205 continue
206 k, v = line.split(": ", 1)
207 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51208 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42209 return CODEREVIEW_SETTINGS.get(key, "")
210
211
[email protected]fb2b8eb2009-04-23 21:03:42212def Warn(msg):
213 ErrorExit(msg, exit=False)
214
215
216def ErrorExit(msg, exit=True):
217 """Print an error message to stderr and optionally exit."""
218 print >>sys.stderr, msg
219 if exit:
220 sys.exit(1)
221
222
223def RunShellWithReturnCode(command, print_output=False):
224 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02225 # Use a shell for subcommands on Windows to get a PATH search, and because svn
226 # may be a batch file.
227 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42228 p = subprocess.Popen(command, stdout=subprocess.PIPE,
229 stderr=subprocess.STDOUT, shell=use_shell,
230 universal_newlines=True)
231 if print_output:
232 output_array = []
233 while True:
234 line = p.stdout.readline()
235 if not line:
236 break
237 if print_output:
238 print line.strip('\n')
239 output_array.append(line)
240 output = "".join(output_array)
241 else:
242 output = p.stdout.read()
243 p.wait()
244 p.stdout.close()
245 return output, p.returncode
246
247
248def RunShell(command, print_output=False):
249 """Executes a command and returns the output."""
250 return RunShellWithReturnCode(command, print_output)[0]
251
252
[email protected]c1675e22009-04-27 20:30:48253def ReadFile(filename, flags='r'):
[email protected]fb2b8eb2009-04-23 21:03:42254 """Returns the contents of a file."""
[email protected]e3608df2009-11-10 20:22:57255 f = open(filename, flags)
256 result = f.read()
257 f.close()
[email protected]fb2b8eb2009-04-23 21:03:42258 return result
259
260
261def WriteFile(filename, contents):
262 """Overwrites the file with the given contents."""
[email protected]e3608df2009-11-10 20:22:57263 f = open(filename, 'w')
264 f.write(contents)
265 f.close()
[email protected]fb2b8eb2009-04-23 21:03:42266
267
[email protected]51ee0072009-06-08 19:20:05268def FilterFlag(args, flag):
269 """Returns True if the flag is present in args list.
270
271 The flag is removed from args if present.
272 """
273 if flag in args:
274 args.remove(flag)
275 return True
276 return False
277
278
[email protected]be0d1ca2009-05-12 19:23:02279class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42280 """Holds information about a changelist.
281
[email protected]32ba2602009-06-06 18:44:48282 name: change name.
283 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
284 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42285 description: the description.
286 files: a list of 2 tuple containing (status, filename) of changed files,
287 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08288 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42289 """
[email protected]32ba2602009-06-06 18:44:48290
291 _SEPARATOR = "\n-----\n"
292 # The info files have the following format:
293 # issue_id, patchset\n (, patchset is optional)
294 # _SEPARATOR\n
295 # filepath1\n
296 # filepath2\n
297 # .
298 # .
299 # filepathn\n
300 # _SEPARATOR\n
301 # description
302
[email protected]8d5c9a52009-06-12 15:59:08303 def __init__(self, name, issue, patchset, description, files, local_root):
[email protected]fb2b8eb2009-04-23 21:03:42304 self.name = name
[email protected]32ba2602009-06-06 18:44:48305 self.issue = int(issue)
306 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42307 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02308 if files is None:
309 files = []
[email protected]17f59f22009-06-12 13:27:24310 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42311 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08312 self._local_root = local_root
[email protected]fb2b8eb2009-04-23 21:03:42313
[email protected]17f59f22009-06-12 13:27:24314 def GetFileNames(self):
315 """Returns the list of file names included in this change."""
[email protected]e3608df2009-11-10 20:22:57316 return [f[1] for f in self._files]
[email protected]17f59f22009-06-12 13:27:24317
318 def GetFiles(self):
319 """Returns the list of files included in this change with their status."""
320 return self._files
321
322 def GetLocalRoot(self):
323 """Returns the local repository checkout root directory."""
324 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42325
[email protected]f0dfba32009-08-07 22:03:37326 def Exists(self):
327 """Returns True if this change already exists (i.e., is not new)."""
328 return (self.issue or self.description or self._files)
329
[email protected]fb2b8eb2009-04-23 21:03:42330 def _NonDeletedFileList(self):
331 """Returns a list of files in this change, not including deleted files."""
[email protected]e3608df2009-11-10 20:22:57332 return [f[1] for f in self.GetFiles()
333 if not f[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42334
335 def _AddedFileList(self):
336 """Returns a list of files added in this change."""
[email protected]e3608df2009-11-10 20:22:57337 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42338
339 def Save(self):
340 """Writes the changelist information to disk."""
[email protected]32ba2602009-06-06 18:44:48341 data = ChangeInfo._SEPARATOR.join([
342 "%d, %d" % (self.issue, self.patchset),
[email protected]17f59f22009-06-12 13:27:24343 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48344 self.description])
[email protected]fb2b8eb2009-04-23 21:03:42345 WriteFile(GetChangelistInfoFile(self.name), data)
346
347 def Delete(self):
348 """Removes the changelist information from disk."""
349 os.remove(GetChangelistInfoFile(self.name))
350
351 def CloseIssue(self):
352 """Closes the Rietveld issue for this changelist."""
353 data = [("description", self.description),]
354 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10355 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42356
357 def UpdateRietveldDescription(self):
358 """Sets the description for an issue on Rietveld."""
359 data = [("description", self.description),]
360 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10361 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42362
363 def MissingTests(self):
364 """Returns True if the change looks like it needs unit tests but has none.
365
366 A change needs unit tests if it contains any new source files or methods.
367 """
368 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
369 # Ignore third_party entirely.
[email protected]e3608df2009-11-10 20:22:57370 files = [f for f in self._NonDeletedFileList()
371 if f.find("third_party") == -1]
372 added_files = [f for f in self._AddedFileList()
373 if f.find("third_party") == -1]
[email protected]fb2b8eb2009-04-23 21:03:42374
375 # If the change is entirely in third_party, we're done.
376 if len(files) == 0:
377 return False
378
379 # Any new or modified test files?
380 # A test file's name ends with "test.*" or "tests.*".
381 test_files = [test for test in files
382 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
383 if len(test_files) > 0:
384 return False
385
386 # Any new source files?
[email protected]e3608df2009-11-10 20:22:57387 source_files = [item for item in added_files
388 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
[email protected]fb2b8eb2009-04-23 21:03:42389 if len(source_files) > 0:
390 return True
391
392 # Do the long test, checking the files for new methods.
393 return self._HasNewMethod()
394
395 def _HasNewMethod(self):
396 """Returns True if the changeset contains any new functions, or if a
397 function signature has been changed.
398
399 A function is identified by starting flush left, containing a "(" before
400 the next flush-left line, and either ending with "{" before the next
401 flush-left line or being followed by an unindented "{".
402
403 Currently this returns True for new methods, new static functions, and
404 methods or functions whose signatures have been changed.
405
406 Inline methods added to header files won't be detected by this. That's
407 acceptable for purposes of determining if a unit test is needed, since
408 inline methods should be trivial.
409 """
410 # To check for methods added to source or header files, we need the diffs.
411 # We'll generate them all, since there aren't likely to be many files
412 # apart from source and headers; besides, we'll want them all if we're
413 # uploading anyway.
414 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24415 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42416
417 definition = ""
418 for line in self.patch.splitlines():
419 if not line.startswith("+"):
420 continue
421 line = line.strip("+").rstrip(" \t")
422 # Skip empty lines, comments, and preprocessor directives.
423 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
424 if line == "" or line.startswith("/") or line.startswith("#"):
425 continue
426
427 # A possible definition ending with "{" is complete, so check it.
428 if definition.endswith("{"):
429 if definition.find("(") != -1:
430 return True
431 definition = ""
432
433 # A { or an indented line, when we're in a definition, continues it.
434 if (definition != "" and
435 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
436 definition += line
437
438 # A flush-left line starts a new possible function definition.
439 elif not line.startswith(" ") and not line.startswith("\t"):
440 definition = line
441
442 return False
443
[email protected]32ba2602009-06-06 18:44:48444 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08445 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48446 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42447
[email protected]32ba2602009-06-06 18:44:48448 Args:
449 fail_on_not_found: if True, this function will quit the program if the
450 changelist doesn't exist.
451 update_status: if True, the svn status will be updated for all the files
452 and unchanged files will be removed.
453
454 Returns: a ChangeInfo object.
455 """
456 info_file = GetChangelistInfoFile(changename)
457 if not os.path.exists(info_file):
458 if fail_on_not_found:
459 ErrorExit("Changelist " + changename + " not found.")
[email protected]8d5c9a52009-06-12 15:59:08460 return ChangeInfo(changename, 0, 0, '', None, local_root)
[email protected]32ba2602009-06-06 18:44:48461 split_data = ReadFile(info_file).split(ChangeInfo._SEPARATOR, 2)
462 if len(split_data) != 3:
463 ErrorExit("Changelist file %s is corrupt" % info_file)
464 items = split_data[0].split(',')
465 issue = 0
466 patchset = 0
467 if items[0]:
468 issue = int(items[0])
469 if len(items) > 1:
470 patchset = int(items[1])
471 files = []
472 for line in split_data[1].splitlines():
473 status = line[:7]
[email protected]e3608df2009-11-10 20:22:57474 filename = line[7:]
475 files.append((status, filename))
[email protected]32ba2602009-06-06 18:44:48476 description = split_data[2]
477 save = False
478 if update_status:
[email protected]e3608df2009-11-10 20:22:57479 for item in files:
480 filename = os.path.join(local_root, item[1])
[email protected]5f3eee32009-09-17 00:34:30481 status_result = gclient_scm.CaptureSVNStatus(filename)
[email protected]32ba2602009-06-06 18:44:48482 if not status_result or not status_result[0][0]:
483 # File has been reverted.
484 save = True
[email protected]e3608df2009-11-10 20:22:57485 files.remove(item)
[email protected]32ba2602009-06-06 18:44:48486 continue
487 status = status_result[0][0]
[email protected]e3608df2009-11-10 20:22:57488 if status != item[0]:
[email protected]32ba2602009-06-06 18:44:48489 save = True
[email protected]e3608df2009-11-10 20:22:57490 files[files.index(item)] = (status, item[1])
[email protected]8d5c9a52009-06-12 15:59:08491 change_info = ChangeInfo(changename, issue, patchset, description, files,
492 local_root)
[email protected]32ba2602009-06-06 18:44:48493 if save:
494 change_info.Save()
495 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42496
497
498def GetChangelistInfoFile(changename):
499 """Returns the file that stores information about a changelist."""
500 if not changename or re.search(r'[^\w-]', changename):
501 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52502 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42503
504
[email protected]8d5c9a52009-06-12 15:59:08505def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
506 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42507 """Loads many changes and merge their files list into one pseudo change.
508
509 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
510 """
511 changes = changenames.split(',')
[email protected]8d5c9a52009-06-12 15:59:08512 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root)
[email protected]fb2b8eb2009-04-23 21:03:42513 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08514 aggregate_change_info._files += ChangeInfo.Load(change,
515 local_root,
516 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24517 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42518 return aggregate_change_info
519
520
[email protected]fb2b8eb2009-04-23 21:03:42521def GetCLs():
522 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52523 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42524 if CODEREVIEW_SETTINGS_FILE in cls:
525 cls.remove(CODEREVIEW_SETTINGS_FILE)
526 return cls
527
528
529def GenerateChangeName():
530 """Generate a random changelist name."""
531 random.seed()
532 current_cl_names = GetCLs()
533 while True:
534 cl_name = (random.choice(string.ascii_lowercase) +
535 random.choice(string.digits) +
536 random.choice(string.ascii_lowercase) +
537 random.choice(string.digits))
538 if cl_name not in current_cl_names:
539 return cl_name
540
541
542def GetModifiedFiles():
543 """Returns a set that maps from changelist name to (status,filename) tuples.
544
545 Files not in a changelist have an empty changelist name. Filenames are in
546 relation to the top level directory of the current repository. Note that
547 only the current directory and subdirectories are scanned, in order to
548 improve performance while still being flexible.
549 """
550 files = {}
551
552 # Since the files are normalized to the root folder of the repositary, figure
553 # out what we need to add to the paths.
554 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
555
556 # Get a list of all files in changelists.
557 files_in_cl = {}
558 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08559 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
560 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24561 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42562 files_in_cl[filename] = change_info.name
563
564 # Get all the modified files.
[email protected]5f3eee32009-09-17 00:34:30565 status_result = gclient_scm.CaptureSVNStatus(None)
[email protected]207fdf32009-04-28 19:57:01566 for line in status_result:
567 status = line[0]
568 filename = line[1]
569 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42570 continue
[email protected]fb2b8eb2009-04-23 21:03:42571 if dir_prefix:
572 filename = os.path.join(dir_prefix, filename)
573 change_list_name = ""
574 if filename in files_in_cl:
575 change_list_name = files_in_cl[filename]
576 files.setdefault(change_list_name, []).append((status, filename))
577
578 return files
579
580
581def GetFilesNotInCL():
582 """Returns a list of tuples (status,filename) that aren't in any changelists.
583
584 See docstring of GetModifiedFiles for information about path of files and
585 which directories are scanned.
586 """
587 modified_files = GetModifiedFiles()
588 if "" not in modified_files:
589 return []
590 return modified_files[""]
591
592
593def SendToRietveld(request_path, payload=None,
594 content_type="application/octet-stream", timeout=None):
595 """Send a POST/GET to Rietveld. Returns the response body."""
596 def GetUserCredentials():
597 """Prompts the user for a username and password."""
598 email = upload.GetEmail()
599 password = getpass.getpass("Password for %s: " % email)
600 return email, password
601
602 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
603 rpc_server = upload.HttpRpcServer(server,
604 GetUserCredentials,
605 host_override=server,
606 save_cookies=True)
607 try:
608 return rpc_server.Send(request_path, payload, content_type, timeout)
[email protected]e3608df2009-11-10 20:22:57609 except urllib2.URLError:
[email protected]fb2b8eb2009-04-23 21:03:42610 if timeout is None:
611 ErrorExit("Error accessing url %s" % request_path)
612 else:
613 return None
614
615
616def GetIssueDescription(issue):
617 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48618 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42619
620
[email protected]88c32d82009-10-12 18:24:05621def Opened(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42622 """Prints a list of modified files in the current directory down."""
623 files = GetModifiedFiles()
624 cl_keys = files.keys()
625 cl_keys.sort()
626 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05627 if not cl_name:
628 continue
629 note = ""
630 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
631 fail_on_not_found=True, update_status=False)
632 if len(change_info.GetFiles()) != len(files[cl_name]):
633 note = " (Note: this changelist contains files outside this directory)"
634 print "\n--- Changelist " + cl_name + note + ":"
[email protected]e3608df2009-11-10 20:22:57635 for filename in files[cl_name]:
636 print "".join(filename)
[email protected]88c32d82009-10-12 18:24:05637 if show_unknown_files:
638 unknown_files = UnknownFiles([])
639 if (files.get('') or (show_unknown_files and len(unknown_files))):
640 print "\n--- Not in any changelist:"
[email protected]e3608df2009-11-10 20:22:57641 for item in files.get('', []):
642 print "".join(item)
[email protected]88c32d82009-10-12 18:24:05643 if show_unknown_files:
[email protected]e3608df2009-11-10 20:22:57644 for filename in unknown_files:
645 print "? %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:42646
647
648def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53649 if argv:
650 if argv[0] == 'try':
651 TryChange(None, ['--help'], swallow_exception=False)
652 return
653 if argv[0] == 'upload':
654 upload.RealMain(['upload.py', '--help'])
655 return
[email protected]fb2b8eb2009-04-23 21:03:42656
657 print (
658"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48659version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42660
661Basic commands:
662-----------------------------------------
663 gcl change change_name
664 Add/remove files to a changelist. Only scans the current directory and
665 subdirectories.
666
667 gcl upload change_name [-r [email protected],[email protected],...]
668 [--send_mail] [--no_try] [--no_presubmit]
[email protected]b2ab4942009-06-11 21:39:19669 [--no_watchlists]
[email protected]fb2b8eb2009-04-23 21:03:42670 Uploads the changelist to the server for review.
671
[email protected]3b217f52009-06-01 17:54:20672 gcl commit change_name [--no_presubmit]
[email protected]fb2b8eb2009-04-23 21:03:42673 Commits the changelist to the repository.
674
675 gcl lint change_name
676 Check all the files in the changelist for possible style violations.
677
678Advanced commands:
679-----------------------------------------
680 gcl delete change_name
681 Deletes a changelist.
682
683 gcl diff change_name
684 Diffs all files in the changelist.
685
686 gcl presubmit change_name
687 Runs presubmit checks without uploading the changelist.
688
689 gcl diff
690 Diffs all files in the current directory and subdirectories that aren't in
691 a changelist.
692
693 gcl changes
694 Lists all the the changelists and the files in them.
695
696 gcl nothave [optional directory]
697 Lists files unknown to Subversion.
698
699 gcl opened
700 Lists modified files in the current directory and subdirectories.
701
702 gcl settings
703 Print the code review settings for this directory.
704
705 gcl status
706 Lists modified and unknown files in the current directory and
707 subdirectories.
708
709 gcl try change_name
710 Sends the change to the tryserver so a trybot can do a test run on your
711 code. To send multiple changes as one path, use a comma-separated list
712 of changenames.
713 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53714
[email protected]bfd09ce2009-08-05 21:17:23715 gcl deleteempties
716 Deletes all changelists that have no files associated with them. Careful,
717 you can lose your descriptions.
718
[email protected]3bcc6ce2009-05-12 22:53:53719 gcl help [command]
720 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42721""")
722
723def GetEditor():
724 editor = os.environ.get("SVN_EDITOR")
725 if not editor:
726 editor = os.environ.get("EDITOR")
727
728 if not editor:
729 if sys.platform.startswith("win"):
730 editor = "notepad"
731 else:
732 editor = "vi"
733
734 return editor
735
736
737def GenerateDiff(files, root=None):
738 """Returns a string containing the diff for the given file list.
739
740 The files in the list should either be absolute paths or relative to the
741 given root. If no root directory is provided, the repository root will be
742 used.
743 """
744 previous_cwd = os.getcwd()
745 if root is None:
746 os.chdir(GetRepositoryRoot())
747 else:
748 os.chdir(root)
749
750 diff = []
[email protected]e3608df2009-11-10 20:22:57751 for filename in files:
[email protected]fb2b8eb2009-04-23 21:03:42752 # Use svn info output instead of os.path.isdir because the latter fails
753 # when the file is deleted.
[email protected]e3608df2009-11-10 20:22:57754 if gclient_scm.CaptureSVNInfo(filename).get("Node Kind") in ("dir",
755 "directory"):
[email protected]fb2b8eb2009-04-23 21:03:42756 continue
757 # If the user specified a custom diff command in their svn config file,
758 # then it'll be used when we do svn diff, which we don't want to happen
759 # since we want the unified diff. Using --diff-cmd=diff doesn't always
760 # work, since they can have another diff executable in their path that
761 # gives different line endings. So we use a bogus temp directory as the
762 # config directory, which gets around these problems.
763 if sys.platform.startswith("win"):
764 parent_dir = tempfile.gettempdir()
765 else:
766 parent_dir = sys.path[0] # tempdir is not secure.
767 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
768 if not os.path.exists(bogus_dir):
769 os.mkdir(bogus_dir)
[email protected]e3608df2009-11-10 20:22:57770 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, filename])
[email protected]fb2b8eb2009-04-23 21:03:42771 if output:
772 diff.append(output)
[email protected]e3608df2009-11-10 20:22:57773 elif IsSVNMoved(filename):
[email protected]c3150202009-05-13 14:31:01774 # svn diff on a mv/cp'd file outputs nothing.
775 # We put in an empty Index entry so upload.py knows about them.
[email protected]e3608df2009-11-10 20:22:57776 diff.append("\nIndex: %s\n" % filename)
[email protected]c3150202009-05-13 14:31:01777 else:
778 # The file is not modified anymore. It should be removed from the set.
779 pass
[email protected]fb2b8eb2009-04-23 21:03:42780 os.chdir(previous_cwd)
781 return "".join(diff)
782
783
[email protected]51ee0072009-06-08 19:20:05784
785def OptionallyDoPresubmitChecks(change_info, committing, args):
786 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
787 return True
[email protected]b0dfd352009-06-10 14:12:54788 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05789
790
[email protected]fb2b8eb2009-04-23 21:03:42791def UploadCL(change_info, args):
[email protected]17f59f22009-06-12 13:27:24792 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42793 print "Nothing to upload, changelist is empty."
794 return
[email protected]51ee0072009-06-08 19:20:05795 if not OptionallyDoPresubmitChecks(change_info, False, args):
796 return
797 no_try = FilterFlag(args, "--no_try") or FilterFlag(args, "--no-try")
[email protected]b2ab4942009-06-11 21:39:19798 no_watchlists = FilterFlag(args, "--no_watchlists") or \
799 FilterFlag(args, "--no-watchlists")
[email protected]fb2b8eb2009-04-23 21:03:42800
801 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05802 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42803 args.append("--send_mail")
804
805 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05806 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42807
[email protected]003c2692009-05-20 13:08:08808 # Disable try when the server is overridden.
809 server_1 = re.compile(r"^-s\b.*")
810 server_2 = re.compile(r"^--server\b.*")
811 for arg in args:
812 if server_1.match(arg) or server_2.match(arg):
813 no_try = True
814 break
[email protected]fb2b8eb2009-04-23 21:03:42815
816 upload_arg = ["upload.py", "-y"]
817 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
818 upload_arg.extend(args)
819
820 desc_file = ""
821 if change_info.issue: # Uploading a new patchset.
822 found_message = False
823 for arg in args:
824 if arg.startswith("--message") or arg.startswith("-m"):
825 found_message = True
826 break
827
828 if not found_message:
829 upload_arg.append("--message=''")
830
[email protected]32ba2602009-06-06 18:44:48831 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42832 else: # First time we upload.
833 handle, desc_file = tempfile.mkstemp(text=True)
834 os.write(handle, change_info.description)
835 os.close(handle)
836
[email protected]b2ab4942009-06-11 21:39:19837 # Watchlist processing -- CC people interested in this changeset
838 # http://dev.chromium.org/developers/contributing-code/watchlists
839 if not no_watchlists:
840 import watchlists
[email protected]17f59f22009-06-12 13:27:24841 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08842 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19843
[email protected]fb2b8eb2009-04-23 21:03:42844 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19845 if not no_watchlists and watchers:
846 # Filter out all empty elements and join by ','
847 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42848 if cc_list:
849 upload_arg.append("--cc=" + cc_list)
850 upload_arg.append("--description_file=" + desc_file + "")
851 if change_info.description:
852 subject = change_info.description[:77]
853 if subject.find("\r\n") != -1:
854 subject = subject[:subject.find("\r\n")]
855 if subject.find("\n") != -1:
856 subject = subject[:subject.find("\n")]
857 if len(change_info.description) > 77:
858 subject = subject + "..."
859 upload_arg.append("--message=" + subject)
860
861 # Change the current working directory before calling upload.py so that it
862 # shows the correct base.
863 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24864 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42865
866 # If we have a lot of files with long paths, then we won't be able to fit
867 # the command to "svn diff". Instead, we generate the diff manually for
868 # each file and concatenate them before passing it to upload.py.
869 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24870 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42871 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48872 if issue and patchset:
873 change_info.issue = int(issue)
874 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42875 change_info.Save()
876
877 if desc_file:
878 os.remove(desc_file)
879
880 # Do background work on Rietveld to lint the file so that the results are
881 # ready when the issue is viewed.
882 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
883
[email protected]57e78552009-09-11 23:04:30884 # Move back before considering try, so GetCodeReviewSettings is
885 # consistent.
886 os.chdir(previous_cwd)
887
[email protected]fb2b8eb2009-04-23 21:03:42888 # Once uploaded to Rietveld, send it to the try server.
889 if not no_try:
890 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
891 if try_on_upload and try_on_upload.lower() == 'true':
[email protected]32ba2602009-06-06 18:44:48892 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42893 if clobber:
[email protected]32ba2602009-06-06 18:44:48894 trychange_args.append('--clobber')
895 TryChange(change_info, trychange_args, swallow_exception=True)
[email protected]fb2b8eb2009-04-23 21:03:42896
[email protected]fb2b8eb2009-04-23 21:03:42897
898
899def PresubmitCL(change_info):
900 """Reports what presubmit checks on the change would report."""
[email protected]17f59f22009-06-12 13:27:24901 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42902 print "Nothing to presubmit check, changelist is empty."
903 return
904
905 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]b0dfd352009-06-10 14:12:54906 DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42907
[email protected]b0dfd352009-06-10 14:12:54908 print "\n*** Presubmit checks for COMMIT would report: ***"
909 DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42910
911
912def TryChange(change_info, args, swallow_exception):
913 """Create a diff file of change_info and send it to the try server."""
914 try:
915 import trychange
916 except ImportError:
917 if swallow_exception:
918 return
919 ErrorExit("You need to install trychange.py to use the try server.")
920
921 if change_info:
922 trychange_args = ['--name', change_info.name]
[email protected]32ba2602009-06-06 18:44:48923 if change_info.issue:
924 trychange_args.extend(["--issue", str(change_info.issue)])
925 if change_info.patchset:
926 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42927 trychange_args.extend(args)
928 trychange.TryChange(trychange_args,
[email protected]17f59f22009-06-12 13:27:24929 file_list=change_info.GetFileNames(),
[email protected]fb2b8eb2009-04-23 21:03:42930 swallow_exception=swallow_exception,
931 prog='gcl try')
932 else:
933 trychange.TryChange(args,
934 file_list=None,
935 swallow_exception=swallow_exception,
936 prog='gcl try')
937
938
939def Commit(change_info, args):
[email protected]17f59f22009-06-12 13:27:24940 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42941 print "Nothing to commit, changelist is empty."
942 return
[email protected]51ee0072009-06-08 19:20:05943 if not OptionallyDoPresubmitChecks(change_info, True, args):
944 return
[email protected]fb2b8eb2009-04-23 21:03:42945
[email protected]1bb04aa2009-06-01 17:52:11946 # We face a problem with svn here: Let's say change 'bleh' modifies
947 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
948 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
949 # The only fix is to use --non-recursive but that has its issues too:
950 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
951 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
952 # directory with child nodes". Yay...
953 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42954 if change_info.issue:
955 # Get the latest description from Rietveld.
956 change_info.description = GetIssueDescription(change_info.issue)
957
958 commit_message = change_info.description.replace('\r\n', '\n')
959 if change_info.issue:
[email protected]32ba2602009-06-06 18:44:48960 commit_message += ('\nReview URL: http://%s/%d' %
[email protected]fb2b8eb2009-04-23 21:03:42961 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
962 change_info.issue))
963
964 handle, commit_filename = tempfile.mkstemp(text=True)
965 os.write(handle, commit_message)
966 os.close(handle)
967
968 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24969 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42970 os.close(handle)
971
972 commit_cmd += ['--file=' + commit_filename]
973 commit_cmd += ['--targets=' + targets_filename]
974 # Change the current working directory before calling commit.
975 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24976 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42977 output = RunShell(commit_cmd, True)
978 os.remove(commit_filename)
979 os.remove(targets_filename)
980 if output.find("Committed revision") != -1:
981 change_info.Delete()
982
983 if change_info.issue:
984 revision = re.compile(".*?\nCommitted revision (\d+)",
985 re.DOTALL).match(output).group(1)
986 viewvc_url = GetCodeReviewSetting("VIEW_VC")
987 change_info.description = change_info.description + '\n'
988 if viewvc_url:
989 change_info.description += "\nCommitted: " + viewvc_url + revision
990 change_info.CloseIssue()
991 os.chdir(previous_cwd)
992
[email protected]2c8d4b22009-06-06 21:03:10993
[email protected]9ce98222009-10-19 20:24:17994def Change(change_info, args):
[email protected]fb2b8eb2009-04-23 21:03:42995 """Creates/edits a changelist."""
[email protected]9ce98222009-10-19 20:24:17996 silent = FilterFlag(args, "--silent")
[email protected]d36b3ed2009-11-09 18:51:42997
998 # Verify the user is running the change command from a read-write checkout.
999 svn_info = gclient_scm.CaptureSVNInfo('.')
1000 if not svn_info:
1001 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
1002 "directory.")
1003 if (svn_info.get('URL', '').startswith('http:') and
[email protected]73171902009-11-09 18:55:461004 not FilterFlag(args, "--force")):
[email protected]d36b3ed2009-11-09 18:51:421005 ErrorExit("This is a read-only checkout. Retry in a read-write checkout "
1006 "or use --force to override.")
1007
[email protected]9ce98222009-10-19 20:24:171008 if (len(args) == 1):
1009 filename = args[0]
1010 f = open(filename, 'rU')
1011 override_description = f.read()
1012 f.close()
1013 else:
1014 override_description = None
1015
[email protected]fb2b8eb2009-04-23 21:03:421016 if change_info.issue:
1017 try:
1018 description = GetIssueDescription(change_info.issue)
1019 except urllib2.HTTPError, err:
1020 if err.code == 404:
1021 # The user deleted the issue in Rietveld, so forget the old issue id.
1022 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:101023 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:421024 change_info.Save()
1025 else:
1026 ErrorExit("Error getting the description from Rietveld: " + err)
1027 else:
[email protected]85532fc2009-06-04 22:36:531028 if override_description:
1029 description = override_description
1030 else:
1031 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:421032
1033 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:231034
[email protected]f0dfba32009-08-07 22:03:371035 # Edited files (as opposed to files with only changed properties) will have
1036 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:531037 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:371038 affected_files = [x for x in other_files if file_re.match(x[0])]
1039 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:421040
1041 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:241042 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:421043 "---Paths in this changelist (" + change_info.name + "):\n")
1044 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
1045 text = (description + separator1 + '\n' +
[email protected]f0dfba32009-08-07 22:03:371046 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
1047
1048 if change_info.Exists():
1049 text += (separator2 +
1050 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
1051 else:
1052 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
1053 separator2)
1054 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:421055
1056 handle, filename = tempfile.mkstemp(text=True)
1057 os.write(handle, text)
1058 os.close(handle)
1059
[email protected]9ce98222009-10-19 20:24:171060 if not silent:
1061 os.system(GetEditor() + " " + filename)
[email protected]fb2b8eb2009-04-23 21:03:421062
1063 result = ReadFile(filename)
1064 os.remove(filename)
1065
1066 if not result:
1067 return
1068
1069 split_result = result.split(separator1, 1)
1070 if len(split_result) != 2:
1071 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1072
1073 new_description = split_result[0]
1074 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531075 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421076 change_info.description = new_description
1077 if change_info.issue:
1078 # Update the Rietveld issue with the new description.
1079 change_info.UpdateRietveldDescription()
1080
1081 new_cl_files = []
1082 for line in cl_files_text.splitlines():
1083 if not len(line):
1084 continue
1085 if line.startswith("---"):
1086 break
1087 status = line[:7]
[email protected]e3608df2009-11-10 20:22:571088 filename = line[7:]
1089 new_cl_files.append((status, filename))
[email protected]bfd09ce2009-08-05 21:17:231090
1091 if (not len(change_info._files)) and (not change_info.issue) and \
1092 (not len(new_description) and (not new_cl_files)):
1093 ErrorExit("Empty changelist not saved")
1094
[email protected]17f59f22009-06-12 13:27:241095 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421096
1097 change_info.Save()
1098 print change_info.name + " changelist saved."
1099 if change_info.MissingTests():
1100 Warn("WARNING: " + MISSING_TEST_MSG)
1101
[email protected]fb2b8eb2009-04-23 21:03:421102# Valid extensions for files we want to lint.
[email protected]e72bb632009-10-29 20:15:481103DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
1104DEFAULT_LINT_IGNORE_REGEX = r""
[email protected]fb2b8eb2009-04-23 21:03:421105
1106def Lint(change_info, args):
1107 """Runs cpplint.py on all the files in |change_info|"""
1108 try:
1109 import cpplint
1110 except ImportError:
1111 ErrorExit("You need to install cpplint.py to lint C++ files.")
1112
1113 # Change the current working directory before calling lint so that it
1114 # shows the correct base.
1115 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241116 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421117
1118 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241119 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421120
[email protected]bb816382009-10-29 01:38:021121 white_list = GetCodeReviewSetting("LINT_REGEX")
1122 if not white_list:
[email protected]e72bb632009-10-29 20:15:481123 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021124 white_regex = re.compile(white_list)
1125 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1126 if not black_list:
[email protected]e72bb632009-10-29 20:15:481127 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021128 black_regex = re.compile(black_list)
[email protected]e3608df2009-11-10 20:22:571129 for filename in filenames:
1130 if white_regex.match(filename):
1131 if black_regex.match(filename):
1132 print "Ignoring file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421133 else:
[email protected]e3608df2009-11-10 20:22:571134 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level)
[email protected]bb816382009-10-29 01:38:021135 else:
[email protected]e3608df2009-11-10 20:22:571136 print "Skipping file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421137
1138 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1139 os.chdir(previous_cwd)
1140
1141
[email protected]b0dfd352009-06-10 14:12:541142def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421143 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1144 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481145 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151146 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411147 change = presubmit_support.SvnChange(change_info.name,
1148 change_info.description,
1149 change_info.GetLocalRoot(),
1150 change_info.GetFiles(),
1151 change_info.issue,
1152 change_info.patchset)
1153 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541154 committing=committing,
[email protected]1033acc2009-05-13 14:36:481155 verbose=False,
1156 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151157 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541158 default_presubmit=root_presubmit,
1159 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551160 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421161 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1162 return result
1163
1164
1165def Changes():
1166 """Print all the changelists and their files."""
1167 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081168 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421169 print "\n--- Changelist " + change_info.name + ":"
[email protected]e3608df2009-11-10 20:22:571170 for filename in change_info.GetFiles():
1171 print "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421172
1173
[email protected]bfd09ce2009-08-05 21:17:231174def DeleteEmptyChangeLists():
1175 """Delete all changelists that have no files."""
1176 print "\n--- Deleting:"
1177 for cl in GetCLs():
1178 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1179 if not len(change_info._files):
1180 print change_info.name
1181 change_info.Delete()
1182
1183
[email protected]fb2b8eb2009-04-23 21:03:421184def main(argv=None):
1185 if argv is None:
1186 argv = sys.argv
1187
1188 if len(argv) == 1:
1189 Help()
1190 return 0;
1191
[email protected]a05be0b2009-06-30 19:13:021192 try:
1193 # Create the directories where we store information about changelists if it
1194 # doesn't exist.
1195 if not os.path.exists(GetInfoDir()):
1196 os.mkdir(GetInfoDir())
1197 if not os.path.exists(GetChangesDir()):
1198 os.mkdir(GetChangesDir())
1199 # For smooth upgrade support, move the files in GetInfoDir() to
1200 # GetChangesDir().
1201 # TODO(maruel): Remove this code in August 2009.
[email protected]e3608df2009-11-10 20:22:571202 for filename in os.listdir(unicode(GetInfoDir())):
1203 file_path = os.path.join(unicode(GetInfoDir()), filename)
1204 if os.path.isfile(file_path) and filename != CODEREVIEW_SETTINGS_FILE:
[email protected]a05be0b2009-06-30 19:13:021205 shutil.move(file_path, GetChangesDir())
1206 if not os.path.exists(GetCacheDir()):
1207 os.mkdir(GetCacheDir())
[email protected]5f3eee32009-09-17 00:34:301208 except gclient_utils.Error:
[email protected]a05be0b2009-06-30 19:13:021209 # Will throw an exception if not run in a svn checkout.
1210 pass
[email protected]fb2b8eb2009-04-23 21:03:421211
1212 # Commands that don't require an argument.
1213 command = argv[1]
[email protected]88c32d82009-10-12 18:24:051214 if command == "opened" or command == "status":
1215 Opened(command == "status")
[email protected]fb2b8eb2009-04-23 21:03:421216 return 0
1217 if command == "nothave":
[email protected]e3608df2009-11-10 20:22:571218 __pychecker__ = 'no-returnvalues'
1219 for filename in UnknownFiles(argv[2:]):
1220 print "? " + "".join(filename)
[email protected]fb2b8eb2009-04-23 21:03:421221 return 0
1222 if command == "changes":
1223 Changes()
1224 return 0
1225 if command == "help":
1226 Help(argv[2:])
1227 return 0
1228 if command == "diff" and len(argv) == 2:
1229 files = GetFilesNotInCL()
1230 print GenerateDiff([x[1] for x in files])
1231 return 0
1232 if command == "settings":
[email protected]e3608df2009-11-10 20:22:571233 # Force load settings
1234 GetCodeReviewSetting("UNKNOWN");
[email protected]a005ccd2009-06-12 13:25:541235 del CODEREVIEW_SETTINGS['__just_initialized']
1236 print '\n'.join(("%s: %s" % (str(k), str(v))
1237 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
[email protected]fb2b8eb2009-04-23 21:03:421238 return 0
[email protected]bfd09ce2009-08-05 21:17:231239 if command == "deleteempties":
1240 DeleteEmptyChangeLists()
1241 return 0
[email protected]fb2b8eb2009-04-23 21:03:421242
[email protected]a31d6582009-11-10 13:55:131243 if command == "change":
1244 if len(argv) == 2:
[email protected]fb2b8eb2009-04-23 21:03:421245 # Generate a random changelist name.
1246 changename = GenerateChangeName()
[email protected]a31d6582009-11-10 13:55:131247 elif argv[2] == '--force':
1248 changename = GenerateChangeName()
1249 # argv[3:] is passed to Change() as |args| later. Change() should receive
1250 # |args| which includes '--force'.
1251 argv.insert(2, changename)
[email protected]fb2b8eb2009-04-23 21:03:421252 else:
[email protected]a31d6582009-11-10 13:55:131253 changename = argv[2]
1254 elif len(argv) == 2:
1255 ErrorExit("Need a changelist name.")
[email protected]fb2b8eb2009-04-23 21:03:421256 else:
1257 changename = argv[2]
1258
1259 # When the command is 'try' and --patchset is used, the patch to try
1260 # is on the Rietveld server. 'change' creates a change so it's fine if the
1261 # change didn't exist. All other commands require an existing change.
1262 fail_on_not_found = command != "try" and command != "change"
1263 if command == "try" and changename.find(',') != -1:
[email protected]8d5c9a52009-06-12 15:59:081264 change_info = LoadChangelistInfoForMultiple(changename, GetRepositoryRoot(),
1265 True, True)
[email protected]fb2b8eb2009-04-23 21:03:421266 else:
[email protected]8d5c9a52009-06-12 15:59:081267 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(),
1268 fail_on_not_found, True)
[email protected]fb2b8eb2009-04-23 21:03:421269
1270 if command == "change":
[email protected]9ce98222009-10-19 20:24:171271 Change(change_info, argv[3:])
[email protected]fb2b8eb2009-04-23 21:03:421272 elif command == "lint":
1273 Lint(change_info, argv[3:])
1274 elif command == "upload":
1275 UploadCL(change_info, argv[3:])
1276 elif command == "presubmit":
1277 PresubmitCL(change_info)
1278 elif command in ("commit", "submit"):
1279 Commit(change_info, argv[3:])
1280 elif command == "delete":
1281 change_info.Delete()
1282 elif command == "try":
1283 # When the change contains no file, send the "changename" positional
1284 # argument to trychange.py.
[email protected]17f59f22009-06-12 13:27:241285 if change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421286 args = argv[3:]
1287 else:
1288 change_info = None
1289 args = argv[2:]
1290 TryChange(change_info, args, swallow_exception=False)
1291 else:
1292 # Everything else that is passed into gcl we redirect to svn, after adding
1293 # the files. This allows commands such as 'gcl diff xxx' to work.
[email protected]f40fbb32009-11-10 13:54:311294 if command == "diff" and not change_info.GetFileNames():
1295 return 0
[email protected]fb2b8eb2009-04-23 21:03:421296 args =["svn", command]
1297 root = GetRepositoryRoot()
[email protected]17f59f22009-06-12 13:27:241298 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
[email protected]fb2b8eb2009-04-23 21:03:421299 RunShell(args, True)
1300 return 0
1301
1302
1303if __name__ == "__main__":
1304 sys.exit(main())