Commit 207fdf36 authored by maruel@chromium.org's avatar maruel@chromium.org

Convert gcl.GetSVNFileInfo() to xml format.

Add gcl's first unit test.
Add version string.
Review URL: http://codereview.chromium.org/100095

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@14771 0039d316-1c4b-4281-b951-d872f2087c98
parent 6a6c9c3b
......@@ -16,6 +16,7 @@ import sys
import tempfile
import upload
import urllib2
import xml.dom.minidom
__version__ = '1.0'
......@@ -47,6 +48,32 @@ MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
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)
......@@ -57,13 +84,21 @@ def IsSVNMoved(filename):
def GetSVNFileInfo(file):
"""Returns a dictionary from the svn info output for the given file."""
output = RunShell(["svn", "info", file])
dom = ParseXML(RunShell(["svn", "info", "--xml", 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)
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
......@@ -87,6 +122,77 @@ def GetSVNFileProperty(file, property_name):
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 = {
'': ' ',
'unversioned': '?',
'modified': 'M',
'added': 'A',
'conflicted': 'C',
'deleted': 'D',
'ignored': 'I',
'replaced': 'R',
# 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.
......@@ -418,8 +524,9 @@ def LoadChangelistInfo(changename, fail_on_not_found=True,
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.
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]:
......@@ -474,12 +581,12 @@ def GetModifiedFiles():
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] == "?":
status_result = GetSVNStatus(None)
for line in status_result:
status = line[0]
filename = line[1]
if status[0] == "?":
continue
status = line[:7]
filename = line[7:].strip()
if dir_prefix:
filename = os.path.join(dir_prefix, filename)
change_list_name = ""
......@@ -530,30 +637,6 @@ def GetIssueDescription(issue):
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()
......
......@@ -134,7 +134,7 @@ def Revert(revisions, force=False, commit=True, send_email=True, message=None,
print ""
# Make sure these files are unmodified with svn status.
status = gcl.RunShell(["svn", "status"] + files)
status = gcl.GetSVNStatus(files)
if status:
if force:
# TODO(maruel): Use the tool to correctly revert '?' files.
......
#!/usr/bin/python
# Copyright (c) 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.
"""Unit tests for gcl.py."""
import os
import unittest
# Local imports
import gcl
class GclTestsBase(unittest.TestCase):
"""Setups and tear downs the mocks but doesn't test anything as-is."""
def setUp(self):
def RunShellMock(filename):
return filename
self._RunShell = gcl.RunShell
gcl.RunShell = RunShellMock
def tearDown(self):
gcl.RunShell = self._RunShell
def compareMembers(self, object, members):
"""If you add a member, be sure to add the relevant test!"""
# Skip over members starting with '_' since they are usually not meant to
# be for public use.
actual_members = [x for x in sorted(dir(object))
if not x.startswith('_')]
expected_members = sorted(members)
if actual_members != expected_members:
diff = ([i for i in actual_members if i not in expected_members] +
[i for i in expected_members if i not in actual_members])
print diff
self.assertEqual(actual_members, expected_members)
class GclUnittest(GclTestsBase):
"""General gcl.py tests."""
def testMembersChanged(self):
members = [
'CODEREVIEW_SETTINGS', 'CODEREVIEW_SETTINGS_FILE', 'CPP_EXTENSIONS',
'Change', 'ChangeInfo', 'Changes', 'Commit', 'DoPresubmitChecks',
'ErrorExit', 'GenerateChangeName', 'GenerateDiff', 'GetCLs',
'GetChangelistInfoFile', 'GetCodeReviewSetting', 'GetEditor',
'GetFilesNotInCL', 'GetInfoDir', 'GetIssueDescription',
'GetModifiedFiles', 'GetNamedNodeText', 'GetNodeNamedAttributeText',
'GetRepositoryRoot', 'GetSVNFileInfo', 'GetSVNStatus',
'GetSVNFileProperty', 'Help', 'IGNORE_PATHS', 'IsSVNMoved', 'IsTreeOpen',
'Lint', 'LoadChangelistInfo', 'LoadChangelistInfoForMultiple',
'MISSING_TEST_MSG', 'Opened', 'ParseXML', 'PresubmitCL', 'ReadFile',
'RunShell',
'RunShellWithReturnCode', 'SEPARATOR', 'SendToRietveld', 'TryChange',
'UnknownFiles', 'UploadCL', 'Warn', 'WriteFile', 'gcl_info_dir',
'getpass', 'main', 'os', 'random', 're', 'read_gcl_info',
'repository_root', 'string', 'subprocess', 'sys', 'tempfile', 'upload',
'urllib2', 'use_shell', 'xml',
]
# If this test fails, you should add the relevant test.
self.compareMembers(gcl, members)
def testGetSVNFileInfo(self):
def RunShellMock(command):
return r"""<?xml version="1.0"?>
<info>
<entry kind="file" path="%s" revision="14628">
<url>http://src.chromium.org/svn/trunk/src/chrome/app/d</url>
<repository><root>http://src.chromium.org/svn</root></repository>
<wc-info>
<schedule>add</schedule>
<depth>infinity</depth>
<copy-from-url>http://src.chromium.org/svn/trunk/src/chrome/app/DEPS</copy-from-url>
<copy-from-rev>14628</copy-from-rev>
<checksum>369f59057ba0e6d9017e28f8bdfb1f43</checksum>
</wc-info>
</entry>
</info>
""" % command[3]
gcl.RunShell = RunShellMock
filename = os.path.join('app', 'd')
info = gcl.GetSVNFileInfo(filename)
expected = {
'URL': 'http://src.chromium.org/svn/trunk/src/chrome/app/d',
'Repository Root': 'http://src.chromium.org/svn',
'Schedule': 'add',
'Copied From URL': 'http://src.chromium.org/svn/trunk/src/chrome/app/DEPS',
'Copied From Rev': '14628',
'Path': filename,
'Node Kind': 'file',
}
self.assertEquals(sorted(info.items()), sorted(expected.items()))
def testGetSVNStatus(self):
def RunShellMock(command):
return r"""<?xml version="1.0"?>
<status>
<target path=".">
<entry path="unversionned_file.txt">
<wc-status props="none" item="unversioned"></wc-status>
</entry>
<entry path="build\internal\essential.vsprops">
<wc-status props="normal" item="modified" revision="14628">
<commit revision="13818">
<author>ajwong@chromium.org</author>
<date>2009-04-16T00:42:06.872358Z</date>
</commit>
</wc-status>
</entry>
<entry path="chrome\app\d">
<wc-status props="none" copied="true" tree-conflicted="true" item="added">
</wc-status>
</entry>
<entry path="chrome\app\DEPS">
<wc-status props="modified" item="modified" revision="14628">
<commit revision="1279">
<author>brettw@google.com</author>
<date>2008-08-23T17:16:42.090152Z</date>
</commit>
</wc-status>
</entry>
<entry path="scripts\master\factory\gclient_factory.py">
<wc-status props="normal" item="conflicted" revision="14725">
<commit revision="14633">
<author>nsylvain@chromium.org</author>
<date>2009-04-27T19:37:17.977400Z</date>
</commit>
</wc-status>
</entry>
</target>
</status>
"""
gcl.RunShell = RunShellMock
info = gcl.GetSVNStatus('.')
expected = [
('? ', 'unversionned_file.txt'),
('M ', 'build\\internal\\essential.vsprops'),
('A + ', 'chrome\\app\\d'),
('MM ', 'chrome\\app\\DEPS'),
('C ', 'scripts\\master\\factory\\gclient_factory.py'),
]
self.assertEquals(sorted(info), sorted(expected))
def testGetSVNStatusEmpty(self):
def RunShellMock(command):
return r"""<?xml version="1.0"?>
<status>
<target
path="perf">
</target>
</status>
"""
gcl.RunShell = RunShellMock
info = gcl.GetSVNStatus(None)
self.assertEquals(info, [])
if __name__ == '__main__':
unittest.main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment