mirror of
https://gerrit.googlesource.com/git-repo
synced 2024-12-21 07:16:21 +00:00
project: migrate worktree .git/ dirs to symlinks
Historically we created a .git/ subdir in each source checkout and symlinked individual files to the .repo/projects/ paths. This layer of indirection isn't actually needed: the .repo/projects/ paths are guaranteed to only ever have a 1-to-1 mapping with the actual git checkout. So we don't need to worry about having files in .git/ be isolated. To that end, change how we manage the actual project checkouts from a dir full of symlinks (and a few files) to a symlink to the internal .repo/projects/ dir. This makes the code simpler & faster. The directory structure we have today is: .repo/ project-objects/chromiumos/third_party/kernel.git/ <paths omitted as not relevant to this change> projects/src/third_party/kernel/ v3.8.git/ config description -> …/project-objects/…/config FETCH_HEAD HEAD hooks/ -> …/project-objects/…/hooks/ info/ -> …/project-objects/…/info/ logs/ objects/ -> …/project-objects/…/objects/ packed-refs refs/ rr-cache/ -> …/project-objects/…/rr-cache/ src/third_party/kernel/ v3.8/ .git/ config -> …/projects/…/v3.8.git/config description -> …/project-objects/…/v3.8.git/description HEAD hooks/ -> …/project-objects/…/v3.8.git/hooks/ index info/ -> …/project-objects/…/v3.8.git/info/ logs/ -> …/projects/…/v3.8.git/logs/ objects/ -> …/project-objects/…/v3.8.git/objects/ packed-refs -> …/projects/…/v3.8.git/packed-refs refs/ -> …/projects/…/v3.8.git/refs/ rr-cache/ -> …/project-objects/…/v3.8.git/rr-cache/ The directory structure we have after this commit: .repo/ <nothing changes> src/third_party/kernel/ v3.8/ .git -> …/projects/…/v3.8.git Bug: https://crbug.com/gerrit/15273 Change-Id: I9dd8def23fbfb2f4cb209a93f8b1b2b24002a444 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/323695 Reviewed-by: Mike Nichols <mikenichols@google.com> Reviewed-by: Xin Li <delphij@google.com> Tested-by: Mike Frysinger <vapier@google.com>
This commit is contained in:
parent
4a478edb44
commit
2a089cfee4
103
project.py
103
project.py
@ -2781,44 +2781,45 @@ class Project(object):
|
|||||||
self._InitMRef()
|
self._InitMRef()
|
||||||
|
|
||||||
def _InitWorkTree(self, force_sync=False, submodules=False):
|
def _InitWorkTree(self, force_sync=False, submodules=False):
|
||||||
realdotgit = os.path.join(self.worktree, '.git')
|
"""Setup the worktree .git path.
|
||||||
tmpdotgit = realdotgit + '.tmp'
|
|
||||||
init_dotgit = not os.path.exists(realdotgit)
|
This is the user-visible path like src/foo/.git/.
|
||||||
if init_dotgit:
|
|
||||||
|
With non-git-worktrees, this will be a symlink to the .repo/projects/ path.
|
||||||
|
With git-worktrees, this will be a .git file using "gitdir: ..." syntax.
|
||||||
|
|
||||||
|
Older checkouts had .git/ directories. If we see that, migrate it.
|
||||||
|
|
||||||
|
This also handles changes in the manifest. Maybe this project was backed
|
||||||
|
by "foo/bar" on the server, but now it's "new/foo/bar". We have to update
|
||||||
|
the path we point to under .repo/projects/ to match.
|
||||||
|
"""
|
||||||
|
dotgit = os.path.join(self.worktree, '.git')
|
||||||
|
|
||||||
|
# If using an old layout style (a directory), migrate it.
|
||||||
|
if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
|
||||||
|
self._MigrateOldWorkTreeGitDir(dotgit)
|
||||||
|
|
||||||
|
init_dotgit = not os.path.exists(dotgit)
|
||||||
if self.use_git_worktrees:
|
if self.use_git_worktrees:
|
||||||
|
if init_dotgit:
|
||||||
self._InitGitWorktree()
|
self._InitGitWorktree()
|
||||||
self._CopyAndLinkFiles()
|
self._CopyAndLinkFiles()
|
||||||
return
|
|
||||||
|
|
||||||
dotgit = tmpdotgit
|
|
||||||
platform_utils.rmtree(tmpdotgit, ignore_errors=True)
|
|
||||||
os.makedirs(tmpdotgit)
|
|
||||||
self._ReferenceGitDir(self.gitdir, tmpdotgit, share_refs=True,
|
|
||||||
copy_all=False)
|
|
||||||
else:
|
else:
|
||||||
dotgit = realdotgit
|
if not init_dotgit:
|
||||||
|
# See if the project has changed.
|
||||||
|
if platform_utils.realpath(self.gitdir) != platform_utils.realpath(dotgit):
|
||||||
|
platform_utils.remove(dotgit)
|
||||||
|
|
||||||
try:
|
if init_dotgit or not os.path.exists(dotgit):
|
||||||
self._CheckDirReference(self.gitdir, dotgit, share_refs=True)
|
os.makedirs(self.worktree, exist_ok=True)
|
||||||
except GitError as e:
|
platform_utils.symlink(os.path.relpath(self.gitdir, self.worktree), dotgit)
|
||||||
if force_sync and not init_dotgit:
|
|
||||||
try:
|
|
||||||
platform_utils.rmtree(dotgit)
|
|
||||||
return self._InitWorkTree(force_sync=False, submodules=submodules)
|
|
||||||
except Exception:
|
|
||||||
raise e
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if init_dotgit:
|
if init_dotgit:
|
||||||
_lwrite(os.path.join(tmpdotgit, HEAD), '%s\n' % self.GetRevisionId())
|
_lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
|
||||||
|
|
||||||
# Now that the .git dir is fully set up, move it to its final home.
|
|
||||||
platform_utils.rename(tmpdotgit, realdotgit)
|
|
||||||
|
|
||||||
# Finish checking out the worktree.
|
# Finish checking out the worktree.
|
||||||
cmd = ['read-tree', '--reset', '-u']
|
cmd = ['read-tree', '--reset', '-u', '-v', HEAD]
|
||||||
cmd.append('-v')
|
|
||||||
cmd.append(HEAD)
|
|
||||||
if GitCommand(self, cmd).Wait() != 0:
|
if GitCommand(self, cmd).Wait() != 0:
|
||||||
raise GitError('Cannot initialize work tree for ' + self.name)
|
raise GitError('Cannot initialize work tree for ' + self.name)
|
||||||
|
|
||||||
@ -2826,6 +2827,50 @@ class Project(object):
|
|||||||
self._SyncSubmodules(quiet=True)
|
self._SyncSubmodules(quiet=True)
|
||||||
self._CopyAndLinkFiles()
|
self._CopyAndLinkFiles()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _MigrateOldWorkTreeGitDir(cls, dotgit):
|
||||||
|
"""Migrate the old worktree .git/ dir style to a symlink.
|
||||||
|
|
||||||
|
This logic specifically only uses state from |dotgit| to figure out where to
|
||||||
|
move content and not |self|. This way if the backing project also changed
|
||||||
|
places, we only do the .git/ dir to .git symlink migration here. The path
|
||||||
|
updates will happen independently.
|
||||||
|
"""
|
||||||
|
# Figure out where in .repo/projects/ it's pointing to.
|
||||||
|
if not os.path.islink(os.path.join(dotgit, 'refs')):
|
||||||
|
raise GitError(f'{dotgit}: unsupported checkout state')
|
||||||
|
gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, 'refs')))
|
||||||
|
|
||||||
|
# Remove known symlink paths that exist in .repo/projects/.
|
||||||
|
KNOWN_LINKS = {
|
||||||
|
'config', 'description', 'hooks', 'info', 'logs', 'objects',
|
||||||
|
'packed-refs', 'refs', 'rr-cache', 'shallow', 'svn',
|
||||||
|
}
|
||||||
|
# Paths that we know will be in both, but are safe to clobber in .repo/projects/.
|
||||||
|
SAFE_TO_CLOBBER = {
|
||||||
|
'COMMIT_EDITMSG', 'FETCH_HEAD', 'HEAD', 'index', 'ORIG_HEAD',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Now walk the paths and sync the .git/ to .repo/projects/.
|
||||||
|
for name in platform_utils.listdir(dotgit):
|
||||||
|
dotgit_path = os.path.join(dotgit, name)
|
||||||
|
if name in KNOWN_LINKS:
|
||||||
|
if platform_utils.islink(dotgit_path):
|
||||||
|
platform_utils.remove(dotgit_path)
|
||||||
|
else:
|
||||||
|
raise GitError(f'{dotgit_path}: should be a symlink')
|
||||||
|
else:
|
||||||
|
gitdir_path = os.path.join(gitdir, name)
|
||||||
|
if name in SAFE_TO_CLOBBER or not os.path.exists(gitdir_path):
|
||||||
|
platform_utils.remove(gitdir_path, missing_ok=True)
|
||||||
|
platform_utils.rename(dotgit_path, gitdir_path)
|
||||||
|
else:
|
||||||
|
raise GitError(f'{dotgit_path}: unknown file; please file a bug')
|
||||||
|
|
||||||
|
# Now that the dir should be empty, clear it out, and symlink it over.
|
||||||
|
platform_utils.rmdir(dotgit)
|
||||||
|
platform_utils.symlink(os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit)
|
||||||
|
|
||||||
def _get_symlink_error_message(self):
|
def _get_symlink_error_message(self):
|
||||||
if platform_utils.isWindows():
|
if platform_utils.isWindows():
|
||||||
return ('Unable to create symbolic link. Please re-run the command as '
|
return ('Unable to create symbolic link. Please re-run the command as '
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -335,3 +336,52 @@ class LinkFile(CopyLinkTestCase):
|
|||||||
platform_utils.symlink(self.tempdir, dest)
|
platform_utils.symlink(self.tempdir, dest)
|
||||||
lf._Link()
|
lf._Link()
|
||||||
self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
|
self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateWorkTreeTests(unittest.TestCase):
|
||||||
|
"""Check _MigrateOldWorkTreeGitDir handling."""
|
||||||
|
|
||||||
|
_SYMLINKS = {
|
||||||
|
'config', 'description', 'hooks', 'info', 'logs', 'objects',
|
||||||
|
'packed-refs', 'refs', 'rr-cache', 'shallow', 'svn',
|
||||||
|
}
|
||||||
|
_FILES = {
|
||||||
|
'COMMIT_EDITMSG', 'FETCH_HEAD', 'HEAD', 'index', 'ORIG_HEAD',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _simple_layout(cls):
|
||||||
|
"""Create a simple repo client checkout to test against."""
|
||||||
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
|
tempdir = Path(tempdir)
|
||||||
|
|
||||||
|
gitdir = tempdir / '.repo/projects/src/test.git'
|
||||||
|
gitdir.mkdir(parents=True)
|
||||||
|
cmd = ['git', 'init', '--bare', str(gitdir)]
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
dotgit = tempdir / 'src/test/.git'
|
||||||
|
dotgit.mkdir(parents=True)
|
||||||
|
for name in cls._SYMLINKS:
|
||||||
|
(dotgit / name).symlink_to(f'../../../.repo/projects/src/test.git/{name}')
|
||||||
|
for name in cls._FILES:
|
||||||
|
(dotgit / name).write_text(name)
|
||||||
|
|
||||||
|
subprocess.run(['tree', '-a', str(dotgit)])
|
||||||
|
yield tempdir
|
||||||
|
|
||||||
|
def test_standard(self):
|
||||||
|
"""Migrate a standard checkout that we expect."""
|
||||||
|
with self._simple_layout() as tempdir:
|
||||||
|
dotgit = tempdir / 'src/test/.git'
|
||||||
|
project.Project._MigrateOldWorkTreeGitDir(str(dotgit))
|
||||||
|
|
||||||
|
# Make sure the dir was transformed into a symlink.
|
||||||
|
self.assertTrue(dotgit.is_symlink())
|
||||||
|
self.assertEqual(str(dotgit.readlink()), '../../.repo/projects/src/test.git')
|
||||||
|
|
||||||
|
# Make sure files were moved over.
|
||||||
|
gitdir = tempdir / '.repo/projects/src/test.git'
|
||||||
|
for name in self._FILES:
|
||||||
|
self.assertEqual(name, (gitdir / name).read_text())
|
||||||
|
Loading…
Reference in New Issue
Block a user