blob: 87417a1ed9203ab10cef407c02de0a3e9eec6213 [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:
181 for line in GetCachedFile(CODEREVIEW_SETTINGS_FILE).splitlines():
[email protected]fb2b8eb2009-04-23 21:03:42182 if not line or line.startswith("#"):
183 continue
184 k, v = line.split(": ", 1)
185 CODEREVIEW_SETTINGS[k] = v
[email protected]98fc2b92009-05-21 14:11:51186 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
[email protected]fb2b8eb2009-04-23 21:03:42187 return CODEREVIEW_SETTINGS.get(key, "")
188
189
190def IsTreeOpen():
191 """Fetches the tree status and returns either True or False."""
192 url = GetCodeReviewSetting('STATUS')
193 status = ""
194 if url:
195 status = urllib2.urlopen(url).read()
196 return status.find('0') == -1
197
198
199def Warn(msg):
200 ErrorExit(msg, exit=False)
201
202
203def ErrorExit(msg, exit=True):
204 """Print an error message to stderr and optionally exit."""
205 print >>sys.stderr, msg
206 if exit:
207 sys.exit(1)
208
209
210def RunShellWithReturnCode(command, print_output=False):
211 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02212 # Use a shell for subcommands on Windows to get a PATH search, and because svn
213 # may be a batch file.
214 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42215 p = subprocess.Popen(command, stdout=subprocess.PIPE,
216 stderr=subprocess.STDOUT, shell=use_shell,
217 universal_newlines=True)
218 if print_output:
219 output_array = []
220 while True:
221 line = p.stdout.readline()
222 if not line:
223 break
224 if print_output:
225 print line.strip('\n')
226 output_array.append(line)
227 output = "".join(output_array)
228 else:
229 output = p.stdout.read()
230 p.wait()
231 p.stdout.close()
232 return output, p.returncode
233
234
235def RunShell(command, print_output=False):
236 """Executes a command and returns the output."""
237 return RunShellWithReturnCode(command, print_output)[0]
238
239
[email protected]c1675e22009-04-27 20:30:48240def ReadFile(filename, flags='r'):
[email protected]fb2b8eb2009-04-23 21:03:42241 """Returns the contents of a file."""
[email protected]c1675e22009-04-27 20:30:48242 file = open(filename, flags)
[email protected]fb2b8eb2009-04-23 21:03:42243 result = file.read()
244 file.close()
245 return result
246
247
248def WriteFile(filename, contents):
249 """Overwrites the file with the given contents."""
250 file = open(filename, 'w')
251 file.write(contents)
252 file.close()
253
254
[email protected]be0d1ca2009-05-12 19:23:02255class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42256 """Holds information about a changelist.
257
258 issue: the Rietveld issue number, of "" if it hasn't been uploaded yet.
259 description: the description.
260 files: a list of 2 tuple containing (status, filename) of changed files,
261 with paths being relative to the top repository directory.
262 """
[email protected]be0d1ca2009-05-12 19:23:02263 def __init__(self, name="", issue="", description="", files=None):
[email protected]fb2b8eb2009-04-23 21:03:42264 self.name = name
265 self.issue = issue
266 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02267 if files is None:
268 files = []
[email protected]fb2b8eb2009-04-23 21:03:42269 self.files = files
270 self.patch = None
271
272 def FileList(self):
273 """Returns a list of files."""
274 return [file[1] for file in self.files]
275
276 def _NonDeletedFileList(self):
277 """Returns a list of files in this change, not including deleted files."""
278 return [file[1] for file in self.files if not file[0].startswith("D")]
279
280 def _AddedFileList(self):
281 """Returns a list of files added in this change."""
282 return [file[1] for file in self.files if file[0].startswith("A")]
283
284 def Save(self):
285 """Writes the changelist information to disk."""
286 data = SEPARATOR.join([self.issue,
287 "\n".join([f[0] + f[1] for f in self.files]),
288 self.description])
289 WriteFile(GetChangelistInfoFile(self.name), data)
290
291 def Delete(self):
292 """Removes the changelist information from disk."""
293 os.remove(GetChangelistInfoFile(self.name))
294
295 def CloseIssue(self):
296 """Closes the Rietveld issue for this changelist."""
297 data = [("description", self.description),]
298 ctype, body = upload.EncodeMultipartFormData(data, [])
299 SendToRietveld("/" + self.issue + "/close", body, ctype)
300
301 def UpdateRietveldDescription(self):
302 """Sets the description for an issue on Rietveld."""
303 data = [("description", self.description),]
304 ctype, body = upload.EncodeMultipartFormData(data, [])
305 SendToRietveld("/" + self.issue + "/description", body, ctype)
306
307 def MissingTests(self):
308 """Returns True if the change looks like it needs unit tests but has none.
309
310 A change needs unit tests if it contains any new source files or methods.
311 """
312 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
313 # Ignore third_party entirely.
314 files = [file for file in self._NonDeletedFileList()
315 if file.find("third_party") == -1]
316 added_files = [file for file in self._AddedFileList()
317 if file.find("third_party") == -1]
318
319 # If the change is entirely in third_party, we're done.
320 if len(files) == 0:
321 return False
322
323 # Any new or modified test files?
324 # A test file's name ends with "test.*" or "tests.*".
325 test_files = [test for test in files
326 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
327 if len(test_files) > 0:
328 return False
329
330 # Any new source files?
331 source_files = [file for file in added_files
332 if os.path.splitext(file)[1] in SOURCE_SUFFIXES]
333 if len(source_files) > 0:
334 return True
335
336 # Do the long test, checking the files for new methods.
337 return self._HasNewMethod()
338
339 def _HasNewMethod(self):
340 """Returns True if the changeset contains any new functions, or if a
341 function signature has been changed.
342
343 A function is identified by starting flush left, containing a "(" before
344 the next flush-left line, and either ending with "{" before the next
345 flush-left line or being followed by an unindented "{".
346
347 Currently this returns True for new methods, new static functions, and
348 methods or functions whose signatures have been changed.
349
350 Inline methods added to header files won't be detected by this. That's
351 acceptable for purposes of determining if a unit test is needed, since
352 inline methods should be trivial.
353 """
354 # To check for methods added to source or header files, we need the diffs.
355 # We'll generate them all, since there aren't likely to be many files
356 # apart from source and headers; besides, we'll want them all if we're
357 # uploading anyway.
358 if self.patch is None:
359 self.patch = GenerateDiff(self.FileList())
360
361 definition = ""
362 for line in self.patch.splitlines():
363 if not line.startswith("+"):
364 continue
365 line = line.strip("+").rstrip(" \t")
366 # Skip empty lines, comments, and preprocessor directives.
367 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
368 if line == "" or line.startswith("/") or line.startswith("#"):
369 continue
370
371 # A possible definition ending with "{" is complete, so check it.
372 if definition.endswith("{"):
373 if definition.find("(") != -1:
374 return True
375 definition = ""
376
377 # A { or an indented line, when we're in a definition, continues it.
378 if (definition != "" and
379 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
380 definition += line
381
382 # A flush-left line starts a new possible function definition.
383 elif not line.startswith(" ") and not line.startswith("\t"):
384 definition = line
385
386 return False
387
388
389SEPARATOR = "\n-----\n"
390# The info files have the following format:
391# issue_id\n
392# SEPARATOR\n
393# filepath1\n
394# filepath2\n
395# .
396# .
397# filepathn\n
398# SEPARATOR\n
399# description
400
401
402def GetChangelistInfoFile(changename):
403 """Returns the file that stores information about a changelist."""
404 if not changename or re.search(r'[^\w-]', changename):
405 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52406 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42407
408
409def LoadChangelistInfoForMultiple(changenames, fail_on_not_found=True,
410 update_status=False):
411 """Loads many changes and merge their files list into one pseudo change.
412
413 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
414 """
415 changes = changenames.split(',')
416 aggregate_change_info = ChangeInfo(name=changenames)
417 for change in changes:
418 aggregate_change_info.files += LoadChangelistInfo(change,
419 fail_on_not_found,
420 update_status).files
421 return aggregate_change_info
422
423
424def LoadChangelistInfo(changename, fail_on_not_found=True,
425 update_status=False):
426 """Gets information about a changelist.
427
428 Args:
429 fail_on_not_found: if True, this function will quit the program if the
430 changelist doesn't exist.
431 update_status: if True, the svn status will be updated for all the files
432 and unchanged files will be removed.
433
434 Returns: a ChangeInfo object.
435 """
436 info_file = GetChangelistInfoFile(changename)
437 if not os.path.exists(info_file):
438 if fail_on_not_found:
439 ErrorExit("Changelist " + changename + " not found.")
440 return ChangeInfo(changename)
441 data = ReadFile(info_file)
442 split_data = data.split(SEPARATOR, 2)
443 if len(split_data) != 3:
444 os.remove(info_file)
445 ErrorExit("Changelist file %s was corrupt and deleted" % info_file)
446 issue = split_data[0]
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:
456 filename = os.path.join(GetRepositoryRoot(), file[1])
[email protected]4810a962009-05-12 21:03:34457 status_result = gclient.CaptureSVNStatus(filename)
[email protected]207fdf32009-04-28 19:57:01458 if not status_result or not status_result[0][0]:
459 # File has been reverted.
[email protected]fb2b8eb2009-04-23 21:03:42460 save = True
461 files.remove(file)
[email protected]8a62d5b2009-05-11 15:59:01462 continue
463 status = status_result[0][0]
464 if status != file[0]:
[email protected]fb2b8eb2009-04-23 21:03:42465 save = True
466 files[files.index(file)] = (status, file[1])
467 change_info = ChangeInfo(changename, issue, description, files)
468 if save:
469 change_info.Save()
470 return change_info
471
472
473def GetCLs():
474 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52475 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42476 if CODEREVIEW_SETTINGS_FILE in cls:
477 cls.remove(CODEREVIEW_SETTINGS_FILE)
478 return cls
479
480
481def GenerateChangeName():
482 """Generate a random changelist name."""
483 random.seed()
484 current_cl_names = GetCLs()
485 while True:
486 cl_name = (random.choice(string.ascii_lowercase) +
487 random.choice(string.digits) +
488 random.choice(string.ascii_lowercase) +
489 random.choice(string.digits))
490 if cl_name not in current_cl_names:
491 return cl_name
492
493
494def GetModifiedFiles():
495 """Returns a set that maps from changelist name to (status,filename) tuples.
496
497 Files not in a changelist have an empty changelist name. Filenames are in
498 relation to the top level directory of the current repository. Note that
499 only the current directory and subdirectories are scanned, in order to
500 improve performance while still being flexible.
501 """
502 files = {}
503
504 # Since the files are normalized to the root folder of the repositary, figure
505 # out what we need to add to the paths.
506 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
507
508 # Get a list of all files in changelists.
509 files_in_cl = {}
510 for cl in GetCLs():
511 change_info = LoadChangelistInfo(cl)
512 for status, filename in change_info.files:
513 files_in_cl[filename] = change_info.name
514
515 # Get all the modified files.
[email protected]4810a962009-05-12 21:03:34516 status_result = gclient.CaptureSVNStatus(None)
[email protected]207fdf32009-04-28 19:57:01517 for line in status_result:
518 status = line[0]
519 filename = line[1]
520 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42521 continue
[email protected]fb2b8eb2009-04-23 21:03:42522 if dir_prefix:
523 filename = os.path.join(dir_prefix, filename)
524 change_list_name = ""
525 if filename in files_in_cl:
526 change_list_name = files_in_cl[filename]
527 files.setdefault(change_list_name, []).append((status, filename))
528
529 return files
530
531
532def GetFilesNotInCL():
533 """Returns a list of tuples (status,filename) that aren't in any changelists.
534
535 See docstring of GetModifiedFiles for information about path of files and
536 which directories are scanned.
537 """
538 modified_files = GetModifiedFiles()
539 if "" not in modified_files:
540 return []
541 return modified_files[""]
542
543
544def SendToRietveld(request_path, payload=None,
545 content_type="application/octet-stream", timeout=None):
546 """Send a POST/GET to Rietveld. Returns the response body."""
547 def GetUserCredentials():
548 """Prompts the user for a username and password."""
549 email = upload.GetEmail()
550 password = getpass.getpass("Password for %s: " % email)
551 return email, password
552
553 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
554 rpc_server = upload.HttpRpcServer(server,
555 GetUserCredentials,
556 host_override=server,
557 save_cookies=True)
558 try:
559 return rpc_server.Send(request_path, payload, content_type, timeout)
560 except urllib2.URLError, e:
561 if timeout is None:
562 ErrorExit("Error accessing url %s" % request_path)
563 else:
564 return None
565
566
567def GetIssueDescription(issue):
568 """Returns the issue description from Rietveld."""
569 return SendToRietveld("/" + issue + "/description")
570
571
[email protected]fb2b8eb2009-04-23 21:03:42572def Opened():
573 """Prints a list of modified files in the current directory down."""
574 files = GetModifiedFiles()
575 cl_keys = files.keys()
576 cl_keys.sort()
577 for cl_name in cl_keys:
578 if cl_name:
579 note = ""
580 if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]):
581 note = " (Note: this changelist contains files outside this directory)"
582 print "\n--- Changelist " + cl_name + note + ":"
583 for file in files[cl_name]:
584 print "".join(file)
585
586
587def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53588 if argv:
589 if argv[0] == 'try':
590 TryChange(None, ['--help'], swallow_exception=False)
591 return
592 if argv[0] == 'upload':
593 upload.RealMain(['upload.py', '--help'])
594 return
[email protected]fb2b8eb2009-04-23 21:03:42595
596 print (
597"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48598version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42599
600Basic commands:
601-----------------------------------------
602 gcl change change_name
603 Add/remove files to a changelist. Only scans the current directory and
604 subdirectories.
605
606 gcl upload change_name [-r [email protected],[email protected],...]
607 [--send_mail] [--no_try] [--no_presubmit]
608 Uploads the changelist to the server for review.
609
[email protected]9b613272009-04-24 01:28:28610 gcl commit change_name [--force]
[email protected]fb2b8eb2009-04-23 21:03:42611 Commits the changelist to the repository.
612
613 gcl lint change_name
614 Check all the files in the changelist for possible style violations.
615
616Advanced commands:
617-----------------------------------------
618 gcl delete change_name
619 Deletes a changelist.
620
621 gcl diff change_name
622 Diffs all files in the changelist.
623
624 gcl presubmit change_name
625 Runs presubmit checks without uploading the changelist.
626
627 gcl diff
628 Diffs all files in the current directory and subdirectories that aren't in
629 a changelist.
630
631 gcl changes
632 Lists all the the changelists and the files in them.
633
634 gcl nothave [optional directory]
635 Lists files unknown to Subversion.
636
637 gcl opened
638 Lists modified files in the current directory and subdirectories.
639
640 gcl settings
641 Print the code review settings for this directory.
642
643 gcl status
644 Lists modified and unknown files in the current directory and
645 subdirectories.
646
647 gcl try change_name
648 Sends the change to the tryserver so a trybot can do a test run on your
649 code. To send multiple changes as one path, use a comma-separated list
650 of changenames.
651 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53652
653 gcl help [command]
654 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42655""")
656
657def GetEditor():
658 editor = os.environ.get("SVN_EDITOR")
659 if not editor:
660 editor = os.environ.get("EDITOR")
661
662 if not editor:
663 if sys.platform.startswith("win"):
664 editor = "notepad"
665 else:
666 editor = "vi"
667
668 return editor
669
670
671def GenerateDiff(files, root=None):
672 """Returns a string containing the diff for the given file list.
673
674 The files in the list should either be absolute paths or relative to the
675 given root. If no root directory is provided, the repository root will be
676 used.
677 """
678 previous_cwd = os.getcwd()
679 if root is None:
680 os.chdir(GetRepositoryRoot())
681 else:
682 os.chdir(root)
683
684 diff = []
685 for file in files:
686 # Use svn info output instead of os.path.isdir because the latter fails
687 # when the file is deleted.
[email protected]46a94102009-05-12 20:32:43688 if gclient.CaptureSVNInfo(file).get("Node Kind") in ("dir", "directory"):
[email protected]fb2b8eb2009-04-23 21:03:42689 continue
690 # If the user specified a custom diff command in their svn config file,
691 # then it'll be used when we do svn diff, which we don't want to happen
692 # since we want the unified diff. Using --diff-cmd=diff doesn't always
693 # work, since they can have another diff executable in their path that
694 # gives different line endings. So we use a bogus temp directory as the
695 # config directory, which gets around these problems.
696 if sys.platform.startswith("win"):
697 parent_dir = tempfile.gettempdir()
698 else:
699 parent_dir = sys.path[0] # tempdir is not secure.
700 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
701 if not os.path.exists(bogus_dir):
702 os.mkdir(bogus_dir)
703 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
704 if output:
705 diff.append(output)
[email protected]c3150202009-05-13 14:31:01706 elif IsSVNMoved(file):
707 # svn diff on a mv/cp'd file outputs nothing.
708 # We put in an empty Index entry so upload.py knows about them.
[email protected]fb2b8eb2009-04-23 21:03:42709 diff.append("\nIndex: %s\n" % file)
[email protected]c3150202009-05-13 14:31:01710 else:
711 # The file is not modified anymore. It should be removed from the set.
712 pass
[email protected]fb2b8eb2009-04-23 21:03:42713 os.chdir(previous_cwd)
714 return "".join(diff)
715
716
717def UploadCL(change_info, args):
718 if not change_info.FileList():
719 print "Nothing to upload, changelist is empty."
720 return
721
722 if not "--no_presubmit" in args:
723 if not DoPresubmitChecks(change_info, committing=False):
724 return
725 else:
726 args.remove("--no_presubmit")
727
728 no_try = "--no_try" in args
729 if no_try:
730 args.remove("--no_try")
731 else:
732 # Support --no-try as --no_try
733 no_try = "--no-try" in args
734 if no_try:
735 args.remove("--no-try")
736
737 # Map --send-mail to --send_mail
738 if "--send-mail" in args:
739 args.remove("--send-mail")
740 args.append("--send_mail")
741
742 # Supports --clobber for the try server.
743 clobber = False
744 if "--clobber" in args:
745 args.remove("--clobber")
746 clobber = True
747
[email protected]003c2692009-05-20 13:08:08748 # Disable try when the server is overridden.
749 server_1 = re.compile(r"^-s\b.*")
750 server_2 = re.compile(r"^--server\b.*")
751 for arg in args:
752 if server_1.match(arg) or server_2.match(arg):
753 no_try = True
754 break
[email protected]fb2b8eb2009-04-23 21:03:42755
756 upload_arg = ["upload.py", "-y"]
757 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
758 upload_arg.extend(args)
759
760 desc_file = ""
761 if change_info.issue: # Uploading a new patchset.
762 found_message = False
763 for arg in args:
764 if arg.startswith("--message") or arg.startswith("-m"):
765 found_message = True
766 break
767
768 if not found_message:
769 upload_arg.append("--message=''")
770
771 upload_arg.append("--issue=" + change_info.issue)
772 else: # First time we upload.
773 handle, desc_file = tempfile.mkstemp(text=True)
774 os.write(handle, change_info.description)
775 os.close(handle)
776
777 cc_list = GetCodeReviewSetting("CC_LIST")
778 if cc_list:
779 upload_arg.append("--cc=" + cc_list)
780 upload_arg.append("--description_file=" + desc_file + "")
781 if change_info.description:
782 subject = change_info.description[:77]
783 if subject.find("\r\n") != -1:
784 subject = subject[:subject.find("\r\n")]
785 if subject.find("\n") != -1:
786 subject = subject[:subject.find("\n")]
787 if len(change_info.description) > 77:
788 subject = subject + "..."
789 upload_arg.append("--message=" + subject)
790
791 # Change the current working directory before calling upload.py so that it
792 # shows the correct base.
793 previous_cwd = os.getcwd()
794 os.chdir(GetRepositoryRoot())
795
796 # If we have a lot of files with long paths, then we won't be able to fit
797 # the command to "svn diff". Instead, we generate the diff manually for
798 # each file and concatenate them before passing it to upload.py.
799 if change_info.patch is None:
800 change_info.patch = GenerateDiff(change_info.FileList())
801 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
802 if issue and issue != change_info.issue:
803 change_info.issue = issue
804 change_info.Save()
805
806 if desc_file:
807 os.remove(desc_file)
808
809 # Do background work on Rietveld to lint the file so that the results are
810 # ready when the issue is viewed.
811 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
812
813 # Once uploaded to Rietveld, send it to the try server.
814 if not no_try:
815 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
816 if try_on_upload and try_on_upload.lower() == 'true':
817 # Use the local diff.
818 args = [
819 "--issue", change_info.issue,
820 "--patchset", patchset,
821 ]
822 if clobber:
823 args.append('--clobber')
824 TryChange(change_info, args, swallow_exception=True)
825
826 os.chdir(previous_cwd)
827
828
829def PresubmitCL(change_info):
830 """Reports what presubmit checks on the change would report."""
831 if not change_info.FileList():
832 print "Nothing to presubmit check, changelist is empty."
833 return
834
835 print "*** Presubmit checks for UPLOAD would report: ***"
836 DoPresubmitChecks(change_info, committing=False)
837
838 print "\n\n*** Presubmit checks for COMMIT would report: ***"
839 DoPresubmitChecks(change_info, committing=True)
840
841
842def TryChange(change_info, args, swallow_exception):
843 """Create a diff file of change_info and send it to the try server."""
844 try:
845 import trychange
846 except ImportError:
847 if swallow_exception:
848 return
849 ErrorExit("You need to install trychange.py to use the try server.")
850
851 if change_info:
852 trychange_args = ['--name', change_info.name]
853 trychange_args.extend(args)
854 trychange.TryChange(trychange_args,
855 file_list=change_info.FileList(),
856 swallow_exception=swallow_exception,
857 prog='gcl try')
858 else:
859 trychange.TryChange(args,
860 file_list=None,
861 swallow_exception=swallow_exception,
862 prog='gcl try')
863
864
865def Commit(change_info, args):
866 if not change_info.FileList():
867 print "Nothing to commit, changelist is empty."
868 return
869
870 if not "--no_presubmit" in args:
871 if not DoPresubmitChecks(change_info, committing=True):
872 return
873 else:
874 args.remove("--no_presubmit")
875
876 no_tree_status_check = ("--force" in args or "-f" in args)
877 if not no_tree_status_check and not IsTreeOpen():
878 print ("Error: The tree is closed. Try again later or use --force to force"
879 " the commit. May the --force be with you.")
880 return
881
882 commit_cmd = ["svn", "commit"]
883 filename = ''
884 if change_info.issue:
885 # Get the latest description from Rietveld.
886 change_info.description = GetIssueDescription(change_info.issue)
887
888 commit_message = change_info.description.replace('\r\n', '\n')
889 if change_info.issue:
890 commit_message += ('\nReview URL: http://%s/%s' %
891 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
892 change_info.issue))
893
894 handle, commit_filename = tempfile.mkstemp(text=True)
895 os.write(handle, commit_message)
896 os.close(handle)
897
898 handle, targets_filename = tempfile.mkstemp(text=True)
899 os.write(handle, "\n".join(change_info.FileList()))
900 os.close(handle)
901
902 commit_cmd += ['--file=' + commit_filename]
903 commit_cmd += ['--targets=' + targets_filename]
904 # Change the current working directory before calling commit.
905 previous_cwd = os.getcwd()
906 os.chdir(GetRepositoryRoot())
907 output = RunShell(commit_cmd, True)
908 os.remove(commit_filename)
909 os.remove(targets_filename)
910 if output.find("Committed revision") != -1:
911 change_info.Delete()
912
913 if change_info.issue:
914 revision = re.compile(".*?\nCommitted revision (\d+)",
915 re.DOTALL).match(output).group(1)
916 viewvc_url = GetCodeReviewSetting("VIEW_VC")
917 change_info.description = change_info.description + '\n'
918 if viewvc_url:
919 change_info.description += "\nCommitted: " + viewvc_url + revision
920 change_info.CloseIssue()
921 os.chdir(previous_cwd)
922
923
924def Change(change_info):
925 """Creates/edits a changelist."""
926 if change_info.issue:
927 try:
928 description = GetIssueDescription(change_info.issue)
929 except urllib2.HTTPError, err:
930 if err.code == 404:
931 # The user deleted the issue in Rietveld, so forget the old issue id.
932 description = change_info.description
933 change_info.issue = ""
934 change_info.Save()
935 else:
936 ErrorExit("Error getting the description from Rietveld: " + err)
937 else:
938 description = change_info.description
939
940 other_files = GetFilesNotInCL()
941
942 separator1 = ("\n---All lines above this line become the description.\n"
943 "---Repository Root: " + GetRepositoryRoot() + "\n"
944 "---Paths in this changelist (" + change_info.name + "):\n")
945 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
946 text = (description + separator1 + '\n' +
947 '\n'.join([f[0] + f[1] for f in change_info.files]) + separator2 +
948 '\n'.join([f[0] + f[1] for f in other_files]) + '\n')
949
950 handle, filename = tempfile.mkstemp(text=True)
951 os.write(handle, text)
952 os.close(handle)
953
954 os.system(GetEditor() + " " + filename)
955
956 result = ReadFile(filename)
957 os.remove(filename)
958
959 if not result:
960 return
961
962 split_result = result.split(separator1, 1)
963 if len(split_result) != 2:
964 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
965
966 new_description = split_result[0]
967 cl_files_text = split_result[1]
968 if new_description != description:
969 change_info.description = new_description
970 if change_info.issue:
971 # Update the Rietveld issue with the new description.
972 change_info.UpdateRietveldDescription()
973
974 new_cl_files = []
975 for line in cl_files_text.splitlines():
976 if not len(line):
977 continue
978 if line.startswith("---"):
979 break
980 status = line[:7]
981 file = line[7:]
982 new_cl_files.append((status, file))
983 change_info.files = new_cl_files
984
985 change_info.Save()
986 print change_info.name + " changelist saved."
987 if change_info.MissingTests():
988 Warn("WARNING: " + MISSING_TEST_MSG)
989
990# We don't lint files in these path prefixes.
991IGNORE_PATHS = ("webkit",)
992
993# Valid extensions for files we want to lint.
994CPP_EXTENSIONS = ("cpp", "cc", "h")
995
996def Lint(change_info, args):
997 """Runs cpplint.py on all the files in |change_info|"""
998 try:
999 import cpplint
1000 except ImportError:
1001 ErrorExit("You need to install cpplint.py to lint C++ files.")
1002
1003 # Change the current working directory before calling lint so that it
1004 # shows the correct base.
1005 previous_cwd = os.getcwd()
1006 os.chdir(GetRepositoryRoot())
1007
1008 # Process cpplints arguments if any.
1009 filenames = cpplint.ParseArguments(args + change_info.FileList())
1010
1011 for file in filenames:
1012 if len([file for suffix in CPP_EXTENSIONS if file.endswith(suffix)]):
1013 if len([file for prefix in IGNORE_PATHS if file.startswith(prefix)]):
1014 print "Ignoring non-Google styled file %s" % file
1015 else:
1016 cpplint.ProcessFile(file, cpplint._cpplint_state.verbose_level)
1017
1018 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1019 os.chdir(previous_cwd)
1020
1021
1022def DoPresubmitChecks(change_info, committing):
1023 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1024 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:481025 import presubmit_support
1026 result = presubmit_support.DoPresubmitChecks(change_info,
1027 committing,
1028 verbose=False,
1029 output_stream=sys.stdout,
1030 input_stream=sys.stdin)
[email protected]fb2b8eb2009-04-23 21:03:421031 if not result:
1032 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1033 return result
1034
1035
1036def Changes():
1037 """Print all the changelists and their files."""
1038 for cl in GetCLs():
1039 change_info = LoadChangelistInfo(cl, True, True)
1040 print "\n--- Changelist " + change_info.name + ":"
1041 for file in change_info.files:
1042 print "".join(file)
1043
1044
1045def main(argv=None):
1046 if argv is None:
1047 argv = sys.argv
1048
1049 if len(argv) == 1:
1050 Help()
1051 return 0;
1052
[email protected]374d65e2009-05-21 14:00:521053 # Create the directories where we store information about changelists if it
[email protected]fb2b8eb2009-04-23 21:03:421054 # doesn't exist.
1055 if not os.path.exists(GetInfoDir()):
1056 os.mkdir(GetInfoDir())
[email protected]374d65e2009-05-21 14:00:521057 if not os.path.exists(GetChangesDir()):
1058 os.mkdir(GetChangesDir())
1059 # For smooth upgrade support, move the files in GetInfoDir() to
1060 # GetChangesDir().
1061 # TODO(maruel): Remove this code in August 2009.
1062 for file in os.listdir(unicode(GetInfoDir())):
1063 file_path = os.path.join(unicode(GetInfoDir()), file)
1064 if os.path.isfile(file_path) and file != CODEREVIEW_SETTINGS_FILE:
1065 shutil.move(file_path, GetChangesDir())
[email protected]98fc2b92009-05-21 14:11:511066 if not os.path.exists(GetCacheDir()):
1067 os.mkdir(GetCacheDir())
[email protected]fb2b8eb2009-04-23 21:03:421068
1069 # Commands that don't require an argument.
1070 command = argv[1]
1071 if command == "opened":
1072 Opened()
1073 return 0
1074 if command == "status":
1075 Opened()
1076 print "\n--- Not in any changelist:"
1077 UnknownFiles([])
1078 return 0
1079 if command == "nothave":
1080 UnknownFiles(argv[2:])
1081 return 0
1082 if command == "changes":
1083 Changes()
1084 return 0
1085 if command == "help":
1086 Help(argv[2:])
1087 return 0
1088 if command == "diff" and len(argv) == 2:
1089 files = GetFilesNotInCL()
1090 print GenerateDiff([x[1] for x in files])
1091 return 0
1092 if command == "settings":
1093 ignore = GetCodeReviewSetting("UNKNOWN");
1094 print CODEREVIEW_SETTINGS
1095 return 0
1096
1097 if len(argv) == 2:
1098 if command == "change":
1099 # Generate a random changelist name.
1100 changename = GenerateChangeName()
1101 else:
1102 ErrorExit("Need a changelist name.")
1103 else:
1104 changename = argv[2]
1105
1106 # When the command is 'try' and --patchset is used, the patch to try
1107 # is on the Rietveld server. 'change' creates a change so it's fine if the
1108 # change didn't exist. All other commands require an existing change.
1109 fail_on_not_found = command != "try" and command != "change"
1110 if command == "try" and changename.find(',') != -1:
1111 change_info = LoadChangelistInfoForMultiple(changename, True, True)
1112 else:
1113 change_info = LoadChangelistInfo(changename, fail_on_not_found, True)
1114
1115 if command == "change":
1116 Change(change_info)
1117 elif command == "lint":
1118 Lint(change_info, argv[3:])
1119 elif command == "upload":
1120 UploadCL(change_info, argv[3:])
1121 elif command == "presubmit":
1122 PresubmitCL(change_info)
1123 elif command in ("commit", "submit"):
1124 Commit(change_info, argv[3:])
1125 elif command == "delete":
1126 change_info.Delete()
1127 elif command == "try":
1128 # When the change contains no file, send the "changename" positional
1129 # argument to trychange.py.
1130 if change_info.files:
1131 args = argv[3:]
1132 else:
1133 change_info = None
1134 args = argv[2:]
1135 TryChange(change_info, args, swallow_exception=False)
1136 else:
1137 # Everything else that is passed into gcl we redirect to svn, after adding
1138 # the files. This allows commands such as 'gcl diff xxx' to work.
1139 args =["svn", command]
1140 root = GetRepositoryRoot()
1141 args.extend([os.path.join(root, x) for x in change_info.FileList()])
1142 RunShell(args, True)
1143 return 0
1144
1145
1146if __name__ == "__main__":
1147 sys.exit(main())