scm_unittest.py revision 2daae5fd11344eaa88a0d92b0f6d65f8d2255c00
1# Copyright (C) 2009 Google Inc. All rights reserved.
2# Copyright (C) 2009 Apple Inc. All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8#    * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10#    * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following disclaimer
12# in the documentation and/or other materials provided with the
13# distribution.
14#    * Neither the name of Google Inc. nor the names of its
15# contributors may be used to endorse or promote products derived from
16# this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30from __future__ import with_statement
31
32import base64
33import codecs
34import getpass
35import os
36import os.path
37import re
38import stat
39import sys
40import subprocess
41import tempfile
42import unittest
43import urllib
44import shutil
45
46from datetime import date
47from webkitpy.common.checkout.api import Checkout
48from webkitpy.common.checkout.scm import detect_scm_system, SCM, SVN, Git, CheckoutNeedsUpdate, commit_error_handler, AuthenticationError, AmbiguousCommitError, find_checkout_root, default_scm
49from webkitpy.common.config.committers import Committer  # FIXME: This should not be needed
50from webkitpy.common.net.bugzilla import Attachment # FIXME: This should not be needed
51from webkitpy.common.system.executive import Executive, run_command, ScriptError
52from webkitpy.common.system.outputcapture import OutputCapture
53from webkitpy.tool.mocktool import MockExecutive
54
55# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
56# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
57
58# FIXME: This should be unified into one of the executive.py commands!
59# Callers could use run_and_throw_if_fail(args, cwd=cwd, quiet=True)
60def run_silent(args, cwd=None):
61    # Note: Not thread safe: http://bugs.python.org/issue2320
62    process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
63    process.communicate() # ignore output
64    exit_code = process.wait()
65    if exit_code:
66        raise ScriptError('Failed to run "%s"  exit_code: %d  cwd: %s' % (args, exit_code, cwd))
67
68
69def write_into_file_at_path(file_path, contents, encoding="utf-8"):
70    if encoding:
71        with codecs.open(file_path, "w", encoding) as file:
72            file.write(contents)
73    else:
74        with open(file_path, "w") as file:
75            file.write(contents)
76
77
78def read_from_path(file_path, encoding="utf-8"):
79    with codecs.open(file_path, "r", encoding) as file:
80        return file.read()
81
82
83def _make_diff(command, *args):
84    # We use this wrapper to disable output decoding. diffs should be treated as
85    # binary files since they may include text files of multiple differnet encodings.
86    return run_command([command, "diff"] + list(args), decode_output=False)
87
88
89def _svn_diff(*args):
90    return _make_diff("svn", *args)
91
92
93def _git_diff(*args):
94    return _make_diff("git", *args)
95
96
97# Exists to share svn repository creation code between the git and svn tests
98class SVNTestRepository:
99    @classmethod
100    def _svn_add(cls, path):
101        run_command(["svn", "add", path])
102
103    @classmethod
104    def _svn_commit(cls, message):
105        run_command(["svn", "commit", "--quiet", "--message", message])
106
107    @classmethod
108    def _setup_test_commits(cls, test_object):
109        # Add some test commits
110        os.chdir(test_object.svn_checkout_path)
111
112        write_into_file_at_path("test_file", "test1")
113        cls._svn_add("test_file")
114        cls._svn_commit("initial commit")
115
116        write_into_file_at_path("test_file", "test1test2")
117        # This used to be the last commit, but doing so broke
118        # GitTest.test_apply_git_patch which use the inverse diff of the last commit.
119        # svn-apply fails to remove directories in Git, see:
120        # https://bugs.webkit.org/show_bug.cgi?id=34871
121        os.mkdir("test_dir")
122        # Slash should always be the right path separator since we use cygwin on Windows.
123        test_file3_path = "test_dir/test_file3"
124        write_into_file_at_path(test_file3_path, "third file")
125        cls._svn_add("test_dir")
126        cls._svn_commit("second commit")
127
128        write_into_file_at_path("test_file", "test1test2test3\n")
129        write_into_file_at_path("test_file2", "second file")
130        cls._svn_add("test_file2")
131        cls._svn_commit("third commit")
132
133        # This 4th commit is used to make sure that our patch file handling
134        # code correctly treats patches as binary and does not attempt to
135        # decode them assuming they're utf-8.
136        write_into_file_at_path("test_file", u"latin1 test: \u00A0\n", "latin1")
137        write_into_file_at_path("test_file2", u"utf-8 test: \u00A0\n", "utf-8")
138        cls._svn_commit("fourth commit")
139
140        # svn does not seem to update after commit as I would expect.
141        run_command(['svn', 'update'])
142
143    @classmethod
144    def setup(cls, test_object):
145        # Create an test SVN repository
146        test_object.svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo")
147        test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path # Not sure this will work on windows
148        # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
149        # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
150        run_command(['svnadmin', 'create', '--pre-1.5-compatible', test_object.svn_repo_path])
151
152        # Create a test svn checkout
153        test_object.svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
154        run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url, test_object.svn_checkout_path])
155
156        # Create and checkout a trunk dir to match the standard svn configuration to match git-svn's expectations
157        os.chdir(test_object.svn_checkout_path)
158        os.mkdir('trunk')
159        cls._svn_add('trunk')
160        # We can add tags and branches as well if we ever need to test those.
161        cls._svn_commit('add trunk')
162
163        # Change directory out of the svn checkout so we can delete the checkout directory.
164        # _setup_test_commits will CD back to the svn checkout directory.
165        os.chdir('/')
166        run_command(['rm', '-rf', test_object.svn_checkout_path])
167        run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url + '/trunk', test_object.svn_checkout_path])
168
169        cls._setup_test_commits(test_object)
170
171    @classmethod
172    def tear_down(cls, test_object):
173        run_command(['rm', '-rf', test_object.svn_repo_path])
174        run_command(['rm', '-rf', test_object.svn_checkout_path])
175
176        # Now that we've deleted the checkout paths, cwddir may be invalid
177        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
178        os.chdir(detect_scm_system(os.path.dirname(__file__)).checkout_root)
179
180
181class StandaloneFunctionsTest(unittest.TestCase):
182    """This class tests any standalone/top-level functions in the package."""
183    def setUp(self):
184        self.orig_cwd = os.path.abspath(os.getcwd())
185        self.orig_abspath = os.path.abspath
186
187        # We capture but ignore the output from stderr to reduce unwanted
188        # logging.
189        self.output = OutputCapture()
190        self.output.capture_output()
191
192    def tearDown(self):
193        os.chdir(self.orig_cwd)
194        os.path.abspath = self.orig_abspath
195        self.output.restore_output()
196
197    def test_find_checkout_root(self):
198        # Test from inside the tree.
199        os.chdir(sys.path[0])
200        dir = find_checkout_root()
201        self.assertNotEqual(dir, None)
202        self.assertTrue(os.path.exists(dir))
203
204        # Test from outside the tree.
205        os.chdir(os.path.expanduser("~"))
206        dir = find_checkout_root()
207        self.assertNotEqual(dir, None)
208        self.assertTrue(os.path.exists(dir))
209
210        # Mock out abspath() to test being not in a checkout at all.
211        os.path.abspath = lambda x: "/"
212        self.assertRaises(SystemExit, find_checkout_root)
213        os.path.abspath = self.orig_abspath
214
215    def test_default_scm(self):
216        # Test from inside the tree.
217        os.chdir(sys.path[0])
218        scm = default_scm()
219        self.assertNotEqual(scm, None)
220
221        # Test from outside the tree.
222        os.chdir(os.path.expanduser("~"))
223        dir = find_checkout_root()
224        self.assertNotEqual(dir, None)
225
226        # Mock out abspath() to test being not in a checkout at all.
227        os.path.abspath = lambda x: "/"
228        self.assertRaises(SystemExit, default_scm)
229        os.path.abspath = self.orig_abspath
230
231# For testing the SCM baseclass directly.
232class SCMClassTests(unittest.TestCase):
233    def setUp(self):
234        self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet.
235
236    def tearDown(self):
237        self.dev_null.close()
238
239    def test_run_command_with_pipe(self):
240        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null)
241        self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n")
242
243        # Test the non-pipe case too:
244        self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n")
245
246        command_returns_non_zero = ['/bin/sh', '--invalid-option']
247        # Test when the input pipe process fails.
248        input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null)
249        self.assertTrue(input_process.poll() != 0)
250        self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout)
251
252        # Test when the run_command process fails.
253        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.
254        self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout)
255
256    def test_error_handlers(self):
257        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"
258        svn_failure_message="""svn: Commit failed (details follow):
259svn: File or directory 'ChangeLog' is out of date; try updating
260svn: resource out of date; try updating
261"""
262        command_does_not_exist = ['does_not_exist', 'invalid_option']
263        self.assertRaises(OSError, run_command, command_does_not_exist)
264        self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error)
265
266        command_returns_non_zero = ['/bin/sh', '--invalid-option']
267        self.assertRaises(ScriptError, run_command, command_returns_non_zero)
268        # Check if returns error text:
269        self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error))
270
271        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message))
272        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message))
273        self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah'))
274
275
276# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
277class SCMTest(unittest.TestCase):
278    def _create_patch(self, patch_contents):
279        # FIXME: This code is brittle if the Attachment API changes.
280        attachment = Attachment({"bug_id": 12345}, None)
281        attachment.contents = lambda: patch_contents
282
283        joe_cool = Committer(name="Joe Cool", email_or_emails=None)
284        attachment.reviewer = lambda: joe_cool
285
286        return attachment
287
288    def _setup_webkittools_scripts_symlink(self, local_scm):
289        webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__)))
290        webkit_scripts_directory = webkit_scm.scripts_directory()
291        local_scripts_directory = local_scm.scripts_directory()
292        os.mkdir(os.path.dirname(local_scripts_directory))
293        os.symlink(webkit_scripts_directory, local_scripts_directory)
294
295    # Tests which both GitTest and SVNTest should run.
296    # FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
297
298    def _shared_test_changed_files(self):
299        write_into_file_at_path("test_file", "changed content")
300        self.assertEqual(self.scm.changed_files(), ["test_file"])
301        write_into_file_at_path("test_dir/test_file3", "new stuff")
302        self.assertEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"])
303        old_cwd = os.getcwd()
304        os.chdir("test_dir")
305        # Validate that changed_files does not change with our cwd, see bug 37015.
306        self.assertEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"])
307        os.chdir(old_cwd)
308
309    def _shared_test_added_files(self):
310        write_into_file_at_path("test_file", "changed content")
311        self.assertEqual(self.scm.added_files(), [])
312
313        write_into_file_at_path("added_file", "new stuff")
314        self.scm.add("added_file")
315
316        os.mkdir("added_dir")
317        write_into_file_at_path("added_dir/added_file2", "new stuff")
318        self.scm.add("added_dir")
319
320        # SVN reports directory changes, Git does not.
321        added_files = self.scm.added_files()
322        if "added_dir" in added_files:
323            added_files.remove("added_dir")
324        self.assertEqual(added_files, ["added_dir/added_file2", "added_file"])
325
326        # Test also to make sure clean_working_directory removes added files
327        self.scm.clean_working_directory()
328        self.assertEqual(self.scm.added_files(), [])
329        self.assertFalse(os.path.exists("added_file"))
330        self.assertFalse(os.path.exists("added_dir"))
331
332    def _shared_test_changed_files_for_revision(self):
333        # SVN reports directory changes, Git does not.
334        changed_files = self.scm.changed_files_for_revision(3)
335        if "test_dir" in changed_files:
336            changed_files.remove("test_dir")
337        self.assertEqual(changed_files, ["test_dir/test_file3", "test_file"])
338        self.assertEqual(sorted(self.scm.changed_files_for_revision(4)), sorted(["test_file", "test_file2"]))  # Git and SVN return different orders.
339        self.assertEqual(self.scm.changed_files_for_revision(2), ["test_file"])
340
341    def _shared_test_contents_at_revision(self):
342        self.assertEqual(self.scm.contents_at_revision("test_file", 3), "test1test2")
343        self.assertEqual(self.scm.contents_at_revision("test_file", 4), "test1test2test3\n")
344
345        # Verify that contents_at_revision returns a byte array, aka str():
346        self.assertEqual(self.scm.contents_at_revision("test_file", 5), u"latin1 test: \u00A0\n".encode("latin1"))
347        self.assertEqual(self.scm.contents_at_revision("test_file2", 5), u"utf-8 test: \u00A0\n".encode("utf-8"))
348
349        self.assertEqual(self.scm.contents_at_revision("test_file2", 4), "second file")
350        # Files which don't exist:
351        # Currently we raise instead of returning None because detecting the difference between
352        # "file not found" and any other error seems impossible with svn (git seems to expose such through the return code).
353        self.assertRaises(ScriptError, self.scm.contents_at_revision, "test_file2", 2)
354        self.assertRaises(ScriptError, self.scm.contents_at_revision, "does_not_exist", 2)
355
356    def _shared_test_revisions_changing_file(self):
357        self.assertEqual(self.scm.revisions_changing_file("test_file"), [5, 4, 3, 2])
358        self.assertRaises(ScriptError, self.scm.revisions_changing_file, "non_existent_file")
359
360    def _shared_test_committer_email_for_revision(self):
361        self.assertEqual(self.scm.committer_email_for_revision(3), getpass.getuser())  # Committer "email" will be the current user
362
363    def _shared_test_reverse_diff(self):
364        self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
365        # Only test the simple case, as any other will end up with conflict markers.
366        self.scm.apply_reverse_diff('5')
367        self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
368
369    def _shared_test_diff_for_revision(self):
370        # Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
371        r3_patch = self.scm.diff_for_revision(4)
372        self.assertTrue(re.search('test3', r3_patch))
373        self.assertFalse(re.search('test4', r3_patch))
374        self.assertTrue(re.search('test2', r3_patch))
375        self.assertTrue(re.search('test2', self.scm.diff_for_revision(3)))
376
377    def _shared_test_svn_apply_git_patch(self):
378        self._setup_webkittools_scripts_symlink(self.scm)
379        git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
380new file mode 100644
381index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90
38260151690
383GIT binary patch
384literal 512
385zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
386zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
387zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
388zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
389zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
390zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
391zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
392z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X
393z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
394ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
395
396literal 0
397HcmV?d00001
398
399"""
400        self.checkout.apply_patch(self._create_patch(git_binary_addition))
401        added = read_from_path('fizzbuzz7.gif', encoding=None)
402        self.assertEqual(512, len(added))
403        self.assertTrue(added.startswith('GIF89a'))
404        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
405
406        # The file already exists.
407        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_addition))
408
409        git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
410index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7
411GIT binary patch
412literal 7
413OcmYex&reD$;sO8*F9L)B
414
415literal 512
416zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
417zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
418zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
419zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
420zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
421zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
422zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
423z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X
424z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
425ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
426
427"""
428        self.checkout.apply_patch(self._create_patch(git_binary_modification))
429        modified = read_from_path('fizzbuzz7.gif', encoding=None)
430        self.assertEqual('foobar\n', modified)
431        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
432
433        # Applying the same modification should fail.
434        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_modification))
435
436        git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
437deleted file mode 100644
438index 323fae0..0000000
439GIT binary patch
440literal 0
441HcmV?d00001
442
443literal 7
444OcmYex&reD$;sO8*F9L)B
445
446"""
447        self.checkout.apply_patch(self._create_patch(git_binary_deletion))
448        self.assertFalse(os.path.exists('fizzbuzz7.gif'))
449        self.assertFalse('fizzbuzz7.gif' in self.scm.changed_files())
450
451        # Cannot delete again.
452        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_deletion))
453
454    def _shared_test_add_recursively(self):
455        os.mkdir("added_dir")
456        write_into_file_at_path("added_dir/added_file", "new stuff")
457        self.scm.add("added_dir/added_file")
458        self.assertTrue("added_dir/added_file" in self.scm.added_files())
459
460
461class SVNTest(SCMTest):
462
463    @staticmethod
464    def _set_date_and_reviewer(changelog_entry):
465        # Joe Cool matches the reviewer set in SCMTest._create_patch
466        changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool')
467        # svn-apply will update ChangeLog entries with today's date.
468        return changelog_entry.replace('DATE_HERE', date.today().isoformat())
469
470    def test_svn_apply(self):
471        first_entry = """2009-10-26  Eric Seidel  <eric@webkit.org>
472
473        Reviewed by Foo Bar.
474
475        Most awesome change ever.
476
477        * scm_unittest.py:
478"""
479        intermediate_entry = """2009-10-27  Eric Seidel  <eric@webkit.org>
480
481        Reviewed by Baz Bar.
482
483        A more awesomer change yet!
484
485        * scm_unittest.py:
486"""
487        one_line_overlap_patch = """Index: ChangeLog
488===================================================================
489--- ChangeLog	(revision 5)
490+++ ChangeLog	(working copy)
491@@ -1,5 +1,13 @@
492 2009-10-26  Eric Seidel  <eric@webkit.org>
493
494+        Reviewed by NOBODY (OOPS!).
495+
496+        Second most awesome change ever.
497+
498+        * scm_unittest.py:
499+
500+2009-10-26  Eric Seidel  <eric@webkit.org>
501+
502         Reviewed by Foo Bar.
503
504         Most awesome change ever.
505"""
506        one_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric@webkit.org>
507
508        Reviewed by REVIEWER_HERE.
509
510        Second most awesome change ever.
511
512        * scm_unittest.py:
513"""
514        two_line_overlap_patch = """Index: ChangeLog
515===================================================================
516--- ChangeLog	(revision 5)
517+++ ChangeLog	(working copy)
518@@ -2,6 +2,14 @@
519
520         Reviewed by Foo Bar.
521
522+        Second most awesome change ever.
523+
524+        * scm_unittest.py:
525+
526+2009-10-26  Eric Seidel  <eric@webkit.org>
527+
528+        Reviewed by Foo Bar.
529+
530         Most awesome change ever.
531
532         * scm_unittest.py:
533"""
534        two_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric@webkit.org>
535
536        Reviewed by Foo Bar.
537
538        Second most awesome change ever.
539
540        * scm_unittest.py:
541"""
542        write_into_file_at_path('ChangeLog', first_entry)
543        run_command(['svn', 'add', 'ChangeLog'])
544        run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit'])
545
546        # Patch files were created against just 'first_entry'.
547        # Add a second commit to make svn-apply have to apply the patches with fuzz.
548        changelog_contents = "%s\n%s" % (intermediate_entry, first_entry)
549        write_into_file_at_path('ChangeLog', changelog_contents)
550        run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit'])
551
552        self._setup_webkittools_scripts_symlink(self.scm)
553        self.checkout.apply_patch(self._create_patch(one_line_overlap_patch))
554        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents)
555        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
556
557        self.scm.revert_files(['ChangeLog'])
558        self.checkout.apply_patch(self._create_patch(two_line_overlap_patch))
559        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents)
560        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
561
562    def setUp(self):
563        SVNTestRepository.setup(self)
564        os.chdir(self.svn_checkout_path)
565        self.scm = detect_scm_system(self.svn_checkout_path)
566        # For historical reasons, we test some checkout code here too.
567        self.checkout = Checkout(self.scm)
568
569    def tearDown(self):
570        SVNTestRepository.tear_down(self)
571
572    def test_detect_scm_system_relative_url(self):
573        scm = detect_scm_system(".")
574        # I wanted to assert that we got the right path, but there was some
575        # crazy magic with temp folder names that I couldn't figure out.
576        self.assertTrue(scm.checkout_root)
577
578    def test_create_patch_is_full_patch(self):
579        test_dir_path = os.path.join(self.svn_checkout_path, "test_dir2")
580        os.mkdir(test_dir_path)
581        test_file_path = os.path.join(test_dir_path, 'test_file2')
582        write_into_file_at_path(test_file_path, 'test content')
583        run_command(['svn', 'add', 'test_dir2'])
584
585        # create_patch depends on 'svn-create-patch', so make a dummy version.
586        scripts_path = os.path.join(self.svn_checkout_path, 'Tools', 'Scripts')
587        os.makedirs(scripts_path)
588        create_patch_path = os.path.join(scripts_path, 'svn-create-patch')
589        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.
590        os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR)
591
592        # Change into our test directory and run the create_patch command.
593        os.chdir(test_dir_path)
594        scm = detect_scm_system(test_dir_path)
595        self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right.
596        patch_contents = scm.create_patch()
597        # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo.
598        self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n.
599
600    def test_detection(self):
601        scm = detect_scm_system(self.svn_checkout_path)
602        self.assertEqual(scm.display_name(), "svn")
603        self.assertEqual(scm.supports_local_commits(), False)
604
605    def test_apply_small_binary_patch(self):
606        patch_contents = """Index: test_file.swf
607===================================================================
608Cannot display: file marked as a binary type.
609svn:mime-type = application/octet-stream
610
611Property changes on: test_file.swf
612___________________________________________________________________
613Name: svn:mime-type
614   + application/octet-stream
615
616
617Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
618"""
619        expected_contents = base64.b64decode("Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==")
620        self._setup_webkittools_scripts_symlink(self.scm)
621        patch_file = self._create_patch(patch_contents)
622        self.checkout.apply_patch(patch_file)
623        actual_contents = read_from_path("test_file.swf", encoding=None)
624        self.assertEqual(actual_contents, expected_contents)
625
626    def test_apply_svn_patch(self):
627        scm = detect_scm_system(self.svn_checkout_path)
628        patch = self._create_patch(_svn_diff("-r5:4"))
629        self._setup_webkittools_scripts_symlink(scm)
630        Checkout(scm).apply_patch(patch)
631
632    def test_apply_svn_patch_force(self):
633        scm = detect_scm_system(self.svn_checkout_path)
634        patch = self._create_patch(_svn_diff("-r3:5"))
635        self._setup_webkittools_scripts_symlink(scm)
636        self.assertRaises(ScriptError, Checkout(scm).apply_patch, patch, force=True)
637
638    def test_commit_logs(self):
639        # Commits have dates and usernames in them, so we can't just direct compare.
640        self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
641        self.assertTrue(re.search('second commit', self.scm.svn_commit_log(3)))
642
643    def _shared_test_commit_with_message(self, username=None):
644        write_into_file_at_path('test_file', 'more test content')
645        commit_text = self.scm.commit_with_message("another test commit", username)
646        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
647
648        self.scm.dryrun = True
649        write_into_file_at_path('test_file', 'still more test content')
650        commit_text = self.scm.commit_with_message("yet another test commit", username)
651        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
652
653    def test_commit_in_subdir(self, username=None):
654        write_into_file_at_path('test_dir/test_file3', 'more test content')
655        os.chdir("test_dir")
656        commit_text = self.scm.commit_with_message("another test commit", username)
657        os.chdir("..")
658        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
659
660    def test_commit_text_parsing(self):
661        self._shared_test_commit_with_message()
662
663    def test_commit_with_username(self):
664        self._shared_test_commit_with_message("dbates@webkit.org")
665
666    def test_commit_without_authorization(self):
667        self.scm.has_authorization_for_realm = lambda realm: False
668        self.assertRaises(AuthenticationError, self._shared_test_commit_with_message)
669
670    def test_has_authorization_for_realm(self):
671        scm = detect_scm_system(self.svn_checkout_path)
672        fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir")
673        svn_config_dir_path = os.path.join(fake_home_dir, ".subversion")
674        os.mkdir(svn_config_dir_path)
675        fake_webkit_auth_file = os.path.join(svn_config_dir_path, "fake_webkit_auth_file")
676        write_into_file_at_path(fake_webkit_auth_file, SVN.svn_server_realm)
677        self.assertTrue(scm.has_authorization_for_realm(SVN.svn_server_realm, home_directory=fake_home_dir))
678        os.remove(fake_webkit_auth_file)
679        os.rmdir(svn_config_dir_path)
680        os.rmdir(fake_home_dir)
681
682    def test_not_have_authorization_for_realm(self):
683        scm = detect_scm_system(self.svn_checkout_path)
684        fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir")
685        svn_config_dir_path = os.path.join(fake_home_dir, ".subversion")
686        os.mkdir(svn_config_dir_path)
687        self.assertFalse(scm.has_authorization_for_realm(SVN.svn_server_realm, home_directory=fake_home_dir))
688        os.rmdir(svn_config_dir_path)
689        os.rmdir(fake_home_dir)
690
691    def test_reverse_diff(self):
692        self._shared_test_reverse_diff()
693
694    def test_diff_for_revision(self):
695        self._shared_test_diff_for_revision()
696
697    def test_svn_apply_git_patch(self):
698        self._shared_test_svn_apply_git_patch()
699
700    def test_changed_files(self):
701        self._shared_test_changed_files()
702
703    def test_changed_files_for_revision(self):
704        self._shared_test_changed_files_for_revision()
705
706    def test_added_files(self):
707        self._shared_test_added_files()
708
709    def test_contents_at_revision(self):
710        self._shared_test_contents_at_revision()
711
712    def test_revisions_changing_file(self):
713        self._shared_test_revisions_changing_file()
714
715    def test_committer_email_for_revision(self):
716        self._shared_test_committer_email_for_revision()
717
718    def test_add_recursively(self):
719        self._shared_test_add_recursively()
720
721    def test_delete(self):
722        os.chdir(self.svn_checkout_path)
723        self.scm.delete("test_file")
724        self.assertTrue("test_file" in self.scm.deleted_files())
725
726    def test_propset_propget(self):
727        filepath = os.path.join(self.svn_checkout_path, "test_file")
728        expected_mime_type = "x-application/foo-bar"
729        self.scm.propset("svn:mime-type", expected_mime_type, filepath)
730        self.assertEqual(expected_mime_type, self.scm.propget("svn:mime-type", filepath))
731
732    def test_show_head(self):
733        write_into_file_at_path("test_file", u"Hello!", "utf-8")
734        SVNTestRepository._svn_commit("fourth commit")
735        self.assertEqual("Hello!", self.scm.show_head('test_file'))
736
737    def test_show_head_binary(self):
738        data = "\244"
739        write_into_file_at_path("binary_file", data, encoding=None)
740        self.scm.add("binary_file")
741        self.scm.commit_with_message("a test commit")
742        self.assertEqual(data, self.scm.show_head('binary_file'))
743
744    def do_test_diff_for_file(self):
745        write_into_file_at_path('test_file', 'some content')
746        self.scm.commit_with_message("a test commit")
747        diff = self.scm.diff_for_file('test_file')
748        self.assertEqual(diff, "")
749
750        write_into_file_at_path("test_file", "changed content")
751        diff = self.scm.diff_for_file('test_file')
752        self.assertTrue("-some content" in diff)
753        self.assertTrue("+changed content" in diff)
754
755    def clean_bogus_dir(self):
756        self.bogus_dir = self.scm._bogus_dir_name()
757        if os.path.exists(self.bogus_dir):
758            shutil.rmtree(self.bogus_dir)
759
760    def test_diff_for_file_with_existing_bogus_dir(self):
761        self.clean_bogus_dir()
762        os.mkdir(self.bogus_dir)
763        self.do_test_diff_for_file()
764        self.assertTrue(os.path.exists(self.bogus_dir))
765        shutil.rmtree(self.bogus_dir)
766
767    def test_diff_for_file_with_missing_bogus_dir(self):
768        self.clean_bogus_dir()
769        self.do_test_diff_for_file()
770        self.assertFalse(os.path.exists(self.bogus_dir))
771
772    def test_svn_lock(self):
773        svn_root_lock_path = ".svn/lock"
774        write_into_file_at_path(svn_root_lock_path, "", "utf-8")
775        # webkit-patch uses a Checkout object and runs update-webkit, just use svn update here.
776        self.assertRaises(ScriptError, run_command, ['svn', 'update'])
777        self.scm.clean_working_directory()
778        self.assertFalse(os.path.exists(svn_root_lock_path))
779        run_command(['svn', 'update'])  # Should succeed and not raise.
780
781
782class GitTest(SCMTest):
783
784    def setUp(self):
785        """Sets up fresh git repository with one commit. Then setups a second git
786        repo that tracks the first one."""
787        self.original_dir = os.getcwd()
788
789        self.untracking_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout2")
790        run_command(['git', 'init', self.untracking_checkout_path])
791
792        os.chdir(self.untracking_checkout_path)
793        write_into_file_at_path('foo_file', 'foo')
794        run_command(['git', 'add', 'foo_file'])
795        run_command(['git', 'commit', '-am', 'dummy commit'])
796        self.untracking_scm = detect_scm_system(self.untracking_checkout_path)
797
798        self.tracking_git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
799        run_command(['git', 'clone', '--quiet', self.untracking_checkout_path, self.tracking_git_checkout_path])
800        os.chdir(self.tracking_git_checkout_path)
801        self.tracking_scm = detect_scm_system(self.tracking_git_checkout_path)
802
803    def tearDown(self):
804        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
805        os.chdir(self.original_dir)
806        run_command(['rm', '-rf', self.tracking_git_checkout_path])
807        run_command(['rm', '-rf', self.untracking_checkout_path])
808
809    def test_remote_branch_ref(self):
810        self.assertEqual(self.tracking_scm.remote_branch_ref(), 'refs/remotes/origin/master')
811
812        os.chdir(self.untracking_checkout_path)
813        self.assertRaises(ScriptError, self.untracking_scm.remote_branch_ref)
814
815    def test_multiple_remotes(self):
816        run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote1'])
817        run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote2'])
818        self.assertEqual(self.tracking_scm.remote_branch_ref(), 'remote1')
819
820    def test_create_patch(self):
821        write_into_file_at_path('test_file_commit1', 'contents')
822        run_command(['git', 'add', 'test_file_commit1'])
823        scm = detect_scm_system(self.untracking_checkout_path)
824        scm.commit_locally_with_message('message')
825
826        patch = scm.create_patch()
827        self.assertFalse(re.search(r'Subversion Revision:', patch))
828
829
830class GitSVNTest(SCMTest):
831
832    def _setup_git_checkout(self):
833        self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
834        # --quiet doesn't make git svn silent, so we use run_silent to redirect output
835        run_silent(['git', 'svn', 'clone', '-T', 'trunk', self.svn_repo_url, self.git_checkout_path])
836        os.chdir(self.git_checkout_path)
837
838    def _tear_down_git_checkout(self):
839        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
840        os.chdir(self.original_dir)
841        run_command(['rm', '-rf', self.git_checkout_path])
842
843    def setUp(self):
844        self.original_dir = os.getcwd()
845
846        SVNTestRepository.setup(self)
847        self._setup_git_checkout()
848        self.scm = detect_scm_system(self.git_checkout_path)
849        # For historical reasons, we test some checkout code here too.
850        self.checkout = Checkout(self.scm)
851
852    def tearDown(self):
853        SVNTestRepository.tear_down(self)
854        self._tear_down_git_checkout()
855
856    def test_detection(self):
857        scm = detect_scm_system(self.git_checkout_path)
858        self.assertEqual(scm.display_name(), "git")
859        self.assertEqual(scm.supports_local_commits(), True)
860
861    def test_read_git_config(self):
862        key = 'test.git-config'
863        value = 'git-config value'
864        run_command(['git', 'config', key, value])
865        self.assertEqual(self.scm.read_git_config(key), value)
866
867    def test_local_commits(self):
868        test_file = os.path.join(self.git_checkout_path, 'test_file')
869        write_into_file_at_path(test_file, 'foo')
870        run_command(['git', 'commit', '-a', '-m', 'local commit'])
871
872        self.assertEqual(len(self.scm.local_commits()), 1)
873
874    def test_discard_local_commits(self):
875        test_file = os.path.join(self.git_checkout_path, 'test_file')
876        write_into_file_at_path(test_file, 'foo')
877        run_command(['git', 'commit', '-a', '-m', 'local commit'])
878
879        self.assertEqual(len(self.scm.local_commits()), 1)
880        self.scm.discard_local_commits()
881        self.assertEqual(len(self.scm.local_commits()), 0)
882
883    def test_delete_branch(self):
884        new_branch = 'foo'
885
886        run_command(['git', 'checkout', '-b', new_branch])
887        self.assertEqual(run_command(['git', 'symbolic-ref', 'HEAD']).strip(), 'refs/heads/' + new_branch)
888
889        run_command(['git', 'checkout', '-b', 'bar'])
890        self.scm.delete_branch(new_branch)
891
892        self.assertFalse(re.search(r'foo', run_command(['git', 'branch'])))
893
894    def test_remote_merge_base(self):
895        # Diff to merge-base should include working-copy changes,
896        # which the diff to svn_branch.. doesn't.
897        test_file = os.path.join(self.git_checkout_path, 'test_file')
898        write_into_file_at_path(test_file, 'foo')
899
900        diff_to_common_base = _git_diff(self.scm.remote_branch_ref() + '..')
901        diff_to_merge_base = _git_diff(self.scm.remote_merge_base())
902
903        self.assertFalse(re.search(r'foo', diff_to_common_base))
904        self.assertTrue(re.search(r'foo', diff_to_merge_base))
905
906    def test_rebase_in_progress(self):
907        svn_test_file = os.path.join(self.svn_checkout_path, 'test_file')
908        write_into_file_at_path(svn_test_file, "svn_checkout")
909        run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
910
911        git_test_file = os.path.join(self.git_checkout_path, 'test_file')
912        write_into_file_at_path(git_test_file, "git_checkout")
913        run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
914
915        # --quiet doesn't make git svn silent, so use run_silent to redirect output
916        self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase.
917
918        scm = detect_scm_system(self.git_checkout_path)
919        self.assertTrue(scm.rebase_in_progress())
920
921        # Make sure our cleanup works.
922        scm.clean_working_directory()
923        self.assertFalse(scm.rebase_in_progress())
924
925        # Make sure cleanup doesn't throw when no rebase is in progress.
926        scm.clean_working_directory()
927
928    def test_commitish_parsing(self):
929        scm = detect_scm_system(self.git_checkout_path)
930
931        # Multiple revisions are cherry-picked.
932        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1)
933        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2)
934
935        # ... is an invalid range specifier
936        self.assertRaises(ScriptError, scm.commit_ids_from_commitish_arguments, ['trunk...HEAD'])
937
938    def test_commitish_order(self):
939        scm = detect_scm_system(self.git_checkout_path)
940
941        commit_range = 'HEAD~3..HEAD'
942
943        actual_commits = scm.commit_ids_from_commitish_arguments([commit_range])
944        expected_commits = []
945        expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines())
946
947        self.assertEqual(actual_commits, expected_commits)
948
949    def test_apply_git_patch(self):
950        scm = detect_scm_system(self.git_checkout_path)
951        # We carefullly pick a diff which does not have a directory addition
952        # as currently svn-apply will error out when trying to remove directories
953        # in Git: https://bugs.webkit.org/show_bug.cgi?id=34871
954        patch = self._create_patch(_git_diff('HEAD..HEAD^'))
955        self._setup_webkittools_scripts_symlink(scm)
956        Checkout(scm).apply_patch(patch)
957
958    def test_apply_git_patch_force(self):
959        scm = detect_scm_system(self.git_checkout_path)
960        patch = self._create_patch(_git_diff('HEAD~2..HEAD'))
961        self._setup_webkittools_scripts_symlink(scm)
962        self.assertRaises(ScriptError, Checkout(scm).apply_patch, patch, force=True)
963
964    def test_commit_text_parsing(self):
965        write_into_file_at_path('test_file', 'more test content')
966        commit_text = self.scm.commit_with_message("another test commit")
967        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
968
969        self.scm.dryrun = True
970        write_into_file_at_path('test_file', 'still more test content')
971        commit_text = self.scm.commit_with_message("yet another test commit")
972        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
973
974    def test_commit_with_message_working_copy_only(self):
975        write_into_file_at_path('test_file_commit1', 'more test content')
976        run_command(['git', 'add', 'test_file_commit1'])
977        scm = detect_scm_system(self.git_checkout_path)
978        commit_text = scm.commit_with_message("yet another test commit")
979
980        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
981        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
982        self.assertTrue(re.search(r'test_file_commit1', svn_log))
983
984    def _local_commit(self, filename, contents, message):
985        write_into_file_at_path(filename, contents)
986        run_command(['git', 'add', filename])
987        self.scm.commit_locally_with_message(message)
988
989    def _one_local_commit(self):
990        self._local_commit('test_file_commit1', 'more test content', 'another test commit')
991
992    def _one_local_commit_plus_working_copy_changes(self):
993        self._one_local_commit()
994        write_into_file_at_path('test_file_commit2', 'still more test content')
995        run_command(['git', 'add', 'test_file_commit2'])
996
997    def _two_local_commits(self):
998        self._one_local_commit()
999        self._local_commit('test_file_commit2', 'still more test content', 'yet another test commit')
1000
1001    def _three_local_commits(self):
1002        self._local_commit('test_file_commit0', 'more test content', 'another test commit')
1003        self._two_local_commits()
1004
1005    def test_revisions_changing_files_with_local_commit(self):
1006        self._one_local_commit()
1007        self.assertEquals(self.scm.revisions_changing_file('test_file_commit1'), [])
1008
1009    def test_commit_with_message(self):
1010        self._one_local_commit_plus_working_copy_changes()
1011        scm = detect_scm_system(self.git_checkout_path)
1012        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "yet another test commit")
1013        commit_text = scm.commit_with_message("yet another test commit", force_squash=True)
1014
1015        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1016        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1017        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1018        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1019
1020    def test_commit_with_message_git_commit(self):
1021        self._two_local_commits()
1022
1023        scm = detect_scm_system(self.git_checkout_path)
1024        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD^")
1025        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1026
1027        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1028        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1029        self.assertFalse(re.search(r'test_file_commit2', svn_log))
1030
1031    def test_commit_with_message_git_commit_range(self):
1032        self._three_local_commits()
1033
1034        scm = detect_scm_system(self.git_checkout_path)
1035        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD~2..HEAD")
1036        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1037
1038        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1039        self.assertFalse(re.search(r'test_file_commit0', svn_log))
1040        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1041        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1042
1043    def test_changed_files_working_copy_only(self):
1044        self._one_local_commit_plus_working_copy_changes()
1045        scm = detect_scm_system(self.git_checkout_path)
1046        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD..")
1047        self.assertFalse(re.search(r'test_file_commit1', svn_log))
1048        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1049
1050    def test_commit_with_message_only_local_commit(self):
1051        self._one_local_commit()
1052        scm = detect_scm_system(self.git_checkout_path)
1053        commit_text = scm.commit_with_message("another test commit")
1054        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1055        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1056
1057    def test_commit_with_message_multiple_local_commits_and_working_copy(self):
1058        self._two_local_commits()
1059        write_into_file_at_path('test_file_commit1', 'working copy change')
1060        scm = detect_scm_system(self.git_checkout_path)
1061
1062        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "another test commit")
1063        commit_text = scm.commit_with_message("another test commit", force_squash=True)
1064
1065        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1066        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1067        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1068        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1069
1070    def test_commit_with_message_git_commit_and_working_copy(self):
1071        self._two_local_commits()
1072        write_into_file_at_path('test_file_commit1', 'working copy change')
1073        scm = detect_scm_system(self.git_checkout_path)
1074        self.assertRaises(ScriptError, scm.commit_with_message, "another test commit", git_commit="HEAD^")
1075
1076    def test_commit_with_message_multiple_local_commits_always_squash(self):
1077        self._two_local_commits()
1078        scm = detect_scm_system(self.git_checkout_path)
1079        scm._assert_can_squash = lambda working_directory_is_clean: True
1080        commit_text = scm.commit_with_message("yet another test commit")
1081        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1082
1083        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1084        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1085        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1086
1087    def test_commit_with_message_multiple_local_commits(self):
1088        self._two_local_commits()
1089        scm = detect_scm_system(self.git_checkout_path)
1090        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "yet another test commit")
1091        commit_text = scm.commit_with_message("yet another test commit", force_squash=True)
1092
1093        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1094
1095        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1096        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1097        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1098
1099    def test_commit_with_message_not_synced(self):
1100        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1101        self._two_local_commits()
1102        scm = detect_scm_system(self.git_checkout_path)
1103        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "another test commit")
1104        commit_text = scm.commit_with_message("another test commit", force_squash=True)
1105
1106        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1107
1108        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1109        self.assertFalse(re.search(r'test_file2', svn_log))
1110        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1111        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1112
1113    def test_commit_with_message_not_synced_with_conflict(self):
1114        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1115        self._local_commit('test_file2', 'asdf', 'asdf commit')
1116
1117        scm = detect_scm_system(self.git_checkout_path)
1118        # There's a conflict between trunk and the test_file2 modification.
1119        self.assertRaises(ScriptError, scm.commit_with_message, "another test commit", force_squash=True)
1120
1121    def test_remote_branch_ref(self):
1122        self.assertEqual(self.scm.remote_branch_ref(), 'refs/remotes/trunk')
1123
1124    def test_reverse_diff(self):
1125        self._shared_test_reverse_diff()
1126
1127    def test_diff_for_revision(self):
1128        self._shared_test_diff_for_revision()
1129
1130    def test_svn_apply_git_patch(self):
1131        self._shared_test_svn_apply_git_patch()
1132
1133    def test_create_patch_local_plus_working_copy(self):
1134        self._one_local_commit_plus_working_copy_changes()
1135        scm = detect_scm_system(self.git_checkout_path)
1136        patch = scm.create_patch()
1137        self.assertTrue(re.search(r'test_file_commit1', patch))
1138        self.assertTrue(re.search(r'test_file_commit2', patch))
1139
1140    def test_create_patch(self):
1141        self._one_local_commit_plus_working_copy_changes()
1142        scm = detect_scm_system(self.git_checkout_path)
1143        patch = scm.create_patch()
1144        self.assertTrue(re.search(r'test_file_commit2', patch))
1145        self.assertTrue(re.search(r'test_file_commit1', patch))
1146        self.assertTrue(re.search(r'Subversion Revision: 5', patch))
1147
1148    def test_create_patch_after_merge(self):
1149        run_command(['git', 'checkout', '-b', 'dummy-branch', 'trunk~3'])
1150        self._one_local_commit()
1151        run_command(['git', 'merge', 'trunk'])
1152
1153        scm = detect_scm_system(self.git_checkout_path)
1154        patch = scm.create_patch()
1155        self.assertTrue(re.search(r'test_file_commit1', patch))
1156        self.assertTrue(re.search(r'Subversion Revision: 5', patch))
1157
1158    def test_create_patch_with_changed_files(self):
1159        self._one_local_commit_plus_working_copy_changes()
1160        scm = detect_scm_system(self.git_checkout_path)
1161        patch = scm.create_patch(changed_files=['test_file_commit2'])
1162        self.assertTrue(re.search(r'test_file_commit2', patch))
1163
1164    def test_create_patch_with_rm_and_changed_files(self):
1165        self._one_local_commit_plus_working_copy_changes()
1166        scm = detect_scm_system(self.git_checkout_path)
1167        os.remove('test_file_commit1')
1168        patch = scm.create_patch()
1169        patch_with_changed_files = scm.create_patch(changed_files=['test_file_commit1', 'test_file_commit2'])
1170        self.assertEquals(patch, patch_with_changed_files)
1171
1172    def test_create_patch_git_commit(self):
1173        self._two_local_commits()
1174        scm = detect_scm_system(self.git_checkout_path)
1175        patch = scm.create_patch(git_commit="HEAD^")
1176        self.assertTrue(re.search(r'test_file_commit1', patch))
1177        self.assertFalse(re.search(r'test_file_commit2', patch))
1178
1179    def test_create_patch_git_commit_range(self):
1180        self._three_local_commits()
1181        scm = detect_scm_system(self.git_checkout_path)
1182        patch = scm.create_patch(git_commit="HEAD~2..HEAD")
1183        self.assertFalse(re.search(r'test_file_commit0', patch))
1184        self.assertTrue(re.search(r'test_file_commit2', patch))
1185        self.assertTrue(re.search(r'test_file_commit1', patch))
1186
1187    def test_create_patch_working_copy_only(self):
1188        self._one_local_commit_plus_working_copy_changes()
1189        scm = detect_scm_system(self.git_checkout_path)
1190        patch = scm.create_patch(git_commit="HEAD..")
1191        self.assertFalse(re.search(r'test_file_commit1', patch))
1192        self.assertTrue(re.search(r'test_file_commit2', patch))
1193
1194    def test_create_patch_multiple_local_commits(self):
1195        self._two_local_commits()
1196        scm = detect_scm_system(self.git_checkout_path)
1197        patch = scm.create_patch()
1198        self.assertTrue(re.search(r'test_file_commit2', patch))
1199        self.assertTrue(re.search(r'test_file_commit1', patch))
1200
1201    def test_create_patch_not_synced(self):
1202        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1203        self._two_local_commits()
1204        scm = detect_scm_system(self.git_checkout_path)
1205        patch = scm.create_patch()
1206        self.assertFalse(re.search(r'test_file2', patch))
1207        self.assertTrue(re.search(r'test_file_commit2', patch))
1208        self.assertTrue(re.search(r'test_file_commit1', patch))
1209
1210    def test_create_binary_patch(self):
1211        # Create a git binary patch and check the contents.
1212        scm = detect_scm_system(self.git_checkout_path)
1213        test_file_name = 'binary_file'
1214        test_file_path = os.path.join(self.git_checkout_path, test_file_name)
1215        file_contents = ''.join(map(chr, range(256)))
1216        write_into_file_at_path(test_file_path, file_contents, encoding=None)
1217        run_command(['git', 'add', test_file_name])
1218        patch = scm.create_patch()
1219        self.assertTrue(re.search(r'\nliteral 0\n', patch))
1220        self.assertTrue(re.search(r'\nliteral 256\n', patch))
1221
1222        # Check if we can apply the created patch.
1223        run_command(['git', 'rm', '-f', test_file_name])
1224        self._setup_webkittools_scripts_symlink(scm)
1225        self.checkout.apply_patch(self._create_patch(patch))
1226        self.assertEqual(file_contents, read_from_path(test_file_path, encoding=None))
1227
1228        # Check if we can create a patch from a local commit.
1229        write_into_file_at_path(test_file_path, file_contents, encoding=None)
1230        run_command(['git', 'add', test_file_name])
1231        run_command(['git', 'commit', '-m', 'binary diff'])
1232        patch_from_local_commit = scm.create_patch('HEAD')
1233        self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
1234        self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
1235
1236    def test_changed_files_local_plus_working_copy(self):
1237        self._one_local_commit_plus_working_copy_changes()
1238        scm = detect_scm_system(self.git_checkout_path)
1239        files = scm.changed_files()
1240        self.assertTrue('test_file_commit1' in files)
1241        self.assertTrue('test_file_commit2' in files)
1242
1243    def test_changed_files_git_commit(self):
1244        self._two_local_commits()
1245        scm = detect_scm_system(self.git_checkout_path)
1246        files = scm.changed_files(git_commit="HEAD^")
1247        self.assertTrue('test_file_commit1' in files)
1248        self.assertFalse('test_file_commit2' in files)
1249
1250    def test_changed_files_git_commit_range(self):
1251        self._three_local_commits()
1252        scm = detect_scm_system(self.git_checkout_path)
1253        files = scm.changed_files(git_commit="HEAD~2..HEAD")
1254        self.assertTrue('test_file_commit0' not in files)
1255        self.assertTrue('test_file_commit1' in files)
1256        self.assertTrue('test_file_commit2' in files)
1257
1258    def test_changed_files_working_copy_only(self):
1259        self._one_local_commit_plus_working_copy_changes()
1260        scm = detect_scm_system(self.git_checkout_path)
1261        files = scm.changed_files(git_commit="HEAD..")
1262        self.assertFalse('test_file_commit1' in files)
1263        self.assertTrue('test_file_commit2' in files)
1264
1265    def test_changed_files_multiple_local_commits(self):
1266        self._two_local_commits()
1267        scm = detect_scm_system(self.git_checkout_path)
1268        files = scm.changed_files()
1269        self.assertTrue('test_file_commit2' in files)
1270        self.assertTrue('test_file_commit1' in files)
1271
1272    def test_changed_files_not_synced(self):
1273        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1274        self._two_local_commits()
1275        scm = detect_scm_system(self.git_checkout_path)
1276        files = scm.changed_files()
1277        self.assertFalse('test_file2' in files)
1278        self.assertTrue('test_file_commit2' in files)
1279        self.assertTrue('test_file_commit1' in files)
1280
1281    def test_changed_files_not_synced(self):
1282        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1283        self._two_local_commits()
1284        scm = detect_scm_system(self.git_checkout_path)
1285        files = scm.changed_files()
1286        self.assertFalse('test_file2' in files)
1287        self.assertTrue('test_file_commit2' in files)
1288        self.assertTrue('test_file_commit1' in files)
1289
1290    def test_changed_files(self):
1291        self._shared_test_changed_files()
1292
1293    def test_changed_files_for_revision(self):
1294        self._shared_test_changed_files_for_revision()
1295
1296    def test_contents_at_revision(self):
1297        self._shared_test_contents_at_revision()
1298
1299    def test_revisions_changing_file(self):
1300        self._shared_test_revisions_changing_file()
1301
1302    def test_added_files(self):
1303        self._shared_test_added_files()
1304
1305    def test_committer_email_for_revision(self):
1306        self._shared_test_committer_email_for_revision()
1307
1308    def test_add_recursively(self):
1309        self._shared_test_add_recursively()
1310
1311    def test_delete(self):
1312        self._two_local_commits()
1313        self.scm.delete('test_file_commit1')
1314        self.assertTrue("test_file_commit1" in self.scm.deleted_files())
1315
1316    def test_to_object_name(self):
1317        relpath = 'test_file_commit1'
1318        fullpath = os.path.join(self.git_checkout_path, relpath)
1319        self._two_local_commits()
1320        self.assertEqual(relpath, self.scm.to_object_name(fullpath))
1321
1322    def test_show_head(self):
1323        self._two_local_commits()
1324        self.assertEqual("more test content", self.scm.show_head('test_file_commit1'))
1325
1326    def test_show_head_binary(self):
1327        self._two_local_commits()
1328        data = "\244"
1329        write_into_file_at_path("binary_file", data, encoding=None)
1330        self.scm.add("binary_file")
1331        self.scm.commit_locally_with_message("a test commit")
1332        self.assertEqual(data, self.scm.show_head('binary_file'))
1333
1334    def test_diff_for_file(self):
1335        self._two_local_commits()
1336        write_into_file_at_path('test_file_commit1', "Updated", encoding=None)
1337
1338        diff = self.scm.diff_for_file('test_file_commit1')
1339        cached_diff = self.scm.diff_for_file('test_file_commit1')
1340        self.assertTrue("+Updated" in diff)
1341        self.assertTrue("-more test content" in diff)
1342
1343        self.scm.add('test_file_commit1')
1344
1345        cached_diff = self.scm.diff_for_file('test_file_commit1')
1346        self.assertTrue("+Updated" in cached_diff)
1347        self.assertTrue("-more test content" in cached_diff)
1348
1349
1350# We need to split off more of these SCM tests to use mocks instead of the filesystem.
1351# This class is the first part of that.
1352class GitTestWithMock(unittest.TestCase):
1353    def setUp(self):
1354        executive = MockExecutive(should_log=False)
1355        # We do this should_log dance to avoid logging when Git.__init__ runs sysctl on mac to check for 64-bit support.
1356        self.scm = Git(None, executive=executive)
1357        executive.should_log = True
1358
1359    def test_create_patch(self):
1360        expected_stderr = "MOCK run_command: ['git', 'merge-base', u'refs/remotes/origin/master', 'HEAD']\nMOCK run_command: ['git', 'diff', '--binary', '--no-ext-diff', '--full-index', '-M', 'MOCK output of child process', '--']\n"
1361        OutputCapture().assert_outputs(self, self.scm.create_patch, kwargs={'changed_files': None}, expected_stderr=expected_stderr)
1362
1363
1364if __name__ == '__main__':
1365    unittest.main()
1366