blob: dffc566330e5934fb9e0ea87eb12141dfd1d9c8f [file] [log] [blame]
[email protected]725f1c32011-04-01 20:24:541#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
[email protected]fb2b8eb2009-04-23 21:03:423# 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
[email protected]62fd6932010-05-27 13:13:236"""\
7Wrapper script around Rietveld's upload.py that simplifies working with groups
8of files.
9"""
[email protected]fb2b8eb2009-04-23 21:03:4210
[email protected]8e13a092010-11-02 19:06:0611import optparse
[email protected]fb2b8eb2009-04-23 21:03:4212import os
13import random
14import re
15import string
[email protected]fb2b8eb2009-04-23 21:03:4216import sys
17import tempfile
[email protected]2f6a0d82010-05-12 00:03:3018import time
[email protected]fb2b8eb2009-04-23 21:03:4219import urllib2
20
[email protected]e13e12a2011-09-08 17:10:1121import breakpad # pylint: disable=W0611
22
[email protected]fa3843e2010-11-01 13:34:3723try:
[email protected]d945f362011-03-11 22:52:1924 import simplejson as json # pylint: disable=F0401
[email protected]fa3843e2010-11-01 13:34:3725except ImportError:
26 try:
[email protected]725f1c32011-04-01 20:24:5427 import json # pylint: disable=F0401
28 except ImportError:
[email protected]fa3843e2010-11-01 13:34:3729 # Import the one included in depot_tools.
30 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
[email protected]d945f362011-03-11 22:52:1931 import simplejson as json # pylint: disable=F0401
[email protected]fa3843e2010-11-01 13:34:3732
[email protected]35625c72011-03-23 17:34:0233import fix_encoding
[email protected]5f3eee32009-09-17 00:34:3034import gclient_utils
[email protected]30becc22011-03-17 01:21:2235import presubmit_support
[email protected]cab38e92011-04-09 00:30:5136import rietveld
[email protected]e13e12a2011-09-08 17:10:1137from scm import SVN
[email protected]31cb48a2011-04-04 18:01:3638import subprocess2
[email protected]e13e12a2011-09-08 17:10:1139from third_party import upload
[email protected]c1675e22009-04-27 20:30:4840
[email protected]cab38e92011-04-09 00:30:5141__version__ = '1.2.1'
[email protected]c1675e22009-04-27 20:30:4842
43
[email protected]fb2b8eb2009-04-23 21:03:4244CODEREVIEW_SETTINGS = {
[email protected]b8260242010-08-19 17:03:1645 # To make gcl send reviews to a server, check in a file named
[email protected]172b6e72010-01-26 00:35:0346 # "codereview.settings" (see |CODEREVIEW_SETTINGS_FILE| below) to your
47 # project's base directory and add the following line to codereview.settings:
48 # CODE_REVIEW_SERVER: codereview.yourserver.org
[email protected]fb2b8eb2009-04-23 21:03:4249}
50
[email protected]fb2b8eb2009-04-23 21:03:4251# globals that store the root of the current repository and the directory where
52# we store information about changelists.
[email protected]98fc2b92009-05-21 14:11:5153REPOSITORY_ROOT = ""
[email protected]fb2b8eb2009-04-23 21:03:4254
55# Filename where we store repository specific information for gcl.
56CODEREVIEW_SETTINGS_FILE = "codereview.settings"
[email protected]b8260242010-08-19 17:03:1657CODEREVIEW_SETTINGS_FILE_NOT_FOUND = (
58 'No %s file found. Please add one.' % CODEREVIEW_SETTINGS_FILE)
[email protected]fb2b8eb2009-04-23 21:03:4259
60# Warning message when the change appears to be missing tests.
61MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
62
[email protected]98fc2b92009-05-21 14:11:5163# Global cache of files cached in GetCacheDir().
64FILES_CACHE = {}
[email protected]fb2b8eb2009-04-23 21:03:4265
[email protected]4c22d722010-05-14 19:01:2266# Valid extensions for files we want to lint.
67DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
68DEFAULT_LINT_IGNORE_REGEX = r"$^"
69
[email protected]30becc22011-03-17 01:21:2270REVIEWERS_REGEX = r'\s*R=(.+)'
[email protected]4c22d722010-05-14 19:01:2271
[email protected]e5299012010-04-07 18:02:2672def CheckHomeForFile(filename):
73 """Checks the users home dir for the existence of the given file. Returns
74 the path to the file if it's there, or None if it is not.
75 """
76 home_vars = ['HOME']
77 if sys.platform in ('cygwin', 'win32'):
78 home_vars.append('USERPROFILE')
79 for home_var in home_vars:
80 home = os.getenv(home_var)
81 if home != None:
82 full_path = os.path.join(home, filename)
83 if os.path.exists(full_path):
84 return full_path
85 return None
[email protected]fb2b8eb2009-04-23 21:03:4286
[email protected]35fe9ad2010-05-25 23:59:5487
[email protected]62fd6932010-05-27 13:13:2388def UnknownFiles():
89 """Runs svn status and returns unknown files."""
90 return [item[1] for item in SVN.CaptureStatus([]) if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0191
92
[email protected]fb2b8eb2009-04-23 21:03:4293def GetRepositoryRoot():
94 """Returns the top level directory of the current repository.
95
96 The directory is returned as an absolute path.
97 """
[email protected]98fc2b92009-05-21 14:11:5198 global REPOSITORY_ROOT
99 if not REPOSITORY_ROOT:
[email protected]94b1ee92009-12-19 20:27:20100 REPOSITORY_ROOT = SVN.GetCheckoutRoot(os.getcwd())
101 if not REPOSITORY_ROOT:
[email protected]5f3eee32009-09-17 00:34:30102 raise gclient_utils.Error("gcl run outside of repository")
[email protected]98fc2b92009-05-21 14:11:51103 return REPOSITORY_ROOT
[email protected]fb2b8eb2009-04-23 21:03:42104
105
106def GetInfoDir():
107 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:52108 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
109
110
111def GetChangesDir():
112 """Returns the directory where gcl change files are stored."""
113 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:42114
115
[email protected]98fc2b92009-05-21 14:11:51116def GetCacheDir():
117 """Returns the directory where gcl change files are stored."""
118 return os.path.join(GetInfoDir(), 'cache')
119
120
121def GetCachedFile(filename, max_age=60*60*24*3, use_root=False):
122 """Retrieves a file from the repository and caches it in GetCacheDir() for
123 max_age seconds.
124
125 use_root: If False, look up the arborescence for the first match, otherwise go
126 directory to the root repository.
127
128 Note: The cache will be inconsistent if the same file is retrieved with both
[email protected]bb816382009-10-29 01:38:02129 use_root=True and use_root=False. Don't be stupid.
[email protected]98fc2b92009-05-21 14:11:51130 """
[email protected]98fc2b92009-05-21 14:11:51131 if filename not in FILES_CACHE:
132 # Don't try to look up twice.
133 FILES_CACHE[filename] = None
[email protected]9b613272009-04-24 01:28:28134 # First we check if we have a cached version.
[email protected]a05be0b2009-06-30 19:13:02135 try:
136 cached_file = os.path.join(GetCacheDir(), filename)
[email protected]31cb48a2011-04-04 18:01:36137 except (gclient_utils.Error, subprocess2.CalledProcessError):
[email protected]a05be0b2009-06-30 19:13:02138 return None
[email protected]98fc2b92009-05-21 14:11:51139 if (not os.path.exists(cached_file) or
[email protected]2f6a0d82010-05-12 00:03:30140 (time.time() - os.stat(cached_file).st_mtime) > max_age):
[email protected]b3b494f2010-08-31 20:40:08141 dir_info = SVN.CaptureInfo('.')
142 repo_root = dir_info['Repository Root']
[email protected]98fc2b92009-05-21 14:11:51143 if use_root:
144 url_path = repo_root
145 else:
[email protected]b3b494f2010-08-31 20:40:08146 url_path = dir_info['URL']
[email protected]9b613272009-04-24 01:28:28147 while True:
[email protected]fa44e4a2009-12-03 01:41:13148 # Look in the repository at the current level for the file.
[email protected]b6ee1672010-08-19 17:06:07149 for _ in range(5):
[email protected]b3b494f2010-08-31 20:40:08150 content = None
[email protected]b6ee1672010-08-19 17:06:07151 try:
152 # Take advantage of the fact that svn won't output to stderr in case
153 # of success but will do in case of failure so don't mind putting
154 # stderr into content_array.
155 content_array = []
[email protected]b3b494f2010-08-31 20:40:08156 svn_path = url_path + '/' + filename
[email protected]17368002010-09-01 23:36:32157 args = ['svn', 'cat', svn_path]
[email protected]02913a52010-08-25 20:50:50158 if sys.platform != 'darwin':
159 # MacOSX 10.5.2 has a bug with svn 1.4.4 that will trigger the
160 # 'Can\'t get username or password' and can be fixed easily.
161 # The fix doesn't work if the user upgraded to svn 1.6.x. Bleh.
162 # I don't have time to fix their broken stuff.
163 args.append('--non-interactive')
[email protected]17d01792010-09-01 18:07:10164 gclient_utils.CheckCallAndFilter(
165 args, cwd='.', filter_fn=content_array.append)
[email protected]b6ee1672010-08-19 17:06:07166 # Exit the loop if the file was found. Override content.
167 content = '\n'.join(content_array)
168 break
[email protected]31cb48a2011-04-04 18:01:36169 except (gclient_utils.Error, subprocess2.CalledProcessError):
[email protected]b6ee1672010-08-19 17:06:07170 if content_array[0].startswith(
171 'svn: Can\'t get username or password'):
172 ErrorExit('Your svn credentials expired. Please run svn update '
173 'to fix the cached credentials')
[email protected]3c842982010-08-20 17:26:49174 if content_array[0].startswith('svn: Can\'t get password'):
175 ErrorExit('If are using a Mac and svn --version shows 1.4.x, '
176 'please hack gcl.py to remove --non-interactive usage, it\'s'
177 'a bug on your installed copy')
[email protected]dcd15222010-12-21 22:43:19178 if (content_array[0].startswith('svn: File not found:') or
179 content_array[0].endswith('path not found')):
180 break
181 # Otherwise, fall through to trying again.
[email protected]b6ee1672010-08-19 17:06:07182 if content:
[email protected]9b613272009-04-24 01:28:28183 break
[email protected]9b613272009-04-24 01:28:28184 if url_path == repo_root:
185 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51186 break
[email protected]9b613272009-04-24 01:28:28187 # Go up one level to try again.
188 url_path = os.path.dirname(url_path)
[email protected]b3b494f2010-08-31 20:40:08189 if content is not None or filename != CODEREVIEW_SETTINGS_FILE:
190 # Write a cached version even if there isn't a file, so we don't try to
191 # fetch it each time. codereview.settings must always be present so do
192 # not cache negative.
193 gclient_utils.FileWrite(cached_file, content or '')
[email protected]98fc2b92009-05-21 14:11:51194 else:
[email protected]0fca4f32009-12-18 15:14:34195 content = gclient_utils.FileRead(cached_file, 'r')
[email protected]98fc2b92009-05-21 14:11:51196 # Keep the content cached in memory.
197 FILES_CACHE[filename] = content
198 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28199
[email protected]98fc2b92009-05-21 14:11:51200
201def GetCodeReviewSetting(key):
202 """Returns a value for the given key for this repository."""
203 # Use '__just_initialized' as a flag to determine if the settings were
204 # already initialized.
[email protected]98fc2b92009-05-21 14:11:51205 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47206 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
207 if settings_file:
208 for line in settings_file.splitlines():
[email protected]807c4462010-07-10 00:45:28209 if not line or line.startswith('#'):
[email protected]b0442182009-06-05 14:20:47210 continue
[email protected]807c4462010-07-10 00:45:28211 if not ':' in line:
212 raise gclient_utils.Error(
213 '%s is invalid, please fix. It\'s content:\n\n%s' %
214 (CODEREVIEW_SETTINGS_FILE, settings_file))
[email protected]dd218e52010-08-23 13:02:41215 k, v = line.split(':', 1)
216 CODEREVIEW_SETTINGS[k.strip()] = v.strip()
[email protected]98fc2b92009-05-21 14:11:51217 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42218 return CODEREVIEW_SETTINGS.get(key, "")
219
220
[email protected]fb2b8eb2009-04-23 21:03:42221def Warn(msg):
[email protected]6e29d572010-06-04 17:32:20222 print >> sys.stderr, msg
[email protected]223b7192010-06-04 18:52:58223
224
225def ErrorExit(msg):
226 print >> sys.stderr, msg
227 sys.exit(1)
[email protected]fb2b8eb2009-04-23 21:03:42228
229
230def RunShellWithReturnCode(command, print_output=False):
231 """Executes a command and returns the output and the return code."""
[email protected]e13e12a2011-09-08 17:10:11232 p = subprocess2.Popen(
233 command, stdout=subprocess2.PIPE,
234 stderr=subprocess2.STDOUT, universal_newlines=True)
[email protected]fb2b8eb2009-04-23 21:03:42235 if print_output:
236 output_array = []
237 while True:
238 line = p.stdout.readline()
239 if not line:
240 break
241 if print_output:
242 print line.strip('\n')
243 output_array.append(line)
244 output = "".join(output_array)
245 else:
246 output = p.stdout.read()
247 p.wait()
248 p.stdout.close()
249 return output, p.returncode
250
251
252def RunShell(command, print_output=False):
253 """Executes a command and returns the output."""
254 return RunShellWithReturnCode(command, print_output)[0]
255
256
[email protected]51ee0072009-06-08 19:20:05257def FilterFlag(args, flag):
258 """Returns True if the flag is present in args list.
259
260 The flag is removed from args if present.
261 """
262 if flag in args:
263 args.remove(flag)
264 return True
265 return False
266
267
[email protected]be0d1ca2009-05-12 19:23:02268class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42269 """Holds information about a changelist.
270
[email protected]32ba2602009-06-06 18:44:48271 name: change name.
272 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
273 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42274 description: the description.
275 files: a list of 2 tuple containing (status, filename) of changed files,
276 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08277 local_root: Local root directory
[email protected]bf1fdca2010-11-01 18:05:36278 rietveld: rietveld server for this change
[email protected]fb2b8eb2009-04-23 21:03:42279 """
[email protected]bf1fdca2010-11-01 18:05:36280 # Kept for unit test support. This is for the old format, it's deprecated.
[email protected]32ba2602009-06-06 18:44:48281 _SEPARATOR = "\n-----\n"
[email protected]32ba2602009-06-06 18:44:48282
[email protected]ea452b32009-11-22 20:04:31283 def __init__(self, name, issue, patchset, description, files, local_root,
[email protected]6493ed12011-04-14 13:51:00284 rietveld_url, needs_upload):
[email protected]fb2b8eb2009-04-23 21:03:42285 self.name = name
[email protected]32ba2602009-06-06 18:44:48286 self.issue = int(issue)
287 self.patchset = int(patchset)
[email protected]20254fc2011-03-22 18:28:59288 self._description = None
289 self._subject = None
290 self._reviewers = None
[email protected]30becc22011-03-17 01:21:22291 self._set_description(description)
[email protected]be0d1ca2009-05-12 19:23:02292 if files is None:
293 files = []
[email protected]17f59f22009-06-12 13:27:24294 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42295 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08296 self._local_root = local_root
[email protected]ea452b32009-11-22 20:04:31297 self.needs_upload = needs_upload
[email protected]cab38e92011-04-09 00:30:51298 self.rietveld = rietveld_url
[email protected]bf1fdca2010-11-01 18:05:36299 if not self.rietveld:
300 # Set the default value.
301 self.rietveld = GetCodeReviewSetting('CODE_REVIEW_SERVER')
[email protected]cab38e92011-04-09 00:30:51302 self._rpc_server = None
[email protected]ea452b32009-11-22 20:04:31303
[email protected]30becc22011-03-17 01:21:22304 def _get_description(self):
[email protected]20254fc2011-03-22 18:28:59305 return self._description
[email protected]30becc22011-03-17 01:21:22306
307 def _set_description(self, description):
[email protected]20254fc2011-03-22 18:28:59308 # TODO(dpranke): Cloned from git_cl.py. These should be shared.
309 if not description:
310 self._description = description
311 return
312
313 parsed_lines = []
314 reviewers_re = re.compile(REVIEWERS_REGEX)
315 reviewers = ''
316 subject = ''
317 for l in description.splitlines():
318 if not subject:
319 subject = l
320 matched_reviewers = reviewers_re.match(l)
321 if matched_reviewers:
322 reviewers = matched_reviewers.group(1).split(',')
323 parsed_lines.append(l)
324
325 if len(subject) > 100:
326 subject = subject[:97] + '...'
327
328 self._subject = subject
329 self._reviewers = reviewers
330 self._description = '\n'.join(parsed_lines)
[email protected]30becc22011-03-17 01:21:22331
332 description = property(_get_description, _set_description)
333
334 @property
335 def reviewers(self):
[email protected]20254fc2011-03-22 18:28:59336 return self._reviewers
[email protected]30becc22011-03-17 01:21:22337
338 @property
339 def subject(self):
[email protected]20254fc2011-03-22 18:28:59340 return self._subject
[email protected]30becc22011-03-17 01:21:22341
[email protected]ea452b32009-11-22 20:04:31342 def NeedsUpload(self):
343 return self.needs_upload
[email protected]fb2b8eb2009-04-23 21:03:42344
[email protected]17f59f22009-06-12 13:27:24345 def GetFileNames(self):
346 """Returns the list of file names included in this change."""
[email protected]e3608df2009-11-10 20:22:57347 return [f[1] for f in self._files]
[email protected]17f59f22009-06-12 13:27:24348
349 def GetFiles(self):
350 """Returns the list of files included in this change with their status."""
351 return self._files
352
353 def GetLocalRoot(self):
354 """Returns the local repository checkout root directory."""
355 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42356
[email protected]f0dfba32009-08-07 22:03:37357 def Exists(self):
358 """Returns True if this change already exists (i.e., is not new)."""
359 return (self.issue or self.description or self._files)
360
[email protected]fb2b8eb2009-04-23 21:03:42361 def _NonDeletedFileList(self):
362 """Returns a list of files in this change, not including deleted files."""
[email protected]e3608df2009-11-10 20:22:57363 return [f[1] for f in self.GetFiles()
364 if not f[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42365
366 def _AddedFileList(self):
367 """Returns a list of files added in this change."""
[email protected]e3608df2009-11-10 20:22:57368 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42369
370 def Save(self):
371 """Writes the changelist information to disk."""
[email protected]fa3843e2010-11-01 13:34:37372 data = json.dumps({
373 'issue': self.issue,
374 'patchset': self.patchset,
375 'needs_upload': self.NeedsUpload(),
376 'files': self.GetFiles(),
[email protected]20254fc2011-03-22 18:28:59377 'description': self.description,
[email protected]bf1fdca2010-11-01 18:05:36378 'rietveld': self.rietveld,
[email protected]fa3843e2010-11-01 13:34:37379 }, sort_keys=True, indent=2)
[email protected]fc83c112009-12-18 15:14:10380 gclient_utils.FileWrite(GetChangelistInfoFile(self.name), data)
[email protected]fb2b8eb2009-04-23 21:03:42381
382 def Delete(self):
383 """Removes the changelist information from disk."""
384 os.remove(GetChangelistInfoFile(self.name))
385
[email protected]cab38e92011-04-09 00:30:51386 def RpcServer(self):
387 if not self._rpc_server:
388 if not self.rietveld:
389 ErrorExit(CODEREVIEW_SETTINGS_FILE_NOT_FOUND)
390 self._rpc_server = rietveld.Rietveld(self.rietveld, None, None)
391 return self._rpc_server
392
[email protected]fb2b8eb2009-04-23 21:03:42393 def CloseIssue(self):
394 """Closes the Rietveld issue for this changelist."""
[email protected]41db6ef2010-11-17 19:42:34395 # Newer versions of Rietveld require us to pass an XSRF token to POST, so
396 # we fetch it from the server.
397 xsrf_token = self.SendToRietveld(
398 '/xsrf_token',
399 extra_headers={'X-Requesting-XSRF-Token': '1'})
400
401 # You cannot close an issue with a GET.
402 # We pass an empty string for the data so it is a POST rather than a GET.
403 data = [("description", self.description),
404 ("xsrf_token", xsrf_token)]
[email protected]fb2b8eb2009-04-23 21:03:42405 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]41db6ef2010-11-17 19:42:34406 self.SendToRietveld('/%d/close' % self.issue, payload=body,
407 content_type=ctype)
[email protected]fb2b8eb2009-04-23 21:03:42408
409 def UpdateRietveldDescription(self):
410 """Sets the description for an issue on Rietveld."""
411 data = [("description", self.description),]
412 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]41db6ef2010-11-17 19:42:34413 self.SendToRietveld('/%d/description' % self.issue, payload=body,
414 content_type=ctype)
[email protected]bf1fdca2010-11-01 18:05:36415
416 def GetIssueDescription(self):
417 """Returns the issue description from Rietveld."""
418 return self.SendToRietveld('/%d/description' % self.issue)
419
420 def PrimeLint(self):
421 """Do background work on Rietveld to lint the file so that the results are
422 ready when the issue is viewed."""
423 if self.issue and self.patchset:
424 self.SendToRietveld('/lint/issue%s_%s' % (self.issue, self.patchset),
425 timeout=1)
426
[email protected]41db6ef2010-11-17 19:42:34427 def SendToRietveld(self, request_path, timeout=None, **kwargs):
[email protected]bf1fdca2010-11-01 18:05:36428 """Send a POST/GET to Rietveld. Returns the response body."""
[email protected]bf1fdca2010-11-01 18:05:36429 try:
[email protected]cab38e92011-04-09 00:30:51430 return self.RpcServer().Send(request_path, timeout=timeout, **kwargs)
[email protected]bf1fdca2010-11-01 18:05:36431 except urllib2.URLError:
432 if timeout is None:
433 ErrorExit('Error accessing url %s' % request_path)
434 else:
435 return None
[email protected]fb2b8eb2009-04-23 21:03:42436
437 def MissingTests(self):
438 """Returns True if the change looks like it needs unit tests but has none.
439
440 A change needs unit tests if it contains any new source files or methods.
441 """
442 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
443 # Ignore third_party entirely.
[email protected]e3608df2009-11-10 20:22:57444 files = [f for f in self._NonDeletedFileList()
445 if f.find("third_party") == -1]
446 added_files = [f for f in self._AddedFileList()
447 if f.find("third_party") == -1]
[email protected]fb2b8eb2009-04-23 21:03:42448
449 # If the change is entirely in third_party, we're done.
450 if len(files) == 0:
451 return False
452
453 # Any new or modified test files?
454 # A test file's name ends with "test.*" or "tests.*".
455 test_files = [test for test in files
456 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
457 if len(test_files) > 0:
458 return False
459
460 # Any new source files?
[email protected]e3608df2009-11-10 20:22:57461 source_files = [item for item in added_files
462 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
[email protected]fb2b8eb2009-04-23 21:03:42463 if len(source_files) > 0:
464 return True
465
466 # Do the long test, checking the files for new methods.
467 return self._HasNewMethod()
468
469 def _HasNewMethod(self):
470 """Returns True if the changeset contains any new functions, or if a
471 function signature has been changed.
472
473 A function is identified by starting flush left, containing a "(" before
474 the next flush-left line, and either ending with "{" before the next
475 flush-left line or being followed by an unindented "{".
476
477 Currently this returns True for new methods, new static functions, and
478 methods or functions whose signatures have been changed.
479
480 Inline methods added to header files won't be detected by this. That's
481 acceptable for purposes of determining if a unit test is needed, since
482 inline methods should be trivial.
483 """
484 # To check for methods added to source or header files, we need the diffs.
485 # We'll generate them all, since there aren't likely to be many files
486 # apart from source and headers; besides, we'll want them all if we're
487 # uploading anyway.
488 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24489 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42490
491 definition = ""
492 for line in self.patch.splitlines():
493 if not line.startswith("+"):
494 continue
495 line = line.strip("+").rstrip(" \t")
496 # Skip empty lines, comments, and preprocessor directives.
497 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
498 if line == "" or line.startswith("/") or line.startswith("#"):
499 continue
500
501 # A possible definition ending with "{" is complete, so check it.
502 if definition.endswith("{"):
503 if definition.find("(") != -1:
504 return True
505 definition = ""
506
507 # A { or an indented line, when we're in a definition, continues it.
508 if (definition != "" and
509 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
510 definition += line
511
512 # A flush-left line starts a new possible function definition.
513 elif not line.startswith(" ") and not line.startswith("\t"):
514 definition = line
515
516 return False
517
[email protected]32ba2602009-06-06 18:44:48518 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08519 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48520 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42521
[email protected]32ba2602009-06-06 18:44:48522 Args:
523 fail_on_not_found: if True, this function will quit the program if the
524 changelist doesn't exist.
525 update_status: if True, the svn status will be updated for all the files
526 and unchanged files will be removed.
527
528 Returns: a ChangeInfo object.
529 """
530 info_file = GetChangelistInfoFile(changename)
531 if not os.path.exists(info_file):
532 if fail_on_not_found:
533 ErrorExit("Changelist " + changename + " not found.")
[email protected]6493ed12011-04-14 13:51:00534 return ChangeInfo(changename, 0, 0, '', None, local_root, None, False)
[email protected]7af6c4d2011-09-26 21:04:18535 content = gclient_utils.FileRead(info_file)
[email protected]8a8ea022010-11-01 13:27:32536 save = False
537 try:
[email protected]fa3843e2010-11-01 13:34:37538 values = ChangeInfo._LoadNewFormat(content)
[email protected]8a8ea022010-11-01 13:27:32539 except ValueError:
[email protected]fa3843e2010-11-01 13:34:37540 try:
541 values = ChangeInfo._LoadOldFormat(content)
542 save = True
543 except ValueError:
544 ErrorExit(
545 ('Changelist file %s is corrupt.\n'
546 'Either run "gcl delete %s" or manually edit the file') % (
547 info_file, changename))
[email protected]8a8ea022010-11-01 13:27:32548 files = values['files']
[email protected]32ba2602009-06-06 18:44:48549 if update_status:
[email protected]79b7ef02010-11-01 13:25:13550 for item in files[:]:
[email protected]e3608df2009-11-10 20:22:57551 filename = os.path.join(local_root, item[1])
[email protected]5aeb7dd2009-11-17 18:09:01552 status_result = SVN.CaptureStatus(filename)
[email protected]32ba2602009-06-06 18:44:48553 if not status_result or not status_result[0][0]:
554 # File has been reverted.
555 save = True
[email protected]e3608df2009-11-10 20:22:57556 files.remove(item)
[email protected]32ba2602009-06-06 18:44:48557 continue
558 status = status_result[0][0]
[email protected]e3608df2009-11-10 20:22:57559 if status != item[0]:
[email protected]32ba2602009-06-06 18:44:48560 save = True
[email protected]e3608df2009-11-10 20:22:57561 files[files.index(item)] = (status, item[1])
[email protected]6493ed12011-04-14 13:51:00562 change_info = ChangeInfo(
563 changename,
564 values['issue'],
565 values['patchset'],
566 values['description'],
567 files,
568 local_root,
569 values.get('rietveld'),
570 values['needs_upload'])
[email protected]32ba2602009-06-06 18:44:48571 if save:
572 change_info.Save()
573 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42574
[email protected]8a8ea022010-11-01 13:27:32575 @staticmethod
576 def _LoadOldFormat(content):
[email protected]bf1fdca2010-11-01 18:05:36577 # The info files have the following format:
578 # issue_id, patchset\n (, patchset is optional)
579 # _SEPARATOR\n
580 # filepath1\n
581 # filepath2\n
582 # .
583 # .
584 # filepathn\n
585 # _SEPARATOR\n
586 # description
[email protected]8a8ea022010-11-01 13:27:32587 split_data = content.split(ChangeInfo._SEPARATOR, 2)
588 if len(split_data) != 3:
589 raise ValueError('Bad change format')
590 values = {
591 'issue': 0,
592 'patchset': 0,
593 'needs_upload': False,
594 'files': [],
595 }
596 items = split_data[0].split(', ')
597 if items[0]:
598 values['issue'] = int(items[0])
599 if len(items) > 1:
600 values['patchset'] = int(items[1])
601 if len(items) > 2:
602 values['needs_upload'] = (items[2] == "dirty")
603 for line in split_data[1].splitlines():
604 status = line[:7]
605 filename = line[7:]
606 values['files'].append((status, filename))
607 values['description'] = split_data[2]
608 return values
609
[email protected]fa3843e2010-11-01 13:34:37610 @staticmethod
611 def _LoadNewFormat(content):
612 return json.loads(content)
613
[email protected]fb2b8eb2009-04-23 21:03:42614
615def GetChangelistInfoFile(changename):
616 """Returns the file that stores information about a changelist."""
617 if not changename or re.search(r'[^\w-]', changename):
618 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52619 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42620
621
[email protected]8d5c9a52009-06-12 15:59:08622def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
623 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42624 """Loads many changes and merge their files list into one pseudo change.
625
626 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
627 """
628 changes = changenames.split(',')
[email protected]6493ed12011-04-14 13:51:00629 aggregate_change_info = ChangeInfo(
630 changenames, 0, 0, '', None, local_root, None, False)
[email protected]fb2b8eb2009-04-23 21:03:42631 for change in changes:
[email protected]6493ed12011-04-14 13:51:00632 aggregate_change_info._files += ChangeInfo.Load(
633 change, local_root, fail_on_not_found, update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42634 return aggregate_change_info
635
636
[email protected]fb2b8eb2009-04-23 21:03:42637def GetCLs():
638 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52639 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42640 if CODEREVIEW_SETTINGS_FILE in cls:
641 cls.remove(CODEREVIEW_SETTINGS_FILE)
642 return cls
643
644
645def GenerateChangeName():
646 """Generate a random changelist name."""
647 random.seed()
648 current_cl_names = GetCLs()
649 while True:
650 cl_name = (random.choice(string.ascii_lowercase) +
651 random.choice(string.digits) +
652 random.choice(string.ascii_lowercase) +
653 random.choice(string.digits))
654 if cl_name not in current_cl_names:
655 return cl_name
656
657
658def GetModifiedFiles():
659 """Returns a set that maps from changelist name to (status,filename) tuples.
660
661 Files not in a changelist have an empty changelist name. Filenames are in
662 relation to the top level directory of the current repository. Note that
663 only the current directory and subdirectories are scanned, in order to
664 improve performance while still being flexible.
665 """
666 files = {}
667
668 # Since the files are normalized to the root folder of the repositary, figure
669 # out what we need to add to the paths.
670 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
671
672 # Get a list of all files in changelists.
673 files_in_cl = {}
674 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08675 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
676 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24677 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42678 files_in_cl[filename] = change_info.name
679
680 # Get all the modified files.
[email protected]5aeb7dd2009-11-17 18:09:01681 status_result = SVN.CaptureStatus(None)
[email protected]207fdf32009-04-28 19:57:01682 for line in status_result:
683 status = line[0]
684 filename = line[1]
685 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42686 continue
[email protected]fb2b8eb2009-04-23 21:03:42687 if dir_prefix:
688 filename = os.path.join(dir_prefix, filename)
689 change_list_name = ""
690 if filename in files_in_cl:
691 change_list_name = files_in_cl[filename]
692 files.setdefault(change_list_name, []).append((status, filename))
693
694 return files
695
696
697def GetFilesNotInCL():
698 """Returns a list of tuples (status,filename) that aren't in any changelists.
699
700 See docstring of GetModifiedFiles for information about path of files and
701 which directories are scanned.
702 """
703 modified_files = GetModifiedFiles()
704 if "" not in modified_files:
705 return []
706 return modified_files[""]
707
708
[email protected]4c22d722010-05-14 19:01:22709def ListFiles(show_unknown_files):
[email protected]fb2b8eb2009-04-23 21:03:42710 files = GetModifiedFiles()
711 cl_keys = files.keys()
712 cl_keys.sort()
713 for cl_name in cl_keys:
[email protected]88c32d82009-10-12 18:24:05714 if not cl_name:
715 continue
716 note = ""
717 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
718 fail_on_not_found=True, update_status=False)
719 if len(change_info.GetFiles()) != len(files[cl_name]):
720 note = " (Note: this changelist contains files outside this directory)"
721 print "\n--- Changelist " + cl_name + note + ":"
[email protected]e3608df2009-11-10 20:22:57722 for filename in files[cl_name]:
723 print "".join(filename)
[email protected]88c32d82009-10-12 18:24:05724 if show_unknown_files:
[email protected]62fd6932010-05-27 13:13:23725 unknown_files = UnknownFiles()
[email protected]88c32d82009-10-12 18:24:05726 if (files.get('') or (show_unknown_files and len(unknown_files))):
727 print "\n--- Not in any changelist:"
[email protected]e3608df2009-11-10 20:22:57728 for item in files.get('', []):
729 print "".join(item)
[email protected]88c32d82009-10-12 18:24:05730 if show_unknown_files:
[email protected]e3608df2009-11-10 20:22:57731 for filename in unknown_files:
732 print "? %s" % filename
[email protected]4c22d722010-05-14 19:01:22733 return 0
[email protected]fb2b8eb2009-04-23 21:03:42734
735
[email protected]20254fc2011-03-22 18:28:59736def GetEditor():
737 editor = os.environ.get("SVN_EDITOR")
738 if not editor:
739 editor = os.environ.get("EDITOR")
740
741 if not editor:
742 if sys.platform.startswith("win"):
743 editor = "notepad"
744 else:
745 editor = "vi"
746
747 return editor
748
749
[email protected]fb2b8eb2009-04-23 21:03:42750def GenerateDiff(files, root=None):
[email protected]f2f9d552009-12-22 00:12:57751 return SVN.GenerateDiff(files, root=root)
[email protected]fb2b8eb2009-04-23 21:03:42752
[email protected]51ee0072009-06-08 19:20:05753
754def OptionallyDoPresubmitChecks(change_info, committing, args):
755 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
[email protected]30becc22011-03-17 01:21:22756 return presubmit_support.PresubmitOutput()
[email protected]b0dfd352009-06-10 14:12:54757 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05758
759
[email protected]62fd6932010-05-27 13:13:23760def defer_attributes(a, b):
761 """Copy attributes from an object (like a function) to another."""
762 for x in dir(a):
763 if not getattr(b, x, None):
764 setattr(b, x, getattr(a, x))
765
766
[email protected]35fe9ad2010-05-25 23:59:54767def need_change(function):
768 """Converts args -> change_info."""
[email protected]3a174252010-10-29 15:54:51769 # pylint: disable=W0612,W0621
[email protected]35fe9ad2010-05-25 23:59:54770 def hook(args):
771 if not len(args) == 1:
772 ErrorExit("You need to pass a change list name")
773 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(), True, True)
774 return function(change_info)
[email protected]62fd6932010-05-27 13:13:23775 defer_attributes(function, hook)
776 hook.need_change = True
777 hook.no_args = True
[email protected]35fe9ad2010-05-25 23:59:54778 return hook
779
780
[email protected]62fd6932010-05-27 13:13:23781def need_change_and_args(function):
782 """Converts args -> change_info."""
[email protected]3a174252010-10-29 15:54:51783 # pylint: disable=W0612,W0621
[email protected]62fd6932010-05-27 13:13:23784 def hook(args):
[email protected]e56fe822010-05-28 20:36:57785 if not args:
786 ErrorExit("You need to pass a change list name")
[email protected]62fd6932010-05-27 13:13:23787 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
788 return function(change_info, args)
789 defer_attributes(function, hook)
790 hook.need_change = True
791 return hook
792
793
794def no_args(function):
795 """Make sure no args are passed."""
[email protected]3a174252010-10-29 15:54:51796 # pylint: disable=W0612,W0621
[email protected]62fd6932010-05-27 13:13:23797 def hook(args):
798 if args:
799 ErrorExit("Doesn't support arguments")
800 return function()
801 defer_attributes(function, hook)
802 hook.no_args = True
803 return hook
804
805
806def attrs(**kwargs):
807 """Decorate a function with new attributes."""
808 def decorate(function):
809 for k in kwargs:
810 setattr(function, k, kwargs[k])
811 return function
812 return decorate
813
814
815@no_args
816def CMDopened():
817 """Lists modified files in the current directory down."""
818 return ListFiles(False)
819
820
821@no_args
822def CMDstatus():
823 """Lists modified and unknown files in the current directory down."""
824 return ListFiles(True)
825
826
827@need_change_and_args
[email protected]2b9aa8e2010-08-25 20:01:42828@attrs(usage='[--no_presubmit] [--no_watchlists]')
[email protected]62fd6932010-05-27 13:13:23829def CMDupload(change_info, args):
830 """Uploads the changelist to the server for review.
831
[email protected]5db5ba52010-07-20 15:50:47832 This does not submit a try job; use gcl try to submit a try job.
[email protected]62fd6932010-05-27 13:13:23833 """
[email protected]10ccd112010-08-24 16:48:42834 if '-s' in args or '--server' in args:
835 ErrorExit('Don\'t use the -s flag, fix codereview.settings instead')
[email protected]17f59f22009-06-12 13:27:24836 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42837 print "Nothing to upload, changelist is empty."
[email protected]35fe9ad2010-05-25 23:59:54838 return 0
[email protected]30becc22011-03-17 01:21:22839
840 output = OptionallyDoPresubmitChecks(change_info, False, args)
841 if not output.should_continue():
[email protected]35fe9ad2010-05-25 23:59:54842 return 1
[email protected]62fd6932010-05-27 13:13:23843 no_watchlists = (FilterFlag(args, "--no_watchlists") or
844 FilterFlag(args, "--no-watchlists"))
[email protected]fb2b8eb2009-04-23 21:03:42845
846 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05847 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42848 args.append("--send_mail")
849
[email protected]fb2b8eb2009-04-23 21:03:42850 upload_arg = ["upload.py", "-y"]
[email protected]bf1fdca2010-11-01 18:05:36851 upload_arg.append("--server=%s" % change_info.rietveld)
[email protected]30becc22011-03-17 01:21:22852
853 reviewers = change_info.reviewers or output.reviewers
854 if (reviewers and
855 not any(arg.startswith('-r') or arg.startswith('--reviewer') for
856 arg in args)):
857 upload_arg.append('--reviewers=%s' % ','.join(reviewers))
858
[email protected]fb2b8eb2009-04-23 21:03:42859 upload_arg.extend(args)
860
861 desc_file = ""
862 if change_info.issue: # Uploading a new patchset.
863 found_message = False
864 for arg in args:
865 if arg.startswith("--message") or arg.startswith("-m"):
866 found_message = True
867 break
868
869 if not found_message:
870 upload_arg.append("--message=''")
871
[email protected]32ba2602009-06-06 18:44:48872 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42873 else: # First time we upload.
874 handle, desc_file = tempfile.mkstemp(text=True)
875 os.write(handle, change_info.description)
876 os.close(handle)
877
[email protected]b2ab4942009-06-11 21:39:19878 # Watchlist processing -- CC people interested in this changeset
879 # http://dev.chromium.org/developers/contributing-code/watchlists
880 if not no_watchlists:
881 import watchlists
[email protected]17f59f22009-06-12 13:27:24882 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08883 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19884
[email protected]fb2b8eb2009-04-23 21:03:42885 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19886 if not no_watchlists and watchers:
[email protected]6e29d572010-06-04 17:32:20887 # Filter out all empty elements and join by ','
888 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42889 if cc_list:
890 upload_arg.append("--cc=" + cc_list)
891 upload_arg.append("--description_file=" + desc_file + "")
[email protected]30becc22011-03-17 01:21:22892 if change_info.subject:
[email protected]b68786e2011-03-17 01:39:09893 upload_arg.append("--message=" + change_info.subject)
[email protected]fb2b8eb2009-04-23 21:03:42894
[email protected]83b6e4b2010-03-09 03:16:14895 if GetCodeReviewSetting("PRIVATE") == "True":
896 upload_arg.append("--private")
897
[email protected]fb2b8eb2009-04-23 21:03:42898 # Change the current working directory before calling upload.py so that it
899 # shows the correct base.
900 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24901 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42902 # If we have a lot of files with long paths, then we won't be able to fit
903 # the command to "svn diff". Instead, we generate the diff manually for
904 # each file and concatenate them before passing it to upload.py.
905 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24906 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]9ce0dff2011-04-04 17:56:50907 try:
908 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
909 except KeyboardInterrupt:
910 sys.exit(1)
[email protected]32ba2602009-06-06 18:44:48911 if issue and patchset:
912 change_info.issue = int(issue)
913 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42914 change_info.Save()
915
916 if desc_file:
917 os.remove(desc_file)
[email protected]bf1fdca2010-11-01 18:05:36918 change_info.PrimeLint()
[email protected]57e78552009-09-11 23:04:30919 os.chdir(previous_cwd)
[email protected]02287952010-07-23 22:36:58920 print "*** Upload does not submit a try; use gcl try to submit a try. ***"
[email protected]35fe9ad2010-05-25 23:59:54921 return 0
[email protected]fb2b8eb2009-04-23 21:03:42922
[email protected]fb2b8eb2009-04-23 21:03:42923
[email protected]8e13a092010-11-02 19:06:06924@need_change_and_args
925@attrs(usage='[--upload]')
926def CMDpresubmit(change_info, args):
[email protected]62fd6932010-05-27 13:13:23927 """Runs presubmit checks on the change.
928
929 The actual presubmit code is implemented in presubmit_support.py and looks
930 for PRESUBMIT.py files."""
[email protected]17f59f22009-06-12 13:27:24931 if not change_info.GetFiles():
[email protected]8e13a092010-11-02 19:06:06932 print('Nothing to presubmit check, changelist is empty.')
[email protected]4c22d722010-05-14 19:01:22933 return 0
[email protected]8e13a092010-11-02 19:06:06934 parser = optparse.OptionParser()
935 parser.add_option('--upload', action='store_true')
936 options, args = parser.parse_args(args)
937 if args:
938 parser.error('Unrecognized args: %s' % args)
939 if options.upload:
940 print('*** Presubmit checks for UPLOAD would report: ***')
941 return not DoPresubmitChecks(change_info, False, False)
942 else:
943 print('*** Presubmit checks for COMMIT would report: ***')
944 return not DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42945
946
947def TryChange(change_info, args, swallow_exception):
948 """Create a diff file of change_info and send it to the try server."""
949 try:
950 import trychange
951 except ImportError:
952 if swallow_exception:
[email protected]35fe9ad2010-05-25 23:59:54953 return 1
[email protected]fb2b8eb2009-04-23 21:03:42954 ErrorExit("You need to install trychange.py to use the try server.")
955
[email protected]18111352009-12-20 17:21:28956 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42957 if change_info:
[email protected]18111352009-12-20 17:21:28958 trychange_args.extend(['--name', change_info.name])
[email protected]32ba2602009-06-06 18:44:48959 if change_info.issue:
960 trychange_args.extend(["--issue", str(change_info.issue)])
961 if change_info.patchset:
962 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]1227c7d2009-12-22 00:54:27963 file_list = change_info.GetFileNames()
[email protected]c0c424b2011-09-26 19:58:21964 change = presubmit_support.SvnChange(change_info.name,
965 change_info.description,
966 change_info.GetLocalRoot(),
967 change_info.GetFiles(),
968 change_info.issue,
969 change_info.patchset,
970 None)
[email protected]fb2b8eb2009-04-23 21:03:42971 else:
[email protected]09c0dba2010-10-14 14:32:22972 file_list = []
[email protected]c0c424b2011-09-26 19:58:21973
[email protected]d3e57542011-03-15 09:43:47974 trychange_args.extend(args)
[email protected]d0891922010-05-31 18:33:16975 return trychange.TryChange(
976 trychange_args,
[email protected]c0c424b2011-09-26 19:58:21977 change=change,
[email protected]d0891922010-05-31 18:33:16978 file_list=file_list,
979 swallow_exception=swallow_exception,
980 prog='gcl try',
981 extra_epilog='\n'
982 'When called from gcl, use the format gcl try <change_name>.\n')
[email protected]fb2b8eb2009-04-23 21:03:42983
984
[email protected]62fd6932010-05-27 13:13:23985@need_change_and_args
986@attrs(usage='[--no_presubmit]')
[email protected]4357af22010-05-27 15:42:34987def CMDcommit(change_info, args):
[email protected]62fd6932010-05-27 13:13:23988 """Commits the changelist to the repository."""
[email protected]17f59f22009-06-12 13:27:24989 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42990 print "Nothing to commit, changelist is empty."
[email protected]4c22d722010-05-14 19:01:22991 return 1
[email protected]73ac0f12011-03-17 23:34:22992
993 output = OptionallyDoPresubmitChecks(change_info, True, args)
994 if not output.should_continue():
[email protected]4c22d722010-05-14 19:01:22995 return 1
[email protected]fb2b8eb2009-04-23 21:03:42996
[email protected]1bb04aa2009-06-01 17:52:11997 # We face a problem with svn here: Let's say change 'bleh' modifies
998 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
999 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
1000 # The only fix is to use --non-recursive but that has its issues too:
1001 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
1002 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
1003 # directory with child nodes". Yay...
1004 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:421005 if change_info.issue:
1006 # Get the latest description from Rietveld.
[email protected]bf1fdca2010-11-01 18:05:361007 change_info.description = change_info.GetIssueDescription()
[email protected]fb2b8eb2009-04-23 21:03:421008
1009 commit_message = change_info.description.replace('\r\n', '\n')
1010 if change_info.issue:
[email protected]bf1fdca2010-11-01 18:05:361011 server = change_info.rietveld
[email protected]fcff9272010-04-29 23:56:191012 if not server.startswith("http://") and not server.startswith("https://"):
1013 server = "http://" + server
1014 commit_message += ('\nReview URL: %s/%d' % (server, change_info.issue))
[email protected]fb2b8eb2009-04-23 21:03:421015
1016 handle, commit_filename = tempfile.mkstemp(text=True)
1017 os.write(handle, commit_message)
1018 os.close(handle)
1019
1020 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:241021 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:421022 os.close(handle)
1023
1024 commit_cmd += ['--file=' + commit_filename]
1025 commit_cmd += ['--targets=' + targets_filename]
1026 # Change the current working directory before calling commit.
1027 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241028 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421029 output = RunShell(commit_cmd, True)
1030 os.remove(commit_filename)
1031 os.remove(targets_filename)
1032 if output.find("Committed revision") != -1:
1033 change_info.Delete()
1034
1035 if change_info.issue:
1036 revision = re.compile(".*?\nCommitted revision (\d+)",
1037 re.DOTALL).match(output).group(1)
1038 viewvc_url = GetCodeReviewSetting("VIEW_VC")
[email protected]30becc22011-03-17 01:21:221039 change_info.description += '\n'
[email protected]fb2b8eb2009-04-23 21:03:421040 if viewvc_url:
1041 change_info.description += "\nCommitted: " + viewvc_url + revision
1042 change_info.CloseIssue()
1043 os.chdir(previous_cwd)
[email protected]4c22d722010-05-14 19:01:221044 return 0
[email protected]fb2b8eb2009-04-23 21:03:421045
[email protected]2c8d4b22009-06-06 21:03:101046
[email protected]35fe9ad2010-05-25 23:59:541047def CMDchange(args):
[email protected]62fd6932010-05-27 13:13:231048 """Creates or edits a changelist.
1049
1050 Only scans the current directory and subdirectories."""
[email protected]35fe9ad2010-05-25 23:59:541051 if len(args) == 0:
1052 # Generate a random changelist name.
1053 changename = GenerateChangeName()
1054 elif args[0] == '--force':
1055 changename = GenerateChangeName()
1056 else:
1057 changename = args[0]
1058 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(), False, True)
[email protected]9ce98222009-10-19 20:24:171059 silent = FilterFlag(args, "--silent")
[email protected]d36b3ed2009-11-09 18:51:421060
1061 # Verify the user is running the change command from a read-write checkout.
[email protected]5aeb7dd2009-11-17 18:09:011062 svn_info = SVN.CaptureInfo('.')
[email protected]d36b3ed2009-11-09 18:51:421063 if not svn_info:
1064 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
1065 "directory.")
[email protected]d36b3ed2009-11-09 18:51:421066
[email protected]35fe9ad2010-05-25 23:59:541067 if len(args) == 2:
[email protected]c8f3cf82010-09-09 20:00:121068 if not os.path.isfile(args[1]):
1069 ErrorExit('The change "%s" doesn\'t exist.' % args[1])
[email protected]35fe9ad2010-05-25 23:59:541070 f = open(args[1], 'rU')
[email protected]9ce98222009-10-19 20:24:171071 override_description = f.read()
1072 f.close()
1073 else:
1074 override_description = None
[email protected]5aeb7dd2009-11-17 18:09:011075
[email protected]ea452b32009-11-22 20:04:311076 if change_info.issue and not change_info.NeedsUpload():
[email protected]fb2b8eb2009-04-23 21:03:421077 try:
[email protected]bf1fdca2010-11-01 18:05:361078 description = change_info.GetIssueDescription()
[email protected]fb2b8eb2009-04-23 21:03:421079 except urllib2.HTTPError, err:
1080 if err.code == 404:
1081 # The user deleted the issue in Rietveld, so forget the old issue id.
1082 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:101083 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:421084 change_info.Save()
1085 else:
1086 ErrorExit("Error getting the description from Rietveld: " + err)
1087 else:
[email protected]85532fc2009-06-04 22:36:531088 if override_description:
1089 description = override_description
1090 else:
1091 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:421092
1093 other_files = GetFilesNotInCL()
[email protected]bfd09ce2009-08-05 21:17:231094
[email protected]f0dfba32009-08-07 22:03:371095 # Edited files (as opposed to files with only changed properties) will have
1096 # a letter for the first character in the status string.
[email protected]85532fc2009-06-04 22:36:531097 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
[email protected]f0dfba32009-08-07 22:03:371098 affected_files = [x for x in other_files if file_re.match(x[0])]
1099 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
[email protected]fb2b8eb2009-04-23 21:03:421100
[email protected]20254fc2011-03-22 18:28:591101 description = description.rstrip() + '\n'
[email protected]30becc22011-03-17 01:21:221102
[email protected]fb2b8eb2009-04-23 21:03:421103 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:241104 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:421105 "---Paths in this changelist (" + change_info.name + "):\n")
1106 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
[email protected]f1c2f432011-09-19 23:29:081107
1108 description_to_write = description
1109 if sys.platform == 'win32':
1110 description_to_write = description.replace('\n', '\r\n')
1111
1112 text = (description_to_write + separator1 + '\n' +
[email protected]20254fc2011-03-22 18:28:591113 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
[email protected]f0dfba32009-08-07 22:03:371114
1115 if change_info.Exists():
[email protected]20254fc2011-03-22 18:28:591116 text += (separator2 +
1117 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
[email protected]f0dfba32009-08-07 22:03:371118 else:
[email protected]20254fc2011-03-22 18:28:591119 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
1120 separator2)
1121 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
[email protected]fb2b8eb2009-04-23 21:03:421122
[email protected]20254fc2011-03-22 18:28:591123 handle, filename = tempfile.mkstemp(text=True)
1124 os.write(handle, text)
1125 os.close(handle)
[email protected]fb2b8eb2009-04-23 21:03:421126
[email protected]20254fc2011-03-22 18:28:591127 # Open up the default editor in the system to get the CL description.
1128 try:
1129 if not silent:
1130 cmd = '%s %s' % (GetEditor(), filename)
1131 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1132 # Msysgit requires the usage of 'env' to be present.
1133 cmd = 'env ' + cmd
[email protected]2a471072011-05-10 17:29:231134 try:
1135 # shell=True to allow the shell to handle all forms of quotes in
1136 # $EDITOR.
[email protected]e13e12a2011-09-08 17:10:111137 subprocess2.check_call(cmd, shell=True)
1138 except subprocess2.CalledProcessError, e:
[email protected]2a471072011-05-10 17:29:231139 ErrorExit('Editor returned %d' % e.returncode)
[email protected]7af6c4d2011-09-26 21:04:181140 result = gclient_utils.FileRead(filename)
[email protected]20254fc2011-03-22 18:28:591141 finally:
1142 os.remove(filename)
[email protected]fb2b8eb2009-04-23 21:03:421143
1144 if not result:
[email protected]4c22d722010-05-14 19:01:221145 return 0
[email protected]fb2b8eb2009-04-23 21:03:421146
1147 split_result = result.split(separator1, 1)
1148 if len(split_result) != 2:
1149 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1150
[email protected]ea452b32009-11-22 20:04:311151 # Update the CL description if it has changed.
[email protected]fb2b8eb2009-04-23 21:03:421152 new_description = split_result[0]
[email protected]f1c2f432011-09-19 23:29:081153
1154 if sys.platform == 'win32':
1155 new_description = new_description.replace('\r\n', '\n')
1156
[email protected]fb2b8eb2009-04-23 21:03:421157 cl_files_text = split_result[1]
[email protected]20254fc2011-03-22 18:28:591158 if new_description != description or override_description:
1159 change_info.description = new_description
[email protected]ea452b32009-11-22 20:04:311160 change_info.needs_upload = True
[email protected]fb2b8eb2009-04-23 21:03:421161
1162 new_cl_files = []
1163 for line in cl_files_text.splitlines():
1164 if not len(line):
1165 continue
1166 if line.startswith("---"):
1167 break
1168 status = line[:7]
[email protected]e3608df2009-11-10 20:22:571169 filename = line[7:]
1170 new_cl_files.append((status, filename))
[email protected]bfd09ce2009-08-05 21:17:231171
[email protected]3a174252010-10-29 15:54:511172 if (not len(change_info.GetFiles()) and not change_info.issue and
[email protected]20254fc2011-03-22 18:28:591173 not len(new_description) and not new_cl_files):
[email protected]bfd09ce2009-08-05 21:17:231174 ErrorExit("Empty changelist not saved")
1175
[email protected]17f59f22009-06-12 13:27:241176 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421177 change_info.Save()
[email protected]53bcf152009-11-13 21:04:101178 if svn_info.get('URL', '').startswith('http:'):
1179 Warn("WARNING: Creating CL in a read-only checkout. You will not be "
1180 "able to commit it!")
1181
[email protected]fb2b8eb2009-04-23 21:03:421182 print change_info.name + " changelist saved."
1183 if change_info.MissingTests():
1184 Warn("WARNING: " + MISSING_TEST_MSG)
1185
[email protected]ea452b32009-11-22 20:04:311186 # Update the Rietveld issue.
1187 if change_info.issue and change_info.NeedsUpload():
1188 change_info.UpdateRietveldDescription()
1189 change_info.needs_upload = False
1190 change_info.Save()
[email protected]4c22d722010-05-14 19:01:221191 return 0
[email protected]ea452b32009-11-22 20:04:311192
1193
[email protected]62fd6932010-05-27 13:13:231194@need_change_and_args
1195def CMDlint(change_info, args):
1196 """Runs cpplint.py on all the files in the change list.
1197
1198 Checks all the files in the changelist for possible style violations.
1199 """
[email protected]fb2b8eb2009-04-23 21:03:421200 try:
1201 import cpplint
[email protected]8b8d8be2011-09-08 15:34:451202 import cpplint_chromium
[email protected]fb2b8eb2009-04-23 21:03:421203 except ImportError:
1204 ErrorExit("You need to install cpplint.py to lint C++ files.")
[email protected]fb2b8eb2009-04-23 21:03:421205 # Change the current working directory before calling lint so that it
1206 # shows the correct base.
1207 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241208 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421209 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241210 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421211
[email protected]bb816382009-10-29 01:38:021212 white_list = GetCodeReviewSetting("LINT_REGEX")
1213 if not white_list:
[email protected]e72bb632009-10-29 20:15:481214 white_list = DEFAULT_LINT_REGEX
[email protected]bb816382009-10-29 01:38:021215 white_regex = re.compile(white_list)
1216 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1217 if not black_list:
[email protected]e72bb632009-10-29 20:15:481218 black_list = DEFAULT_LINT_IGNORE_REGEX
[email protected]bb816382009-10-29 01:38:021219 black_regex = re.compile(black_list)
[email protected]8b8d8be2011-09-08 15:34:451220 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
[email protected]b17b55b2010-11-03 14:42:371221 # Access to a protected member _XX of a client class
1222 # pylint: disable=W0212
[email protected]e3608df2009-11-10 20:22:571223 for filename in filenames:
1224 if white_regex.match(filename):
1225 if black_regex.match(filename):
1226 print "Ignoring file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421227 else:
[email protected]8b8d8be2011-09-08 15:34:451228 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1229 extra_check_functions)
[email protected]bb816382009-10-29 01:38:021230 else:
[email protected]e3608df2009-11-10 20:22:571231 print "Skipping file %s" % filename
[email protected]fb2b8eb2009-04-23 21:03:421232
1233 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1234 os.chdir(previous_cwd)
[email protected]4c22d722010-05-14 19:01:221235 return 1
[email protected]fb2b8eb2009-04-23 21:03:421236
1237
[email protected]b0dfd352009-06-10 14:12:541238def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421239 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
[email protected]0ff1fab2009-05-22 13:08:151240 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411241 change = presubmit_support.SvnChange(change_info.name,
1242 change_info.description,
1243 change_info.GetLocalRoot(),
1244 change_info.GetFiles(),
1245 change_info.issue,
[email protected]58407af2011-04-12 23:15:571246 change_info.patchset,
1247 None)
[email protected]cab38e92011-04-09 00:30:511248 output = presubmit_support.DoPresubmitChecks(
1249 change=change,
1250 committing=committing,
1251 verbose=False,
1252 output_stream=sys.stdout,
1253 input_stream=sys.stdin,
1254 default_presubmit=root_presubmit,
1255 may_prompt=may_prompt,
[email protected]239f4112011-06-03 20:08:231256 rietveld_obj=change_info.RpcServer())
[email protected]5ac21012011-03-16 02:58:251257 if not output.should_continue() and may_prompt:
1258 # TODO(dpranke): move into DoPresubmitChecks(), unify cmd line args.
[email protected]fb2b8eb2009-04-23 21:03:421259 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
[email protected]5ac21012011-03-16 02:58:251260
[email protected]30becc22011-03-17 01:21:221261 return output
[email protected]fb2b8eb2009-04-23 21:03:421262
1263
[email protected]62fd6932010-05-27 13:13:231264@no_args
1265def CMDchanges():
[email protected]4c22d722010-05-14 19:01:221266 """Lists all the changelists and their files."""
[email protected]fb2b8eb2009-04-23 21:03:421267 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081268 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421269 print "\n--- Changelist " + change_info.name + ":"
[email protected]e3608df2009-11-10 20:22:571270 for filename in change_info.GetFiles():
1271 print "".join(filename)
[email protected]4c22d722010-05-14 19:01:221272 return 0
[email protected]fb2b8eb2009-04-23 21:03:421273
1274
[email protected]62fd6932010-05-27 13:13:231275@no_args
1276def CMDdeleteempties():
[email protected]bfd09ce2009-08-05 21:17:231277 """Delete all changelists that have no files."""
1278 print "\n--- Deleting:"
1279 for cl in GetCLs():
1280 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]3a174252010-10-29 15:54:511281 if not len(change_info.GetFiles()):
[email protected]bfd09ce2009-08-05 21:17:231282 print change_info.name
1283 change_info.Delete()
[email protected]4c22d722010-05-14 19:01:221284 return 0
1285
1286
[email protected]62fd6932010-05-27 13:13:231287@no_args
1288def CMDnothave():
[email protected]4c22d722010-05-14 19:01:221289 """Lists files unknown to Subversion."""
[email protected]62fd6932010-05-27 13:13:231290 for filename in UnknownFiles():
[email protected]4c22d722010-05-14 19:01:221291 print "? " + "".join(filename)
1292 return 0
1293
1294
[email protected]62fd6932010-05-27 13:13:231295@attrs(usage='<svn options>')
[email protected]35fe9ad2010-05-25 23:59:541296def CMDdiff(args):
[email protected]62fd6932010-05-27 13:13:231297 """Diffs all files in the changelist or all files that aren't in a CL."""
[email protected]35fe9ad2010-05-25 23:59:541298 files = None
1299 if args:
[email protected]62fd6932010-05-27 13:13:231300 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
[email protected]35fe9ad2010-05-25 23:59:541301 files = change_info.GetFileNames()
1302 else:
[email protected]707c1482010-06-02 19:52:421303 files = [f[1] for f in GetFilesNotInCL()]
[email protected]38729702010-06-01 23:42:031304
1305 root = GetRepositoryRoot()
1306 cmd = ['svn', 'diff']
1307 cmd.extend([os.path.join(root, x) for x in files])
1308 cmd.extend(args)
1309 return RunShellWithReturnCode(cmd, print_output=True)[1]
[email protected]4c22d722010-05-14 19:01:221310
1311
[email protected]62fd6932010-05-27 13:13:231312@no_args
1313def CMDsettings():
1314 """Prints code review settings for this checkout."""
[email protected]4c22d722010-05-14 19:01:221315 # Force load settings
[email protected]6e29d572010-06-04 17:32:201316 GetCodeReviewSetting("UNKNOWN")
[email protected]4c22d722010-05-14 19:01:221317 del CODEREVIEW_SETTINGS['__just_initialized']
1318 print '\n'.join(("%s: %s" % (str(k), str(v))
1319 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
1320 return 0
1321
1322
[email protected]35fe9ad2010-05-25 23:59:541323@need_change
1324def CMDdescription(change_info):
[email protected]4c22d722010-05-14 19:01:221325 """Prints the description of the specified change to stdout."""
[email protected]4c22d722010-05-14 19:01:221326 print change_info.description
1327 return 0
1328
1329
[email protected]79b7ef02010-11-01 13:25:131330def CMDdelete(args):
[email protected]4c22d722010-05-14 19:01:221331 """Deletes a changelist."""
[email protected]79b7ef02010-11-01 13:25:131332 if not len(args) == 1:
1333 ErrorExit('You need to pass a change list name')
[email protected]a2d7edf2011-04-04 17:53:451334 filepath = GetChangelistInfoFile(args[0])
1335 if not os.path.isfile(filepath):
1336 ErrorExit('You need to pass a valid change list name')
1337 os.remove(filepath)
[email protected]4c22d722010-05-14 19:01:221338 return 0
1339
1340
[email protected]35fe9ad2010-05-25 23:59:541341def CMDtry(args):
[email protected]62fd6932010-05-27 13:13:231342 """Sends the change to the tryserver to do a test run on your code.
[email protected]4c22d722010-05-14 19:01:221343
1344 To send multiple changes as one path, use a comma-separated list of
1345 changenames. Use 'gcl help try' for more information!"""
1346 # When the change contains no file, send the "changename" positional
1347 # argument to trychange.py.
[email protected]35fe9ad2010-05-25 23:59:541348 # When the command is 'try' and --patchset is used, the patch to try
1349 # is on the Rietveld server.
1350 if not args:
1351 ErrorExit("You need to pass a change list name")
1352 if args[0].find(',') != -1:
1353 change_info = LoadChangelistInfoForMultiple(args[0], GetRepositoryRoot(),
1354 True, True)
1355 else:
1356 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(),
1357 False, True)
[email protected]4c22d722010-05-14 19:01:221358 if change_info.GetFiles():
[email protected]35fe9ad2010-05-25 23:59:541359 args = args[1:]
[email protected]4c22d722010-05-14 19:01:221360 else:
1361 change_info = None
[email protected]35fe9ad2010-05-25 23:59:541362 return TryChange(change_info, args, swallow_exception=False)
[email protected]4c22d722010-05-14 19:01:221363
1364
[email protected]62fd6932010-05-27 13:13:231365@attrs(usage='<old-name> <new-name>')
[email protected]35fe9ad2010-05-25 23:59:541366def CMDrename(args):
[email protected]4c22d722010-05-14 19:01:221367 """Renames an existing change."""
[email protected]35fe9ad2010-05-25 23:59:541368 if len(args) != 2:
[email protected]4c22d722010-05-14 19:01:221369 ErrorExit("Usage: gcl rename <old-name> <new-name>.")
[email protected]35fe9ad2010-05-25 23:59:541370 src, dst = args
[email protected]4c22d722010-05-14 19:01:221371 src_file = GetChangelistInfoFile(src)
1372 if not os.path.isfile(src_file):
1373 ErrorExit("Change '%s' does not exist." % src)
1374 dst_file = GetChangelistInfoFile(dst)
1375 if os.path.isfile(dst_file):
1376 ErrorExit("Change '%s' already exists; pick a new name." % dst)
1377 os.rename(src_file, dst_file)
1378 print "Change '%s' renamed '%s'." % (src, dst)
1379 return 0
[email protected]bfd09ce2009-08-05 21:17:231380
1381
[email protected]35fe9ad2010-05-25 23:59:541382def CMDpassthru(args):
[email protected]62fd6932010-05-27 13:13:231383 """Everything else that is passed into gcl we redirect to svn.
1384
1385 It assumes a change list name is passed and is converted with the files names.
1386 """
[email protected]35fe9ad2010-05-25 23:59:541387 args = ["svn", args[0]]
1388 if len(args) > 1:
1389 root = GetRepositoryRoot()
1390 change_info = ChangeInfo.Load(args[1], root, True, True)
1391 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
1392 return RunShellWithReturnCode(args, print_output=True)[1]
1393
1394
[email protected]62fd6932010-05-27 13:13:231395def Command(name):
1396 return getattr(sys.modules[__name__], 'CMD' + name, None)
1397
1398
1399def GenUsage(command):
1400 """Modify an OptParse object with the function's documentation."""
1401 obj = Command(command)
1402 display = command
1403 more = getattr(obj, 'usage', '')
1404 if command == 'help':
1405 display = '<command>'
[email protected]3a174252010-10-29 15:54:511406 need_change_val = ''
[email protected]62fd6932010-05-27 13:13:231407 if getattr(obj, 'need_change', None):
[email protected]3a174252010-10-29 15:54:511408 need_change_val = ' <change_list>'
[email protected]62fd6932010-05-27 13:13:231409 options = ' [options]'
1410 if getattr(obj, 'no_args', None):
1411 options = ''
[email protected]3a174252010-10-29 15:54:511412 res = 'Usage: gcl %s%s%s %s\n\n' % (display, need_change_val, options, more)
[email protected]62fd6932010-05-27 13:13:231413 res += re.sub('\n ', '\n', obj.__doc__)
1414 return res
1415
1416
1417def CMDhelp(args):
1418 """Prints this help or help for the given command."""
1419 if args and 'CMD' + args[0] in dir(sys.modules[__name__]):
1420 print GenUsage(args[0])
1421
1422 # These commands defer to external tools so give this info too.
1423 if args[0] == 'try':
1424 TryChange(None, ['--help'], swallow_exception=False)
1425 if args[0] == 'upload':
1426 upload.RealMain(['upload.py', '--help'])
1427 return 0
1428
1429 print GenUsage('help')
1430 print sys.modules[__name__].__doc__
1431 print 'version ' + __version__ + '\n'
1432
1433 print('Commands are:\n' + '\n'.join([
1434 ' %-12s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1435 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1436 return 0
1437
1438
[email protected]35fe9ad2010-05-25 23:59:541439def main(argv):
[email protected]c3a15a22010-11-20 03:12:271440 if sys.hexversion < 0x02050000:
1441 print >> sys.stderr, (
1442 '\nYour python version is unsupported, please upgrade.\n')
[email protected]c68f9cb2010-06-17 20:34:181443 if not argv:
1444 argv = ['help']
1445 command = Command(argv[0])
1446 # Help can be run from anywhere.
1447 if command == CMDhelp:
1448 return command(argv[1:])
1449
[email protected]a05be0b2009-06-30 19:13:021450 try:
[email protected]62fd6932010-05-27 13:13:231451 GetRepositoryRoot()
[email protected]31cb48a2011-04-04 18:01:361452 except (gclient_utils.Error, subprocess2.CalledProcessError):
[email protected]58c19382010-09-22 19:53:591453 print >> sys.stderr, 'To use gcl, you need to be in a subversion checkout.'
[email protected]62fd6932010-05-27 13:13:231454 return 1
1455
1456 # Create the directories where we store information about changelists if it
1457 # doesn't exist.
[email protected]807c4462010-07-10 00:45:281458 try:
1459 if not os.path.exists(GetInfoDir()):
1460 os.mkdir(GetInfoDir())
1461 if not os.path.exists(GetChangesDir()):
1462 os.mkdir(GetChangesDir())
1463 if not os.path.exists(GetCacheDir()):
1464 os.mkdir(GetCacheDir())
[email protected]fb2b8eb2009-04-23 21:03:421465
[email protected]807c4462010-07-10 00:45:281466 if command:
1467 return command(argv[1:])
1468 # Unknown command, try to pass that to svn
1469 return CMDpassthru(argv)
[email protected]31cb48a2011-04-04 18:01:361470 except (gclient_utils.Error, subprocess2.CalledProcessError), e:
[email protected]58c19382010-09-22 19:53:591471 print >> sys.stderr, 'Got an exception'
1472 print >> sys.stderr, str(e)
1473 return 1
[email protected]42c7f662010-10-06 23:52:461474 except upload.ClientLoginError, e:
1475 print >> sys.stderr, 'Got an exception logging in to Rietveld'
1476 print >> sys.stderr, str(e)
[email protected]58c19382010-09-22 19:53:591477 except urllib2.HTTPError, e:
1478 if e.code != 500:
1479 raise
1480 print >> sys.stderr, (
1481 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
[email protected]7633e472010-09-22 20:03:141482 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))
[email protected]58c19382010-09-22 19:53:591483 return 1
1484
[email protected]fb2b8eb2009-04-23 21:03:421485
1486if __name__ == "__main__":
[email protected]35625c72011-03-23 17:34:021487 fix_encoding.fix_encoding()
[email protected]35fe9ad2010-05-25 23:59:541488 sys.exit(main(sys.argv[1:]))