summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/common
diff options
context:
space:
mode:
authorBen Murdoch <benm@google.com>2010-05-11 18:35:50 +0100
committerBen Murdoch <benm@google.com>2010-05-14 10:23:05 +0100
commit21939df44de1705786c545cd1bf519d47250322d (patch)
treeef56c310f5c0cdc379c2abb2e212308a3281ce20 /WebKitTools/Scripts/webkitpy/common
parent4ff1d8891d520763f17675827154340c7c740f90 (diff)
downloadexternal_webkit-21939df44de1705786c545cd1bf519d47250322d.zip
external_webkit-21939df44de1705786c545cd1bf519d47250322d.tar.gz
external_webkit-21939df44de1705786c545cd1bf519d47250322d.tar.bz2
Merge Webkit at r58956: Initial merge by Git.
Change-Id: I1d9fb60ea2c3f2ddc04c17a871acdb39353be228
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/common')
-rw-r--r--WebKitTools/Scripts/webkitpy/common/array_stream.py66
-rw-r--r--WebKitTools/Scripts/webkitpy/common/array_stream_unittest.py78
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/api.py26
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py44
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/changelog.py4
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/changelog_unittest.py37
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/commitinfo.py2
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/scm.py199
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py367
-rw-r--r--WebKitTools/Scripts/webkitpy/common/config/committers.py10
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/bugzilla.py53
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/buildbot.py21
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py4
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/rietveld.py30
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/statusserver.py6
-rw-r--r--WebKitTools/Scripts/webkitpy/common/prettypatch.py13
-rw-r--r--WebKitTools/Scripts/webkitpy/common/prettypatch_unittest.py70
-rwxr-xr-xWebKitTools/Scripts/webkitpy/common/system/autoinstall.py27
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/deprecated_logging.py12
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py1
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/executive.py112
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py57
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/user.py2
23 files changed, 998 insertions, 243 deletions
diff --git a/WebKitTools/Scripts/webkitpy/common/array_stream.py b/WebKitTools/Scripts/webkitpy/common/array_stream.py
new file mode 100644
index 0000000..e425d02
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/common/array_stream.py
@@ -0,0 +1,66 @@
+#!/usr/bin/python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Package that private an array-based implementation of a stream."""
+
+
+class ArrayStream(object):
+ """Simple class that implmements a stream interface on top of an array.
+
+ This is used primarily by unit test classes to mock output streams. It
+ performs a similar function to StringIO, but (a) it is write-only, and
+ (b) it can be used to retrieve each individual write(); StringIO
+ concatenates all of the writes together.
+ """
+
+ def __init__(self):
+ self._contents = []
+
+ def write(self, msg):
+ """Implement stream.write() by appending to the stream's contents."""
+ self._contents.append(msg)
+
+ def get(self):
+ """Return the contents of a stream (as an array)."""
+ return self._contents
+
+ def reset(self):
+ """Empty the stream."""
+ self._contents = []
+
+ def empty(self):
+ """Return whether the stream is empty."""
+ return (len(self._contents) == 0)
+
+ def flush(self):
+ """Flush the stream (a no-op implemented for compatibility)."""
+ pass
+
+ def __repr__(self):
+ return '<ArrayStream: ' + str(self._contents) + '>'
diff --git a/WebKitTools/Scripts/webkitpy/common/array_stream_unittest.py b/WebKitTools/Scripts/webkitpy/common/array_stream_unittest.py
new file mode 100644
index 0000000..1a9b34a
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/common/array_stream_unittest.py
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Unit tests for array_stream.py."""
+
+import pdb
+import unittest
+
+from webkitpy.common.array_stream import ArrayStream
+
+
+class ArrayStreamTest(unittest.TestCase):
+ def assertEmpty(self, a_stream):
+ self.assertTrue(a_stream.empty())
+
+ def assertNotEmpty(self, a_stream):
+ self.assertFalse(a_stream.empty())
+
+ def assertContentsMatch(self, a_stream, contents):
+ self.assertEquals(a_stream.get(), contents)
+
+ def test_basics(self):
+ a = ArrayStream()
+ self.assertEmpty(a)
+ self.assertContentsMatch(a, [])
+
+ a.flush()
+ self.assertEmpty(a)
+ self.assertContentsMatch(a, [])
+
+ a.write("foo")
+ a.write("bar")
+ self.assertNotEmpty(a)
+ self.assertContentsMatch(a, ["foo", "bar"])
+
+ a.flush()
+ self.assertNotEmpty(a)
+ self.assertContentsMatch(a, ["foo", "bar"])
+
+ a.reset()
+ self.assertEmpty(a)
+ self.assertContentsMatch(a, [])
+
+ self.assertEquals(str(a), "<ArrayStream: []>")
+
+ a.write("foo")
+ self.assertNotEmpty(a)
+ self.assertContentsMatch(a, ["foo"])
+ self.assertEquals(str(a), "<ArrayStream: ['foo']>")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/api.py b/WebKitTools/Scripts/webkitpy/common/checkout/api.py
index c4e2b69..a5ac939 100644
--- a/WebKitTools/Scripts/webkitpy/common/checkout/api.py
+++ b/WebKitTools/Scripts/webkitpy/common/checkout/api.py
@@ -27,7 +27,6 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
-import subprocess
import StringIO
from webkitpy.common.checkout.changelog import ChangeLog
@@ -50,7 +49,11 @@ class Checkout(object):
def _latest_entry_for_changelog_at_revision(self, changelog_path, revision):
changelog_contents = self._scm.contents_at_revision(changelog_path, revision)
- return ChangeLog.parse_latest_entry_from_file(StringIO.StringIO(changelog_contents))
+ # contents_at_revision returns a byte array (str()), but we know
+ # that ChangeLog files are utf-8. parse_latest_entry_from_file
+ # expects a file-like object which vends unicode(), so we decode here.
+ changelog_file = StringIO.StringIO(changelog_contents.decode("utf-8"))
+ return ChangeLog.parse_latest_entry_from_file(changelog_file)
def changelog_entries_for_revision(self, revision):
changed_files = self._scm.changed_files_for_revision(revision)
@@ -80,16 +83,16 @@ class Checkout(object):
def bug_id_for_revision(self, revision):
return self.commit_info_for_revision(revision).bug_id()
- def modified_changelogs(self):
+ def modified_changelogs(self, git_commit, squash):
# SCM returns paths relative to scm.checkout_root
# Callers (especially those using the ChangeLog class) may
# expect absolute paths, so this method returns absolute paths.
- changed_files = self._scm.changed_files()
+ changed_files = self._scm.changed_files(git_commit, squash)
absolute_paths = [os.path.join(self._scm.checkout_root, path) for path in changed_files]
return [path for path in absolute_paths if self._is_path_to_changelog(path)]
- def commit_message_for_this_commit(self):
- changelog_paths = self.modified_changelogs()
+ def commit_message_for_this_commit(self, git_commit, squash):
+ changelog_paths = self.modified_changelogs(git_commit, squash)
if not len(changelog_paths):
raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n"
"All changes require a ChangeLog. See:\n"
@@ -106,32 +109,29 @@ class Checkout(object):
# FIXME: We should sort and label the ChangeLog messages like commit-log-editor does.
return CommitMessage("".join(changelog_messages).splitlines())
- def bug_id_for_this_commit(self):
+ def bug_id_for_this_commit(self, git_commit, squash):
try:
- return parse_bug_id(self.commit_message_for_this_commit().message())
+ return parse_bug_id(self.commit_message_for_this_commit(git_commit, squash).message())
except ScriptError, e:
pass # We might not have ChangeLogs.
def apply_patch(self, patch, force=False):
# It's possible that the patch was not made from the root directory.
# We should detect and handle that case.
- # FIXME: Use Executive instead of subprocess here.
- curl_process = subprocess.Popen(['curl', '--location', '--silent', '--show-error', patch.url()], stdout=subprocess.PIPE)
# FIXME: Move _scm.script_path here once we get rid of all the dependencies.
args = [self._scm.script_path('svn-apply')]
if patch.reviewer():
args += ['--reviewer', patch.reviewer().full_name]
if force:
args.append('--force')
-
- run_command(args, input=curl_process.stdout)
+ run_command(args, input=patch.contents())
def apply_reverse_diff(self, revision):
self._scm.apply_reverse_diff(revision)
# We revert the ChangeLogs because removing lines from a ChangeLog
# doesn't make sense. ChangeLogs are append only.
- changelog_paths = self.modified_changelogs()
+ changelog_paths = self.modified_changelogs(git_commit=None, squash=False)
if len(changelog_paths):
self._scm.revert_files(changelog_paths)
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py b/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py
index e99caee..1436379 100644
--- a/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py
@@ -26,6 +26,9 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+from __future__ import with_statement
+
+import codecs
import os
import shutil
import tempfile
@@ -37,14 +40,14 @@ from webkitpy.common.checkout.scm import detect_scm_system, CommitMessage
from webkitpy.common.system.outputcapture import OutputCapture
from webkitpy.thirdparty.mock import Mock
+
# FIXME: Copied from scm_unittest.py
-def write_into_file_at_path(file_path, contents):
- new_file = open(file_path, 'w')
- new_file.write(contents)
- new_file.close()
+def write_into_file_at_path(file_path, contents, encoding="utf-8"):
+ with codecs.open(file_path, "w", encoding) as file:
+ file.write(contents)
-_changelog1entry1 = """2010-03-25 Eric Seidel <eric@webkit.org>
+_changelog1entry1 = u"""2010-03-25 Tor Arne Vestb\u00f8 <vestbo@webkit.org>
Unreviewed build fix to un-break webkit-patch land.
@@ -53,7 +56,7 @@ _changelog1entry1 = """2010-03-25 Eric Seidel <eric@webkit.org>
* Scripts/webkitpy/common/checkout/api.py: import scm.CommitMessage
"""
-_changelog1entry2 = """2010-03-25 Adam Barth <abarth@webkit.org>
+_changelog1entry2 = u"""2010-03-25 Adam Barth <abarth@webkit.org>
Reviewed by Eric Seidel.
@@ -62,8 +65,8 @@ _changelog1entry2 = """2010-03-25 Adam Barth <abarth@webkit.org>
* Scripts/webkitpy/common/checkout/api.py:
"""
-_changelog1 = "\n".join([_changelog1entry1, _changelog1entry2])
-_changelog2 = """2010-03-25 Eric Seidel <eric@webkit.org>
+_changelog1 = u"\n".join([_changelog1entry1, _changelog1entry2])
+_changelog2 = u"""2010-03-25 Tor Arne Vestb\u00f8 <vestbo@webkit.org>
Unreviewed build fix to un-break webkit-patch land.
@@ -79,7 +82,7 @@ _changelog2 = """2010-03-25 Eric Seidel <eric@webkit.org>
"""
class CommitMessageForThisCommitTest(unittest.TestCase):
- expected_commit_message = """2010-03-25 Eric Seidel <eric@webkit.org>
+ expected_commit_message = u"""2010-03-25 Tor Arne Vestb\u00f8 <vestbo@webkit.org>
Unreviewed build fix to un-break webkit-patch land.
@@ -87,7 +90,7 @@ class CommitMessageForThisCommitTest(unittest.TestCase):
https://bugs.webkit.org/show_bug.cgi?id=36629
* Scripts/webkitpy/common/checkout/api.py: import scm.CommitMessage
-2010-03-25 Eric Seidel <eric@webkit.org>
+2010-03-25 Tor Arne Vestb\u00f8 <vestbo@webkit.org>
Unreviewed build fix to un-break webkit-patch land.
@@ -111,10 +114,11 @@ class CommitMessageForThisCommitTest(unittest.TestCase):
# ChangeLog is difficult to mock at current.
def test_commit_message_for_this_commit(self):
checkout = Checkout(None)
- checkout.modified_changelogs = lambda: ["ChangeLog1", "ChangeLog2"]
+ checkout.modified_changelogs = lambda git_commit, squash: ["ChangeLog1", "ChangeLog2"]
output = OutputCapture()
expected_stderr = "Parsing ChangeLog: ChangeLog1\nParsing ChangeLog: ChangeLog2\n"
- commit_message = output.assert_outputs(self, checkout.commit_message_for_this_commit, expected_stderr=expected_stderr)
+ commit_message = output.assert_outputs(self, checkout.commit_message_for_this_commit,
+ kwargs={"git_commit": None, "squash": False}, expected_stderr=expected_stderr)
self.assertEqual(commit_message.message(), self.expected_commit_message)
@@ -124,7 +128,9 @@ class CheckoutTest(unittest.TestCase):
def mock_contents_at_revision(changelog_path, revision):
self.assertEqual(changelog_path, "foo")
self.assertEqual(revision, "bar")
- return _changelog1
+ # contents_at_revision is expected to return a byte array (str)
+ # so we encode our unicode ChangeLog down to a utf-8 stream.
+ return _changelog1.encode("utf-8")
scm.contents_at_revision = mock_contents_at_revision
checkout = Checkout(scm)
entry = checkout._latest_entry_for_changelog_at_revision("foo", "bar")
@@ -137,8 +143,8 @@ class CheckoutTest(unittest.TestCase):
checkout.changelog_entries_for_revision = lambda revision: [ChangeLogEntry(_changelog1entry1)]
commitinfo = checkout.commit_info_for_revision(4)
self.assertEqual(commitinfo.bug_id(), 36629)
- self.assertEqual(commitinfo.author_name(), "Eric Seidel")
- self.assertEqual(commitinfo.author_email(), "eric@webkit.org")
+ self.assertEqual(commitinfo.author_name(), u"Tor Arne Vestb\u00f8")
+ self.assertEqual(commitinfo.author_email(), "vestbo@webkit.org")
self.assertEqual(commitinfo.reviewer_text(), None)
self.assertEqual(commitinfo.reviewer(), None)
self.assertEqual(commitinfo.committer_email(), "committer@example.com")
@@ -157,13 +163,13 @@ class CheckoutTest(unittest.TestCase):
def test_bug_id_for_this_commit(self):
scm = Mock()
checkout = Checkout(scm)
- checkout.commit_message_for_this_commit = lambda: CommitMessage(ChangeLogEntry(_changelog1entry1).contents().splitlines())
- self.assertEqual(checkout.bug_id_for_this_commit(), 36629)
+ checkout.commit_message_for_this_commit = lambda git_commit, squash: CommitMessage(ChangeLogEntry(_changelog1entry1).contents().splitlines())
+ self.assertEqual(checkout.bug_id_for_this_commit(git_commit=None, squash=False), 36629)
def test_modified_changelogs(self):
scm = Mock()
scm.checkout_root = "/foo/bar"
- scm.changed_files = lambda:["file1", "ChangeLog", "relative/path/ChangeLog"]
+ scm.changed_files = lambda git_commit, squash: ["file1", "ChangeLog", "relative/path/ChangeLog"]
checkout = Checkout(scm)
expected_changlogs = ["/foo/bar/ChangeLog", "/foo/bar/relative/path/ChangeLog"]
- self.assertEqual(checkout.modified_changelogs(), expected_changlogs)
+ self.assertEqual(checkout.modified_changelogs(git_commit=None, squash=False), expected_changlogs)
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/changelog.py b/WebKitTools/Scripts/webkitpy/common/checkout/changelog.py
index e93896f..6220fbd 100644
--- a/WebKitTools/Scripts/webkitpy/common/checkout/changelog.py
+++ b/WebKitTools/Scripts/webkitpy/common/checkout/changelog.py
@@ -99,10 +99,14 @@ class ChangeLog(object):
@staticmethod
def parse_latest_entry_from_file(changelog_file):
+ """changelog_file must be a file-like object which returns
+ unicode strings. Use codecs.open or StringIO(unicode())
+ to pass file objects to this class."""
date_line_regexp = re.compile(ChangeLogEntry.date_line_regexp)
entry_lines = []
# The first line should be a date line.
first_line = changelog_file.readline()
+ assert(isinstance(first_line, unicode))
if not date_line_regexp.match(first_line):
return None
entry_lines.append(first_line)
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/changelog_unittest.py b/WebKitTools/Scripts/webkitpy/common/checkout/changelog_unittest.py
index 9210c9c..864428a 100644
--- a/WebKitTools/Scripts/webkitpy/common/checkout/changelog_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/checkout/changelog_unittest.py
@@ -26,9 +26,12 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-import unittest
+from __future__ import with_statement
+
+import codecs
import os
import tempfile
+import unittest
from StringIO import StringIO
@@ -52,7 +55,7 @@ class ChangeLogsTest(unittest.TestCase):
'''
# More example text than we need. Eventually we need to support parsing this all and write tests for the parsing.
- _example_changelog = '''2009-08-17 David Kilzer <ddkilzer@apple.com>
+ _example_changelog = u"""2009-08-17 Tor Arne Vestb\xf8 <vestbo@webkit.org>
<http://webkit.org/b/28393> check-webkit-style: add check for use of std::max()/std::min() instead of MAX()/MIN()
@@ -84,10 +87,10 @@ class ChangeLogsTest(unittest.TestCase):
so we can't assert here.
== Rolled over to ChangeLog-2009-06-16 ==
-'''
+"""
def test_latest_entry_parse(self):
- changelog_contents = "%s\n%s" % (self._example_entry, self._example_changelog)
+ changelog_contents = u"%s\n%s" % (self._example_entry, self._example_changelog)
changelog_file = StringIO(changelog_contents)
latest_entry = ChangeLog.parse_latest_entry_from_file(changelog_file)
self.assertEquals(latest_entry.contents(), self._example_entry)
@@ -97,19 +100,17 @@ class ChangeLogsTest(unittest.TestCase):
self.assertTrue(latest_entry.reviewer()) # Make sure that our UTF8-based lookup of Tor works.
@staticmethod
- def _write_tmp_file_with_contents(contents):
+ def _write_tmp_file_with_contents(byte_array):
+ assert(isinstance(byte_array, str))
(file_descriptor, file_path) = tempfile.mkstemp() # NamedTemporaryFile always deletes the file on close in python < 2.6
- file = os.fdopen(file_descriptor, 'w')
- file.write(contents)
- file.close()
+ with os.fdopen(file_descriptor, "w") as file:
+ file.write(byte_array)
return file_path
@staticmethod
- def _read_file_contents(file_path):
- file = open(file_path)
- contents = file.read()
- file.close()
- return contents
+ def _read_file_contents(file_path, encoding):
+ with codecs.open(file_path, "r", encoding) as file:
+ return file.read()
_new_entry_boilerplate = '''2009-08-19 Eric Seidel <eric@webkit.org>
@@ -121,11 +122,11 @@ class ChangeLogsTest(unittest.TestCase):
'''
def test_set_reviewer(self):
- changelog_contents = "%s\n%s" % (self._new_entry_boilerplate, self._example_changelog)
- changelog_path = self._write_tmp_file_with_contents(changelog_contents)
+ changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate, self._example_changelog)
+ changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8"))
reviewer_name = 'Test Reviewer'
ChangeLog(changelog_path).set_reviewer(reviewer_name)
- actual_contents = self._read_file_contents(changelog_path)
+ actual_contents = self._read_file_contents(changelog_path, "utf-8")
expected_contents = changelog_contents.replace('NOBODY (OOPS!)', reviewer_name)
os.remove(changelog_path)
self.assertEquals(actual_contents, expected_contents)
@@ -169,8 +170,8 @@ class ChangeLogsTest(unittest.TestCase):
'''
def _assert_update_for_revert_output(self, args, expected_entry):
- changelog_contents = "%s\n%s" % (self._new_entry_boilerplate, self._example_changelog)
- changelog_path = self._write_tmp_file_with_contents(changelog_contents)
+ changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate, self._example_changelog)
+ changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8"))
changelog = ChangeLog(changelog_path)
changelog.update_for_revert(*args)
actual_entry = changelog.latest_entry()
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/commitinfo.py b/WebKitTools/Scripts/webkitpy/common/checkout/commitinfo.py
index 7c3315f..448d530 100644
--- a/WebKitTools/Scripts/webkitpy/common/checkout/commitinfo.py
+++ b/WebKitTools/Scripts/webkitpy/common/checkout/commitinfo.py
@@ -28,8 +28,6 @@
#
# WebKit's python module for holding information on a commit
-import StringIO
-
from webkitpy.common.checkout.changelog import view_source_url
from webkitpy.common.config.committers import CommitterList
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py
index 2704f07..02e114a 100644
--- a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py
+++ b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py
@@ -41,11 +41,13 @@ from webkitpy.common.system.deprecated_logging import error, log
def detect_scm_system(path):
- if SVN.in_working_directory(path):
- return SVN(cwd=path)
+ absolute_path = os.path.abspath(path)
+
+ if SVN.in_working_directory(absolute_path):
+ return SVN(cwd=absolute_path)
- if Git.in_working_directory(path):
- return Git(cwd=path)
+ if Git.in_working_directory(absolute_path):
+ return Git(cwd=absolute_path)
return None
@@ -145,7 +147,7 @@ class SCM:
return filenames
def strip_r_from_svn_revision(self, svn_revision):
- match = re.match("^r(?P<svn_revision>\d+)", svn_revision)
+ match = re.match("^r(?P<svn_revision>\d+)", unicode(svn_revision))
if (match):
return match.group('svn_revision')
return svn_revision
@@ -178,7 +180,7 @@ class SCM:
def add(self, path):
raise NotImplementedError, "subclasses must implement"
- def changed_files(self):
+ def changed_files(self, git_commit=None, squash=None):
raise NotImplementedError, "subclasses must implement"
def changed_files_for_revision(self):
@@ -193,7 +195,7 @@ class SCM:
def display_name(self):
raise NotImplementedError, "subclasses must implement"
- def create_patch(self):
+ def create_patch(self, git_commit=None, squash=None):
raise NotImplementedError, "subclasses must implement"
def committer_email_for_revision(self, revision):
@@ -211,7 +213,10 @@ class SCM:
def revert_files(self, file_paths):
raise NotImplementedError, "subclasses must implement"
- def commit_with_message(self, message, username=None):
+ def should_squash(self, squash):
+ raise NotImplementedError, "subclasses must implement"
+
+ def commit_with_message(self, message, username=None, git_commit=None, squash=None):
raise NotImplementedError, "subclasses must implement"
def svn_commit_log(self, svn_revision):
@@ -229,12 +234,6 @@ class SCM:
def svn_merge_base():
raise NotImplementedError, "subclasses must implement"
- def create_patch_from_local_commit(self, commit_id):
- error("Your source control manager does not support creating a patch from a local commit.")
-
- def create_patch_since_local_commit(self, commit_id):
- error("Your source control manager does not support creating a patch from a local commit.")
-
def commit_locally_with_message(self, message):
error("Your source control manager does not support local commits.")
@@ -308,7 +307,7 @@ class SVN(SCM):
return self.cached_version
def working_directory_is_clean(self):
- return run_command(["svn", "diff"], cwd=self.checkout_root) == ""
+ return run_command(["svn", "diff"], cwd=self.checkout_root, decode_output=False) == ""
def clean_working_directory(self):
# svn revert -R is not as awesome as git reset --hard.
@@ -339,12 +338,13 @@ class SVN(SCM):
# path is assumed to be cwd relative?
run_command(["svn", "add", path])
- def changed_files(self):
+ def changed_files(self, git_commit=None, squash=None):
return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("ACDMR"))
def changed_files_for_revision(self, revision):
# As far as I can tell svn diff --summarize output looks just like svn status output.
- status_command = ["svn", "diff", "--summarize", "-c", str(revision)]
+ # No file contents printed, thus utf-8 auto-decoding in run_command is fine.
+ status_command = ["svn", "diff", "--summarize", "-c", revision]
return self.run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR"))
def conflicted_files(self):
@@ -360,19 +360,26 @@ class SVN(SCM):
def display_name(self):
return "svn"
- def create_patch(self):
- return run_command(self.script_path("svn-create-patch"), cwd=self.checkout_root, return_stderr=False)
+ def create_patch(self, git_commit=None, squash=None):
+ """Returns a byte array (str()) representing the patch file.
+ Patch files are effectively binary since they may contain
+ files of multiple different encodings."""
+ return run_command([self.script_path("svn-create-patch")],
+ cwd=self.checkout_root, return_stderr=False,
+ decode_output=False)
def committer_email_for_revision(self, revision):
- return run_command(["svn", "propget", "svn:author", "--revprop", "-r", str(revision)]).rstrip()
+ return run_command(["svn", "propget", "svn:author", "--revprop", "-r", revision]).rstrip()
def contents_at_revision(self, path, revision):
+ """Returns a byte array (str()) containing the contents
+ of path @ revision in the repository."""
remote_path = "%s/%s" % (self._repository_url(), path)
- return run_command(["svn", "cat", "-r", str(revision), remote_path])
+ return run_command(["svn", "cat", "-r", revision, remote_path], decode_output=False)
def diff_for_revision(self, revision):
# FIXME: This should probably use cwd=self.checkout_root
- return run_command(['svn', 'diff', '-c', str(revision)])
+ return run_command(['svn', 'diff', '-c', revision])
def _repository_url(self):
return self.value_from_svn_info(self.checkout_root, 'URL')
@@ -389,7 +396,12 @@ class SVN(SCM):
# FIXME: This should probably use cwd=self.checkout_root.
run_command(['svn', 'revert'] + file_paths)
- def commit_with_message(self, message, username=None):
+ def should_squash(self, squash):
+ # SVN doesn't support the concept of squashing.
+ return False
+
+ def commit_with_message(self, message, username=None, git_commit=None, squash=None):
+ # squash and git-commit are not used by SVN.
if self.dryrun:
# Return a string which looks like a commit so that things which parse this output will succeed.
return "Dry run, no commit.\nCommitted revision 0."
@@ -405,7 +417,7 @@ class SVN(SCM):
return run_command(svn_commit_args, error_handler=commit_error_handler)
def svn_commit_log(self, svn_revision):
- svn_revision = self.strip_r_from_svn_revision(str(svn_revision))
+ svn_revision = self.strip_r_from_svn_revision(svn_revision)
return run_command(['svn', 'log', '--non-interactive', '--revision', svn_revision]);
def last_svn_commit_log(self):
@@ -466,6 +478,7 @@ class Git(SCM):
def status_command(self):
# git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.
+ # No file contents printed, thus utf-8 autodecoding in run_command is fine.
return ["git", "diff", "--name-status", "HEAD"]
def _status_regexp(self, expected_types):
@@ -475,8 +488,24 @@ class Git(SCM):
# path is assumed to be cwd relative?
run_command(["git", "add", path])
- def changed_files(self):
- status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', 'HEAD']
+ def _merge_base(self, git_commit, squash):
+ if git_commit:
+ # FIXME: Calling code should turn commit ranges into a list of commit IDs
+ # and then treat each commit separately.
+ if '..' not in git_commit:
+ git_commit = git_commit + "^.." + git_commit
+ return git_commit
+
+ if self.should_squash(squash):
+ return self.svn_merge_base()
+
+ # FIXME: Non-squash behavior should match commit_with_message. It raises an error
+ # if there are working copy changes and --squash or --no-squash wasn't passed in.
+ # If --no-squash, then it should proceed with each local commit as a separate patch.
+ return 'HEAD'
+
+ def changed_files(self, git_commit=None, squash=None):
+ status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', "--no-ext-diff", "--full-index", self._merge_base(git_commit, squash)]
return self.run_status_and_extract_filenames(status_command, self._status_regexp("ADM"))
def _changes_files_for_commit(self, git_commit):
@@ -490,6 +519,8 @@ class Git(SCM):
return self._changes_files_for_commit(commit_id)
def conflicted_files(self):
+ # We do not need to pass decode_output for this diff command
+ # as we're passing --name-status which does not output any data.
status_command = ['git', 'diff', '--name-status', '-C', '-M', '--diff-filter=U']
return self.run_status_and_extract_filenames(status_command, self._status_regexp("U"))
@@ -503,9 +534,12 @@ class Git(SCM):
def display_name(self):
return "git"
- def create_patch(self):
+ def create_patch(self, git_commit=None, squash=None):
+ """Returns a byte array (str()) representing the patch file.
+ Patch files are effectively binary since they may contain
+ files of multiple different encodings."""
# FIXME: This should probably use cwd=self.checkout_root
- return run_command(['git', 'diff', '--binary', 'HEAD'])
+ return run_command(['git', 'diff', '--binary', "--no-ext-diff", "--full-index", "-M", self._merge_base(git_commit, squash)], decode_output=False)
@classmethod
def git_commit_from_svn_revision(cls, revision):
@@ -517,11 +551,13 @@ class Git(SCM):
return git_commit
def contents_at_revision(self, path, revision):
- return run_command(["git", "show", "%s:%s" % (self.git_commit_from_svn_revision(revision), path)])
+ """Returns a byte array (str()) containing the contents
+ of path @ revision in the repository."""
+ return run_command(["git", "show", "%s:%s" % (self.git_commit_from_svn_revision(revision), path)], decode_output=False)
def diff_for_revision(self, revision):
git_commit = self.git_commit_from_svn_revision(revision)
- return self.create_patch_from_local_commit(git_commit)
+ return self.create_patch(git_commit)
def committer_email_for_revision(self, revision):
git_commit = self.git_commit_from_svn_revision(revision)
@@ -538,11 +574,100 @@ class Git(SCM):
def revert_files(self, file_paths):
run_command(['git', 'checkout', 'HEAD'] + file_paths)
- def commit_with_message(self, message, username=None):
+ def should_squash(self, squash):
+ if squash is not None:
+ # Squash is specified on the command-line.
+ return squash
+
+ config_squash = Git.read_git_config('webkit-patch.squash')
+ if (config_squash and config_squash is not ""):
+ return config_squash.lower() == "true"
+
+ # Only raise an error if there are actually multiple commits to squash.
+ num_local_commits = len(self.local_commits())
+ if num_local_commits > 1 or num_local_commits > 0 and not self.working_directory_is_clean():
+ working_directory_message = "" if self.working_directory_is_clean() else " and working copy changes"
+ raise ScriptError(message="""There are %s local commits%s. Do one of the following:
+1) Use --squash or --no-squash
+2) git config webkit-patch.squash true/false
+""" % (num_local_commits, working_directory_message))
+
+ return None
+
+ def commit_with_message(self, message, username=None, git_commit=None, squash=None):
# Username is ignored during Git commits.
- self.commit_locally_with_message(message)
+ if git_commit:
+ # Need working directory changes to be committed so we can checkout the merge branch.
+ if not self.working_directory_is_clean():
+ # FIXME: webkit-patch land will modify the ChangeLogs to correct the reviewer.
+ # That will modify the working-copy and cause us to hit this error.
+ # The ChangeLog modification could be made to modify the existing local commit?
+ raise ScriptError(message="Working copy is modified. Cannot commit individual git_commits.")
+ return self._commit_on_branch(message, git_commit)
+
+ squash = self.should_squash(squash)
+ if squash:
+ run_command(['git', 'reset', '--soft', self.svn_branch_name()])
+ self.commit_locally_with_message(message)
+ elif not self.working_directory_is_clean():
+ if not len(self.local_commits()):
+ # There are only working copy changes. Assume they should be committed.
+ self.commit_locally_with_message(message)
+ elif squash is None:
+ # The user didn't explicitly say to squash or not squash. There are local commits
+ # and working copy changes. Not clear what the user wants.
+ raise ScriptError(message="""There are local commits and working copy changes. Do one of the following:
+1) Commit/revert working copy changes.
+2) Use --squash or --no-squash
+3) git config webkit-patch.squash true/false
+""")
+
+ # FIXME: This will commit all local commits, each with it's own message. We should restructure
+ # so that each local commit has the appropriate commit message based off it's ChangeLogs.
return self.push_local_commits_to_server()
+ def _commit_on_branch(self, message, git_commit):
+ branch_ref = run_command(['git', 'symbolic-ref', 'HEAD']).strip()
+ branch_name = branch_ref.replace('refs/heads/', '')
+ commit_ids = self.commit_ids_from_commitish_arguments([git_commit])
+
+ # We want to squash all this branch's commits into one commit with the proper description.
+ # We do this by doing a "merge --squash" into a new commit branch, then dcommitting that.
+ MERGE_BRANCH = 'webkit-patch-land'
+ self.delete_branch(MERGE_BRANCH)
+
+ # We might be in a directory that's present in this branch but not in the
+ # trunk. Move up to the top of the tree so that git commands that expect a
+ # valid CWD won't fail after we check out the merge branch.
+ os.chdir(self.checkout_root)
+
+ # Stuff our change into the merge branch.
+ # We wrap in a try...finally block so if anything goes wrong, we clean up the branches.
+ commit_succeeded = True
+ try:
+ run_command(['git', 'checkout', '-q', '-b', MERGE_BRANCH, self.svn_branch_name()])
+
+ for commit in commit_ids:
+ # We're on a different branch now, so convert "head" to the branch name.
+ commit = re.sub(r'(?i)head', branch_name, commit)
+ # FIXME: Once changed_files and create_patch are modified to separately handle each
+ # commit in a commit range, commit each cherry pick so they'll get dcommitted separately.
+ run_command(['git', 'cherry-pick', '--no-commit', commit])
+
+ run_command(['git', 'commit', '-m', message])
+ output = self.push_local_commits_to_server()
+ except Exception, e:
+ log("COMMIT FAILED: " + str(e))
+ output = "Commit failed."
+ commit_succeeded = False
+ finally:
+ # And then swap back to the original branch and clean up.
+ self.clean_working_directory()
+ run_command(['git', 'checkout', '-q', branch_name])
+ self.delete_branch(MERGE_BRANCH)
+
+ return output
+
def svn_commit_log(self, svn_revision):
svn_revision = self.strip_r_from_svn_revision(svn_revision)
return run_command(['git', 'svn', 'log', '-r', svn_revision])
@@ -560,13 +685,9 @@ class Git(SCM):
return run_command(['git', 'merge-base', self.svn_branch_name(), 'HEAD']).strip()
def svn_branch_name(self):
- return Git.read_git_config('svn-remote.svn.fetch').split(':')[1]
-
- def create_patch_from_local_commit(self, commit_id):
- return run_command(['git', 'diff', '--binary', commit_id + "^.." + commit_id])
-
- def create_patch_since_local_commit(self, commit_id):
- return run_command(['git', 'diff', '--binary', commit_id])
+ # FIXME: This should so something like: Git.read_git_config('svn-remote.svn.fetch').split(':')[1]
+ # but that doesn't work if the git repo is tracking multiple svn branches.
+ return 'trunk'
def commit_locally_with_message(self, message):
run_command(['git', 'commit', '--all', '-F', '-'], input=message)
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py b/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py
index c0a64d4..5a2c094 100644
--- a/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py
@@ -27,7 +27,10 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+from __future__ import with_statement
+
import base64
+import codecs
import getpass
import os
import os.path
@@ -49,23 +52,39 @@ from webkitpy.common.system.executive import Executive, run_command, ScriptError
# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
# FIXME: This should be unified into one of the executive.py commands!
+# Callers could use run_and_throw_if_fail(args, cwd=cwd, quiet=True)
def run_silent(args, cwd=None):
+ # Note: Not thread safe: http://bugs.python.org/issue2320
process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
process.communicate() # ignore output
exit_code = process.wait()
if exit_code:
raise ScriptError('Failed to run "%s" exit_code: %d cwd: %s' % (args, exit_code, cwd))
-def write_into_file_at_path(file_path, contents):
- file = open(file_path, 'w')
- file.write(contents)
- file.close()
-def read_from_path(file_path):
- file = open(file_path, 'r')
- contents = file.read()
- file.close()
- return contents
+def write_into_file_at_path(file_path, contents, encoding="utf-8"):
+ with codecs.open(file_path, "w", encoding) as file:
+ file.write(contents)
+
+
+def read_from_path(file_path, encoding="utf-8"):
+ with codecs.open(file_path, "r", encoding) as file:
+ return file.read()
+
+
+def _make_diff(command, *args):
+ # We use this wrapper to disable output decoding. diffs should be treated as
+ # binary files since they may include text files of multiple differnet encodings.
+ return run_command([command, "diff"] + list(args), decode_output=False)
+
+
+def _svn_diff(*args):
+ return _make_diff("svn", *args)
+
+
+def _git_diff(*args):
+ return _make_diff("git", *args)
+
# Exists to share svn repository creation code between the git and svn tests
class SVNTestRepository:
@@ -103,7 +122,11 @@ class SVNTestRepository:
cls._svn_add("test_file2")
cls._svn_commit("third commit")
- write_into_file_at_path("test_file", "test1test2test3\ntest4\n")
+ # This 4th commit is used to make sure that our patch file handling
+ # code correctly treats patches as binary and does not attempt to
+ # decode them assuming they're utf-8.
+ write_into_file_at_path("test_file", u"latin1 test: \u00A0\n", "latin1")
+ write_into_file_at_path("test_file2", u"utf-8 test: \u00A0\n", "utf-8")
cls._svn_commit("fourth commit")
# svn does not seem to update after commit as I would expect.
@@ -122,6 +145,19 @@ class SVNTestRepository:
test_object.svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url, test_object.svn_checkout_path])
+ # Create and checkout a trunk dir to match the standard svn configuration to match git-svn's expectations
+ os.chdir(test_object.svn_checkout_path)
+ os.mkdir('trunk')
+ cls._svn_add('trunk')
+ # We can add tags and branches as well if we ever need to test those.
+ cls._svn_commit('add trunk')
+
+ # Change directory out of the svn checkout so we can delete the checkout directory.
+ # _setup_test_commits will CD back to the svn checkout directory.
+ os.chdir('/')
+ run_command(['rm', '-rf', test_object.svn_checkout_path])
+ run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url + '/trunk', test_object.svn_checkout_path])
+
cls._setup_test_commits(test_object)
@classmethod
@@ -181,15 +217,12 @@ svn: resource out of date; try updating
# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
class SCMTest(unittest.TestCase):
def _create_patch(self, patch_contents):
- patch_path = os.path.join(self.svn_checkout_path, 'patch.diff')
- write_into_file_at_path(patch_path, patch_contents)
- patch = {}
- patch['bug_id'] = '12345'
- patch['url'] = 'file://%s' % urllib.pathname2url(patch_path)
+ # FIXME: This code is brittle if the Attachment API changes.
+ attachment = Attachment({"bug_id": 12345}, None)
+ attachment.contents = lambda: patch_contents
- attachment = Attachment(patch, None) # FIXME: This is a hack, scm.py shouldn't be fetching attachment data.
joe_cool = Committer(name="Joe Cool", email_or_emails=None)
- attachment._reviewer = joe_cool
+ attachment.reviewer = lambda: joe_cool
return attachment
@@ -202,15 +235,6 @@ class SCMTest(unittest.TestCase):
# Tests which both GitTest and SVNTest should run.
# FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
- def _shared_test_commit_with_message(self, username="dbates@webkit.org"):
- write_into_file_at_path('test_file', 'more test content')
- commit_text = self.scm.commit_with_message("another test commit", username)
- self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '5')
-
- self.scm.dryrun = True
- write_into_file_at_path('test_file', 'still more test content')
- commit_text = self.scm.commit_with_message("yet another test commit", username)
- self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
def _shared_test_changed_files(self):
write_into_file_at_path("test_file", "changed content")
@@ -248,19 +272,22 @@ class SCMTest(unittest.TestCase):
def _shared_test_changed_files_for_revision(self):
# SVN reports directory changes, Git does not.
- changed_files = self.scm.changed_files_for_revision(2)
+ changed_files = self.scm.changed_files_for_revision(3)
if "test_dir" in changed_files:
changed_files.remove("test_dir")
self.assertEqual(changed_files, ["test_dir/test_file3", "test_file"])
- self.assertEqual(sorted(self.scm.changed_files_for_revision(3)), sorted(["test_file", "test_file2"])) # Git and SVN return different orders.
- self.assertEqual(self.scm.changed_files_for_revision(4), ["test_file"])
+ self.assertEqual(sorted(self.scm.changed_files_for_revision(4)), sorted(["test_file", "test_file2"])) # Git and SVN return different orders.
+ self.assertEqual(self.scm.changed_files_for_revision(2), ["test_file"])
def _shared_test_contents_at_revision(self):
- self.assertEqual(self.scm.contents_at_revision("test_file", 2), "test1test2")
- self.assertEqual(self.scm.contents_at_revision("test_file", 3), "test1test2test3\n")
- self.assertEqual(self.scm.contents_at_revision("test_file", 4), "test1test2test3\ntest4\n")
+ self.assertEqual(self.scm.contents_at_revision("test_file", 3), "test1test2")
+ self.assertEqual(self.scm.contents_at_revision("test_file", 4), "test1test2test3\n")
+
+ # Verify that contents_at_revision returns a byte array, aka str():
+ self.assertEqual(self.scm.contents_at_revision("test_file", 5), u"latin1 test: \u00A0\n".encode("latin1"))
+ self.assertEqual(self.scm.contents_at_revision("test_file2", 5), u"utf-8 test: \u00A0\n".encode("utf-8"))
- self.assertEqual(self.scm.contents_at_revision("test_file2", 3), "second file")
+ self.assertEqual(self.scm.contents_at_revision("test_file2", 4), "second file")
# Files which don't exist:
# Currently we raise instead of returning None because detecting the difference between
# "file not found" and any other error seems impossible with svn (git seems to expose such through the return code).
@@ -268,21 +295,21 @@ class SCMTest(unittest.TestCase):
self.assertRaises(ScriptError, self.scm.contents_at_revision, "does_not_exist", 2)
def _shared_test_committer_email_for_revision(self):
- self.assertEqual(self.scm.committer_email_for_revision(2), getpass.getuser()) # Committer "email" will be the current user
+ self.assertEqual(self.scm.committer_email_for_revision(3), getpass.getuser()) # Committer "email" will be the current user
def _shared_test_reverse_diff(self):
self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
# Only test the simple case, as any other will end up with conflict markers.
- self.scm.apply_reverse_diff('4')
+ self.scm.apply_reverse_diff('5')
self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
def _shared_test_diff_for_revision(self):
# Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
- r3_patch = self.scm.diff_for_revision(3)
+ r3_patch = self.scm.diff_for_revision(4)
self.assertTrue(re.search('test3', r3_patch))
self.assertFalse(re.search('test4', r3_patch))
self.assertTrue(re.search('test2', r3_patch))
- self.assertTrue(re.search('test2', self.scm.diff_for_revision(2)))
+ self.assertTrue(re.search('test2', self.scm.diff_for_revision(3)))
def _shared_test_svn_apply_git_patch(self):
self._setup_webkittools_scripts_symlink(self.scm)
@@ -308,7 +335,7 @@ HcmV?d00001
"""
self.checkout.apply_patch(self._create_patch(git_binary_addition))
- added = read_from_path('fizzbuzz7.gif')
+ added = read_from_path('fizzbuzz7.gif', encoding=None)
self.assertEqual(512, len(added))
self.assertTrue(added.startswith('GIF89a'))
self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
@@ -336,7 +363,7 @@ ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
"""
self.checkout.apply_patch(self._create_patch(git_binary_modification))
- modified = read_from_path('fizzbuzz7.gif')
+ modified = read_from_path('fizzbuzz7.gif', encoding=None)
self.assertEqual('foobar\n', modified)
self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
@@ -473,6 +500,12 @@ class SVNTest(SCMTest):
def tearDown(self):
SVNTestRepository.tear_down(self)
+ def test_detect_scm_system_relative_url(self):
+ scm = detect_scm_system(".")
+ # I wanted to assert that we got the right path, but there was some
+ # crazy magic with temp folder names that I couldn't figure out.
+ self.assertTrue(scm.checkout_root)
+
def test_create_patch_is_full_patch(self):
test_dir_path = os.path.join(self.svn_checkout_path, "test_dir2")
os.mkdir(test_dir_path)
@@ -518,25 +551,35 @@ Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
self._setup_webkittools_scripts_symlink(self.scm)
patch_file = self._create_patch(patch_contents)
self.checkout.apply_patch(patch_file)
- actual_contents = read_from_path("test_file.swf")
+ actual_contents = read_from_path("test_file.swf", encoding=None)
self.assertEqual(actual_contents, expected_contents)
def test_apply_svn_patch(self):
scm = detect_scm_system(self.svn_checkout_path)
- patch = self._create_patch(run_command(['svn', 'diff', '-r4:3']))
+ patch = self._create_patch(_svn_diff("-r5:4"))
self._setup_webkittools_scripts_symlink(scm)
Checkout(scm).apply_patch(patch)
def test_apply_svn_patch_force(self):
scm = detect_scm_system(self.svn_checkout_path)
- patch = self._create_patch(run_command(['svn', 'diff', '-r2:4']))
+ patch = self._create_patch(_svn_diff("-r3:5"))
self._setup_webkittools_scripts_symlink(scm)
self.assertRaises(ScriptError, Checkout(scm).apply_patch, patch, force=True)
def test_commit_logs(self):
# Commits have dates and usernames in them, so we can't just direct compare.
self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
- self.assertTrue(re.search('second commit', self.scm.svn_commit_log(2)))
+ self.assertTrue(re.search('second commit', self.scm.svn_commit_log(3)))
+
+ def _shared_test_commit_with_message(self, username=None):
+ write_into_file_at_path('test_file', 'more test content')
+ commit_text = self.scm.commit_with_message("another test commit", username)
+ self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
+
+ self.scm.dryrun = True
+ write_into_file_at_path('test_file', 'still more test content')
+ commit_text = self.scm.commit_with_message("yet another test commit", username)
+ self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
def test_commit_text_parsing(self):
self._shared_test_commit_with_message()
@@ -595,7 +638,7 @@ class GitTest(SCMTest):
def _setup_git_clone_of_svn_repository(self):
self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
# --quiet doesn't make git svn silent, so we use run_silent to redirect output
- run_silent(['git', 'svn', '--quiet', 'clone', self.svn_repo_url, self.git_checkout_path])
+ run_silent(['git', 'svn', 'clone', '-T', 'trunk', self.svn_repo_url, self.git_checkout_path])
def _tear_down_git_clone_of_svn_repository(self):
run_command(['rm', '-rf', self.git_checkout_path])
@@ -657,8 +700,8 @@ class GitTest(SCMTest):
test_file = os.path.join(self.git_checkout_path, 'test_file')
write_into_file_at_path(test_file, 'foo')
- diff_to_common_base = run_command(['git', 'diff', self.scm.svn_branch_name() + '..'])
- diff_to_merge_base = run_command(['git', 'diff', self.scm.svn_merge_base()])
+ diff_to_common_base = _git_diff(self.scm.svn_branch_name() + '..')
+ diff_to_merge_base = _git_diff(self.scm.svn_merge_base())
self.assertFalse(re.search(r'foo', diff_to_common_base))
self.assertTrue(re.search(r'foo', diff_to_merge_base))
@@ -711,18 +754,126 @@ class GitTest(SCMTest):
# We carefullly pick a diff which does not have a directory addition
# as currently svn-apply will error out when trying to remove directories
# in Git: https://bugs.webkit.org/show_bug.cgi?id=34871
- patch = self._create_patch(run_command(['git', 'diff', 'HEAD..HEAD^']))
+ patch = self._create_patch(_git_diff('HEAD..HEAD^'))
self._setup_webkittools_scripts_symlink(scm)
Checkout(scm).apply_patch(patch)
def test_apply_git_patch_force(self):
scm = detect_scm_system(self.git_checkout_path)
- patch = self._create_patch(run_command(['git', 'diff', 'HEAD~2..HEAD']))
+ patch = self._create_patch(_git_diff('HEAD~2..HEAD'))
self._setup_webkittools_scripts_symlink(scm)
self.assertRaises(ScriptError, Checkout(scm).apply_patch, patch, force=True)
def test_commit_text_parsing(self):
- self._shared_test_commit_with_message()
+ write_into_file_at_path('test_file', 'more test content')
+ self.scm.commit_locally_with_message("another test commit")
+ commit_text = self.scm.commit_with_message("another test commit")
+ self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
+
+ self.scm.dryrun = True
+ write_into_file_at_path('test_file', 'still more test content')
+ self.scm.commit_locally_with_message("yet another test commit")
+ commit_text = self.scm.commit_with_message("yet another test commit")
+ self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
+
+ def _one_local_commit_plus_working_copy_changes(self):
+ write_into_file_at_path('test_file_commit1', 'more test content')
+ run_command(['git', 'add', 'test_file_commit1'])
+ self.scm.commit_locally_with_message("another test commit")
+
+ write_into_file_at_path('test_file_commit2', 'still more test content')
+ run_command(['git', 'add', 'test_file_commit2'])
+
+ def test_commit_with_message_working_copy_only(self):
+ write_into_file_at_path('test_file_commit1', 'more test content')
+ run_command(['git', 'add', 'test_file_commit1'])
+ scm = detect_scm_system(self.git_checkout_path)
+ commit_text = scm.commit_with_message("yet another test commit")
+
+ self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+ svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+ self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+ def test_commit_with_message_squashed(self):
+ self._one_local_commit_plus_working_copy_changes()
+ scm = detect_scm_system(self.git_checkout_path)
+ commit_text = scm.commit_with_message("yet another test commit", squash=True)
+
+ self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+ svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+ self.assertTrue(re.search(r'test_file_commit2', svn_log))
+ self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+ def _two_local_commits(self):
+ write_into_file_at_path('test_file_commit1', 'more test content')
+ run_command(['git', 'add', 'test_file_commit1'])
+ self.scm.commit_locally_with_message("another test commit")
+
+ write_into_file_at_path('test_file_commit2', 'still more test content')
+ run_command(['git', 'add', 'test_file_commit2'])
+ self.scm.commit_locally_with_message("yet another test commit")
+
+ def test_commit_with_message_git_commit(self):
+ self._two_local_commits()
+
+ scm = detect_scm_system(self.git_checkout_path)
+ commit_text = scm.commit_with_message("another test commit", git_commit="HEAD^")
+ self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+ svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+ self.assertTrue(re.search(r'test_file_commit1', svn_log))
+ self.assertFalse(re.search(r'test_file_commit2', svn_log))
+
+ def test_commit_with_message_git_commit_range(self):
+ self._two_local_commits()
+
+ scm = detect_scm_system(self.git_checkout_path)
+ commit_text = scm.commit_with_message("another test commit", git_commit="HEAD~2..HEAD")
+ self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+ svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+ self.assertTrue(re.search(r'test_file_commit1', svn_log))
+ self.assertTrue(re.search(r'test_file_commit2', svn_log))
+
+ def test_commit_with_message_multiple_local_commits(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertRaises(ScriptError, scm.commit_with_message, ["another test commit"])
+
+ def test_commit_with_message_multiple_local_commits_and_working_copy(self):
+ self._two_local_commits()
+ write_into_file_at_path('test_file_commit1', 'working copy change')
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertRaises(ScriptError, scm.commit_with_message, ["another test commit"])
+
+ def test_commit_with_message_git_commit_and_working_copy(self):
+ self._two_local_commits()
+ write_into_file_at_path('test_file_commit1', 'working copy change')
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertRaises(ScriptError, scm.commit_with_message, ["another test commit", 'git_commit="HEAD^"'])
+
+ def test_commit_with_message_multiple_local_commits_no_squash(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ commit_text = scm.commit_with_message("yet another test commit", squash=False)
+ self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+ svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+ self.assertTrue(re.search(r'test_file_commit2', svn_log))
+ self.assertFalse(re.search(r'test_file_commit1', svn_log))
+
+ svn_log = run_command(['git', 'svn', 'log', '--limit=2', '--verbose'])
+ self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+ def test_commit_with_message_multiple_local_commits_squash(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ commit_text = scm.commit_with_message("yet another test commit", squash=True)
+ self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+ svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+ self.assertTrue(re.search(r'test_file_commit2', svn_log))
+ self.assertTrue(re.search(r'test_file_commit1', svn_log))
def test_reverse_diff(self):
self._shared_test_reverse_diff()
@@ -733,13 +884,66 @@ class GitTest(SCMTest):
def test_svn_apply_git_patch(self):
self._shared_test_svn_apply_git_patch()
+ def test_create_patch_local_plus_working_copy(self):
+ self._one_local_commit_plus_working_copy_changes()
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertRaises(ScriptError, scm.create_patch)
+
+ def test_create_patch_multiple_local_commits(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertRaises(ScriptError, scm.create_patch)
+
+ def test_create_patch_squashed(self):
+ self._one_local_commit_plus_working_copy_changes()
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = scm.create_patch(squash=True)
+ self.assertTrue(re.search(r'test_file_commit2', patch))
+ self.assertTrue(re.search(r'test_file_commit1', patch))
+
+ def test_create_patch_not_squashed(self):
+ self._one_local_commit_plus_working_copy_changes()
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = scm.create_patch(squash=False)
+ self.assertTrue(re.search(r'test_file_commit2', patch))
+ self.assertFalse(re.search(r'test_file_commit1', patch))
+
+ def test_create_patch_git_commit(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = scm.create_patch(git_commit="HEAD^")
+ self.assertTrue(re.search(r'test_file_commit1', patch))
+ self.assertFalse(re.search(r'test_file_commit2', patch))
+
+ def test_create_patch_git_commit_range(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = scm.create_patch(git_commit="HEAD~2..HEAD")
+ self.assertTrue(re.search(r'test_file_commit2', patch))
+ self.assertTrue(re.search(r'test_file_commit1', patch))
+
+ def test_create_patch_multiple_local_commits_no_squash(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = scm.create_patch(squash=False)
+ # FIXME: It's weird that with squash=False, create_patch/changed_files ignores local commits,
+ # but commit_with_message commits them.
+ self.assertTrue(patch == "")
+
+ def test_create_patch_multiple_local_commits_squash(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = scm.create_patch(squash=True)
+ self.assertTrue(re.search(r'test_file_commit2', patch))
+ self.assertTrue(re.search(r'test_file_commit1', patch))
+
def test_create_binary_patch(self):
# Create a git binary patch and check the contents.
scm = detect_scm_system(self.git_checkout_path)
test_file_name = 'binary_file'
test_file_path = os.path.join(self.git_checkout_path, test_file_name)
file_contents = ''.join(map(chr, range(256)))
- write_into_file_at_path(test_file_path, file_contents)
+ write_into_file_at_path(test_file_path, file_contents, encoding=None)
run_command(['git', 'add', test_file_name])
patch = scm.create_patch()
self.assertTrue(re.search(r'\nliteral 0\n', patch))
@@ -749,19 +953,68 @@ class GitTest(SCMTest):
run_command(['git', 'rm', '-f', test_file_name])
self._setup_webkittools_scripts_symlink(scm)
self.checkout.apply_patch(self._create_patch(patch))
- self.assertEqual(file_contents, read_from_path(test_file_path))
+ self.assertEqual(file_contents, read_from_path(test_file_path, encoding=None))
# Check if we can create a patch from a local commit.
- write_into_file_at_path(test_file_path, file_contents)
+ write_into_file_at_path(test_file_path, file_contents, encoding=None)
run_command(['git', 'add', test_file_name])
run_command(['git', 'commit', '-m', 'binary diff'])
- patch_from_local_commit = scm.create_patch_from_local_commit('HEAD')
+ patch_from_local_commit = scm.create_patch('HEAD')
self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
- patch_since_local_commit = scm.create_patch_since_local_commit('HEAD^1')
- self.assertTrue(re.search(r'\nliteral 0\n', patch_since_local_commit))
- self.assertTrue(re.search(r'\nliteral 256\n', patch_since_local_commit))
- self.assertEqual(patch_from_local_commit, patch_since_local_commit)
+
+ def test_changed_files_local_plus_working_copy(self):
+ self._one_local_commit_plus_working_copy_changes()
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertRaises(ScriptError, scm.changed_files)
+
+ def test_changed_files_multiple_local_commits(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertRaises(ScriptError, scm.changed_files)
+
+ def test_changed_files_squashed(self):
+ self._one_local_commit_plus_working_copy_changes()
+ scm = detect_scm_system(self.git_checkout_path)
+ files = scm.changed_files(squash=True)
+ self.assertTrue('test_file_commit2' in files)
+ self.assertTrue('test_file_commit1' in files)
+
+ def test_changed_files_not_squashed(self):
+ self._one_local_commit_plus_working_copy_changes()
+ scm = detect_scm_system(self.git_checkout_path)
+ files = scm.changed_files(squash=False)
+ self.assertTrue('test_file_commit2' in files)
+ self.assertFalse('test_file_commit1' in files)
+
+ def test_changed_files_git_commit(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ files = scm.changed_files(git_commit="HEAD^")
+ self.assertTrue('test_file_commit1' in files)
+ self.assertFalse('test_file_commit2' in files)
+
+ def test_changed_files_git_commit_range(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ files = scm.changed_files(git_commit="HEAD~2..HEAD")
+ self.assertTrue('test_file_commit1' in files)
+ self.assertTrue('test_file_commit2' in files)
+
+ def test_changed_files_multiple_local_commits_no_squash(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ files = scm.changed_files(squash=False)
+ # FIXME: It's weird that with squash=False, create_patch/changed_files ignores local commits,
+ # but commit_with_message commits them.
+ self.assertTrue(len(files) == 0)
+
+ def test_changed_files_multiple_local_commits_squash(self):
+ self._two_local_commits()
+ scm = detect_scm_system(self.git_checkout_path)
+ files = scm.changed_files(squash=True)
+ self.assertTrue('test_file_commit2' in files)
+ self.assertTrue('test_file_commit1' in files)
def test_changed_files(self):
self._shared_test_changed_files()
diff --git a/WebKitTools/Scripts/webkitpy/common/config/committers.py b/WebKitTools/Scripts/webkitpy/common/config/committers.py
index a92dbd3..56887ab 100644
--- a/WebKitTools/Scripts/webkitpy/common/config/committers.py
+++ b/WebKitTools/Scripts/webkitpy/common/config/committers.py
@@ -86,7 +86,6 @@ committers_unable_to_review = [
Committer("Carol Szabo", "carol.szabo@nokia.com"),
Committer("Chang Shu", "Chang.Shu@nokia.com"),
Committer("Chris Fleizach", "cfleizach@apple.com"),
- Committer("Chris Jerdonek", "cjerdonek@webkit.org", "cjerdonek"),
Committer("Chris Marrin", "cmarrin@apple.com", "cmarrin"),
Committer("Chris Petersen", "cpetersen@apple.com", "cpetersen"),
Committer("Christian Dywan", ["christian@twotoasts.de", "christian@webkit.org"]),
@@ -94,6 +93,7 @@ committers_unable_to_review = [
Committer("Csaba Osztrogonac", "ossy@webkit.org", "ossy"),
Committer("David Smith", ["catfish.man@gmail.com", "dsmith@webkit.org"], "catfishman"),
Committer("Dean Jackson", "dino@apple.com", "dino"),
+ Committer("Diego Gonzalez", ["diegohcg@webkit.org", "diego.gonzalez@openbossa.org"], "diegohcg"),
Committer("Dirk Pranke", "dpranke@chromium.org"),
Committer("Drew Wilson", "atwilson@chromium.org", "atwilson"),
Committer("Dumitru Daniliuc", "dumi@chromium.org", "dumi"),
@@ -101,6 +101,8 @@ committers_unable_to_review = [
Committer("Enrica Casucci", "enrica@apple.com"),
Committer("Erik Arvidsson", "arv@chromium.org", "arv"),
Committer("Eric Roman", "eroman@chromium.org", "eroman"),
+ Committer("Evan Martin", "evan@chromium.org", "evmar"),
+ Committer("Evan Stade", "estade@chromium.org", "estade"),
Committer("Feng Qian", "feng@chromium.org"),
Committer("Fumitoshi Ukai", "ukai@chromium.org", "ukai"),
Committer("Gabor Loki", "loki@webkit.org", "loki04"),
@@ -153,13 +155,13 @@ committers_unable_to_review = [
Committer("Ryosuke Niwa", "rniwa@webkit.org", "rniwa"),
Committer("Scott Violet", "sky@chromium.org", "sky"),
Committer("Stephen White", "senorblanco@chromium.org", "senorblanco"),
- Committer("Steve Block", "steveblock@google.com"),
Committer("Tony Chang", "tony@chromium.org", "tony^work"),
Committer("Trey Matteson", "trey@usa.net", "trey"),
Committer("Tristan O'Tierney", ["tristan@otierney.net", "tristan@apple.com"]),
Committer("Victor Wang", "victorw@chromium.org"),
Committer("Vitaly Repeshko", "vitalyr@chromium.org"),
Committer("William Siegrist", "wsiegrist@apple.com", "wms"),
+ Committer("Xiaomei Ji", "xji@chromium.org", "xji"),
Committer("Yael Aharon", "yael.aharon@nokia.com"),
Committer("Yaar Schnitman", ["yaar@chromium.org", "yaar@google.com"]),
Committer("Yong Li", ["yong.li@torchmobile.com", "yong.li.webkit@gmail.com"], "yong"),
@@ -191,6 +193,7 @@ reviewers_list = [
Reviewer("Brady Eidson", "beidson@apple.com", "bradee-oh"),
Reviewer("Cameron Zwarich", ["zwarich@apple.com", "cwzwarich@apple.com", "cwzwarich@webkit.org"]),
Reviewer("Chris Blumenberg", "cblu@apple.com", "cblu"),
+ Reviewer("Chris Jerdonek", "cjerdonek@webkit.org", "cjerdonek"),
Reviewer("Dan Bernstein", ["mitz@webkit.org", "mitz@apple.com"], "mitzpettel"),
Reviewer("Daniel Bates", "dbates@webkit.org", "dydz"),
Reviewer("Darin Adler", "darin@apple.com", "darin"),
@@ -237,10 +240,11 @@ reviewers_list = [
Reviewer("Simon Fraser", "simon.fraser@apple.com", "smfr"),
Reviewer("Simon Hausmann", ["hausmann@webkit.org", "hausmann@kde.org", "simon.hausmann@nokia.com"], "tronical"),
Reviewer("Stephanie Lewis", "slewis@apple.com", "sundiamonde"),
+ Reviewer("Steve Block", "steveblock@google.com", "steveblock"),
Reviewer("Steve Falkenburg", "sfalken@apple.com", "sfalken"),
Reviewer("Tim Omernick", "timo@apple.com"),
Reviewer("Timothy Hatcher", ["timothy@hatcher.name", "timothy@apple.com"], "xenon"),
- Reviewer(u'Tor Arne Vestb\xf8', "vestbo@webkit.org", "torarne"),
+ Reviewer(u"Tor Arne Vestb\u00f8", "vestbo@webkit.org", "torarne"),
Reviewer("Vicki Murley", "vicki@apple.com"),
Reviewer("Xan Lopez", ["xan.lopez@gmail.com", "xan@gnome.org", "xan@webkit.org"], "xan"),
Reviewer("Yury Semikhatsky", "yurys@chromium.org", "yurys"),
diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py
index 6920d67..4311a00 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py
@@ -32,7 +32,7 @@
import os.path
import re
-import subprocess
+import StringIO
from datetime import datetime # used in timestamp()
@@ -116,6 +116,10 @@ class Attachment(object):
# depends on the current behavior.
return self._attachment_dictionary.get("url")
+ def contents(self):
+ # FIXME: We shouldn't be grabbing at _bugzilla.
+ return self._bug._bugzilla.fetch_attachment_contents(self.id())
+
def _validate_flag_value(self, flag):
email = self._attachment_dictionary.get("%s_email" % flag)
if not email:
@@ -427,7 +431,16 @@ class Bugzilla(object):
if flag['status'] == '+':
attachment[result_key] = flag['setter']
+ def _string_contents(self, soup):
+ # WebKit's bugzilla instance uses UTF-8.
+ # BeautifulSoup always returns Unicode strings, however
+ # the .string method returns a (unicode) NavigableString.
+ # NavigableString can confuse other parts of the code, so we
+ # convert from NavigableString to a real unicode() object using unicode().
+ return unicode(soup.string)
+
def _parse_attachment_element(self, element, bug_id):
+
attachment = {}
attachment['bug_id'] = bug_id
attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
@@ -435,9 +448,9 @@ class Bugzilla(object):
attachment['id'] = int(element.find('attachid').string)
# FIXME: No need to parse out the url here.
attachment['url'] = self.attachment_url_for_id(attachment['id'])
- attachment['name'] = unicode(element.find('desc').string)
- attachment['attacher_email'] = str(element.find('attacher').string)
- attachment['type'] = str(element.find('type').string)
+ attachment['name'] = self._string_contents(element.find('desc'))
+ attachment['attacher_email'] = self._string_contents(element.find('attacher'))
+ attachment['type'] = self._string_contents(element.find('type'))
self._parse_attachment_flag(
element, 'review', attachment, 'reviewer_email')
self._parse_attachment_flag(
@@ -448,10 +461,10 @@ class Bugzilla(object):
soup = BeautifulSoup(page)
bug = {}
bug["id"] = int(soup.find("bug_id").string)
- bug["title"] = unicode(soup.find("short_desc").string)
- bug["reporter_email"] = str(soup.find("reporter").string)
- bug["assigned_to_email"] = str(soup.find("assigned_to").string)
- bug["cc_emails"] = [str(element.string)
+ bug["title"] = self._string_contents(soup.find("short_desc"))
+ bug["reporter_email"] = self._string_contents(soup.find("reporter"))
+ bug["assigned_to_email"] = self._string_contents(soup.find("assigned_to"))
+ bug["cc_emails"] = [self._string_contents(element)
for element in soup.findAll('cc')]
bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
return bug
@@ -476,6 +489,12 @@ class Bugzilla(object):
def fetch_bug(self, bug_id):
return Bug(self.fetch_bug_dictionary(bug_id), self)
+ def fetch_attachment_contents(self, attachment_id):
+ attachment_url = self.attachment_url_for_id(attachment_id)
+ # We need to authenticate to download patches from security bugs.
+ self.authenticate()
+ return self.browser.open(attachment_url).read()
+
def _parse_bug_id_from_attachment_page(self, page):
# The "Up" relation happens to point to the bug.
up_link = BeautifulSoup(page).find('link', rel='Up')
@@ -568,6 +587,7 @@ class Bugzilla(object):
patch_name = "bug-%s-%s.patch" % (bug_id, timestamp())
else:
patch_name ="%s.patch" % timestamp()
+
self.browser.add_file(patch_file_object,
"text/plain",
patch_name,
@@ -575,7 +595,7 @@ class Bugzilla(object):
def add_patch_to_bug(self,
bug_id,
- patch_file_object,
+ diff,
description,
comment_text=None,
mark_for_review=False,
@@ -594,6 +614,11 @@ class Bugzilla(object):
self.browser.open("%sattachment.cgi?action=enter&bugid=%s" % (
self.bug_server_url, bug_id))
self.browser.select_form(name="entryform")
+
+ # _fill_attachment_form expects a file-like object
+ # Patch files are already binary, so no encoding needed.
+ assert(isinstance(diff, str))
+ patch_file_object = StringIO.StringIO(diff)
self._fill_attachment_form(description,
patch_file_object,
mark_for_review=mark_for_review,
@@ -628,7 +653,7 @@ class Bugzilla(object):
bug_title,
bug_description,
component=None,
- patch_file_object=None,
+ diff=None,
patch_description=None,
cc=None,
blocked=None,
@@ -653,11 +678,15 @@ class Bugzilla(object):
if cc:
self.browser["cc"] = cc
if blocked:
- self.browser["blocked"] = str(blocked)
+ self.browser["blocked"] = unicode(blocked)
self.browser["short_desc"] = bug_title
self.browser["comment"] = bug_description
- if patch_file_object:
+ if diff:
+ # _fill_attachment_form expects a file-like object
+ # Patch files are already binary, so no encoding needed.
+ assert(isinstance(diff, str))
+ patch_file_object = StringIO.StringIO(diff)
self._fill_attachment_form(
patch_description,
patch_file_object,
diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py
index 753e909..6c6ed43 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py
@@ -44,7 +44,7 @@ _log = get_logger(__file__)
class Builder(object):
def __init__(self, name, buildbot):
- self._name = unicode(name)
+ self._name = name
self._buildbot = buildbot
self._builds_cache = {}
self._revision_to_build_number = None
@@ -223,12 +223,12 @@ class LayoutTestResults(object):
parsed_results = {}
tables = BeautifulSoup(page).findAll("table")
for table in tables:
- table_title = table.findPreviousSibling("p").string
+ table_title = unicode(table.findPreviousSibling("p").string)
if table_title not in cls.expected_keys:
# This Exception should only ever be hit if run-webkit-tests changes its results.html format.
- raise Exception("Unhandled title: %s" % str(table_title))
+ raise Exception("Unhandled title: %s" % table_title)
# We might want to translate table titles into identifiers before storing.
- parsed_results[table_title] = [row.find("a").string for row in table.findAll("tr")]
+ parsed_results[table_title] = [unicode(row.find("a").string) for row in table.findAll("tr")]
return parsed_results
@@ -319,7 +319,6 @@ class BuildBot(object):
"Leopard",
"Tiger",
"Windows.*Build",
- "Windows.*Debug.*Test",
"GTK",
"Qt",
"Chromium",
@@ -361,7 +360,7 @@ class BuildBot(object):
# First cell is the name
name_link = status_cells[0].find('a')
- builder["name"] = name_link.string
+ builder["name"] = unicode(name_link.string)
self._parse_last_build_cell(builder, status_cells[1])
self._parse_current_build_cell(builder, status_cells[2])
@@ -410,13 +409,13 @@ class BuildBot(object):
return urllib2.urlopen(build_status_url)
def _parse_twisted_file_row(self, file_row):
- string_or_empty = lambda string: str(string) if string else ""
+ string_or_empty = lambda soup: unicode(soup.string) if soup.string else u""
file_cells = file_row.findAll('td')
return {
- "filename" : string_or_empty(file_cells[0].find("a").string),
- "size" : string_or_empty(file_cells[1].string),
- "type" : string_or_empty(file_cells[2].string),
- "encoding" : string_or_empty(file_cells[3].string),
+ "filename": string_or_empty(file_cells[0].find("a")),
+ "size": string_or_empty(file_cells[1]),
+ "type": string_or_empty(file_cells[2]),
+ "encoding": string_or_empty(file_cells[3]),
}
def _parse_twisted_directory_listing(self, page):
diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py
index f765f6e..5e04745 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py
@@ -51,7 +51,7 @@ class BuilderTest(unittest.TestCase):
def setUp(self):
self.buildbot = BuildBot()
- self.builder = Builder("Test Builder", self.buildbot)
+ self.builder = Builder(u"Test Builder \u2661", self.buildbot)
self._install_fetch_build(lambda build_number: ["test1", "test2"])
def test_find_failure_transition(self):
@@ -271,7 +271,6 @@ class BuildBotTest(unittest.TestCase):
"Leopard",
"Tiger",
"Windows.*Build",
- "Windows.*Debug.*Test",
"GTK",
"Qt",
"Chromium",
@@ -286,7 +285,6 @@ class BuildBotTest(unittest.TestCase):
{'name': u'SnowLeopard Intel Release (Tests)', },
{'name': u'Windows Release (Build)', },
{'name': u'Windows Debug (Build)', },
- {'name': u'Windows Debug (Tests)', },
{'name': u'GTK Linux 32-bit Release', },
{'name': u'GTK Linux 32-bit Debug', },
{'name': u'GTK Linux 64-bit Debug', },
diff --git a/WebKitTools/Scripts/webkitpy/common/net/rietveld.py b/WebKitTools/Scripts/webkitpy/common/net/rietveld.py
index a9e5b1a..9cc97f2 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/rietveld.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/rietveld.py
@@ -26,6 +26,7 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import logging
import os
import re
import stat
@@ -50,23 +51,19 @@ class Rietveld(object):
def __init__(self, executive, dryrun=False):
self.dryrun = dryrun
self._executive = executive
- self._upload_py = upload.__file__
- # Chop off the last character so we modify permissions on the py file instead of the pyc.
- if os.path.splitext(self._upload_py)[1] == ".pyc":
- self._upload_py = self._upload_py[:-1]
- os.chmod(self._upload_py, os.stat(self._upload_py).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def url_for_issue(self, codereview_issue):
if not codereview_issue:
return None
return "%s%s" % (config.codereview_server_url, codereview_issue)
- def post(self, message=None, codereview_issue=None, cc=None):
+ def post(self, diff, message=None, codereview_issue=None, cc=None):
if not message:
raise ScriptError("Rietveld requires a message.")
args = [
- self._upload_py,
+ # First argument is empty string to mimic sys.argv.
+ "",
"--assume_yes",
"--server=%s" % config.codereview_server_host,
"--message=%s" % message,
@@ -80,10 +77,15 @@ class Rietveld(object):
log("Would have run %s" % args)
return
- output = self._executive.run_and_throw_if_fail(args)
- match = re.search("Issue created\. URL: " +
- config.codereview_server_regex +
- "(?P<codereview_issue>\d+)",
- output)
- if match:
- return int(match.group('codereview_issue'))
+ # Set logging level to avoid rietveld's logging spew.
+ old_level_name = logging.getLogger().getEffectiveLevel()
+ logging.getLogger().setLevel(logging.ERROR)
+
+ # Use RealMain instead of calling upload from the commandline so that
+ # we can pass in the diff ourselves. Otherwise, upload will just use
+ # git diff for git checkouts, which doesn't respect --squash and --git-commit.
+ issue, patchset = upload.RealMain(args[1:], data=diff)
+
+ # Reset logging level to the original value.
+ logging.getLogger().setLevel(old_level_name)
+ return issue
diff --git a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py
index e8987a9..d9b52a2 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py
@@ -52,9 +52,9 @@ class StatusServer:
if not patch:
return
if patch.bug_id():
- self.browser["bug_id"] = str(patch.bug_id())
+ self.browser["bug_id"] = unicode(patch.bug_id())
if patch.id():
- self.browser["patch_id"] = str(patch.id())
+ self.browser["patch_id"] = unicode(patch.id())
def _add_results_file(self, results_file):
if not results_file:
@@ -79,7 +79,7 @@ class StatusServer:
update_svn_revision_url = "%s/update-svn-revision" % self.url
self.browser.open(update_svn_revision_url)
self.browser.select_form(name="update_svn_revision")
- self.browser["number"] = str(svn_revision_number)
+ self.browser["number"] = unicode(svn_revision_number)
self.browser["broken_bot"] = broken_bot
return self.browser.submit().read()
diff --git a/WebKitTools/Scripts/webkitpy/common/prettypatch.py b/WebKitTools/Scripts/webkitpy/common/prettypatch.py
index 8157f9c..4e92a53 100644
--- a/WebKitTools/Scripts/webkitpy/common/prettypatch.py
+++ b/WebKitTools/Scripts/webkitpy/common/prettypatch.py
@@ -31,11 +31,15 @@ import tempfile
class PrettyPatch(object):
+ # FIXME: PrettyPatch should not require checkout_root.
def __init__(self, executive, checkout_root):
self._executive = executive
self._checkout_root = checkout_root
def pretty_diff_file(self, diff):
+ # Diffs can contain multiple text files of different encodings
+ # so we always deal with them as byte arrays, not unicode strings.
+ assert(isinstance(diff, str))
pretty_diff = self.pretty_diff(diff)
diff_file = tempfile.NamedTemporaryFile(suffix=".html")
diff_file.write(pretty_diff)
@@ -43,6 +47,11 @@ class PrettyPatch(object):
return diff_file
def pretty_diff(self, diff):
+ # pretify.rb will hang forever if given no input.
+ # Avoid the hang by returning an empty string.
+ if not diff:
+ return ""
+
pretty_patch_path = os.path.join(self._checkout_root,
"BugsSite", "PrettyPatch")
prettify_path = os.path.join(pretty_patch_path, "prettify.rb")
@@ -52,4 +61,6 @@ class PrettyPatch(object):
pretty_patch_path,
prettify_path,
]
- return self._executive.run_command(args, input=diff)
+ # PrettyPatch does not modify the encoding of the diff output
+ # so we can't expect it to be utf-8.
+ return self._executive.run_command(args, input=diff, decode_output=False)
diff --git a/WebKitTools/Scripts/webkitpy/common/prettypatch_unittest.py b/WebKitTools/Scripts/webkitpy/common/prettypatch_unittest.py
new file mode 100644
index 0000000..1307856
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/common/prettypatch_unittest.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os.path
+import unittest
+
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.prettypatch import PrettyPatch
+
+
+class PrettyPatchTest(unittest.TestCase):
+
+ _diff_with_multiple_encodings = """
+Index: utf8_test
+===================================================================
+--- utf8_test\t(revision 0)
++++ utf8_test\t(revision 0)
+@@ -0,0 +1 @@
++utf-8 test: \xc2\xa0
+Index: latin1_test
+===================================================================
+--- latin1_test\t(revision 0)
++++ latin1_test\t(revision 0)
+@@ -0,0 +1 @@
++latin1 test: \xa0
+"""
+
+ def _webkit_root(self):
+ webkitpy_common = os.path.dirname(__file__)
+ webkitpy = os.path.dirname(webkitpy_common)
+ scripts = os.path.dirname(webkitpy)
+ webkit_tools = os.path.dirname(scripts)
+ webkit_root = os.path.dirname(webkit_tools)
+ return webkit_root
+
+ def test_pretty_diff_encodings(self):
+ pretty_patch = PrettyPatch(Executive(), self._webkit_root())
+ pretty = pretty_patch.pretty_diff(self._diff_with_multiple_encodings)
+ self.assertTrue(pretty) # We got some output
+ self.assertTrue(isinstance(pretty, str)) # It's a byte array, not unicode
+
+ def test_pretty_print_empty_string(self):
+ # Make sure that an empty diff does not hang the process.
+ pretty_patch = PrettyPatch(Executive(), self._webkit_root())
+ self.assertEqual(pretty_patch.pretty_diff(""), "")
diff --git a/WebKitTools/Scripts/webkitpy/common/system/autoinstall.py b/WebKitTools/Scripts/webkitpy/common/system/autoinstall.py
index 32fd2cf..9adab29 100755
--- a/WebKitTools/Scripts/webkitpy/common/system/autoinstall.py
+++ b/WebKitTools/Scripts/webkitpy/common/system/autoinstall.py
@@ -30,6 +30,10 @@
"""Support for automatically downloading Python packages from an URL."""
+
+from __future__ import with_statement
+
+import codecs
import logging
import new
import os
@@ -114,7 +118,7 @@ class AutoInstaller(object):
os.makedirs(path)
- def _write_file(self, path, text):
+ def _write_file(self, path, text, encoding):
"""Create a file at the given path with given text.
This method overwrites any existing file.
@@ -122,11 +126,8 @@ class AutoInstaller(object):
"""
_log.debug("Creating file...")
_log.debug(' "%s"' % path)
- file = open(path, "w")
- try:
+ with codecs.open(path, "w", encoding) as file:
file.write(text)
- finally:
- file.close()
def _set_up_target_dir(self, target_dir, append_to_search_path,
make_package):
@@ -154,7 +155,7 @@ class AutoInstaller(object):
if not os.path.exists(init_path):
text = ("# This file is required for Python to search this "
"directory for modules.\n")
- self._write_file(init_path, text)
+ self._write_file(init_path, text, "ascii")
def _create_scratch_directory_inner(self, prefix):
"""Create a scratch directory without exception handling.
@@ -216,11 +217,8 @@ class AutoInstaller(object):
_log.debug("No URL file found.")
return False
- file = open(version_path, "r")
- try:
+ with codecs.open(version_path, "r", "utf-8") as file:
version = file.read()
- finally:
- file.close()
return version.strip() == url.strip()
@@ -231,7 +229,7 @@ class AutoInstaller(object):
_log.debug(' URL: "%s"' % url)
_log.debug(' To: "%s"' % version_path)
- self._write_file(version_path, url)
+ self._write_file(version_path, url, "utf-8")
def _extract_targz(self, path, scratch_dir):
# tarfile.extractall() extracts to a path without the
@@ -284,6 +282,8 @@ class AutoInstaller(object):
# Otherwise, it is a file.
try:
+ # We open this file w/o encoding, as we're reading/writing
+ # the raw byte-stream from the zip file.
outfile = open(path, 'wb')
except IOError, err:
# Not all zip files seem to list the directories explicitly,
@@ -384,9 +384,8 @@ class AutoInstaller(object):
self._log_transfer("Starting download...", url, target_path)
- stream = file(target_path, "wb")
- bytes = self._download_to_stream(url, stream)
- stream.close()
+ with open(target_path, "wb") as stream:
+ bytes = self._download_to_stream(url, stream)
_log.debug("Downloaded %s bytes." % bytes)
diff --git a/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging.py b/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging.py
index ba1c5eb..9e6b529 100644
--- a/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging.py
+++ b/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging.py
@@ -30,24 +30,30 @@
# WebKit's Python module for logging
# This module is now deprecated in favor of python's built-in logging.py.
+import codecs
import os
import sys
+
def log(string):
print >> sys.stderr, string
+
def error(string):
log("ERROR: %s" % string)
exit(1)
+
# Simple class to split output between multiple destinations
class tee:
def __init__(self, *files):
self.files = files
- def write(self, string):
+ # Callers should pass an already encoded string for writing.
+ def write(self, bytes):
for file in self.files:
- file.write(string)
+ file.write(bytes)
+
class OutputTee:
def __init__(self):
@@ -71,7 +77,7 @@ class OutputTee:
(log_directory, log_name) = os.path.split(log_path)
if log_directory and not os.path.exists(log_directory):
os.makedirs(log_directory)
- return open(log_path, 'a+')
+ return codecs.open(log_path, "a+", "utf-8")
def _tee_outputs_to_files(self, files):
if not self._original_stdout:
diff --git a/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py
index 2b71803..3778162 100644
--- a/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py
@@ -27,7 +27,6 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
-import subprocess
import StringIO
import tempfile
import unittest
diff --git a/WebKitTools/Scripts/webkitpy/common/system/executive.py b/WebKitTools/Scripts/webkitpy/common/system/executive.py
index b6126e4..11eb051 100644
--- a/WebKitTools/Scripts/webkitpy/common/system/executive.py
+++ b/WebKitTools/Scripts/webkitpy/common/system/executive.py
@@ -87,10 +87,20 @@ def run_command(*args, **kwargs):
class Executive(object):
+ def _should_close_fds(self):
+ # We need to pass close_fds=True to work around Python bug #2320
+ # (otherwise we can hang when we kill DumpRenderTree when we are running
+ # multiple threads). See http://bugs.python.org/issue2320 .
+ # Note that close_fds isn't supported on Windows, but this bug only
+ # shows up on Mac and Linux.
+ return sys.platform not in ('win32', 'cygwin')
+
def _run_command_with_teed_output(self, args, teed_output):
+ args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int())
child_process = subprocess.Popen(args,
stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ stderr=subprocess.STDOUT,
+ close_fds=self._should_close_fds())
# Use our own custom wait loop because Popen ignores a tee'd
# stderr/stdout.
@@ -98,15 +108,24 @@ class Executive(object):
while True:
output_line = child_process.stdout.readline()
if output_line == "" and child_process.poll() != None:
+ # poll() is not threadsafe and can throw OSError due to:
+ # http://bugs.python.org/issue1731717
return child_process.poll()
+ # We assume that the child process wrote to us in utf-8,
+ # so no re-encoding is necessary before writing here.
teed_output.write(output_line)
- def run_and_throw_if_fail(self, args, quiet=False):
+ # FIXME: Remove this deprecated method and move callers to run_command.
+ # FIXME: This method is a hack to allow running command which both
+ # capture their output and print out to stdin. Useful for things
+ # like "build-webkit" where we want to display to the user that we're building
+ # but still have the output to stuff into a log file.
+ def run_and_throw_if_fail(self, args, quiet=False, decode_output=True):
# Cache the child's output locally so it can be used for error reports.
child_out_file = StringIO.StringIO()
tee_stdout = sys.stdout
if quiet:
- dev_null = open(os.devnull, "w")
+ dev_null = open(os.devnull, "w") # FIXME: Does this need an encoding?
tee_stdout = dev_null
child_stdout = tee(child_out_file, tee_stdout)
exit_code = self._run_command_with_teed_output(args, child_stdout)
@@ -116,6 +135,10 @@ class Executive(object):
child_output = child_out_file.getvalue()
child_out_file.close()
+ # We assume the child process output utf-8
+ if decode_output:
+ child_output = child_output.decode("utf-8")
+
if exit_code:
raise ScriptError(script_args=args,
exit_code=exit_code,
@@ -140,17 +163,39 @@ class Executive(object):
return 2
def kill_process(self, pid):
+ """Attempts to kill the given pid.
+ Will fail silently if pid does not exist or insufficient permisssions."""
if platform.system() == "Windows":
# According to http://docs.python.org/library/os.html
# os.kill isn't available on Windows. However, when I tried it
# using Cygwin, it worked fine. We should investigate whether
# we need this platform specific code here.
- subprocess.call(('taskkill.exe', '/f', '/pid', str(pid)),
- stdin=open(os.devnull, 'r'),
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
+ command = ["taskkill.exe", "/f", "/pid", str(pid)]
+ # taskkill will exit 128 if the process is not found.
+ self.run_command(command, error_handler=self.ignore_error)
return
- os.kill(pid, signal.SIGKILL)
+ try:
+ os.kill(pid, signal.SIGKILL)
+ except OSError, e:
+ # FIXME: We should make non-silent failure an option.
+ pass
+
+ def kill_all(self, process_name):
+ """Attempts to kill processes matching process_name.
+ Will fail silently if no process are found."""
+ if platform.system() == "Windows":
+ # We might want to automatically append .exe?
+ command = ["taskkill.exe", "/f", "/im", process_name]
+ # taskkill will exit 128 if the process is not found.
+ self.run_command(command, error_handler=self.ignore_error)
+ return
+
+ # FIXME: This is inconsistent that kill_all uses TERM and kill_process
+ # uses KILL. Windows is always using /f (which seems like -KILL).
+ # We should pick one mode, or add support for switching between them.
+ # Note: Mac OS X 10.6 requires -SIGNALNAME before -u USER
+ command = ["killall", "-TERM", "-u", os.getenv("USER"), process_name]
+ self.run_command(command, error_handler=self.ignore_error)
# Error handlers do not need to be static methods once all callers are
# updated to use an Executive object.
@@ -163,38 +208,51 @@ class Executive(object):
def ignore_error(error):
pass
- # FIXME: This should be merged with run_and_throw_if_fail
+ def _compute_stdin(self, input):
+ """Returns (stdin, string_to_communicate)"""
+ # FIXME: We should be returning /dev/null for stdin
+ # or closing stdin after process creation to prevent
+ # child processes from getting input from the user.
+ if not input:
+ return (None, None)
+ if hasattr(input, "read"): # Check if the input is a file.
+ return (input, None) # Assume the file is in the right encoding.
+
+ # Popen in Python 2.5 and before does not automatically encode unicode objects.
+ # http://bugs.python.org/issue5290
+ # See https://bugs.webkit.org/show_bug.cgi?id=37528
+ # for an example of a regresion caused by passing a unicode string directly.
+ # FIXME: We may need to encode differently on different platforms.
+ if isinstance(input, unicode):
+ input = input.encode("utf-8")
+ return (subprocess.PIPE, input)
+ # FIXME: run_and_throw_if_fail should be merged into this method.
def run_command(self,
args,
cwd=None,
input=None,
error_handler=None,
return_exit_code=False,
- return_stderr=True):
- if hasattr(input, 'read'): # Check if the input is a file.
- stdin = input
- string_to_communicate = None
- else:
- stdin = None
- if input:
- stdin = subprocess.PIPE
- # string_to_communicate seems to need to be a str for proper
- # communication with shell commands.
- # See https://bugs.webkit.org/show_bug.cgi?id=37528
- # For an example of a regresion caused by passing a unicode string through.
- string_to_communicate = str(input)
- if return_stderr:
- stderr = subprocess.STDOUT
- else:
- stderr = None
+ return_stderr=True,
+ decode_output=True):
+ """Popen wrapper for convenience and to work around python bugs."""
+ args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int())
+ stdin, string_to_communicate = self._compute_stdin(input)
+ stderr = subprocess.STDOUT if return_stderr else None
process = subprocess.Popen(args,
stdin=stdin,
stdout=subprocess.PIPE,
stderr=stderr,
- cwd=cwd)
+ cwd=cwd,
+ close_fds=self._should_close_fds())
output = process.communicate(string_to_communicate)[0]
+ # run_command automatically decodes to unicode() unless explicitly told not to.
+ if decode_output:
+ output = output.decode("utf-8")
+ # wait() is not threadsafe and can throw OSError due to:
+ # http://bugs.python.org/issue1731717
exit_code = process.wait()
if return_exit_code:
diff --git a/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py
index ac380f8..ce91269 100644
--- a/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2010 Google Inc. All rights reserved.
# Copyright (C) 2009 Daniel Bates (dbates@intudata.com). All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@@ -27,10 +27,14 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import signal
+import subprocess
+import sys
import unittest
from webkitpy.common.system.executive import Executive, run_command
+
class ExecutiveTest(unittest.TestCase):
def test_run_command_with_bad_command(self):
@@ -38,5 +42,52 @@ class ExecutiveTest(unittest.TestCase):
run_command(["foo_bar_command_blah"], error_handler=Executive.ignore_error, return_exit_code=True)
self.failUnlessRaises(OSError, run_bad_command)
-if __name__ == '__main__':
- unittest.main()
+ def test_run_command_with_unicode(self):
+ """Validate that it is safe to pass unicode() objects
+ to Executive.run* methods, and they will return unicode()
+ objects by default unless decode_output=False"""
+ executive = Executive()
+ unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
+ utf8_tor = unicode_tor.encode("utf-8")
+
+ output = executive.run_command(["cat"], input=unicode_tor)
+ self.assertEquals(output, unicode_tor)
+
+ output = executive.run_command(["echo", "-n", unicode_tor])
+ self.assertEquals(output, unicode_tor)
+
+ output = executive.run_command(["echo", "-n", unicode_tor], decode_output=False)
+ self.assertEquals(output, utf8_tor)
+
+ # Make sure that str() input also works.
+ output = executive.run_command(["cat"], input=utf8_tor, decode_output=False)
+ self.assertEquals(output, utf8_tor)
+
+ # FIXME: We should only have one run* method to test
+ output = executive.run_and_throw_if_fail(["echo", "-n", unicode_tor], quiet=True)
+ self.assertEquals(output, unicode_tor)
+
+ output = executive.run_and_throw_if_fail(["echo", "-n", unicode_tor], quiet=True, decode_output=False)
+ self.assertEquals(output, utf8_tor)
+
+ def test_kill_process(self):
+ executive = Executive()
+ # FIXME: This may need edits to work right on windows.
+ # We use "yes" because it loops forever.
+ process = subprocess.Popen(["yes"], stdout=subprocess.PIPE)
+ self.assertEqual(process.poll(), None) # Process is running
+ executive.kill_process(process.pid)
+ self.assertEqual(process.wait(), -signal.SIGKILL)
+ # Killing again should fail silently.
+ executive.kill_process(process.pid)
+
+ def test_kill_all(self):
+ executive = Executive()
+ # FIXME: This may need edits to work right on windows.
+ # We use "yes" because it loops forever.
+ process = subprocess.Popen(["yes"], stdout=subprocess.PIPE)
+ self.assertEqual(process.poll(), None) # Process is running
+ executive.kill_all("yes")
+ self.assertEqual(process.wait(), -signal.SIGTERM)
+ # Killing again should fail silently.
+ executive.kill_all("yes")
diff --git a/WebKitTools/Scripts/webkitpy/common/system/user.py b/WebKitTools/Scripts/webkitpy/common/system/user.py
index 076f965..64995bb 100644
--- a/WebKitTools/Scripts/webkitpy/common/system/user.py
+++ b/WebKitTools/Scripts/webkitpy/common/system/user.py
@@ -62,11 +62,13 @@ class User(object):
def edit(self, files):
editor = os.environ.get("EDITOR") or "vi"
args = shlex.split(editor)
+ # Note: Not thread safe: http://bugs.python.org/issue2320
subprocess.call(args + files)
def page(self, message):
pager = os.environ.get("PAGER") or "less"
try:
+ # Note: Not thread safe: http://bugs.python.org/issue2320
child_process = subprocess.Popen([pager], stdin=subprocess.PIPE)
child_process.communicate(input=message)
except IOError, e: