diff --git a/utils/update_checkout/tests/scheme_mock.py b/utils/update_checkout/tests/scheme_mock.py index 4f788041c0d94..07a30741d2986 100644 --- a/utils/update_checkout/tests/scheme_mock.py +++ b/utils/update_checkout/tests/scheme_mock.py @@ -145,10 +145,11 @@ def setup_mock_remote(base_dir, base_config): BASEDIR_ENV_VAR = 'UPDATECHECKOUT_TEST_WORKSPACE_DIR' CURRENT_FILE_DIR = os.path.dirname(os.path.abspath(__file__)) +UPDATE_CHECKOUT_EXECUTABLE = 'update-checkout.cmd' if os.name == 'nt' else 'update-checkout' UPDATE_CHECKOUT_PATH = os.path.abspath(os.path.join(CURRENT_FILE_DIR, os.path.pardir, os.path.pardir, - 'update-checkout')) + UPDATE_CHECKOUT_EXECUTABLE)) class SchemeMockTestCase(unittest.TestCase): diff --git a/utils/update_checkout/tests/test_locked_repository.py b/utils/update_checkout/tests/test_locked_repository.py new file mode 100644 index 0000000000000..28cd71b8d930f --- /dev/null +++ b/utils/update_checkout/tests/test_locked_repository.py @@ -0,0 +1,89 @@ +import unittest +from unittest.mock import patch + +from update_checkout.update_checkout import _is_any_repository_locked + +class TestIsAnyRepositoryLocked(unittest.TestCase): + @patch("os.path.exists") + @patch("os.path.isdir") + @patch("os.listdir") + def test_repository_with_lock_file(self, mock_listdir, mock_isdir, mock_exists): + pool_args = [ + ("/fake_path", None, "repo1"), + ("/fake_path", None, "repo2"), + ] + + def listdir_side_effect(path): + if "repo1" in path: + return ["index.lock", "config"] + elif "repo2" in path: + return ["HEAD", "config"] + return [] + + mock_exists.return_value = True + mock_isdir.return_value = True + mock_listdir.side_effect = listdir_side_effect + + result = _is_any_repository_locked(pool_args) + self.assertEqual(result, {"repo1"}) + + @patch("os.path.exists") + @patch("os.path.isdir") + @patch("os.listdir") + def test_repository_without_git_dir(self, mock_listdir, mock_isdir, mock_exists): + pool_args = [ + ("/fake_path", None, "repo1"), + ] + + mock_exists.return_value = False + mock_isdir.return_value = False + mock_listdir.return_value = [] + + result = _is_any_repository_locked(pool_args) + self.assertEqual(result, set()) + + @patch("os.path.exists") + @patch("os.path.isdir") + @patch("os.listdir") + def test_repository_with_git_file(self, mock_listdir, mock_isdir, mock_exists): + pool_args = [ + ("/fake_path", None, "repo1"), + ] + + mock_exists.return_value = True + mock_isdir.return_value = False + mock_listdir.return_value = [] + + result = _is_any_repository_locked(pool_args) + self.assertEqual(result, set()) + + @patch("os.path.exists") + @patch("os.path.isdir") + @patch("os.listdir") + def test_repository_with_multiple_lock_files(self, mock_listdir, mock_isdir, mock_exists): + pool_args = [ + ("/fake_path", None, "repo1"), + ] + + mock_exists.return_value = True + mock_isdir.return_value = True + mock_listdir.return_value = ["index.lock", "merge.lock", "HEAD"] + + result = _is_any_repository_locked(pool_args) + self.assertEqual(result, {"repo1"}) + + @patch("os.path.exists") + @patch("os.path.isdir") + @patch("os.listdir") + def test_repository_with_no_lock_files(self, mock_listdir, mock_isdir, mock_exists): + pool_args = [ + ("/fake_path", None, "repo1"), + ] + + mock_exists.return_value = True + mock_isdir.return_value = True + mock_listdir.return_value = ["HEAD", "config", "logs"] + + result = _is_any_repository_locked(pool_args) + self.assertEqual(result, set()) + diff --git a/utils/update_checkout/update_checkout/update_checkout.py b/utils/update_checkout/update_checkout/update_checkout.py index 01cb9dd0e38be..708e3187789ea 100755 --- a/utils/update_checkout/update_checkout/update_checkout.py +++ b/utils/update_checkout/update_checkout/update_checkout.py @@ -16,6 +16,7 @@ import sys import traceback from multiprocessing import Lock, Pool, cpu_count, freeze_support +from typing import Set, List, Any from build_swift.build_swift.constants import SWIFT_SOURCE_ROOT @@ -67,8 +68,11 @@ def check_parallel_results(results, op): if r is not None: if fail_count == 0: print("======%s FAILURES======" % op) - print("%s failed (ret=%d): %s" % (r.repo_path, r.ret, r)) fail_count += 1 + if isinstance(r, str): + print(r) + continue + print("%s failed (ret=%d): %s" % (r.repo_path, r.ret, r)) if r.stderr: print(r.stderr) return fail_count @@ -329,6 +333,30 @@ def get_scheme_map(config, scheme_name): return None +def _is_any_repository_locked(pool_args: List[Any]) -> Set[str]: + """Returns the set of locked repositories. + + A repository is considered to be locked if its .git directory contains a + file ending in ".lock". + + Args: + pool_args (List[Any]): List of arguments passed to the + `update_single_repository` function. + + Returns: + Set[str]: The names of the locked repositories if any. + """ + + repos = [(x[0], x[2]) for x in pool_args] + locked_repositories = set() + for source_root, repo_name in repos: + dot_git_path = os.path.join(source_root, repo_name, ".git") + if not os.path.exists(dot_git_path) or not os.path.isdir(dot_git_path): + continue + for file in os.listdir(dot_git_path): + if file.endswith(".lock"): + locked_repositories.add(repo_name) + return locked_repositories def update_all_repositories(args, config, scheme_name, scheme_map, cross_repos_pr): pool_args = [] @@ -363,6 +391,12 @@ def update_all_repositories(args, config, scheme_name, scheme_map, cross_repos_p cross_repos_pr] pool_args.append(my_args) + locked_repositories: set[str] = _is_any_repository_locked(pool_args) + if len(locked_repositories) > 0: + return [ + f"'{repo_name}' is locked by git. Cannot update it." + for repo_name in locked_repositories + ] return run_parallel(update_single_repository, pool_args, args.n_processes)