summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--git_config.py4
-rw-r--r--project.py2
-rw-r--r--subcmds/sync.py96
-rw-r--r--tests/test_subcmds_sync.py55
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
diff --git a/project.py b/project.py
index b975b72a..9d7476b4 100644
--- a/project.py
+++ b/project.py
@@ -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.
60RETRY_JITTER_PERCENT = 0.1 60RETRY_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
16import unittest
17from unittest import mock 17from unittest import mock
18 18
19import pytest 19import pytest
@@ -21,17 +21,14 @@ import pytest
21from subcmds import sync 21from 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)
35def test_get_current_branch_only(use_superproject, cli_args, result): 32def 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
46class 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))