diff options
| author | Mike Frysinger <vapier@google.com> | 2020-02-09 02:28:34 -0500 |
|---|---|---|
| committer | Mike Frysinger <vapier@google.com> | 2020-02-19 18:11:33 +0000 |
| commit | 979d5bdc3ebe45998a76dbbaff46c33d4e59683b (patch) | |
| tree | 97c639570a60dd02a6ee9b712dff091a83ac8110 | |
| parent | 56ce3468b4f2faa1cccfea01dc91e7db73fb3843 (diff) | |
| download | git-repo-979d5bdc3ebe45998a76dbbaff46c33d4e59683b.tar.gz | |
add experimental git worktree support
This provides initial support for using git worktrees internally
instead of our own ad-hoc symlink tree. It's been lightly tested
which is why it's not currently exposed via --help.
When people opt-in to worktrees in an existing repo client checkout,
no projects are migrated. Instead, only new projects will use the
worktree method. This allows for limited testing/opting in without
having to completely blow things away or get a second checkout.
Bug: https://crbug.com/gerrit/11486
Change-Id: Ic3ff891b30940a6ba497b406b2a387e0a8517ed8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254075
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
| -rw-r--r-- | docs/internal-fs-layout.md | 6 | ||||
| -rw-r--r-- | manifest_xml.py | 35 | ||||
| -rw-r--r-- | project.py | 70 | ||||
| -rwxr-xr-x | repo | 2 | ||||
| -rw-r--r-- | subcmds/init.py | 24 | ||||
| -rw-r--r-- | subcmds/sync.py | 24 |
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, []) |
| @@ -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) |
| @@ -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 | ||
| 17 | from __future__ import print_function | 17 | from __future__ import print_function |
| 18 | |||
| 19 | import optparse | ||
| 18 | import os | 20 | import os |
| 19 | import platform | 21 | import platform |
| 20 | import re | 22 | import 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 | ||
| 17 | from __future__ import print_function | 17 | from __future__ import print_function |
| 18 | |||
| 19 | import errno | ||
| 18 | import json | 20 | import json |
| 19 | import netrc | 21 | import netrc |
| 20 | from optparse import SUPPRESS_HELP | 22 | from 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 |
