Skip to content

Commit b7c6fd1

Browse files
[update-checkout] add the 'status' subcommand
1 parent 1885c9b commit b7c6fd1

File tree

9 files changed

+234
-42
lines changed

9 files changed

+234
-42
lines changed

utils/update_checkout/tests/test_locked_repository.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import unittest
33
from unittest.mock import patch
44

5-
from update_checkout.update_checkout import UpdateArguments, _is_any_repository_locked
5+
from update_checkout.update_checkout import UpdateArguments
6+
from update_checkout.git_command import is_any_repository_locked
67

78
FAKE_PATH = Path("/fake_path")
89

@@ -46,7 +47,7 @@ def iterdir_side_effect(path: Path):
4647
mock_is_dir.return_value = True
4748
mock_iterdir.side_effect = iterdir_side_effect
4849

49-
result = _is_any_repository_locked(pool_args)
50+
result = is_any_repository_locked(pool_args)
5051
self.assertEqual(result, {"repo1"})
5152

5253
@patch("pathlib.Path.exists")
@@ -61,7 +62,7 @@ def test_repository_without_git_dir(self, mock_iterdir, mock_is_dir, mock_exists
6162
mock_is_dir.return_value = False
6263
mock_iterdir.return_value = []
6364

64-
result = _is_any_repository_locked(pool_args)
65+
result = is_any_repository_locked(pool_args)
6566
self.assertEqual(result, set())
6667

6768
@patch("pathlib.Path.exists")
@@ -76,7 +77,7 @@ def test_repository_with_git_file(self, mock_iterdir, mock_is_dir, mock_exists):
7677
mock_is_dir.return_value = False
7778
mock_iterdir.return_value = []
7879

79-
result = _is_any_repository_locked(pool_args)
80+
result = is_any_repository_locked(pool_args)
8081
self.assertEqual(result, set())
8182

8283
@patch("pathlib.Path.exists")
@@ -95,7 +96,7 @@ def test_repository_with_multiple_lock_files(
9596
FAKE_PATH.joinpath(x) for x in ("index.lock", "merge.lock", "HEAD")
9697
]
9798

98-
result = _is_any_repository_locked(pool_args)
99+
result = is_any_repository_locked(pool_args)
99100
self.assertEqual(result, {"repo1"})
100101

101102
@patch("pathlib.Path.exists")
@@ -114,5 +115,5 @@ def test_repository_with_no_lock_files(
114115
FAKE_PATH.joinpath(x) for x in ("HEAD", "config", "logs")
115116
]
116117

117-
result = _is_any_repository_locked(pool_args)
118+
result = is_any_repository_locked(pool_args)
118119
self.assertEqual(result, set())
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import subprocess
2+
import tempfile
3+
from typing import List
4+
import unittest
5+
from pathlib import Path
6+
7+
from .scheme_mock import UPDATE_CHECKOUT_PATH
8+
9+
10+
class TestStatusCommand(unittest.TestCase):
11+
def call(self, args: List[str], cwd: Path):
12+
subprocess.check_call(
13+
args, cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
14+
)
15+
16+
def init_repo(self, path: Path, with_changes: bool):
17+
self.call(args=["git", "init"], cwd=path)
18+
self.call(
19+
args=["git", "config", "--local", "user.name", "swift_test"], cwd=path
20+
)
21+
self.call(
22+
args=["git", "config", "--local", "user.email", "[email protected]"],
23+
cwd=path,
24+
)
25+
self.call(args=["git", "config", "commit.gpgsign", "false"], cwd=path)
26+
27+
(path / "file.txt").write_text("initial\n")
28+
self.call(args=["git", "add", "file.txt"], cwd=path)
29+
self.call(args=["git", "commit", "-m", "initial commit"], cwd=path)
30+
31+
if with_changes:
32+
(path / "file.txt").write_text("modified\n")
33+
34+
def test_repositories_with_active_changes(self):
35+
with tempfile.TemporaryDirectory() as tmpdir:
36+
root = Path(tmpdir)
37+
38+
repo_1 = root / "repo_1"
39+
repo_2 = root / "repo_2"
40+
repo_1.mkdir()
41+
repo_2.mkdir()
42+
43+
self.init_repo(repo_1, True)
44+
self.init_repo(repo_2, True)
45+
46+
output = subprocess.run(
47+
[UPDATE_CHECKOUT_PATH, "--source-root", str(root), "status"],
48+
capture_output=True,
49+
text=True,
50+
).stdout
51+
52+
self.assertIn("The following repositories have active changes:", output)
53+
self.assertIn(" - [repo_1] 1 active change", output)
54+
self.assertIn(" - [repo_2] 1 active change", output)
55+
56+
def test_repositories_without_active_changes(self):
57+
with tempfile.TemporaryDirectory() as tmpdir:
58+
root = Path(tmpdir)
59+
60+
repo_1 = root / "repo_1"
61+
repo_2 = root / "repo_2"
62+
repo_1.mkdir()
63+
repo_2.mkdir()
64+
65+
self.init_repo(repo_1, False)
66+
self.init_repo(repo_2, False)
67+
68+
output = subprocess.run(
69+
[UPDATE_CHECKOUT_PATH, "--source-root", str(root), "status"],
70+
capture_output=True,
71+
text=True,
72+
).stdout
73+
74+
self.assertIn("No repositories have active changes.", output)
75+
76+
77+
if __name__ == "__main__":
78+
unittest.main()

utils/update_checkout/update_checkout/cli_arguments.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import argparse
22
from pathlib import Path
3-
from typing import List, Optional
3+
from typing import Any, List, Optional
44

55
from build_swift.build_swift.constants import SWIFT_SOURCE_ROOT
66

@@ -26,6 +26,7 @@ class CliArguments(argparse.Namespace):
2626
source_root: Path
2727
use_submodules: bool
2828
verbose: bool
29+
command: Optional[Any]
2930

3031
@staticmethod
3132
def parse_args() -> "CliArguments":
@@ -153,4 +154,8 @@ def parse_args() -> "CliArguments":
153154
help="Increases the script's verbosity.",
154155
action="store_true",
155156
)
157+
158+
subparsers = parser.add_subparsers(dest='command')
159+
subparsers.add_parser('status', help='Print the status of all the repositories')
160+
156161
return parser.parse_args()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .status import StatusCommand
2+
3+
__all__ = ["StatusCommand"]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from typing import List, Tuple
2+
3+
from ..parallel_runner import (
4+
ParallelRunner,
5+
RunnerArguments,
6+
)
7+
from ..runner_arguments import UpdateArguments
8+
from ..cli_arguments import CliArguments
9+
from ..git_command import (
10+
Git,
11+
is_any_repository_locked,
12+
is_git_repository,
13+
)
14+
15+
16+
class StatusCommand:
17+
_args: CliArguments
18+
_pool_args: List[UpdateArguments]
19+
20+
def __init__(self, args: CliArguments):
21+
self._args = args
22+
self._pool_args = []
23+
for repo_path in args.source_root.iterdir():
24+
if not is_git_repository(repo_path):
25+
continue
26+
repo_name = repo_path.name
27+
28+
my_args = RunnerArguments(
29+
repo_name=repo_name,
30+
output_prefix="Checking the status of",
31+
source_root=args.source_root,
32+
verbose=args.verbose,
33+
)
34+
self._pool_args.append(my_args)
35+
36+
def run(self):
37+
# TODO: If we add more commands, make error handling more generic
38+
locked_repositories = is_any_repository_locked(self._pool_args)
39+
if len(locked_repositories) > 0:
40+
results = [
41+
Exception(f"'{repo_name}' is locked by git. Cannot update it.")
42+
for repo_name in locked_repositories
43+
]
44+
else:
45+
results = ParallelRunner(
46+
self._get_uncommitted_changes_of_repository,
47+
self._pool_args,
48+
self._args.n_processes,
49+
).run()
50+
51+
return self._handle_results(results)
52+
53+
def _handle_results(self, results) -> int:
54+
ret = ParallelRunner.check_results(results, "STATUS")
55+
if ret > 0:
56+
return ret
57+
58+
repos_with_active_changes: List[Tuple[str, int]] = []
59+
for result, pool_arg in zip(results, self._pool_args):
60+
if result is None or result[0] == "":
61+
continue
62+
status_output: str = result[0]
63+
repos_with_active_changes.append((pool_arg.repo_name, len(status_output.splitlines())))
64+
65+
if len(repos_with_active_changes) == 0:
66+
print("No repositories have active changes.")
67+
else:
68+
print("The following repositories have active changes:")
69+
for repo_name, file_count in repos_with_active_changes:
70+
msg = f" - [{repo_name}] {file_count} active change"
71+
if file_count > 1:
72+
msg += "s"
73+
print(msg)
74+
75+
return 0
76+
77+
@staticmethod
78+
def _get_uncommitted_changes_of_repository(pool_args: RunnerArguments):
79+
repo_path = pool_args.source_root.joinpath(pool_args.repo_name)
80+
return Git.run(repo_path, ["status", "--short"], fatal=True)

