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.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
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
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.
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.
36 from webkitpy.common.system.executive import Executive, ScriptError
37 from webkitpy.common.system.executive_mock import MockExecutive
38 from webkitpy.common.system.filesystem import FileSystem
39 from webkitpy.common.system.filesystem_mock import MockFileSystem
40 from webkitpy.common.checkout.scm.detection import detect_scm_system
41 from webkitpy.common.checkout.scm.git import Git, AmbiguousCommitError
42 from webkitpy.common.checkout.scm.scm import SCM
43 from webkitpy.common.checkout.scm.svn import SVN
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).
49 cached_svn_repo_path = None
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)
58 class SCMTestBase(unittest.TestCase):
59 def __init__(self, *args, **kwargs):
60 super(SCMTestBase, self).__init__(*args, **kwargs)
64 self.original_cwd = None
67 self.executive = Executive()
68 self.fs = FileSystem()
69 self.original_cwd = self.fs.getcwd()
72 self._chdir(self.original_cwd)
74 def _join(self, *comps):
75 return self.fs.join(*comps)
77 def _chdir(self, path):
80 def _mkdir(self, path):
81 assert not self.fs.exists(path)
82 self.fs.maybe_make_directory(path)
84 def _mkdtemp(self, **kwargs):
85 return str(self.fs.mkdtemp(**kwargs))
87 def _remove(self, path):
90 def _rmtree(self, path):
93 def _run(self, *args, **kwargs):
94 return self.executive.run_command(*args, **kwargs)
96 def _run_silent(self, args, **kwargs):
97 self.executive.run_and_throw_if_fail(args, quiet=True, **kwargs)
99 def _write_text_file(self, path, contents):
100 self.fs.write_text_file(path, contents)
102 def _write_binary_file(self, path, contents):
103 self.fs.write_binary_file(path, contents)
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)
110 def _svn_diff(self, *args):
111 return self._make_diff("svn", *args)
113 def _git_diff(self, *args):
114 return self._make_diff("git", *args)
116 def _svn_add(self, path):
117 self._run(["svn", "add", path])
119 def _svn_commit(self, message):
120 self._run(["svn", "commit", "--quiet", "--message", message])
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
128 if not cached_svn_repo_path:
129 cached_svn_repo_path = self._set_up_svn_repo()
130 original_cwd = self.original_cwd
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])
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])
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])
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)
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')
157 self._rmtree(svn_checkout_path)
158 self._chdir(self.original_cwd)
160 self._set_up_svn_test_commits(svn_repo_url + "/trunk")
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])
167 # Add some test commits
168 self._chdir(svn_checkout_path)
170 self._write_text_file("test_file", "test1")
171 self._svn_add("test_file")
172 self._svn_commit("initial commit")
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")
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")
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")
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)
203 def _tear_down_svn_checkout(self):
204 self._rmtree(self.temp_directory)
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())
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())
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())
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'))
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'))
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())
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())
259 class SVNTest(SCMTestBase):
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
268 super(SVNTest, self).tearDown()
269 self._tear_down_svn_checkout()
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)
277 def test_detection(self):
278 self.assertEqual(self.scm.display_name(), "svn")
279 self.assertEqual(self.scm.supports_local_commits(), False)
281 def test_add_recursively(self):
282 self._shared_test_add_recursively()
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())
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())
295 def test_delete_recursively(self):
296 self._shared_test_delete_recursively()
298 def test_delete_recursively_or_not(self):
299 self._shared_test_delete_recursively_or_not()
302 self._shared_test_move()
304 def test_move_recursive(self):
305 self._shared_test_move_recursive()
308 class GitTest(SCMTestBase):
310 super(GitTest, self).setUp()
311 self._set_up_git_checkouts()
314 super(GitTest, self).tearDown()
315 self._tear_down_git_checkouts()
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."""
320 self.untracking_checkout_path = self._mkdtemp(suffix="git_test_checkout2")
321 self._run(['git', 'init', self.untracking_checkout_path])
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)
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)
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])
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)
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')
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')
354 patch = scm.create_patch()
355 self.assertNotRegexpMatches(patch, r'Subversion Revision:')
357 def test_exists(self):
358 scm = self.untracking_scm
359 self._shared_test_exists(scm, scm.commit_locally_with_message)
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')
367 class GitSVNTest(SCMTestBase):
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
376 super(GitSVNTest, self).tearDown()
377 self._tear_down_svn_checkout()
378 self._tear_down_gitsvn_checkout()
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')
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'])
391 def _tear_down_gitsvn_checkout(self):
392 self._rmtree(self.git_checkout_path)
394 def test_detection(self):
395 self.assertEqual(self.scm.display_name(), "git")
396 self.assertEqual(self.scm.supports_local_commits(), True)
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)
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'])
409 self.assertEqual(len(self.scm._local_commits()), 1)
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'])
416 self.assertEqual(len(self.scm._local_commits()), 1)
417 self.scm._discard_local_commits()
418 self.assertEqual(len(self.scm._local_commits()), 0)
420 def test_delete_branch(self):
423 self._run(['git', 'checkout', '-b', new_branch])
424 self.assertEqual(self._run(['git', 'symbolic-ref', 'HEAD']).strip(), 'refs/heads/' + new_branch)
426 self._run(['git', 'checkout', '-b', 'bar'])
427 self.scm.delete_branch(new_branch)
429 self.assertNotRegexpMatches(self._run(['git', 'branch']), r'foo')
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)
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'])
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'])
444 self.assertTrue(self.scm._rebase_in_progress())
446 # Make sure our cleanup works.
447 self.scm._discard_working_directory_changes()
448 self.assertFalse(self.scm._rebase_in_progress())
450 # Make sure cleanup doesn't throw when no rebase is in progress.
451 self.scm._discard_working_directory_changes()
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)
458 def _one_local_commit(self):
459 self._local_commit('test_file_commit1', 'more test content', 'another test commit')
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'])
466 def _second_local_commit(self):
467 self._local_commit('test_file_commit2', 'still more test content', 'yet another test commit')
469 def _two_local_commits(self):
470 self._one_local_commit()
471 self._second_local_commit()
473 def _three_local_commits(self):
474 self._local_commit('test_file_commit0', 'more test content', 'another test commit')
475 self._two_local_commits()
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())
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)
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')
495 def test_remote_branch_ref(self):
496 remote_branch_ref = self.scm._remote_branch_ref()
498 self.assertEqual(remote_branch_ref, 'refs/remotes/origin/trunk')
500 self.assertEqual(remote_branch_ref, 'refs/remotes/trunk')
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')
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')
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'])
520 patch = self.scm.create_patch()
521 self.assertRegexpMatches(patch, r'test_file_commit1')
522 self.assertRegexpMatches(patch, r'Subversion Revision: 5')
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')
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)
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')
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')
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')
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')
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')
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')
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'])
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')
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)
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)
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)
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)
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)
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)
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)
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)
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'])
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)
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)
659 def test_add_recursively(self):
660 self._shared_test_add_recursively()
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())
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())
673 def test_delete_recursively(self):
674 self._shared_test_delete_recursively()
676 def test_delete_recursively_or_not(self):
677 self._shared_test_delete_recursively_or_not()
680 self._shared_test_move()
682 def test_move_recursive(self):
683 self._shared_test_move_recursive()
685 def test_exists(self):
686 self._shared_test_exists(self.scm, self.scm.commit_locally_with_message)
689 class GitTestWithMock(SCMTestBase):
691 scm = Git(cwd=".", executive=MockExecutive(), filesystem=MockFileSystem())
692 scm.read_git_config = lambda *args, **kw: "MOCKKEY:MOCKVALUE"
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')
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')
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')