summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/internal-fs-layout.md6
-rw-r--r--manifest_xml.py35
-rw-r--r--project.py70
-rwxr-xr-xrepo2
-rw-r--r--subcmds/init.py24
-rw-r--r--subcmds/sync.py24
6 files changed, 138 insertions, 23 deletions
diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md
index 8e62cde2..f4740291 100644
--- a/docs/internal-fs-layout.md
+++ b/docs/internal-fs-layout.md
@@ -102,6 +102,11 @@ support, see the [manifest-format.md] file.
102 respective servers ... 102 respective servers ...
103* `subprojects/`: Like `projects/`, but for git submodules. 103* `subprojects/`: Like `projects/`, but for git submodules.
104* `subproject-objects/`: Like `project-objects/`, but for git submodules. 104* `subproject-objects/`: Like `project-objects/`, but for git submodules.
105* `worktrees/`: Bare checkouts of every project synced by the manifest. The
106 filesystem layout matches the `<project name=...` setting in the manifest
107 (i.e. the path on the remote server).
108
109 This is used when git worktrees are enabled.
105 110
106### Global settings 111### Global settings
107 112
@@ -121,6 +126,7 @@ User controlled settings are initialized when running `repo init`.
121| repo.partialclone | `--partial-clone` | Create [partial git clones] | 126| repo.partialclone | `--partial-clone` | Create [partial git clones] |
122| repo.reference | `--reference` | Reference repo client checkout | 127| repo.reference | `--reference` | Reference repo client checkout |
123| repo.submodules | `--submodules` | Sync git submodules | 128| repo.submodules | `--submodules` | Sync git submodules |
129| repo.worktree | `--worktree` | Use `git worktree` for checkouts |
124| user.email | `--config-name` | User's e-mail address; Copied into `.git/config` when checking out a new project | 130| user.email | `--config-name` | User's e-mail address; Copied into `.git/config` when checking out a new project |
125| user.name | `--config-name` | User's name; Copied into `.git/config` when checking out a new project | 131| user.name | `--config-name` | User's name; Copied into `.git/config` when checking out a new project |
126 132
diff --git a/manifest_xml.py b/manifest_xml.py
index 7f38d8c3..41628003 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -146,9 +146,17 @@ class XmlManifest(object):
146 gitdir=os.path.join(repodir, 'repo/.git'), 146 gitdir=os.path.join(repodir, 'repo/.git'),
147 worktree=os.path.join(repodir, 'repo')) 147 worktree=os.path.join(repodir, 'repo'))
148 148
149 self.manifestProject = MetaProject(self, 'manifests', 149 mp = MetaProject(self, 'manifests',
150 gitdir=os.path.join(repodir, 'manifests.git'), 150 gitdir=os.path.join(repodir, 'manifests.git'),
151 worktree=os.path.join(repodir, 'manifests')) 151 worktree=os.path.join(repodir, 'manifests'))
152 self.manifestProject = mp
153
154 # This is a bit hacky, but we're in a chicken & egg situation: all the
155 # normal repo settings live in the manifestProject which we just setup
156 # above, so we couldn't easily query before that. We assume Project()
157 # init doesn't care if this changes afterwards.
158 if mp.config.GetBoolean('repo.worktree'):
159 mp.use_git_worktrees = True
152 160
153 self._Unload() 161 self._Unload()
154 162
@@ -428,6 +436,10 @@ class XmlManifest(object):
428 return self.manifestProject.config.GetBoolean('repo.mirror') 436 return self.manifestProject.config.GetBoolean('repo.mirror')
429 437
430 @property 438 @property
439 def UseGitWorktrees(self):
440 return self.manifestProject.config.GetBoolean('repo.worktree')
441
442 @property
431 def IsArchive(self): 443 def IsArchive(self):
432 return self.manifestProject.config.GetBoolean('repo.archive') 444 return self.manifestProject.config.GetBoolean('repo.archive')
433 445
@@ -873,8 +885,10 @@ class XmlManifest(object):
873 groups = self._ParseGroups(groups) 885 groups = self._ParseGroups(groups)
874 886
875 if parent is None: 887 if parent is None:
876 relpath, worktree, gitdir, objdir = self.GetProjectPaths(name, path) 888 relpath, worktree, gitdir, objdir, use_git_worktrees = \
889 self.GetProjectPaths(name, path)
877 else: 890 else:
891 use_git_worktrees = False
878 relpath, worktree, gitdir, objdir = \ 892 relpath, worktree, gitdir, objdir = \
879 self.GetSubprojectPaths(parent, name, path) 893 self.GetSubprojectPaths(parent, name, path)
880 894
@@ -903,6 +917,7 @@ class XmlManifest(object):
903 upstream=upstream, 917 upstream=upstream,
904 parent=parent, 918 parent=parent,
905 dest_branch=dest_branch, 919 dest_branch=dest_branch,
920 use_git_worktrees=use_git_worktrees,
906 **extra_proj_attrs) 921 **extra_proj_attrs)
907 922
908 for n in node.childNodes: 923 for n in node.childNodes:
@@ -918,6 +933,7 @@ class XmlManifest(object):
918 return project 933 return project
919 934
920 def GetProjectPaths(self, name, path): 935 def GetProjectPaths(self, name, path):
936 use_git_worktrees = False
921 relpath = path 937 relpath = path
922 if self.IsMirror: 938 if self.IsMirror:
923 worktree = None 939 worktree = None
@@ -926,8 +942,15 @@ class XmlManifest(object):
926 else: 942 else:
927 worktree = os.path.join(self.topdir, path).replace('\\', '/') 943 worktree = os.path.join(self.topdir, path).replace('\\', '/')
928 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path) 944 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path)
929 objdir = os.path.join(self.repodir, 'project-objects', '%s.git' % name) 945 # We allow people to mix git worktrees & non-git worktrees for now.
930 return relpath, worktree, gitdir, objdir 946 # This allows for in situ migration of repo clients.
947 if os.path.exists(gitdir) or not self.UseGitWorktrees:
948 objdir = os.path.join(self.repodir, 'project-objects', '%s.git' % name)
949 else:
950 use_git_worktrees = True
951 gitdir = os.path.join(self.repodir, 'worktrees', '%s.git' % name)
952 objdir = gitdir
953 return relpath, worktree, gitdir, objdir, use_git_worktrees
931 954
932 def GetProjectsWithName(self, name): 955 def GetProjectsWithName(self, name):
933 return self._projects.get(name, []) 956 return self._projects.get(name, [])
diff --git a/project.py b/project.py
index 86c9ef00..3a7ac9e8 100644
--- a/project.py
+++ b/project.py
@@ -866,6 +866,7 @@ class Project(object):
866 clone_depth=None, 866 clone_depth=None,
867 upstream=None, 867 upstream=None,
868 parent=None, 868 parent=None,
869 use_git_worktrees=False,
869 is_derived=False, 870 is_derived=False,
870 dest_branch=None, 871 dest_branch=None,
871 optimized_fetch=False, 872 optimized_fetch=False,
@@ -889,6 +890,7 @@ class Project(object):
889 sync_tags: The `sync-tags` attribute of manifest.xml's project element. 890 sync_tags: The `sync-tags` attribute of manifest.xml's project element.
890 upstream: The `upstream` attribute of manifest.xml's project element. 891 upstream: The `upstream` attribute of manifest.xml's project element.
891 parent: The parent Project object. 892 parent: The parent Project object.
893 use_git_worktrees: Whether to use `git worktree` for this project.
892 is_derived: False if the project was explicitly defined in the manifest; 894 is_derived: False if the project was explicitly defined in the manifest;
893 True if the project is a discovered submodule. 895 True if the project is a discovered submodule.
894 dest_branch: The branch to which to push changes for review by default. 896 dest_branch: The branch to which to push changes for review by default.
@@ -923,6 +925,10 @@ class Project(object):
923 self.clone_depth = clone_depth 925 self.clone_depth = clone_depth
924 self.upstream = upstream 926 self.upstream = upstream
925 self.parent = parent 927 self.parent = parent
928 # NB: Do not use this setting in __init__ to change behavior so that the
929 # manifest.git checkout can inspect & change it after instantiating. See
930 # the XmlManifest init code for more info.
931 self.use_git_worktrees = use_git_worktrees
926 self.is_derived = is_derived 932 self.is_derived = is_derived
927 self.optimized_fetch = optimized_fetch 933 self.optimized_fetch = optimized_fetch
928 self.subprojects = [] 934 self.subprojects = []
@@ -1872,15 +1878,19 @@ class Project(object):
1872 except KeyError: 1878 except KeyError:
1873 head = None 1879 head = None
1874 if revid and head and revid == head: 1880 if revid and head and revid == head:
1875 ref = os.path.join(self.gitdir, R_HEADS + name) 1881 if self.use_git_worktrees:
1876 try: 1882 self.work_git.update_ref(HEAD, revid)
1877 os.makedirs(os.path.dirname(ref)) 1883 branch.Save()
1878 except OSError: 1884 else:
1879 pass 1885 ref = os.path.join(self.gitdir, R_HEADS + name)
1880 _lwrite(ref, '%s\n' % revid) 1886 try:
1881 _lwrite(self.GetHeadPath(), 'ref: %s%s\n' % (R_HEADS, name)) 1887 os.makedirs(os.path.dirname(ref))
1882 branch.Save() 1888 except OSError:
1883 return True 1889 pass
1890 _lwrite(ref, '%s\n' % revid)
1891 _lwrite(self.GetHeadPath(), 'ref: %s%s\n' % (R_HEADS, name))
1892 branch.Save()
1893 return True
1884 1894
1885 if GitCommand(self, 1895 if GitCommand(self,
1886 ['checkout', '-b', branch.name, revid], 1896 ['checkout', '-b', branch.name, revid],
@@ -2617,6 +2627,11 @@ class Project(object):
2617 os.makedirs(self.objdir) 2627 os.makedirs(self.objdir)
2618 self.bare_objdir.init() 2628 self.bare_objdir.init()
2619 2629
2630 # Enable per-worktree config file support if possible. This is more a
2631 # nice-to-have feature for users rather than a hard requirement.
2632 if self.use_git_worktrees and git_require((2, 19, 0)):
2633 self.config.SetString('extensions.worktreeConfig', 'true')
2634
2620 # If we have a separate directory to hold refs, initialize it as well. 2635 # If we have a separate directory to hold refs, initialize it as well.
2621 if self.objdir != self.gitdir: 2636 if self.objdir != self.gitdir:
2622 if init_git_dir: 2637 if init_git_dir:
@@ -2651,13 +2666,15 @@ class Project(object):
2651 mirror_git = os.path.join(ref_dir, self.name + '.git') 2666 mirror_git = os.path.join(ref_dir, self.name + '.git')
2652 repo_git = os.path.join(ref_dir, '.repo', 'projects', 2667 repo_git = os.path.join(ref_dir, '.repo', 'projects',
2653 self.relpath + '.git') 2668 self.relpath + '.git')
2669 worktrees_git = os.path.join(ref_dir, '.repo', 'worktrees',
2670 self.name + '.git')
2654 2671
2655 if os.path.exists(mirror_git): 2672 if os.path.exists(mirror_git):
2656 ref_dir = mirror_git 2673 ref_dir = mirror_git
2657
2658 elif os.path.exists(repo_git): 2674 elif os.path.exists(repo_git):
2659 ref_dir = repo_git 2675 ref_dir = repo_git
2660 2676 elif os.path.exists(worktrees_git):
2677 ref_dir = worktrees_git
2661 else: 2678 else:
2662 ref_dir = None 2679 ref_dir = None
2663 2680
@@ -2765,6 +2782,10 @@ class Project(object):
2765 self.bare_git.symbolic_ref('-m', msg, ref, dst) 2782 self.bare_git.symbolic_ref('-m', msg, ref, dst)
2766 2783
2767 def _CheckDirReference(self, srcdir, destdir, share_refs): 2784 def _CheckDirReference(self, srcdir, destdir, share_refs):
2785 # Git worktrees don't use symlinks to share at all.
2786 if self.use_git_worktrees:
2787 return
2788
2768 symlink_files = self.shareable_files[:] 2789 symlink_files = self.shareable_files[:]
2769 symlink_dirs = self.shareable_dirs[:] 2790 symlink_dirs = self.shareable_dirs[:]
2770 if share_refs: 2791 if share_refs:
@@ -2864,11 +2885,38 @@ class Project(object):
2864 else: 2885 else:
2865 raise 2886 raise
2866 2887
2888 def _InitGitWorktree(self):
2889 """Init the project using git worktrees."""
2890 self.bare_git.worktree('prune')
2891 self.bare_git.worktree('add', '-ff', '--checkout', '--detach', '--lock',
2892 self.worktree, self.GetRevisionId())
2893
2894 # Rewrite the internal state files to use relative paths between the
2895 # checkouts & worktrees.
2896 dotgit = os.path.join(self.worktree, '.git')
2897 with open(dotgit, 'r') as fp:
2898 # Figure out the checkout->worktree path.
2899 setting = fp.read()
2900 assert setting.startswith('gitdir:')
2901 git_worktree_path = setting.split(':', 1)[1].strip()
2902 # Use relative path from checkout->worktree.
2903 with open(dotgit, 'w') as fp:
2904 print('gitdir:', os.path.relpath(git_worktree_path, self.worktree),
2905 file=fp)
2906 # Use relative path from worktree->checkout.
2907 with open(os.path.join(git_worktree_path, 'gitdir'), 'w') as fp:
2908 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
2909
2867 def _InitWorkTree(self, force_sync=False, submodules=False): 2910 def _InitWorkTree(self, force_sync=False, submodules=False):
2868 realdotgit = os.path.join(self.worktree, '.git') 2911 realdotgit = os.path.join(self.worktree, '.git')
2869 tmpdotgit = realdotgit + '.tmp' 2912 tmpdotgit = realdotgit + '.tmp'
2870 init_dotgit = not os.path.exists(realdotgit) 2913 init_dotgit = not os.path.exists(realdotgit)
2871 if init_dotgit: 2914 if init_dotgit:
2915 if self.use_git_worktrees:
2916 self._InitGitWorktree()
2917 self._CopyAndLinkFiles()
2918 return
2919
2872 dotgit = tmpdotgit 2920 dotgit = tmpdotgit
2873 platform_utils.rmtree(tmpdotgit, ignore_errors=True) 2921 platform_utils.rmtree(tmpdotgit, ignore_errors=True)
2874 os.makedirs(tmpdotgit) 2922 os.makedirs(tmpdotgit)
diff --git a/repo b/repo
index 77e7fe90..743c28b3 100755
--- a/repo
+++ b/repo
@@ -302,6 +302,8 @@ def GetParser(gitc_init=False):
302 group.add_option('--clone-filter', action='store', default='blob:none', 302 group.add_option('--clone-filter', action='store', default='blob:none',
303 help='filter for use with --partial-clone ' 303 help='filter for use with --partial-clone '
304 '[default: %default]') 304 '[default: %default]')
305 group.add_option('--worktree', action='store_true',
306 help=optparse.SUPPRESS_HELP)
305 group.add_option('--archive', action='store_true', 307 group.add_option('--archive', action='store_true',
306 help='checkout an archive instead of a git repository for ' 308 help='checkout an archive instead of a git repository for '
307 'each project. See git archive.') 309 'each project. See git archive.')
diff --git a/subcmds/init.py b/subcmds/init.py
index 3c68c2c3..8a29321e 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -15,6 +15,8 @@
15# limitations under the License. 15# limitations under the License.
16 16
17from __future__ import print_function 17from __future__ import print_function
18
19import optparse
18import os 20import os
19import platform 21import platform
20import re 22import re
@@ -128,6 +130,10 @@ to update the working directory files.
128 g.add_option('--clone-filter', action='store', default='blob:none', 130 g.add_option('--clone-filter', action='store', default='blob:none',
129 dest='clone_filter', 131 dest='clone_filter',
130 help='filter for use with --partial-clone [default: %default]') 132 help='filter for use with --partial-clone [default: %default]')
133 # TODO(vapier): Expose option with real help text once this has been in the
134 # wild for a while w/out significant bug reports. Goal is by ~Sep 2020.
135 g.add_option('--worktree', action='store_true',
136 help=optparse.SUPPRESS_HELP)
131 g.add_option('--archive', 137 g.add_option('--archive',
132 dest='archive', action='store_true', 138 dest='archive', action='store_true',
133 help='checkout an archive instead of a git repository for ' 139 help='checkout an archive instead of a git repository for '
@@ -246,6 +252,20 @@ to update the working directory files.
246 if opt.dissociate: 252 if opt.dissociate:
247 m.config.SetString('repo.dissociate', 'true') 253 m.config.SetString('repo.dissociate', 'true')
248 254
255 if opt.worktree:
256 if opt.mirror:
257 print('fatal: --mirror and --worktree are incompatible',
258 file=sys.stderr)
259 sys.exit(1)
260 if opt.submodules:
261 print('fatal: --submodules and --worktree are incompatible',
262 file=sys.stderr)
263 sys.exit(1)
264 m.config.SetString('repo.worktree', 'true')
265 if is_new:
266 m.use_git_worktrees = True
267 print('warning: --worktree is experimental!', file=sys.stderr)
268
249 if opt.archive: 269 if opt.archive:
250 if is_new: 270 if is_new:
251 m.config.SetString('repo.archive', 'true') 271 m.config.SetString('repo.archive', 'true')
@@ -459,6 +479,10 @@ to update the working directory files.
459 % ('.'.join(str(x) for x in MIN_GIT_VERSION_SOFT),), 479 % ('.'.join(str(x) for x in MIN_GIT_VERSION_SOFT),),
460 file=sys.stderr) 480 file=sys.stderr)
461 481
482 if opt.worktree:
483 # Older versions of git supported worktree, but had dangerous gc bugs.
484 git_require((2, 15, 0), fail=True, msg='git gc worktree corruption')
485
462 self._SyncManifest(opt) 486 self._SyncManifest(opt)
463 self._LinkManifest(opt.manifest_name) 487 self._LinkManifest(opt.manifest_name)
464 488
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 0ac308e6..49867a97 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -15,6 +15,8 @@
15# limitations under the License. 15# limitations under the License.
16 16
17from __future__ import print_function 17from __future__ import print_function
18
19import errno
18import json 20import json
19import netrc 21import netrc
20from optparse import SUPPRESS_HELP 22from optparse import SUPPRESS_HELP
@@ -569,7 +571,8 @@ later is required to fix a server side protocol bug.
569 gc_gitdirs = {} 571 gc_gitdirs = {}
570 for project in projects: 572 for project in projects:
571 # Make sure pruning never kicks in with shared projects. 573 # Make sure pruning never kicks in with shared projects.
572 if len(project.manifest.GetProjectsWithName(project.name)) > 1: 574 if (not project.use_git_worktrees and
575 len(project.manifest.GetProjectsWithName(project.name)) > 1):
573 print('%s: Shared project %s found, disabling pruning.' % 576 print('%s: Shared project %s found, disabling pruning.' %
574 (project.relpath, project.name)) 577 (project.relpath, project.name))
575 if git_require((2, 7, 0)): 578 if git_require((2, 7, 0)):
@@ -637,13 +640,22 @@ later is required to fix a server side protocol bug.
637 # Delete the .git directory first, so we're less likely to have a partially 640 # Delete the .git directory first, so we're less likely to have a partially
638 # working git repository around. There shouldn't be any git projects here, 641 # working git repository around. There shouldn't be any git projects here,
639 # so rmtree works. 642 # so rmtree works.
643 dotgit = os.path.join(path, '.git')
644 # Try to remove plain files first in case of git worktrees. If this fails
645 # for any reason, we'll fall back to rmtree, and that'll display errors if
646 # it can't remove things either.
647 try:
648 platform_utils.remove(dotgit)
649 except OSError:
650 pass
640 try: 651 try:
641 platform_utils.rmtree(os.path.join(path, '.git')) 652 platform_utils.rmtree(dotgit)
642 except OSError as e: 653 except OSError as e:
643 print('Failed to remove %s (%s)' % (os.path.join(path, '.git'), str(e)), file=sys.stderr) 654 if e.errno != errno.ENOENT:
644 print('error: Failed to delete obsolete path %s' % path, file=sys.stderr) 655 print('error: %s: %s' % (dotgit, str(e)), file=sys.stderr)
645 print(' remove manually, then run sync again', file=sys.stderr) 656 print('error: %s: Failed to delete obsolete path; remove manually, then '
646 return 1 657 'run sync again' % (path,), file=sys.stderr)
658 return 1
647 659
648 # Delete everything under the worktree, except for directories that contain 660 # Delete everything under the worktree, except for directories that contain
649 # another git project 661 # another git project