utils/update_checkout/update_checkout/git_command.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import os
21
from pathlib import Path
32
import shlex
43
import subprocess
54
import sys
6-
from typing import List, Any, Optional, Dict, Tuple
5+
from typing import List, Any, Optional, Dict, Set, Tuple
6+
7+
from .runner_arguments import RunnerArguments
78

89

910
class GitException(Exception):
@@ -119,3 +120,45 @@ def _quote(arg: Any) -> str:
119120
@staticmethod
120121
def _quote_command(command: List[Any]) -> str:
121122
return " ".join(Git._quote(arg) for arg in command)
123+
124+
125+
def is_git_repository(path: Path) -> bool:
126+
"""Returns whether a Path object is a Git repository.
127+
128+
Args:
129+
path (Path): The path object to check.
130+
131+
Returns:
132+
bool: Whether the path is a Git repository.
133+
"""
134+
135+
if not path.is_dir():
136+
return False
137+
git_dir_path = path.joinpath(".git")
138+
return git_dir_path.exists() and git_dir_path.is_dir()
139+
140+
141+
def is_any_repository_locked(pool_args: List[RunnerArguments]) -> Set[str]:
142+
"""Returns the set of locked repositories.
143+
144+
A repository is considered to be locked if its .git directory contains a
145+
file ending in ".lock".
146+
147+
Args:
148+
pool_args (List[RunnerArguments]): List of arguments passed to the
149+
`update_single_repository` function.
150+
151+
Returns:
152+
Set[str]: The names of the locked repositories if any.
153+
"""
154+
155+
repos = [(x.source_root, x.repo_name) for x in pool_args]
156+
locked_repositories = set()
157+
for source_root, repo_name in repos:
158+
dot_git_path = source_root.joinpath(repo_name, ".git")
159+
if not dot_git_path.exists() or not dot_git_path.is_dir():
160+
continue
161+
for file in dot_git_path.iterdir():
162+
if file.suffix == ".lock":
163+
locked_repositories.add(repo_name)
164+
return locked_repositories

