• 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
34import unittest
35
36from webkitpy.common.system.executive import Executive, ScriptError
37from webkitpy.common.system.executive_mock import MockExecutive
38from webkitpy.common.system.filesystem import FileSystem
39from webkitpy.common.system.filesystem_mock import MockFileSystem
40from webkitpy.common.checkout.scm.detection import detect_scm_system
41from webkitpy.common.checkout.scm.git import Git, AmbiguousCommitError
42from webkitpy.common.checkout.scm.scm import SCM
43from 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).
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        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
259class 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
308class 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
367class 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
689class 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