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