• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@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
31import atexit
32import os
33import shutil
34
35from webkitpy.common.system.executive import Executive, ScriptError
36from webkitpy.common.system.executive_mock import MockExecutive
37from webkitpy.common.system.filesystem import FileSystem
38from webkitpy.common.system.filesystem_mock import MockFileSystem
39from webkitpy.common.checkout.scm.detection import detect_scm_system
40from webkitpy.common.checkout.scm.git import Git, AmbiguousCommitError
41from webkitpy.common.checkout.scm.scm import SCM
42from webkitpy.common.checkout.scm.svn import SVN
43import 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).
48original_cwd = None
49cached_svn_repo_path = None
50
51@atexit.register
52def 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
58class 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
257class 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
306class 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
365class 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
678class 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