Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(281)

Unified Diff: depot_tools/gcl.py

Issue 92087: Create the Next Generation of depot_tools. Eh. (Closed) Base URL: svn://chrome-svn.corp.google.com/chrome/trunk/tools/
Patch Set: Created 11 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « depot_tools/gcl.bat ('k') | depot_tools/gclient » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « depot_tools/gcl.bat ('k') | depot_tools/gclient » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698