Home | History | Annotate | Download | only in scm
      1 # Copyright (C) 2009 Google Inc. All rights reserved.
      2 # Copyright (C) 2009 Apple Inc. All rights reserved.
      3 # Copyright (C) 2011 Daniel Bates (dbates (at] intudata.com). All rights reserved.
      4 #
      5 # Redistribution and use in source and binary forms, with or without
      6 # modification, are permitted provided that the following conditions are
      7 # met:
      8 #
      9 #    * Redistributions of source code must retain the above copyright
     10 # notice, this list of conditions and the following disclaimer.
     11 #    * Redistributions in binary form must reproduce the above
     12 # copyright notice, this list of conditions and the following disclaimer
     13 # in the documentation and/or other materials provided with the
     14 # distribution.
     15 #    * Neither the name of Google Inc. nor the names of its
     16 # contributors may be used to endorse or promote products derived from
     17 # this software without specific prior written permission.
     18 #
     19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30 
     31 import atexit
     32 import os
     33 import shutil
     34 import unittest
     35 
     36 from webkitpy.common.system.executive import Executive, ScriptError
     37 from webkitpy.common.system.executive_mock import MockExecutive
     38 from webkitpy.common.system.filesystem import FileSystem
     39 from webkitpy.common.system.filesystem_mock import MockFileSystem
     40 from webkitpy.common.checkout.scm.detection import detect_scm_system
     41 from webkitpy.common.checkout.scm.git import Git, AmbiguousCommitError
     42 from webkitpy.common.checkout.scm.scm import SCM
     43 from webkitpy.common.checkout.scm.svn import SVN
     44 
     45 
     46 # We cache the mock SVN repo so that we don't create it again for each call to an SVNTest or GitTest test_ method.
     47 # We store it in a global variable so that we can delete this cached repo on exit(3).
     48 original_cwd = None
     49 cached_svn_repo_path = None
     50 
     51 @atexit.register
     52 def delete_cached_svn_repo_at_exit():
     53     if cached_svn_repo_path:
     54         os.chdir(original_cwd)
     55         shutil.rmtree(cached_svn_repo_path)
     56 
     57 
     58 class SCMTestBase(unittest.TestCase):
     59     def __init__(self, *args, **kwargs):
     60         super(SCMTestBase, self).__init__(*args, **kwargs)
     61         self.scm = None
     62         self.executive = None
     63         self.fs = None
     64         self.original_cwd = None
     65 
     66     def setUp(self):
     67         self.executive = Executive()
     68         self.fs = FileSystem()
     69         self.original_cwd = self.fs.getcwd()
     70 
     71     def tearDown(self):
     72         self._chdir(self.original_cwd)
     73 
     74     def _join(self, *comps):
     75         return self.fs.join(*comps)
     76 
     77     def _chdir(self, path):
     78         self.fs.chdir(path)
     79 
     80     def _mkdir(self, path):
     81         assert not self.fs.exists(path)
     82         self.fs.maybe_make_directory(path)
     83 
     84     def _mkdtemp(self, **kwargs):
     85         return str(self.fs.mkdtemp(**kwargs))
     86 
     87     def _remove(self, path):
     88         self.fs.remove(path)
     89 
     90     def _rmtree(self, path):
     91         self.fs.rmtree(path)
     92 
     93     def _run(self, *args, **kwargs):
     94         return self.executive.run_command(*args, **kwargs)
     95 
     96     def _run_silent(self, args, **kwargs):
     97         self.executive.run_and_throw_if_fail(args, quiet=True, **kwargs)
     98 
     99     def _write_text_file(self, path, contents):
    100         self.fs.write_text_file(path, contents)
    101 
    102     def _write_binary_file(self, path, contents):
    103         self.fs.write_binary_file(path, contents)
    104 
    105     def _make_diff(self, command, *args):
    106         # We use this wrapper to disable output decoding. diffs should be treated as
    107         # binary files since they may include text files of multiple differnet encodings.
    108         return self._run([command, "diff"] + list(args), decode_output=False)
    109 
    110     def _svn_diff(self, *args):
    111         return self._make_diff("svn", *args)
    112 
    113     def _git_diff(self, *args):
    114         return self._make_diff("git", *args)
    115 
    116     def _svn_add(self, path):
    117         self._run(["svn", "add", path])
    118 
    119     def _svn_commit(self, message):
    120         self._run(["svn", "commit", "--quiet", "--message", message])
    121 
    122     # This is a hot function since it's invoked by unittest before calling each test_ method in SVNTest and
    123     # GitTest. We create a mock SVN repo once and then perform an SVN checkout from a filesystem copy of
    124     # it since it's expensive to create the mock repo.
    125     def _set_up_svn_checkout(self):
    126         global cached_svn_repo_path
    127         global original_cwd
    128         if not cached_svn_repo_path:
    129             cached_svn_repo_path = self._set_up_svn_repo()
    130             original_cwd = self.original_cwd
    131 
    132         self.temp_directory = self._mkdtemp(suffix="svn_test")
    133         self.svn_repo_path = self._join(self.temp_directory, "repo")
    134         self.svn_repo_url = "file://%s" % self.svn_repo_path
    135         self.svn_checkout_path = self._join(self.temp_directory, "checkout")
    136         shutil.copytree(cached_svn_repo_path, self.svn_repo_path)
    137         self._run(['svn', 'checkout', '--quiet', self.svn_repo_url + "/trunk", self.svn_checkout_path])
    138 
    139     def _set_up_svn_repo(self):
    140         svn_repo_path = self._mkdtemp(suffix="svn_test_repo")
    141         svn_repo_url = "file://%s" % svn_repo_path  # Not sure this will work on windows
    142         # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
    143         # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
    144         self._run(['svnadmin', 'create', '--pre-1.5-compatible', svn_repo_path])
    145 
    146         # Create a test svn checkout
    147         svn_checkout_path = self._mkdtemp(suffix="svn_test_checkout")
    148         self._run(['svn', 'checkout', '--quiet', svn_repo_url, svn_checkout_path])
    149 
    150         # Create and checkout a trunk dir to match the standard svn configuration to match git-svn's expectations
    151         self._chdir(svn_checkout_path)
    152         self._mkdir('trunk')
    153         self._svn_add('trunk')
    154         # We can add tags and branches as well if we ever need to test those.
    155         self._svn_commit('add trunk')
    156 
    157         self._rmtree(svn_checkout_path)
    158         self._chdir(self.original_cwd)
    159 
    160         self._set_up_svn_test_commits(svn_repo_url + "/trunk")
    161         return svn_repo_path
    162 
    163     def _set_up_svn_test_commits(self, svn_repo_url):
    164         svn_checkout_path = self._mkdtemp(suffix="svn_test_checkout")
    165         self._run(['svn', 'checkout', '--quiet', svn_repo_url, svn_checkout_path])
    166 
    167         # Add some test commits
    168         self._chdir(svn_checkout_path)
    169 
    170         self._write_text_file("test_file", "test1")
    171         self._svn_add("test_file")
    172         self._svn_commit("initial commit")
    173 
    174         self._write_text_file("test_file", "test1test2")
    175         # This used to be the last commit, but doing so broke
    176         # GitTest.test_apply_git_patch which use the inverse diff of the last commit.
    177         # svn-apply fails to remove directories in Git, see:
    178         # https://bugs.webkit.org/show_bug.cgi?id=34871
    179         self._mkdir("test_dir")
    180         # Slash should always be the right path separator since we use cygwin on Windows.
    181         test_file3_path = "test_dir/test_file3"
    182         self._write_text_file(test_file3_path, "third file")
    183         self._svn_add("test_dir")
    184         self._svn_commit("second commit")
    185 
    186         self._write_text_file("test_file", "test1test2test3\n")
    187         self._write_text_file("test_file2", "second file")
    188         self._svn_add("test_file2")
    189         self._svn_commit("third commit")
    190 
    191         # This 4th commit is used to make sure that our patch file handling
    192         # code correctly treats patches as binary and does not attempt to
    193         # decode them assuming they're utf-8.
    194         self._write_binary_file("test_file", u"latin1 test: \u00A0\n".encode("latin-1"))
    195         self._write_binary_file("test_file2", u"utf-8 test: \u00A0\n".encode("utf-8"))
    196         self._svn_commit("fourth commit")
    197 
    198         # svn does not seem to update after commit as I would expect.
    199         self._run(['svn', 'update'])
    200         self._rmtree(svn_checkout_path)
    201         self._chdir(self.original_cwd)
    202 
    203     def _tear_down_svn_checkout(self):
    204         self._rmtree(self.temp_directory)
    205 
    206     def _shared_test_add_recursively(self):
    207         self._mkdir("added_dir")
    208         self._write_text_file("added_dir/added_file", "new stuff")
    209         self.scm.add("added_dir/added_file")
    210         self.assertIn("added_dir/added_file", self.scm._added_files())
    211 
    212     def _shared_test_delete_recursively(self):
    213         self._mkdir("added_dir")
    214         self._write_text_file("added_dir/added_file", "new stuff")
    215         self.scm.add("added_dir/added_file")
    216         self.assertIn("added_dir/added_file", self.scm._added_files())
    217         self.scm.delete("added_dir/added_file")
    218         self.assertNotIn("added_dir", self.scm._added_files())
    219 
    220     def _shared_test_delete_recursively_or_not(self):
    221         self._mkdir("added_dir")
    222         self._write_text_file("added_dir/added_file", "new stuff")
    223         self._write_text_file("added_dir/another_added_file", "more new stuff")
    224         self.scm.add("added_dir/added_file")
    225         self.scm.add("added_dir/another_added_file")
    226         self.assertIn("added_dir/added_file", self.scm._added_files())
    227         self.assertIn("added_dir/another_added_file", self.scm._added_files())
    228         self.scm.delete("added_dir/added_file")
    229         self.assertIn("added_dir/another_added_file", self.scm._added_files())
    230 
    231     def _shared_test_exists(self, scm, commit_function):
    232         self._chdir(scm.checkout_root)
    233         self.assertFalse(scm.exists('foo.txt'))
    234         self._write_text_file('foo.txt', 'some stuff')
    235         self.assertFalse(scm.exists('foo.txt'))
    236         scm.add('foo.txt')
    237         commit_function('adding foo')
    238         self.assertTrue(scm.exists('foo.txt'))
    239         scm.delete('foo.txt')
    240         commit_function('deleting foo')
    241         self.assertFalse(scm.exists('foo.txt'))
    242 
    243     def _shared_test_move(self):
    244         self._write_text_file('added_file', 'new stuff')
    245         self.scm.add('added_file')
    246         self.scm.move('added_file', 'moved_file')
    247         self.assertIn('moved_file', self.scm._added_files())
    248 
    249     def _shared_test_move_recursive(self):
    250         self._mkdir("added_dir")
    251         self._write_text_file('added_dir/added_file', 'new stuff')
    252         self._write_text_file('added_dir/another_added_file', 'more new stuff')
    253         self.scm.add('added_dir')
    254         self.scm.move('added_dir', 'moved_dir')
    255         self.assertIn('moved_dir/added_file', self.scm._added_files())
    256         self.assertIn('moved_dir/another_added_file', self.scm._added_files())
    257 
    258 
    259 class SVNTest(SCMTestBase):
    260     def setUp(self):
    261         super(SVNTest, self).setUp()
    262         self._set_up_svn_checkout()
    263         self._chdir(self.svn_checkout_path)
    264         self.scm = detect_scm_system(self.svn_checkout_path)
    265         self.scm.svn_server_realm = None
    266 
    267     def tearDown(self):
    268         super(SVNTest, self).tearDown()
    269         self._tear_down_svn_checkout()
    270 
    271     def test_detect_scm_system_relative_url(self):
    272         scm = detect_scm_system(".")
    273         # I wanted to assert that we got the right path, but there was some
    274         # crazy magic with temp folder names that I couldn't figure out.
    275         self.assertTrue(scm.checkout_root)
    276 
    277     def test_detection(self):
    278         self.assertEqual(self.scm.display_name(), "svn")
    279         self.assertEqual(self.scm.supports_local_commits(), False)
    280 
    281     def test_add_recursively(self):
    282         self._shared_test_add_recursively()
    283 
    284     def test_delete(self):
    285         self._chdir(self.svn_checkout_path)
    286         self.scm.delete("test_file")
    287         self.assertIn("test_file", self.scm._deleted_files())
    288 
    289     def test_delete_list(self):
    290         self._chdir(self.svn_checkout_path)
    291         self.scm.delete_list(["test_file", "test_file2"])
    292         self.assertIn("test_file", self.scm._deleted_files())
    293         self.assertIn("test_file2", self.scm._deleted_files())
    294 
    295     def test_delete_recursively(self):
    296         self._shared_test_delete_recursively()
    297 
    298     def test_delete_recursively_or_not(self):
    299         self._shared_test_delete_recursively_or_not()
    300 
    301     def test_move(self):
    302         self._shared_test_move()
    303 
    304     def test_move_recursive(self):
    305         self._shared_test_move_recursive()
    306 
    307 
    308 class GitTest(SCMTestBase):
    309     def setUp(self):
    310         super(GitTest, self).setUp()
    311         self._set_up_git_checkouts()
    312 
    313     def tearDown(self):
    314         super(GitTest, self).tearDown()
    315         self._tear_down_git_checkouts()
    316 
    317     def _set_up_git_checkouts(self):
    318         """Sets up fresh git repository with one commit. Then sets up a second git repo that tracks the first one."""
    319 
    320         self.untracking_checkout_path = self._mkdtemp(suffix="git_test_checkout2")
    321         self._run(['git', 'init', self.untracking_checkout_path])
    322 
    323         self._chdir(self.untracking_checkout_path)
    324         self._write_text_file('foo_file', 'foo')
    325         self._run(['git', 'add', 'foo_file'])
    326         self._run(['git', 'commit', '-am', 'dummy commit'])
    327         self.untracking_scm = detect_scm_system(self.untracking_checkout_path)
    328 
    329         self.tracking_git_checkout_path = self._mkdtemp(suffix="git_test_checkout")
    330         self._run(['git', 'clone', '--quiet', self.untracking_checkout_path, self.tracking_git_checkout_path])
    331         self._chdir(self.tracking_git_checkout_path)
    332         self.tracking_scm = detect_scm_system(self.tracking_git_checkout_path)
    333 
    334     def _tear_down_git_checkouts(self):
    335         self._run(['rm', '-rf', self.tracking_git_checkout_path])
    336         self._run(['rm', '-rf', self.untracking_checkout_path])
    337 
    338     def test_remote_branch_ref(self):
    339         self.assertEqual(self.tracking_scm._remote_branch_ref(), 'refs/remotes/origin/master')
    340         self._chdir(self.untracking_checkout_path)
    341         self.assertRaises(ScriptError, self.untracking_scm._remote_branch_ref)
    342 
    343     def test_multiple_remotes(self):
    344         self._run(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote1'])
    345         self._run(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote2'])
    346         self.assertEqual(self.tracking_scm._remote_branch_ref(), 'remote1')
    347 
    348     def test_create_patch(self):
    349         self._write_text_file('test_file_commit1', 'contents')
    350         self._run(['git', 'add', 'test_file_commit1'])
    351         scm = self.tracking_scm
    352         scm.commit_locally_with_message('message')
    353 
    354         patch = scm.create_patch()
    355         self.assertNotRegexpMatches(patch, r'Subversion Revision:')
    356 
    357     def test_exists(self):
    358         scm = self.untracking_scm
    359         self._shared_test_exists(scm, scm.commit_locally_with_message)
    360 
    361     def test_rename_files(self):
    362         scm = self.tracking_scm
    363         scm.move('foo_file', 'bar_file')
    364         scm.commit_locally_with_message('message')
    365 
    366 
    367 class GitSVNTest(SCMTestBase):
    368     def setUp(self):
    369         super(GitSVNTest, self).setUp()
    370         self._set_up_svn_checkout()
    371         self._set_up_gitsvn_checkout()
    372         self.scm = detect_scm_system(self.git_checkout_path)
    373         self.scm.svn_server_realm = None
    374 
    375     def tearDown(self):
    376         super(GitSVNTest, self).tearDown()
    377         self._tear_down_svn_checkout()
    378         self._tear_down_gitsvn_checkout()
    379 
    380     def _set_up_gitsvn_checkout(self):
    381         self.git_checkout_path = self._mkdtemp(suffix="git_test_checkout")
    382         # --quiet doesn't make git svn silent
    383         self._run_silent(['git', 'svn', 'clone', '-T', 'trunk', self.svn_repo_url, self.git_checkout_path])
    384         self._chdir(self.git_checkout_path)
    385         self.git_v2 = self._run(['git', '--version']).startswith('git version 2')
    386         if self.git_v2:
    387             # The semantics of 'git svn clone -T' changed in v2 (apparently), so the branch names are different.
    388             # This works around it, for compatibility w/ v1.
    389             self._run_silent(['git', 'branch', 'trunk', 'origin/trunk'])
    390 
    391     def _tear_down_gitsvn_checkout(self):
    392         self._rmtree(self.git_checkout_path)
    393 
    394     def test_detection(self):
    395         self.assertEqual(self.scm.display_name(), "git")
    396         self.assertEqual(self.scm.supports_local_commits(), True)
    397 
    398     def test_read_git_config(self):
    399         key = 'test.git-config'
    400         value = 'git-config value'
    401         self._run(['git', 'config', key, value])
    402         self.assertEqual(self.scm.read_git_config(key), value)
    403 
    404     def test_local_commits(self):
    405         test_file = self._join(self.git_checkout_path, 'test_file')
    406         self._write_text_file(test_file, 'foo')
    407         self._run(['git', 'commit', '-a', '-m', 'local commit'])
    408 
    409         self.assertEqual(len(self.scm._local_commits()), 1)
    410 
    411     def test_discard_local_commits(self):
    412         test_file = self._join(self.git_checkout_path, 'test_file')
    413         self._write_text_file(test_file, 'foo')
    414         self._run(['git', 'commit', '-a', '-m', 'local commit'])
    415 
    416         self.assertEqual(len(self.scm._local_commits()), 1)
    417         self.scm._discard_local_commits()
    418         self.assertEqual(len(self.scm._local_commits()), 0)
    419 
    420     def test_delete_branch(self):
    421         new_branch = 'foo'
    422 
    423         self._run(['git', 'checkout', '-b', new_branch])
    424         self.assertEqual(self._run(['git', 'symbolic-ref', 'HEAD']).strip(), 'refs/heads/' + new_branch)
    425 
    426         self._run(['git', 'checkout', '-b', 'bar'])
    427         self.scm.delete_branch(new_branch)
    428 
    429         self.assertNotRegexpMatches(self._run(['git', 'branch']), r'foo')
    430 
    431     def test_rebase_in_progress(self):
    432         svn_test_file = self._join(self.svn_checkout_path, 'test_file')
    433         self._write_text_file(svn_test_file, "svn_checkout")
    434         self._run(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
    435 
    436         git_test_file = self._join(self.git_checkout_path, 'test_file')
    437         self._write_text_file(git_test_file, "git_checkout")
    438         self._run(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
    439 
    440         # Should fail due to a conflict leaving us mid-rebase.
    441         # we use self._run_slient because --quiet doesn't actually make git svn silent.
    442         self.assertRaises(ScriptError, self._run_silent, ['git', 'svn', '--quiet', 'rebase'])
    443 
    444         self.assertTrue(self.scm._rebase_in_progress())
    445 
    446         # Make sure our cleanup works.
    447         self.scm._discard_working_directory_changes()
    448         self.assertFalse(self.scm._rebase_in_progress())
    449 
    450         # Make sure cleanup doesn't throw when no rebase is in progress.
    451         self.scm._discard_working_directory_changes()
    452 
    453     def _local_commit(self, filename, contents, message):
    454         self._write_text_file(filename, contents)
    455         self._run(['git', 'add', filename])
    456         self.scm.commit_locally_with_message(message)
    457 
    458     def _one_local_commit(self):
    459         self._local_commit('test_file_commit1', 'more test content', 'another test commit')
    460 
    461     def _one_local_commit_plus_working_copy_changes(self):
    462         self._one_local_commit()
    463         self._write_text_file('test_file_commit2', 'still more test content')
    464         self._run(['git', 'add', 'test_file_commit2'])
    465 
    466     def _second_local_commit(self):
    467         self._local_commit('test_file_commit2', 'still more test content', 'yet another test commit')
    468 
    469     def _two_local_commits(self):
    470         self._one_local_commit()
    471         self._second_local_commit()
    472 
    473     def _three_local_commits(self):
    474         self._local_commit('test_file_commit0', 'more test content', 'another test commit')
    475         self._two_local_commits()
    476 
    477     def test_locally_commit_all_working_copy_changes(self):
    478         self._local_commit('test_file', 'test content', 'test commit')
    479         self._write_text_file('test_file', 'changed test content')
    480         self.assertTrue(self.scm.has_working_directory_changes())
    481         self.scm.commit_locally_with_message('all working copy changes')
    482         self.assertFalse(self.scm.has_working_directory_changes())
    483 
    484     def test_locally_commit_no_working_copy_changes(self):
    485         self._local_commit('test_file', 'test content', 'test commit')
    486         self._write_text_file('test_file', 'changed test content')
    487         self.assertTrue(self.scm.has_working_directory_changes())
    488         self.assertRaises(ScriptError, self.scm.commit_locally_with_message, 'no working copy changes', False)
    489 
    490     def _test_upstream_branch(self):
    491         self._run(['git', 'checkout', '-t', '-b', 'my-branch'])
    492         self._run(['git', 'checkout', '-t', '-b', 'my-second-branch'])
    493         self.assertEqual(self.scm._upstream_branch(), 'my-branch')
    494 
    495     def test_remote_branch_ref(self):
    496         remote_branch_ref = self.scm._remote_branch_ref()
    497         if self.git_v2:
    498             self.assertEqual(remote_branch_ref, 'refs/remotes/origin/trunk')
    499         else:
    500             self.assertEqual(remote_branch_ref, 'refs/remotes/trunk')
    501 
    502     def test_create_patch_local_plus_working_copy(self):
    503         self._one_local_commit_plus_working_copy_changes()
    504         patch = self.scm.create_patch()
    505         self.assertRegexpMatches(patch, r'test_file_commit1')
    506         self.assertRegexpMatches(patch, r'test_file_commit2')
    507 
    508     def test_create_patch(self):
    509         self._one_local_commit_plus_working_copy_changes()
    510         patch = self.scm.create_patch()
    511         self.assertRegexpMatches(patch, r'test_file_commit2')
    512         self.assertRegexpMatches(patch, r'test_file_commit1')
    513         self.assertRegexpMatches(patch, r'Subversion Revision: 5')
    514 
    515     def test_create_patch_after_merge(self):
    516         self._run(['git', 'checkout', '-b', 'dummy-branch', 'trunk~3'])
    517         self._one_local_commit()
    518         self._run(['git', 'merge', 'trunk'])
    519 
    520         patch = self.scm.create_patch()
    521         self.assertRegexpMatches(patch, r'test_file_commit1')
    522         self.assertRegexpMatches(patch, r'Subversion Revision: 5')
    523 
    524     def test_create_patch_with_changed_files(self):
    525         self._one_local_commit_plus_working_copy_changes()
    526         patch = self.scm.create_patch(changed_files=['test_file_commit2'])
    527         self.assertRegexpMatches(patch, r'test_file_commit2')
    528 
    529     def test_create_patch_with_rm_and_changed_files(self):
    530         self._one_local_commit_plus_working_copy_changes()
    531         self._remove('test_file_commit1')
    532         patch = self.scm.create_patch()
    533         patch_with_changed_files = self.scm.create_patch(changed_files=['test_file_commit1', 'test_file_commit2'])
    534         self.assertEqual(patch, patch_with_changed_files)
    535 
    536     def test_create_patch_git_commit(self):
    537         self._two_local_commits()
    538         patch = self.scm.create_patch(git_commit="HEAD^")
    539         self.assertRegexpMatches(patch, r'test_file_commit1')
    540         self.assertNotRegexpMatches(patch, r'test_file_commit2')
    541 
    542     def test_create_patch_git_commit_range(self):
    543         self._three_local_commits()
    544         patch = self.scm.create_patch(git_commit="HEAD~2..HEAD")
    545         self.assertNotRegexpMatches(patch, r'test_file_commit0')
    546         self.assertRegexpMatches(patch, r'test_file_commit2')
    547         self.assertRegexpMatches(patch, r'test_file_commit1')
    548 
    549     def test_create_patch_working_copy_only(self):
    550         self._one_local_commit_plus_working_copy_changes()
    551         patch = self.scm.create_patch(git_commit="HEAD....")
    552         self.assertNotRegexpMatches(patch, r'test_file_commit1')
    553         self.assertRegexpMatches(patch, r'test_file_commit2')
    554 
    555     def test_create_patch_multiple_local_commits(self):
    556         self._two_local_commits()
    557         patch = self.scm.create_patch()
    558         self.assertRegexpMatches(patch, r'test_file_commit2')
    559         self.assertRegexpMatches(patch, r'test_file_commit1')
    560 
    561     def test_create_patch_not_synced(self):
    562         self._run(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
    563         self._two_local_commits()
    564         patch = self.scm.create_patch()
    565         self.assertNotRegexpMatches(patch, r'test_file2')
    566         self.assertRegexpMatches(patch, r'test_file_commit2')
    567         self.assertRegexpMatches(patch, r'test_file_commit1')
    568 
    569     def test_create_binary_patch(self):
    570         # Create a git binary patch and check the contents.
    571         test_file_name = 'binary_file'
    572         test_file_path = self.fs.join(self.git_checkout_path, test_file_name)
    573         file_contents = ''.join(map(chr, range(256)))
    574         self._write_binary_file(test_file_path, file_contents)
    575         self._run(['git', 'add', test_file_name])
    576         patch = self.scm.create_patch()
    577         self.assertRegexpMatches(patch, r'\nliteral 0\n')
    578         self.assertRegexpMatches(patch, r'\nliteral 256\n')
    579 
    580         # Check if we can create a patch from a local commit.
    581         self._write_binary_file(test_file_path, file_contents)
    582         self._run(['git', 'add', test_file_name])
    583         self._run(['git', 'commit', '-m', 'binary diff'])
    584 
    585         patch_from_local_commit = self.scm.create_patch('HEAD')
    586         self.assertRegexpMatches(patch_from_local_commit, r'\nliteral 0\n')
    587         self.assertRegexpMatches(patch_from_local_commit, r'\nliteral 256\n')
    588 
    589 
    590     def test_changed_files_local_plus_working_copy(self):
    591         self._one_local_commit_plus_working_copy_changes()
    592         files = self.scm.changed_files()
    593         self.assertIn('test_file_commit1', files)
    594         self.assertIn('test_file_commit2', files)
    595 
    596         # working copy should *not* be in the list.
    597         files = self.scm.changed_files('trunk..')
    598         self.assertIn('test_file_commit1', files)
    599         self.assertNotIn('test_file_commit2', files)
    600 
    601         # working copy *should* be in the list.
    602         files = self.scm.changed_files('trunk....')
    603         self.assertIn('test_file_commit1', files)
    604         self.assertIn('test_file_commit2', files)
    605 
    606     def test_changed_files_git_commit(self):
    607         self._two_local_commits()
    608         files = self.scm.changed_files(git_commit="HEAD^")
    609         self.assertIn('test_file_commit1', files)
    610         self.assertNotIn('test_file_commit2', files)
    611 
    612     def test_changed_files_git_commit_range(self):
    613         self._three_local_commits()
    614         files = self.scm.changed_files(git_commit="HEAD~2..HEAD")
    615         self.assertNotIn('test_file_commit0', files)
    616         self.assertIn('test_file_commit1', files)
    617         self.assertIn('test_file_commit2', files)
    618 
    619     def test_changed_files_working_copy_only(self):
    620         self._one_local_commit_plus_working_copy_changes()
    621         files = self.scm.changed_files(git_commit="HEAD....")
    622         self.assertNotIn('test_file_commit1', files)
    623         self.assertIn('test_file_commit2', files)
    624 
    625     def test_changed_files_multiple_local_commits(self):
    626         self._two_local_commits()
    627         files = self.scm.changed_files()
    628         self.assertIn('test_file_commit2', files)
    629         self.assertIn('test_file_commit1', files)
    630 
    631     def test_changed_files_not_synced(self):
    632         self._run(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
    633         self._two_local_commits()
    634         files = self.scm.changed_files()
    635         self.assertNotIn('test_file2', files)
    636         self.assertIn('test_file_commit2', files)
    637         self.assertIn('test_file_commit1', files)
    638 
    639     def test_changed_files_upstream(self):
    640         self._run(['git', 'checkout', '-t', '-b', 'my-branch'])
    641         self._one_local_commit()
    642         self._run(['git', 'checkout', '-t', '-b', 'my-second-branch'])
    643         self._second_local_commit()
    644         self._write_text_file('test_file_commit0', 'more test content')
    645         self._run(['git', 'add', 'test_file_commit0'])
    646 
    647         # equivalent to 'git diff my-branch..HEAD, should not include working changes
    648         files = self.scm.changed_files(git_commit='UPSTREAM..')
    649         self.assertNotIn('test_file_commit1', files)
    650         self.assertIn('test_file_commit2', files)
    651         self.assertNotIn('test_file_commit0', files)
    652 
    653         # equivalent to 'git diff my-branch', *should* include working changes
    654         files = self.scm.changed_files(git_commit='UPSTREAM....')
    655         self.assertNotIn('test_file_commit1', files)
    656         self.assertIn('test_file_commit2', files)
    657         self.assertIn('test_file_commit0', files)
    658 
    659     def test_add_recursively(self):
    660         self._shared_test_add_recursively()
    661 
    662     def test_delete(self):
    663         self._two_local_commits()
    664         self.scm.delete('test_file_commit1')
    665         self.assertIn("test_file_commit1", self.scm._deleted_files())
    666 
    667     def test_delete_list(self):
    668         self._two_local_commits()
    669         self.scm.delete_list(["test_file_commit1", "test_file_commit2"])
    670         self.assertIn("test_file_commit1", self.scm._deleted_files())
    671         self.assertIn("test_file_commit2", self.scm._deleted_files())
    672 
    673     def test_delete_recursively(self):
    674         self._shared_test_delete_recursively()
    675 
    676     def test_delete_recursively_or_not(self):
    677         self._shared_test_delete_recursively_or_not()
    678 
    679     def test_move(self):
    680         self._shared_test_move()
    681 
    682     def test_move_recursive(self):
    683         self._shared_test_move_recursive()
    684 
    685     def test_exists(self):
    686         self._shared_test_exists(self.scm, self.scm.commit_locally_with_message)
    687 
    688 
    689 class GitTestWithMock(SCMTestBase):
    690     def make_scm(self):
    691         scm = Git(cwd=".", executive=MockExecutive(), filesystem=MockFileSystem())
    692         scm.read_git_config = lambda *args, **kw: "MOCKKEY:MOCKVALUE"
    693         return scm
    694 
    695     def test_timestamp_of_revision(self):
    696         scm = self.make_scm()
    697         scm.find_checkout_root = lambda path: ''
    698         scm._run_git = lambda args: 'Date: 2013-02-08 08:05:49 +0000'
    699         self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-08T08:05:49Z')
    700 
    701         scm._run_git = lambda args: 'Date: 2013-02-08 01:02:03 +0130'
    702         self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-07T23:32:03Z')
    703 
    704         scm._run_git = lambda args: 'Date: 2013-02-08 01:55:21 -0800'
    705         self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-08T09:55:21Z')
    706