Index: depot_tools/gcl.py |
=================================================================== |
--- depot_tools/gcl.py (revision 0) |
+++ depot_tools/gcl.py (revision 0) |
@@ -0,0 +1,1122 @@ |
+#!/usr/bin/python |
+# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+# |
+# Wrapper script around Rietveld's upload.py that groups files into |
+# changelists. |
+ |
+import getpass |
+import os |
+import random |
+import re |
+import string |
+import subprocess |
+import sys |
+import tempfile |
+import upload |
+import urllib2 |
+ |
+CODEREVIEW_SETTINGS = { |
+ # Default values. |
+ "CODE_REVIEW_SERVER": "codereview.chromium.org", |
+ "CC_LIST": "chromium-reviews@googlegroups.com", |
+ "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=", |
+} |
+ |
+# Use a shell for subcommands on Windows to get a PATH search, and because svn |
+# may be a batch file. |
+use_shell = sys.platform.startswith("win") |
+ |
+# globals that store the root of the current repository and the directory where |
+# we store information about changelists. |
+repository_root = "" |
+gcl_info_dir = "" |
+ |
+# Filename where we store repository specific information for gcl. |
+CODEREVIEW_SETTINGS_FILE = "codereview.settings" |
+ |
+# Warning message when the change appears to be missing tests. |
+MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" |
+ |
+# Global cache of files cached in GetInfoDir(). |
+FILES_CACHE = {} |
+ |
+ |
+def IsSVNMoved(filename): |
+ """Determine if a file has been added through svn mv""" |
+ info = GetSVNFileInfo(filename) |
+ return (info.get('Copied From URL') and |
+ info.get('Copied From Rev') and |
+ info.get('Schedule') == 'add') |
+ |
+ |
+def GetSVNFileInfo(file): |
+ """Returns a dictionary from the svn info output for the given file.""" |
+ output = RunShell(["svn", "info", file]) |
+ result = {} |
+ re_key_value_pair = re.compile('^(.*)\: (.*)$') |
+ for line in output.splitlines(): |
+ key_value_pair = re_key_value_pair.match(line) |
+ if key_value_pair: |
+ result[key_value_pair.group(1)] = key_value_pair.group(2) |
+ return result |
+ |
+ |
+def GetSVNFileProperty(file, property_name): |
+ """Returns the value of an SVN property for the given file. |
+ |
+ Args: |
+ file: The file to check |
+ property_name: The name of the SVN property, e.g. "svn:mime-type" |
+ |
+ Returns: |
+ The value of the property, which will be the empty string if the property |
+ is not set on the file. If the file is not under version control, the |
+ empty string is also returned. |
+ """ |
+ output = RunShell(["svn", "propget", property_name, file]) |
+ if (output.startswith("svn: ") and |
+ output.endswith("is not under version control")): |
+ return "" |
+ else: |
+ return output |
+ |
+ |
+def GetRepositoryRoot(): |
+ """Returns the top level directory of the current repository. |
+ |
+ The directory is returned as an absolute path. |
+ """ |
+ global repository_root |
+ if not repository_root: |
+ cur_dir_repo_root = GetSVNFileInfo(os.getcwd()).get("Repository Root") |
+ if not cur_dir_repo_root: |
+ raise Exception("gcl run outside of repository") |
+ |
+ repository_root = os.getcwd() |
+ while True: |
+ parent = os.path.dirname(repository_root) |
+ if GetSVNFileInfo(parent).get("Repository Root") != cur_dir_repo_root: |
+ break |
+ repository_root = parent |
+ return repository_root |
+ |
+ |
+def GetInfoDir(): |
+ """Returns the directory where gcl info files are stored.""" |
+ global gcl_info_dir |
+ if not gcl_info_dir: |
+ gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info') |
+ return gcl_info_dir |
+ |
+ |
+def GetCachedFile(filename, max_age=60*60*24*3, use_root=False): |
+ """Retrieves a file from the repository and caches it in GetInfoDir() for |
+ max_age seconds. |
+ |
+ use_root: If False, look up the arborescence for the first match, otherwise go |
+ directory to the root repository. |
+ """ |
+ global FILES_CACHE |
+ if filename not in FILES_CACHE: |
+ # Don't try to look up twice. |
+ FILES_CACHE[filename] = None |
+ # First we check if we have a cached version. |
+ cached_file = os.path.join(GetInfoDir(), filename) |
+ if (not os.path.exists(cached_file) or |
+ os.stat(cached_file).st_mtime > max_age): |
+ dir_info = GetSVNFileInfo(".") |
+ repo_root = dir_info["Repository Root"] |
+ if use_root: |
+ url_path = repo_root |
+ else: |
+ url_path = dir_info["URL"] |
+ content = "" |
+ while True: |
+ # Look for the codereview.settings file at the current level. |
+ svn_path = url_path + "/" + filename |
+ content, rc = RunShellWithReturnCode(["svn", "cat", svn_path]) |
+ if not rc: |
+ # Exit the loop if the file was found. Override content. |
+ break |
+ # Make sure to mark settings as empty if not found. |
+ content = "" |
+ if url_path == repo_root: |
+ # Reached the root. Abandoning search. |
+ break |
+ # Go up one level to try again. |
+ url_path = os.path.dirname(url_path) |
+ # Write a cached version even if there isn't a file, so we don't try to |
+ # fetch it each time. |
+ WriteFile(cached_file, content) |
+ else: |
+ content = ReadFile(cached_settings_file) |
+ FILES_CACHE[filename] = content |
+ return FILES_CACHE[filename] |
+ |
+ |
+def GetCodeReviewSetting(key): |
+ """Returns a value for the given key for this repository.""" |
+ # Use '__just_initialized' as a flag to determine if the settings were |
+ # already initialized. |
+ if '__just_initialized' not in CODEREVIEW_SETTINGS: |
+ for line in GetCachedFile(CODEREVIEW_SETTINGS_FILE).splitlines(): |
+ if not line or line.startswith("#"): |
+ continue |
+ k, v = line.split(": ", 1) |
+ CODEREVIEW_SETTINGS[k] = v |
+ CODEREVIEW_SETTINGS.setdefault('__just_initialized', None) |
+ return CODEREVIEW_SETTINGS.get(key, "") |
+ |
+ |
+def IsTreeOpen(): |
+ """Fetches the tree status and returns either True or False.""" |
+ url = GetCodeReviewSetting('STATUS') |
+ status = "" |
+ if url: |
+ status = urllib2.urlopen(url).read() |
+ return status.find('0') == -1 |
+ |
+ |
+def Warn(msg): |
+ ErrorExit(msg, exit=False) |
+ |
+ |
+def ErrorExit(msg, exit=True): |
+ """Print an error message to stderr and optionally exit.""" |
+ print >>sys.stderr, msg |
+ if exit: |
+ sys.exit(1) |
+ |
+ |
+def RunShellWithReturnCode(command, print_output=False): |
+ """Executes a command and returns the output and the return code.""" |
+ p = subprocess.Popen(command, stdout=subprocess.PIPE, |
+ stderr=subprocess.STDOUT, shell=use_shell, |
+ universal_newlines=True) |
+ if print_output: |
+ output_array = [] |
+ while True: |
+ line = p.stdout.readline() |
+ if not line: |
+ break |
+ if print_output: |
+ print line.strip('\n') |
+ output_array.append(line) |
+ output = "".join(output_array) |
+ else: |
+ output = p.stdout.read() |
+ p.wait() |
+ p.stdout.close() |
+ return output, p.returncode |
+ |
+ |
+def RunShell(command, print_output=False): |
+ """Executes a command and returns the output.""" |
+ return RunShellWithReturnCode(command, print_output)[0] |
+ |
+ |
+def ReadFile(filename): |
+ """Returns the contents of a file.""" |
+ file = open(filename, 'r') |
+ result = file.read() |
+ file.close() |
+ return result |
+ |
+ |
+def WriteFile(filename, contents): |
+ """Overwrites the file with the given contents.""" |
+ file = open(filename, 'w') |
+ file.write(contents) |
+ file.close() |
+ |
+ |
+class ChangeInfo: |
+ """Holds information about a changelist. |
+ |
+ issue: the Rietveld issue number, of "" if it hasn't been uploaded yet. |
+ description: the description. |
+ files: a list of 2 tuple containing (status, filename) of changed files, |
+ with paths being relative to the top repository directory. |
+ """ |
+ def __init__(self, name="", issue="", description="", files=[]): |
+ self.name = name |
+ self.issue = issue |
+ self.description = description |
+ self.files = files |
+ self.patch = None |
+ |
+ def FileList(self): |
+ """Returns a list of files.""" |
+ return [file[1] for file in self.files] |
+ |
+ def _NonDeletedFileList(self): |
+ """Returns a list of files in this change, not including deleted files.""" |
+ return [file[1] for file in self.files if not file[0].startswith("D")] |
+ |
+ def _AddedFileList(self): |
+ """Returns a list of files added in this change.""" |
+ return [file[1] for file in self.files if file[0].startswith("A")] |
+ |
+ def Save(self): |
+ """Writes the changelist information to disk.""" |
+ data = SEPARATOR.join([self.issue, |
+ "\n".join([f[0] + f[1] for f in self.files]), |
+ self.description]) |
+ WriteFile(GetChangelistInfoFile(self.name), data) |
+ |
+ def Delete(self): |
+ """Removes the changelist information from disk.""" |
+ os.remove(GetChangelistInfoFile(self.name)) |
+ |
+ def CloseIssue(self): |
+ """Closes the Rietveld issue for this changelist.""" |
+ data = [("description", self.description),] |
+ ctype, body = upload.EncodeMultipartFormData(data, []) |
+ SendToRietveld("/" + self.issue + "/close", body, ctype) |
+ |
+ def UpdateRietveldDescription(self): |
+ """Sets the description for an issue on Rietveld.""" |
+ data = [("description", self.description),] |
+ ctype, body = upload.EncodeMultipartFormData(data, []) |
+ SendToRietveld("/" + self.issue + "/description", body, ctype) |
+ |
+ def MissingTests(self): |
+ """Returns True if the change looks like it needs unit tests but has none. |
+ |
+ A change needs unit tests if it contains any new source files or methods. |
+ """ |
+ SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"] |
+ # Ignore third_party entirely. |
+ files = [file for file in self._NonDeletedFileList() |
+ if file.find("third_party") == -1] |
+ added_files = [file for file in self._AddedFileList() |
+ if file.find("third_party") == -1] |
+ |
+ # If the change is entirely in third_party, we're done. |
+ if len(files) == 0: |
+ return False |
+ |
+ # Any new or modified test files? |
+ # A test file's name ends with "test.*" or "tests.*". |
+ test_files = [test for test in files |
+ if os.path.splitext(test)[0].rstrip("s").endswith("test")] |
+ if len(test_files) > 0: |
+ return False |
+ |
+ # Any new source files? |
+ source_files = [file for file in added_files |
+ if os.path.splitext(file)[1] in SOURCE_SUFFIXES] |
+ if len(source_files) > 0: |
+ return True |
+ |
+ # Do the long test, checking the files for new methods. |
+ return self._HasNewMethod() |
+ |
+ def _HasNewMethod(self): |
+ """Returns True if the changeset contains any new functions, or if a |
+ function signature has been changed. |
+ |
+ A function is identified by starting flush left, containing a "(" before |
+ the next flush-left line, and either ending with "{" before the next |
+ flush-left line or being followed by an unindented "{". |
+ |
+ Currently this returns True for new methods, new static functions, and |
+ methods or functions whose signatures have been changed. |
+ |
+ Inline methods added to header files won't be detected by this. That's |
+ acceptable for purposes of determining if a unit test is needed, since |
+ inline methods should be trivial. |
+ """ |
+ # To check for methods added to source or header files, we need the diffs. |
+ # We'll generate them all, since there aren't likely to be many files |
+ # apart from source and headers; besides, we'll want them all if we're |
+ # uploading anyway. |
+ if self.patch is None: |
+ self.patch = GenerateDiff(self.FileList()) |
+ |
+ definition = "" |
+ for line in self.patch.splitlines(): |
+ if not line.startswith("+"): |
+ continue |
+ line = line.strip("+").rstrip(" \t") |
+ # Skip empty lines, comments, and preprocessor directives. |
+ # TODO(pamg): Handle multiline comments if it turns out to be a problem. |
+ if line == "" or line.startswith("/") or line.startswith("#"): |
+ continue |
+ |
+ # A possible definition ending with "{" is complete, so check it. |
+ if definition.endswith("{"): |
+ if definition.find("(") != -1: |
+ return True |
+ definition = "" |
+ |
+ # A { or an indented line, when we're in a definition, continues it. |
+ if (definition != "" and |
+ (line == "{" or line.startswith(" ") or line.startswith("\t"))): |
+ definition += line |
+ |
+ # A flush-left line starts a new possible function definition. |
+ elif not line.startswith(" ") and not line.startswith("\t"): |
+ definition = line |
+ |
+ return False |
+ |
+ |
+SEPARATOR = "\n-----\n" |
+# The info files have the following format: |
+# issue_id\n |
+# SEPARATOR\n |
+# filepath1\n |
+# filepath2\n |
+# . |
+# . |
+# filepathn\n |
+# SEPARATOR\n |
+# description |
+ |
+ |
+def GetChangelistInfoFile(changename): |
+ """Returns the file that stores information about a changelist.""" |
+ if not changename or re.search(r'[^\w-]', changename): |
+ ErrorExit("Invalid changelist name: " + changename) |
+ return os.path.join(GetInfoDir(), changename) |
+ |
+ |
+def LoadChangelistInfoForMultiple(changenames, fail_on_not_found=True, |
+ update_status=False): |
+ """Loads many changes and merge their files list into one pseudo change. |
+ |
+ This is mainly usefull to concatenate many changes into one for a 'gcl try'. |
+ """ |
+ changes = changenames.split(',') |
+ aggregate_change_info = ChangeInfo(name=changenames) |
+ for change in changes: |
+ aggregate_change_info.files += LoadChangelistInfo(change, |
+ fail_on_not_found, |
+ update_status).files |
+ return aggregate_change_info |
+ |
+ |
+def LoadChangelistInfo(changename, fail_on_not_found=True, |
+ update_status=False): |
+ """Gets information about a changelist. |
+ |
+ Args: |
+ fail_on_not_found: if True, this function will quit the program if the |
+ changelist doesn't exist. |
+ update_status: if True, the svn status will be updated for all the files |
+ and unchanged files will be removed. |
+ |
+ Returns: a ChangeInfo object. |
+ """ |
+ info_file = GetChangelistInfoFile(changename) |
+ if not os.path.exists(info_file): |
+ if fail_on_not_found: |
+ ErrorExit("Changelist " + changename + " not found.") |
+ return ChangeInfo(changename) |
+ data = ReadFile(info_file) |
+ split_data = data.split(SEPARATOR, 2) |
+ if len(split_data) != 3: |
+ os.remove(info_file) |
+ ErrorExit("Changelist file %s was corrupt and deleted" % info_file) |
+ issue = split_data[0] |
+ files = [] |
+ for line in split_data[1].splitlines(): |
+ status = line[:7] |
+ file = line[7:] |
+ files.append((status, file)) |
+ description = split_data[2] |
+ save = False |
+ if update_status: |
+ for file in files: |
+ filename = os.path.join(GetRepositoryRoot(), file[1]) |
+ status = RunShell(["svn", "status", filename])[:7] |
+ if not status: # File has been reverted. |
+ save = True |
+ files.remove(file) |
+ elif status != file[0]: |
+ save = True |
+ files[files.index(file)] = (status, file[1]) |
+ change_info = ChangeInfo(changename, issue, description, files) |
+ if save: |
+ change_info.Save() |
+ return change_info |
+ |
+ |
+def GetCLs(): |
+ """Returns a list of all the changelists in this repository.""" |
+ cls = os.listdir(GetInfoDir()) |
+ if CODEREVIEW_SETTINGS_FILE in cls: |
+ cls.remove(CODEREVIEW_SETTINGS_FILE) |
+ return cls |
+ |
+ |
+def GenerateChangeName(): |
+ """Generate a random changelist name.""" |
+ random.seed() |
+ current_cl_names = GetCLs() |
+ while True: |
+ cl_name = (random.choice(string.ascii_lowercase) + |
+ random.choice(string.digits) + |
+ random.choice(string.ascii_lowercase) + |
+ random.choice(string.digits)) |
+ if cl_name not in current_cl_names: |
+ return cl_name |
+ |
+ |
+def GetModifiedFiles(): |
+ """Returns a set that maps from changelist name to (status,filename) tuples. |
+ |
+ Files not in a changelist have an empty changelist name. Filenames are in |
+ relation to the top level directory of the current repository. Note that |
+ only the current directory and subdirectories are scanned, in order to |
+ improve performance while still being flexible. |
+ """ |
+ files = {} |
+ |
+ # Since the files are normalized to the root folder of the repositary, figure |
+ # out what we need to add to the paths. |
+ dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep) |
+ |
+ # Get a list of all files in changelists. |
+ files_in_cl = {} |
+ for cl in GetCLs(): |
+ change_info = LoadChangelistInfo(cl) |
+ for status, filename in change_info.files: |
+ files_in_cl[filename] = change_info.name |
+ |
+ # Get all the modified files. |
+ status = RunShell(["svn", "status"]) |
+ for line in status.splitlines(): |
+ if not len(line) or line[0] == "?": |
+ continue |
+ status = line[:7] |
+ filename = line[7:].strip() |
+ if dir_prefix: |
+ filename = os.path.join(dir_prefix, filename) |
+ change_list_name = "" |
+ if filename in files_in_cl: |
+ change_list_name = files_in_cl[filename] |
+ files.setdefault(change_list_name, []).append((status, filename)) |
+ |
+ return files |
+ |
+ |
+def GetFilesNotInCL(): |
+ """Returns a list of tuples (status,filename) that aren't in any changelists. |
+ |
+ See docstring of GetModifiedFiles for information about path of files and |
+ which directories are scanned. |
+ """ |
+ modified_files = GetModifiedFiles() |
+ if "" not in modified_files: |
+ return [] |
+ return modified_files[""] |
+ |
+ |
+def SendToRietveld(request_path, payload=None, |
+ content_type="application/octet-stream", timeout=None): |
+ """Send a POST/GET to Rietveld. Returns the response body.""" |
+ def GetUserCredentials(): |
+ """Prompts the user for a username and password.""" |
+ email = upload.GetEmail() |
+ password = getpass.getpass("Password for %s: " % email) |
+ return email, password |
+ |
+ server = GetCodeReviewSetting("CODE_REVIEW_SERVER") |
+ rpc_server = upload.HttpRpcServer(server, |
+ GetUserCredentials, |
+ host_override=server, |
+ save_cookies=True) |
+ try: |
+ return rpc_server.Send(request_path, payload, content_type, timeout) |
+ except urllib2.URLError, e: |
+ if timeout is None: |
+ ErrorExit("Error accessing url %s" % request_path) |
+ else: |
+ return None |
+ |
+ |
+def GetIssueDescription(issue): |
+ """Returns the issue description from Rietveld.""" |
+ return SendToRietveld("/" + issue + "/description") |
+ |
+ |
+def UnknownFiles(extra_args): |
+ """Runs svn status and prints unknown files. |
+ |
+ Any args in |extra_args| are passed to the tool to support giving alternate |
+ code locations. |
+ """ |
+ args = ["svn", "status"] |
+ args += extra_args |
+ p = subprocess.Popen(args, stdout = subprocess.PIPE, |
+ stderr = subprocess.STDOUT, shell = use_shell) |
+ while 1: |
+ line = p.stdout.readline() |
+ if not line: |
+ break |
+ if line[0] != '?': |
+ continue # Not an unknown file to svn. |
+ # The lines look like this: |
+ # "? foo.txt" |
+ # and we want just "foo.txt" |
+ print line[7:].strip() |
+ p.wait() |
+ p.stdout.close() |
+ |
+ |
+def Opened(): |
+ """Prints a list of modified files in the current directory down.""" |
+ files = GetModifiedFiles() |
+ cl_keys = files.keys() |
+ cl_keys.sort() |
+ for cl_name in cl_keys: |
+ if cl_name: |
+ note = "" |
+ if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]): |
+ note = " (Note: this changelist contains files outside this directory)" |
+ print "\n--- Changelist " + cl_name + note + ":" |
+ for file in files[cl_name]: |
+ print "".join(file) |
+ |
+ |
+def Help(argv=None): |
+ if argv and argv[0] == 'try': |
+ TryChange(None, ['--help'], swallow_exception=False) |
+ return |
+ |
+ print ( |
+"""GCL is a wrapper for Subversion that simplifies working with groups of files. |
+ |
+Basic commands: |
+----------------------------------------- |
+ gcl change change_name |
+ Add/remove files to a changelist. Only scans the current directory and |
+ subdirectories. |
+ |
+ gcl upload change_name [-r reviewer1@gmail.com,reviewer2@gmail.com,...] |
+ [--send_mail] [--no_try] [--no_presubmit] |
+ Uploads the changelist to the server for review. |
+ |
+ gcl commit change_name [--no_presubmit] [--force] |
+ Commits the changelist to the repository. |
+ |
+ gcl lint change_name |
+ Check all the files in the changelist for possible style violations. |
+ |
+Advanced commands: |
+----------------------------------------- |
+ gcl delete change_name |
+ Deletes a changelist. |
+ |
+ gcl diff change_name |
+ Diffs all files in the changelist. |
+ |
+ gcl presubmit change_name |
+ Runs presubmit checks without uploading the changelist. |
+ |
+ gcl diff |
+ Diffs all files in the current directory and subdirectories that aren't in |
+ a changelist. |
+ |
+ gcl changes |
+ Lists all the the changelists and the files in them. |
+ |
+ gcl nothave [optional directory] |
+ Lists files unknown to Subversion. |
+ |
+ gcl opened |
+ Lists modified files in the current directory and subdirectories. |
+ |
+ gcl settings |
+ Print the code review settings for this directory. |
+ |
+ gcl status |
+ Lists modified and unknown files in the current directory and |
+ subdirectories. |
+ |
+ gcl try change_name |
+ Sends the change to the tryserver so a trybot can do a test run on your |
+ code. To send multiple changes as one path, use a comma-separated list |
+ of changenames. |
+ --> Use 'gcl help try' for more information! |
+""") |
+ |
+def GetEditor(): |
+ editor = os.environ.get("SVN_EDITOR") |
+ if not editor: |
+ editor = os.environ.get("EDITOR") |
+ |
+ if not editor: |
+ if sys.platform.startswith("win"): |
+ editor = "notepad" |
+ else: |
+ editor = "vi" |
+ |
+ return editor |
+ |
+ |
+def GenerateDiff(files, root=None): |
+ """Returns a string containing the diff for the given file list. |
+ |
+ The files in the list should either be absolute paths or relative to the |
+ given root. If no root directory is provided, the repository root will be |
+ used. |
+ """ |
+ previous_cwd = os.getcwd() |
+ if root is None: |
+ os.chdir(GetRepositoryRoot()) |
+ else: |
+ os.chdir(root) |
+ |
+ diff = [] |
+ for file in files: |
+ # Use svn info output instead of os.path.isdir because the latter fails |
+ # when the file is deleted. |
+ if GetSVNFileInfo(file).get("Node Kind") == "directory": |
+ continue |
+ # If the user specified a custom diff command in their svn config file, |
+ # then it'll be used when we do svn diff, which we don't want to happen |
+ # since we want the unified diff. Using --diff-cmd=diff doesn't always |
+ # work, since they can have another diff executable in their path that |
+ # gives different line endings. So we use a bogus temp directory as the |
+ # config directory, which gets around these problems. |
+ if sys.platform.startswith("win"): |
+ parent_dir = tempfile.gettempdir() |
+ else: |
+ parent_dir = sys.path[0] # tempdir is not secure. |
+ bogus_dir = os.path.join(parent_dir, "temp_svn_config") |
+ if not os.path.exists(bogus_dir): |
+ os.mkdir(bogus_dir) |
+ output = RunShell(["svn", "diff", "--config-dir", bogus_dir, file]) |
+ if output: |
+ diff.append(output) |
+ # On Posix platforms, svn diff on a mv/cp'd file outputs nothing. |
+ # We put in an empty Index entry so upload.py knows about them. |
+ elif not sys.platform.startswith("win") and IsSVNMoved(file): |
+ diff.append("\nIndex: %s\n" % file) |
+ os.chdir(previous_cwd) |
+ return "".join(diff) |
+ |
+ |
+def UploadCL(change_info, args): |
+ if not change_info.FileList(): |
+ print "Nothing to upload, changelist is empty." |
+ return |
+ |
+ if not "--no_presubmit" in args: |
+ if not DoPresubmitChecks(change_info, committing=False): |
+ return |
+ else: |
+ args.remove("--no_presubmit") |
+ |
+ no_try = "--no_try" in args |
+ if no_try: |
+ args.remove("--no_try") |
+ else: |
+ # Support --no-try as --no_try |
+ no_try = "--no-try" in args |
+ if no_try: |
+ args.remove("--no-try") |
+ |
+ # Map --send-mail to --send_mail |
+ if "--send-mail" in args: |
+ args.remove("--send-mail") |
+ args.append("--send_mail") |
+ |
+ # Supports --clobber for the try server. |
+ clobber = False |
+ if "--clobber" in args: |
+ args.remove("--clobber") |
+ clobber = True |
+ |
+ # TODO(pamg): Do something when tests are missing. The plan is to upload a |
+ # message to Rietveld and have it shown in the UI attached to this patch. |
+ |
+ upload_arg = ["upload.py", "-y"] |
+ upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER")) |
+ upload_arg.extend(args) |
+ |
+ desc_file = "" |
+ if change_info.issue: # Uploading a new patchset. |
+ found_message = False |
+ for arg in args: |
+ if arg.startswith("--message") or arg.startswith("-m"): |
+ found_message = True |
+ break |
+ |
+ if not found_message: |
+ upload_arg.append("--message=''") |
+ |
+ upload_arg.append("--issue=" + change_info.issue) |
+ else: # First time we upload. |
+ handle, desc_file = tempfile.mkstemp(text=True) |
+ os.write(handle, change_info.description) |
+ os.close(handle) |
+ |
+ cc_list = GetCodeReviewSetting("CC_LIST") |
+ if cc_list: |
+ upload_arg.append("--cc=" + cc_list) |
+ upload_arg.append("--description_file=" + desc_file + "") |
+ if change_info.description: |
+ subject = change_info.description[:77] |
+ if subject.find("\r\n") != -1: |
+ subject = subject[:subject.find("\r\n")] |
+ if subject.find("\n") != -1: |
+ subject = subject[:subject.find("\n")] |
+ if len(change_info.description) > 77: |
+ subject = subject + "..." |
+ upload_arg.append("--message=" + subject) |
+ |
+ # Change the current working directory before calling upload.py so that it |
+ # shows the correct base. |
+ previous_cwd = os.getcwd() |
+ os.chdir(GetRepositoryRoot()) |
+ |
+ # If we have a lot of files with long paths, then we won't be able to fit |
+ # the command to "svn diff". Instead, we generate the diff manually for |
+ # each file and concatenate them before passing it to upload.py. |
+ if change_info.patch is None: |
+ change_info.patch = GenerateDiff(change_info.FileList()) |
+ issue, patchset = upload.RealMain(upload_arg, change_info.patch) |
+ if issue and issue != change_info.issue: |
+ change_info.issue = issue |
+ change_info.Save() |
+ |
+ if desc_file: |
+ os.remove(desc_file) |
+ |
+ # Do background work on Rietveld to lint the file so that the results are |
+ # ready when the issue is viewed. |
+ SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5) |
+ |
+ # Once uploaded to Rietveld, send it to the try server. |
+ if not no_try: |
+ try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD') |
+ if try_on_upload and try_on_upload.lower() == 'true': |
+ # Use the local diff. |
+ args = [ |
+ "--issue", change_info.issue, |
+ "--patchset", patchset, |
+ ] |
+ if clobber: |
+ args.append('--clobber') |
+ TryChange(change_info, args, swallow_exception=True) |
+ |
+ os.chdir(previous_cwd) |
+ |
+ |
+def PresubmitCL(change_info): |
+ """Reports what presubmit checks on the change would report.""" |
+ if not change_info.FileList(): |
+ print "Nothing to presubmit check, changelist is empty." |
+ return |
+ |
+ print "*** Presubmit checks for UPLOAD would report: ***" |
+ DoPresubmitChecks(change_info, committing=False) |
+ |
+ print "\n\n*** Presubmit checks for COMMIT would report: ***" |
+ DoPresubmitChecks(change_info, committing=True) |
+ |
+ |
+def TryChange(change_info, args, swallow_exception): |
+ """Create a diff file of change_info and send it to the try server.""" |
+ try: |
+ import trychange |
+ except ImportError: |
+ if swallow_exception: |
+ return |
+ ErrorExit("You need to install trychange.py to use the try server.") |
+ |
+ if change_info: |
+ trychange_args = ['--name', change_info.name] |
+ trychange_args.extend(args) |
+ trychange.TryChange(trychange_args, |
+ file_list=change_info.FileList(), |
+ swallow_exception=swallow_exception, |
+ prog='gcl try') |
+ else: |
+ trychange.TryChange(args, |
+ file_list=None, |
+ swallow_exception=swallow_exception, |
+ prog='gcl try') |
+ |
+ |
+def Commit(change_info, args): |
+ if not change_info.FileList(): |
+ print "Nothing to commit, changelist is empty." |
+ return |
+ |
+ if not "--no_presubmit" in args: |
+ if not DoPresubmitChecks(change_info, committing=True): |
+ return |
+ else: |
+ args.remove("--no_presubmit") |
+ |
+ no_tree_status_check = ("--force" in args or "-f" in args) |
+ if not no_tree_status_check and not IsTreeOpen(): |
+ print ("Error: The tree is closed. Try again later or use --force to force" |
+ " the commit. May the --force be with you.") |
+ return |
+ |
+ commit_cmd = ["svn", "commit"] |
+ filename = '' |
+ if change_info.issue: |
+ # Get the latest description from Rietveld. |
+ change_info.description = GetIssueDescription(change_info.issue) |
+ |
+ commit_message = change_info.description.replace('\r\n', '\n') |
+ if change_info.issue: |
+ commit_message += ('\nReview URL: http://%s/%s' % |
+ (GetCodeReviewSetting("CODE_REVIEW_SERVER"), |
+ change_info.issue)) |
+ |
+ handle, commit_filename = tempfile.mkstemp(text=True) |
+ os.write(handle, commit_message) |
+ os.close(handle) |
+ |
+ handle, targets_filename = tempfile.mkstemp(text=True) |
+ os.write(handle, "\n".join(change_info.FileList())) |
+ os.close(handle) |
+ |
+ commit_cmd += ['--file=' + commit_filename] |
+ commit_cmd += ['--targets=' + targets_filename] |
+ # Change the current working directory before calling commit. |
+ previous_cwd = os.getcwd() |
+ os.chdir(GetRepositoryRoot()) |
+ output = RunShell(commit_cmd, True) |
+ os.remove(commit_filename) |
+ os.remove(targets_filename) |
+ if output.find("Committed revision") != -1: |
+ change_info.Delete() |
+ |
+ if change_info.issue: |
+ revision = re.compile(".*?\nCommitted revision (\d+)", |
+ re.DOTALL).match(output).group(1) |
+ viewvc_url = GetCodeReviewSetting("VIEW_VC") |
+ change_info.description = change_info.description + '\n' |
+ if viewvc_url: |
+ change_info.description += "\nCommitted: " + viewvc_url + revision |
+ change_info.CloseIssue() |
+ os.chdir(previous_cwd) |
+ |
+ |
+def Change(change_info): |
+ """Creates/edits a changelist.""" |
+ if change_info.issue: |
+ try: |
+ description = GetIssueDescription(change_info.issue) |
+ except urllib2.HTTPError, err: |
+ if err.code == 404: |
+ # The user deleted the issue in Rietveld, so forget the old issue id. |
+ description = change_info.description |
+ change_info.issue = "" |
+ change_info.Save() |
+ else: |
+ ErrorExit("Error getting the description from Rietveld: " + err) |
+ else: |
+ description = change_info.description |
+ |
+ other_files = GetFilesNotInCL() |
+ |
+ separator1 = ("\n---All lines above this line become the description.\n" |
+ "---Repository Root: " + GetRepositoryRoot() + "\n" |
+ "---Paths in this changelist (" + change_info.name + "):\n") |
+ separator2 = "\n\n---Paths modified but not in any changelist:\n\n" |
+ text = (description + separator1 + '\n' + |
+ '\n'.join([f[0] + f[1] for f in change_info.files]) + separator2 + |
+ '\n'.join([f[0] + f[1] for f in other_files]) + '\n') |
+ |
+ handle, filename = tempfile.mkstemp(text=True) |
+ os.write(handle, text) |
+ os.close(handle) |
+ |
+ os.system(GetEditor() + " " + filename) |
+ |
+ result = ReadFile(filename) |
+ os.remove(filename) |
+ |
+ if not result: |
+ return |
+ |
+ split_result = result.split(separator1, 1) |
+ if len(split_result) != 2: |
+ ErrorExit("Don't modify the text starting with ---!\n\n" + result) |
+ |
+ new_description = split_result[0] |
+ cl_files_text = split_result[1] |
+ if new_description != description: |
+ change_info.description = new_description |
+ if change_info.issue: |
+ # Update the Rietveld issue with the new description. |
+ change_info.UpdateRietveldDescription() |
+ |
+ new_cl_files = [] |
+ for line in cl_files_text.splitlines(): |
+ if not len(line): |
+ continue |
+ if line.startswith("---"): |
+ break |
+ status = line[:7] |
+ file = line[7:] |
+ new_cl_files.append((status, file)) |
+ change_info.files = new_cl_files |
+ |
+ change_info.Save() |
+ print change_info.name + " changelist saved." |
+ if change_info.MissingTests(): |
+ Warn("WARNING: " + MISSING_TEST_MSG) |
+ |
+# We don't lint files in these path prefixes. |
+IGNORE_PATHS = ("webkit",) |
+ |
+# Valid extensions for files we want to lint. |
+CPP_EXTENSIONS = ("cpp", "cc", "h") |
+ |
+def Lint(change_info, args): |
+ """Runs cpplint.py on all the files in |change_info|""" |
+ try: |
+ import cpplint |
+ except ImportError: |
+ ErrorExit("You need to install cpplint.py to lint C++ files.") |
+ |
+ # Change the current working directory before calling lint so that it |
+ # shows the correct base. |
+ previous_cwd = os.getcwd() |
+ os.chdir(GetRepositoryRoot()) |
+ |
+ # Process cpplints arguments if any. |
+ filenames = cpplint.ParseArguments(args + change_info.FileList()) |
+ |
+ for file in filenames: |
+ if len([file for suffix in CPP_EXTENSIONS if file.endswith(suffix)]): |
+ if len([file for prefix in IGNORE_PATHS if file.startswith(prefix)]): |
+ print "Ignoring non-Google styled file %s" % file |
+ else: |
+ cpplint.ProcessFile(file, cpplint._cpplint_state.verbose_level) |
+ |
+ print "Total errors found: %d\n" % cpplint._cpplint_state.error_count |
+ os.chdir(previous_cwd) |
+ |
+ |
+def DoPresubmitChecks(change_info, committing): |
+ """Imports presubmit, then calls presubmit.DoPresubmitChecks.""" |
+ # Need to import here to avoid circular dependency. |
+ import presubmit |
+ result = presubmit.DoPresubmitChecks(change_info, |
+ committing, |
+ verbose=False, |
+ output_stream=sys.stdout, |
+ input_stream=sys.stdin, |
+ default_presubmit= |
+ GetCachedFile('PRESUBMIT.py', |
+ use_root=True)) |
+ if not result: |
+ print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)" |
+ return result |
+ |
+ |
+def Changes(): |
+ """Print all the changelists and their files.""" |
+ for cl in GetCLs(): |
+ change_info = LoadChangelistInfo(cl, True, True) |
+ print "\n--- Changelist " + change_info.name + ":" |
+ for file in change_info.files: |
+ print "".join(file) |
+ |
+ |
+def main(argv=None): |
+ if argv is None: |
+ argv = sys.argv |
+ |
+ if len(argv) == 1: |
+ Help() |
+ return 0; |
+ |
+ # Create the directory where we store information about changelists if it |
+ # doesn't exist. |
+ if not os.path.exists(GetInfoDir()): |
+ os.mkdir(GetInfoDir()) |
+ |
+ # Commands that don't require an argument. |
+ command = argv[1] |
+ if command == "opened": |
+ Opened() |
+ return 0 |
+ if command == "status": |
+ Opened() |
+ print "\n--- Not in any changelist:" |
+ UnknownFiles([]) |
+ return 0 |
+ if command == "nothave": |
+ UnknownFiles(argv[2:]) |
+ return 0 |
+ if command == "changes": |
+ Changes() |
+ return 0 |
+ if command == "help": |
+ Help(argv[2:]) |
+ return 0 |
+ if command == "diff" and len(argv) == 2: |
+ files = GetFilesNotInCL() |
+ print GenerateDiff([x[1] for x in files]) |
+ return 0 |
+ if command == "settings": |
+ ignore = GetCodeReviewSetting("UNKNOWN"); |
+ print CODEREVIEW_SETTINGS |
+ return 0 |
+ |
+ if len(argv) == 2: |
+ if command == "change": |
+ # Generate a random changelist name. |
+ changename = GenerateChangeName() |
+ else: |
+ ErrorExit("Need a changelist name.") |
+ else: |
+ changename = argv[2] |
+ |
+ # When the command is 'try' and --patchset is used, the patch to try |
+ # is on the Rietveld server. 'change' creates a change so it's fine if the |
+ # change didn't exist. All other commands require an existing change. |
+ fail_on_not_found = command != "try" and command != "change" |
+ if command == "try" and changename.find(',') != -1: |
+ change_info = LoadChangelistInfoForMultiple(changename, True, True) |
+ else: |
+ change_info = LoadChangelistInfo(changename, fail_on_not_found, True) |
+ |
+ if command == "change": |
+ Change(change_info) |
+ elif command == "lint": |
+ Lint(change_info, argv[3:]) |
+ elif command == "upload": |
+ UploadCL(change_info, argv[3:]) |
+ elif command == "presubmit": |
+ PresubmitCL(change_info) |
+ elif command in ("commit", "submit"): |
+ Commit(change_info, argv[3:]) |
+ elif command == "delete": |
+ change_info.Delete() |
+ elif command == "try": |
+ # When the change contains no file, send the "changename" positional |
+ # argument to trychange.py. |
+ if change_info.files: |
+ args = argv[3:] |
+ else: |
+ change_info = None |
+ args = argv[2:] |
+ TryChange(change_info, args, swallow_exception=False) |
+ else: |
+ # Everything else that is passed into gcl we redirect to svn, after adding |
+ # the files. This allows commands such as 'gcl diff xxx' to work. |
+ args =["svn", command] |
+ root = GetRepositoryRoot() |
+ args.extend([os.path.join(root, x) for x in change_info.FileList()]) |
+ RunShell(args, True) |
+ return 0 |
+ |
+ |
+if __name__ == "__main__": |
+ sys.exit(main()) |
Property changes on: depot_tools\gcl.py |
___________________________________________________________________ |
Added: svn:executable |
+ * |
Added: svn:eol-style |
+ LF |