utils/update_checkout/update_checkout/parallel_runner.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import shutil
88

99
from .git_command import GitException
10-
1110
from .runner_arguments import (
1211
RunnerArguments,
1312
AdditionalSwiftSourcesArguments,
@@ -84,7 +83,7 @@ def __init__(
8483
):
8584
def run_safely(*args, **kwargs):
8685
try:
87-
fn(*args, **kwargs)
86+
return fn(*args, **kwargs)
8887
except GitException as e:
8988
return e
9089

@@ -158,6 +157,9 @@ def check_results(
158157
for r in results:
159158
if r is None:
160159
continue
160+
if isinstance(r, tuple) and len(r) == 3:
161+
if r[1] == 0:
162+
continue
161163
if fail_count == 0:
162164
print(f"======{operation} FAILURES======")
163165
fail_count += 1

utils/update_checkout/update_checkout/runner_arguments.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
@dataclass
99
class RunnerArguments:
1010
repo_name: str
11-
scheme_name: str
1211
output_prefix: str
12+
source_root: Path
1313
verbose: bool
1414

1515

1616
@dataclass
1717
class UpdateArguments(RunnerArguments):
18-
source_root: Path
18+
scheme_name: str
1919
config: Dict[str, Any]
2020
scheme_map: Any
2121
tag: Optional[str]
@@ -28,6 +28,7 @@ class UpdateArguments(RunnerArguments):
2828

2929
@dataclass
3030
class AdditionalSwiftSourcesArguments(RunnerArguments):
31+
scheme_name: str
3132
args: CliArguments
3233
repo_info: str
3334
repo_branch: str

0 commit comments

Comments
 (0)