diff options
| author | Gavin Mak <gavinmak@google.com> | 2026-04-01 23:03:09 +0000 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2026-04-09 12:09:40 -0700 |
| commit | 3b0eebeccfd4447e4a50ae143f37d0f7817723be (patch) | |
| tree | 5bffc4647d4f883eac0a514ef304b8f2fd75c32c /tests | |
| parent | 00991bfb42c4f3b0b7a50aa8f475ac1c8369924b (diff) | |
| download | git-repo-3b0eebeccfd4447e4a50ae143f37d0f7817723be.tar.gz | |
project: implement stateless sync pruning logic
Implement in-situ shallow re-fetching and garbage collection logic.
Enables repositories with sync-strategy="stateless" to reclaim disk
space by running reflog expire and git gc --prune=now if the working
tree is clean and has no local commits.
Bug: 498730431
Change-Id: I940bdc9b74da29d3f7b13566667dcddea769ebd3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/568463
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_project.py | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/tests/test_project.py b/tests/test_project.py index 501707eaf..a2d90d803 100644 --- a/tests/test_project.py +++ b/tests/test_project.py | |||
| @@ -21,6 +21,7 @@ import subprocess | |||
| 21 | import tempfile | 21 | import tempfile |
| 22 | from typing import Optional | 22 | from typing import Optional |
| 23 | import unittest | 23 | import unittest |
| 24 | from unittest import mock | ||
| 24 | 25 | ||
| 25 | import utils_for_test | 26 | import utils_for_test |
| 26 | 27 | ||
| @@ -565,3 +566,120 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase): | |||
| 565 | 566 | ||
| 566 | fakeproj.config.SetString("manifest.platform", "auto") | 567 | fakeproj.config.SetString("manifest.platform", "auto") |
| 567 | self.assertEqual(fakeproj.manifest_platform, "auto") | 568 | self.assertEqual(fakeproj.manifest_platform, "auto") |
| 569 | |||
| 570 | |||
| 571 | class StatelessSyncTests(unittest.TestCase): | ||
| 572 | """Tests for stateless sync strategy.""" | ||
| 573 | |||
| 574 | def _get_project(self, tempdir): | ||
| 575 | manifest = mock.MagicMock() | ||
| 576 | manifest.manifestProject.depth = None | ||
| 577 | manifest.manifestProject.dissociate = False | ||
| 578 | manifest.manifestProject.clone_filter = None | ||
| 579 | manifest.is_multimanifest = False | ||
| 580 | manifest.manifestProject.config.GetBoolean.return_value = False | ||
| 581 | |||
| 582 | remote = mock.MagicMock() | ||
| 583 | remote.name = "origin" | ||
| 584 | remote.url = "http://" | ||
| 585 | |||
| 586 | proj = project.Project( | ||
| 587 | manifest=manifest, | ||
| 588 | name="test-project", | ||
| 589 | remote=remote, | ||
| 590 | gitdir=os.path.join(tempdir, ".git"), | ||
| 591 | objdir=os.path.join(tempdir, ".git"), | ||
| 592 | worktree=tempdir, | ||
| 593 | relpath="test-project", | ||
| 594 | revisionExpr="1234abcd", | ||
| 595 | revisionId=None, | ||
| 596 | sync_strategy="stateless", | ||
| 597 | ) | ||
| 598 | proj._CheckForImmutableRevision = mock.MagicMock(return_value=False) | ||
| 599 | proj._LsRemote = mock.MagicMock( | ||
| 600 | return_value="1234abcd\trefs/heads/main\n" | ||
| 601 | ) | ||
| 602 | proj.bare_git = mock.MagicMock() | ||
| 603 | proj.bare_git.rev_parse.return_value = "5678abcd" | ||
| 604 | proj.bare_git.rev_list.return_value = ["0"] | ||
| 605 | proj.IsDirty = mock.MagicMock(return_value=False) | ||
| 606 | proj.GetBranches = mock.MagicMock(return_value=[]) | ||
| 607 | proj.DeleteWorktree = mock.MagicMock() | ||
| 608 | proj._InitGitDir = mock.MagicMock() | ||
| 609 | proj._RemoteFetch = mock.MagicMock(return_value=True) | ||
| 610 | proj._InitRemote = mock.MagicMock() | ||
| 611 | proj._InitMRef = mock.MagicMock() | ||
| 612 | return proj | ||
| 613 | |||
| 614 | def test_sync_network_half_stateless_prune_needed(self): | ||
| 615 | """Test stateless sync queues prune when needed.""" | ||
| 616 | with utils_for_test.TempGitTree() as tempdir: | ||
| 617 | proj = self._get_project(tempdir) | ||
| 618 | res = proj.Sync_NetworkHalf() | ||
| 619 | |||
| 620 | self.assertTrue(res.success) | ||
| 621 | proj.DeleteWorktree.assert_not_called() | ||
| 622 | self.assertTrue(proj.stateless_prune_needed) | ||
| 623 | proj._RemoteFetch.assert_called_once() | ||
| 624 | |||
| 625 | def test_sync_local_half_stateless_prune(self): | ||
| 626 | """Test stateless GC pruning is queued in Sync_LocalHalf.""" | ||
| 627 | with utils_for_test.TempGitTree() as tempdir: | ||
| 628 | proj = self._get_project(tempdir) | ||
| 629 | proj.stateless_prune_needed = True | ||
| 630 | |||
| 631 | proj._Checkout = mock.MagicMock() | ||
| 632 | proj._InitWorkTree = mock.MagicMock() | ||
| 633 | proj.IsRebaseInProgress = mock.MagicMock(return_value=False) | ||
| 634 | proj.IsCherryPickInProgress = mock.MagicMock(return_value=False) | ||
| 635 | proj.bare_ref = mock.MagicMock() | ||
| 636 | proj.bare_ref.all = {} | ||
| 637 | proj.GetRevisionId = mock.MagicMock(return_value="1234abcd") | ||
| 638 | proj._CopyAndLinkFiles = mock.MagicMock() | ||
| 639 | |||
| 640 | proj.work_git = mock.MagicMock() | ||
| 641 | proj.work_git.GetHead.return_value = "5678abcd" | ||
| 642 | |||
| 643 | syncbuf = project.SyncBuffer(proj.config) | ||
| 644 | |||
| 645 | with mock.patch("project.GitCommand") as mock_git_cmd: | ||
| 646 | mock_cmd_instance = mock.MagicMock() | ||
| 647 | mock_cmd_instance.Wait.return_value = 0 | ||
| 648 | mock_git_cmd.return_value = mock_cmd_instance | ||
| 649 | |||
| 650 | proj.Sync_LocalHalf(syncbuf) | ||
| 651 | syncbuf.Finish() | ||
| 652 | |||
| 653 | self.assertEqual(mock_git_cmd.call_count, 2) | ||
| 654 | mock_git_cmd.assert_any_call( | ||
| 655 | proj, ["reflog", "expire", "--expire=all", "--all"], bare=True | ||
| 656 | ) | ||
| 657 | mock_git_cmd.assert_any_call( | ||
| 658 | proj, | ||
| 659 | ["gc", "--prune=now"], | ||
| 660 | bare=True, | ||
| 661 | capture_stdout=True, | ||
| 662 | capture_stderr=True, | ||
| 663 | ) | ||
| 664 | |||
| 665 | def test_sync_network_half_stateless_skips_if_stash(self): | ||
| 666 | """Test stateless sync skips if stash exists.""" | ||
| 667 | with utils_for_test.TempGitTree() as tempdir: | ||
| 668 | proj = self._get_project(tempdir) | ||
| 669 | proj.HasStash = mock.MagicMock(return_value=True) | ||
| 670 | |||
| 671 | res = proj.Sync_NetworkHalf() | ||
| 672 | |||
| 673 | self.assertTrue(res.success) | ||
| 674 | self.assertFalse(getattr(proj, "stateless_prune_needed", False)) | ||
| 675 | |||
| 676 | def test_sync_network_half_stateless_skips_if_local_commits(self): | ||
| 677 | """Test stateless sync skips if there are local-only commits.""" | ||
| 678 | with utils_for_test.TempGitTree() as tempdir: | ||
| 679 | proj = self._get_project(tempdir) | ||
| 680 | proj.bare_git.rev_list.return_value = ["1"] | ||
| 681 | |||
| 682 | res = proj.Sync_NetworkHalf() | ||
| 683 | |||
| 684 | self.assertTrue(res.success) | ||
| 685 | self.assertFalse(getattr(proj, "stateless_prune_needed", False)) | ||
