diff options
| -rw-r--r-- | project.py | 117 | ||||
| -rw-r--r-- | tests/test_project.py | 50 |
2 files changed, 131 insertions, 36 deletions
| @@ -2781,50 +2781,95 @@ class Project(object): | |||
| 2781 | self._InitMRef() | 2781 | self._InitMRef() |
| 2782 | 2782 | ||
| 2783 | def _InitWorkTree(self, force_sync=False, submodules=False): | 2783 | def _InitWorkTree(self, force_sync=False, submodules=False): |
| 2784 | realdotgit = os.path.join(self.worktree, '.git') | 2784 | """Setup the worktree .git path. |
| 2785 | tmpdotgit = realdotgit + '.tmp' | 2785 | |
| 2786 | init_dotgit = not os.path.exists(realdotgit) | 2786 | This is the user-visible path like src/foo/.git/. |
| 2787 | if init_dotgit: | 2787 | |
| 2788 | if self.use_git_worktrees: | 2788 | With non-git-worktrees, this will be a symlink to the .repo/projects/ path. |
| 2789 | With git-worktrees, this will be a .git file using "gitdir: ..." syntax. | ||
| 2790 | |||
| 2791 | Older checkouts had .git/ directories. If we see that, migrate it. | ||
| 2792 | |||
| 2793 | This also handles changes in the manifest. Maybe this project was backed | ||
| 2794 | by "foo/bar" on the server, but now it's "new/foo/bar". We have to update | ||
| 2795 | the path we point to under .repo/projects/ to match. | ||
| 2796 | """ | ||
| 2797 | dotgit = os.path.join(self.worktree, '.git') | ||
| 2798 | |||
| 2799 | # If using an old layout style (a directory), migrate it. | ||
| 2800 | if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit): | ||
| 2801 | self._MigrateOldWorkTreeGitDir(dotgit) | ||
| 2802 | |||
| 2803 | init_dotgit = not os.path.exists(dotgit) | ||
| 2804 | if self.use_git_worktrees: | ||
| 2805 | if init_dotgit: | ||
| 2789 | self._InitGitWorktree() | 2806 | self._InitGitWorktree() |
| 2790 | self._CopyAndLinkFiles() | 2807 | self._CopyAndLinkFiles() |
| 2791 | return | ||
| 2792 | |||
| 2793 | dotgit = tmpdotgit | ||
| 2794 | platform_utils.rmtree(tmpdotgit, ignore_errors=True) | ||
| 2795 | os.makedirs(tmpdotgit) | ||
| 2796 | self._ReferenceGitDir(self.gitdir, tmpdotgit, share_refs=True, | ||
| 2797 | copy_all=False) | ||
| 2798 | else: | 2808 | else: |
| 2799 | dotgit = realdotgit | 2809 | if not init_dotgit: |
| 2810 | # See if the project has changed. | ||
| 2811 | if platform_utils.realpath(self.gitdir) != platform_utils.realpath(dotgit): | ||
| 2812 | platform_utils.remove(dotgit) | ||
| 2800 | 2813 | ||
| 2801 | try: | 2814 | if init_dotgit or not os.path.exists(dotgit): |
| 2802 | self._CheckDirReference(self.gitdir, dotgit, share_refs=True) | 2815 | os.makedirs(self.worktree, exist_ok=True) |
| 2803 | except GitError as e: | 2816 | platform_utils.symlink(os.path.relpath(self.gitdir, self.worktree), dotgit) |
| 2804 | if force_sync and not init_dotgit: | ||
| 2805 | try: | ||
| 2806 | platform_utils.rmtree(dotgit) | ||
| 2807 | return self._InitWorkTree(force_sync=False, submodules=submodules) | ||
| 2808 | except Exception: | ||
| 2809 | raise e | ||
| 2810 | raise e | ||
| 2811 | 2817 | ||
| 2812 | if init_dotgit: | 2818 | if init_dotgit: |
| 2813 | _lwrite(os.path.join(tmpdotgit, HEAD), '%s\n' % self.GetRevisionId()) | 2819 | _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId()) |
| 2814 | 2820 | ||
| 2815 | # Now that the .git dir is fully set up, move it to its final home. | 2821 | # Finish checking out the worktree. |
| 2816 | platform_utils.rename(tmpdotgit, realdotgit) | 2822 | cmd = ['read-tree', '--reset', '-u', '-v', HEAD] |
| 2823 | if GitCommand(self, cmd).Wait() != 0: | ||
| 2824 | raise GitError('Cannot initialize work tree for ' + self.name) | ||
| 2817 | 2825 | ||
| 2818 | # Finish checking out the worktree. | 2826 | if submodules: |
| 2819 | cmd = ['read-tree', '--reset', '-u'] | 2827 | self._SyncSubmodules(quiet=True) |
| 2820 | cmd.append('-v') | 2828 | self._CopyAndLinkFiles() |
| 2821 | cmd.append(HEAD) | ||
| 2822 | if GitCommand(self, cmd).Wait() != 0: | ||
| 2823 | raise GitError('Cannot initialize work tree for ' + self.name) | ||
| 2824 | 2829 | ||
| 2825 | if submodules: | 2830 | @classmethod |
| 2826 | self._SyncSubmodules(quiet=True) | 2831 | def _MigrateOldWorkTreeGitDir(cls, dotgit): |
| 2827 | self._CopyAndLinkFiles() | 2832 | """Migrate the old worktree .git/ dir style to a symlink. |
| 2833 | |||
| 2834 | This logic specifically only uses state from |dotgit| to figure out where to | ||
| 2835 | move content and not |self|. This way if the backing project also changed | ||
| 2836 | places, we only do the .git/ dir to .git symlink migration here. The path | ||
| 2837 | updates will happen independently. | ||
| 2838 | """ | ||
| 2839 | # Figure out where in .repo/projects/ it's pointing to. | ||
| 2840 | if not os.path.islink(os.path.join(dotgit, 'refs')): | ||
| 2841 | raise GitError(f'{dotgit}: unsupported checkout state') | ||
| 2842 | gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, 'refs'))) | ||
| 2843 | |||
| 2844 | # Remove known symlink paths that exist in .repo/projects/. | ||
| 2845 | KNOWN_LINKS = { | ||
| 2846 | 'config', 'description', 'hooks', 'info', 'logs', 'objects', | ||
| 2847 | 'packed-refs', 'refs', 'rr-cache', 'shallow', 'svn', | ||
| 2848 | } | ||
| 2849 | # Paths that we know will be in both, but are safe to clobber in .repo/projects/. | ||
| 2850 | SAFE_TO_CLOBBER = { | ||
| 2851 | 'COMMIT_EDITMSG', 'FETCH_HEAD', 'HEAD', 'index', 'ORIG_HEAD', | ||
| 2852 | } | ||
| 2853 | |||
| 2854 | # Now walk the paths and sync the .git/ to .repo/projects/. | ||
| 2855 | for name in platform_utils.listdir(dotgit): | ||
| 2856 | dotgit_path = os.path.join(dotgit, name) | ||
| 2857 | if name in KNOWN_LINKS: | ||
| 2858 | if platform_utils.islink(dotgit_path): | ||
| 2859 | platform_utils.remove(dotgit_path) | ||
| 2860 | else: | ||
| 2861 | raise GitError(f'{dotgit_path}: should be a symlink') | ||
| 2862 | else: | ||
| 2863 | gitdir_path = os.path.join(gitdir, name) | ||
| 2864 | if name in SAFE_TO_CLOBBER or not os.path.exists(gitdir_path): | ||
| 2865 | platform_utils.remove(gitdir_path, missing_ok=True) | ||
| 2866 | platform_utils.rename(dotgit_path, gitdir_path) | ||
| 2867 | else: | ||
| 2868 | raise GitError(f'{dotgit_path}: unknown file; please file a bug') | ||
| 2869 | |||
| 2870 | # Now that the dir should be empty, clear it out, and symlink it over. | ||
| 2871 | platform_utils.rmdir(dotgit) | ||
| 2872 | platform_utils.symlink(os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit) | ||
| 2828 | 2873 | ||
| 2829 | def _get_symlink_error_message(self): | 2874 | def _get_symlink_error_message(self): |
| 2830 | if platform_utils.isWindows(): | 2875 | if platform_utils.isWindows(): |
diff --git a/tests/test_project.py b/tests/test_project.py index 9b2cc4e9..d578fe84 100644 --- a/tests/test_project.py +++ b/tests/test_project.py | |||
| @@ -16,6 +16,7 @@ | |||
| 16 | 16 | ||
| 17 | import contextlib | 17 | import contextlib |
| 18 | import os | 18 | import os |
| 19 | from pathlib import Path | ||
| 19 | import shutil | 20 | import shutil |
| 20 | import subprocess | 21 | import subprocess |
| 21 | import tempfile | 22 | import tempfile |
| @@ -335,3 +336,52 @@ class LinkFile(CopyLinkTestCase): | |||
| 335 | platform_utils.symlink(self.tempdir, dest) | 336 | platform_utils.symlink(self.tempdir, dest) |
| 336 | lf._Link() | 337 | lf._Link() |
| 337 | self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest)) | 338 | self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest)) |
| 339 | |||
| 340 | |||
| 341 | class MigrateWorkTreeTests(unittest.TestCase): | ||
| 342 | """Check _MigrateOldWorkTreeGitDir handling.""" | ||
| 343 | |||
| 344 | _SYMLINKS = { | ||
| 345 | 'config', 'description', 'hooks', 'info', 'logs', 'objects', | ||
| 346 | 'packed-refs', 'refs', 'rr-cache', 'shallow', 'svn', | ||
| 347 | } | ||
| 348 | _FILES = { | ||
| 349 | 'COMMIT_EDITMSG', 'FETCH_HEAD', 'HEAD', 'index', 'ORIG_HEAD', | ||
| 350 | } | ||
| 351 | |||
| 352 | @classmethod | ||
| 353 | @contextlib.contextmanager | ||
| 354 | def _simple_layout(cls): | ||
| 355 | """Create a simple repo client checkout to test against.""" | ||
| 356 | with tempfile.TemporaryDirectory() as tempdir: | ||
| 357 | tempdir = Path(tempdir) | ||
| 358 | |||
| 359 | gitdir = tempdir / '.repo/projects/src/test.git' | ||
| 360 | gitdir.mkdir(parents=True) | ||
| 361 | cmd = ['git', 'init', '--bare', str(gitdir)] | ||
| 362 | subprocess.check_call(cmd) | ||
| 363 | |||
| 364 | dotgit = tempdir / 'src/test/.git' | ||
| 365 | dotgit.mkdir(parents=True) | ||
| 366 | for name in cls._SYMLINKS: | ||
| 367 | (dotgit / name).symlink_to(f'../../../.repo/projects/src/test.git/{name}') | ||
| 368 | for name in cls._FILES: | ||
| 369 | (dotgit / name).write_text(name) | ||
| 370 | |||
| 371 | subprocess.run(['tree', '-a', str(dotgit)]) | ||
| 372 | yield tempdir | ||
| 373 | |||
| 374 | def test_standard(self): | ||
| 375 | """Migrate a standard checkout that we expect.""" | ||
| 376 | with self._simple_layout() as tempdir: | ||
| 377 | dotgit = tempdir / 'src/test/.git' | ||
| 378 | project.Project._MigrateOldWorkTreeGitDir(str(dotgit)) | ||
| 379 | |||
| 380 | # Make sure the dir was transformed into a symlink. | ||
| 381 | self.assertTrue(dotgit.is_symlink()) | ||
| 382 | self.assertEqual(str(dotgit.readlink()), '../../.repo/projects/src/test.git') | ||
| 383 | |||
| 384 | # Make sure files were moved over. | ||
| 385 | gitdir = tempdir / '.repo/projects/src/test.git' | ||
| 386 | for name in self._FILES: | ||
| 387 | self.assertEqual(name, (gitdir / name).read_text()) | ||
