From 5cb0251248111129d1bd7f5ad396ba65c7695768 Mon Sep 17 00:00:00 2001 From: Gavin Mak Date: Fri, 6 Feb 2026 20:54:20 +0000 Subject: [PATCH] gc: fix untargeted projects being deleted `delete_unused_projects` needs a full list of active projects to figure out which orphaned .git dirs need to be deleted. Otherwise it thinks that only the projects specified in args are active. Bug: 447626164 Change-Id: I02beebf6a01c77742a8db78221452d71cd78ea73 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/550061 Tested-by: Gavin Mak Reviewed-by: Mike Frysinger Commit-Queue: Gavin Mak --- subcmds/gc.py | 11 +++++- tests/test_subcmds_gc.py | 82 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/test_subcmds_gc.py diff --git a/subcmds/gc.py b/subcmds/gc.py index 4e5bcabff..a23d5e068 100644 --- a/subcmds/gc.py +++ b/subcmds/gc.py @@ -284,7 +284,16 @@ class Gc(Command): args, all_manifests=not opt.this_manifest_only ) - ret = self.delete_unused_projects(projects, opt) + # If the user specified projects, fetch the global list separately + # to avoid deleting untargeted projects. + if args: + all_projects = self.GetProjects( + [], all_manifests=not opt.this_manifest_only + ) + else: + all_projects = projects + + ret = self.delete_unused_projects(all_projects, opt) if ret != 0: return ret diff --git a/tests/test_subcmds_gc.py b/tests/test_subcmds_gc.py new file mode 100644 index 000000000..708f4a7ec --- /dev/null +++ b/tests/test_subcmds_gc.py @@ -0,0 +1,82 @@ +# Copyright (C) 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittests for the subcmds/gc.py module.""" + +import unittest +from unittest import mock + +from subcmds import gc + + +class GcCommand(unittest.TestCase): + """Tests for gc command.""" + + def setUp(self): + self.cmd = gc.Gc() + self.opt, self.args = self.cmd.OptionParser.parse_args([]) + self.opt.this_manifest_only = False + self.opt.repack = False + + self.mock_get_projects = mock.patch.object( + self.cmd, "GetProjects" + ).start() + + self.mock_delete = mock.patch.object( + self.cmd, "delete_unused_projects", return_value=0 + ).start() + + self.mock_repack = mock.patch.object( + self.cmd, "repack_projects", return_value=0 + ).start() + + def tearDown(self): + mock.patch.stopall() + + def test_gc_no_args(self): + """Test gc without specific projects.""" + self.mock_get_projects.return_value = ["all_projects"] + + self.cmd.Execute(self.opt, []) + + self.mock_get_projects.assert_called_once_with([], all_manifests=True) + self.mock_delete.assert_called_once_with(["all_projects"], self.opt) + self.mock_repack.assert_not_called() + + def test_gc_with_args(self): + """Test gc with specific projects uses all_projects for delete.""" + self.mock_get_projects.side_effect = [["projA"], ["all_projects"]] + self.opt.repack = True + + self.cmd.Execute(self.opt, ["projA"]) + + self.mock_get_projects.assert_has_calls( + [ + mock.call(["projA"], all_manifests=True), + mock.call([], all_manifests=True), + ] + ) + + self.mock_delete.assert_called_once_with(["all_projects"], self.opt) + self.mock_repack.assert_called_once_with(["projA"], self.opt) + + def test_gc_exit_on_delete_failure(self): + """Test gc exits if delete_unused_projects fails.""" + self.mock_get_projects.return_value = ["all_projects"] + self.mock_delete.return_value = 1 + self.opt.repack = True + + ret = self.cmd.Execute(self.opt, []) + self.assertEqual(ret, 1) + self.mock_repack.assert_not_called()