diff options
| author | Oswald Buddenhagen <oswald.buddenhagen@qt.io> | 2017-05-30 12:48:17 +0200 |
|---|---|---|
| committer | Oswald Buddenhagen <oswald.buddenhagen@qt.io> | 2017-05-30 12:48:17 +0200 |
| commit | 881da28418d380042aa95a97f0cbd42560a64f7c (patch) | |
| tree | a794dff3274695e99c651902dde93d934ea7a5af /Tools/Scripts/webkitpy/common/checkout | |
| parent | 7e104c57a70fdf551bb3d22a5d637cdcbc69dbea (diff) | |
| parent | 0fcedcd17cc00d3dd44c718b3cb36c1033319671 (diff) | |
| download | qtwebkit-881da28418d380042aa95a97f0cbd42560a64f7c.tar.gz | |
Merge 'wip/next' into dev
Change-Id: Iff9ee5e23bb326c4371ec8ed81d56f2f05d680e9
Diffstat (limited to 'Tools/Scripts/webkitpy/common/checkout')
22 files changed, 0 insertions, 5802 deletions
diff --git a/Tools/Scripts/webkitpy/common/checkout/__init__.py b/Tools/Scripts/webkitpy/common/checkout/__init__.py deleted file mode 100644 index f385ae4f1..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Required for Python to search this directory for module files - -from .checkout import Checkout diff --git a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer.py b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer.py deleted file mode 100644 index d2d53a568..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright (C) 2011, 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 copy -import logging - - -_log = logging.getLogger(__name__) - - -# Yes, it's a hypergraph. -# FIXME: Should this function live with the ports somewhere? -# Perhaps this should move onto PortFactory? -def _baseline_search_hypergraph(host, port_names): - hypergraph = {} - - # These edges in the hypergraph aren't visible on build.webkit.org, - # but they impose constraints on how we optimize baselines. - hypergraph.update(_VIRTUAL_PORTS) - - # FIXME: Should we get this constant from somewhere? - fallback_path = ['LayoutTests'] - - port_factory = host.port_factory - for port_name in port_names: - port = port_factory.get(port_name) - webkit_base = port.webkit_base() - search_path = port.baseline_search_path() - if search_path: - hypergraph[port_name] = [host.filesystem.relpath(path, webkit_base) for path in search_path] + fallback_path - return hypergraph - - -_VIRTUAL_PORTS = { - 'mac-future': ['LayoutTests/platform/mac-future', 'LayoutTests/platform/mac', 'LayoutTests'], - 'win-future': ['LayoutTests/platform/win-future', 'LayoutTests/platform/win', 'LayoutTests'], - 'qt-unknown': ['LayoutTests/platform/qt-unknown', 'LayoutTests/platform/qt', 'LayoutTests'], -} - - -# FIXME: Should this function be somewhere more general? -def _invert_dictionary(dictionary): - inverted_dictionary = {} - for key, value in dictionary.items(): - if inverted_dictionary.get(value): - inverted_dictionary[value].append(key) - else: - inverted_dictionary[value] = [key] - return inverted_dictionary - - -class BaselineOptimizer(object): - def __init__(self, host, port_names): - self._host = host - self._filesystem = self._host.filesystem - self._scm = self._host.scm() - self._hypergraph = _baseline_search_hypergraph(host, port_names) - self._directories = reduce(set.union, map(set, self._hypergraph.values())) - - def read_results_by_directory(self, baseline_name): - results_by_directory = {} - for directory in self._directories: - path = self._filesystem.join(self._scm.checkout_root, directory, baseline_name) - if self._filesystem.exists(path): - results_by_directory[directory] = self._filesystem.sha1(path) - return results_by_directory - - def _results_by_port_name(self, results_by_directory): - results_by_port_name = {} - for port_name, search_path in self._hypergraph.items(): - for directory in search_path: - if directory in results_by_directory: - results_by_port_name[port_name] = results_by_directory[directory] - break - return results_by_port_name - - def _most_specific_common_directory(self, port_names): - paths = [self._hypergraph[port_name] for port_name in port_names] - common_directories = reduce(set.intersection, map(set, paths)) - - def score(directory): - return sum([path.index(directory) for path in paths]) - - _, directory = sorted([(score(directory), directory) for directory in common_directories])[0] - return directory - - def _filter_port_names_by_result(self, predicate, port_names_by_result): - filtered_port_names_by_result = {} - for result, port_names in port_names_by_result.items(): - filtered_port_names = filter(predicate, port_names) - if filtered_port_names: - filtered_port_names_by_result[result] = filtered_port_names - return filtered_port_names_by_result - - def _place_results_in_most_specific_common_directory(self, port_names_by_result, results_by_directory): - for result, port_names in port_names_by_result.items(): - directory = self._most_specific_common_directory(port_names) - results_by_directory[directory] = result - - def _find_optimal_result_placement(self, baseline_name): - results_by_directory = self.read_results_by_directory(baseline_name) - results_by_port_name = self._results_by_port_name(results_by_directory) - port_names_by_result = _invert_dictionary(results_by_port_name) - - new_results_by_directory = self._optimize_by_most_specific_common_directory(results_by_directory, results_by_port_name, port_names_by_result) - if not new_results_by_directory: - new_results_by_directory = self._optimize_by_pushing_results_up(results_by_directory, results_by_port_name, port_names_by_result) - - return results_by_directory, new_results_by_directory - - def _optimize_by_most_specific_common_directory(self, results_by_directory, results_by_port_name, port_names_by_result): - new_results_by_directory = {} - unsatisfied_port_names_by_result = port_names_by_result - while unsatisfied_port_names_by_result: - self._place_results_in_most_specific_common_directory(unsatisfied_port_names_by_result, new_results_by_directory) - new_results_by_port_name = self._results_by_port_name(new_results_by_directory) - - def is_unsatisfied(port_name): - return results_by_port_name[port_name] != new_results_by_port_name[port_name] - - new_unsatisfied_port_names_by_result = self._filter_port_names_by_result(is_unsatisfied, port_names_by_result) - - if len(new_unsatisfied_port_names_by_result.values()) >= len(unsatisfied_port_names_by_result.values()): - return {} # Frowns. We do not appear to be converging. - unsatisfied_port_names_by_result = new_unsatisfied_port_names_by_result - - return new_results_by_directory - - def _optimize_by_pushing_results_up(self, results_by_directory, results_by_port_name, port_names_by_result): - try: - results_by_directory = results_by_directory - best_so_far = results_by_directory - while True: - new_results_by_directory = copy.copy(best_so_far) - for port_name in self._hypergraph.keys(): - fallback_path = self._hypergraph[port_name] - current_index, current_directory = self._find_in_fallbackpath(fallback_path, results_by_port_name[port_name], best_so_far) - current_result = results_by_port_name[port_name] - for index in range(current_index + 1, len(fallback_path)): - new_directory = fallback_path[index] - if not new_directory in new_results_by_directory: - new_results_by_directory[new_directory] = current_result - if current_directory in new_results_by_directory: - del new_results_by_directory[current_directory] - elif new_results_by_directory[new_directory] == current_result: - if current_directory in new_results_by_directory: - del new_results_by_directory[current_directory] - else: - # The new_directory contains a different result, so stop trying to push results up. - break - - if len(new_results_by_directory) >= len(best_so_far): - # We've failed to improve, so give up. - break - best_so_far = new_results_by_directory - - return best_so_far - except KeyError as e: - # FIXME: KeyErrors get raised if we're missing baselines. We should handle this better. - return {} - - def _find_in_fallbackpath(self, fallback_path, current_result, results_by_directory): - for index, directory in enumerate(fallback_path): - if directory in results_by_directory and (results_by_directory[directory] == current_result): - return index, directory - assert False, "result %s not found in fallback_path %s, %s" % (current_result, fallback_path, results_by_directory) - - def _filtered_results_by_port_name(self, results_by_directory): - results_by_port_name = self._results_by_port_name(results_by_directory) - for port_name in _VIRTUAL_PORTS.keys(): - if port_name in results_by_port_name: - del results_by_port_name[port_name] - return results_by_port_name - - def _platform(self, filename): - platform_dir = 'LayoutTests' + self._filesystem.sep + 'platform' + self._filesystem.sep - if filename.startswith(platform_dir): - return filename.replace(platform_dir, '').split(self._filesystem.sep)[0] - platform_dir = self._filesystem.join(self._scm.checkout_root, platform_dir) - if filename.startswith(platform_dir): - return filename.replace(platform_dir, '').split(self._filesystem.sep)[0] - return '(generic)' - - def _move_baselines(self, baseline_name, results_by_directory, new_results_by_directory): - data_for_result = {} - for directory, result in results_by_directory.items(): - if not result in data_for_result: - source = self._filesystem.join(self._scm.checkout_root, directory, baseline_name) - data_for_result[result] = self._filesystem.read_binary_file(source) - - file_names = [] - for directory, result in results_by_directory.items(): - if new_results_by_directory.get(directory) != result: - file_names.append(self._filesystem.join(self._scm.checkout_root, directory, baseline_name)) - if file_names: - _log.debug(" Deleting:") - for platform_dir in sorted(self._platform(filename) for filename in file_names): - _log.debug(" " + platform_dir) - self._scm.delete_list(file_names) - else: - _log.debug(" (Nothing to delete)") - - file_names = [] - for directory, result in new_results_by_directory.items(): - if results_by_directory.get(directory) != result: - destination = self._filesystem.join(self._scm.checkout_root, directory, baseline_name) - self._filesystem.maybe_make_directory(self._filesystem.split(destination)[0]) - self._filesystem.write_binary_file(destination, data_for_result[result]) - file_names.append(destination) - if file_names: - _log.debug(" Adding:") - for platform_dir in sorted(self._platform(filename) for filename in file_names): - _log.debug(" " + platform_dir) - self._scm.add_list(file_names) - else: - _log.debug(" (Nothing to add)") - - def directories_by_result(self, baseline_name): - results_by_directory = self.read_results_by_directory(baseline_name) - return _invert_dictionary(results_by_directory) - - def write_by_directory(self, results_by_directory, writer, indent): - for path in sorted(results_by_directory): - writer("%s%s: %s" % (indent, self._platform(path), results_by_directory[path][0:6])) - - def optimize(self, baseline_name): - basename = self._filesystem.basename(baseline_name) - results_by_directory, new_results_by_directory = self._find_optimal_result_placement(baseline_name) - self.new_results_by_directory = new_results_by_directory - if new_results_by_directory == results_by_directory: - if new_results_by_directory: - _log.debug(" %s: (already optimal)" % basename) - self.write_by_directory(results_by_directory, _log.debug, " ") - else: - _log.debug(" %s: (no baselines found)" % basename) - return True - if self._filtered_results_by_port_name(results_by_directory) != self._filtered_results_by_port_name(new_results_by_directory): - _log.warning(" %s: optimization failed" % basename) - self.write_by_directory(results_by_directory, _log.warning, " ") - return False - - _log.debug(" %s:" % basename) - _log.debug(" Before: ") - self.write_by_directory(results_by_directory, _log.debug, " ") - _log.debug(" After: ") - self.write_by_directory(new_results_by_directory, _log.debug, " ") - - self._move_baselines(baseline_name, results_by_directory, new_results_by_directory) - return True diff --git a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py deleted file mode 100644 index dcd649a5a..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2011 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 sys -import unittest2 as unittest - -from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer -from webkitpy.common.system.filesystem_mock import MockFileSystem -from webkitpy.common.host_mock import MockHost - - -class TestBaselineOptimizer(BaselineOptimizer): - def __init__(self, mock_results_by_directory): - host = MockHost() - BaselineOptimizer.__init__(self, host, host.port_factory.all_port_names()) - self._mock_results_by_directory = mock_results_by_directory - - # We override this method for testing so we don't have to construct an - # elaborate mock file system. - def read_results_by_directory(self, baseline_name): - return self._mock_results_by_directory - - def _move_baselines(self, baseline_name, results_by_directory, new_results_by_directory): - self.new_results_by_directory = new_results_by_directory - - -class BaselineOptimizerTest(unittest.TestCase): - def _assertOptimization(self, results_by_directory, expected_new_results_by_directory): - baseline_optimizer = TestBaselineOptimizer(results_by_directory) - self.assertTrue(baseline_optimizer.optimize('mock-baseline.png')) - self.assertEqual(baseline_optimizer.new_results_by_directory, expected_new_results_by_directory) - - def _assertOptimizationFailed(self, results_by_directory): - baseline_optimizer = TestBaselineOptimizer(results_by_directory) - self.assertFalse(baseline_optimizer.optimize('mock-baseline.png')) - - def test_move_baselines(self): - host = MockHost() - host.filesystem.write_binary_file('/mock-checkout/LayoutTests/platform/mac-lion/another/test-expected.txt', 'result A') - host.filesystem.write_binary_file('/mock-checkout/LayoutTests/platform/mac-lion-wk2/another/test-expected.txt', 'result A') - host.filesystem.write_binary_file('/mock-checkout/LayoutTests/platform/mac/another/test-expected.txt', 'result B') - baseline_optimizer = BaselineOptimizer(host, host.port_factory.all_port_names()) - baseline_optimizer._move_baselines('another/test-expected.txt', { - 'LayoutTests/platform/mac-lion': 'aaa', - 'LayoutTests/platform/mac-lion-wk2': 'aaa', - 'LayoutTests/platform/mac': 'bbb', - }, { - 'LayoutTests/platform/mac': 'aaa', - }) - self.assertEqual(host.filesystem.read_binary_file('/mock-checkout/LayoutTests/platform/mac/another/test-expected.txt'), 'result A') - - def test_efl(self): - self._assertOptimization({ - 'LayoutTests/platform/efl': '462d03b9c025db1b0392d7453310dbee5f9a9e74', - }, { - 'LayoutTests/platform/efl': '462d03b9c025db1b0392d7453310dbee5f9a9e74', - }) - - def test_no_add_mac_future(self): - self._assertOptimization({ - 'LayoutTests/platform/mac': '29a1715a6470d5dd9486a142f609708de84cdac8', - 'LayoutTests/platform/win-xp': '453e67177a75b2e79905154ece0efba6e5bfb65d', - 'LayoutTests/platform/mac-lion': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f', - }, { - 'LayoutTests/platform/mac': '29a1715a6470d5dd9486a142f609708de84cdac8', - 'LayoutTests/platform/win-xp': '453e67177a75b2e79905154ece0efba6e5bfb65d', - 'LayoutTests/platform/mac-lion': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f', - }) - - def test_mac_future(self): - self._assertOptimization({ - 'LayoutTests/platform/mac-lion': '462d03b9c025db1b0392d7453310dbee5f9a9e74', - }, { - 'LayoutTests/platform/mac-lion': '462d03b9c025db1b0392d7453310dbee5f9a9e74', - }) - - def test_qt_unknown(self): - self._assertOptimization({ - 'LayoutTests/platform/qt': '462d03b9c025db1b0392d7453310dbee5f9a9e74', - }, { - 'LayoutTests/platform/qt': '462d03b9c025db1b0392d7453310dbee5f9a9e74', - }) - - def test_win_does_not_drop_to_win_7sp0(self): - self._assertOptimization({ - 'LayoutTests/platform/win': '1', - 'LayoutTests/platform/mac': '2', - 'LayoutTests/platform/gtk': '3', - 'LayoutTests/platform/qt': '4', - }, { - 'LayoutTests/platform/win': '1', - 'LayoutTests/platform/mac': '2', - 'LayoutTests/platform/gtk': '3', - 'LayoutTests/platform/qt': '4', - }) - - def test_common_directory_includes_root(self): - # This test case checks that we don't throw an exception when we fail - # to optimize. - self._assertOptimizationFailed({ - 'LayoutTests/platform/gtk': 'e8608763f6241ddacdd5c1ef1973ba27177d0846', - 'LayoutTests/platform/qt': 'bcbd457d545986b7abf1221655d722363079ac87', - 'LayoutTests/platform/mac': 'e8608763f6241ddacdd5c1ef1973ba27177d0846', - }) - - self._assertOptimization({ - 'LayoutTests': '9c876f8c3e4cc2aef9519a6c1174eb3432591127', - }, { - 'LayoutTests': '9c876f8c3e4cc2aef9519a6c1174eb3432591127', - }) - - def test_complex_shadowing(self): - # This test relies on OS specific functionality, so it doesn't work on Windows. - # FIXME: What functionality does this rely on? When can we remove this if? - if sys.platform == 'win32': - return - self._assertOptimization({ - 'LayoutTests/platform/mac': '5daa78e55f05d9f0d1bb1f32b0cd1bc3a01e9364', - 'LayoutTests/platform/mac-lion': '7ad045ece7c030e2283c5d21d9587be22bcba56e', - 'LayoutTests/platform/win-xp': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7', - }, { - 'LayoutTests/platform/mac': '5daa78e55f05d9f0d1bb1f32b0cd1bc3a01e9364', - 'LayoutTests/platform/mac-lion': '7ad045ece7c030e2283c5d21d9587be22bcba56e', - 'LayoutTests/platform/win-xp': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7', - }) - - def test_virtual_ports_filtered(self): - self._assertOptimization({ - 'LayoutTests/platform/gtk': '3', - 'LayoutTests/platform/efl': '3', - 'LayoutTests/platform/qt': '4', - 'LayoutTests/platform/mac': '5', - }, { - 'LayoutTests': '3', - 'LayoutTests/platform/qt': '4', - 'LayoutTests/platform/mac': '5', - }) diff --git a/Tools/Scripts/webkitpy/common/checkout/changelog.py b/Tools/Scripts/webkitpy/common/checkout/changelog.py deleted file mode 100644 index 47c6b64c5..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/changelog.py +++ /dev/null @@ -1,459 +0,0 @@ -# Copyright (C) 2009, 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. -# -# WebKit's Python module for parsing and modifying ChangeLog files - -import logging -import re -from StringIO import StringIO -import textwrap - -from webkitpy.common.config.committers import CommitterList -from webkitpy.common.system.filesystem import FileSystem -import webkitpy.common.config.urls as config_urls - -_log = logging.getLogger(__name__) - - -# FIXME: parse_bug_id_from_changelog should not be a free function. -# Parse the bug ID out of a Changelog message based on the format that is -# used by prepare-ChangeLog -def parse_bug_id_from_changelog(message): - if not message: - return None - match = re.search("^\s*" + config_urls.bug_url_short + "$", message, re.MULTILINE) - if match: - return int(match.group('bug_id')) - match = re.search("^\s*" + config_urls.bug_url_long + "$", message, re.MULTILINE) - if match: - return int(match.group('bug_id')) - # We weren't able to find a bug URL in the format used by prepare-ChangeLog. Fall back to the - # first bug URL found anywhere in the message. - return config_urls.parse_bug_id(message) - - -class ChangeLogEntry(object): - # e.g. 2009-06-03 Eric Seidel <eric@webkit.org> - date_line_regexp = r'^(?P<date>\d{4}-\d{2}-\d{2})\s+(?P<authors>(?P<name>[^<]+?)\s+<(?P<email>[^<>]+)>.*?)$' - - # e.g. * Source/WebCore/page/EventHandler.cpp: Implement FooBarQuux. - touched_files_regexp = r'^\s*\*\s*(?P<file>[A-Za-z0-9_\-\./\\]+)\s*\:' - # e.g. (ChangeLogEntry.touched_functions): Added. - touched_functions_regexp = r'^\s*\((?P<function>[^)]*)\):' - - # e.g. Reviewed by Darin Adler. - # (Discard everything after the first period to match more invalid lines.) - reviewed_by_regexp = r'^\s*((\w+\s+)+and\s+)?(Review|Rubber(\s*|-)stamp)(s|ed)?\s+([a-z]+\s+)*?by\s+(?P<reviewer>.*?)[\.,]?\s*$' - - reviewed_byless_regexp = r'^\s*((Review|Rubber(\s*|-)stamp)(s|ed)?|RS)(\s+|\s*=\s*)(?P<reviewer>([A-Z]\w+\s*)+)[\.,]?\s*$' - - reviewer_name_noise_regexp = re.compile(r""" - (\s+((tweaked\s+)?and\s+)?(landed|committed|okayed)\s+by.+) # "landed by", "commented by", etc... - |(^(Reviewed\s+)?by\s+) # extra "Reviewed by" or "by" - |([(<]\s*[\w_\-\.]+@[\w_\-\.]+[>)]) # email addresses - |([(<](https?://?bugs.)webkit.org[^>)]+[>)]) # bug url - |("[^"]+") # wresler names like 'Sean/Shawn/Shaun' in 'Geoffrey "Sean/Shawn/Shaun" Garen' - |('[^']+') # wresler names like "The Belly" in "Sam 'The Belly' Weinig" - |((Mr|Ms|Dr|Mrs|Prof)\.(\s+|$)) - """, re.IGNORECASE | re.VERBOSE) - - reviewer_name_casesensitive_noise_regexp = re.compile(r""" - ((\s+|^)(and\s+)?([a-z-]+\s+){5,}by\s+) # e.g. "and given a good once-over by" - |(\(\s*(?!(and|[A-Z])).+\)) # any parenthesis that doesn't start with "and" or a capital letter - |(with(\s+[a-z-]+)+) # phrases with "with no hesitation" in "Sam Weinig with no hesitation" - """, re.VERBOSE) - - reviewer_name_noise_needing_a_backreference_regexp = re.compile(r""" - (\S\S)\.(?:(\s.+|$)) # Text after the two word characters (don't match initials) and a period followed by a space. - """, re.IGNORECASE | re.VERBOSE) - - nobody_regexp = re.compile(r"""(\s+|^)nobody( - ((,|\s+-)?\s+(\w+\s+)+fix.*) # e.g. nobody, build fix... - |(\s*\([^)]+\).*) # NOBODY (..)... - |$)""", re.IGNORECASE | re.VERBOSE) - - # e.g. == Rolled over to ChangeLog-2011-02-16 == - rolled_over_regexp = r'^== Rolled over to ChangeLog-\d{4}-\d{2}-\d{2} ==$' - - # e.g. git-svn-id: http://svn.webkit.org/repository/webkit/trunk@96161 268f45cc-cd09-0410-ab3c-d52691b4dbfc - svn_id_regexp = r'git-svn-id: http://svn.webkit.org/repository/webkit/trunk@(?P<svnid>\d+) ' - - split_names_regexp = r'\s*(?:,(?:\s+and\s+|&)?|(?:^|\s+)and\s+|&&|[/+&])\s*' - - def __init__(self, contents, committer_list=CommitterList(), revision=None): - self._contents = contents - self._committer_list = committer_list - self._revision = revision - self._parse_entry() - - @classmethod - def _parse_reviewer_text(cls, text): - match = re.search(ChangeLogEntry.reviewed_by_regexp, text, re.MULTILINE | re.IGNORECASE) - if not match: - # There are cases where people omit "by". We match it only if reviewer part looked nice - # in order to avoid matching random lines that start with Reviewed - match = re.search(ChangeLogEntry.reviewed_byless_regexp, text, re.MULTILINE | re.IGNORECASE) - if not match: - return None, None - - reviewer_text = match.group("reviewer") - - reviewer_text = ChangeLogEntry.nobody_regexp.sub('', reviewer_text) - reviewer_text = ChangeLogEntry.reviewer_name_noise_regexp.sub('', reviewer_text) - reviewer_text = ChangeLogEntry.reviewer_name_casesensitive_noise_regexp.sub('', reviewer_text) - reviewer_text = ChangeLogEntry.reviewer_name_noise_needing_a_backreference_regexp.sub(r'\1', reviewer_text) - reviewer_text = reviewer_text.replace('(', '').replace(')', '') - reviewer_text = re.sub(r'\s\s+|[,.]\s*$', ' ', reviewer_text).strip() - if not len(reviewer_text): - return None, None - - reviewer_list = ChangeLogEntry._split_reviewer_names(reviewer_text) - - # Get rid of "reviewers" like "even though this is just a..." in "Reviewed by Sam Weinig, even though this is just a..." - # and "who wrote the original code" in "Noam Rosenthal, who wrote the original code" - reviewer_list = [reviewer for reviewer in reviewer_list if not re.match('^who\s|^([a-z]+(\s+|\.|$)){6,}$', reviewer)] - - return reviewer_text, reviewer_list - - @classmethod - def _split_reviewer_names(cls, text): - return re.split(ChangeLogEntry.split_names_regexp, text) - - @classmethod - def _split_author_names_with_emails(cls, text): - regex = '>' + ChangeLogEntry.split_names_regexp - names = re.split(regex, text) - if len(names) > 1: - names = [name + ">" for name in names[:-1]] + [names[-1]] - return names - - def _fuzz_match_reviewers(self, reviewers_text_list): - if not reviewers_text_list: - return [] - list_of_reviewers = [self._committer_list.contributors_by_fuzzy_match(reviewer)[0] for reviewer in reviewers_text_list] - # Flatten lists and get rid of any reviewers with more than one candidate. - return [reviewers[0] for reviewers in list_of_reviewers if len(reviewers) == 1] - - @classmethod - def _parse_author_name_and_email(cls, author_name_and_email): - match = re.match(r'(?P<name>.+?)\s+<(?P<email>[^>]+)>', author_name_and_email) - return {'name': match.group("name"), 'email': match.group("email")} - - @classmethod - def _parse_author_text(cls, text): - if not text: - return [] - authors = cls._split_author_names_with_emails(text) - assert(authors and len(authors) >= 1) - return [cls._parse_author_name_and_email(author) for author in authors] - - @classmethod - def _parse_touched_functions(cls, text): - result = {} - cur_file = None - for line in text.splitlines(): - file_match = re.match(cls.touched_files_regexp, line) - if file_match: - cur_file = file_match.group("file") - result[cur_file] = [] - func_match = re.match(cls.touched_functions_regexp, line) - if func_match and cur_file: - result[cur_file].append(func_match.group("function")) - return result - - @classmethod - def _parse_bug_description(cls, text): - # If line 4 is a bug url, line 3 is the bug description. - # It's too hard to guess in other cases, so we return None. - lines = text.splitlines() - if len(lines) < 4: - return None - for bug_url in (config_urls.bug_url_short, config_urls.bug_url_long): - if re.match("^\s*" + bug_url + "$", lines[3]): - return lines[2].strip() - return None - - def _parse_entry(self): - match = re.match(self.date_line_regexp, self._contents, re.MULTILINE) - if not match: - _log.warning("Creating invalid ChangeLogEntry:\n%s" % self._contents) - - self._date_line = match.group() - self._date = match.group("date") - self._bug_description = self._parse_bug_description(self._contents) - - # FIXME: group("name") does not seem to be Unicode? Probably due to self._contents not being unicode. - self._author_text = match.group("authors") if match else None - self._authors = ChangeLogEntry._parse_author_text(self._author_text) - - self._reviewer_text, self._reviewers_text_list = ChangeLogEntry._parse_reviewer_text(self._contents) - self._reviewers = self._fuzz_match_reviewers(self._reviewers_text_list) - self._author = self._committer_list.contributor_by_email(self.author_email()) or self._committer_list.contributor_by_name(self.author_name()) - - self._touched_files = re.findall(self.touched_files_regexp, self._contents, re.MULTILINE) - self._touched_functions = self._parse_touched_functions(self._contents) - - def date_line(self): - return self._date_line - - def date(self): - return self._date - - def author_text(self): - return self._author_text - - def revision(self): - return self._revision - - def author_name(self): - return self._authors[0]['name'] - - def author_email(self): - return self._authors[0]['email'] - - def author(self): - return self._author # Might be None - - def authors(self): - return self._authors - - # FIXME: Eventually we would like to map reviwer names to reviewer objects. - # See https://bugs.webkit.org/show_bug.cgi?id=26533 - def reviewer_text(self): - return self._reviewer_text - - # Might be None, might also not be a Reviewer! - def reviewer(self): - return self._reviewers[0] if len(self._reviewers) > 0 else None - - def reviewers(self): - return self._reviewers - - def has_valid_reviewer(self): - if self._reviewers_text_list: - for reviewer in self._reviewers_text_list: - reviewer = self._committer_list.committer_by_name(reviewer) - if reviewer: - return True - return bool(re.search("unreviewed", self._contents, re.IGNORECASE)) - - def contents(self): - return self._contents - - def bug_id(self): - return parse_bug_id_from_changelog(self._contents) - - def bug_description(self): - return self._bug_description - - def touched_files(self): - return self._touched_files - - # Returns a dict from file name to lists of function names. - def touched_functions(self): - return self._touched_functions - - def touched_files_text(self): - match = re.search(self.touched_files_regexp, self._contents, re.MULTILINE) - return self._contents[match.start():].lstrip("\n\r") if match else "" - - # Determine if any text has been added to the section on touched files - def is_touched_files_text_clean(self): - file_line_end = r"( (Added|Removed|(Copied|Renamed) from [A-Za-z0-9_\-./\\]+).)?$" - for line in self.touched_files_text().splitlines(): - if re.match(self.touched_files_regexp + file_line_end, line): - continue - if re.match(self.touched_functions_regexp + "$", line): - continue - return False - return True - -# FIXME: Various methods on ChangeLog should move into ChangeLogEntry instead. -class ChangeLog(object): - - def __init__(self, path, filesystem=None): - self.path = path - self._filesystem = filesystem or FileSystem() - - _changelog_indent = " " * 8 - - @classmethod - def parse_latest_entry_from_file(cls, changelog_file): - try: - return next(cls.parse_entries_from_file(changelog_file)) - except StopIteration, e: - return None - - svn_blame_regexp = re.compile(r'^(\s*(?P<revision>\d+) [^ ]+)\s*(?P<line>.*?\n)') - - @classmethod - def _separate_revision_and_line(cls, line): - match = cls.svn_blame_regexp.match(line) - if not match: - return None, line - return int(match.group('revision')), match.group('line') - - @classmethod - def parse_entries_from_file(cls, changelog_file): - """changelog_file must be a file-like object which returns - unicode strings, e.g. from StringIO(unicode()) or - fs.open_text_file_for_reading()""" - date_line_regexp = re.compile(ChangeLogEntry.date_line_regexp) - rolled_over_regexp = re.compile(ChangeLogEntry.rolled_over_regexp) - - # The first line should be a date line. - revision, first_line = cls._separate_revision_and_line(changelog_file.readline()) - assert(isinstance(first_line, unicode)) - if not date_line_regexp.match(cls.svn_blame_regexp.sub('', first_line)): - raise StopIteration - - entry_lines = [first_line] - revisions_in_entry = {revision: 1} if revision != None else None - for line in changelog_file: - if revisions_in_entry: - revision, line = cls._separate_revision_and_line(line) - - if rolled_over_regexp.match(line): - break - - if date_line_regexp.match(line): - most_probable_revision = max(revisions_in_entry, key=revisions_in_entry.__getitem__) if revisions_in_entry else None - # Remove the extra newline at the end - yield ChangeLogEntry(''.join(entry_lines[:-1]), revision=most_probable_revision) - entry_lines = [] - revisions_in_entry = {revision: 0} - - entry_lines.append(line) - if revisions_in_entry: - revisions_in_entry[revision] = revisions_in_entry.get(revision, 0) + 1 - - most_probable_revision = max(revisions_in_entry, key=revisions_in_entry.__getitem__) if revisions_in_entry else None - yield ChangeLogEntry(''.join(entry_lines[:-1]), revision=most_probable_revision) - - def latest_entry(self): - # ChangeLog files are always UTF-8, we read them in as such to support Reviewers with unicode in their names. - changelog_file = self._filesystem.open_text_file_for_reading(self.path) - try: - return self.parse_latest_entry_from_file(changelog_file) - finally: - changelog_file.close() - - # _wrap_line and _wrap_lines exist to work around - # http://bugs.python.org/issue1859 - - def _wrap_line(self, line): - return textwrap.fill(line, - width=70, - initial_indent=self._changelog_indent, - # Don't break urls which may be longer than width. - break_long_words=False, - subsequent_indent=self._changelog_indent) - - # Workaround as suggested by guido in - # http://bugs.python.org/issue1859#msg60040 - - def _wrap_lines(self, message): - lines = [self._wrap_line(line) for line in message.splitlines()] - return "\n".join(lines) - - def update_with_unreviewed_message(self, message): - first_boilerplate_line_regexp = re.compile( - "%sNeed a short description \(OOPS!\)\." % self._changelog_indent) - removing_boilerplate = False - result = StringIO() - with self._filesystem.open_text_file_for_reading(self.path) as file: - for line in file: - if first_boilerplate_line_regexp.search(line): - message_lines = self._wrap_lines(message) - result.write(first_boilerplate_line_regexp.sub(message_lines, line)) - # Remove all the ChangeLog boilerplate before the first changed - # file. - removing_boilerplate = True - elif removing_boilerplate: - if line.find('*') >= 0: # each changed file is preceded by a * - removing_boilerplate = False - - if not removing_boilerplate: - result.write(line) - self._filesystem.write_text_file(self.path, result.getvalue()) - - def set_reviewer(self, reviewer): - latest_entry = self.latest_entry() - latest_entry_contents = latest_entry.contents() - reviewer_text = latest_entry.reviewer() - found_nobody = re.search("NOBODY\s*\(OOPS!\)", latest_entry_contents, re.MULTILINE) - - if not found_nobody and not reviewer_text: - bug_url_number_of_items = len(re.findall(config_urls.bug_url_long, latest_entry_contents, re.MULTILINE)) - bug_url_number_of_items += len(re.findall(config_urls.bug_url_short, latest_entry_contents, re.MULTILINE)) - result = StringIO() - with self._filesystem.open_text_file_for_reading(self.path) as file: - for line in file: - found_bug_url = re.search(config_urls.bug_url_long, line) - if not found_bug_url: - found_bug_url = re.search(config_urls.bug_url_short, line) - result.write(line) - if found_bug_url: - if bug_url_number_of_items == 1: - result.write("\n Reviewed by %s.\n" % reviewer) - bug_url_number_of_items -= 1 - self._filesystem.write_text_file(self.path, result.getvalue()) - else: - data = self._filesystem.read_text_file(self.path) - newdata = data.replace("NOBODY (OOPS!)", reviewer) - self._filesystem.write_text_file(self.path, newdata) - - def set_short_description_and_bug_url(self, short_description, bug_url): - message = "%s\n%s%s" % (short_description, self._changelog_indent, bug_url) - bug_boilerplate = "%sNeed the bug URL (OOPS!).\n" % self._changelog_indent - result = StringIO() - with self._filesystem.open_text_file_for_reading(self.path) as file: - for line in file: - line = line.replace("Need a short description (OOPS!).", message) - if line != bug_boilerplate: - result.write(line) - self._filesystem.write_text_file(self.path, result.getvalue()) - - def delete_entries(self, num_entries): - date_line_regexp = re.compile(ChangeLogEntry.date_line_regexp) - rolled_over_regexp = re.compile(ChangeLogEntry.rolled_over_regexp) - entries = 0 - result = StringIO() - with self._filesystem.open_text_file_for_reading(self.path) as file: - for line in file: - if date_line_regexp.match(line): - entries += 1 - elif rolled_over_regexp.match(line): - entries = num_entries + 1 - if entries > num_entries: - result.write(line) - self._filesystem.write_text_file(self.path, result.getvalue()) - - def prepend_text(self, text): - data = self._filesystem.read_text_file(self.path) - self._filesystem.write_text_file(self.path, text + data) diff --git a/Tools/Scripts/webkitpy/common/checkout/changelog_unittest.py b/Tools/Scripts/webkitpy/common/checkout/changelog_unittest.py deleted file mode 100644 index 05b21e0d3..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/changelog_unittest.py +++ /dev/null @@ -1,667 +0,0 @@ -# Copyright (C) 2009 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 unittest2 as unittest - -from StringIO import StringIO - -from webkitpy.common.system.filesystem_mock import MockFileSystem -from webkitpy.common.checkout.changelog import * - - -class ChangeLogTest(unittest.TestCase): - - _changelog_path = 'Tools/ChangeLog' - - _example_entry = u'''2009-08-17 Peter Kasting <pkasting@google.com> - - Reviewed by Tor Arne Vestb\xf8. - - https://bugs.webkit.org/show_bug.cgi?id=27323 - Only add Cygwin to the path when it isn't already there. This avoids - causing problems for people who purposefully have non-Cygwin versions of - executables like svn in front of the Cygwin ones in their paths. - - * DumpRenderTree/win/DumpRenderTree.vcproj: - * DumpRenderTree/win/ImageDiff.vcproj: - * DumpRenderTree/win/TestNetscapePlugin/TestNetscapePlugin.vcproj: -''' - - _rolled_over_footer = '== Rolled over to ChangeLog-2009-06-16 ==' - - # More example text than we need. Eventually we need to support parsing this all and write tests for the parsing. - _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() - - Reviewed by David Levin. - - * Scripts/modules/cpp_style.py: - (_ERROR_CATEGORIES): Added 'runtime/max_min_macros'. - (check_max_min_macros): Added. Returns level 4 error when MAX() - and MIN() macros are used in header files and C++ source files. - (check_style): Added call to check_max_min_macros(). - * Scripts/modules/cpp_style_unittest.py: Added unit tests. - (test_max_macro): Added. - (test_min_macro): Added. - -2009-08-16 David Kilzer <ddkilzer@apple.com> - - Backed out r47343 which was mistakenly committed - - * Scripts/bugzilla-tool: - * Scripts/modules/scm.py: - -2009-06-18 Darin Adler <darin@apple.com> - - Rubber stamped by Mark Rowe. - - * DumpRenderTree/mac/DumpRenderTreeWindow.mm: - (-[DumpRenderTreeWindow close]): Resolved crashes seen during regression - tests. The close method can be called on a window that's already closed - so we can't assert here. - -2011-11-04 Benjamin Poulain <bpoulain@apple.com> - - [Mac] ResourceRequest's nsURLRequest() does not differentiate null and empty URLs with CFNetwork - https://bugs.webkit.org/show_bug.cgi?id=71539 - - Reviewed by David Kilzer. - - In order to have CFURL and NSURL to be consistent when both are used on Mac, - KURL::createCFURL() is changed to support empty URL values. - - * This change log entry is made up to test _parse_entry: - * a list of things - - * platform/cf/KURLCFNet.cpp: - (WebCore::createCFURLFromBuffer): - (WebCore::KURL::createCFURL): - * platform/mac/KURLMac.mm : - (WebCore::KURL::operator NSURL *): - (WebCore::KURL::createCFURL): - * WebCoreSupport/ChromeClientEfl.cpp: - (WebCore::ChromeClientEfl::closeWindowSoon): call new function and moves its - previous functionality there. - * ewk/ewk_private.h: - * ewk/ewk_view.cpp: - -2011-03-02 Carol Szabo <carol.szabo@nokia.com> - - Reviewed by David Hyatt <hyatt@apple.com> - - content property doesn't support quotes - https://bugs.webkit.org/show_bug.cgi?id=6503 - - Added full support for quotes as defined by CSS 2.1. - - Tests: fast/css/content/content-quotes-01.html - fast/css/content/content-quotes-02.html - fast/css/content/content-quotes-03.html - fast/css/content/content-quotes-04.html - fast/css/content/content-quotes-05.html - fast/css/content/content-quotes-06.html - -2011-03-31 Brent Fulgham <bfulgham@webkit.org> - - Reviewed Adam Roben. - - [WinCairo] Implement Missing drawWindowsBitmap method. - https://bugs.webkit.org/show_bug.cgi?id=57409 - -2011-03-28 Dirk Pranke <dpranke@chromium.org> - - RS=Tony Chang. - - r81977 moved FontPlatformData.h from - WebCore/platform/graphics/cocoa to platform/graphics. This - change updates the chromium build accordingly. - - https://bugs.webkit.org/show_bug.cgi?id=57281 - - * platform/graphics/chromium/CrossProcessFontLoading.mm: - -2011-05-04 Alexis Menard <alexis.menard@openbossa.org> - - Unreviewed warning fix. - - The variable is just used in the ASSERT macro. Let's use ASSERT_UNUSED to avoid - a warning in Release build. - - * accessibility/AccessibilityRenderObject.cpp: - (WebCore::lastChildConsideringContinuation): - -2011-10-11 Antti Koivisto <antti@apple.com> - - Resolve regular and visited link style in a single pass - https://bugs.webkit.org/show_bug.cgi?id=69838 - - Reviewed by Darin Adler - - We can simplify and speed up selector matching by removing the recursive matching done - to generate the style for the :visited pseudo selector. Both regular and visited link style - can be generated in a single pass through the style selector. - -== Rolled over to ChangeLog-2009-06-16 == -""" - - def test_parse_bug_id_from_changelog(self): - commit_text = ''' -2011-03-23 Ojan Vafai <ojan@chromium.org> - - Add failing result for WebKit2. All tests that require - focus fail on WebKit2. See https://bugs.webkit.org/show_bug.cgi?id=56988. - - * platform/mac-wk2/fast/css/pseudo-any-expected.txt: Added. - - ''' - - self.assertEqual(56988, parse_bug_id_from_changelog(commit_text)) - - commit_text = ''' -2011-03-23 Ojan Vafai <ojan@chromium.org> - - Add failing result for WebKit2. All tests that require - focus fail on WebKit2. See https://bugs.webkit.org/show_bug.cgi?id=56988. - https://bugs.webkit.org/show_bug.cgi?id=12345 - - * platform/mac-wk2/fast/css/pseudo-any-expected.txt: Added. - - ''' - - self.assertEqual(12345, parse_bug_id_from_changelog(commit_text)) - - commit_text = ''' -2011-03-31 Adam Roben <aroben@apple.com> - - Quote the executable path we pass to ::CreateProcessW - - This will ensure that spaces in the path will be interpreted correctly. - - Fixes <http://webkit.org/b/57569> Web process sometimes fails to launch when there are - spaces in its path - - Reviewed by Steve Falkenburg. - - * UIProcess/Launcher/win/ProcessLauncherWin.cpp: - (WebKit::ProcessLauncher::launchProcess): Surround the executable path in quotes. - - ''' - - self.assertEqual(57569, parse_bug_id_from_changelog(commit_text)) - - commit_text = ''' -2011-03-29 Timothy Hatcher <timothy@apple.com> - - Update WebCore Localizable.strings to contain WebCore, WebKit/mac and WebKit2 strings. - - https://webkit.org/b/57354 - - Reviewed by Sam Weinig. - - * English.lproj/Localizable.strings: Updated. - * StringsNotToBeLocalized.txt: Removed. To hard to maintain in WebCore. - * platform/network/cf/LoaderRunLoopCF.h: Remove a single quote in an #error so - extract-localizable-strings does not complain about unbalanced single quotes. - ''' - - self.assertEqual(57354, parse_bug_id_from_changelog(commit_text)) - - def test_parse_log_entries_from_changelog(self): - changelog_file = StringIO(self._example_changelog) - parsed_entries = list(ChangeLog.parse_entries_from_file(changelog_file)) - self.assertEqual(len(parsed_entries), 9) - self.assertEqual(parsed_entries[0].date_line(), u"2009-08-17 Tor Arne Vestb\xf8 <vestbo@webkit.org>") - self.assertEqual(parsed_entries[0].date(), "2009-08-17") - self.assertEqual(parsed_entries[0].reviewer_text(), "David Levin") - self.assertEqual(parsed_entries[0].is_touched_files_text_clean(), False) - self.assertEqual(parsed_entries[1].date_line(), "2009-08-16 David Kilzer <ddkilzer@apple.com>") - self.assertEqual(parsed_entries[1].date(), "2009-08-16") - self.assertEqual(parsed_entries[1].author_email(), "ddkilzer@apple.com") - self.assertEqual(parsed_entries[1].touched_files_text(), " * Scripts/bugzilla-tool:\n * Scripts/modules/scm.py:\n") - self.assertEqual(parsed_entries[1].is_touched_files_text_clean(), True) - self.assertEqual(parsed_entries[2].reviewer_text(), "Mark Rowe") - self.assertEqual(parsed_entries[2].touched_files(), ["DumpRenderTree/mac/DumpRenderTreeWindow.mm"]) - self.assertEqual(parsed_entries[2].touched_functions(), {"DumpRenderTree/mac/DumpRenderTreeWindow.mm": ["-[DumpRenderTreeWindow close]"]}) - self.assertEqual(parsed_entries[2].is_touched_files_text_clean(), False) - self.assertEqual(parsed_entries[3].author_name(), "Benjamin Poulain") - self.assertEqual(parsed_entries[3].touched_files(), ["platform/cf/KURLCFNet.cpp", "platform/mac/KURLMac.mm", - "WebCoreSupport/ChromeClientEfl.cpp", "ewk/ewk_private.h", "ewk/ewk_view.cpp"]) - self.assertEqual(parsed_entries[3].touched_functions(), {"platform/cf/KURLCFNet.cpp": ["WebCore::createCFURLFromBuffer", "WebCore::KURL::createCFURL"], - "platform/mac/KURLMac.mm": ["WebCore::KURL::operator NSURL *", "WebCore::KURL::createCFURL"], - "WebCoreSupport/ChromeClientEfl.cpp": ["WebCore::ChromeClientEfl::closeWindowSoon"], "ewk/ewk_private.h": [], "ewk/ewk_view.cpp": []}) - self.assertEqual(parsed_entries[3].bug_description(), "[Mac] ResourceRequest's nsURLRequest() does not differentiate null and empty URLs with CFNetwork") - self.assertEqual(parsed_entries[4].reviewer_text(), "David Hyatt") - self.assertIsNone(parsed_entries[4].bug_description()) - self.assertEqual(parsed_entries[5].reviewer_text(), "Adam Roben") - self.assertEqual(parsed_entries[6].reviewer_text(), "Tony Chang") - self.assertIsNone(parsed_entries[7].reviewer_text()) - self.assertEqual(parsed_entries[8].reviewer_text(), 'Darin Adler') - - def test_parse_log_entries_from_annotated_file(self): - # Note that there are trailing spaces on some of the lines intentionally. - changelog_file = StringIO(u"100000 ossy@webkit.org 2011-11-11 Csaba Osztrogon\u00e1c <ossy@webkit.org>\n" - u"100000 ossy@webkit.org\n" - u"100000 ossy@webkit.org 100,000 !!!\n" - u"100000 ossy@webkit.org \n" - u"100000 ossy@webkit.org Reviewed by Zoltan Herczeg.\n" - u"100000 ossy@webkit.org \n" - u"100000 ossy@webkit.org * ChangeLog: Point out revision 100,000.\n" - u"100000 ossy@webkit.org \n" - u"93798 ap@apple.com 2011-08-25 Alexey Proskuryakov <ap@apple.com>\n" - u"93798 ap@apple.com \n" - u"93798 ap@apple.com Fix build when GCC 4.2 is not installed.\n" - u"93798 ap@apple.com \n" - u"93798 ap@apple.com * gtest/xcode/Config/CompilerVersion.xcconfig: Copied from Source/WebCore/Configurations/CompilerVersion.xcconfig.\n" - u"93798 ap@apple.com * gtest/xcode/Config/General.xcconfig:\n" - u"93798 ap@apple.com Use the same compiler version as other projects do.\n" - u"93798 ap@apple.com\n" - u"99491 andreas.kling@nokia.com 2011-11-03 Andreas Kling <kling@webkit.org>\n" - u"99491 andreas.kling@nokia.com \n" - u"99190 andreas.kling@nokia.com Unreviewed build fix, sigh.\n" - u"99190 andreas.kling@nokia.com \n" - u"99190 andreas.kling@nokia.com * css/CSSFontFaceRule.h:\n" - u"99190 andreas.kling@nokia.com * css/CSSMutableStyleDeclaration.h:\n" - u"99190 andreas.kling@nokia.com\n" - u"99190 andreas.kling@nokia.com 2011-11-03 Andreas Kling <kling@webkit.org>\n" - u"99190 andreas.kling@nokia.com \n" - u"99187 andreas.kling@nokia.com Unreviewed build fix, out-of-line StyleSheet::parentStyleSheet()\n" - u"99187 andreas.kling@nokia.com again since there's a cycle in the includes between CSSRule/StyleSheet.\n" - u"99187 andreas.kling@nokia.com \n" - u"99187 andreas.kling@nokia.com * css/StyleSheet.cpp:\n" - u"99187 andreas.kling@nokia.com (WebCore::StyleSheet::parentStyleSheet):\n" - u"99187 andreas.kling@nokia.com * css/StyleSheet.h:\n" - u"99187 andreas.kling@nokia.com \n") - - parsed_entries = list(ChangeLog.parse_entries_from_file(changelog_file)) - self.assertEqual(parsed_entries[0].revision(), 100000) - self.assertEqual(parsed_entries[0].reviewer_text(), "Zoltan Herczeg") - self.assertEqual(parsed_entries[0].author_name(), u"Csaba Osztrogon\u00e1c") - self.assertEqual(parsed_entries[0].author_email(), "ossy@webkit.org") - self.assertEqual(parsed_entries[1].revision(), 93798) - self.assertEqual(parsed_entries[1].author_name(), "Alexey Proskuryakov") - self.assertEqual(parsed_entries[2].revision(), 99190) - self.assertEqual(parsed_entries[2].author_name(), "Andreas Kling") - self.assertEqual(parsed_entries[3].revision(), 99187) - self.assertEqual(parsed_entries[3].author_name(), "Andreas Kling") - - def _assert_parse_reviewer_text_and_list(self, text, expected_reviewer_text, expected_reviewer_text_list=None): - reviewer_text, reviewer_text_list = ChangeLogEntry._parse_reviewer_text(text) - self.assertEqual(reviewer_text, expected_reviewer_text) - if expected_reviewer_text_list: - self.assertEqual(reviewer_text_list, expected_reviewer_text_list) - else: - self.assertEqual(reviewer_text_list, [expected_reviewer_text]) - - def _assert_parse_reviewer_text_list(self, text, expected_reviewer_text_list): - reviewer_text, reviewer_text_list = ChangeLogEntry._parse_reviewer_text(text) - self.assertEqual(reviewer_text_list, expected_reviewer_text_list) - - def test_parse_reviewer_text(self): - self._assert_parse_reviewer_text_and_list(' reviewed by Ryosuke Niwa, Oliver Hunt, and Dimitri Glazkov', - 'Ryosuke Niwa, Oliver Hunt, and Dimitri Glazkov', ['Ryosuke Niwa', 'Oliver Hunt', 'Dimitri Glazkov']) - self._assert_parse_reviewer_text_and_list('Reviewed by Brady Eidson and David Levin, landed by Brady Eidson', - 'Brady Eidson and David Levin', ['Brady Eidson', 'David Levin']) - - self._assert_parse_reviewer_text_and_list('Reviewed by Simon Fraser. Committed by Beth Dakin.', 'Simon Fraser') - self._assert_parse_reviewer_text_and_list('Reviewed by Geoff Garen. V8 fixes courtesy of Dmitry Titov.', 'Geoff Garen') - self._assert_parse_reviewer_text_and_list('Reviewed by Adam Roben&Dirk Schulze', 'Adam Roben&Dirk Schulze', ['Adam Roben', 'Dirk Schulze']) - self._assert_parse_reviewer_text_and_list('Rubber stamps by Darin Adler & Sam Weinig.', 'Darin Adler & Sam Weinig', ['Darin Adler', 'Sam Weinig']) - - self._assert_parse_reviewer_text_and_list('Reviewed by adam,andy and andy adam, andy smith', - 'adam,andy and andy adam, andy smith', ['adam', 'andy', 'andy adam', 'andy smith']) - - self._assert_parse_reviewer_text_and_list('rubber stamped by Oliver Hunt (oliver@apple.com) and Darin Adler (darin@apple.com)', - 'Oliver Hunt and Darin Adler', ['Oliver Hunt', 'Darin Adler']) - - self._assert_parse_reviewer_text_and_list('rubber Stamped by David Hyatt <hyatt@apple.com>', 'David Hyatt') - self._assert_parse_reviewer_text_and_list('Rubber-stamped by Antti Koivisto.', 'Antti Koivisto') - self._assert_parse_reviewer_text_and_list('Rubberstamped by Dan Bernstein.', 'Dan Bernstein') - self._assert_parse_reviewer_text_and_list('Reviews by Ryosuke Niwa', 'Ryosuke Niwa') - self._assert_parse_reviewer_text_and_list('Reviews Ryosuke Niwa', 'Ryosuke Niwa') - self._assert_parse_reviewer_text_and_list('Rubberstamp Ryosuke Niwa', 'Ryosuke Niwa') - self._assert_parse_reviewer_text_and_list('Typed and reviewed by Alexey Proskuryakov.', 'Alexey Proskuryakov') - self._assert_parse_reviewer_text_and_list('Reviewed and landed by Brady Eidson', 'Brady Eidson') - self._assert_parse_reviewer_text_and_list('Reviewed by rniwa@webkit.org.', 'rniwa@webkit.org') - self._assert_parse_reviewer_text_and_list('Reviewed by Dirk Schulze / Darin Adler.', 'Dirk Schulze / Darin Adler', ['Dirk Schulze', 'Darin Adler']) - self._assert_parse_reviewer_text_and_list('Reviewed by Sam Weinig + Oliver Hunt.', 'Sam Weinig + Oliver Hunt', ['Sam Weinig', 'Oliver Hunt']) - - self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig, and given a good once-over by Jeff Miller.', ['Sam Weinig', 'Jeff Miller']) - self._assert_parse_reviewer_text_list(' Reviewed by Sam Weinig, even though this is just a...', ['Sam Weinig']) - self._assert_parse_reviewer_text_list('Rubber stamped by by Gustavo Noronha Silva', ['Gustavo Noronha Silva']) - self._assert_parse_reviewer_text_list('Rubberstamped by Noam Rosenthal, who wrote the original code.', ['Noam Rosenthal']) - self._assert_parse_reviewer_text_list('Reviewed by Dan Bernstein (relanding of r47157)', ['Dan Bernstein']) - self._assert_parse_reviewer_text_list('Reviewed by Geoffrey "Sean/Shawn/Shaun" Garen', ['Geoffrey Garen']) - self._assert_parse_reviewer_text_list('Reviewed by Dave "Messy" Hyatt.', ['Dave Hyatt']) - self._assert_parse_reviewer_text_list('Reviewed by Sam \'The Belly\' Weinig', ['Sam Weinig']) - self._assert_parse_reviewer_text_list('Rubber-stamped by David "I\'d prefer not" Hyatt.', ['David Hyatt']) - self._assert_parse_reviewer_text_list('Reviewed by Mr. Geoffrey Garen.', ['Geoffrey Garen']) - self._assert_parse_reviewer_text_list('Reviewed by Darin (ages ago)', ['Darin']) - self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig (except for a few comment and header tweaks).', ['Sam Weinig']) - self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig (all but the FormDataListItem rename)', ['Sam Weinig']) - self._assert_parse_reviewer_text_list('Reviewed by Darin Adler, tweaked and landed by Beth.', ['Darin Adler']) - self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig with no hesitation', ['Sam Weinig']) - self._assert_parse_reviewer_text_list('Reviewed by Oliver Hunt, okayed by Darin Adler.', ['Oliver Hunt']) - self._assert_parse_reviewer_text_list('Reviewed by Darin Adler).', ['Darin Adler']) - - # For now, we let unofficial reviewers recognized as reviewers - self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig, Anders Carlsson, and (unofficially) Adam Barth.', - ['Sam Weinig', 'Anders Carlsson', 'Adam Barth']) - - self._assert_parse_reviewer_text_list('Reviewed by NOBODY.', None) - self._assert_parse_reviewer_text_list('Reviewed by NOBODY - Build Fix.', None) - self._assert_parse_reviewer_text_list('Reviewed by NOBODY, layout tests fix.', None) - self._assert_parse_reviewer_text_list('Reviewed by NOBODY (Qt build fix pt 2).', None) - self._assert_parse_reviewer_text_list('Reviewed by NOBODY(rollout)', None) - self._assert_parse_reviewer_text_list('Reviewed by NOBODY (Build fix, forgot to svn add this file)', None) - self._assert_parse_reviewer_text_list('Reviewed by nobody (trivial follow up fix), Joseph Pecoraro LGTM-ed.', None) - - def _entry_with_author(self, author_text): - return ChangeLogEntry('''2009-08-19 AUTHOR_TEXT - - Reviewed by Ryosuke Niwa - - * Scripts/bugzilla-tool: -'''.replace("AUTHOR_TEXT", author_text)) - - def _entry_with_reviewer(self, reviewer_line): - return ChangeLogEntry('''2009-08-19 Eric Seidel <eric@webkit.org> - - REVIEW_LINE - - * Scripts/bugzilla-tool: -'''.replace("REVIEW_LINE", reviewer_line)) - - def _contributors(self, names): - return [CommitterList().contributor_by_name(name) for name in names] - - def _assert_fuzzy_reviewer_match(self, reviewer_text, expected_text_list, expected_contributors): - unused, reviewer_text_list = ChangeLogEntry._parse_reviewer_text(reviewer_text) - self.assertEqual(reviewer_text_list, expected_text_list) - self.assertEqual(self._entry_with_reviewer(reviewer_text).reviewers(), self._contributors(expected_contributors)) - - def test_fuzzy_reviewer_match__none(self): - self._assert_fuzzy_reviewer_match('Reviewed by BUILD FIX', ['BUILD FIX'], []) - self._assert_fuzzy_reviewer_match('Reviewed by Mac build fix', ['Mac build fix'], []) - - def test_fuzzy_reviewer_match_adam_barth(self): - self._assert_fuzzy_reviewer_match('Reviewed by Adam Barth.:w', ['Adam Barth.:w'], ['Adam Barth']) - - def test_fuzzy_reviewer_match_darin_adler_et_al(self): - self._assert_fuzzy_reviewer_match('Reviewed by Darin Adler in <https://bugs.webkit.org/show_bug.cgi?id=47736>.', ['Darin Adler in'], ['Darin Adler']) - self._assert_fuzzy_reviewer_match('Reviewed by Darin Adler, Dan Bernstein, Adele Peterson, and others.', - ['Darin Adler', 'Dan Bernstein', 'Adele Peterson', 'others'], ['Darin Adler', 'Dan Bernstein', 'Adele Peterson']) - - def test_fuzzy_reviewer_match_dimitri_glazkov(self): - self._assert_fuzzy_reviewer_match('Reviewed by Dimitri Glazkov, build fix', ['Dimitri Glazkov', 'build fix'], ['Dimitri Glazkov']) - - def test_fuzzy_reviewer_match_george_staikos(self): - self._assert_fuzzy_reviewer_match('Reviewed by George Staikos (and others)', ['George Staikos', 'others'], ['George Staikos']) - - def test_fuzzy_reviewer_match_mark_rowe(self): - self._assert_fuzzy_reviewer_match('Reviewed by Mark Rowe, but Dan Bernstein also reviewed and asked thoughtful questions.', - ['Mark Rowe', 'but Dan Bernstein also reviewed', 'asked thoughtful questions'], ['Mark Rowe']) - - def test_fuzzy_reviewer_match_initial(self): - self._assert_fuzzy_reviewer_match('Reviewed by Alejandro G. Castro.', - ['Alejandro G. Castro'], ['Alejandro G. Castro']) - self._assert_fuzzy_reviewer_match('Reviewed by G. Alejandro G. Castro and others.', - ['G. Alejandro G. Castro', 'others'], ['Alejandro G. Castro']) - - # If a reviewer has a name that ended with an initial, the regular expression - # will incorrectly trim the last period, but it will still match fuzzily to - # the full reviewer name. - self._assert_fuzzy_reviewer_match('Reviewed by G. Alejandro G. G. Castro G.', - ['G. Alejandro G. G. Castro G'], ['Alejandro G. Castro']) - - def _assert_parse_authors(self, author_text, expected_contributors): - parsed_authors = [(author['name'], author['email']) for author in self._entry_with_author(author_text).authors()] - self.assertEqual(parsed_authors, expected_contributors) - - def test_parse_authors(self): - self._assert_parse_authors(u'Aaron Colwell <acolwell@chromium.org>', [(u'Aaron Colwell', u'acolwell@chromium.org')]) - self._assert_parse_authors('Eric Seidel <eric@webkit.org>, Ryosuke Niwa <rniwa@webkit.org>', - [('Eric Seidel', 'eric@webkit.org'), ('Ryosuke Niwa', 'rniwa@webkit.org')]) - self._assert_parse_authors('Zan Dobersek <zandobersek@gmail.com> and Philippe Normand <pnormand@igalia.com>', - [('Zan Dobersek', 'zandobersek@gmail.com'), ('Philippe Normand', 'pnormand@igalia.com')]) - self._assert_parse_authors('New Contributor <new@webkit.org> and Noob <noob@webkit.org>', - [('New Contributor', 'new@webkit.org'), ('Noob', 'noob@webkit.org')]) - self._assert_parse_authors('Adam Barth <abarth@webkit.org> && Benjamin Poulain <bpoulain@apple.com>', - [('Adam Barth', 'abarth@webkit.org'), ('Benjamin Poulain', 'bpoulain@apple.com')]) - self._assert_parse_authors(u'Pawe\u0142 Hajdan, Jr. <phajdan.jr@chromium.org>', - [(u'Pawe\u0142 Hajdan, Jr.', u'phajdan.jr@chromium.org')]) - self._assert_parse_authors(u'Pawe\u0142 Hajdan, Jr. <phajdan.jr@chromium.org>, Adam Barth <abarth@webkit.org>', - [(u'Pawe\u0142 Hajdan, Jr.', u'phajdan.jr@chromium.org'), (u'Adam Barth', u'abarth@webkit.org')]) - - def _assert_has_valid_reviewer(self, reviewer_line, expected): - self.assertEqual(self._entry_with_reviewer(reviewer_line).has_valid_reviewer(), expected) - - def test_has_valid_reviewer(self): - self._assert_has_valid_reviewer("Reviewed by Eric Seidel.", True) - self._assert_has_valid_reviewer("Reviewed by Eric Seidel", True) # Not picky about the '.' - self._assert_has_valid_reviewer("Reviewed by Eric.", False) - self._assert_has_valid_reviewer("Reviewed by Eric C Seidel.", False) - self._assert_has_valid_reviewer("Rubber-stamped by Eric.", False) - self._assert_has_valid_reviewer("Rubber-stamped by Eric Seidel.", True) - self._assert_has_valid_reviewer("Rubber stamped by Eric.", False) - self._assert_has_valid_reviewer("Rubber stamped by Eric Seidel.", True) - self._assert_has_valid_reviewer("Unreviewed build fix.", True) - - def test_is_touched_files_text_clean(self): - tests = [ - ('''2013-01-30 Timothy Loh <timloh@chromium.com> - - Make ChangeLogEntry detect annotations by prepare-ChangeLog (Added/Removed/Copied from/Renamed from) as clean. - https://bugs.webkit.org/show_bug.cgi?id=108433 - - * Scripts/webkitpy/common/checkout/changelog.py: - (ChangeLogEntry.is_touched_files_text_clean): - * Scripts/webkitpy/common/checkout/changelog_unittest.py: - (test_is_touched_files_text_clean): -''', True), - ('''2013-01-10 Alan Cutter <alancutter@chromium.org> - - Perform some file operations (automatically added comments). - - * QueueStatusServer/config/charts.py: Copied from Tools/QueueStatusServer/model/queuelog.py. - (get_time_unit): - * QueueStatusServer/handlers/queuecharts.py: Added. - (QueueCharts): - * Scripts/webkitpy/tool/bot/testdata/webkit_sheriff_0.js: Removed. - * EWSTools/build-vm.sh: Renamed from Tools/EWSTools/cold-boot.sh. -''', True), - ('''2013-01-30 Timothy Loh <timloh@chromium.com> - - Add unit test (manually added comment). - - * Scripts/webkitpy/common/checkout/changelog_unittest.py: - (test_is_touched_files_text_clean): Added. -''', False), - ('''2013-01-30 Timothy Loh <timloh@chromium.com> - - Add file (manually added comment). - - * Scripts/webkitpy/common/checkout/super_changelog.py: Copied from the internet. -''', False), - ] - - for contents, expected_result in tests: - entry = ChangeLogEntry(contents) - self.assertEqual(entry.is_touched_files_text_clean(), expected_result) - - def test_latest_entry_parse(self): - 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.assertEqual(latest_entry.contents(), self._example_entry) - self.assertEqual(latest_entry.author_name(), "Peter Kasting") - self.assertEqual(latest_entry.author_email(), "pkasting@google.com") - self.assertEqual(latest_entry.reviewer_text(), u"Tor Arne Vestb\xf8") - touched_files = ["DumpRenderTree/win/DumpRenderTree.vcproj", "DumpRenderTree/win/ImageDiff.vcproj", "DumpRenderTree/win/TestNetscapePlugin/TestNetscapePlugin.vcproj"] - self.assertEqual(latest_entry.touched_files(), touched_files) - self.assertEqual(latest_entry.touched_functions(), dict((f, []) for f in touched_files)) - - self.assertTrue(latest_entry.reviewer()) # Make sure that our UTF8-based lookup of Tor works. - - def test_latest_entry_parse_single_entry(self): - changelog_contents = u"%s\n%s" % (self._example_entry, self._rolled_over_footer) - changelog_file = StringIO(changelog_contents) - latest_entry = ChangeLog.parse_latest_entry_from_file(changelog_file) - self.assertEqual(latest_entry.contents(), self._example_entry) - self.assertEqual(latest_entry.author_name(), "Peter Kasting") - - # FIXME: We really should be getting this from prepare-ChangeLog itself. - _new_entry_boilerplate = '''2009-08-19 Eric Seidel <eric@webkit.org> - - Need a short description (OOPS!). - Need the bug URL (OOPS!). - - Reviewed by NOBODY (OOPS!). - - * Scripts/bugzilla-tool: -''' - - _new_entry_boilerplate_with_bugurl = '''2009-08-19 Eric Seidel <eric@webkit.org> - - Need a short description (OOPS!). - https://bugs.webkit.org/show_bug.cgi?id=12345 - - Reviewed by NOBODY (OOPS!). - - * Scripts/bugzilla-tool: -''' - - _new_entry_boilerplate_with_multiple_bugurl = '''2009-08-19 Eric Seidel <eric@webkit.org> - - Need a short description (OOPS!). - https://bugs.webkit.org/show_bug.cgi?id=12345 - http://webkit.org/b/12345 - - Reviewed by NOBODY (OOPS!). - - * Scripts/bugzilla-tool: -''' - - _new_entry_boilerplate_without_reviewer_line = '''2009-08-19 Eric Seidel <eric@webkit.org> - - Need a short description (OOPS!). - https://bugs.webkit.org/show_bug.cgi?id=12345 - - * Scripts/bugzilla-tool: -''' - - _new_entry_boilerplate_without_reviewer_multiple_bugurl = '''2009-08-19 Eric Seidel <eric@webkit.org> - - Need a short description (OOPS!). - https://bugs.webkit.org/show_bug.cgi?id=12345 - http://webkit.org/b/12345 - - * Scripts/bugzilla-tool: -''' - - def test_set_reviewer(self): - fs = MockFileSystem() - - changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate_with_bugurl, self._example_changelog) - reviewer_name = 'Test Reviewer' - fs.write_text_file(self._changelog_path, changelog_contents) - ChangeLog(self._changelog_path, fs).set_reviewer(reviewer_name) - actual_contents = fs.read_text_file(self._changelog_path) - expected_contents = changelog_contents.replace('NOBODY (OOPS!)', reviewer_name) - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) - - changelog_contents_without_reviewer_line = u"%s\n%s" % (self._new_entry_boilerplate_without_reviewer_line, self._example_changelog) - fs.write_text_file(self._changelog_path, changelog_contents_without_reviewer_line) - ChangeLog(self._changelog_path, fs).set_reviewer(reviewer_name) - actual_contents = fs.read_text_file(self._changelog_path) - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) - - changelog_contents_without_reviewer_line = u"%s\n%s" % (self._new_entry_boilerplate_without_reviewer_multiple_bugurl, self._example_changelog) - fs.write_text_file(self._changelog_path, changelog_contents_without_reviewer_line) - ChangeLog(self._changelog_path, fs).set_reviewer(reviewer_name) - actual_contents = fs.read_text_file(self._changelog_path) - changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate_with_multiple_bugurl, self._example_changelog) - expected_contents = changelog_contents.replace('NOBODY (OOPS!)', reviewer_name) - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) - - def test_set_short_description_and_bug_url(self): - fs = MockFileSystem() - - changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate_with_bugurl, self._example_changelog) - fs.write_text_file(self._changelog_path, changelog_contents) - short_description = "A short description" - bug_url = "http://example.com/b/2344" - ChangeLog(self._changelog_path, fs).set_short_description_and_bug_url(short_description, bug_url) - actual_contents = fs.read_text_file(self._changelog_path) - expected_message = "%s\n %s" % (short_description, bug_url) - expected_contents = changelog_contents.replace("Need a short description (OOPS!).", expected_message) - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) - - changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate, self._example_changelog) - fs.write_text_file(self._changelog_path, changelog_contents) - short_description = "A short description 2" - bug_url = "http://example.com/b/2345" - ChangeLog(self._changelog_path, fs).set_short_description_and_bug_url(short_description, bug_url) - actual_contents = fs.read_text_file(self._changelog_path) - expected_message = "%s\n %s" % (short_description, bug_url) - expected_contents = changelog_contents.replace("Need a short description (OOPS!).\n Need the bug URL (OOPS!).", expected_message) - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) - - def test_delete_entries(self): - fs = MockFileSystem() - fs.write_text_file(self._changelog_path, self._example_changelog) - ChangeLog(self._changelog_path, fs).delete_entries(8) - actual_contents = fs.read_text_file(self._changelog_path) - expected_contents = """2011-10-11 Antti Koivisto <antti@apple.com> - - Resolve regular and visited link style in a single pass - https://bugs.webkit.org/show_bug.cgi?id=69838 - - Reviewed by Darin Adler - - We can simplify and speed up selector matching by removing the recursive matching done - to generate the style for the :visited pseudo selector. Both regular and visited link style - can be generated in a single pass through the style selector. - -== Rolled over to ChangeLog-2009-06-16 == -""" - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) - - ChangeLog(self._changelog_path, fs).delete_entries(2) - actual_contents = fs.read_text_file(self._changelog_path) - expected_contents = "== Rolled over to ChangeLog-2009-06-16 ==\n" - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) - - - def test_prepend_text(self): - fs = MockFileSystem() - fs.write_text_file(self._changelog_path, self._example_changelog) - ChangeLog(self._changelog_path, fs).prepend_text(self._example_entry + "\n") - actual_contents = fs.read_text_file(self._changelog_path) - expected_contents = self._example_entry + "\n" + self._example_changelog - self.assertEqual(actual_contents.splitlines(), expected_contents.splitlines()) diff --git a/Tools/Scripts/webkitpy/common/checkout/checkout.py b/Tools/Scripts/webkitpy/common/checkout/checkout.py deleted file mode 100644 index 60e15b29c..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/checkout.py +++ /dev/null @@ -1,173 +0,0 @@ -# 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 StringIO - -from webkitpy.common.config import urls -from webkitpy.common.checkout.changelog import ChangeLog, parse_bug_id_from_changelog -from webkitpy.common.checkout.commitinfo import CommitInfo -from webkitpy.common.checkout.scm import CommitMessage -from webkitpy.common.memoized import memoized -from webkitpy.common.system.executive import ScriptError - - -# This class represents the WebKit-specific parts of the checkout (like ChangeLogs). -# FIXME: Move a bunch of ChangeLog-specific processing from SCM to this object. -# NOTE: All paths returned from this class should be absolute. -class Checkout(object): - def __init__(self, scm, executive=None, filesystem=None): - self._scm = scm - # FIXME: We shouldn't be grabbing at private members on scm. - self._executive = executive or self._scm._executive - self._filesystem = filesystem or self._scm._filesystem - - def is_path_to_changelog(self, path): - return self._filesystem.basename(path) == "ChangeLog" - - def _latest_entry_for_changelog_at_revision(self, changelog_path, revision): - changelog_contents = self._scm.contents_at_revision(changelog_path, revision) - # 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. - # Old revisions of Sources/WebKit/wx/ChangeLog have some invalid utf8 characters. - changelog_file = StringIO.StringIO(changelog_contents.decode("utf-8", "ignore")) - return ChangeLog.parse_latest_entry_from_file(changelog_file) - - def changelog_entries_for_revision(self, revision, changed_files=None): - if not changed_files: - changed_files = self._scm.changed_files_for_revision(revision) - # FIXME: This gets confused if ChangeLog files are moved, as - # deletes are still "changed files" per changed_files_for_revision. - # FIXME: For now we hack around this by caching any exceptions - # which result from having deleted files included the changed_files list. - changelog_entries = [] - for path in changed_files: - if not self.is_path_to_changelog(path): - continue - try: - changelog_entries.append(self._latest_entry_for_changelog_at_revision(path, revision)) - except ScriptError: - pass - return changelog_entries - - def _changelog_data_for_revision(self, revision): - changed_files = self._scm.changed_files_for_revision(revision) - changelog_entries = self.changelog_entries_for_revision(revision, changed_files=changed_files) - # Assume for now that the first entry has everything we need: - # FIXME: This will throw an exception if there were no ChangeLogs. - if not len(changelog_entries): - return None - changelog_entry = changelog_entries[0] - return { - "bug_id": parse_bug_id_from_changelog(changelog_entry.contents()), - "author_name": changelog_entry.author_name(), - "author_email": changelog_entry.author_email(), - "author": changelog_entry.author(), - "reviewer_text": changelog_entry.reviewer_text(), - "reviewer": changelog_entry.reviewer(), - "contents": changelog_entry.contents(), - "changed_files": changed_files, - } - - @memoized - def commit_info_for_revision(self, revision): - committer_email = self._scm.committer_email_for_revision(revision) - changelog_data = self._changelog_data_for_revision(revision) - if not changelog_data: - return None - return CommitInfo(revision, committer_email, changelog_data) - - def bug_id_for_revision(self, revision): - return self.commit_info_for_revision(revision).bug_id() - - def _modified_files_matching_predicate(self, git_commit, predicate, changed_files=None): - # 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. - if not changed_files: - changed_files = self._scm.changed_files(git_commit) - return filter(predicate, map(self._scm.absolute_path, changed_files)) - - def modified_changelogs(self, git_commit, changed_files=None): - return self._modified_files_matching_predicate(git_commit, self.is_path_to_changelog, changed_files=changed_files) - - def modified_non_changelogs(self, git_commit, changed_files=None): - return self._modified_files_matching_predicate(git_commit, lambda path: not self.is_path_to_changelog(path), changed_files=changed_files) - - def commit_message_for_this_commit(self, git_commit, changed_files=None, return_stderr=False): - changelog_paths = self.modified_changelogs(git_commit, changed_files) - 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 %s" % urls.contribution_guidelines) - - message_text = self._scm.run([self._scm.script_path('commit-log-editor'), '--print-log'] + changelog_paths, return_stderr=return_stderr) - return CommitMessage(message_text.splitlines()) - - def recent_commit_infos_for_files(self, paths): - revisions = set(sum(map(self._scm.revisions_changing_file, paths), [])) - return set(map(self.commit_info_for_revision, revisions)) - - def suggested_reviewers(self, git_commit, changed_files=None): - changed_files = self.modified_non_changelogs(git_commit, changed_files) - commit_infos = sorted(self.recent_commit_infos_for_files(changed_files), key=lambda info: info.revision(), reverse=True) - reviewers = filter(lambda person: person and person.can_review, sum(map(lambda info: [info.reviewer(), info.author()], commit_infos), [])) - unique_reviewers = reduce(lambda suggestions, reviewer: suggestions + [reviewer if reviewer not in suggestions else None], reviewers, []) - return filter(lambda reviewer: reviewer, unique_reviewers) - - def bug_id_for_this_commit(self, git_commit, changed_files=None): - try: - return parse_bug_id_from_changelog(self.commit_message_for_this_commit(git_commit, changed_files).message()) - except ScriptError, e: - pass # We might not have ChangeLogs. - - def apply_patch(self, patch): - # It's possible that the patch was not made from the root directory. - # We should detect and handle that case. - # FIXME: Move _scm.script_path here once we get rid of all the dependencies. - # --force (continue after errors) is the common case, so we always use it. - args = [self._scm.script_path('svn-apply'), "--force"] - if patch.reviewer(): - args += ['--reviewer', patch.reviewer().full_name] - self._executive.run_command(args, input=patch.contents(), cwd=self._scm.checkout_root) - - 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(git_commit=None) - if len(changelog_paths): - self._scm.revert_files(changelog_paths) - - conflicts = self._scm.conflicted_files() - if len(conflicts): - raise ScriptError(message="Failed to apply reverse diff for revision %s because of the following conflicts:\n%s" % (revision, "\n".join(conflicts))) - - def apply_reverse_diffs(self, revision_list): - for revision in sorted(revision_list, reverse=True): - self.apply_reverse_diff(revision) diff --git a/Tools/Scripts/webkitpy/common/checkout/checkout_mock.py b/Tools/Scripts/webkitpy/common/checkout/checkout_mock.py deleted file mode 100644 index 8a17145ca..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/checkout_mock.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (C) 2011 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. - -from .commitinfo import CommitInfo - -# FIXME: These imports are wrong, we should use a shared MockCommittersList. -from webkitpy.common.config.committers import CommitterList -from webkitpy.common.net.bugzilla.bugzilla_mock import _mock_reviewers -from webkitpy.common.system.filesystem_mock import MockFileSystem - - -class MockCommitMessage(object): - def message(self): - return "This is a fake commit message that is at least 50 characters." - - -committer_list = CommitterList() - -mock_revisions = { - 1: CommitInfo(852, "eric@webkit.org", { - "bug_id": 50000, - "author_name": "Adam Barth", - "author_email": "abarth@webkit.org", - "author": committer_list.contributor_by_email("abarth@webkit.org"), - "reviewer_text": "Darin Adler", - "reviewer": committer_list.committer_by_name("Darin Adler"), - "changed_files": [ - "path/to/file", - "another/file", - ], - }), - 3001: CommitInfo(3001, "tomz@codeaurora.org", { - "bug_id": 50004, - "author_name": "Tom Zakrajsek", - "author_email": "tomz@codeaurora.org", - "author": committer_list.contributor_by_email("tomz@codeaurora.org"), - "reviewer_text": "Darin Adler", - "reviewer": committer_list.committer_by_name("Darin Adler"), - "changed_files": [ - "path/to/file", - "another/file", - ], - }) -} - -class MockCheckout(object): - def __init__(self): - # FIXME: It's unclear if a MockCheckout is very useful. A normal Checkout - # with a MockSCM/MockFileSystem/MockExecutive is probably better. - self._filesystem = MockFileSystem() - - def commit_info_for_revision(self, svn_revision): - # There are legacy tests that all expected these revision numbers to map - # to the same commit description (now mock_revisions[1]) - if svn_revision in [32, 123, 852, 853, 854, 1234, 21654, 21655, 21656]: - return mock_revisions[1] - - if svn_revision in mock_revisions: - return mock_revisions[svn_revision] - - # any "unrecognized" svn_revision will return None. - - def is_path_to_changelog(self, path): - return self._filesystem.basename(path) == "ChangeLog" - - def bug_id_for_revision(self, svn_revision): - return 12345 - - def recent_commit_infos_for_files(self, paths): - return [self.commit_info_for_revision(32)] - - def modified_changelogs(self, git_commit, changed_files=None): - # Ideally we'd return something more interesting here. The problem is - # that LandDiff will try to actually read the patch from disk! - return [] - - def commit_message_for_this_commit(self, git_commit, changed_files=None): - return MockCommitMessage() - - def apply_patch(self, patch): - pass - - def apply_reverse_diffs(self, revision): - pass - - def suggested_reviewers(self, git_commit, changed_files=None): - # FIXME: We should use a shared mock commiter list. - return [_mock_reviewers[0]] diff --git a/Tools/Scripts/webkitpy/common/checkout/checkout_unittest.py b/Tools/Scripts/webkitpy/common/checkout/checkout_unittest.py deleted file mode 100644 index 587798e77..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/checkout_unittest.py +++ /dev/null @@ -1,260 +0,0 @@ -# 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 codecs -import os -import shutil -import tempfile -import unittest2 as unittest - -from .checkout import Checkout -from .changelog import ChangeLogEntry -from .scm import CommitMessage, SCMDetector -from .scm.scm_mock import MockSCM -from webkitpy.common.webkit_finder import WebKitFinder -from webkitpy.common.system.executive import Executive, ScriptError -from webkitpy.common.system.filesystem import FileSystem # FIXME: This should not be needed. -from webkitpy.common.system.filesystem_mock import MockFileSystem -from webkitpy.common.system.executive_mock import MockExecutive -from webkitpy.common.system.outputcapture import OutputCapture -from webkitpy.thirdparty.mock import Mock - - -_changelog1entry1 = u"""2010-03-25 Tor Arne Vestb\u00f8 <vestbo@webkit.org> - - Unreviewed build fix to un-break webkit-patch land. - - Move commit_message_for_this_commit from scm to checkout - https://bugs.webkit.org/show_bug.cgi?id=36629 - - * Scripts/webkitpy/common/checkout/api.py: import scm.CommitMessage -""" -_changelog1entry2 = u"""2010-03-25 Adam Barth <abarth@webkit.org> - - Reviewed by Eric Seidel. - - Move commit_message_for_this_commit from scm to checkout - https://bugs.webkit.org/show_bug.cgi?id=36629 - - * Scripts/webkitpy/common/checkout/api.py: -""" -_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. - - Second part of this complicated change by me, Tor Arne Vestb\u00f8! - - * Path/To/Complicated/File: Added. - -2010-03-25 Adam Barth <abarth@webkit.org> - - Reviewed by Eric Seidel. - - Filler change. -""" - -class CommitMessageForThisCommitTest(unittest.TestCase): - expected_commit_message = u"""Unreviewed build fix to un-break webkit-patch land. - -Tools: - -Move commit_message_for_this_commit from scm to checkout -https://bugs.webkit.org/show_bug.cgi?id=36629 - -* Scripts/webkitpy/common/checkout/api.py: import scm.CommitMessage - -LayoutTests: - -Second part of this complicated change by me, Tor Arne Vestb\u00f8! - -* Path/To/Complicated/File: Added. -""" - - def setUp(self): - # FIXME: This should not need to touch the filesystem, however - # ChangeLog is difficult to mock at current. - self.filesystem = FileSystem() - self.temp_dir = str(self.filesystem.mkdtemp(suffix="changelogs")) - self.old_cwd = self.filesystem.getcwd() - self.filesystem.chdir(self.temp_dir) - self.webkit_base = WebKitFinder(self.filesystem).webkit_base() - - # Trick commit-log-editor into thinking we're in a Subversion working copy so it won't - # complain about not being able to figure out what SCM is in use. - # FIXME: VCSTools.pm is no longer so easily fooled. It logs because "svn info" doesn't - # treat a bare .svn directory being part of an svn checkout. - self.filesystem.maybe_make_directory(".svn") - - self.changelogs = map(self.filesystem.abspath, (self.filesystem.join("Tools", "ChangeLog"), self.filesystem.join("LayoutTests", "ChangeLog"))) - for path, contents in zip(self.changelogs, (_changelog1, _changelog2)): - self.filesystem.maybe_make_directory(self.filesystem.dirname(path)) - self.filesystem.write_text_file(path, contents) - - def tearDown(self): - self.filesystem.rmtree(self.temp_dir) - self.filesystem.chdir(self.old_cwd) - - def test_commit_message_for_this_commit(self): - executive = Executive() - - def mock_run(*args, **kwargs): - # Note that we use a real Executive here, not a MockExecutive, so we can test that we're - # invoking commit-log-editor correctly. - env = os.environ.copy() - env['CHANGE_LOG_EMAIL_ADDRESS'] = 'vestbo@webkit.org' - kwargs['env'] = env - return executive.run_command(*args, **kwargs) - - detector = SCMDetector(self.filesystem, executive) - real_scm = detector.detect_scm_system(self.webkit_base) - - mock_scm = MockSCM() - mock_scm.run = mock_run - mock_scm.script_path = real_scm.script_path - - checkout = Checkout(mock_scm) - checkout.modified_changelogs = lambda git_commit, changed_files=None: self.changelogs - commit_message = checkout.commit_message_for_this_commit(git_commit=None, return_stderr=True) - # Throw away the first line - a warning about unknown VCS root. - commit_message.message_lines = commit_message.message_lines[1:] - self.assertMultiLineEqual(commit_message.message(), self.expected_commit_message) - - -class CheckoutTest(unittest.TestCase): - def _make_checkout(self): - return Checkout(scm=MockSCM(), filesystem=MockFileSystem(), executive=MockExecutive()) - - def test_latest_entry_for_changelog_at_revision(self): - def mock_contents_at_revision(changelog_path, revision): - self.assertEqual(changelog_path, "foo") - self.assertEqual(revision, "bar") - # contents_at_revision is expected to return a byte array (str) - # so we encode our unicode ChangeLog down to a utf-8 stream. - # The ChangeLog utf-8 decoding should ignore invalid codepoints. - invalid_utf8 = "\255" - return _changelog1.encode("utf-8") + invalid_utf8 - checkout = self._make_checkout() - checkout._scm.contents_at_revision = mock_contents_at_revision - entry = checkout._latest_entry_for_changelog_at_revision("foo", "bar") - self.assertMultiLineEqual(entry.contents(), _changelog1entry1) # Pylint is confused about this line, pylint: disable=E1101 - - # FIXME: This tests a hack around our current changed_files handling. - # Right now changelog_entries_for_revision tries to fetch deleted files - # from revisions, resulting in a ScriptError exception. Test that we - # recover from those and still return the other ChangeLog entries. - def test_changelog_entries_for_revision(self): - checkout = self._make_checkout() - checkout._scm.changed_files_for_revision = lambda revision: ['foo/ChangeLog', 'bar/ChangeLog'] - - def mock_latest_entry_for_changelog_at_revision(path, revision): - if path == "foo/ChangeLog": - return 'foo' - raise ScriptError() - - checkout._latest_entry_for_changelog_at_revision = mock_latest_entry_for_changelog_at_revision - - # Even though fetching one of the entries failed, the other should succeed. - entries = checkout.changelog_entries_for_revision(1) - self.assertEqual(len(entries), 1) - self.assertEqual(entries[0], 'foo') - - def test_commit_info_for_revision(self): - checkout = self._make_checkout() - checkout._scm.changed_files_for_revision = lambda revision: ['path/to/file', 'another/file'] - checkout._scm.committer_email_for_revision = lambda revision, changed_files=None: "committer@example.com" - checkout.changelog_entries_for_revision = lambda revision, changed_files=None: [ChangeLogEntry(_changelog1entry1)] - commitinfo = checkout.commit_info_for_revision(4) - self.assertEqual(commitinfo.bug_id(), 36629) - self.assertEqual(commitinfo.author_name(), u"Tor Arne Vestb\u00f8") - self.assertEqual(commitinfo.author_email(), "vestbo@webkit.org") - self.assertIsNone(commitinfo.reviewer_text()) - self.assertIsNone(commitinfo.reviewer()) - self.assertEqual(commitinfo.committer_email(), "committer@example.com") - self.assertIsNone(commitinfo.committer()) - self.assertEqual(commitinfo.to_json(), { - 'bug_id': 36629, - 'author_email': 'vestbo@webkit.org', - 'changed_files': [ - 'path/to/file', - 'another/file', - ], - 'reviewer_text': None, - 'author_name': u'Tor Arne Vestb\xf8', - }) - - checkout.changelog_entries_for_revision = lambda revision, changed_files=None: [] - self.assertIsNone(checkout.commit_info_for_revision(1)) - - def test_bug_id_for_revision(self): - checkout = self._make_checkout() - checkout._scm.committer_email_for_revision = lambda revision: "committer@example.com" - checkout.changelog_entries_for_revision = lambda revision, changed_files=None: [ChangeLogEntry(_changelog1entry1)] - self.assertEqual(checkout.bug_id_for_revision(4), 36629) - - def test_bug_id_for_this_commit(self): - checkout = self._make_checkout() - checkout.commit_message_for_this_commit = lambda git_commit, changed_files=None: CommitMessage(ChangeLogEntry(_changelog1entry1).contents().splitlines()) - self.assertEqual(checkout.bug_id_for_this_commit(git_commit=None), 36629) - - def test_modified_changelogs(self): - checkout = self._make_checkout() - checkout._scm.checkout_root = "/foo/bar" - checkout._scm.changed_files = lambda git_commit: ["file1", "ChangeLog", "relative/path/ChangeLog"] - expected_changlogs = ["/foo/bar/ChangeLog", "/foo/bar/relative/path/ChangeLog"] - self.assertEqual(checkout.modified_changelogs(git_commit=None), expected_changlogs) - - def test_suggested_reviewers(self): - def mock_changelog_entries_for_revision(revision, changed_files=None): - if revision % 2 == 0: - return [ChangeLogEntry(_changelog1entry1)] - return [ChangeLogEntry(_changelog1entry2)] - - def mock_revisions_changing_file(path, limit=5): - if path.endswith("ChangeLog"): - return [3] - return [4, 8] - - checkout = self._make_checkout() - checkout._scm.checkout_root = "/foo/bar" - checkout._scm.changed_files = lambda git_commit: ["file1", "file2", "relative/path/ChangeLog"] - checkout._scm.revisions_changing_file = mock_revisions_changing_file - checkout.changelog_entries_for_revision = mock_changelog_entries_for_revision - reviewers = checkout.suggested_reviewers(git_commit=None) - reviewer_names = [reviewer.full_name for reviewer in reviewers] - self.assertEqual(reviewer_names, [u'Tor Arne Vestb\xf8']) - - def test_apply_patch(self): - checkout = self._make_checkout() - checkout._executive = MockExecutive(should_log=True) - checkout._scm.script_path = lambda script: script - mock_patch = Mock() - mock_patch.contents = lambda: "foo" - mock_patch.reviewer = lambda: None - expected_logs = "MOCK run_command: ['svn-apply', '--force'], cwd=/mock-checkout, input=foo\n" - OutputCapture().assert_outputs(self, checkout.apply_patch, [mock_patch], expected_logs=expected_logs) diff --git a/Tools/Scripts/webkitpy/common/checkout/commitinfo.py b/Tools/Scripts/webkitpy/common/checkout/commitinfo.py deleted file mode 100644 index 79cb79f7c..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/commitinfo.py +++ /dev/null @@ -1,100 +0,0 @@ -# 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. -# -# WebKit's python module for holding information on a commit - -from webkitpy.common.config import urls -from webkitpy.common.config.committers import CommitterList - - -class CommitInfo(object): - def __init__(self, revision, committer_email, changelog_data, committer_list=CommitterList()): - self._revision = revision - self._committer_email = committer_email - self._changelog_data = changelog_data - - # Derived values: - self._committer = committer_list.committer_by_email(committer_email) - - def revision(self): - return self._revision - - def committer(self): - return self._committer # None if committer isn't in contributors.json - - def committer_email(self): - return self._committer_email - - def bug_id(self): - return self._changelog_data["bug_id"] # May be None - - def author(self): - return self._changelog_data["author"] # May be None - - def author_name(self): - return self._changelog_data["author_name"] - - def author_email(self): - return self._changelog_data["author_email"] - - def reviewer(self): - return self._changelog_data["reviewer"] # May be None - - def reviewer_text(self): - return self._changelog_data["reviewer_text"] # May be None - - def changed_files(self): - return self._changelog_data["changed_files"] - - def to_json(self): - return { - "bug_id": self.bug_id(), - "author_name": self.author_name(), - "author_email": self.author_email(), - "reviewer_text": self.reviewer_text(), - "changed_files": self.changed_files(), - } - - def responsible_parties(self): - responsible_parties = [ - self.committer(), - self.author(), - self.reviewer(), - ] - return set([party for party in responsible_parties if party]) # Filter out None - - # FIXME: It is slightly lame that this "view" method is on this "model" class (in MVC terms) - def blame_string(self, bugs): - string = "r%s:\n" % self.revision() - string += " %s\n" % urls.view_revision_url(self.revision()) - string += " Bug: %s (%s)\n" % (self.bug_id(), bugs.bug_url_for_bug_id(self.bug_id())) - author_line = "\"%s\" <%s>" % (self.author_name(), self.author_email()) - string += " Author: %s\n" % (self.author() or author_line) - string += " Reviewer: %s\n" % (self.reviewer() or self.reviewer_text()) - string += " Committer: %s" % self.committer() - return string diff --git a/Tools/Scripts/webkitpy/common/checkout/commitinfo_unittest.py b/Tools/Scripts/webkitpy/common/checkout/commitinfo_unittest.py deleted file mode 100644 index 826673de6..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/commitinfo_unittest.py +++ /dev/null @@ -1,61 +0,0 @@ -# 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 unittest2 as unittest - -from webkitpy.common.checkout.commitinfo import CommitInfo -from webkitpy.common.config.committers import CommitterList, Committer, Reviewer - -class CommitInfoTest(unittest.TestCase): - - def test_commit_info_creation(self): - author = Committer("Author", "author@example.com") - committer = Committer("Committer", "committer@example.com") - reviewer = Reviewer("Reviewer", "reviewer@example.com") - committer_list = CommitterList(committers=[author, committer], reviewers=[reviewer]) - - changelog_data = { - "bug_id": 1234, - "author_name": "Committer", - "author_email": "author@example.com", - "author": author, - "reviewer_text": "Reviewer", - "reviewer": reviewer, - } - commit = CommitInfo(123, "committer@example.com", changelog_data, committer_list) - - self.assertEqual(commit.revision(), 123) - self.assertEqual(commit.bug_id(), 1234) - self.assertEqual(commit.author_name(), "Committer") - self.assertEqual(commit.author_email(), "author@example.com") - self.assertEqual(commit.author(), author) - self.assertEqual(commit.reviewer_text(), "Reviewer") - self.assertEqual(commit.reviewer(), reviewer) - self.assertEqual(commit.committer(), committer) - self.assertEqual(commit.committer_email(), "committer@example.com") - self.assertEqual(commit.responsible_parties(), set([author, committer, reviewer])) diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py deleted file mode 100644 index 3a9ea9224..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (C) 2009 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. - -"""WebKit's Python module for interacting with patches.""" - -import logging -import re - -_log = logging.getLogger(__name__) - - -# FIXME: This is broken. We should compile our regexps up-front -# instead of using a custom cache. -_regexp_compile_cache = {} - - -# FIXME: This function should be removed. -def match(pattern, string): - """Matches the string with the pattern, caching the compiled regexp.""" - if not pattern in _regexp_compile_cache: - _regexp_compile_cache[pattern] = re.compile(pattern) - return _regexp_compile_cache[pattern].match(string) - - -# FIXME: This belongs on DiffParser (e.g. as to_svn_diff()). -def git_diff_to_svn_diff(line): - """Converts a git formatted diff line to a svn formatted line. - - Args: - line: A string representing a line of the diff. - """ - # FIXME: This list should be a class member on DiffParser. - # These regexp patterns should be compiled once instead of every time. - conversion_patterns = (("^diff --git \w/(.+) \w/(?P<FilePath>.+)", lambda matched: "Index: " + matched.group('FilePath') + "\n"), - ("^new file.*", lambda matched: "\n"), - ("^index (([0-9a-f]{7}\.\.[0-9a-f]{7})|([0-9a-f]{40}\.\.[0-9a-f]{40})) [0-9]{6}", lambda matched: "===================================================================\n"), - ("^--- \w/(?P<FilePath>.+)", lambda matched: "--- " + matched.group('FilePath') + "\n"), - ("^\+\+\+ \w/(?P<FilePath>.+)", lambda matched: "+++ " + matched.group('FilePath') + "\n")) - - for pattern, conversion in conversion_patterns: - matched = match(pattern, line) - if matched: - return conversion(matched) - return line - - -# This function exists so we can unittest get_diff_converter function -def svn_diff_to_svn_diff(line): - return line - - -# FIXME: This method belongs on DiffParser -def get_diff_converter(lines): - """Gets a converter function of diff lines. - - Args: - lines: The lines of a diff file. - If this line is git formatted, we'll return a - converter from git to SVN. - """ - for i, line in enumerate(lines[:-1]): - # Stop when we find the first patch - if line[:3] == "+++" and lines[i + 1] == "---": - break - if match(r"^diff --git \w/", line): - return git_diff_to_svn_diff - return svn_diff_to_svn_diff - -_INITIAL_STATE = 1 -_DECLARED_FILE_PATH = 2 -_PROCESSING_CHUNK = 3 - - -class DiffFile(object): - """Contains the information for one file in a patch. - - The field "lines" is a list which contains tuples in this format: - (deleted_line_number, new_line_number, line_string) - If deleted_line_number is zero, it means this line is newly added. - If new_line_number is zero, it means this line is deleted. - """ - # FIXME: Tuples generally grow into classes. We should consider - # adding a DiffLine object. - - def added_or_modified_line_numbers(self): - # This logic was moved from patchreader.py, but may not be - # the right API for this object long-term. - return [line[1] for line in self.lines if not line[0]] - - def __init__(self, filename): - self.filename = filename - self.lines = [] - - def add_new_line(self, line_number, line): - self.lines.append((0, line_number, line)) - - def add_deleted_line(self, line_number, line): - self.lines.append((line_number, 0, line)) - - def add_unchanged_line(self, deleted_line_number, new_line_number, line): - self.lines.append((deleted_line_number, new_line_number, line)) - - -# If this is going to be called DiffParser, it should be a re-useable parser. -# Otherwise we should rename it to ParsedDiff or just Diff. -class DiffParser(object): - """A parser for a patch file. - - The field "files" is a dict whose key is the filename and value is - a DiffFile object. - """ - - def __init__(self, diff_input): - """Parses a diff. - - Args: - diff_input: An iterable object. - """ - self.files = self._parse_into_diff_files(diff_input) - - # FIXME: This function is way too long and needs to be broken up. - def _parse_into_diff_files(self, diff_input): - files = {} - state = _INITIAL_STATE - current_file = None - old_diff_line = None - new_diff_line = None - transform_line = get_diff_converter(diff_input) - for line in diff_input: - line = line.rstrip("\n") - line = transform_line(line) - - file_declaration = match(r"^Index: (?P<FilePath>.+)", line) - if file_declaration: - filename = file_declaration.group('FilePath') - current_file = DiffFile(filename) - files[filename] = current_file - state = _DECLARED_FILE_PATH - continue - - lines_changed = match(r"^@@ -(?P<OldStartLine>\d+)(,\d+)? \+(?P<NewStartLine>\d+)(,\d+)? @@", line) - if lines_changed: - if state != _DECLARED_FILE_PATH and state != _PROCESSING_CHUNK: - _log.error('Unexpected line change without file path ' - 'declaration: %r' % line) - old_diff_line = int(lines_changed.group('OldStartLine')) - new_diff_line = int(lines_changed.group('NewStartLine')) - state = _PROCESSING_CHUNK - continue - - if state == _PROCESSING_CHUNK: - if line.startswith('+'): - current_file.add_new_line(new_diff_line, line[1:]) - new_diff_line += 1 - elif line.startswith('-'): - current_file.add_deleted_line(old_diff_line, line[1:]) - old_diff_line += 1 - elif line.startswith(' '): - current_file.add_unchanged_line(old_diff_line, new_diff_line, line[1:]) - old_diff_line += 1 - new_diff_line += 1 - elif line == '\\ No newline at end of file': - # Nothing to do. We may still have some added lines. - pass - else: - _log.error('Unexpected diff format when parsing a ' - 'chunk: %r' % line) - return files diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_parser_unittest.py b/Tools/Scripts/webkitpy/common/checkout/diff_parser_unittest.py deleted file mode 100644 index 78dab26bc..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/diff_parser_unittest.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (C) 2009 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 cStringIO as StringIO -import unittest2 as unittest -import diff_parser -import re - -from webkitpy.common.checkout.diff_test_data import DIFF_TEST_DATA - -class DiffParserTest(unittest.TestCase): - maxDiff = None - - def test_diff_parser(self, parser = None): - if not parser: - parser = diff_parser.DiffParser(DIFF_TEST_DATA.splitlines()) - self.assertEqual(3, len(parser.files)) - - self.assertTrue('WebCore/rendering/style/StyleFlexibleBoxData.h' in parser.files) - diff = parser.files['WebCore/rendering/style/StyleFlexibleBoxData.h'] - self.assertEqual(7, len(diff.lines)) - # The first two unchaged lines. - self.assertEqual((47, 47), diff.lines[0][0:2]) - self.assertEqual('', diff.lines[0][2]) - self.assertEqual((48, 48), diff.lines[1][0:2]) - self.assertEqual(' unsigned align : 3; // EBoxAlignment', diff.lines[1][2]) - # The deleted line - self.assertEqual((50, 0), diff.lines[3][0:2]) - self.assertEqual(' unsigned orient: 1; // EBoxOrient', diff.lines[3][2]) - - # The first file looks OK. Let's check the next, more complicated file. - self.assertTrue('WebCore/rendering/style/StyleRareInheritedData.cpp' in parser.files) - diff = parser.files['WebCore/rendering/style/StyleRareInheritedData.cpp'] - # There are 3 chunks. - self.assertEqual(7 + 7 + 9, len(diff.lines)) - # Around an added line. - self.assertEqual((60, 61), diff.lines[9][0:2]) - self.assertEqual((0, 62), diff.lines[10][0:2]) - self.assertEqual((61, 63), diff.lines[11][0:2]) - # Look through the last chunk, which contains both add's and delete's. - self.assertEqual((81, 83), diff.lines[14][0:2]) - self.assertEqual((82, 84), diff.lines[15][0:2]) - self.assertEqual((83, 85), diff.lines[16][0:2]) - self.assertEqual((84, 0), diff.lines[17][0:2]) - self.assertEqual((0, 86), diff.lines[18][0:2]) - self.assertEqual((0, 87), diff.lines[19][0:2]) - self.assertEqual((85, 88), diff.lines[20][0:2]) - self.assertEqual((86, 89), diff.lines[21][0:2]) - self.assertEqual((87, 90), diff.lines[22][0:2]) - - # Check if a newly added file is correctly handled. - diff = parser.files['LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum'] - self.assertEqual(1, len(diff.lines)) - self.assertEqual((0, 1), diff.lines[0][0:2]) - - def test_diff_converter(self): - comment_lines = [ - "Hey guys,\n", - "\n", - "See my awesome patch below!\n", - "\n", - " - Cool Hacker\n", - "\n", - ] - - revision_lines = [ - "Subversion Revision 289799\n", - ] - - svn_diff_lines = [ - "Index: Tools/Scripts/webkitpy/common/checkout/diff_parser.py\n", - "===================================================================\n", - "--- Tools/Scripts/webkitpy/common/checkout/diff_parser.py\n", - "+++ Tools/Scripts/webkitpy/common/checkout/diff_parser.py\n", - "@@ -59,6 +59,7 @@ def git_diff_to_svn_diff(line):\n", - ] - self.assertEqual(diff_parser.get_diff_converter(svn_diff_lines), diff_parser.svn_diff_to_svn_diff) - self.assertEqual(diff_parser.get_diff_converter(comment_lines + svn_diff_lines), diff_parser.svn_diff_to_svn_diff) - self.assertEqual(diff_parser.get_diff_converter(revision_lines + svn_diff_lines), diff_parser.svn_diff_to_svn_diff) - - git_diff_lines = [ - "diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py\n", - "index 3c5b45b..0197ead 100644\n", - "--- a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py\n", - "+++ b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py\n", - "@@ -59,6 +59,7 @@ def git_diff_to_svn_diff(line):\n", - ] - self.assertEqual(diff_parser.get_diff_converter(git_diff_lines), diff_parser.git_diff_to_svn_diff) - self.assertEqual(diff_parser.get_diff_converter(comment_lines + git_diff_lines), diff_parser.git_diff_to_svn_diff) - self.assertEqual(diff_parser.get_diff_converter(revision_lines + git_diff_lines), diff_parser.git_diff_to_svn_diff) - - def test_git_mnemonicprefix(self): - p = re.compile(r' ([a|b])/') - - prefixes = [ - { 'a' : 'i', 'b' : 'w' }, # git-diff (compares the (i)ndex and the (w)ork tree) - { 'a' : 'c', 'b' : 'w' }, # git-diff HEAD (compares a (c)ommit and the (w)ork tree) - { 'a' : 'c', 'b' : 'i' }, # git diff --cached (compares a (c)ommit and the (i)ndex) - { 'a' : 'o', 'b' : 'w' }, # git-diff HEAD:file1 file2 (compares an (o)bject and a (w)ork tree entity) - { 'a' : '1', 'b' : '2' }, # git diff --no-index a b (compares two non-git things (1) and (2)) - ] - - for prefix in prefixes: - patch = p.sub(lambda x: " %s/" % prefix[x.group(1)], DIFF_TEST_DATA) - self.test_diff_parser(diff_parser.DiffParser(patch.splitlines())) - - def test_git_diff_to_svn_diff(self): - output = """\ -Index: Tools/Scripts/webkitpy/common/checkout/diff_parser.py -=================================================================== ---- Tools/Scripts/webkitpy/common/checkout/diff_parser.py -+++ Tools/Scripts/webkitpy/common/checkout/diff_parser.py -@@ -59,6 +59,7 @@ def git_diff_to_svn_diff(line): - A - B - C -+D - E - F -""" - - inputfmt = StringIO.StringIO("""\ -diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py -index 2ed552c4555db72df16b212547f2c125ae301a04..72870482000c0dba64ce4300ed782c03ee79b74f 100644 ---- a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py -+++ b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py -@@ -59,6 +59,7 @@ def git_diff_to_svn_diff(line): - A - B - C -+D - E - F -""") - shortfmt = StringIO.StringIO("""\ -diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py -index b48b162..f300960 100644 ---- a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py -+++ b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py -@@ -59,6 +59,7 @@ def git_diff_to_svn_diff(line): - A - B - C -+D - E - F -""") - - self.assertMultiLineEqual(output, ''.join(diff_parser.git_diff_to_svn_diff(x) for x in shortfmt.readlines())) - self.assertMultiLineEqual(output, ''.join(diff_parser.git_diff_to_svn_diff(x) for x in inputfmt.readlines())) diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_test_data.py b/Tools/Scripts/webkitpy/common/checkout/diff_test_data.py deleted file mode 100644 index 5f1719da8..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/diff_test_data.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (C) 2011 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. - -# FIXME: Store this as a .patch file in some new fixtures directory or similar. -DIFF_TEST_DATA = '''diff --git a/WebCore/rendering/style/StyleFlexibleBoxData.h b/WebCore/rendering/style/StyleFlexibleBoxData.h -index f5d5e74..3b6aa92 100644 ---- a/WebCore/rendering/style/StyleFlexibleBoxData.h -+++ b/WebCore/rendering/style/StyleFlexibleBoxData.h -@@ -47,7 +47,6 @@ public: - - unsigned align : 3; // EBoxAlignment - unsigned pack: 3; // EBoxAlignment -- unsigned orient: 1; // EBoxOrient - unsigned lines : 1; // EBoxLines - - private: -diff --git a/WebCore/rendering/style/StyleRareInheritedData.cpp b/WebCore/rendering/style/StyleRareInheritedData.cpp -index ce21720..324929e 100644 ---- a/WebCore/rendering/style/StyleRareInheritedData.cpp -+++ b/WebCore/rendering/style/StyleRareInheritedData.cpp -@@ -39,6 +39,7 @@ StyleRareInheritedData::StyleRareInheritedData() - , textSizeAdjust(RenderStyle::initialTextSizeAdjust()) - , resize(RenderStyle::initialResize()) - , userSelect(RenderStyle::initialUserSelect()) -+ , boxOrient(RenderStyle::initialBoxOrient()) - { - } - -@@ -58,6 +59,7 @@ StyleRareInheritedData::StyleRareInheritedData(const StyleRareInheritedData& o) - , textSizeAdjust(o.textSizeAdjust) - , resize(o.resize) - , userSelect(o.userSelect) -+ , boxOrient(o.boxOrient) - { - } - -@@ -81,7 +83,8 @@ bool StyleRareInheritedData::operator==(const StyleRareInheritedData& o) const - && khtmlLineBreak == o.khtmlLineBreak - && textSizeAdjust == o.textSizeAdjust - && resize == o.resize -- && userSelect == o.userSelect; -+ && userSelect == o.userSelect -+ && boxOrient == o.boxOrient; - } - - bool StyleRareInheritedData::shadowDataEquivalent(const StyleRareInheritedData& o) const -diff --git a/LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum b/LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum -new file mode 100644 -index 0000000..6db26bd ---- /dev/null -+++ b/LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum -@@ -0,0 +1 @@ -+61a373ee739673a9dcd7bac62b9f182e -\ No newline at end of file -''' diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/__init__.py b/Tools/Scripts/webkitpy/common/checkout/scm/__init__.py deleted file mode 100644 index f691f58e1..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Required for Python to search this directory for module files - -# We only export public API here. -from .commitmessage import CommitMessage -from .detection import SCMDetector -from .git import Git, AmbiguousCommitError -from .scm import SCM, AuthenticationError, CheckoutNeedsUpdate -from .svn import SVN diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/commitmessage.py b/Tools/Scripts/webkitpy/common/checkout/scm/commitmessage.py deleted file mode 100644 index be0d431f9..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/commitmessage.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved. -# Copyright (c) 2009 Apple 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 re - - -def _first_non_empty_line_after_index(lines, index=0): - first_non_empty_line = index - for line in lines[index:]: - if re.match("^\s*$", line): - first_non_empty_line += 1 - else: - break - return first_non_empty_line - - -class CommitMessage: - def __init__(self, message): - self.message_lines = message[_first_non_empty_line_after_index(message, 0):] - - def body(self, lstrip=False): - lines = self.message_lines[_first_non_empty_line_after_index(self.message_lines, 1):] - if lstrip: - lines = [line.lstrip() for line in lines] - return "\n".join(lines) + "\n" - - def description(self, lstrip=False, strip_url=False): - line = self.message_lines[0] - if lstrip: - line = line.lstrip() - if strip_url: - line = re.sub("^(\s*)<.+> ", "\1", line) - return line - - def message(self): - return "\n".join(self.message_lines) + "\n" diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/detection.py b/Tools/Scripts/webkitpy/common/checkout/scm/detection.py deleted file mode 100644 index e635b4075..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/detection.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved. -# Copyright (c) 2009 Apple 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 logging - -from webkitpy.common.system.filesystem import FileSystem -from webkitpy.common.system.executive import Executive - -from .svn import SVN -from .git import Git - -_log = logging.getLogger(__name__) - - -class SCMDetector(object): - def __init__(self, filesystem, executive): - self._filesystem = filesystem - self._executive = executive - - def default_scm(self, patch_directories=None): - """Return the default SCM object as determined by the CWD and running code. - - Returns the default SCM object for the current working directory; if the - CWD is not in a checkout, then we attempt to figure out if the SCM module - itself is part of a checkout, and return that one. If neither is part of - a checkout, None is returned. - """ - cwd = self._filesystem.getcwd() - scm_system = self.detect_scm_system(cwd, patch_directories) - if not scm_system: - script_directory = self._filesystem.dirname(self._filesystem.path_to_module(self.__module__)) - scm_system = self.detect_scm_system(script_directory, patch_directories) - if scm_system: - _log.info("The current directory (%s) is not a WebKit checkout, using %s" % (cwd, scm_system.checkout_root)) - else: - raise Exception("FATAL: Failed to determine the SCM system for either %s or %s" % (cwd, script_directory)) - return scm_system - - def detect_scm_system(self, path, patch_directories=None): - absolute_path = self._filesystem.abspath(path) - - if patch_directories == []: - patch_directories = None - - if SVN.in_working_directory(absolute_path, executive=self._executive): - return SVN(cwd=absolute_path, patch_directories=patch_directories, filesystem=self._filesystem, executive=self._executive) - - if Git.in_working_directory(absolute_path, executive=self._executive): - return Git(cwd=absolute_path, filesystem=self._filesystem, executive=self._executive) - - return None - - -# FIXME: These free functions are all deprecated: - -def detect_scm_system(path, patch_directories=None): - return SCMDetector(FileSystem(), Executive()).detect_scm_system(path, patch_directories) diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/detection_unittest.py b/Tools/Scripts/webkitpy/common/checkout/scm/detection_unittest.py deleted file mode 100644 index 593f093c4..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/detection_unittest.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (C) 2009 Google Inc. All rights reserved. -# Copyright (C) 2009 Apple Inc. All rights reserved. -# Copyright (C) 2011 Daniel Bates (dbates@intudata.com). 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 unittest2 as unittest - -from .detection import SCMDetector -from webkitpy.common.system.filesystem_mock import MockFileSystem -from webkitpy.common.system.executive_mock import MockExecutive -from webkitpy.common.system.outputcapture import OutputCapture - - -class SCMDetectorTest(unittest.TestCase): - def test_detect_scm_system(self): - filesystem = MockFileSystem() - executive = MockExecutive(should_log=True) - detector = SCMDetector(filesystem, executive) - - expected_logs = """\ -MOCK run_command: ['svn', 'info'], cwd=/ -MOCK run_command: ['git', 'rev-parse', '--is-inside-work-tree'], cwd=/ -""" - scm = OutputCapture().assert_outputs(self, detector.detect_scm_system, ["/"], expected_logs=expected_logs) - self.assertIsNone(scm) - # FIXME: This should make a synthetic tree and test SVN and Git detection in that tree. diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/git.py b/Tools/Scripts/webkitpy/common/checkout/scm/git.py deleted file mode 100644 index 58eda7032..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/git.py +++ /dev/null @@ -1,514 +0,0 @@ -# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved. -# Copyright (c) 2009 Apple 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 datetime -import logging -import os -import re - -from webkitpy.common.memoized import memoized -from webkitpy.common.system.executive import Executive, ScriptError - -from .commitmessage import CommitMessage -from .scm import AuthenticationError, SCM, commit_error_handler -from .svn import SVN, SVNRepository - -_log = logging.getLogger(__name__) - - -class AmbiguousCommitError(Exception): - def __init__(self, num_local_commits, has_working_directory_changes): - Exception.__init__(self, "Found %s local commits and the working directory is %s" % ( - num_local_commits, ["clean", "not clean"][has_working_directory_changes])) - self.num_local_commits = num_local_commits - self.has_working_directory_changes = has_working_directory_changes - - -class Git(SCM, SVNRepository): - - # Git doesn't appear to document error codes, but seems to return - # 1 or 128, mostly. - ERROR_FILE_IS_MISSING = 128 - - executable_name = 'git' - - def __init__(self, cwd, **kwargs): - SCM.__init__(self, cwd, **kwargs) - self._check_git_architecture() - - def _machine_is_64bit(self): - import platform - # This only is tested on Mac. - if not platform.mac_ver()[0]: - return False - - # platform.architecture()[0] can be '64bit' even if the machine is 32bit: - # http://mail.python.org/pipermail/pythonmac-sig/2009-September/021648.html - # Use the sysctl command to find out what the processor actually supports. - return self.run(['sysctl', '-n', 'hw.cpu64bit_capable']).rstrip() == '1' - - def _executable_is_64bit(self, path): - # Again, platform.architecture() fails us. On my machine - # git_bits = platform.architecture(executable=git_path, bits='default')[0] - # git_bits is just 'default', meaning the call failed. - file_output = self.run(['file', path]) - return re.search('x86_64', file_output) - - def _check_git_architecture(self): - if not self._machine_is_64bit(): - return - - # We could path-search entirely in python or with - # which.py (http://code.google.com/p/which), but this is easier: - git_path = self.run(['which', self.executable_name]).rstrip() - if self._executable_is_64bit(git_path): - return - - webkit_dev_thread_url = "https://lists.webkit.org/pipermail/webkit-dev/2010-December/015287.html" - _log.warning("This machine is 64-bit, but the git binary (%s) does not support 64-bit.\nInstall a 64-bit git for better performance, see:\n%s\n" % (git_path, webkit_dev_thread_url)) - - def _run_git(self, command_args, **kwargs): - full_command_args = [self.executable_name] + command_args - full_kwargs = kwargs - if not 'cwd' in full_kwargs: - full_kwargs['cwd'] = self.checkout_root - return self.run(full_command_args, **full_kwargs) - - @classmethod - def in_working_directory(cls, path, executive=None): - try: - executive = executive or Executive() - return executive.run_command([cls.executable_name, 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true" - except OSError, e: - # The Windows bots seem to through a WindowsError when git isn't installed. - return False - - def find_checkout_root(self, path): - # "git rev-parse --show-cdup" would be another way to get to the root - checkout_root = self._run_git(['rev-parse', '--show-toplevel'], cwd=(path or "./")).strip() - if not self._filesystem.isabs(checkout_root): # Sometimes git returns relative paths - checkout_root = self._filesystem.join(path, checkout_root) - return checkout_root - - def to_object_name(self, filepath): - # FIXME: This can't be the right way to append a slash. - root_end_with_slash = self._filesystem.join(self.find_checkout_root(self._filesystem.dirname(filepath)), '') - # FIXME: This seems to want some sort of rel_path instead? - return filepath.replace(root_end_with_slash, '') - - @classmethod - def read_git_config(cls, key, cwd=None, executive=None): - # FIXME: This should probably use cwd=self.checkout_root. - # Pass --get-all for cases where the config has multiple values - # Pass the cwd if provided so that we can handle the case of running webkit-patch outside of the working directory. - # FIXME: This should use an Executive. - executive = executive or Executive() - return executive.run_command([cls.executable_name, "config", "--get-all", key], error_handler=Executive.ignore_error, cwd=cwd).rstrip('\n') - - @staticmethod - def commit_success_regexp(): - return "^Committed r(?P<svn_revision>\d+)$" - - def discard_local_commits(self): - self._run_git(['reset', '--hard', self.remote_branch_ref()]) - - def local_commits(self): - return self._run_git(['log', '--pretty=oneline', 'HEAD...' + self.remote_branch_ref()]).splitlines() - - def rebase_in_progress(self): - return self._filesystem.exists(self.absolute_path(self._filesystem.join('.git', 'rebase-apply'))) - - def has_working_directory_changes(self): - return self._run_git(['diff', 'HEAD', '--no-renames', '--name-only']) != "" - - def discard_working_directory_changes(self): - # Could run git clean here too, but that wouldn't match subversion - self._run_git(['reset', 'HEAD', '--hard']) - # Aborting rebase even though this does not match subversion - if self.rebase_in_progress(): - self._run_git(['rebase', '--abort']) - - 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 self.run is fine. - return [self.executable_name, "diff", "--name-status", "--no-renames", "HEAD"] - - def _status_regexp(self, expected_types): - return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types - - def add_list(self, paths): - self._run_git(["add"] + paths) - - def delete_list(self, paths): - return self._run_git(["rm", "-f"] + paths) - - def exists(self, path): - return_code = self._run_git(["show", "HEAD:%s" % path], return_exit_code=True, decode_output=False) - return return_code != self.ERROR_FILE_IS_MISSING - - def _branch_from_ref(self, ref): - return ref.replace('refs/heads/', '') - - def _current_branch(self): - return self._branch_from_ref(self._run_git(['symbolic-ref', '-q', 'HEAD']).strip()) - - def _upstream_branch(self): - current_branch = self._current_branch() - return self._branch_from_ref(self.read_git_config('branch.%s.merge' % current_branch, cwd=self.checkout_root, executive=self._executive).strip()) - - def merge_base(self, git_commit): - if git_commit: - # Rewrite UPSTREAM to the upstream branch - if 'UPSTREAM' in git_commit: - upstream = self._upstream_branch() - if not upstream: - raise ScriptError(message='No upstream/tracking branch set.') - git_commit = git_commit.replace('UPSTREAM', upstream) - - # Special-case <refname>.. to include working copy changes, e.g., 'HEAD....' shows only the diffs from HEAD. - if git_commit.endswith('....'): - return git_commit[:-4] - - if '..' not in git_commit: - git_commit = git_commit + "^.." + git_commit - return git_commit - - return self.remote_merge_base() - - def changed_files(self, git_commit=None): - # FIXME: --diff-filter could be used to avoid the "extract_filenames" step. - status_command = [self.executable_name, 'diff', '-r', '--name-status', "--no-renames", "--no-ext-diff", "--full-index", self.merge_base(git_commit)] - # FIXME: I'm not sure we're returning the same set of files that SVN.changed_files is. - # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R) - return self.run_status_and_extract_filenames(status_command, self._status_regexp("ADM")) - - def _changes_files_for_commit(self, git_commit): - # --pretty="format:" makes git show not print the commit log header, - changed_files = self._run_git(["show", "--pretty=format:", "--name-only", git_commit]).splitlines() - # instead it just prints a blank line at the top, so we skip the blank line: - return changed_files[1:] - - def changed_files_for_revision(self, revision): - commit_id = self.git_commit_from_svn_revision(revision) - return self._changes_files_for_commit(commit_id) - - def revisions_changing_file(self, path, limit=5): - # raise a script error if path does not exists to match the behavior of the svn implementation. - if not self._filesystem.exists(path): - raise ScriptError(message="Path %s does not exist." % path) - - # git rev-list head --remove-empty --limit=5 -- path would be equivalent. - commit_ids = self._run_git(["log", "--remove-empty", "--pretty=format:%H", "-%s" % limit, "--", path]).splitlines() - return filter(lambda revision: revision, map(self.svn_revision_from_git_commit, commit_ids)) - - 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 = [self.executable_name, 'diff', '--name-status', '--no-renames', '--diff-filter=U'] - return self.run_status_and_extract_filenames(status_command, self._status_regexp("U")) - - def added_files(self): - return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("A")) - - def deleted_files(self): - return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("D")) - - @staticmethod - def supports_local_commits(): - return True - - def display_name(self): - return "git" - - def _most_recent_log_matching(self, grep_str, path): - # We use '--grep=' + foo rather than '--grep', foo because - # git 1.7.0.4 (and earlier) didn't support the separate arg. - return self._run_git(['log', '-1', '--grep=' + grep_str, '--date=iso', self.find_checkout_root(path)]) - - def svn_revision(self, path): - git_log = self._most_recent_log_matching('git-svn-id:', path) - match = re.search("^\s*git-svn-id:.*@(?P<svn_revision>\d+)\ ", git_log, re.MULTILINE) - if not match: - return "" - return str(match.group('svn_revision')) - - def timestamp_of_revision(self, path, revision): - git_log = self._most_recent_log_matching('git-svn-id:.*@%s' % revision, path) - match = re.search("^Date:\s*(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([+-])(\d{2})(\d{2})$", git_log, re.MULTILINE) - if not match: - return "" - - # Manually modify the timezone since Git doesn't have an option to show it in UTC. - # Git also truncates milliseconds but we're going to ignore that for now. - time_with_timezone = datetime.datetime(int(match.group(1)), int(match.group(2)), int(match.group(3)), - int(match.group(4)), int(match.group(5)), int(match.group(6)), 0) - - sign = 1 if match.group(7) == '+' else -1 - time_without_timezone = time_with_timezone - datetime.timedelta(hours=sign * int(match.group(8)), minutes=int(match.group(9))) - return time_without_timezone.strftime('%Y-%m-%dT%H:%M:%SZ') - - def prepend_svn_revision(self, diff): - revision = self.head_svn_revision() - if not revision: - return diff - - return "Subversion Revision: " + revision + '\n' + diff - - def create_patch(self, git_commit=None, changed_files=None): - """Returns a byte array (str()) representing the patch file. - Patch files are effectively binary since they may contain - files of multiple different encodings.""" - - # Put code changes at the top of the patch and layout tests - # at the bottom, this makes for easier reviewing. - config_path = self._filesystem.dirname(self._filesystem.path_to_module('webkitpy.common.config')) - order_file = self._filesystem.join(config_path, 'orderfile') - order = "" - if self._filesystem.exists(order_file): - order = "-O%s" % order_file - - command = [self.executable_name, 'diff', '--binary', '--no-color', "--no-ext-diff", "--full-index", "--no-renames", order, self.merge_base(git_commit), "--"] - if changed_files: - command += changed_files - return self.prepend_svn_revision(self.run(command, decode_output=False, cwd=self.checkout_root)) - - def _run_git_svn_find_rev(self, arg): - # git svn find-rev always exits 0, even when the revision or commit is not found. - return self._run_git(['svn', 'find-rev', arg]).rstrip() - - def _string_to_int_or_none(self, string): - try: - return int(string) - except ValueError, e: - return None - - @memoized - def git_commit_from_svn_revision(self, svn_revision): - # FIXME: https://bugs.webkit.org/show_bug.cgi?id=111668 - # We should change this to run git log --grep 'git-svn-id' instead - # so that we don't require git+svn to be set up. - git_commit = self._run_git_svn_find_rev('r%s' % svn_revision) - if not git_commit: - # FIXME: Alternatively we could offer to update the checkout? Or return None? - raise ScriptError(message='Failed to find git commit for revision %s, your checkout likely needs an update.' % svn_revision) - return git_commit - - @memoized - def svn_revision_from_git_commit(self, git_commit): - svn_revision = self._run_git_svn_find_rev(git_commit) - return self._string_to_int_or_none(svn_revision) - - def contents_at_revision(self, path, revision): - """Returns a byte array (str()) containing the contents - of path @ revision in the repository.""" - return self._run_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(git_commit) - - def diff_for_file(self, path, log=None): - return self._run_git(['diff', 'HEAD', '--no-renames', '--', path]) - - def show_head(self, path): - return self._run_git(['show', 'HEAD:' + self.to_object_name(path)], decode_output=False) - - def committer_email_for_revision(self, revision): - git_commit = self.git_commit_from_svn_revision(revision) - committer_email = self._run_git(["log", "-1", "--pretty=format:%ce", git_commit]) - # Git adds an extra @repository_hash to the end of every committer email, remove it: - return committer_email.rsplit("@", 1)[0] - - def apply_reverse_diff(self, revision): - # Assume the revision is an svn revision. - git_commit = self.git_commit_from_svn_revision(revision) - # I think this will always fail due to ChangeLogs. - self._run_git(['revert', '--no-commit', git_commit], error_handler=Executive.ignore_error) - - def revert_files(self, file_paths): - self._run_git(['checkout', 'HEAD'] + file_paths) - - def _assert_can_squash(self, has_working_directory_changes): - squash = self.read_git_config('webkit-patch.commit-should-always-squash', cwd=self.checkout_root, executive=self._executive) - should_squash = squash and squash.lower() == "true" - - if not should_squash: - # Only warn 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 has_working_directory_changes): - raise AmbiguousCommitError(num_local_commits, has_working_directory_changes) - - def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None): - # Username is ignored during Git commits. - has_working_directory_changes = self.has_working_directory_changes() - - if git_commit: - # Special-case HEAD.. to mean working-copy changes only. - if git_commit.upper() == 'HEAD..': - if not has_working_directory_changes: - raise ScriptError(message="The working copy is not modified. --git-commit=HEAD.. only commits working copy changes.") - self.commit_locally_with_message(message) - return self._commit_on_branch(message, 'HEAD', username=username, password=password) - - # Need working directory changes to be committed so we can checkout the merge branch. - if has_working_directory_changes: - # 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, username=username, password=password) - - if not force_squash: - self._assert_can_squash(has_working_directory_changes) - self._run_git(['reset', '--soft', self.remote_merge_base()]) - self.commit_locally_with_message(message) - return self.push_local_commits_to_server(username=username, password=password) - - def _commit_on_branch(self, message, git_commit, username=None, password=None): - branch_name = self._current_branch() - 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_NAME = 'webkit-patch-land' - self.delete_branch(MERGE_BRANCH_NAME) - - # 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. - # FIXME: We should never be using chdir! We can instead pass cwd= to run_command/self.run! - self._filesystem.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: - self._run_git(['checkout', '-q', '-b', MERGE_BRANCH_NAME, self.remote_branch_ref()]) - - 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. - self._run_git(['cherry-pick', '--no-commit', commit]) - - self._run_git(['commit', '-m', message]) - output = self.push_local_commits_to_server(username=username, password=password) - except Exception, e: - _log.warning("COMMIT FAILED: " + str(e)) - output = "Commit failed." - commit_succeeded = False - finally: - # And then swap back to the original branch and clean up. - self.discard_working_directory_changes() - self._run_git(['checkout', '-q', branch_name]) - self.delete_branch(MERGE_BRANCH_NAME) - - return output - - def svn_commit_log(self, svn_revision): - svn_revision = self.strip_r_from_svn_revision(svn_revision) - return self._run_git(['svn', 'log', '-r', svn_revision]) - - def last_svn_commit_log(self): - return self._run_git(['svn', 'log', '--limit=1']) - - def svn_blame(self, path): - return self._run_git(['svn', 'blame', path]) - - # Git-specific methods: - def _branch_ref_exists(self, branch_ref): - return self._run_git(['show-ref', '--quiet', '--verify', branch_ref], return_exit_code=True) == 0 - - def delete_branch(self, branch_name): - if self._branch_ref_exists('refs/heads/' + branch_name): - self._run_git(['branch', '-D', branch_name]) - - def remote_merge_base(self): - return self._run_git(['merge-base', self.remote_branch_ref(), 'HEAD']).strip() - - def remote_branch_ref(self): - # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists. - remote_branch_refs = self.read_git_config('svn-remote.svn.fetch', cwd=self.checkout_root, executive=self._executive) - if not remote_branch_refs: - remote_master_ref = 'refs/remotes/origin/master' - if not self._branch_ref_exists(remote_master_ref): - raise ScriptError(message="Can't find a branch to diff against. svn-remote.svn.fetch is not in the git config and %s does not exist" % remote_master_ref) - return remote_master_ref - - # FIXME: What's the right behavior when there are multiple svn-remotes listed? - # For now, just use the first one. - first_remote_branch_ref = remote_branch_refs.split('\n')[0] - return first_remote_branch_ref.split(':')[1] - - def commit_locally_with_message(self, message): - self._run_git(['commit', '--all', '-F', '-'], input=message) - - def push_local_commits_to_server(self, username=None, password=None): - dcommit_command = ['svn', 'dcommit'] - if (not username or not password) and not self.has_authorization_for_realm(self.svn_server_realm): - raise AuthenticationError(self.svn_server_host, prompt_for_password=True) - if username: - dcommit_command.extend(["--username", username]) - output = self._run_git(dcommit_command, error_handler=commit_error_handler, input=password) - return output - - # This function supports the following argument formats: - # no args : rev-list trunk..HEAD - # A..B : rev-list A..B - # A...B : error! - # A B : [A, B] (different from git diff, which would use "rev-list A..B") - def commit_ids_from_commitish_arguments(self, args): - if not len(args): - args.append('%s..HEAD' % self.remote_branch_ref()) - - commit_ids = [] - for commitish in args: - if '...' in commitish: - raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish) - elif '..' in commitish: - commit_ids += reversed(self._run_git(['rev-list', commitish]).splitlines()) - else: - # Turn single commits or branch or tag names into commit ids. - commit_ids += self._run_git(['rev-parse', '--revs-only', commitish]).splitlines() - return commit_ids - - def commit_message_for_local_commit(self, commit_id): - commit_lines = self._run_git(['cat-file', 'commit', commit_id]).splitlines() - - # Skip the git headers. - first_line_after_headers = 0 - for line in commit_lines: - first_line_after_headers += 1 - if line == "": - break - return CommitMessage(commit_lines[first_line_after_headers:]) - - def files_changed_summary_for_commit(self, commit_id): - return self._run_git(['diff-tree', '--shortstat', '--no-renames', '--no-commit-id', commit_id]) diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/scm.py b/Tools/Scripts/webkitpy/common/checkout/scm/scm.py deleted file mode 100644 index b005ea239..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/scm.py +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright (c) 2009, Google Inc. All rights reserved. -# Copyright (c) 2009 Apple 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. -# -# Python module for interacting with an SCM system (like SVN or Git) - -import logging -import re -import sys - -from webkitpy.common.system.executive import Executive, ScriptError -from webkitpy.common.system.filesystem import FileSystem - -_log = logging.getLogger(__name__) - - -class CheckoutNeedsUpdate(ScriptError): - def __init__(self, script_args, exit_code, output, cwd): - ScriptError.__init__(self, script_args=script_args, exit_code=exit_code, output=output, cwd=cwd) - - -# FIXME: Should be moved onto SCM -def commit_error_handler(error): - if re.search("resource out of date", error.output): - raise CheckoutNeedsUpdate(script_args=error.script_args, exit_code=error.exit_code, output=error.output, cwd=error.cwd) - Executive.default_error_handler(error) - - -class AuthenticationError(Exception): - def __init__(self, server_host, prompt_for_password=False): - self.server_host = server_host - self.prompt_for_password = prompt_for_password - - - -# SCM methods are expected to return paths relative to self.checkout_root. -class SCM: - def __init__(self, cwd, executive=None, filesystem=None): - self.cwd = cwd - self._executive = executive or Executive() - self._filesystem = filesystem or FileSystem() - self.checkout_root = self.find_checkout_root(self.cwd) - - # A wrapper used by subclasses to create processes. - def run(self, args, cwd=None, input=None, error_handler=None, return_exit_code=False, return_stderr=True, decode_output=True): - # FIXME: We should set cwd appropriately. - return self._executive.run_command(args, - cwd=cwd, - input=input, - error_handler=error_handler, - return_exit_code=return_exit_code, - return_stderr=return_stderr, - decode_output=decode_output) - - # SCM always returns repository relative path, but sometimes we need - # absolute paths to pass to rm, etc. - def absolute_path(self, repository_relative_path): - return self._filesystem.join(self.checkout_root, repository_relative_path) - - # FIXME: This belongs in Checkout, not SCM. - def scripts_directory(self): - return self._filesystem.join(self.checkout_root, "Tools", "Scripts") - - # FIXME: This belongs in Checkout, not SCM. - def script_path(self, script_name): - return self._filesystem.join(self.scripts_directory(), script_name) - - def run_status_and_extract_filenames(self, status_command, status_regexp): - filenames = [] - # We run with cwd=self.checkout_root so that returned-paths are root-relative. - for line in self.run(status_command, cwd=self.checkout_root).splitlines(): - match = re.search(status_regexp, line) - if not match: - continue - # status = match.group('status') - filename = match.group('filename') - filenames.append(filename) - return filenames - - def strip_r_from_svn_revision(self, svn_revision): - match = re.match("^r(?P<svn_revision>\d+)", unicode(svn_revision)) - if (match): - return match.group('svn_revision') - return svn_revision - - def svn_revision_from_commit_text(self, commit_text): - match = re.search(self.commit_success_regexp(), commit_text, re.MULTILINE) - return match.group('svn_revision') - - @staticmethod - def _subclass_must_implement(): - raise NotImplementedError("subclasses must implement") - - @classmethod - def in_working_directory(cls, path, executive=None): - SCM._subclass_must_implement() - - def find_checkout_root(self, path): - SCM._subclass_must_implement() - - @staticmethod - def commit_success_regexp(): - SCM._subclass_must_implement() - - def status_command(self): - self._subclass_must_implement() - - def add(self, path): - self.add_list([path]) - - def add_list(self, paths): - self._subclass_must_implement() - - def delete(self, path): - self.delete_list([path]) - - def delete_list(self, paths): - self._subclass_must_implement() - - def exists(self, path): - self._subclass_must_implement() - - def changed_files(self, git_commit=None): - self._subclass_must_implement() - - def changed_files_for_revision(self, revision): - self._subclass_must_implement() - - def revisions_changing_file(self, path, limit=5): - self._subclass_must_implement() - - def added_files(self): - self._subclass_must_implement() - - def conflicted_files(self): - self._subclass_must_implement() - - def display_name(self): - self._subclass_must_implement() - - def head_svn_revision(self): - return self.svn_revision(self.checkout_root) - - def svn_revision(self, path): - """Returns the latest svn revision found in the checkout.""" - self._subclass_must_implement() - - def timestamp_of_revision(self, path, revision): - self._subclass_must_implement() - - def create_patch(self, git_commit=None, changed_files=None): - self._subclass_must_implement() - - def committer_email_for_revision(self, revision): - self._subclass_must_implement() - - def contents_at_revision(self, path, revision): - self._subclass_must_implement() - - def diff_for_revision(self, revision): - self._subclass_must_implement() - - def diff_for_file(self, path, log=None): - self._subclass_must_implement() - - def show_head(self, path): - self._subclass_must_implement() - - def apply_reverse_diff(self, revision): - self._subclass_must_implement() - - def revert_files(self, file_paths): - self._subclass_must_implement() - - def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None): - self._subclass_must_implement() - - def svn_commit_log(self, svn_revision): - self._subclass_must_implement() - - def last_svn_commit_log(self): - self._subclass_must_implement() - - def svn_blame(self, path): - self._subclass_must_implement() - - def has_working_directory_changes(self): - self._subclass_must_implement() - - def discard_working_directory_changes(self): - self._subclass_must_implement() - - #-------------------------------------------------------------------------- - # Subclasses must indicate if they support local commits, - # but the SCM baseclass will only call local_commits methods when this is true. - @staticmethod - def supports_local_commits(): - SCM._subclass_must_implement() - - def local_commits(self): - return [] - - def has_local_commits(self): - return len(self.local_commits()) > 0 - - def discard_local_commits(self): - return - - def remote_merge_base(self): - SCM._subclass_must_implement() - - def commit_locally_with_message(self, message): - _log.error("Your source control manager does not support local commits.") - sys.exit(1) - - def local_changes_exist(self): - return (self.supports_local_commits() and self.has_local_commits()) or self.has_working_directory_changes() - - def discard_local_changes(self): - if self.has_working_directory_changes(): - self.discard_working_directory_changes() - - if self.has_local_commits(): - self.discard_local_commits() diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/scm_mock.py b/Tools/Scripts/webkitpy/common/checkout/scm/scm_mock.py deleted file mode 100644 index c5d10fcb1..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/scm_mock.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (C) 2011 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. - -from webkitpy.common.checkout.scm import CommitMessage -from webkitpy.common.system.filesystem_mock import MockFileSystem -from webkitpy.common.system.executive_mock import MockExecutive - - -class MockSCM(object): - def __init__(self, filesystem=None, executive=None): - self.checkout_root = "/mock-checkout" - self.added_paths = set() - self._filesystem = filesystem or MockFileSystem() - self._executive = executive or MockExecutive() - - def add(self, destination_path): - self.add_list([destination_path]) - - def add_list(self, destination_paths): - self.added_paths.update(set(destination_paths)) - - def has_working_directory_changes(self): - return False - - def discard_working_directory_changes(self): - pass - - def supports_local_commits(self): - return True - - def has_local_commits(self): - return False - - def discard_local_commits(self): - pass - - def discard_local_changes(self): - pass - - def exists(self, path): - # TestRealMain.test_real_main (and several other rebaseline tests) are sensitive to this return value. - # We should make those tests more robust, but for now we just return True always (since no test needs otherwise). - return True - - def absolute_path(self, *comps): - return self._filesystem.join(self.checkout_root, *comps) - - def changed_files(self, git_commit=None): - return ["MockFile1"] - - def changed_files_for_revision(self, revision): - return ["MockFile1"] - - def head_svn_revision(self): - return '1234' - - def svn_revision(self, path): - return '5678' - - def timestamp_of_revision(self, path, revision): - return '2013-02-01 08:48:05 +0000' - - def create_patch(self, git_commit, changed_files=None): - return "Patch1" - - def commit_ids_from_commitish_arguments(self, args): - return ["Commitish1", "Commitish2"] - - def committer_email_for_revision(self, revision): - return "mock@webkit.org" - - def commit_locally_with_message(self, message): - pass - - def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None): - pass - - def merge_base(self, git_commit): - return None - - def commit_message_for_local_commit(self, commit_id): - if commit_id == "Commitish1": - return CommitMessage("CommitMessage1\n" \ - "https://bugs.example.org/show_bug.cgi?id=50000\n") - if commit_id == "Commitish2": - return CommitMessage("CommitMessage2\n" \ - "https://bugs.example.org/show_bug.cgi?id=50001\n") - raise Exception("Bogus commit_id in commit_message_for_local_commit.") - - def diff_for_file(self, path, log=None): - return path + '-diff' - - def diff_for_revision(self, revision): - return "DiffForRevision%s\nhttp://bugs.webkit.org/show_bug.cgi?id=12345" % revision - - def show_head(self, path): - return path - - def svn_revision_from_commit_text(self, commit_text): - return "49824" - - def delete(self, path): - return self.delete_list([path]) - - def delete_list(self, paths): - if not self._filesystem: - return - for path in paths: - if self._filesystem.exists(path): - self._filesystem.remove(path) diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/scm_unittest.py b/Tools/Scripts/webkitpy/common/checkout/scm/scm_unittest.py deleted file mode 100644 index 15432f0e8..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/scm_unittest.py +++ /dev/null @@ -1,1600 +0,0 @@ -# Copyright (C) 2009 Google Inc. All rights reserved. -# Copyright (C) 2009 Apple Inc. All rights reserved. -# Copyright (C) 2011 Daniel Bates (dbates@intudata.com). 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 atexit -import base64 -import codecs -import getpass -import os -import os.path -import re -import stat -import sys -import subprocess -import tempfile -import time -import unittest2 as unittest -import urllib -import shutil - -from datetime import date -from webkitpy.common.checkout.checkout import Checkout -from webkitpy.common.config.committers import Committer # FIXME: This should not be needed -from webkitpy.common.net.bugzilla import Attachment # FIXME: This should not be needed -from webkitpy.common.system.executive import Executive, ScriptError -from webkitpy.common.system.filesystem_mock import MockFileSystem -from webkitpy.common.system.outputcapture import OutputCapture -from webkitpy.common.system.executive_mock import MockExecutive -from .git import Git, AmbiguousCommitError -from .detection import detect_scm_system -from .scm import SCM, CheckoutNeedsUpdate, commit_error_handler, AuthenticationError -from .svn import SVN - - -# We cache the mock SVN repo so that we don't create it again for each call to an SVNTest or GitTest test_ method. -# We store it in a global variable so that we can delete this cached repo on exit(3). -# FIXME: Remove this once we migrate to Python 2.7. Unittest in Python 2.7 supports module-specific setup and teardown functions. -cached_svn_repo_path = None - - -def remove_dir(path): - # Change directory to / to ensure that we aren't in the directory we want to delete. - os.chdir('/') - shutil.rmtree(path) - - -# FIXME: Remove this once we migrate to Python 2.7. Unittest in Python 2.7 supports module-specific setup and teardown functions. -@atexit.register -def delete_cached_mock_repo_at_exit(): - if cached_svn_repo_path: - remove_dir(cached_svn_repo_path) - -# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.) -# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from. - -def run_command(*args, **kwargs): - # FIXME: This should not be a global static. - # New code should use Executive.run_command directly instead - return Executive().run_command(*args, **kwargs) - - -# 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, encoding="utf-8"): - if encoding: - with codecs.open(file_path, "w", encoding) as file: - file.write(contents) - else: - with open(file_path, "w") 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. - # FIXME: This should use an Executive. - 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(object): - @classmethod - def _svn_add(cls, path): - run_command(["svn", "add", path]) - - @classmethod - def _svn_commit(cls, message): - run_command(["svn", "commit", "--quiet", "--message", message]) - - @classmethod - def _setup_test_commits(cls, svn_repo_url): - - svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout") - run_command(['svn', 'checkout', '--quiet', svn_repo_url, svn_checkout_path]) - - # Add some test commits - os.chdir(svn_checkout_path) - - write_into_file_at_path("test_file", "test1") - cls._svn_add("test_file") - cls._svn_commit("initial commit") - - write_into_file_at_path("test_file", "test1test2") - # This used to be the last commit, but doing so broke - # GitTest.test_apply_git_patch which use the inverse diff of the last commit. - # svn-apply fails to remove directories in Git, see: - # https://bugs.webkit.org/show_bug.cgi?id=34871 - os.mkdir("test_dir") - # Slash should always be the right path separator since we use cygwin on Windows. - test_file3_path = "test_dir/test_file3" - write_into_file_at_path(test_file3_path, "third file") - cls._svn_add("test_dir") - cls._svn_commit("second commit") - - write_into_file_at_path("test_file", "test1test2test3\n") - write_into_file_at_path("test_file2", "second file") - cls._svn_add("test_file2") - cls._svn_commit("third commit") - - # 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. - run_command(['svn', 'update']) - remove_dir(svn_checkout_path) - - # This is a hot function since it's invoked by unittest before calling each test_ method in SVNTest and - # GitTest. We create a mock SVN repo once and then perform an SVN checkout from a filesystem copy of - # it since it's expensive to create the mock repo. - @classmethod - def setup(cls, test_object): - global cached_svn_repo_path - if not cached_svn_repo_path: - cached_svn_repo_path = cls._setup_mock_repo() - - test_object.temp_directory = tempfile.mkdtemp(suffix="svn_test") - test_object.svn_repo_path = os.path.join(test_object.temp_directory, "repo") - test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path - test_object.svn_checkout_path = os.path.join(test_object.temp_directory, "checkout") - shutil.copytree(cached_svn_repo_path, test_object.svn_repo_path) - run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url + "/trunk", test_object.svn_checkout_path]) - - @classmethod - def _setup_mock_repo(cls): - # Create an test SVN repository - svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo") - svn_repo_url = "file://%s" % svn_repo_path # Not sure this will work on windows - # git svn complains if we don't pass --pre-1.5-compatible, not sure why: - # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477 - run_command(['svnadmin', 'create', '--pre-1.5-compatible', svn_repo_path]) - - # Create a test svn checkout - svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout") - run_command(['svn', 'checkout', '--quiet', svn_repo_url, svn_checkout_path]) - - # Create and checkout a trunk dir to match the standard svn configuration to match git-svn's expectations - os.chdir(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. - remove_dir(svn_checkout_path) - - cls._setup_test_commits(svn_repo_url + "/trunk") - return svn_repo_path - - @classmethod - def tear_down(cls, test_object): - remove_dir(test_object.temp_directory) - - # Now that we've deleted the checkout paths, cwddir may be invalid - # Change back to a valid directory so that later calls to os.getcwd() do not fail. - if os.path.isabs(__file__): - path = os.path.dirname(__file__) - else: - path = sys.path[0] - os.chdir(detect_scm_system(path).checkout_root) - - -# For testing the SCM baseclass directly. -class SCMClassTests(unittest.TestCase): - def setUp(self): - self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet. - - def tearDown(self): - self.dev_null.close() - - def test_run_command_with_pipe(self): - input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) - self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n") - - # Test the non-pipe case too: - self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n") - - command_returns_non_zero = ['/bin/sh', '--invalid-option'] - # Test when the input pipe process fails. - input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null) - self.assertNotEqual(input_process.poll(), 0) - self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout) - - # Test when the run_command process fails. - input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) # grep shows usage and calls exit(2) when called w/o arguments. - self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout) - - def test_error_handlers(self): - git_failure_message="Merge conflict during commit: Your file or directory 'WebCore/ChangeLog' is probably out-of-date: resource out of date; try updating at /usr/local/libexec/git-core//git-svn line 469" - svn_failure_message="""svn: Commit failed (details follow): -svn: File or directory 'ChangeLog' is out of date; try updating -svn: resource out of date; try updating -""" - command_does_not_exist = ['does_not_exist', 'invalid_option'] - self.assertRaises(OSError, run_command, command_does_not_exist) - self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error) - - command_returns_non_zero = ['/bin/sh', '--invalid-option'] - self.assertRaises(ScriptError, run_command, command_returns_non_zero) - # Check if returns error text: - self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error)) - - self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message)) - self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message)) - self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah')) - - -# 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): - # FIXME: This code is brittle if the Attachment API changes. - attachment = Attachment({"bug_id": 12345}, None) - attachment.contents = lambda: patch_contents - - joe_cool = Committer("Joe Cool", "joe@cool.com") - attachment.reviewer = lambda: joe_cool - - return attachment - - def _setup_webkittools_scripts_symlink(self, local_scm): - webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__))) - webkit_scripts_directory = webkit_scm.scripts_directory() - local_scripts_directory = local_scm.scripts_directory() - os.mkdir(os.path.dirname(local_scripts_directory)) - os.symlink(webkit_scripts_directory, local_scripts_directory) - - # 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_changed_files(self): - write_into_file_at_path("test_file", "changed content") - self.assertItemsEqual(self.scm.changed_files(), ["test_file"]) - write_into_file_at_path("test_dir/test_file3", "new stuff") - self.assertItemsEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"]) - old_cwd = os.getcwd() - os.chdir("test_dir") - # Validate that changed_files does not change with our cwd, see bug 37015. - self.assertItemsEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"]) - os.chdir(old_cwd) - - def _shared_test_added_files(self): - write_into_file_at_path("test_file", "changed content") - self.assertItemsEqual(self.scm.added_files(), []) - - write_into_file_at_path("added_file", "new stuff") - self.scm.add("added_file") - - write_into_file_at_path("added_file3", "more new stuff") - write_into_file_at_path("added_file4", "more new stuff") - self.scm.add_list(["added_file3", "added_file4"]) - - os.mkdir("added_dir") - write_into_file_at_path("added_dir/added_file2", "new stuff") - self.scm.add("added_dir") - - # SVN reports directory changes, Git does not. - added_files = self.scm.added_files() - if "added_dir" in added_files: - added_files.remove("added_dir") - self.assertItemsEqual(added_files, ["added_dir/added_file2", "added_file", "added_file3", "added_file4"]) - - # Test also to make sure discard_working_directory_changes removes added files - self.scm.discard_working_directory_changes() - self.assertItemsEqual(self.scm.added_files(), []) - self.assertFalse(os.path.exists("added_file")) - self.assertFalse(os.path.exists("added_file3")) - self.assertFalse(os.path.exists("added_file4")) - self.assertFalse(os.path.exists("added_dir")) - - def _shared_test_changed_files_for_revision(self): - # SVN reports directory changes, Git does not. - changed_files = self.scm.changed_files_for_revision(3) - if "test_dir" in changed_files: - changed_files.remove("test_dir") - self.assertItemsEqual(changed_files, ["test_dir/test_file3", "test_file"]) - self.assertItemsEqual(self.scm.changed_files_for_revision(4), ["test_file", "test_file2"]) # Git and SVN return different orders. - self.assertItemsEqual(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", 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", 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). - self.assertRaises(ScriptError, self.scm.contents_at_revision, "test_file2", 2) - self.assertRaises(ScriptError, self.scm.contents_at_revision, "does_not_exist", 2) - - def _shared_test_revisions_changing_file(self): - self.assertItemsEqual(self.scm.revisions_changing_file("test_file"), [5, 4, 3, 2]) - self.assertRaises(ScriptError, self.scm.revisions_changing_file, "non_existent_file") - - def _shared_test_committer_email_for_revision(self): - 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('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(4) - self.assertRegexpMatches(r3_patch, 'test3') - self.assertNotRegexpMatches(r3_patch, 'test4') - self.assertRegexpMatches(r3_patch, 'test2') - self.assertRegexpMatches(self.scm.diff_for_revision(3), 'test2') - - def _shared_test_svn_apply_git_patch(self): - self._setup_webkittools_scripts_symlink(self.scm) - git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif -new file mode 100644 -index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90 -60151690 -GIT binary patch -literal 512 -zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c? -zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap -zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ -zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A -zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&) -zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b -zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB -z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X -z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4 -ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H - -literal 0 -HcmV?d00001 - -""" - self.checkout.apply_patch(self._create_patch(git_binary_addition)) - added = read_from_path('fizzbuzz7.gif', encoding=None) - self.assertEqual(512, len(added)) - self.assertTrue(added.startswith('GIF89a')) - self.assertIn('fizzbuzz7.gif', self.scm.changed_files()) - - # The file already exists. - self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_addition)) - - git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif -index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7 -GIT binary patch -literal 7 -OcmYex&reD$;sO8*F9L)B - -literal 512 -zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c? -zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap -zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ -zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A -zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&) -zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b -zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB -z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X -z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4 -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', encoding=None) - self.assertEqual('foobar\n', modified) - self.assertIn('fizzbuzz7.gif', self.scm.changed_files()) - - # Applying the same modification should fail. - self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_modification)) - - git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif -deleted file mode 100644 -index 323fae0..0000000 -GIT binary patch -literal 0 -HcmV?d00001 - -literal 7 -OcmYex&reD$;sO8*F9L)B - -""" - self.checkout.apply_patch(self._create_patch(git_binary_deletion)) - self.assertFalse(os.path.exists('fizzbuzz7.gif')) - self.assertNotIn('fizzbuzz7.gif', self.scm.changed_files()) - - # Cannot delete again. - self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_deletion)) - - def _shared_test_add_recursively(self): - os.mkdir("added_dir") - write_into_file_at_path("added_dir/added_file", "new stuff") - self.scm.add("added_dir/added_file") - self.assertIn("added_dir/added_file", self.scm.added_files()) - - def _shared_test_delete_recursively(self): - os.mkdir("added_dir") - write_into_file_at_path("added_dir/added_file", "new stuff") - self.scm.add("added_dir/added_file") - self.assertIn("added_dir/added_file", self.scm.added_files()) - self.scm.delete("added_dir/added_file") - self.assertNotIn("added_dir", self.scm.added_files()) - - def _shared_test_delete_recursively_or_not(self): - os.mkdir("added_dir") - write_into_file_at_path("added_dir/added_file", "new stuff") - write_into_file_at_path("added_dir/another_added_file", "more new stuff") - self.scm.add("added_dir/added_file") - self.scm.add("added_dir/another_added_file") - self.assertIn("added_dir/added_file", self.scm.added_files()) - self.assertIn("added_dir/another_added_file", self.scm.added_files()) - self.scm.delete("added_dir/added_file") - self.assertIn("added_dir/another_added_file", self.scm.added_files()) - - def _shared_test_exists(self, scm, commit_function): - os.chdir(scm.checkout_root) - self.assertFalse(scm.exists('foo.txt')) - write_into_file_at_path('foo.txt', 'some stuff') - self.assertFalse(scm.exists('foo.txt')) - scm.add('foo.txt') - commit_function('adding foo') - self.assertTrue(scm.exists('foo.txt')) - scm.delete('foo.txt') - commit_function('deleting foo') - self.assertFalse(scm.exists('foo.txt')) - - def _shared_test_head_svn_revision(self): - self.assertEqual(self.scm.head_svn_revision(), '5') - - -# Context manager that overrides the current timezone. -class TimezoneOverride(object): - def __init__(self, timezone_string): - self._timezone_string = timezone_string - - def __enter__(self): - if hasattr(time, 'tzset'): - self._saved_timezone = os.environ.get('TZ', None) - os.environ['TZ'] = self._timezone_string - time.tzset() - - def __exit__(self, type, value, traceback): - if hasattr(time, 'tzset'): - if self._saved_timezone: - os.environ['TZ'] = self._saved_timezone - else: - del os.environ['TZ'] - time.tzset() - - -class SVNTest(SCMTest): - - @staticmethod - def _set_date_and_reviewer(changelog_entry): - # Joe Cool matches the reviewer set in SCMTest._create_patch - changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool') - # svn-apply will update ChangeLog entries with today's date (as in Cupertino, CA, US) - with TimezoneOverride('PST8PDT'): - return changelog_entry.replace('DATE_HERE', date.today().isoformat()) - - def test_svn_apply(self): - first_entry = """2009-10-26 Eric Seidel <eric@webkit.org> - - Reviewed by Foo Bar. - - Most awesome change ever. - - * scm_unittest.py: -""" - intermediate_entry = """2009-10-27 Eric Seidel <eric@webkit.org> - - Reviewed by Baz Bar. - - A more awesomer change yet! - - * scm_unittest.py: -""" - one_line_overlap_patch = """Index: ChangeLog -=================================================================== ---- ChangeLog (revision 5) -+++ ChangeLog (working copy) -@@ -1,5 +1,13 @@ - 2009-10-26 Eric Seidel <eric@webkit.org> -%(whitespace)s -+ Reviewed by NOBODY (OOPS!). -+ -+ Second most awesome change ever. -+ -+ * scm_unittest.py: -+ -+2009-10-26 Eric Seidel <eric@webkit.org> -+ - Reviewed by Foo Bar. -%(whitespace)s - Most awesome change ever. -""" % {'whitespace': ' '} - one_line_overlap_entry = """DATE_HERE Eric Seidel <eric@webkit.org> - - Reviewed by REVIEWER_HERE. - - Second most awesome change ever. - - * scm_unittest.py: -""" - two_line_overlap_patch = """Index: ChangeLog -=================================================================== ---- ChangeLog (revision 5) -+++ ChangeLog (working copy) -@@ -2,6 +2,14 @@ -%(whitespace)s - Reviewed by Foo Bar. -%(whitespace)s -+ Second most awesome change ever. -+ -+ * scm_unittest.py: -+ -+2009-10-26 Eric Seidel <eric@webkit.org> -+ -+ Reviewed by Foo Bar. -+ - Most awesome change ever. -%(whitespace)s - * scm_unittest.py: -""" % {'whitespace': ' '} - two_line_overlap_entry = """DATE_HERE Eric Seidel <eric@webkit.org> - - Reviewed by Foo Bar. - - Second most awesome change ever. - - * scm_unittest.py: -""" - write_into_file_at_path('ChangeLog', first_entry) - run_command(['svn', 'add', 'ChangeLog']) - run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit']) - - # Patch files were created against just 'first_entry'. - # Add a second commit to make svn-apply have to apply the patches with fuzz. - changelog_contents = "%s\n%s" % (intermediate_entry, first_entry) - write_into_file_at_path('ChangeLog', changelog_contents) - run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit']) - - self._setup_webkittools_scripts_symlink(self.scm) - self.checkout.apply_patch(self._create_patch(one_line_overlap_patch)) - expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents) - self.assertEqual(read_from_path('ChangeLog'), expected_changelog_contents) - - self.scm.revert_files(['ChangeLog']) - self.checkout.apply_patch(self._create_patch(two_line_overlap_patch)) - expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents) - self.assertEqual(read_from_path('ChangeLog'), expected_changelog_contents) - - def setUp(self): - SVNTestRepository.setup(self) - os.chdir(self.svn_checkout_path) - self.scm = detect_scm_system(self.svn_checkout_path) - self.scm.svn_server_realm = None - # For historical reasons, we test some checkout code here too. - self.checkout = Checkout(self.scm) - - 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) - test_file_path = os.path.join(test_dir_path, 'test_file2') - write_into_file_at_path(test_file_path, 'test content') - run_command(['svn', 'add', 'test_dir2']) - - # create_patch depends on 'svn-create-patch', so make a dummy version. - scripts_path = os.path.join(self.svn_checkout_path, 'Tools', 'Scripts') - os.makedirs(scripts_path) - create_patch_path = os.path.join(scripts_path, 'svn-create-patch') - write_into_file_at_path(create_patch_path, '#!/bin/sh\necho $PWD') # We could pass -n to prevent the \n, but not all echo accept -n. - os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR) - - # Change into our test directory and run the create_patch command. - os.chdir(test_dir_path) - scm = detect_scm_system(test_dir_path) - self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right. - patch_contents = scm.create_patch() - # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo. - self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n. - - def test_detection(self): - self.assertEqual(self.scm.display_name(), "svn") - self.assertEqual(self.scm.supports_local_commits(), False) - - def test_apply_small_binary_patch(self): - patch_contents = """Index: test_file.swf -=================================================================== -Cannot display: file marked as a binary type. -svn:mime-type = application/octet-stream - -Property changes on: test_file.swf -___________________________________________________________________ -Name: svn:mime-type - + application/octet-stream - - -Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA== -""" - expected_contents = base64.b64decode("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", encoding=None) - self.assertEqual(actual_contents, expected_contents) - - def test_apply_svn_patch(self): - patch = self._create_patch(_svn_diff("-r5:4")) - self._setup_webkittools_scripts_symlink(self.scm) - Checkout(self.scm).apply_patch(patch) - - def test_commit_logs(self): - # Commits have dates and usernames in them, so we can't just direct compare. - self.assertRegexpMatches(self.scm.last_svn_commit_log(), 'fourth commit') - self.assertRegexpMatches(self.scm.svn_commit_log(3), 'second commit') - - 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') - - def test_commit_in_subdir(self, username=None): - write_into_file_at_path('test_dir/test_file3', 'more test content') - os.chdir("test_dir") - commit_text = self.scm.commit_with_message("another test commit", username) - os.chdir("..") - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - - def test_commit_text_parsing(self): - self._shared_test_commit_with_message() - - def test_commit_with_username(self): - self._shared_test_commit_with_message("dbates@webkit.org") - - def test_commit_without_authorization(self): - # FIXME: https://bugs.webkit.org/show_bug.cgi?id=111669 - # This test ends up looking in the actal $HOME/.subversion for authorization, - # which makes it fragile. For now, set it to use a realm that won't be authorized, - # but we should really plumb through a fake_home_dir here like we do in - # test_has_authorization_for_realm. - self.scm.svn_server_realm = '<http://svn.example.com:80> Example' - self.assertRaises(AuthenticationError, self._shared_test_commit_with_message) - - def test_has_authorization_for_realm_using_credentials_with_passtype(self): - credentials = """ -K 8 -passtype -V 8 -keychain -K 15 -svn:realmstring -V 39 -<http://svn.webkit.org:80> Mac OS Forge -K 8 -username -V 17 -dbates@webkit.org -END -""" - self.assertTrue(self._test_has_authorization_for_realm_using_credentials(SVN.svn_server_realm, credentials)) - - def test_has_authorization_for_realm_using_credentials_with_password(self): - credentials = """ -K 15 -svn:realmstring -V 39 -<http://svn.webkit.org:80> Mac OS Forge -K 8 -username -V 17 -dbates@webkit.org -K 8 -password -V 4 -blah -END -""" - self.assertTrue(self._test_has_authorization_for_realm_using_credentials(SVN.svn_server_realm, credentials)) - - def _test_has_authorization_for_realm_using_credentials(self, realm, credentials): - fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir") - svn_config_dir_path = os.path.join(fake_home_dir, ".subversion") - os.mkdir(svn_config_dir_path) - fake_webkit_auth_file = os.path.join(svn_config_dir_path, "fake_webkit_auth_file") - write_into_file_at_path(fake_webkit_auth_file, credentials) - result = self.scm.has_authorization_for_realm(realm, home_directory=fake_home_dir) - os.remove(fake_webkit_auth_file) - os.rmdir(svn_config_dir_path) - os.rmdir(fake_home_dir) - return result - - def test_not_have_authorization_for_realm_with_credentials_missing_password_and_passtype(self): - credentials = """ -K 15 -svn:realmstring -V 39 -<http://svn.webkit.org:80> Mac OS Forge -K 8 -username -V 17 -dbates@webkit.org -END -""" - self.assertFalse(self._test_has_authorization_for_realm_using_credentials(SVN.svn_server_realm, credentials)) - - def test_not_have_authorization_for_realm_when_missing_credentials_file(self): - fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir") - svn_config_dir_path = os.path.join(fake_home_dir, ".subversion") - os.mkdir(svn_config_dir_path) - self.assertFalse(self.scm.has_authorization_for_realm(SVN.svn_server_realm, home_directory=fake_home_dir)) - os.rmdir(svn_config_dir_path) - os.rmdir(fake_home_dir) - - def test_reverse_diff(self): - self._shared_test_reverse_diff() - - def test_diff_for_revision(self): - self._shared_test_diff_for_revision() - - def test_svn_apply_git_patch(self): - self._shared_test_svn_apply_git_patch() - - def test_changed_files(self): - self._shared_test_changed_files() - - def test_changed_files_for_revision(self): - self._shared_test_changed_files_for_revision() - - def test_added_files(self): - self._shared_test_added_files() - - def test_contents_at_revision(self): - self._shared_test_contents_at_revision() - - def test_revisions_changing_file(self): - self._shared_test_revisions_changing_file() - - def test_committer_email_for_revision(self): - self._shared_test_committer_email_for_revision() - - def test_add_recursively(self): - self._shared_test_add_recursively() - - def test_delete(self): - os.chdir(self.svn_checkout_path) - self.scm.delete("test_file") - self.assertIn("test_file", self.scm.deleted_files()) - - def test_delete_list(self): - os.chdir(self.svn_checkout_path) - self.scm.delete_list(["test_file", "test_file2"]) - self.assertIn("test_file", self.scm.deleted_files()) - self.assertIn("test_file2", self.scm.deleted_files()) - - def test_delete_recursively(self): - self._shared_test_delete_recursively() - - def test_delete_recursively_or_not(self): - self._shared_test_delete_recursively_or_not() - - def test_head_svn_revision(self): - self._shared_test_head_svn_revision() - - def test_propset_propget(self): - filepath = os.path.join(self.svn_checkout_path, "test_file") - expected_mime_type = "x-application/foo-bar" - self.scm.propset("svn:mime-type", expected_mime_type, filepath) - self.assertEqual(expected_mime_type, self.scm.propget("svn:mime-type", filepath)) - - def test_show_head(self): - write_into_file_at_path("test_file", u"Hello!", "utf-8") - SVNTestRepository._svn_commit("fourth commit") - self.assertEqual("Hello!", self.scm.show_head('test_file')) - - def test_show_head_binary(self): - data = "\244" - write_into_file_at_path("binary_file", data, encoding=None) - self.scm.add("binary_file") - self.scm.commit_with_message("a test commit") - self.assertEqual(data, self.scm.show_head('binary_file')) - - def do_test_diff_for_file(self): - write_into_file_at_path('test_file', 'some content') - self.scm.commit_with_message("a test commit") - diff = self.scm.diff_for_file('test_file') - self.assertEqual(diff, "") - - write_into_file_at_path("test_file", "changed content") - diff = self.scm.diff_for_file('test_file') - self.assertIn("-some content", diff) - self.assertIn("+changed content", diff) - - def clean_bogus_dir(self): - self.bogus_dir = self.scm._bogus_dir_name() - if os.path.exists(self.bogus_dir): - shutil.rmtree(self.bogus_dir) - - def test_diff_for_file_with_existing_bogus_dir(self): - self.clean_bogus_dir() - os.mkdir(self.bogus_dir) - self.do_test_diff_for_file() - self.assertTrue(os.path.exists(self.bogus_dir)) - shutil.rmtree(self.bogus_dir) - - def test_diff_for_file_with_missing_bogus_dir(self): - self.clean_bogus_dir() - self.do_test_diff_for_file() - self.assertFalse(os.path.exists(self.bogus_dir)) - - def test_svn_lock(self): - if self.scm.svn_version() >= "1.7": - # the following technique with .svn/lock then svn update doesn't work with subversion client 1.7 or later - pass - else: - svn_root_lock_path = ".svn/lock" - write_into_file_at_path(svn_root_lock_path, "", "utf-8") - # webkit-patch uses a Checkout object and runs update-webkit, just use svn update here. - self.assertRaises(ScriptError, run_command, ['svn', 'update']) - self.scm.discard_working_directory_changes() - self.assertFalse(os.path.exists(svn_root_lock_path)) - run_command(['svn', 'update']) # Should succeed and not raise. - - def test_exists(self): - self._shared_test_exists(self.scm, self.scm.commit_with_message) - -class GitTest(SCMTest): - - def setUp(self): - """Sets up fresh git repository with one commit. Then setups a second git - repo that tracks the first one.""" - # FIXME: We should instead clone a git repo that is tracking an SVN repo. - # That better matches what we do with WebKit. - self.original_dir = os.getcwd() - - self.untracking_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout2") - run_command(['git', 'init', self.untracking_checkout_path]) - - os.chdir(self.untracking_checkout_path) - write_into_file_at_path('foo_file', 'foo') - run_command(['git', 'add', 'foo_file']) - run_command(['git', 'commit', '-am', 'dummy commit']) - self.untracking_scm = detect_scm_system(self.untracking_checkout_path) - - self.tracking_git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout") - run_command(['git', 'clone', '--quiet', self.untracking_checkout_path, self.tracking_git_checkout_path]) - os.chdir(self.tracking_git_checkout_path) - self.tracking_scm = detect_scm_system(self.tracking_git_checkout_path) - - def tearDown(self): - # Change back to a valid directory so that later calls to os.getcwd() do not fail. - os.chdir(self.original_dir) - run_command(['rm', '-rf', self.tracking_git_checkout_path]) - run_command(['rm', '-rf', self.untracking_checkout_path]) - - def test_remote_branch_ref(self): - self.assertEqual(self.tracking_scm.remote_branch_ref(), 'refs/remotes/origin/master') - - os.chdir(self.untracking_checkout_path) - self.assertRaises(ScriptError, self.untracking_scm.remote_branch_ref) - - def test_multiple_remotes(self): - run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote1']) - run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote2']) - self.assertEqual(self.tracking_scm.remote_branch_ref(), 'remote1') - - def test_create_patch(self): - write_into_file_at_path('test_file_commit1', 'contents') - run_command(['git', 'add', 'test_file_commit1']) - scm = self.tracking_scm - scm.commit_locally_with_message('message') - - patch = scm.create_patch() - self.assertNotRegexpMatches(patch, r'Subversion Revision:') - - def test_orderfile(self): - os.mkdir("Tools") - os.mkdir("Source") - os.mkdir("LayoutTests") - os.mkdir("Websites") - - # Slash should always be the right path separator since we use cygwin on Windows. - Tools_ChangeLog = "Tools/ChangeLog" - write_into_file_at_path(Tools_ChangeLog, "contents") - Source_ChangeLog = "Source/ChangeLog" - write_into_file_at_path(Source_ChangeLog, "contents") - LayoutTests_ChangeLog = "LayoutTests/ChangeLog" - write_into_file_at_path(LayoutTests_ChangeLog, "contents") - Websites_ChangeLog = "Websites/ChangeLog" - write_into_file_at_path(Websites_ChangeLog, "contents") - - Tools_ChangeFile = "Tools/ChangeFile" - write_into_file_at_path(Tools_ChangeFile, "contents") - Source_ChangeFile = "Source/ChangeFile" - write_into_file_at_path(Source_ChangeFile, "contents") - LayoutTests_ChangeFile = "LayoutTests/ChangeFile" - write_into_file_at_path(LayoutTests_ChangeFile, "contents") - Websites_ChangeFile = "Websites/ChangeFile" - write_into_file_at_path(Websites_ChangeFile, "contents") - - run_command(['git', 'add', 'Tools/ChangeLog']) - run_command(['git', 'add', 'LayoutTests/ChangeLog']) - run_command(['git', 'add', 'Source/ChangeLog']) - run_command(['git', 'add', 'Websites/ChangeLog']) - run_command(['git', 'add', 'Tools/ChangeFile']) - run_command(['git', 'add', 'LayoutTests/ChangeFile']) - run_command(['git', 'add', 'Source/ChangeFile']) - run_command(['git', 'add', 'Websites/ChangeFile']) - scm = self.tracking_scm - scm.commit_locally_with_message('message') - - patch = scm.create_patch() - self.assertTrue(re.search(r'Tools/ChangeLog', patch).start() < re.search(r'Tools/ChangeFile', patch).start()) - self.assertTrue(re.search(r'Websites/ChangeLog', patch).start() < re.search(r'Websites/ChangeFile', patch).start()) - self.assertTrue(re.search(r'Source/ChangeLog', patch).start() < re.search(r'Source/ChangeFile', patch).start()) - self.assertTrue(re.search(r'LayoutTests/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start()) - - self.assertTrue(re.search(r'Source/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start()) - self.assertTrue(re.search(r'Tools/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start()) - self.assertTrue(re.search(r'Websites/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start()) - - self.assertTrue(re.search(r'Source/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start()) - self.assertTrue(re.search(r'Tools/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start()) - self.assertTrue(re.search(r'Websites/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start()) - - self.assertTrue(re.search(r'Source/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start()) - self.assertTrue(re.search(r'Tools/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start()) - self.assertTrue(re.search(r'Websites/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start()) - - def test_exists(self): - scm = self.untracking_scm - self._shared_test_exists(scm, scm.commit_locally_with_message) - - def test_head_svn_revision(self): - scm = detect_scm_system(self.untracking_checkout_path) - # If we cloned a git repo tracking an SVN repo, this would give the same result as - # self._shared_test_head_svn_revision(). - self.assertEqual(scm.head_svn_revision(), '') - - def test_rename_files(self): - scm = self.tracking_scm - - run_command(['git', 'mv', 'foo_file', 'bar_file']) - scm.commit_locally_with_message('message') - - patch = scm.create_patch() - self.assertNotRegexpMatches(patch, r'rename from ') - self.assertNotRegexpMatches(patch, r'rename to ') - - -class GitSVNTest(SCMTest): - - def _setup_git_checkout(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', 'clone', '-T', 'trunk', self.svn_repo_url, self.git_checkout_path]) - os.chdir(self.git_checkout_path) - - def _tear_down_git_checkout(self): - # Change back to a valid directory so that later calls to os.getcwd() do not fail. - os.chdir(self.original_dir) - run_command(['rm', '-rf', self.git_checkout_path]) - - def setUp(self): - self.original_dir = os.getcwd() - - SVNTestRepository.setup(self) - self._setup_git_checkout() - self.scm = detect_scm_system(self.git_checkout_path) - self.scm.svn_server_realm = None - # For historical reasons, we test some checkout code here too. - self.checkout = Checkout(self.scm) - - def tearDown(self): - SVNTestRepository.tear_down(self) - self._tear_down_git_checkout() - - def test_detection(self): - self.assertEqual(self.scm.display_name(), "git") - self.assertEqual(self.scm.supports_local_commits(), True) - - def test_read_git_config(self): - key = 'test.git-config' - value = 'git-config value' - run_command(['git', 'config', key, value]) - self.assertEqual(self.scm.read_git_config(key), value) - - def test_local_commits(self): - test_file = os.path.join(self.git_checkout_path, 'test_file') - write_into_file_at_path(test_file, 'foo') - run_command(['git', 'commit', '-a', '-m', 'local commit']) - - self.assertEqual(len(self.scm.local_commits()), 1) - - def test_discard_local_commits(self): - test_file = os.path.join(self.git_checkout_path, 'test_file') - write_into_file_at_path(test_file, 'foo') - run_command(['git', 'commit', '-a', '-m', 'local commit']) - - self.assertEqual(len(self.scm.local_commits()), 1) - self.scm.discard_local_commits() - self.assertEqual(len(self.scm.local_commits()), 0) - - def test_delete_branch(self): - new_branch = 'foo' - - run_command(['git', 'checkout', '-b', new_branch]) - self.assertEqual(run_command(['git', 'symbolic-ref', 'HEAD']).strip(), 'refs/heads/' + new_branch) - - run_command(['git', 'checkout', '-b', 'bar']) - self.scm.delete_branch(new_branch) - - self.assertNotRegexpMatches(run_command(['git', 'branch']), r'foo') - - def test_remote_merge_base(self): - # Diff to merge-base should include working-copy changes, - # which the diff to svn_branch.. doesn't. - test_file = os.path.join(self.git_checkout_path, 'test_file') - write_into_file_at_path(test_file, 'foo') - - diff_to_common_base = _git_diff(self.scm.remote_branch_ref() + '..') - diff_to_merge_base = _git_diff(self.scm.remote_merge_base()) - - self.assertNotRegexpMatches(diff_to_common_base, r'foo') - self.assertRegexpMatches(diff_to_merge_base, r'foo') - - def test_rebase_in_progress(self): - svn_test_file = os.path.join(self.svn_checkout_path, 'test_file') - write_into_file_at_path(svn_test_file, "svn_checkout") - run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path) - - git_test_file = os.path.join(self.git_checkout_path, 'test_file') - write_into_file_at_path(git_test_file, "git_checkout") - run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort']) - - # --quiet doesn't make git svn silent, so use run_silent to redirect output - self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase. - - self.assertTrue(self.scm.rebase_in_progress()) - - # Make sure our cleanup works. - self.scm.discard_working_directory_changes() - self.assertFalse(self.scm.rebase_in_progress()) - - # Make sure cleanup doesn't throw when no rebase is in progress. - self.scm.discard_working_directory_changes() - - def test_commitish_parsing(self): - # Multiple revisions are cherry-picked. - self.assertEqual(len(self.scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1) - self.assertEqual(len(self.scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2) - - # ... is an invalid range specifier - self.assertRaises(ScriptError, self.scm.commit_ids_from_commitish_arguments, ['trunk...HEAD']) - - def test_commitish_order(self): - commit_range = 'HEAD~3..HEAD' - - actual_commits = self.scm.commit_ids_from_commitish_arguments([commit_range]) - expected_commits = [] - expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines()) - - self.assertEqual(actual_commits, expected_commits) - - def test_apply_git_patch(self): - # 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(_git_diff('HEAD..HEAD^')) - self._setup_webkittools_scripts_symlink(self.scm) - Checkout(self.scm).apply_patch(patch) - - def test_commit_text_parsing(self): - write_into_file_at_path('test_file', 'more test content') - commit_text = self.scm.commit_with_message("another test commit") - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - - 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']) - commit_text = self.scm.commit_with_message("yet another test commit") - - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertRegexpMatches(svn_log, r'test_file_commit1') - - def _local_commit(self, filename, contents, message): - write_into_file_at_path(filename, contents) - run_command(['git', 'add', filename]) - self.scm.commit_locally_with_message(message) - - def _one_local_commit(self): - self._local_commit('test_file_commit1', 'more test content', 'another test commit') - - def _one_local_commit_plus_working_copy_changes(self): - self._one_local_commit() - write_into_file_at_path('test_file_commit2', 'still more test content') - run_command(['git', 'add', 'test_file_commit2']) - - def _second_local_commit(self): - self._local_commit('test_file_commit2', 'still more test content', 'yet another test commit') - - def _two_local_commits(self): - self._one_local_commit() - self._second_local_commit() - - def _three_local_commits(self): - self._local_commit('test_file_commit0', 'more test content', 'another test commit') - self._two_local_commits() - - def test_revisions_changing_files_with_local_commit(self): - self._one_local_commit() - self.assertItemsEqual(self.scm.revisions_changing_file('test_file_commit1'), []) - - def test_commit_with_message(self): - self._one_local_commit_plus_working_copy_changes() - self.assertRaises(AmbiguousCommitError, self.scm.commit_with_message, "yet another test commit") - commit_text = self.scm.commit_with_message("yet another test commit", force_squash=True) - - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertRegexpMatches(svn_log, r'test_file_commit2') - self.assertRegexpMatches(svn_log, r'test_file_commit1') - - def test_commit_with_message_git_commit(self): - self._two_local_commits() - - commit_text = self.scm.commit_with_message("another test commit", git_commit="HEAD^") - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertRegexpMatches(svn_log, r'test_file_commit1') - self.assertNotRegexpMatches(svn_log, r'test_file_commit2') - - def test_commit_with_message_git_commit_range(self): - self._three_local_commits() - - commit_text = self.scm.commit_with_message("another test commit", git_commit="HEAD~2..HEAD") - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertNotRegexpMatches(svn_log, r'test_file_commit0') - self.assertRegexpMatches(svn_log, r'test_file_commit1') - self.assertRegexpMatches(svn_log, r'test_file_commit2') - - def test_commit_with_message_only_local_commit(self): - self._one_local_commit() - commit_text = self.scm.commit_with_message("another test commit") - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertRegexpMatches(svn_log, r'test_file_commit1') - - 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') - - self.assertRaises(AmbiguousCommitError, self.scm.commit_with_message, "another test commit") - commit_text = self.scm.commit_with_message("another test commit", force_squash=True) - - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertRegexpMatches(svn_log, r'test_file_commit2') - self.assertRegexpMatches(svn_log, r'test_file_commit1') - - 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') - self.assertRaises(ScriptError, self.scm.commit_with_message, "another test commit", git_commit="HEAD^") - - def test_commit_with_message_multiple_local_commits_always_squash(self): - run_command(['git', 'config', 'webkit-patch.commit-should-always-squash', 'true']) - self._two_local_commits() - commit_text = self.scm.commit_with_message("yet another test commit") - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertRegexpMatches(svn_log, r'test_file_commit2') - self.assertRegexpMatches(svn_log, r'test_file_commit1') - - def test_commit_with_message_multiple_local_commits(self): - self._two_local_commits() - self.assertRaises(AmbiguousCommitError, self.scm.commit_with_message, "yet another test commit") - commit_text = self.scm.commit_with_message("yet another test commit", force_squash=True) - - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertRegexpMatches(svn_log, r'test_file_commit2') - self.assertRegexpMatches(svn_log, r'test_file_commit1') - - def test_commit_with_message_not_synced(self): - run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3']) - self._two_local_commits() - self.assertRaises(AmbiguousCommitError, self.scm.commit_with_message, "another test commit") - commit_text = self.scm.commit_with_message("another test commit", force_squash=True) - - self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6') - - svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose']) - self.assertNotRegexpMatches(svn_log, r'test_file2') - self.assertRegexpMatches(svn_log, r'test_file_commit2') - self.assertRegexpMatches(svn_log, r'test_file_commit1') - - def test_commit_with_message_not_synced_with_conflict(self): - run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3']) - self._local_commit('test_file2', 'asdf', 'asdf commit') - - # There's a conflict between trunk and the test_file2 modification. - self.assertRaises(ScriptError, self.scm.commit_with_message, "another test commit", force_squash=True) - - def test_upstream_branch(self): - run_command(['git', 'checkout', '-t', '-b', 'my-branch']) - run_command(['git', 'checkout', '-t', '-b', 'my-second-branch']) - self.assertEqual(self.scm._upstream_branch(), 'my-branch') - - def test_remote_branch_ref(self): - self.assertEqual(self.scm.remote_branch_ref(), 'refs/remotes/trunk') - - def test_reverse_diff(self): - self._shared_test_reverse_diff() - - def test_diff_for_revision(self): - self._shared_test_diff_for_revision() - - 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() - patch = self.scm.create_patch() - self.assertRegexpMatches(patch, r'test_file_commit1') - self.assertRegexpMatches(patch, r'test_file_commit2') - - def test_create_patch(self): - self._one_local_commit_plus_working_copy_changes() - patch = self.scm.create_patch() - self.assertRegexpMatches(patch, r'test_file_commit2') - self.assertRegexpMatches(patch, r'test_file_commit1') - self.assertRegexpMatches(patch, r'Subversion Revision: 5') - - def test_create_patch_after_merge(self): - run_command(['git', 'checkout', '-b', 'dummy-branch', 'trunk~3']) - self._one_local_commit() - run_command(['git', 'merge', 'trunk']) - - patch = self.scm.create_patch() - self.assertRegexpMatches(patch, r'test_file_commit1') - self.assertRegexpMatches(patch, r'Subversion Revision: 5') - - def test_create_patch_with_changed_files(self): - self._one_local_commit_plus_working_copy_changes() - patch = self.scm.create_patch(changed_files=['test_file_commit2']) - self.assertRegexpMatches(patch, r'test_file_commit2') - - def test_create_patch_with_rm_and_changed_files(self): - self._one_local_commit_plus_working_copy_changes() - os.remove('test_file_commit1') - patch = self.scm.create_patch() - patch_with_changed_files = self.scm.create_patch(changed_files=['test_file_commit1', 'test_file_commit2']) - self.assertEqual(patch, patch_with_changed_files) - - def test_create_patch_git_commit(self): - self._two_local_commits() - patch = self.scm.create_patch(git_commit="HEAD^") - self.assertRegexpMatches(patch, r'test_file_commit1') - self.assertNotRegexpMatches(patch, r'test_file_commit2') - - def test_create_patch_git_commit_range(self): - self._three_local_commits() - patch = self.scm.create_patch(git_commit="HEAD~2..HEAD") - self.assertNotRegexpMatches(patch, r'test_file_commit0') - self.assertRegexpMatches(patch, r'test_file_commit2') - self.assertRegexpMatches(patch, r'test_file_commit1') - - def test_create_patch_working_copy_only(self): - self._one_local_commit_plus_working_copy_changes() - patch = self.scm.create_patch(git_commit="HEAD....") - self.assertNotRegexpMatches(patch, r'test_file_commit1') - self.assertRegexpMatches(patch, r'test_file_commit2') - - def test_create_patch_multiple_local_commits(self): - self._two_local_commits() - patch = self.scm.create_patch() - self.assertRegexpMatches(patch, r'test_file_commit2') - self.assertRegexpMatches(patch, r'test_file_commit1') - - def test_create_patch_not_synced(self): - run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3']) - self._two_local_commits() - patch = self.scm.create_patch() - self.assertNotRegexpMatches(patch, r'test_file2') - self.assertRegexpMatches(patch, r'test_file_commit2') - self.assertRegexpMatches(patch, r'test_file_commit1') - - def test_create_binary_patch(self): - # Create a git binary patch and check the contents. - 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, encoding=None) - run_command(['git', 'add', test_file_name]) - patch = self.scm.create_patch() - self.assertRegexpMatches(patch, r'\nliteral 0\n') - self.assertRegexpMatches(patch, r'\nliteral 256\n') - - # Check if we can apply the created patch. - run_command(['git', 'rm', '-f', test_file_name]) - self._setup_webkittools_scripts_symlink(self.scm) - self.checkout.apply_patch(self._create_patch(patch)) - 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, encoding=None) - run_command(['git', 'add', test_file_name]) - run_command(['git', 'commit', '-m', 'binary diff']) - - patch_from_local_commit = self.scm.create_patch('HEAD') - self.assertRegexpMatches(patch_from_local_commit, r'\nliteral 0\n') - self.assertRegexpMatches(patch_from_local_commit, r'\nliteral 256\n') - - def test_changed_files_local_plus_working_copy(self): - self._one_local_commit_plus_working_copy_changes() - files = self.scm.changed_files() - self.assertIn('test_file_commit1', files) - self.assertIn('test_file_commit2', files) - - # working copy should *not* be in the list. - files = self.scm.changed_files('trunk..') - self.assertIn('test_file_commit1', files) - self.assertNotIn('test_file_commit2', files) - - # working copy *should* be in the list. - files = self.scm.changed_files('trunk....') - self.assertIn('test_file_commit1', files) - self.assertIn('test_file_commit2', files) - - def test_changed_files_git_commit(self): - self._two_local_commits() - files = self.scm.changed_files(git_commit="HEAD^") - self.assertIn('test_file_commit1', files) - self.assertNotIn('test_file_commit2', files) - - def test_changed_files_git_commit_range(self): - self._three_local_commits() - files = self.scm.changed_files(git_commit="HEAD~2..HEAD") - self.assertNotIn('test_file_commit0', files) - self.assertIn('test_file_commit1', files) - self.assertIn('test_file_commit2', files) - - def test_changed_files_working_copy_only(self): - self._one_local_commit_plus_working_copy_changes() - files = self.scm.changed_files(git_commit="HEAD....") - self.assertNotIn('test_file_commit1', files) - self.assertIn('test_file_commit2', files) - - def test_changed_files_multiple_local_commits(self): - self._two_local_commits() - files = self.scm.changed_files() - self.assertIn('test_file_commit2', files) - self.assertIn('test_file_commit1', files) - - def test_changed_files_not_synced(self): - run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3']) - self._two_local_commits() - files = self.scm.changed_files() - self.assertNotIn('test_file2', files) - self.assertIn('test_file_commit2', files) - self.assertIn('test_file_commit1', files) - - def test_changed_files_not_synced(self): - run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3']) - self._two_local_commits() - files = self.scm.changed_files() - self.assertNotIn('test_file2', files) - self.assertIn('test_file_commit2', files) - self.assertIn('test_file_commit1', files) - - def test_changed_files(self): - self._shared_test_changed_files() - - def test_changed_files_for_revision(self): - self._shared_test_changed_files_for_revision() - - def test_changed_files_upstream(self): - run_command(['git', 'checkout', '-t', '-b', 'my-branch']) - self._one_local_commit() - run_command(['git', 'checkout', '-t', '-b', 'my-second-branch']) - self._second_local_commit() - write_into_file_at_path('test_file_commit0', 'more test content') - run_command(['git', 'add', 'test_file_commit0']) - - # equivalent to 'git diff my-branch..HEAD, should not include working changes - files = self.scm.changed_files(git_commit='UPSTREAM..') - self.assertNotIn('test_file_commit1', files) - self.assertIn('test_file_commit2', files) - self.assertNotIn('test_file_commit0', files) - - # equivalent to 'git diff my-branch', *should* include working changes - files = self.scm.changed_files(git_commit='UPSTREAM....') - self.assertNotIn('test_file_commit1', files) - self.assertIn('test_file_commit2', files) - self.assertIn('test_file_commit0', files) - - def test_contents_at_revision(self): - self._shared_test_contents_at_revision() - - def test_revisions_changing_file(self): - self._shared_test_revisions_changing_file() - - def test_added_files(self): - self._shared_test_added_files() - - def test_committer_email_for_revision(self): - self._shared_test_committer_email_for_revision() - - def test_add_recursively(self): - self._shared_test_add_recursively() - - def test_delete(self): - self._two_local_commits() - self.scm.delete('test_file_commit1') - self.assertIn("test_file_commit1", self.scm.deleted_files()) - - def test_delete_list(self): - self._two_local_commits() - self.scm.delete_list(["test_file_commit1", "test_file_commit2"]) - self.assertIn("test_file_commit1", self.scm.deleted_files()) - self.assertIn("test_file_commit2", self.scm.deleted_files()) - - def test_delete_recursively(self): - self._shared_test_delete_recursively() - - def test_delete_recursively_or_not(self): - self._shared_test_delete_recursively_or_not() - - def test_head_svn_revision(self): - self._shared_test_head_svn_revision() - - def test_to_object_name(self): - relpath = 'test_file_commit1' - fullpath = os.path.realpath(os.path.join(self.git_checkout_path, relpath)) - self.assertEqual(relpath, self.scm.to_object_name(fullpath)) - - def test_show_head(self): - self._two_local_commits() - self.assertEqual("more test content", self.scm.show_head('test_file_commit1')) - - def test_show_head_binary(self): - self._two_local_commits() - data = "\244" - write_into_file_at_path("binary_file", data, encoding=None) - self.scm.add("binary_file") - self.scm.commit_locally_with_message("a test commit") - self.assertEqual(data, self.scm.show_head('binary_file')) - - def test_diff_for_file(self): - self._two_local_commits() - write_into_file_at_path('test_file_commit1', "Updated", encoding=None) - - diff = self.scm.diff_for_file('test_file_commit1') - cached_diff = self.scm.diff_for_file('test_file_commit1') - self.assertIn("+Updated", diff) - self.assertIn("-more test content", diff) - - self.scm.add('test_file_commit1') - - cached_diff = self.scm.diff_for_file('test_file_commit1') - self.assertIn("+Updated", cached_diff) - self.assertIn("-more test content", cached_diff) - - def test_exists(self): - self._shared_test_exists(self.scm, self.scm.commit_locally_with_message) - - -# We need to split off more of these SCM tests to use mocks instead of the filesystem. -# This class is the first part of that. -class GitTestWithMock(unittest.TestCase): - maxDiff = None - - def make_scm(self, logging_executive=False): - # We do this should_log dance to avoid logging when Git.__init__ runs sysctl on mac to check for 64-bit support. - scm = Git(cwd=".", executive=MockExecutive(), filesystem=MockFileSystem()) - scm.read_git_config = lambda *args, **kw: "MOCKKEY:MOCKVALUE" - scm._executive._should_log = logging_executive - return scm - - def test_create_patch(self): - scm = self.make_scm(logging_executive=True) - expected_stderr = """\ -MOCK run_command: ['git', 'merge-base', 'MOCKVALUE', 'HEAD'], cwd=%(checkout)s -MOCK run_command: ['git', 'diff', '--binary', '--no-color', '--no-ext-diff', '--full-index', '--no-renames', '', 'MOCK output of child process', '--'], cwd=%(checkout)s -MOCK run_command: ['git', 'rev-parse', '--show-toplevel'], cwd=%(checkout)s -MOCK run_command: ['git', 'log', '-1', '--grep=git-svn-id:', '--date=iso', './MOCK output of child process/MOCK output of child process'], cwd=%(checkout)s -""" % {'checkout': scm.checkout_root} - OutputCapture().assert_outputs(self, scm.create_patch, expected_logs=expected_stderr) - - def test_push_local_commits_to_server_with_username_and_password(self): - self.assertEqual(self.make_scm().push_local_commits_to_server(username='dbates@webkit.org', password='blah'), "MOCK output of child process") - - def test_push_local_commits_to_server_without_username_and_password(self): - self.assertRaises(AuthenticationError, self.make_scm().push_local_commits_to_server) - - def test_push_local_commits_to_server_with_username_and_without_password(self): - self.assertRaises(AuthenticationError, self.make_scm().push_local_commits_to_server, {'username': 'dbates@webkit.org'}) - - def test_push_local_commits_to_server_without_username_and_with_password(self): - self.assertRaises(AuthenticationError, self.make_scm().push_local_commits_to_server, {'password': 'blah'}) - - def test_timestamp_of_revision(self): - scm = self.make_scm() - scm.find_checkout_root = lambda path: '' - scm._run_git = lambda args: 'Date: 2013-02-08 08:05:49 +0000' - self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-08T08:05:49Z') - - scm._run_git = lambda args: 'Date: 2013-02-08 01:02:03 +0130' - self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-07T23:32:03Z') - - scm._run_git = lambda args: 'Date: 2013-02-08 01:55:21 -0800' - self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-08T09:55:21Z') diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/svn.py b/Tools/Scripts/webkitpy/common/checkout/scm/svn.py deleted file mode 100644 index c146f5d32..000000000 --- a/Tools/Scripts/webkitpy/common/checkout/scm/svn.py +++ /dev/null @@ -1,380 +0,0 @@ -# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved. -# Copyright (c) 2009 Apple 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 logging -import os -import random -import re -import shutil -import string -import sys -import tempfile - -from webkitpy.common.memoized import memoized -from webkitpy.common.system.executive import Executive, ScriptError - -from .scm import AuthenticationError, SCM, commit_error_handler - -_log = logging.getLogger(__name__) - - -# A mixin class that represents common functionality for SVN and Git-SVN. -class SVNRepository(object): - # FIXME: These belong in common.config.urls - svn_server_host = "svn.webkit.org" - svn_server_realm = "<http://svn.webkit.org:80> Mac OS Forge" - - def has_authorization_for_realm(self, realm, home_directory=os.getenv("HOME")): - # If we are working on a file:// repository realm will be None - if realm is None: - return True - # ignore false positives for methods implemented in the mixee class. pylint: disable=E1101 - # Assumes find and grep are installed. - if not os.path.isdir(os.path.join(home_directory, ".subversion")): - return False - find_args = ["find", ".subversion", "-type", "f", "-exec", "grep", "-q", realm, "{}", ";", "-print"] - find_output = self.run(find_args, cwd=home_directory, error_handler=Executive.ignore_error).rstrip() - if not find_output or not os.path.isfile(os.path.join(home_directory, find_output)): - return False - # Subversion either stores the password in the credential file, indicated by the presence of the key "password", - # or uses the system password store (e.g. Keychain on Mac OS X) as indicated by the presence of the key "passtype". - # We assume that these keys will not coincide with the actual credential data (e.g. that a person's username - # isn't "password") so that we can use grep. - if self.run(["grep", "password", find_output], cwd=home_directory, return_exit_code=True) == 0: - return True - return self.run(["grep", "passtype", find_output], cwd=home_directory, return_exit_code=True) == 0 - - -class SVN(SCM, SVNRepository): - - executable_name = "svn" - - _svn_metadata_files = frozenset(['.svn', '_svn']) - - def __init__(self, cwd, patch_directories, **kwargs): - SCM.__init__(self, cwd, **kwargs) - self._bogus_dir = None - if patch_directories == []: - raise Exception(message='Empty list of patch directories passed to SCM.__init__') - elif patch_directories == None: - self._patch_directories = [self._filesystem.relpath(cwd, self.checkout_root)] - else: - self._patch_directories = patch_directories - - @classmethod - def in_working_directory(cls, path, executive=None): - if os.path.isdir(os.path.join(path, '.svn')): - # This is a fast shortcut for svn info that is usually correct for SVN < 1.7, - # but doesn't work for SVN >= 1.7. - return True - - executive = executive or Executive() - svn_info_args = [cls.executable_name, 'info'] - exit_code = executive.run_command(svn_info_args, cwd=path, return_exit_code=True) - return (exit_code == 0) - - def find_uuid(self, path): - if not self.in_working_directory(path): - return None - return self.value_from_svn_info(path, 'Repository UUID') - - @classmethod - def value_from_svn_info(cls, path, field_name): - svn_info_args = [cls.executable_name, 'info'] - # FIXME: This method should use a passed in executive or be made an instance method and use self._executive. - info_output = Executive().run_command(svn_info_args, cwd=path).rstrip() - match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE) - if not match: - raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name) - return match.group('value').rstrip('\r') - - def find_checkout_root(self, path): - uuid = self.find_uuid(path) - # If |path| is not in a working directory, we're supposed to return |path|. - if not uuid: - return path - # Search up the directory hierarchy until we find a different UUID. - last_path = None - while True: - if uuid != self.find_uuid(path): - return last_path - last_path = path - (path, last_component) = self._filesystem.split(path) - if last_path == path: - return None - - @staticmethod - def commit_success_regexp(): - return "^Committed revision (?P<svn_revision>\d+)\.$" - - def _run_svn(self, args, **kwargs): - return self.run([self.executable_name] + args, **kwargs) - - @memoized - def svn_version(self): - return self._run_svn(['--version', '--quiet']) - - def has_working_directory_changes(self): - # FIXME: What about files which are not committed yet? - return self._run_svn(["diff"], cwd=self.checkout_root, decode_output=False) != "" - - def discard_working_directory_changes(self): - # Make sure there are no locks lying around from a previously aborted svn invocation. - # This is slightly dangerous, as it's possible the user is running another svn process - # on this checkout at the same time. However, it's much more likely that we're running - # under windows and svn just sucks (or the user interrupted svn and it failed to clean up). - self._run_svn(["cleanup"], cwd=self.checkout_root) - - # svn revert -R is not as awesome as git reset --hard. - # It will leave added files around, causing later svn update - # calls to fail on the bots. We make this mirror git reset --hard - # by deleting any added files as well. - added_files = reversed(sorted(self.added_files())) - # added_files() returns directories for SVN, we walk the files in reverse path - # length order so that we remove files before we try to remove the directories. - self._run_svn(["revert", "-R", "."], cwd=self.checkout_root) - for path in added_files: - # This is robust against cwd != self.checkout_root - absolute_path = self.absolute_path(path) - # Completely lame that there is no easy way to remove both types with one call. - if os.path.isdir(path): - os.rmdir(absolute_path) - else: - os.remove(absolute_path) - - def status_command(self): - return [self.executable_name, 'status'] - - def _status_regexp(self, expected_types): - field_count = 6 if self.svn_version() > "1.6" else 5 - return "^(?P<status>[%s]).{%s} (?P<filename>.+)$" % (expected_types, field_count) - - def _add_parent_directories(self, path): - """Does 'svn add' to the path and its parents.""" - if self.in_working_directory(path): - return - self.add(path) - - def add_list(self, paths): - for path in paths: - self._add_parent_directories(os.path.dirname(os.path.abspath(path))) - if self.svn_version() >= "1.7": - # For subversion client 1.7 and later, need to add '--parents' option to ensure intermediate directories - # are added; in addition, 1.7 returns an exit code of 1 from svn add if one or more of the requested - # adds are already under version control, including intermediate directories subject to addition - # due to --parents - svn_add_args = ['svn', 'add', '--parents'] + paths - exit_code = self.run(svn_add_args, return_exit_code=True) - if exit_code and exit_code != 1: - raise ScriptError(script_args=svn_add_args, exit_code=exit_code) - else: - self._run_svn(["add"] + paths) - - def _delete_parent_directories(self, path): - if not self.in_working_directory(path): - return - if set(os.listdir(path)) - self._svn_metadata_files: - return # Directory has non-trivial files in it. - self.delete(path) - - def delete_list(self, paths): - for path in paths: - abs_path = os.path.abspath(path) - parent, base = os.path.split(abs_path) - result = self._run_svn(["delete", "--force", base], cwd=parent) - self._delete_parent_directories(os.path.dirname(abs_path)) - return result - - def exists(self, path): - return not self._run_svn(["info", path], return_exit_code=True, decode_output=False) - - def changed_files(self, git_commit=None): - status_command = [self.executable_name, "status"] - status_command.extend(self._patch_directories) - # ACDMR: Addded, Conflicted, Deleted, Modified or Replaced - return self.run_status_and_extract_filenames(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. - # No file contents printed, thus utf-8 auto-decoding in self.run is fine. - status_command = [self.executable_name, "diff", "--summarize", "-c", revision] - return self.run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR")) - - def revisions_changing_file(self, path, limit=5): - revisions = [] - # svn log will exit(1) (and thus self.run will raise) if the path does not exist. - log_command = ['log', '--quiet', '--limit=%s' % limit, path] - for line in self._run_svn(log_command, cwd=self.checkout_root).splitlines(): - match = re.search('^r(?P<revision>\d+) ', line) - if not match: - continue - revisions.append(int(match.group('revision'))) - return revisions - - def conflicted_files(self): - return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("C")) - - def added_files(self): - return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("A")) - - def deleted_files(self): - return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("D")) - - @staticmethod - def supports_local_commits(): - return False - - def display_name(self): - return "svn" - - def svn_revision(self, path): - return self.value_from_svn_info(path, 'Revision') - - def timestamp_of_revision(self, path, revision): - # We use --xml to get timestamps like 2013-02-08T08:18:04.964409Z - repository_root = self.value_from_svn_info(self.checkout_root, 'Repository Root') - info_output = Executive().run_command([self.executable_name, 'log', '-r', revision, '--xml', repository_root], cwd=path).rstrip() - match = re.search(r"^<date>(?P<value>.+)</date>\r?$", info_output, re.MULTILINE) - return match.group('value') - - # FIXME: This method should be on Checkout. - def create_patch(self, git_commit=None, changed_files=None): - """Returns a byte array (str()) representing the patch file. - Patch files are effectively binary since they may contain - files of multiple different encodings.""" - if changed_files == []: - return "" - elif changed_files == None: - changed_files = [] - return self.run([self.script_path("svn-create-patch")] + changed_files, - cwd=self.checkout_root, return_stderr=False, - decode_output=False) - - def committer_email_for_revision(self, revision): - return self._run_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 self._run_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 self._run_svn(['diff', '-c', revision]) - - def _bogus_dir_name(self): - rnd = ''.join(random.sample(string.ascii_letters, 5)) - if sys.platform.startswith("win"): - parent_dir = tempfile.gettempdir() - else: - parent_dir = sys.path[0] # tempdir is not secure. - return os.path.join(parent_dir, "temp_svn_config_" + rnd) - - def _setup_bogus_dir(self, log): - self._bogus_dir = self._bogus_dir_name() - if not os.path.exists(self._bogus_dir): - os.mkdir(self._bogus_dir) - self._delete_bogus_dir = True - else: - self._delete_bogus_dir = False - if log: - log.debug(' Html: temp config dir: "%s".', self._bogus_dir) - - def _teardown_bogus_dir(self, log): - if self._delete_bogus_dir: - shutil.rmtree(self._bogus_dir, True) - if log: - log.debug(' Html: removed temp config dir: "%s".', self._bogus_dir) - self._bogus_dir = None - - def diff_for_file(self, path, log=None): - self._setup_bogus_dir(log) - try: - args = ['diff'] - if self._bogus_dir: - args += ['--config-dir', self._bogus_dir] - args.append(path) - return self._run_svn(args, cwd=self.checkout_root) - finally: - self._teardown_bogus_dir(log) - - def show_head(self, path): - return self._run_svn(['cat', '-r', 'BASE', path], decode_output=False) - - def _repository_url(self): - return self.value_from_svn_info(self.checkout_root, 'URL') - - def apply_reverse_diff(self, revision): - # '-c -revision' applies the inverse diff of 'revision' - svn_merge_args = ['merge', '--non-interactive', '-c', '-%s' % revision, self._repository_url()] - _log.warning("svn merge has been known to take more than 10 minutes to complete. It is recommended you use git for rollouts.") - _log.debug("Running 'svn %s'" % " ".join(svn_merge_args)) - # FIXME: Should this use cwd=self.checkout_root? - self._run_svn(svn_merge_args) - - def revert_files(self, file_paths): - # FIXME: This should probably use cwd=self.checkout_root. - self._run_svn(['revert'] + file_paths) - - def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None): - # git-commit and force are not used by SVN. - svn_commit_args = ["commit"] - - if not username and not self.has_authorization_for_realm(self.svn_server_realm): - raise AuthenticationError(self.svn_server_host) - if username: - svn_commit_args.extend(["--username", username]) - - svn_commit_args.extend(["-m", message]) - - if changed_files: - svn_commit_args.extend(changed_files) - - return self._run_svn(svn_commit_args, cwd=self.checkout_root, error_handler=commit_error_handler) - - def svn_commit_log(self, svn_revision): - svn_revision = self.strip_r_from_svn_revision(svn_revision) - return self._run_svn(['log', '--non-interactive', '--revision', svn_revision]) - - def last_svn_commit_log(self): - # BASE is the checkout revision, HEAD is the remote repository revision - # http://svnbook.red-bean.com/en/1.0/ch03s03.html - return self.svn_commit_log('BASE') - - def svn_blame(self, path): - return self._run_svn(['blame', path]) - - def propset(self, pname, pvalue, path): - dir, base = os.path.split(path) - return self._run_svn(['pset', pname, pvalue, base], cwd=dir) - - def propget(self, pname, path): - dir, base = os.path.split(path) - return self._run_svn(['pget', pname, base], cwd=dir).encode('utf-8').rstrip("\n") |
