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 
     35 from webkitpy.common.system.executive import Executive, ScriptError
     36 from webkitpy.common.system.executive_mock import MockExecutive
     37 from webkitpy.common.system.filesystem import FileSystem
     38 from webkitpy.common.system.filesystem_mock import MockFileSystem
     39 from webkitpy.common.checkout.scm.detection import detect_scm_system
     40 from webkitpy.common.checkout.scm.git import Git, AmbiguousCommitError
     41 from webkitpy.common.checkout.scm.scm import SCM
     42 from webkitpy.common.checkout.scm.svn import SVN
     43 import webkitpy.thirdparty.unittest2 as unittest
     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 
    159         self._set_up_svn_test_commits(svn_repo_url + "/trunk")
    160         return svn_repo_path
    161 
    162     def _set_up_svn_test_commits(self, svn_repo_url):
    163         svn_checkout_path = self._mkdtemp(suffix="svn_test_checkout")
    164         self._run(['svn', 'checkout', '--quiet', svn_repo_url, svn_checkout_path])
    165 
    166         # Add some test commits
    167         self._chdir(svn_checkout_path)
    168 
    169         self._write_text_file("test_file", "test1")
    170         self._svn_add("test_file")
    171         self._svn_commit("initial commit")
    172 
    173         self._write_text_file("test_file", "test1test2")
    174         # This used to be the last commit, but doing so broke
    175         # GitTest.test_apply_git_patch which use the inverse diff of the last commit.
    176         # svn-apply fails to remove directories in Git, see:
    177         # https://bugs.webkit.org/show_bug.cgi?id=34871
    178         self._mkdir("test_dir")
    179         # Slash should always be the right path separator since we use cygwin on Windows.
    180         test_file3_path = "test_dir/test_file3"
    181         self._write_text_file(test_file3_path, "third file")
    182         self._svn_add("test_dir")
    183         self._svn_commit("second commit")
    184 
    185         self._write_text_file("test_file", "test1test2test3\n")
    186         self._write_text_file("test_file2", "second file")
    187         self._svn_add("test_file2")
    188         self._svn_commit("third commit")
    189 
    190         # This 4th commit is used to make sure that our patch file handling
    191         # code correctly treats patches as binary and does not attempt to
    192         # decode them assuming they're utf-8.
    193         self._write_binary_file("test_file", u"latin1 test: \u00A0\n".encode("latin-1"))
    194         self._write_binary_file("test_file2", u"utf-8 test: \u00A0\n".encode("utf-8"))
    195         self._svn_commit("fourth commit")
    196 
    197         # svn does not seem to update after commit as I would expect.
    198         self._run(['svn', 'update'])
    199         self._rmtree(svn_checkout_path)
    200 
    201     def _tear_down_svn_checkout(self):
    202         self._rmtree(self.temp_directory)
    203 
    204     def _shared_test_add_recursively(self):
    205         self._mkdir("added_dir")
    206         self._write_text_file("added_dir/added_file", "new stuff")
    207         self.scm.add("added_dir/added_file")
    208         self.assertIn("added_dir/added_file", self.scm._added_files())
    209 
    210     def _shared_test_delete_recursively(self):
    211         self._mkdir("added_dir")
    212         self._write_text_file("added_dir/added_file", "new stuff")
    213         self.scm.add("added_dir/added_file")
    214         self.assertIn("added_dir/added_file", self.scm._added_files())
    215         self.scm.delete("added_dir/added_file")
    216         self.assertNotIn("added_dir", self.scm._added_files())
    217 
    218     def _shared_test_delete_recursively_or_not(self):
    219         self._mkdir("added_dir")
    220         self._write_text_file("added_dir/added_file", "new stuff")
    221         self._write_text_file("added_dir/another_added_file", "more new stuff")
    222         self.scm.add("added_dir/added_file")
    223         self.scm.add("added_dir/another_added_file")
    224         self.assertIn("added_dir/added_file", self.scm._added_files())
    225         self.assertIn("added_dir/another_added_file", self.scm._added_files())
    226         self.scm.delete("added_dir/added_file")
    227         self.assertIn("added_dir/another_added_file", self.scm._added_files())
    228 
    229     def _shared_test_exists(self, scm, commit_function):
    230         self._chdir(scm.checkout_root)
    231         self.assertFalse(scm.exists('foo.txt'))
    232         self._write_text_file('foo.txt', 'some stuff')
    233         self.assertFalse(scm.exists('foo.txt'))
    234         scm.add('foo.txt')
    235         commit_function('adding foo')
    236         self.assertTrue(scm.exists('foo.txt'))
    237         scm.delete('foo.txt')
    238         commit_function('deleting foo')
    239         self.assertFalse(scm.exists('foo.txt'))
    240 
    241     def _shared_test_move(self):
    242         self._write_text_file('added_file', 'new stuff')
    243         self.scm.add('added_file')
    244         self.scm.move('added_file', 'moved_file')
    245         self.assertIn('moved_file', self.scm._added_files())
    246 
    247     def _shared_test_move_recursive(self):
    248         self._mkdir("added_dir")
    249         self._write_text_file('added_dir/added_file', 'new stuff')
    250         self._write_text_file('added_dir/another_added_file', 'more new stuff')
    251         self.scm.add('added_dir')
    252         self.scm.move('added_dir', 'moved_dir')
    253         self.assertIn('moved_dir/added_file', self.scm._added_files())
    254         self.assertIn('moved_dir/another_added_file', self.scm._added_files())
    255 
    256 
    257 class SVNTest(SCMTestBase):
    258     def setUp(self):
    259         super(SVNTest, self).setUp()
    260         self._set_up_svn_checkout()
    261         self._chdir(self.svn_checkout_path)
    262         self.scm = detect_scm_system(self.svn_checkout_path)
    263         self.scm.svn_server_realm = None
    264 
    265     def tearDown(self):
    266         super(SVNTest, self).tearDown()
    267         self._tear_down_svn_checkout()
    268 
    269     def test_detect_scm_system_relative_url(self):
    270         scm = detect_scm_system(".")
    271         # I wanted to assert that we got the right path, but there was some
    272         # crazy magic with temp folder names that I couldn't figure out.
    273         self.assertTrue(scm.checkout_root)
    274 
    275     def test_detection(self):
    276         self.assertEqual(self.scm.display_name(), "svn")
    277         self.assertEqual(self.scm.supports_local_commits(), False)
    278 
    279     def test_add_recursively(self):
    280         self._shared_test_add_recursively()
    281 
    282     def test_delete(self):
    283         self._chdir(self.svn_checkout_path)
    284         self.scm.delete("test_file")
    285         self.assertIn("test_file", self.scm._deleted_files())
    286 
    287     def test_delete_list(self):
    288         self._chdir(self.svn_checkout_path)
    289         self.scm.delete_list(["test_file", "test_file2"])
    290         self.assertIn("test_file", self.scm._deleted_files())
    291         self.assertIn("test_file2", self.scm._deleted_files())
    292 
    293     def test_delete_recursively(self):
    294         self._shared_test_delete_recursively()
    295 
    296     def test_delete_recursively_or_not(self):
    297         self._shared_test_delete_recursively_or_not()
    298 
    299     def test_move(self):
    300         self._shared_test_move()
    301 
    302     def test_move_recursive(self):
    303         self._shared_test_move_recursive()
    304 
    305 
    306 class GitTest(SCMTestBase):
    307     def setUp(self):
    308         super(GitTest, self).setUp()
    309         self._set_up_git_checkouts()
    310 
    311     def tearDown(self):
    312         super(GitTest, self).tearDown()
    313         self._tear_down_git_checkouts()
    314 
    315     def _set_up_git_checkouts(self):
    316         """Sets up fresh git repository with one commit. Then sets up a second git repo that tracks the first one."""
    317 
    318         self.untracking_checkout_path = self._mkdtemp(suffix="git_test_checkout2")
    319         self._run(['git', 'init', self.untracking_checkout_path])
    320 
    321         self._chdir(self.untracking_checkout_path)
    322         self._write_text_file('foo_file', 'foo')
    323         self._run(['git', 'add', 'foo_file'])
    324         self._run(['git', 'commit', '-am', 'dummy commit'])
    325         self.untracking_scm = detect_scm_system(self.untracking_checkout_path)
    326 
    327         self.tracking_git_checkout_path = self._mkdtemp(suffix="git_test_checkout")
    328         self._run(['git', 'clone', '--quiet', self.untracking_checkout_path, self.tracking_git_checkout_path])
    329         self._chdir(self.tracking_git_checkout_path)
    330         self.tracking_scm = detect_scm_system(self.tracking_git_checkout_path)
    331 
    332     def _tear_down_git_checkouts(self):
    333         self._run(['rm', '-rf', self.tracking_git_checkout_path])
    334         self._run(['rm', '-rf', self.untracking_checkout_path])
    335 
    336     def test_remote_branch_ref(self):
    337         self.assertEqual(self.tracking_scm._remote_branch_ref(), 'refs/remotes/origin/master')
    338         self._chdir(self.untracking_checkout_path)
    339         self.assertRaises(ScriptError, self.untracking_scm._remote_branch_ref)
    340 
    341     def test_multiple_remotes(self):
    342         self._run(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote1'])
    343         self._run(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote2'])
    344         self.assertEqual(self.tracking_scm._remote_branch_ref(), 'remote1')
    345 
    346     def test_create_patch(self):
    347         self._write_text_file('test_file_commit1', 'contents')
    348         self._run(['git', 'add', 'test_file_commit1'])
    349         scm = self.tracking_scm
    350         scm.commit_locally_with_message('message')
    351 
    352         patch = scm.create_patch()
    353         self.assertNotRegexpMatches(patch, r'Subversion Revision:')
    354 
    355     def test_exists(self):
    356         scm = self.untracking_scm
    357         self._shared_test_exists(scm, scm.commit_locally_with_message)
    358 
    359     def test_rename_files(self):
    360         scm = self.tracking_scm
    361         scm.move('foo_file', 'bar_file')
    362         scm.commit_locally_with_message('message')
    363 
    364 
    365 class GitSVNTest(SCMTestBase):
    366     def setUp(self):
    367         super(GitSVNTest, self).setUp()
    368         self._set_up_svn_checkout()
    369         self._set_up_gitsvn_checkout()
    370         self.scm = detect_scm_system(self.git_checkout_path)
    371         self.scm.svn_server_realm = None
    372 
    373     def tearDown(self):
    374         super(GitSVNTest, self).tearDown()
    375         self._tear_down_svn_checkout()
    376         self._tear_down_gitsvn_checkout()
    377 
    378     def _set_up_gitsvn_checkout(self):
    379         self.git_checkout_path = self._mkdtemp(suffix="git_test_checkout")
    380         # --quiet doesn't make git svn silent
    381         self._run_silent(['git', 'svn', 'clone', '-T', 'trunk', self.svn_repo_url, self.git_checkout_path])
    382         self._chdir(self.git_checkout_path)
    383 
    384     def _tear_down_gitsvn_checkout(self):
    385         self._rmtree(self.git_checkout_path)
    386 
    387     def test_detection(self):
    388         self.assertEqual(self.scm.display_name(), "git")
    389         self.assertEqual(self.scm.supports_local_commits(), True)
    390 
    391     def test_read_git_config(self):
    392         key = 'test.git-config'
    393         value = 'git-config value'
    394         self._run(['git', 'config', key, value])
    395         self.assertEqual(self.scm.read_git_config(key), value)
    396 
    397     def test_local_commits(self):
    398         test_file = self._join(self.git_checkout_path, 'test_file')
    399         self._write_text_file(test_file, 'foo')
    400         self._run(['git', 'commit', '-a', '-m', 'local commit'])
    401 
    402         self.assertEqual(len(self.scm._local_commits()), 1)
    403 
    404     def test_discard_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         self.scm._discard_local_commits()
    411         self.assertEqual(len(self.scm._local_commits()), 0)
    412 
    413     def test_delete_branch(self):
    414         new_branch = 'foo'
    415 
    416         self._run(['git', 'checkout', '-b', new_branch])
    417         self.assertEqual(self._run(['git', 'symbolic-ref', 'HEAD']).strip(), 'refs/heads/' + new_branch)
    418 
    419         self._run(['git', 'checkout', '-b', 'bar'])
    420         self.scm.delete_branch(new_branch)
    421 
    422         self.assertNotRegexpMatches(self._run(['git', 'branch']), r'foo')
    423 
    424     def test_rebase_in_progress(self):
    425         svn_test_file = self._join(self.svn_checkout_path, 'test_file')
    426         self._write_text_file(svn_test_file, "svn_checkout")
    427         self._run(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
    428 
    429         git_test_file = self._join(self.git_checkout_path, 'test_file')
    430         self._write_text_file(git_test_file, "git_checkout")
    431         self._run(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
    432 
    433         # Should fail due to a conflict leaving us mid-rebase.
    434         # we use self._run_slient because --quiet doesn't actually make git svn silent.
    435         self.assertRaises(ScriptError, self._run_silent, ['git', 'svn', '--quiet', 'rebase'])
    436 
    437         self.assertTrue(self.scm._rebase_in_progress())
    438 
    439         # Make sure our cleanup works.
    440         self.scm._discard_working_directory_changes()
    441         self.assertFalse(self.scm._rebase_in_progress())
    442 
    443         # Make sure cleanup doesn't throw when no rebase is in progress.
    444         self.scm._discard_working_directory_changes()
    445 
    446     def _local_commit(self, filename, contents, message):
    447         self._write_text_file(filename, contents)
    448         self._run(['git', 'add', filename])
    449         self.scm.commit_locally_with_message(message)
    450 
    451     def _one_local_commit(self):
    452         self._local_commit('test_file_commit1', 'more test content', 'another test commit')
    453 
    454     def _one_local_commit_plus_working_copy_changes(self):
    455         self._one_local_commit()
    456         self._write_text_file('test_file_commit2', 'still more test content')
    457         self._run(['git', 'add', 'test_file_commit2'])
    458 
    459     def _second_local_commit(self):
    460         self._local_commit('test_file_commit2', 'still more test content', 'yet another test commit')
    461 
    462     def _two_local_commits(self):
    463         self._one_local_commit()
    464         self._second_local_commit()
    465 
    466     def _three_local_commits(self):
    467         self._local_commit('test_file_commit0', 'more test content', 'another test commit')
    468         self._two_local_commits()
    469 
    470     def test_locally_commit_all_working_copy_changes(self):
    471         self._local_commit('test_file', 'test content', 'test commit')
    472         self._write_text_file('test_file', 'changed test content')
    473         self.assertTrue(self.scm.has_working_directory_changes())
    474         self.scm.commit_locally_with_message('all working copy changes')
    475         self.assertFalse(self.scm.has_working_directory_changes())
    476 
    477     def test_locally_commit_no_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.assertRaises(ScriptError, self.scm.commit_locally_with_message, 'no working copy changes', False)
    482 
    483     def _test_upstream_branch(self):
    484         self._run(['git', 'checkout', '-t', '-b', 'my-branch'])
    485         self._run(['git', 'checkout', '-t', '-b', 'my-second-branch'])
    486         self.assertEqual(self.scm._upstream_branch(), 'my-branch')
    487 
    488     def test_remote_branch_ref(self):
    489         self.assertEqual(self.scm._remote_branch_ref(), 'refs/remotes/trunk')
    490 
    491     def test_create_patch_local_plus_working_copy(self):
    492         self._one_local_commit_plus_working_copy_changes()
    493         patch = self.scm.create_patch()
    494         self.assertRegexpMatches(patch, r'test_file_commit1')
    495         self.assertRegexpMatches(patch, r'test_file_commit2')
    496 
    497     def test_create_patch(self):
    498         self._one_local_commit_plus_working_copy_changes()
    499         patch = self.scm.create_patch()
    500         self.assertRegexpMatches(patch, r'test_file_commit2')
    501         self.assertRegexpMatches(patch, r'test_file_commit1')
    502         self.assertRegexpMatches(patch, r'Subversion Revision: 5')
    503 
    504     def test_create_patch_after_merge(self):
    505         self._run(['git', 'checkout', '-b', 'dummy-branch', 'trunk~3'])
    506         self._one_local_commit()
    507         self._run(['git', 'merge', 'trunk'])
    508 
    509         patch = self.scm.create_patch()
    510         self.assertRegexpMatches(patch, r'test_file_commit1')
    511         self.assertRegexpMatches(patch, r'Subversion Revision: 5')
    512 
    513     def test_create_patch_with_changed_files(self):
    514         self._one_local_commit_plus_working_copy_changes()
    515         patch = self.scm.create_patch(changed_files=['test_file_commit2'])
    516         self.assertRegexpMatches(patch, r'test_file_commit2')
    517 
    518     def test_create_patch_with_rm_and_changed_files(self):
    519         self._one_local_commit_plus_working_copy_changes()
    520         self._remove('test_file_commit1')
    521         patch = self.scm.create_patch()
    522         patch_with_changed_files = self.scm.create_patch(changed_files=['test_file_commit1', 'test_file_commit2'])
    523         self.assertEqual(patch, patch_with_changed_files)
    524 
    525     def test_create_patch_git_commit(self):
    526         self._two_local_commits()
    527         patch = self.scm.create_patch(git_commit="HEAD^")
    528         self.assertRegexpMatches(patch, r'test_file_commit1')
    529         self.assertNotRegexpMatches(patch, r'test_file_commit2')
    530 
    531     def test_create_patch_git_commit_range(self):
    532         self._three_local_commits()
    533         patch = self.scm.create_patch(git_commit="HEAD~2..HEAD")
    534         self.assertNotRegexpMatches(patch, r'test_file_commit0')
    535         self.assertRegexpMatches(patch, r'test_file_commit2')
    536         self.assertRegexpMatches(patch, r'test_file_commit1')
    537 
    538     def test_create_patch_working_copy_only(self):
    539         self._one_local_commit_plus_working_copy_changes()
    540         patch = self.scm.create_patch(git_commit="HEAD....")
    541         self.assertNotRegexpMatches(patch, r'test_file_commit1')
    542         self.assertRegexpMatches(patch, r'test_file_commit2')
    543 
    544     def test_create_patch_multiple_local_commits(self):
    545         self._two_local_commits()
    546         patch = self.scm.create_patch()
    547         self.assertRegexpMatches(patch, r'test_file_commit2')
    548         self.assertRegexpMatches(patch, r'test_file_commit1')
    549 
    550     def test_create_patch_not_synced(self):
    551         self._run(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
    552         self._two_local_commits()
    553         patch = self.scm.create_patch()
    554         self.assertNotRegexpMatches(patch, r'test_file2')
    555         self.assertRegexpMatches(patch, r'test_file_commit2')
    556         self.assertRegexpMatches(patch, r'test_file_commit1')
    557 
    558     def test_create_binary_patch(self):
    559         # Create a git binary patch and check the contents.
    560         test_file_name = 'binary_file'
    561         test_file_path = self.fs.join(self.git_checkout_path, test_file_name)
    562         file_contents = ''.join(map(chr, range(256)))
    563         self._write_binary_file(test_file_path, file_contents)
    564         self._run(['git', 'add', test_file_name])
    565         patch = self.scm.create_patch()
    566         self.assertRegexpMatches(patch, r'\nliteral 0\n')
    567         self.assertRegexpMatches(patch, r'\nliteral 256\n')
    568 
    569         # Check if we can create a patch from a local commit.
    570         self._write_binary_file(test_file_path, file_contents)
    571         self._run(['git', 'add', test_file_name])
    572         self._run(['git', 'commit', '-m', 'binary diff'])
    573 
    574         patch_from_local_commit = self.scm.create_patch('HEAD')
    575         self.assertRegexpMatches(patch_from_local_commit, r'\nliteral 0\n')
    576         self.assertRegexpMatches(patch_from_local_commit, r'\nliteral 256\n')
    577 
    578 
    579     def test_changed_files_local_plus_working_copy(self):
    580         self._one_local_commit_plus_working_copy_changes()
    581         files = self.scm.changed_files()
    582         self.assertIn('test_file_commit1', files)
    583         self.assertIn('test_file_commit2', files)
    584 
    585         # working copy should *not* be in the list.
    586         files = self.scm.changed_files('trunk..')
    587         self.assertIn('test_file_commit1', files)
    588         self.assertNotIn('test_file_commit2', files)
    589 
    590         # working copy *should* be in the list.
    591         files = self.scm.changed_files('trunk....')
    592         self.assertIn('test_file_commit1', files)
    593         self.assertIn('test_file_commit2', files)
    594 
    595     def test_changed_files_git_commit(self):
    596         self._two_local_commits()
    597         files = self.scm.changed_files(git_commit="HEAD^")
    598         self.assertIn('test_file_commit1', files)
    599         self.assertNotIn('test_file_commit2', files)
    600 
    601     def test_changed_files_git_commit_range(self):
    602         self._three_local_commits()
    603         files = self.scm.changed_files(git_commit="HEAD~2..HEAD")
    604         self.assertNotIn('test_file_commit0', files)
    605         self.assertIn('test_file_commit1', files)
    606         self.assertIn('test_file_commit2', files)
    607 
    608     def test_changed_files_working_copy_only(self):
    609         self._one_local_commit_plus_working_copy_changes()
    610         files = self.scm.changed_files(git_commit="HEAD....")
    611         self.assertNotIn('test_file_commit1', files)
    612         self.assertIn('test_file_commit2', files)
    613 
    614     def test_changed_files_multiple_local_commits(self):
    615         self._two_local_commits()
    616         files = self.scm.changed_files()
    617         self.assertIn('test_file_commit2', files)
    618         self.assertIn('test_file_commit1', files)
    619 
    620     def test_changed_files_not_synced(self):
    621         self._run(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
    622         self._two_local_commits()
    623         files = self.scm.changed_files()
    624         self.assertNotIn('test_file2', files)
    625         self.assertIn('test_file_commit2', files)
    626         self.assertIn('test_file_commit1', files)
    627 
    628     def test_changed_files_upstream(self):
    629         self._run(['git', 'checkout', '-t', '-b', 'my-branch'])
    630         self._one_local_commit()
    631         self._run(['git', 'checkout', '-t', '-b', 'my-second-branch'])
    632         self._second_local_commit()
    633         self._write_text_file('test_file_commit0', 'more test content')
    634         self._run(['git', 'add', 'test_file_commit0'])
    635 
    636         # equivalent to 'git diff my-branch..HEAD, should not include working changes
    637         files = self.scm.changed_files(git_commit='UPSTREAM..')
    638         self.assertNotIn('test_file_commit1', files)
    639         self.assertIn('test_file_commit2', files)
    640         self.assertNotIn('test_file_commit0', files)
    641 
    642         # equivalent to 'git diff my-branch', *should* include working changes
    643         files = self.scm.changed_files(git_commit='UPSTREAM....')
    644         self.assertNotIn('test_file_commit1', files)
    645         self.assertIn('test_file_commit2', files)
    646         self.assertIn('test_file_commit0', files)
    647 
    648     def test_add_recursively(self):
    649         self._shared_test_add_recursively()
    650 
    651     def test_delete(self):
    652         self._two_local_commits()
    653         self.scm.delete('test_file_commit1')
    654         self.assertIn("test_file_commit1", self.scm._deleted_files())
    655 
    656     def test_delete_list(self):
    657         self._two_local_commits()
    658         self.scm.delete_list(["test_file_commit1", "test_file_commit2"])
    659         self.assertIn("test_file_commit1", self.scm._deleted_files())
    660         self.assertIn("test_file_commit2", self.scm._deleted_files())
    661 
    662     def test_delete_recursively(self):
    663         self._shared_test_delete_recursively()
    664 
    665     def test_delete_recursively_or_not(self):
    666         self._shared_test_delete_recursively_or_not()
    667 
    668     def test_move(self):
    669         self._shared_test_move()
    670 
    671     def test_move_recursive(self):
    672         self._shared_test_move_recursive()
    673 
    674     def test_exists(self):
    675         self._shared_test_exists(self.scm, self.scm.commit_locally_with_message)
    676 
    677 
    678 class GitTestWithMock(SCMTestBase):
    679     def make_scm(self):
    680         scm = Git(cwd=".", executive=MockExecutive(), filesystem=MockFileSystem())
    681         scm.read_git_config = lambda *args, **kw: "MOCKKEY:MOCKVALUE"
    682         return scm
    683 
    684     def test_timestamp_of_revision(self):
    685         scm = self.make_scm()
    686         scm.find_checkout_root = lambda path: ''
    687         scm._run_git = lambda args: 'Date: 2013-02-08 08:05:49 +0000'
    688         self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-08T08:05:49Z')
    689 
    690         scm._run_git = lambda args: 'Date: 2013-02-08 01:02:03 +0130'
    691         self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-07T23:32:03Z')
    692 
    693         scm._run_git = lambda args: 'Date: 2013-02-08 01:55:21 -0800'
    694         self.assertEqual(scm.timestamp_of_revision('some-path', '12345'), '2013-02-08T09:55:21Z')
    695