blob: 09f6b7b3b2a1171835162357ac862906b1db7b51 [file] [log] [blame]
[email protected]fb2b8eb2009-04-23 21:03:421#!/usr/bin/python
2# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5#
6# Wrapper script around Rietveld's upload.py that groups files into
7# changelists.
8
9import getpass
10import os
11import random
12import re
[email protected]374d65e2009-05-21 14:00:5213import shutil
[email protected]fb2b8eb2009-04-23 21:03:4214import string
15import subprocess
16import sys
17import tempfile
18import upload
19import urllib2
[email protected]207fdf32009-04-28 19:57:0120import xml.dom.minidom
[email protected]fb2b8eb2009-04-23 21:03:4221
[email protected]46a94102009-05-12 20:32:4322# gcl now depends on gclient.
23import gclient
[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]46a94102009-05-12 20:32:4353 info = gclient.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]4810a962009-05-12 21:03:3485 return [item[1] for item in gclient.CaptureSVNStatus(extra_args)
86 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]46a94102009-05-12 20:32:4396 infos = gclient.CaptureSVNInfo(os.getcwd(), print_error=False)
97 cur_dir_repo_root = infos.get("Repository Root")
[email protected]fb2b8eb2009-04-23 21:03:4298 if not cur_dir_repo_root:
99 raise Exception("gcl run outside of repository")
100
[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]8c3ccf32009-05-20 18:28:37104 if (gclient.CaptureSVNInfo(parent, print_error=False).get(
105 "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
134 use_root=True and use_root=False on the same file. Don't be stupid.
135 """
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]98fc2b92009-05-21 14:11:51141 cached_file = os.path.join(GetCacheDir(), filename)
142 if (not os.path.exists(cached_file) or
143 os.stat(cached_file).st_mtime > max_age):
[email protected]46a94102009-05-12 20:32:43144 dir_info = gclient.CaptureSVNInfo(".")
[email protected]9b613272009-04-24 01:28:28145 repo_root = dir_info["Repository Root"]
[email protected]98fc2b92009-05-21 14:11:51146 if use_root:
147 url_path = repo_root
148 else:
149 url_path = dir_info["URL"]
150 content = ""
[email protected]9b613272009-04-24 01:28:28151 while True:
152 # Look for the codereview.settings file at the current level.
[email protected]98fc2b92009-05-21 14:11:51153 svn_path = url_path + "/" + filename
154 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
[email protected]9b613272009-04-24 01:28:28155 if not rc:
[email protected]98fc2b92009-05-21 14:11:51156 # Exit the loop if the file was found. Override content.
[email protected]9b613272009-04-24 01:28:28157 break
158 # Make sure to mark settings as empty if not found.
[email protected]98fc2b92009-05-21 14:11:51159 content = ""
[email protected]9b613272009-04-24 01:28:28160 if url_path == repo_root:
161 # Reached the root. Abandoning search.
[email protected]98fc2b92009-05-21 14:11:51162 break
[email protected]9b613272009-04-24 01:28:28163 # Go up one level to try again.
164 url_path = os.path.dirname(url_path)
[email protected]9b613272009-04-24 01:28:28165 # Write a cached version even if there isn't a file, so we don't try to
166 # fetch it each time.
[email protected]98fc2b92009-05-21 14:11:51167 WriteFile(cached_file, content)
168 else:
169 content = ReadFile(cached_settings_file)
170 # Keep the content cached in memory.
171 FILES_CACHE[filename] = content
172 return FILES_CACHE[filename]
[email protected]9b613272009-04-24 01:28:28173
[email protected]98fc2b92009-05-21 14:11:51174
175def GetCodeReviewSetting(key):
176 """Returns a value for the given key for this repository."""
177 # Use '__just_initialized' as a flag to determine if the settings were
178 # already initialized.
179 global CODEREVIEW_SETTINGS
180 if '__just_initialized' not in CODEREVIEW_SETTINGS:
[email protected]b0442182009-06-05 14:20:47181 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
182 if settings_file:
183 for line in settings_file.splitlines():
184 if not line or line.startswith("#"):
185 continue
186 k, v = line.split(": ", 1)
187 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51188 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42189 return CODEREVIEW_SETTINGS.get(key, "")
190
191
[email protected]fb2b8eb2009-04-23 21:03:42192def Warn(msg):
193 ErrorExit(msg, exit=False)
194
195
196def ErrorExit(msg, exit=True):
197 """Print an error message to stderr and optionally exit."""
198 print >>sys.stderr, msg
199 if exit:
200 sys.exit(1)
201
202
203def RunShellWithReturnCode(command, print_output=False):
204 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02205 # Use a shell for subcommands on Windows to get a PATH search, and because svn
206 # may be a batch file.
207 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42208 p = subprocess.Popen(command, stdout=subprocess.PIPE,
209 stderr=subprocess.STDOUT, shell=use_shell,
210 universal_newlines=True)
211 if print_output:
212 output_array = []
213 while True:
214 line = p.stdout.readline()
215 if not line:
216 break
217 if print_output:
218 print line.strip('\n')
219 output_array.append(line)
220 output = "".join(output_array)
221 else:
222 output = p.stdout.read()
223 p.wait()
224 p.stdout.close()
225 return output, p.returncode
226
227
228def RunShell(command, print_output=False):
229 """Executes a command and returns the output."""
230 return RunShellWithReturnCode(command, print_output)[0]
231
232
[email protected]c1675e22009-04-27 20:30:48233def ReadFile(filename, flags='r'):
[email protected]fb2b8eb2009-04-23 21:03:42234 """Returns the contents of a file."""
[email protected]c1675e22009-04-27 20:30:48235 file = open(filename, flags)
[email protected]fb2b8eb2009-04-23 21:03:42236 result = file.read()
237 file.close()
238 return result
239
240
241def WriteFile(filename, contents):
242 """Overwrites the file with the given contents."""
243 file = open(filename, 'w')
244 file.write(contents)
245 file.close()
246
247
[email protected]51ee0072009-06-08 19:20:05248def FilterFlag(args, flag):
249 """Returns True if the flag is present in args list.
250
251 The flag is removed from args if present.
252 """
253 if flag in args:
254 args.remove(flag)
255 return True
256 return False
257
258
[email protected]be0d1ca2009-05-12 19:23:02259class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42260 """Holds information about a changelist.
261
[email protected]32ba2602009-06-06 18:44:48262 name: change name.
263 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
264 patchset: the Rietveld latest patchset number or 0.
[email protected]fb2b8eb2009-04-23 21:03:42265 description: the description.
266 files: a list of 2 tuple containing (status, filename) of changed files,
267 with paths being relative to the top repository directory.
[email protected]8d5c9a52009-06-12 15:59:08268 local_root: Local root directory
[email protected]fb2b8eb2009-04-23 21:03:42269 """
[email protected]32ba2602009-06-06 18:44:48270
271 _SEPARATOR = "\n-----\n"
272 # The info files have the following format:
273 # issue_id, patchset\n (, patchset is optional)
274 # _SEPARATOR\n
275 # filepath1\n
276 # filepath2\n
277 # .
278 # .
279 # filepathn\n
280 # _SEPARATOR\n
281 # description
282
[email protected]8d5c9a52009-06-12 15:59:08283 def __init__(self, name, issue, patchset, description, files, local_root):
[email protected]fb2b8eb2009-04-23 21:03:42284 self.name = name
[email protected]32ba2602009-06-06 18:44:48285 self.issue = int(issue)
286 self.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42287 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02288 if files is None:
289 files = []
[email protected]17f59f22009-06-12 13:27:24290 self._files = files
[email protected]fb2b8eb2009-04-23 21:03:42291 self.patch = None
[email protected]8d5c9a52009-06-12 15:59:08292 self._local_root = local_root
[email protected]fb2b8eb2009-04-23 21:03:42293
[email protected]17f59f22009-06-12 13:27:24294 def GetFileNames(self):
295 """Returns the list of file names included in this change."""
296 return [file[1] for file in self._files]
297
298 def GetFiles(self):
299 """Returns the list of files included in this change with their status."""
300 return self._files
301
302 def GetLocalRoot(self):
303 """Returns the local repository checkout root directory."""
304 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42305
306 def _NonDeletedFileList(self):
307 """Returns a list of files in this change, not including deleted files."""
[email protected]17f59f22009-06-12 13:27:24308 return [file[1] for file in self.GetFiles()
309 if not file[0].startswith("D")]
[email protected]fb2b8eb2009-04-23 21:03:42310
311 def _AddedFileList(self):
312 """Returns a list of files added in this change."""
[email protected]17f59f22009-06-12 13:27:24313 return [file[1] for file in self.GetFiles() if file[0].startswith("A")]
[email protected]fb2b8eb2009-04-23 21:03:42314
315 def Save(self):
316 """Writes the changelist information to disk."""
[email protected]32ba2602009-06-06 18:44:48317 data = ChangeInfo._SEPARATOR.join([
318 "%d, %d" % (self.issue, self.patchset),
[email protected]17f59f22009-06-12 13:27:24319 "\n".join([f[0] + f[1] for f in self.GetFiles()]),
[email protected]32ba2602009-06-06 18:44:48320 self.description])
[email protected]fb2b8eb2009-04-23 21:03:42321 WriteFile(GetChangelistInfoFile(self.name), data)
322
323 def Delete(self):
324 """Removes the changelist information from disk."""
325 os.remove(GetChangelistInfoFile(self.name))
326
327 def CloseIssue(self):
328 """Closes the Rietveld issue for this changelist."""
329 data = [("description", self.description),]
330 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10331 SendToRietveld("/%d/close" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42332
333 def UpdateRietveldDescription(self):
334 """Sets the description for an issue on Rietveld."""
335 data = [("description", self.description),]
336 ctype, body = upload.EncodeMultipartFormData(data, [])
[email protected]2c8d4b22009-06-06 21:03:10337 SendToRietveld("/%d/description" % self.issue, body, ctype)
[email protected]fb2b8eb2009-04-23 21:03:42338
339 def MissingTests(self):
340 """Returns True if the change looks like it needs unit tests but has none.
341
342 A change needs unit tests if it contains any new source files or methods.
343 """
344 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
345 # Ignore third_party entirely.
346 files = [file for file in self._NonDeletedFileList()
347 if file.find("third_party") == -1]
348 added_files = [file for file in self._AddedFileList()
349 if file.find("third_party") == -1]
350
351 # If the change is entirely in third_party, we're done.
352 if len(files) == 0:
353 return False
354
355 # Any new or modified test files?
356 # A test file's name ends with "test.*" or "tests.*".
357 test_files = [test for test in files
358 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
359 if len(test_files) > 0:
360 return False
361
362 # Any new source files?
363 source_files = [file for file in added_files
364 if os.path.splitext(file)[1] in SOURCE_SUFFIXES]
365 if len(source_files) > 0:
366 return True
367
368 # Do the long test, checking the files for new methods.
369 return self._HasNewMethod()
370
371 def _HasNewMethod(self):
372 """Returns True if the changeset contains any new functions, or if a
373 function signature has been changed.
374
375 A function is identified by starting flush left, containing a "(" before
376 the next flush-left line, and either ending with "{" before the next
377 flush-left line or being followed by an unindented "{".
378
379 Currently this returns True for new methods, new static functions, and
380 methods or functions whose signatures have been changed.
381
382 Inline methods added to header files won't be detected by this. That's
383 acceptable for purposes of determining if a unit test is needed, since
384 inline methods should be trivial.
385 """
386 # To check for methods added to source or header files, we need the diffs.
387 # We'll generate them all, since there aren't likely to be many files
388 # apart from source and headers; besides, we'll want them all if we're
389 # uploading anyway.
390 if self.patch is None:
[email protected]17f59f22009-06-12 13:27:24391 self.patch = GenerateDiff(self.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42392
393 definition = ""
394 for line in self.patch.splitlines():
395 if not line.startswith("+"):
396 continue
397 line = line.strip("+").rstrip(" \t")
398 # Skip empty lines, comments, and preprocessor directives.
399 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
400 if line == "" or line.startswith("/") or line.startswith("#"):
401 continue
402
403 # A possible definition ending with "{" is complete, so check it.
404 if definition.endswith("{"):
405 if definition.find("(") != -1:
406 return True
407 definition = ""
408
409 # A { or an indented line, when we're in a definition, continues it.
410 if (definition != "" and
411 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
412 definition += line
413
414 # A flush-left line starts a new possible function definition.
415 elif not line.startswith(" ") and not line.startswith("\t"):
416 definition = line
417
418 return False
419
[email protected]32ba2602009-06-06 18:44:48420 @staticmethod
[email protected]8d5c9a52009-06-12 15:59:08421 def Load(changename, local_root, fail_on_not_found, update_status):
[email protected]32ba2602009-06-06 18:44:48422 """Gets information about a changelist.
[email protected]fb2b8eb2009-04-23 21:03:42423
[email protected]32ba2602009-06-06 18:44:48424 Args:
425 fail_on_not_found: if True, this function will quit the program if the
426 changelist doesn't exist.
427 update_status: if True, the svn status will be updated for all the files
428 and unchanged files will be removed.
429
430 Returns: a ChangeInfo object.
431 """
432 info_file = GetChangelistInfoFile(changename)
433 if not os.path.exists(info_file):
434 if fail_on_not_found:
435 ErrorExit("Changelist " + changename + " not found.")
[email protected]8d5c9a52009-06-12 15:59:08436 return ChangeInfo(changename, 0, 0, '', None, local_root)
[email protected]32ba2602009-06-06 18:44:48437 split_data = ReadFile(info_file).split(ChangeInfo._SEPARATOR, 2)
438 if len(split_data) != 3:
439 ErrorExit("Changelist file %s is corrupt" % info_file)
440 items = split_data[0].split(',')
441 issue = 0
442 patchset = 0
443 if items[0]:
444 issue = int(items[0])
445 if len(items) > 1:
446 patchset = int(items[1])
447 files = []
448 for line in split_data[1].splitlines():
449 status = line[:7]
450 file = line[7:]
451 files.append((status, file))
452 description = split_data[2]
453 save = False
454 if update_status:
455 for file in files:
[email protected]8d5c9a52009-06-12 15:59:08456 filename = os.path.join(local_root, file[1])
[email protected]32ba2602009-06-06 18:44:48457 status_result = gclient.CaptureSVNStatus(filename)
458 if not status_result or not status_result[0][0]:
459 # File has been reverted.
460 save = True
461 files.remove(file)
462 continue
463 status = status_result[0][0]
464 if status != file[0]:
465 save = True
466 files[files.index(file)] = (status, file[1])
[email protected]8d5c9a52009-06-12 15:59:08467 change_info = ChangeInfo(changename, issue, patchset, description, files,
468 local_root)
[email protected]32ba2602009-06-06 18:44:48469 if save:
470 change_info.Save()
471 return change_info
[email protected]fb2b8eb2009-04-23 21:03:42472
473
474def GetChangelistInfoFile(changename):
475 """Returns the file that stores information about a changelist."""
476 if not changename or re.search(r'[^\w-]', changename):
477 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52478 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42479
480
[email protected]8d5c9a52009-06-12 15:59:08481def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
482 update_status):
[email protected]fb2b8eb2009-04-23 21:03:42483 """Loads many changes and merge their files list into one pseudo change.
484
485 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
486 """
487 changes = changenames.split(',')
[email protected]8d5c9a52009-06-12 15:59:08488 aggregate_change_info = ChangeInfo(changenames, 0, 0, '', None, local_root)
[email protected]fb2b8eb2009-04-23 21:03:42489 for change in changes:
[email protected]8d5c9a52009-06-12 15:59:08490 aggregate_change_info._files += ChangeInfo.Load(change,
491 local_root,
492 fail_on_not_found,
[email protected]17f59f22009-06-12 13:27:24493 update_status).GetFiles()
[email protected]fb2b8eb2009-04-23 21:03:42494 return aggregate_change_info
495
496
[email protected]fb2b8eb2009-04-23 21:03:42497def GetCLs():
498 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52499 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42500 if CODEREVIEW_SETTINGS_FILE in cls:
501 cls.remove(CODEREVIEW_SETTINGS_FILE)
502 return cls
503
504
505def GenerateChangeName():
506 """Generate a random changelist name."""
507 random.seed()
508 current_cl_names = GetCLs()
509 while True:
510 cl_name = (random.choice(string.ascii_lowercase) +
511 random.choice(string.digits) +
512 random.choice(string.ascii_lowercase) +
513 random.choice(string.digits))
514 if cl_name not in current_cl_names:
515 return cl_name
516
517
518def GetModifiedFiles():
519 """Returns a set that maps from changelist name to (status,filename) tuples.
520
521 Files not in a changelist have an empty changelist name. Filenames are in
522 relation to the top level directory of the current repository. Note that
523 only the current directory and subdirectories are scanned, in order to
524 improve performance while still being flexible.
525 """
526 files = {}
527
528 # Since the files are normalized to the root folder of the repositary, figure
529 # out what we need to add to the paths.
530 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
531
532 # Get a list of all files in changelists.
533 files_in_cl = {}
534 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:08535 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
536 fail_on_not_found=True, update_status=False)
[email protected]17f59f22009-06-12 13:27:24537 for status, filename in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42538 files_in_cl[filename] = change_info.name
539
540 # Get all the modified files.
[email protected]4810a962009-05-12 21:03:34541 status_result = gclient.CaptureSVNStatus(None)
[email protected]207fdf32009-04-28 19:57:01542 for line in status_result:
543 status = line[0]
544 filename = line[1]
545 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42546 continue
[email protected]fb2b8eb2009-04-23 21:03:42547 if dir_prefix:
548 filename = os.path.join(dir_prefix, filename)
549 change_list_name = ""
550 if filename in files_in_cl:
551 change_list_name = files_in_cl[filename]
552 files.setdefault(change_list_name, []).append((status, filename))
553
554 return files
555
556
557def GetFilesNotInCL():
558 """Returns a list of tuples (status,filename) that aren't in any changelists.
559
560 See docstring of GetModifiedFiles for information about path of files and
561 which directories are scanned.
562 """
563 modified_files = GetModifiedFiles()
564 if "" not in modified_files:
565 return []
566 return modified_files[""]
567
568
569def SendToRietveld(request_path, payload=None,
570 content_type="application/octet-stream", timeout=None):
571 """Send a POST/GET to Rietveld. Returns the response body."""
572 def GetUserCredentials():
573 """Prompts the user for a username and password."""
574 email = upload.GetEmail()
575 password = getpass.getpass("Password for %s: " % email)
576 return email, password
577
578 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
579 rpc_server = upload.HttpRpcServer(server,
580 GetUserCredentials,
581 host_override=server,
582 save_cookies=True)
583 try:
584 return rpc_server.Send(request_path, payload, content_type, timeout)
585 except urllib2.URLError, e:
586 if timeout is None:
587 ErrorExit("Error accessing url %s" % request_path)
588 else:
589 return None
590
591
592def GetIssueDescription(issue):
593 """Returns the issue description from Rietveld."""
[email protected]32ba2602009-06-06 18:44:48594 return SendToRietveld("/%d/description" % issue)
[email protected]fb2b8eb2009-04-23 21:03:42595
596
[email protected]fb2b8eb2009-04-23 21:03:42597def Opened():
598 """Prints a list of modified files in the current directory down."""
599 files = GetModifiedFiles()
600 cl_keys = files.keys()
601 cl_keys.sort()
602 for cl_name in cl_keys:
603 if cl_name:
604 note = ""
[email protected]8d5c9a52009-06-12 15:59:08605 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
606 fail_on_not_found=True, update_status=False)
607 if len(change_info.GetFiles()) != len(files[cl_name]):
[email protected]fb2b8eb2009-04-23 21:03:42608 note = " (Note: this changelist contains files outside this directory)"
609 print "\n--- Changelist " + cl_name + note + ":"
610 for file in files[cl_name]:
611 print "".join(file)
612
613
614def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53615 if argv:
616 if argv[0] == 'try':
617 TryChange(None, ['--help'], swallow_exception=False)
618 return
619 if argv[0] == 'upload':
620 upload.RealMain(['upload.py', '--help'])
621 return
[email protected]fb2b8eb2009-04-23 21:03:42622
623 print (
624"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48625version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42626
627Basic commands:
628-----------------------------------------
629 gcl change change_name
630 Add/remove files to a changelist. Only scans the current directory and
631 subdirectories.
632
633 gcl upload change_name [-r [email protected],[email protected],...]
634 [--send_mail] [--no_try] [--no_presubmit]
[email protected]b2ab4942009-06-11 21:39:19635 [--no_watchlists]
[email protected]fb2b8eb2009-04-23 21:03:42636 Uploads the changelist to the server for review.
637
[email protected]3b217f52009-06-01 17:54:20638 gcl commit change_name [--no_presubmit]
[email protected]fb2b8eb2009-04-23 21:03:42639 Commits the changelist to the repository.
640
641 gcl lint change_name
642 Check all the files in the changelist for possible style violations.
643
644Advanced commands:
645-----------------------------------------
646 gcl delete change_name
647 Deletes a changelist.
648
649 gcl diff change_name
650 Diffs all files in the changelist.
651
652 gcl presubmit change_name
653 Runs presubmit checks without uploading the changelist.
654
655 gcl diff
656 Diffs all files in the current directory and subdirectories that aren't in
657 a changelist.
658
659 gcl changes
660 Lists all the the changelists and the files in them.
661
662 gcl nothave [optional directory]
663 Lists files unknown to Subversion.
664
665 gcl opened
666 Lists modified files in the current directory and subdirectories.
667
668 gcl settings
669 Print the code review settings for this directory.
670
671 gcl status
672 Lists modified and unknown files in the current directory and
673 subdirectories.
674
675 gcl try change_name
676 Sends the change to the tryserver so a trybot can do a test run on your
677 code. To send multiple changes as one path, use a comma-separated list
678 of changenames.
679 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53680
681 gcl help [command]
682 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42683""")
684
685def GetEditor():
686 editor = os.environ.get("SVN_EDITOR")
687 if not editor:
688 editor = os.environ.get("EDITOR")
689
690 if not editor:
691 if sys.platform.startswith("win"):
692 editor = "notepad"
693 else:
694 editor = "vi"
695
696 return editor
697
698
699def GenerateDiff(files, root=None):
700 """Returns a string containing the diff for the given file list.
701
702 The files in the list should either be absolute paths or relative to the
703 given root. If no root directory is provided, the repository root will be
704 used.
705 """
706 previous_cwd = os.getcwd()
707 if root is None:
708 os.chdir(GetRepositoryRoot())
709 else:
710 os.chdir(root)
711
712 diff = []
713 for file in files:
714 # Use svn info output instead of os.path.isdir because the latter fails
715 # when the file is deleted.
[email protected]46a94102009-05-12 20:32:43716 if gclient.CaptureSVNInfo(file).get("Node Kind") in ("dir", "directory"):
[email protected]fb2b8eb2009-04-23 21:03:42717 continue
718 # If the user specified a custom diff command in their svn config file,
719 # then it'll be used when we do svn diff, which we don't want to happen
720 # since we want the unified diff. Using --diff-cmd=diff doesn't always
721 # work, since they can have another diff executable in their path that
722 # gives different line endings. So we use a bogus temp directory as the
723 # config directory, which gets around these problems.
724 if sys.platform.startswith("win"):
725 parent_dir = tempfile.gettempdir()
726 else:
727 parent_dir = sys.path[0] # tempdir is not secure.
728 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
729 if not os.path.exists(bogus_dir):
730 os.mkdir(bogus_dir)
731 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
732 if output:
733 diff.append(output)
[email protected]c3150202009-05-13 14:31:01734 elif IsSVNMoved(file):
735 # svn diff on a mv/cp'd file outputs nothing.
736 # We put in an empty Index entry so upload.py knows about them.
[email protected]fb2b8eb2009-04-23 21:03:42737 diff.append("\nIndex: %s\n" % file)
[email protected]c3150202009-05-13 14:31:01738 else:
739 # The file is not modified anymore. It should be removed from the set.
740 pass
[email protected]fb2b8eb2009-04-23 21:03:42741 os.chdir(previous_cwd)
742 return "".join(diff)
743
744
[email protected]51ee0072009-06-08 19:20:05745
746def OptionallyDoPresubmitChecks(change_info, committing, args):
747 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
748 return True
[email protected]b0dfd352009-06-10 14:12:54749 return DoPresubmitChecks(change_info, committing, True)
[email protected]51ee0072009-06-08 19:20:05750
751
[email protected]fb2b8eb2009-04-23 21:03:42752def UploadCL(change_info, args):
[email protected]17f59f22009-06-12 13:27:24753 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42754 print "Nothing to upload, changelist is empty."
755 return
[email protected]51ee0072009-06-08 19:20:05756 if not OptionallyDoPresubmitChecks(change_info, False, args):
757 return
758 no_try = FilterFlag(args, "--no_try") or FilterFlag(args, "--no-try")
[email protected]b2ab4942009-06-11 21:39:19759 no_watchlists = FilterFlag(args, "--no_watchlists") or \
760 FilterFlag(args, "--no-watchlists")
[email protected]fb2b8eb2009-04-23 21:03:42761
762 # Map --send-mail to --send_mail
[email protected]51ee0072009-06-08 19:20:05763 if FilterFlag(args, "--send-mail"):
[email protected]fb2b8eb2009-04-23 21:03:42764 args.append("--send_mail")
765
766 # Supports --clobber for the try server.
[email protected]51ee0072009-06-08 19:20:05767 clobber = FilterFlag(args, "--clobber")
[email protected]fb2b8eb2009-04-23 21:03:42768
[email protected]003c2692009-05-20 13:08:08769 # Disable try when the server is overridden.
770 server_1 = re.compile(r"^-s\b.*")
771 server_2 = re.compile(r"^--server\b.*")
772 for arg in args:
773 if server_1.match(arg) or server_2.match(arg):
774 no_try = True
775 break
[email protected]fb2b8eb2009-04-23 21:03:42776
777 upload_arg = ["upload.py", "-y"]
778 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
779 upload_arg.extend(args)
780
781 desc_file = ""
782 if change_info.issue: # Uploading a new patchset.
783 found_message = False
784 for arg in args:
785 if arg.startswith("--message") or arg.startswith("-m"):
786 found_message = True
787 break
788
789 if not found_message:
790 upload_arg.append("--message=''")
791
[email protected]32ba2602009-06-06 18:44:48792 upload_arg.append("--issue=%d" % change_info.issue)
[email protected]fb2b8eb2009-04-23 21:03:42793 else: # First time we upload.
794 handle, desc_file = tempfile.mkstemp(text=True)
795 os.write(handle, change_info.description)
796 os.close(handle)
797
[email protected]b2ab4942009-06-11 21:39:19798 # Watchlist processing -- CC people interested in this changeset
799 # http://dev.chromium.org/developers/contributing-code/watchlists
800 if not no_watchlists:
801 import watchlists
[email protected]17f59f22009-06-12 13:27:24802 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
[email protected]07f01862009-06-12 16:51:08803 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
[email protected]b2ab4942009-06-11 21:39:19804
[email protected]fb2b8eb2009-04-23 21:03:42805 cc_list = GetCodeReviewSetting("CC_LIST")
[email protected]b2ab4942009-06-11 21:39:19806 if not no_watchlists and watchers:
807 # Filter out all empty elements and join by ','
808 cc_list = ','.join(filter(None, [cc_list] + watchers))
[email protected]fb2b8eb2009-04-23 21:03:42809 if cc_list:
810 upload_arg.append("--cc=" + cc_list)
811 upload_arg.append("--description_file=" + desc_file + "")
812 if change_info.description:
813 subject = change_info.description[:77]
814 if subject.find("\r\n") != -1:
815 subject = subject[:subject.find("\r\n")]
816 if subject.find("\n") != -1:
817 subject = subject[:subject.find("\n")]
818 if len(change_info.description) > 77:
819 subject = subject + "..."
820 upload_arg.append("--message=" + subject)
821
822 # Change the current working directory before calling upload.py so that it
823 # shows the correct base.
824 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24825 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42826
827 # If we have a lot of files with long paths, then we won't be able to fit
828 # the command to "svn diff". Instead, we generate the diff manually for
829 # each file and concatenate them before passing it to upload.py.
830 if change_info.patch is None:
[email protected]17f59f22009-06-12 13:27:24831 change_info.patch = GenerateDiff(change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:42832 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
[email protected]32ba2602009-06-06 18:44:48833 if issue and patchset:
834 change_info.issue = int(issue)
835 change_info.patchset = int(patchset)
[email protected]fb2b8eb2009-04-23 21:03:42836 change_info.Save()
837
838 if desc_file:
839 os.remove(desc_file)
840
841 # Do background work on Rietveld to lint the file so that the results are
842 # ready when the issue is viewed.
843 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
844
845 # Once uploaded to Rietveld, send it to the try server.
846 if not no_try:
847 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
848 if try_on_upload and try_on_upload.lower() == 'true':
[email protected]32ba2602009-06-06 18:44:48849 trychange_args = []
[email protected]fb2b8eb2009-04-23 21:03:42850 if clobber:
[email protected]32ba2602009-06-06 18:44:48851 trychange_args.append('--clobber')
852 TryChange(change_info, trychange_args, swallow_exception=True)
[email protected]fb2b8eb2009-04-23 21:03:42853
854 os.chdir(previous_cwd)
855
856
857def PresubmitCL(change_info):
858 """Reports what presubmit checks on the change would report."""
[email protected]17f59f22009-06-12 13:27:24859 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42860 print "Nothing to presubmit check, changelist is empty."
861 return
862
863 print "*** Presubmit checks for UPLOAD would report: ***"
[email protected]b0dfd352009-06-10 14:12:54864 DoPresubmitChecks(change_info, False, False)
[email protected]fb2b8eb2009-04-23 21:03:42865
[email protected]b0dfd352009-06-10 14:12:54866 print "\n*** Presubmit checks for COMMIT would report: ***"
867 DoPresubmitChecks(change_info, True, False)
[email protected]fb2b8eb2009-04-23 21:03:42868
869
870def TryChange(change_info, args, swallow_exception):
871 """Create a diff file of change_info and send it to the try server."""
872 try:
873 import trychange
874 except ImportError:
875 if swallow_exception:
876 return
877 ErrorExit("You need to install trychange.py to use the try server.")
878
879 if change_info:
880 trychange_args = ['--name', change_info.name]
[email protected]32ba2602009-06-06 18:44:48881 if change_info.issue:
882 trychange_args.extend(["--issue", str(change_info.issue)])
883 if change_info.patchset:
884 trychange_args.extend(["--patchset", str(change_info.patchset)])
[email protected]fb2b8eb2009-04-23 21:03:42885 trychange_args.extend(args)
886 trychange.TryChange(trychange_args,
[email protected]17f59f22009-06-12 13:27:24887 file_list=change_info.GetFileNames(),
[email protected]fb2b8eb2009-04-23 21:03:42888 swallow_exception=swallow_exception,
889 prog='gcl try')
890 else:
891 trychange.TryChange(args,
892 file_list=None,
893 swallow_exception=swallow_exception,
894 prog='gcl try')
895
896
897def Commit(change_info, args):
[email protected]17f59f22009-06-12 13:27:24898 if not change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:42899 print "Nothing to commit, changelist is empty."
900 return
[email protected]51ee0072009-06-08 19:20:05901 if not OptionallyDoPresubmitChecks(change_info, True, args):
902 return
[email protected]fb2b8eb2009-04-23 21:03:42903
[email protected]1bb04aa2009-06-01 17:52:11904 # We face a problem with svn here: Let's say change 'bleh' modifies
905 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
906 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
907 # The only fix is to use --non-recursive but that has its issues too:
908 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
909 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
910 # directory with child nodes". Yay...
911 commit_cmd = ["svn", "commit"]
[email protected]fb2b8eb2009-04-23 21:03:42912 filename = ''
913 if change_info.issue:
914 # Get the latest description from Rietveld.
915 change_info.description = GetIssueDescription(change_info.issue)
916
917 commit_message = change_info.description.replace('\r\n', '\n')
918 if change_info.issue:
[email protected]32ba2602009-06-06 18:44:48919 commit_message += ('\nReview URL: http://%s/%d' %
[email protected]fb2b8eb2009-04-23 21:03:42920 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
921 change_info.issue))
922
923 handle, commit_filename = tempfile.mkstemp(text=True)
924 os.write(handle, commit_message)
925 os.close(handle)
926
927 handle, targets_filename = tempfile.mkstemp(text=True)
[email protected]17f59f22009-06-12 13:27:24928 os.write(handle, "\n".join(change_info.GetFileNames()))
[email protected]fb2b8eb2009-04-23 21:03:42929 os.close(handle)
930
931 commit_cmd += ['--file=' + commit_filename]
932 commit_cmd += ['--targets=' + targets_filename]
933 # Change the current working directory before calling commit.
934 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:24935 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:42936 output = RunShell(commit_cmd, True)
937 os.remove(commit_filename)
938 os.remove(targets_filename)
939 if output.find("Committed revision") != -1:
940 change_info.Delete()
941
942 if change_info.issue:
943 revision = re.compile(".*?\nCommitted revision (\d+)",
944 re.DOTALL).match(output).group(1)
945 viewvc_url = GetCodeReviewSetting("VIEW_VC")
946 change_info.description = change_info.description + '\n'
947 if viewvc_url:
948 change_info.description += "\nCommitted: " + viewvc_url + revision
949 change_info.CloseIssue()
950 os.chdir(previous_cwd)
951
[email protected]2c8d4b22009-06-06 21:03:10952
[email protected]85532fc2009-06-04 22:36:53953def Change(change_info, override_description):
[email protected]fb2b8eb2009-04-23 21:03:42954 """Creates/edits a changelist."""
955 if change_info.issue:
956 try:
957 description = GetIssueDescription(change_info.issue)
958 except urllib2.HTTPError, err:
959 if err.code == 404:
960 # The user deleted the issue in Rietveld, so forget the old issue id.
961 description = change_info.description
[email protected]2c8d4b22009-06-06 21:03:10962 change_info.issue = 0
[email protected]fb2b8eb2009-04-23 21:03:42963 change_info.Save()
964 else:
965 ErrorExit("Error getting the description from Rietveld: " + err)
966 else:
[email protected]85532fc2009-06-04 22:36:53967 if override_description:
968 description = override_description
969 else:
970 description = change_info.description
[email protected]fb2b8eb2009-04-23 21:03:42971
972 other_files = GetFilesNotInCL()
[email protected]85532fc2009-06-04 22:36:53973
974 #Edited files will have a letter for the first character in a string.
975 #This regex looks for the presence of that character.
976 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
977 affected_files = filter(lambda x: file_re.match(x[0]), other_files)
978 unaffected_files = filter(lambda x: not file_re.match(x[0]), other_files)
[email protected]fb2b8eb2009-04-23 21:03:42979
980 separator1 = ("\n---All lines above this line become the description.\n"
[email protected]17f59f22009-06-12 13:27:24981 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
[email protected]fb2b8eb2009-04-23 21:03:42982 "---Paths in this changelist (" + change_info.name + "):\n")
983 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
984 text = (description + separator1 + '\n' +
[email protected]17f59f22009-06-12 13:27:24985 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]) +
986 separator2 +
[email protected]85532fc2009-06-04 22:36:53987 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
988 '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n')
[email protected]fb2b8eb2009-04-23 21:03:42989
990 handle, filename = tempfile.mkstemp(text=True)
991 os.write(handle, text)
992 os.close(handle)
993
994 os.system(GetEditor() + " " + filename)
995
996 result = ReadFile(filename)
997 os.remove(filename)
998
999 if not result:
1000 return
1001
1002 split_result = result.split(separator1, 1)
1003 if len(split_result) != 2:
1004 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
1005
1006 new_description = split_result[0]
1007 cl_files_text = split_result[1]
[email protected]85532fc2009-06-04 22:36:531008 if new_description != description or override_description:
[email protected]fb2b8eb2009-04-23 21:03:421009 change_info.description = new_description
1010 if change_info.issue:
1011 # Update the Rietveld issue with the new description.
1012 change_info.UpdateRietveldDescription()
1013
1014 new_cl_files = []
1015 for line in cl_files_text.splitlines():
1016 if not len(line):
1017 continue
1018 if line.startswith("---"):
1019 break
1020 status = line[:7]
1021 file = line[7:]
1022 new_cl_files.append((status, file))
[email protected]17f59f22009-06-12 13:27:241023 change_info._files = new_cl_files
[email protected]fb2b8eb2009-04-23 21:03:421024
1025 change_info.Save()
1026 print change_info.name + " changelist saved."
1027 if change_info.MissingTests():
1028 Warn("WARNING: " + MISSING_TEST_MSG)
1029
1030# We don't lint files in these path prefixes.
1031IGNORE_PATHS = ("webkit",)
1032
1033# Valid extensions for files we want to lint.
1034CPP_EXTENSIONS = ("cpp", "cc", "h")
1035
1036def Lint(change_info, args):
1037 """Runs cpplint.py on all the files in |change_info|"""
1038 try:
1039 import cpplint
1040 except ImportError:
1041 ErrorExit("You need to install cpplint.py to lint C++ files.")
1042
1043 # Change the current working directory before calling lint so that it
1044 # shows the correct base.
1045 previous_cwd = os.getcwd()
[email protected]17f59f22009-06-12 13:27:241046 os.chdir(change_info.GetLocalRoot())
[email protected]fb2b8eb2009-04-23 21:03:421047
1048 # Process cpplints arguments if any.
[email protected]17f59f22009-06-12 13:27:241049 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
[email protected]fb2b8eb2009-04-23 21:03:421050
1051 for file in filenames:
1052 if len([file for suffix in CPP_EXTENSIONS if file.endswith(suffix)]):
1053 if len([file for prefix in IGNORE_PATHS if file.startswith(prefix)]):
1054 print "Ignoring non-Google styled file %s" % file
1055 else:
1056 cpplint.ProcessFile(file, cpplint._cpplint_state.verbose_level)
1057
1058 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1059 os.chdir(previous_cwd)
1060
1061
[email protected]b0dfd352009-06-10 14:12:541062def DoPresubmitChecks(change_info, committing, may_prompt):
[email protected]fb2b8eb2009-04-23 21:03:421063 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1064 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481065 import presubmit_support
[email protected]0ff1fab2009-05-22 13:08:151066 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
[email protected]2e501802009-06-12 22:00:411067 change = presubmit_support.SvnChange(change_info.name,
1068 change_info.description,
1069 change_info.GetLocalRoot(),
1070 change_info.GetFiles(),
1071 change_info.issue,
1072 change_info.patchset)
1073 result = presubmit_support.DoPresubmitChecks(change=change,
[email protected]b0dfd352009-06-10 14:12:541074 committing=committing,
[email protected]1033acc2009-05-13 14:36:481075 verbose=False,
1076 output_stream=sys.stdout,
[email protected]0ff1fab2009-05-22 13:08:151077 input_stream=sys.stdin,
[email protected]b0dfd352009-06-10 14:12:541078 default_presubmit=root_presubmit,
1079 may_prompt=may_prompt)
[email protected]21b893b2009-06-10 18:56:551080 if not result and may_prompt:
[email protected]fb2b8eb2009-04-23 21:03:421081 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1082 return result
1083
1084
1085def Changes():
1086 """Print all the changelists and their files."""
1087 for cl in GetCLs():
[email protected]8d5c9a52009-06-12 15:59:081088 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
[email protected]fb2b8eb2009-04-23 21:03:421089 print "\n--- Changelist " + change_info.name + ":"
[email protected]17f59f22009-06-12 13:27:241090 for file in change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421091 print "".join(file)
1092
1093
1094def main(argv=None):
1095 if argv is None:
1096 argv = sys.argv
1097
1098 if len(argv) == 1:
1099 Help()
1100 return 0;
1101
[email protected]374d65e2009-05-21 14:00:521102 # Create the directories where we store information about changelists if it
[email protected]fb2b8eb2009-04-23 21:03:421103 # doesn't exist.
1104 if not os.path.exists(GetInfoDir()):
1105 os.mkdir(GetInfoDir())
[email protected]374d65e2009-05-21 14:00:521106 if not os.path.exists(GetChangesDir()):
1107 os.mkdir(GetChangesDir())
1108 # For smooth upgrade support, move the files in GetInfoDir() to
1109 # GetChangesDir().
1110 # TODO(maruel): Remove this code in August 2009.
1111 for file in os.listdir(unicode(GetInfoDir())):
1112 file_path = os.path.join(unicode(GetInfoDir()), file)
1113 if os.path.isfile(file_path) and file != CODEREVIEW_SETTINGS_FILE:
1114 shutil.move(file_path, GetChangesDir())
[email protected]98fc2b92009-05-21 14:11:511115 if not os.path.exists(GetCacheDir()):
1116 os.mkdir(GetCacheDir())
[email protected]fb2b8eb2009-04-23 21:03:421117
1118 # Commands that don't require an argument.
1119 command = argv[1]
1120 if command == "opened":
1121 Opened()
1122 return 0
1123 if command == "status":
1124 Opened()
1125 print "\n--- Not in any changelist:"
1126 UnknownFiles([])
1127 return 0
1128 if command == "nothave":
1129 UnknownFiles(argv[2:])
1130 return 0
1131 if command == "changes":
1132 Changes()
1133 return 0
1134 if command == "help":
1135 Help(argv[2:])
1136 return 0
1137 if command == "diff" and len(argv) == 2:
1138 files = GetFilesNotInCL()
1139 print GenerateDiff([x[1] for x in files])
1140 return 0
1141 if command == "settings":
1142 ignore = GetCodeReviewSetting("UNKNOWN");
[email protected]a005ccd2009-06-12 13:25:541143 del CODEREVIEW_SETTINGS['__just_initialized']
1144 print '\n'.join(("%s: %s" % (str(k), str(v))
1145 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
[email protected]fb2b8eb2009-04-23 21:03:421146 return 0
1147
1148 if len(argv) == 2:
1149 if command == "change":
1150 # Generate a random changelist name.
1151 changename = GenerateChangeName()
1152 else:
1153 ErrorExit("Need a changelist name.")
1154 else:
1155 changename = argv[2]
1156
1157 # When the command is 'try' and --patchset is used, the patch to try
1158 # is on the Rietveld server. 'change' creates a change so it's fine if the
1159 # change didn't exist. All other commands require an existing change.
1160 fail_on_not_found = command != "try" and command != "change"
1161 if command == "try" and changename.find(',') != -1:
[email protected]8d5c9a52009-06-12 15:59:081162 change_info = LoadChangelistInfoForMultiple(changename, GetRepositoryRoot(),
1163 True, True)
[email protected]fb2b8eb2009-04-23 21:03:421164 else:
[email protected]8d5c9a52009-06-12 15:59:081165 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(),
1166 fail_on_not_found, True)
[email protected]fb2b8eb2009-04-23 21:03:421167
1168 if command == "change":
[email protected]85532fc2009-06-04 22:36:531169 if (len(argv) == 4):
1170 filename = argv[3]
1171 f = open(filename, 'rU')
1172 override_description = f.read()
1173 f.close()
1174 else:
1175 override_description = None
1176 Change(change_info, override_description)
[email protected]fb2b8eb2009-04-23 21:03:421177 elif command == "lint":
1178 Lint(change_info, argv[3:])
1179 elif command == "upload":
1180 UploadCL(change_info, argv[3:])
1181 elif command == "presubmit":
1182 PresubmitCL(change_info)
1183 elif command in ("commit", "submit"):
1184 Commit(change_info, argv[3:])
1185 elif command == "delete":
1186 change_info.Delete()
1187 elif command == "try":
1188 # When the change contains no file, send the "changename" positional
1189 # argument to trychange.py.
[email protected]17f59f22009-06-12 13:27:241190 if change_info.GetFiles():
[email protected]fb2b8eb2009-04-23 21:03:421191 args = argv[3:]
1192 else:
1193 change_info = None
1194 args = argv[2:]
1195 TryChange(change_info, args, swallow_exception=False)
1196 else:
1197 # Everything else that is passed into gcl we redirect to svn, after adding
1198 # the files. This allows commands such as 'gcl diff xxx' to work.
1199 args =["svn", command]
1200 root = GetRepositoryRoot()
[email protected]17f59f22009-06-12 13:27:241201 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
[email protected]fb2b8eb2009-04-23 21:03:421202 RunShell(args, True)
1203 return 0
1204
1205
1206if __name__ == "__main__":
1207 sys.exit(main())