|
|
|
#!/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
|
|
|
|
import xml.dom.minidom
|
|
|
|
|
|
|
|
|
|
|
|
__version__ = '1.0'
|
|
|
|
|
|
|
|
|
|
|
|
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!"
|
|
|
|
|
|
|
|
# Caches whether we read the codereview.settings file yet or not.
|
|
|
|
read_gcl_info = False
|
|
|
|
|
|
|
|
|
|
|
|
### Simplified XML processing functions.
|
|
|
|
|
|
|
|
def ParseXML(output):
|
|
|
|
try:
|
|
|
|
return xml.dom.minidom.parseString(output)
|
|
|
|
except xml.parsers.expat.ExpatError:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def GetNamedNodeText(node, node_name):
|
|
|
|
child_nodes = node.getElementsByTagName(node_name)
|
|
|
|
if not child_nodes:
|
|
|
|
return None
|
|
|
|
assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
|
|
|
|
return child_nodes[0].firstChild.nodeValue
|
|
|
|
|
|
|
|
|
|
|
|
def GetNodeNamedAttributeText(node, node_name, attribute_name):
|
|
|
|
child_nodes = node.getElementsByTagName(node_name)
|
|
|
|
if not child_nodes:
|
|
|
|
return None
|
|
|
|
assert len(child_nodes) == 1
|
|
|
|
return child_nodes[0].getAttribute(attribute_name)
|
|
|
|
|
|
|
|
|
|
|
|
### SVN Functions
|
|
|
|
|
|
|
|
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."""
|
|
|
|
dom = ParseXML(RunShell(["svn", "info", "--xml", file]))
|
|
|
|
result = {}
|
|
|
|
if dom:
|
|
|
|
# /info/entry/
|
|
|
|
# url
|
|
|
|
# reposityory/(root|uuid)
|
|
|
|
# wc-info/(schedule|depth)
|
|
|
|
# commit/(author|date)
|
|
|
|
result['Node Kind'] = GetNodeNamedAttributeText(dom, 'entry', 'kind')
|
|
|
|
result['Repository Root'] = GetNamedNodeText(dom, 'root')
|
|
|
|
result['Schedule'] = GetNamedNodeText(dom, 'schedule')
|
|
|
|
result['URL'] = GetNamedNodeText(dom, 'url')
|
|
|
|
result['Path'] = GetNodeNamedAttributeText(dom, 'entry', 'path')
|
|
|
|
result['Copied From URL'] = GetNamedNodeText(dom, 'copy-from-url')
|
|
|
|
result['Copied From Rev'] = GetNamedNodeText(dom, 'copy-from-rev')
|
|
|
|
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 GetSVNStatus(file):
|
|
|
|
"""Returns the svn 1.5 svn status emulated output.
|
|
|
|
|
|
|
|
@file can be a string (one file) or a list of files."""
|
|
|
|
command = ["svn", "status", "--xml"]
|
|
|
|
if file is None:
|
|
|
|
pass
|
|
|
|
elif isinstance(file, basestring):
|
|
|
|
command.append(file)
|
|
|
|
else:
|
|
|
|
command.extend(file)
|
|
|
|
|
|
|
|
status_letter = {
|
|
|
|
'': ' ',
|
|
|
|
'added': 'A',
|
|
|
|
'conflicted': 'C',
|
|
|
|
'deleted': 'D',
|
|
|
|
'ignored': 'I',
|
|
|
|
'missing': '!',
|
|
|
|
'modified': 'M',
|
|
|
|
'normal': ' ',
|
|
|
|
'replaced': 'R',
|
|
|
|
'unversioned': '?',
|
|
|
|
# TODO(maruel): Find the corresponding strings for X, ~
|
|
|
|
}
|
|
|
|
dom = ParseXML(RunShell(command))
|
|
|
|
results = []
|
|
|
|
if dom:
|
|
|
|
# /status/target/entry/(wc-status|commit|author|date)
|
|
|
|
for target in dom.getElementsByTagName('target'):
|
|
|
|
base_path = target.getAttribute('path')
|
|
|
|
for entry in target.getElementsByTagName('entry'):
|
|
|
|
file = entry.getAttribute('path')
|
|
|
|
wc_status = entry.getElementsByTagName('wc-status')
|
|
|
|
assert len(wc_status) == 1
|
|
|
|
# Emulate svn 1.5 status ouput...
|
|
|
|
statuses = [' ' for i in range(7)]
|
|
|
|
# Col 0
|
|
|
|
xml_item_status = wc_status[0].getAttribute('item')
|
|
|
|
if xml_item_status in status_letter:
|
|
|
|
statuses[0] = status_letter[xml_item_status]
|
|
|
|
else:
|
|
|
|
raise Exception('Unknown item status "%s"; please implement me!' %
|
|
|
|
xml_item_status)
|
|
|
|
# Col 1
|
|
|
|
xml_props_status = wc_status[0].getAttribute('props')
|
|
|
|
if xml_props_status == 'modified':
|
|
|
|
statuses[1] = 'M'
|
|
|
|
elif xml_props_status == 'conflicted':
|
|
|
|
statuses[1] = 'C'
|
|
|
|
elif (not xml_props_status or xml_props_status == 'none' or
|
|
|
|
xml_props_status == 'normal'):
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
raise Exception('Unknown props status "%s"; please implement me!' %
|
|
|
|
xml_props_status)
|
|
|
|
# Col 3
|
|
|
|
if wc_status[0].getAttribute('copied') == 'true':
|
|
|
|
statuses[3] = '+'
|
|
|
|
item = (''.join(statuses), file)
|
|
|
|
results.append(item)
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
return [item[1] for item in GetSVNStatus(extra_args) if item[0][0] == '?']
|
|
|
|
|
|
|
|
|
|
|
|
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 GetCodeReviewSetting(key):
|
|
|
|
"""Returns a value for the given key for this repository."""
|
|
|
|
global read_gcl_info
|
|
|
|
if not read_gcl_info:
|
|
|
|
read_gcl_info = True
|
|
|
|
# First we check if we have a cached version.
|
|
|
|
cached_settings_file = os.path.join(GetInfoDir(), CODEREVIEW_SETTINGS_FILE)
|
|
|
|
if (not os.path.exists(cached_settings_file) or
|
|
|
|
os.stat(cached_settings_file).st_mtime > 60*60*24*3):
|
|
|
|
dir_info = GetSVNFileInfo(".")
|
|
|
|
repo_root = dir_info["Repository Root"]
|
|
|
|
url_path = dir_info["URL"]
|
|
|
|
settings = ""
|
|
|
|
while True:
|
|
|
|
# Look for the codereview.settings file at the current level.
|
|
|
|
svn_path = url_path + "/" + CODEREVIEW_SETTINGS_FILE
|
|
|
|
settings, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
|
|
|
|
if not rc:
|
|
|
|
# Exit the loop if the file was found.
|
|
|
|
break
|
|
|
|
# Make sure to mark settings as empty if not found.
|
|
|
|
settings = ""
|
|
|
|
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_settings_file, settings)
|
|
|
|
|
|
|
|
output = ReadFile(cached_settings_file)
|
|
|
|
for line in output.splitlines():
|
|
|
|
if not line or line.startswith("#"):
|
|
|
|
continue
|
|
|
|
k, v = line.split(": ", 1)
|
|
|
|
CODEREVIEW_SETTINGS[k] = v
|
|
|
|
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, flags='r'):
|
|
|
|
"""Returns the contents of a file."""
|
|
|
|
file = open(filename, flags)
|
|
|
|
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_result = GetSVNStatus(filename)
|
|
|
|
if not status_result or not status_result[0][0]:
|
|
|
|
# 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_result = GetSVNStatus(None)
|
|
|
|
for line in status_result:
|
|
|
|
status = line[0]
|
|
|
|
filename = line[1]
|
|
|
|
if status[0] == "?":
|
|
|
|
continue
|
|
|
|
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 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.
|
|
|
|
version """ + __version__ + """
|
|
|
|
|
|
|
|
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 [--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)
|
|
|
|
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())
|