diff options
| author | LaMont Jones <lamontjones@google.com> | 2022-11-02 22:01:29 +0000 |
|---|---|---|
| committer | LaMont Jones <lamontjones@google.com> | 2022-11-03 23:01:16 +0000 |
| commit | fa8d939c8f6a3d25d9a203f28b16a71d891dcc1c (patch) | |
| tree | 1d3519d54c1ef256b0acaa19a0d601897a318411 | |
| parent | a6c52f566acfbff5b0f37158c0d33adf05d250e5 (diff) | |
| download | git-repo-fa8d939c8f6a3d25d9a203f28b16a71d891dcc1c.tar.gz | |
sync: clear preciousObjects when set in error.
If this is a project that is not using object sharing (there is only one
copy of the remote project) then clear preciousObjects.
To override this for a project, run:
git config --replace-all repo.preservePreciousObjects true
Change-Id: If3ea061c631c5ecd44ead84f68576012e2c7405c
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/350235
Reviewed-by: Jonathan Nieder <jrn@google.com>
Tested-by: LaMont Jones <lamontjones@google.com>
| -rw-r--r-- | git_config.py | 4 | ||||
| -rw-r--r-- | project.py | 2 | ||||
| -rw-r--r-- | subcmds/sync.py | 96 | ||||
| -rw-r--r-- | tests/test_subcmds_sync.py | 55 |
4 files changed, 126 insertions, 31 deletions
diff --git a/git_config.py b/git_config.py index 98cade32..94378e9a 100644 --- a/git_config.py +++ b/git_config.py | |||
| @@ -219,8 +219,8 @@ class GitConfig(object): | |||
| 219 | """Set the value(s) for a key. | 219 | """Set the value(s) for a key. |
| 220 | Only this configuration file is modified. | 220 | Only this configuration file is modified. |
| 221 | 221 | ||
| 222 | The supplied value should be either a string, | 222 | The supplied value should be either a string, or a list of strings (to |
| 223 | or a list of strings (to store multiple values). | 223 | store multiple values), or None (to delete the key). |
| 224 | """ | 224 | """ |
| 225 | key = _key(name) | 225 | key = _key(name) |
| 226 | 226 | ||
| @@ -59,7 +59,7 @@ MAXIMUM_RETRY_SLEEP_SEC = 3600.0 | |||
| 59 | # +-10% random jitter is added to each Fetches retry sleep duration. | 59 | # +-10% random jitter is added to each Fetches retry sleep duration. |
| 60 | RETRY_JITTER_PERCENT = 0.1 | 60 | RETRY_JITTER_PERCENT = 0.1 |
| 61 | 61 | ||
| 62 | # Whether to use alternates. | 62 | # Whether to use alternates. Switching back and forth is *NOT* supported. |
| 63 | # TODO(vapier): Remove knob once behavior is verified. | 63 | # TODO(vapier): Remove knob once behavior is verified. |
| 64 | _ALTERNATES = os.environ.get('REPO_USE_ALTERNATES') == '1' | 64 | _ALTERNATES = os.environ.get('REPO_USE_ALTERNATES') == '1' |
| 65 | 65 | ||
diff --git a/subcmds/sync.py b/subcmds/sync.py index 082b254f..83c9ad36 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
| @@ -755,33 +755,87 @@ later is required to fix a server side protocol bug. | |||
| 755 | shutil.copy(os.path.join(pack_dir, fname), bak_fname + '.tmp') | 755 | shutil.copy(os.path.join(pack_dir, fname), bak_fname + '.tmp') |
| 756 | shutil.move(bak_fname + '.tmp', bak_fname) | 756 | shutil.move(bak_fname + '.tmp', bak_fname) |
| 757 | 757 | ||
| 758 | @staticmethod | ||
| 759 | def _GetPreciousObjectsState(project: Project, opt): | ||
| 760 | """Get the preciousObjects state for the project. | ||
| 761 | |||
| 762 | Args: | ||
| 763 | project (Project): the project to examine, and possibly correct. | ||
| 764 | opt (optparse.Values): options given to sync. | ||
| 765 | |||
| 766 | Returns: | ||
| 767 | Expected state of extensions.preciousObjects: | ||
| 768 | False: Should be disabled. (not present) | ||
| 769 | True: Should be enabled. | ||
| 770 | """ | ||
| 771 | if project.use_git_worktrees: | ||
| 772 | return False | ||
| 773 | projects = project.manifest.GetProjectsWithName(project.name, | ||
| 774 | all_manifests=True) | ||
| 775 | if len(projects) == 1: | ||
| 776 | return False | ||
| 777 | relpath = project.RelPath(local=opt.this_manifest_only) | ||
| 778 | if len(projects) > 1: | ||
| 779 | # Objects are potentially shared with another project. | ||
| 780 | # See the logic in Project.Sync_NetworkHalf regarding UseAlternates. | ||
| 781 | # - When False, shared projects share (via symlink) | ||
| 782 | # .repo/project-objects/{PROJECT_NAME}.git as the one-and-only objects | ||
| 783 | # directory. All objects are precious, since there is no project with a | ||
| 784 | # complete set of refs. | ||
| 785 | # - When True, shared projects share (via info/alternates) | ||
| 786 | # .repo/project-objects/{PROJECT_NAME}.git as an alternate object store, | ||
| 787 | # which is written only on the first clone of the project, and is not | ||
| 788 | # written subsequently. (When Sync_NetworkHalf sees that it exists, it | ||
| 789 | # makes sure that the alternates file points there, and uses a | ||
| 790 | # project-local .git/objects directory for all syncs going forward. | ||
| 791 | # We do not support switching between the options. The environment | ||
| 792 | # variable is present for testing and migration only. | ||
| 793 | return not project.UseAlternates | ||
| 794 | print(f'\r{relpath}: project not found in manifest.', file=sys.stderr) | ||
| 795 | return False | ||
| 796 | |||
| 797 | def _RepairPreciousObjectsState(self, project: Project, opt): | ||
| 798 | """Correct the preciousObjects state for the project. | ||
| 799 | |||
| 800 | Args: | ||
| 801 | project (Project): the project to examine, and possibly correct. | ||
| 802 | opt (optparse.Values): options given to sync. | ||
| 803 | """ | ||
| 804 | expected = self._GetPreciousObjectsState(project, opt) | ||
| 805 | actual = project.config.GetBoolean('extensions.preciousObjects') or False | ||
| 806 | relpath = project.RelPath(local = opt.this_manifest_only) | ||
| 807 | |||
| 808 | if (expected != actual and | ||
| 809 | not project.config.GetBoolean('repo.preservePreciousObjects')): | ||
| 810 | # If this is unexpected, log it and repair. | ||
| 811 | Trace(f'{relpath} expected preciousObjects={expected}, got {actual}') | ||
| 812 | if expected: | ||
| 813 | if not opt.quiet: | ||
| 814 | print('\r%s: Shared project %s found, disabling pruning.' % | ||
| 815 | (relpath, project.name)) | ||
| 816 | if git_require((2, 7, 0)): | ||
| 817 | project.EnableRepositoryExtension('preciousObjects') | ||
| 818 | else: | ||
| 819 | # This isn't perfect, but it's the best we can do with old git. | ||
| 820 | print('\r%s: WARNING: shared projects are unreliable when using ' | ||
| 821 | 'old versions of git; please upgrade to git-2.7.0+.' | ||
| 822 | % (relpath,), | ||
| 823 | file=sys.stderr) | ||
| 824 | project.config.SetString('gc.pruneExpire', 'never') | ||
| 825 | else: | ||
| 826 | if not opt.quiet: | ||
| 827 | print(f'\r{relpath}: not shared, disabling pruning.') | ||
| 828 | project.config.SetString('extensions.preciousObjects', None) | ||
| 829 | project.config.SetString('gc.pruneExpire', None) | ||
| 830 | |||
| 758 | def _GCProjects(self, projects, opt, err_event): | 831 | def _GCProjects(self, projects, opt, err_event): |
| 759 | pm = Progress('Garbage collecting', len(projects), delay=False, quiet=opt.quiet) | 832 | pm = Progress('Garbage collecting', len(projects), delay=False, quiet=opt.quiet) |
| 760 | pm.update(inc=0, msg='prescan') | 833 | pm.update(inc=0, msg='prescan') |
| 761 | 834 | ||
| 762 | tidy_dirs = {} | 835 | tidy_dirs = {} |
| 763 | for project in projects: | 836 | for project in projects: |
| 764 | # Make sure pruning never kicks in with shared projects that do not use | 837 | self._RepairPreciousObjectsState(project, opt) |
| 765 | # alternates to avoid corruption. | 838 | |
| 766 | if (not project.use_git_worktrees and | ||
| 767 | len(project.manifest.GetProjectsWithName(project.name, all_manifests=True)) > 1): | ||
| 768 | if project.UseAlternates: | ||
| 769 | # Undo logic set by previous versions of repo. | ||
| 770 | project.config.SetString('extensions.preciousObjects', None) | ||
| 771 | project.config.SetString('gc.pruneExpire', None) | ||
| 772 | else: | ||
| 773 | if not opt.quiet: | ||
| 774 | print('\r%s: Shared project %s found, disabling pruning.' % | ||
| 775 | (project.relpath, project.name)) | ||
| 776 | if git_require((2, 7, 0)): | ||
| 777 | project.EnableRepositoryExtension('preciousObjects') | ||
| 778 | else: | ||
| 779 | # This isn't perfect, but it's the best we can do with old git. | ||
| 780 | print('\r%s: WARNING: shared projects are unreliable when using old ' | ||
| 781 | 'versions of git; please upgrade to git-2.7.0+.' | ||
| 782 | % (project.relpath,), | ||
| 783 | file=sys.stderr) | ||
| 784 | project.config.SetString('gc.pruneExpire', 'never') | ||
| 785 | project.config.SetString('gc.autoDetach', 'false') | 839 | project.config.SetString('gc.autoDetach', 'false') |
| 786 | # Only call git gc once per objdir, but call pack-refs for the remainder. | 840 | # Only call git gc once per objdir, but call pack-refs for the remainder. |
| 787 | if project.objdir not in tidy_dirs: | 841 | if project.objdir not in tidy_dirs: |
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index aad713f2..13f3f873 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py | |||
| @@ -11,9 +11,9 @@ | |||
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and | 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. | 13 | # limitations under the License. |
| 14 | |||
| 15 | """Unittests for the subcmds/sync.py module.""" | 14 | """Unittests for the subcmds/sync.py module.""" |
| 16 | 15 | ||
| 16 | import unittest | ||
| 17 | from unittest import mock | 17 | from unittest import mock |
| 18 | 18 | ||
| 19 | import pytest | 19 | import pytest |
| @@ -21,17 +21,14 @@ import pytest | |||
| 21 | from subcmds import sync | 21 | from subcmds import sync |
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | @pytest.mark.parametrize( | 24 | @pytest.mark.parametrize('use_superproject, cli_args, result', [ |
| 25 | 'use_superproject, cli_args, result', | ||
| 26 | [ | ||
| 27 | (True, ['--current-branch'], True), | 25 | (True, ['--current-branch'], True), |
| 28 | (True, ['--no-current-branch'], True), | 26 | (True, ['--no-current-branch'], True), |
| 29 | (True, [], True), | 27 | (True, [], True), |
| 30 | (False, ['--current-branch'], True), | 28 | (False, ['--current-branch'], True), |
| 31 | (False, ['--no-current-branch'], False), | 29 | (False, ['--no-current-branch'], False), |
| 32 | (False, [], None), | 30 | (False, [], None), |
| 33 | ] | 31 | ]) |
| 34 | ) | ||
| 35 | def test_get_current_branch_only(use_superproject, cli_args, result): | 32 | def test_get_current_branch_only(use_superproject, cli_args, result): |
| 36 | """Test Sync._GetCurrentBranchOnly logic. | 33 | """Test Sync._GetCurrentBranchOnly logic. |
| 37 | 34 | ||
| @@ -41,5 +38,49 @@ def test_get_current_branch_only(use_superproject, cli_args, result): | |||
| 41 | cmd = sync.Sync() | 38 | cmd = sync.Sync() |
| 42 | opts, _ = cmd.OptionParser.parse_args(cli_args) | 39 | opts, _ = cmd.OptionParser.parse_args(cli_args) |
| 43 | 40 | ||
| 44 | with mock.patch('git_superproject.UseSuperproject', return_value=use_superproject): | 41 | with mock.patch('git_superproject.UseSuperproject', |
| 42 | return_value=use_superproject): | ||
| 45 | assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result | 43 | assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result |
| 44 | |||
| 45 | |||
| 46 | class GetPreciousObjectsState(unittest.TestCase): | ||
| 47 | """Tests for _GetPreciousObjectsState.""" | ||
| 48 | |||
| 49 | def setUp(self): | ||
| 50 | """Common setup.""" | ||
| 51 | self.cmd = sync.Sync() | ||
| 52 | self.project = p = mock.MagicMock(use_git_worktrees=False, | ||
| 53 | UseAlternates=False) | ||
| 54 | p.manifest.GetProjectsWithName.return_value = [p] | ||
| 55 | |||
| 56 | self.opt = mock.Mock(spec_set=['this_manifest_only']) | ||
| 57 | self.opt.this_manifest_only = False | ||
| 58 | |||
| 59 | def test_worktrees(self): | ||
| 60 | """False for worktrees.""" | ||
| 61 | self.project.use_git_worktrees = True | ||
| 62 | self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt)) | ||
| 63 | |||
| 64 | def test_not_shared(self): | ||
| 65 | """Singleton project.""" | ||
| 66 | self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt)) | ||
| 67 | |||
| 68 | def test_shared(self): | ||
| 69 | """Shared project.""" | ||
| 70 | self.project.manifest.GetProjectsWithName.return_value = [ | ||
| 71 | self.project, self.project | ||
| 72 | ] | ||
| 73 | self.assertTrue(self.cmd._GetPreciousObjectsState(self.project, self.opt)) | ||
| 74 | |||
| 75 | def test_shared_with_alternates(self): | ||
| 76 | """Shared project, with alternates.""" | ||
| 77 | self.project.manifest.GetProjectsWithName.return_value = [ | ||
| 78 | self.project, self.project | ||
| 79 | ] | ||
| 80 | self.project.UseAlternates = True | ||
| 81 | self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt)) | ||
| 82 | |||
| 83 | def test_not_found(self): | ||
| 84 | """Project not found in manifest.""" | ||
| 85 | self.project.manifest.GetProjectsWithName.return_value = [] | ||
| 86 | self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt)) | ||
