multi-workspace 3.2.2__tar.gz → 3.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/PKG-INFO +1 -1
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/_version.py +2 -2
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/app_api.py +160 -15
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/ignore_files.py +9 -4
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/service.py +13 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/.gitignore +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/LICENSE +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/README.md +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/__init__.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/__main__.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/add.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/api.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/bootstrap.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/cli.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/cli_helpers.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/collaborator.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/convert_monorepo.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/doctor.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/errors.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/git_helpers.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/git_run.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/git_set_branch.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/init.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/logging.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/managed_blocks.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/open.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/paths.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/registry.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/remove.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/repo_urls.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/repos.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/resources/init_readme.md +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/settings.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_agents.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_github.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_vscode.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_vscode_devcontainer.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_vscode_extensions.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_vscode_helpers.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_vscode_launch.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_vscode_settings.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/sync_vscode_tasks.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/utils.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/multi/worktree.py +0 -0
- {multi_workspace-3.2.2 → multi_workspace-3.2.3}/pyproject.toml +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '3.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (3, 2,
|
|
21
|
+
__version__ = version = '3.2.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 2, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -2,6 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import subprocess
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError, as_completed
|
|
5
8
|
from pathlib import Path
|
|
6
9
|
from typing import Any, Literal, TypedDict
|
|
7
10
|
|
|
@@ -10,11 +13,29 @@ from multi.errors import NoRepositoriesError
|
|
|
10
13
|
from multi.paths import Paths
|
|
11
14
|
from multi.repos import Repository, load_repos
|
|
12
15
|
|
|
13
|
-
RepoStatus = Literal[
|
|
16
|
+
RepoStatus = Literal[
|
|
17
|
+
"clean",
|
|
18
|
+
"dirty",
|
|
19
|
+
"out-of-sync",
|
|
20
|
+
"no_git",
|
|
21
|
+
"missing",
|
|
22
|
+
"unknown",
|
|
23
|
+
]
|
|
14
24
|
|
|
15
25
|
HISTORY_GROUP_WINDOW_MS = 60_000
|
|
16
26
|
HISTORY_MAX_COMMITS_PER_REPO = 250
|
|
17
27
|
GIT_HISTORY_FORMAT = "%H%x1f%h%x1f%an%x1f%ae%x1f%at%x1f%s%x1f%b%x1e"
|
|
28
|
+
PROJECT_SUMMARY_METADATA_CACHE_TTL_SECONDS = 10.0
|
|
29
|
+
PROJECT_SUMMARY_WORKERS = 8
|
|
30
|
+
PROJECT_SUMMARY_TIMEOUT_SECONDS = 12.0
|
|
31
|
+
|
|
32
|
+
_project_summary_cache_lock = threading.Lock()
|
|
33
|
+
_project_summary_executor = ThreadPoolExecutor(
|
|
34
|
+
max_workers=PROJECT_SUMMARY_WORKERS,
|
|
35
|
+
thread_name_prefix="multi-project-summary",
|
|
36
|
+
)
|
|
37
|
+
_cached_project_summary_metadata: dict[str, tuple[float, dict[str, Any]]] = {}
|
|
38
|
+
_refreshing_project_summary_paths: set[str] = set()
|
|
18
39
|
|
|
19
40
|
|
|
20
41
|
class RepoSyncState(TypedDict):
|
|
@@ -104,7 +125,7 @@ def _get_git_status_internal(repo_path: Path, ignore_untracked: bool) -> RepoSta
|
|
|
104
125
|
return "missing"
|
|
105
126
|
|
|
106
127
|
if not (repo_path / ".git").exists():
|
|
107
|
-
return "
|
|
128
|
+
return "no_git"
|
|
108
129
|
|
|
109
130
|
status_args = ["status", "--porcelain"]
|
|
110
131
|
if ignore_untracked:
|
|
@@ -184,7 +205,9 @@ def _get_latest_commit_ms_for_repo(repo_path: Path) -> int | None:
|
|
|
184
205
|
return None
|
|
185
206
|
|
|
186
207
|
try:
|
|
187
|
-
stdout = _run_git(
|
|
208
|
+
stdout = _run_git(
|
|
209
|
+
["log", "-1", "--format=%at"], cwd=repo_path, timeout=10
|
|
210
|
+
).stdout
|
|
188
211
|
authored_at_seconds = int(stdout.strip())
|
|
189
212
|
return authored_at_seconds * 1000
|
|
190
213
|
except (OSError, ValueError, subprocess.SubprocessError):
|
|
@@ -194,10 +217,11 @@ def _get_latest_commit_ms_for_repo(repo_path: Path) -> int | None:
|
|
|
194
217
|
def _worst_status(a: RepoStatus, b: RepoStatus) -> RepoStatus:
|
|
195
218
|
severity = {
|
|
196
219
|
"clean": 0,
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"
|
|
220
|
+
"unknown": 1,
|
|
221
|
+
"no_git": 2,
|
|
222
|
+
"out-of-sync": 3,
|
|
223
|
+
"dirty": 4,
|
|
224
|
+
"missing": 5,
|
|
201
225
|
}
|
|
202
226
|
return a if severity[a] >= severity[b] else b
|
|
203
227
|
|
|
@@ -214,10 +238,10 @@ def get_project_status(repo_path: str | Path) -> RepoStatus:
|
|
|
214
238
|
],
|
|
215
239
|
]
|
|
216
240
|
worst = statuses[0]
|
|
217
|
-
if worst in {"missing", "
|
|
241
|
+
if worst in {"missing", "no_git"}:
|
|
218
242
|
worst = "clean"
|
|
219
243
|
for status in statuses[1:]:
|
|
220
|
-
if status in {"missing", "
|
|
244
|
+
if status in {"missing", "no_git"}:
|
|
221
245
|
continue
|
|
222
246
|
worst = _worst_status(worst, status)
|
|
223
247
|
return worst
|
|
@@ -311,10 +335,13 @@ def _history_commit_from_record(
|
|
|
311
335
|
"authorName": author_name or "",
|
|
312
336
|
"authorEmail": author_email or "",
|
|
313
337
|
"authoredAtMs": authored_at_ms,
|
|
314
|
-
"authoredAtIso": __import__("datetime")
|
|
338
|
+
"authoredAtIso": __import__("datetime")
|
|
339
|
+
.datetime.fromtimestamp(
|
|
315
340
|
authored_at_ms / 1000,
|
|
316
341
|
tz=__import__("datetime").timezone.utc,
|
|
317
|
-
)
|
|
342
|
+
)
|
|
343
|
+
.isoformat()
|
|
344
|
+
.replace("+00:00", "Z"),
|
|
318
345
|
"repoPath": str(repo_path),
|
|
319
346
|
"repoName": repo_name,
|
|
320
347
|
}
|
|
@@ -365,7 +392,9 @@ def group_history_commits(commits: list[dict[str, Any]]) -> list[dict[str, Any]]
|
|
|
365
392
|
for commit in sorted_commits:
|
|
366
393
|
current_group = groups[-1] if groups else None
|
|
367
394
|
previous_commit = (
|
|
368
|
-
current_group["commits"][-1]
|
|
395
|
+
current_group["commits"][-1]
|
|
396
|
+
if current_group and current_group["commits"]
|
|
397
|
+
else None
|
|
369
398
|
)
|
|
370
399
|
should_merge = (
|
|
371
400
|
previous_commit is not None
|
|
@@ -483,7 +512,7 @@ def _doctor_result(repo_path: Path) -> dict[str, Any] | None:
|
|
|
483
512
|
}
|
|
484
513
|
|
|
485
514
|
|
|
486
|
-
def
|
|
515
|
+
def _project_summary_static_payload(repo_path: str | Path) -> dict[str, Any]:
|
|
487
516
|
project_path = Path(repo_path)
|
|
488
517
|
name = project_path.name or str(project_path)
|
|
489
518
|
try:
|
|
@@ -495,15 +524,131 @@ def get_project_summary(repo_path: str | Path) -> dict[str, Any]:
|
|
|
495
524
|
"path": str(project_path),
|
|
496
525
|
"name": name,
|
|
497
526
|
"lastModifiedMs": _normalize_optional_ms(last_modified_ms),
|
|
498
|
-
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _project_summary_metadata(repo_path: str | Path) -> dict[str, Any]:
|
|
531
|
+
project_path = Path(repo_path)
|
|
532
|
+
return {
|
|
533
|
+
"lastCommitMs": _normalize_optional_ms(
|
|
534
|
+
get_project_last_commit_ms(project_path)
|
|
535
|
+
),
|
|
499
536
|
"status": get_project_status(project_path),
|
|
500
537
|
"doctorResult": _doctor_result(project_path),
|
|
501
538
|
"subRepos": _subrepo_payloads(project_path),
|
|
502
539
|
}
|
|
503
540
|
|
|
504
541
|
|
|
542
|
+
def _project_summary_fallback_metadata() -> dict[str, Any]:
|
|
543
|
+
return {
|
|
544
|
+
"lastCommitMs": None,
|
|
545
|
+
"status": "unknown",
|
|
546
|
+
"doctorResult": None,
|
|
547
|
+
"subRepos": None,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _cached_project_metadata(repo_path: str) -> tuple[dict[str, Any] | None, bool]:
|
|
552
|
+
now = time.monotonic()
|
|
553
|
+
with _project_summary_cache_lock:
|
|
554
|
+
cached = _cached_project_summary_metadata.get(repo_path)
|
|
555
|
+
if cached is None:
|
|
556
|
+
return None, False
|
|
557
|
+
cached_at, metadata = cached
|
|
558
|
+
return dict(
|
|
559
|
+
metadata
|
|
560
|
+
), now - cached_at < PROJECT_SUMMARY_METADATA_CACHE_TTL_SECONDS
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _refresh_project_summary_metadata_task(repo_path: str) -> None:
|
|
564
|
+
try:
|
|
565
|
+
metadata = _project_summary_metadata(repo_path)
|
|
566
|
+
with _project_summary_cache_lock:
|
|
567
|
+
_cached_project_summary_metadata[repo_path] = (time.monotonic(), metadata)
|
|
568
|
+
finally:
|
|
569
|
+
with _project_summary_cache_lock:
|
|
570
|
+
_refreshing_project_summary_paths.discard(repo_path)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _schedule_project_summary_metadata_refresh(repo_paths: list[str]) -> None:
|
|
574
|
+
now = time.monotonic()
|
|
575
|
+
scheduled: list[str] = []
|
|
576
|
+
with _project_summary_cache_lock:
|
|
577
|
+
for repo_path in dict.fromkeys(path for path in repo_paths if path):
|
|
578
|
+
cached = _cached_project_summary_metadata.get(repo_path)
|
|
579
|
+
if (
|
|
580
|
+
cached is not None
|
|
581
|
+
and now - cached[0] < PROJECT_SUMMARY_METADATA_CACHE_TTL_SECONDS
|
|
582
|
+
):
|
|
583
|
+
continue
|
|
584
|
+
if repo_path in _refreshing_project_summary_paths:
|
|
585
|
+
continue
|
|
586
|
+
_refreshing_project_summary_paths.add(repo_path)
|
|
587
|
+
scheduled.append(repo_path)
|
|
588
|
+
|
|
589
|
+
for repo_path in scheduled:
|
|
590
|
+
_project_summary_executor.submit(
|
|
591
|
+
_refresh_project_summary_metadata_task, repo_path
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _project_summary_payload(repo_path: str | Path) -> dict[str, Any]:
|
|
596
|
+
path_key = str(repo_path)
|
|
597
|
+
metadata, is_fresh = _cached_project_metadata(path_key)
|
|
598
|
+
if not is_fresh:
|
|
599
|
+
_schedule_project_summary_metadata_refresh([path_key])
|
|
600
|
+
return {
|
|
601
|
+
**_project_summary_static_payload(repo_path),
|
|
602
|
+
**(metadata or _project_summary_fallback_metadata()),
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def get_project_summary(repo_path: str | Path) -> dict[str, Any]:
|
|
607
|
+
return {
|
|
608
|
+
**_project_summary_static_payload(repo_path),
|
|
609
|
+
**_project_summary_metadata(repo_path),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
505
613
|
def get_projects_summary(repo_paths: list[str]) -> list[dict[str, Any]]:
|
|
506
|
-
return [
|
|
614
|
+
return [_project_summary_payload(repo_path) for repo_path in repo_paths]
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def refresh_projects_summary(repo_paths: list[str]) -> list[dict[str, Any]]:
|
|
618
|
+
unique_paths = list(dict.fromkeys(path for path in repo_paths if path))
|
|
619
|
+
futures = {
|
|
620
|
+
_project_summary_executor.submit(
|
|
621
|
+
_project_summary_metadata, repo_path
|
|
622
|
+
): repo_path
|
|
623
|
+
for repo_path in unique_paths
|
|
624
|
+
}
|
|
625
|
+
refreshed: dict[str, dict[str, Any]] = {}
|
|
626
|
+
deadline = time.monotonic() + PROJECT_SUMMARY_TIMEOUT_SECONDS
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
for future in as_completed(futures, timeout=PROJECT_SUMMARY_TIMEOUT_SECONDS):
|
|
630
|
+
repo_path = futures[future]
|
|
631
|
+
remaining = max(0.0, deadline - time.monotonic())
|
|
632
|
+
try:
|
|
633
|
+
metadata = future.result(timeout=remaining)
|
|
634
|
+
except Exception:
|
|
635
|
+
metadata = _project_summary_fallback_metadata()
|
|
636
|
+
refreshed[repo_path] = metadata
|
|
637
|
+
with _project_summary_cache_lock:
|
|
638
|
+
_cached_project_summary_metadata[repo_path] = (
|
|
639
|
+
time.monotonic(),
|
|
640
|
+
metadata,
|
|
641
|
+
)
|
|
642
|
+
except TimeoutError:
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
return [
|
|
646
|
+
{
|
|
647
|
+
**_project_summary_static_payload(repo_path),
|
|
648
|
+
**refreshed.get(repo_path, _project_summary_fallback_metadata()),
|
|
649
|
+
}
|
|
650
|
+
for repo_path in unique_paths
|
|
651
|
+
]
|
|
507
652
|
|
|
508
653
|
|
|
509
654
|
def get_project_detail(repo_path: str | Path) -> dict[str, Any]:
|
|
@@ -74,7 +74,7 @@ def _search_entries(paths: Paths) -> List[str]:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def _subrepo_generated_entries(repo_path: Path, paths: Paths) -> List[str]:
|
|
77
|
-
entries = [
|
|
77
|
+
entries = []
|
|
78
78
|
if would_generate_agents_for_repo(repo_path, paths):
|
|
79
79
|
entries.extend(["CLAUDE.md", "AGENTS.md"])
|
|
80
80
|
repo_vscode_dir = repo_path / ".vscode"
|
|
@@ -82,7 +82,13 @@ def _subrepo_generated_entries(repo_path: Path, paths: Paths) -> List[str]:
|
|
|
82
82
|
repo_vscode_dir / "settings.local.json"
|
|
83
83
|
).exists():
|
|
84
84
|
entries.append(".vscode/settings.json")
|
|
85
|
-
return entries
|
|
85
|
+
return _with_generated_files_header(entries)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _with_generated_files_header(entries: List[str]) -> List[str]:
|
|
89
|
+
if not entries:
|
|
90
|
+
return []
|
|
91
|
+
return ["# These files are generated by multi.", *entries]
|
|
86
92
|
|
|
87
93
|
|
|
88
94
|
def clear_subrepo_generated_file_block(repo_path: Path) -> None:
|
|
@@ -135,7 +141,6 @@ def remove_ignore_entries_for_repos(paths: Paths, repo_names: List[str]) -> None
|
|
|
135
141
|
|
|
136
142
|
def update_gitignore_with_generated_files(paths: Paths):
|
|
137
143
|
root_generated_entries = [
|
|
138
|
-
"# These files are generated by multi.",
|
|
139
144
|
".vscode/settings.json",
|
|
140
145
|
".vscode/tasks.json",
|
|
141
146
|
".vscode/launch.json",
|
|
@@ -144,7 +149,7 @@ def update_gitignore_with_generated_files(paths: Paths):
|
|
|
144
149
|
if would_generate_agents_for_repo(paths.root_dir, paths, is_root=True):
|
|
145
150
|
root_generated_entries.extend(["CLAUDE.md", "AGENTS.md"])
|
|
146
151
|
IgnoreFile(paths.gitignore_path, GENERATED_FILES_BLOCK).set_managed_lines(
|
|
147
|
-
root_generated_entries
|
|
152
|
+
_with_generated_files_header(root_generated_entries)
|
|
148
153
|
)
|
|
149
154
|
|
|
150
155
|
if paths.settings.is_monorepo() or not paths.settings.get("repos"):
|
|
@@ -12,6 +12,7 @@ from multi.app_api import (
|
|
|
12
12
|
get_history_group_diff,
|
|
13
13
|
get_project_detail,
|
|
14
14
|
get_projects_summary,
|
|
15
|
+
refresh_projects_summary,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
@@ -66,6 +67,18 @@ class MultiServiceHandler(BaseHTTPRequestHandler):
|
|
|
66
67
|
)
|
|
67
68
|
return
|
|
68
69
|
|
|
70
|
+
if self.path == "/v1/projects/status":
|
|
71
|
+
repo_paths = body.get("repoPaths", [])
|
|
72
|
+
if not isinstance(repo_paths, list) or not all(
|
|
73
|
+
isinstance(item, str) for item in repo_paths
|
|
74
|
+
):
|
|
75
|
+
raise ValueError("repoPaths must be a list of strings.")
|
|
76
|
+
self._write_json(
|
|
77
|
+
HTTPStatus.OK,
|
|
78
|
+
{"projects": refresh_projects_summary(repo_paths)},
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
69
82
|
if self.path == "/v1/project/detail":
|
|
70
83
|
repo_path = body.get("repoPath")
|
|
71
84
|
if not isinstance(repo_path, str) or not repo_path:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|