docker-stack 2.2.3__tar.gz → 2.2.4__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.
- {docker_stack-2.2.3 → docker_stack-2.2.4}/PKG-INFO +1 -1
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/cli.py +85 -11
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack.egg-info/PKG-INFO +1 -1
- {docker_stack-2.2.3 → docker_stack-2.2.4}/setup.py +1 -1
- {docker_stack-2.2.3 → docker_stack-2.2.4}/tests/test_docker_stack.py +85 -11
- {docker_stack-2.2.3 → docker_stack-2.2.4}/README.md +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/__init__.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/command_runner.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/compose.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/docker_objects.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/envsubst.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/envsubst_merge.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/helpers.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/login.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/manager_api.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/markers.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/merge_conf.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/registry.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/shell_auth.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack/url_parser.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack.egg-info/SOURCES.txt +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack.egg-info/dependency_links.txt +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack.egg-info/entry_points.txt +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack.egg-info/requires.txt +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/docker_stack.egg-info/top_level.txt +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/pyproject.toml +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/setup.cfg +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/tests/test_docker_objects.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/tests/test_load_env.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/tests/test_login.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/tests/test_manager_api.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/tests/test_node_ls.py +0 -0
- {docker_stack-2.2.3 → docker_stack-2.2.4}/tests/test_shell_auth.py +0 -0
|
@@ -350,6 +350,38 @@ def _print_cluster_containers(payload: Dict[str, object], *, no_trunc: bool = Fa
|
|
|
350
350
|
return 1 if errors else 0
|
|
351
351
|
|
|
352
352
|
|
|
353
|
+
def _exec_process(command: List[str], env: Optional[Dict[str, str]] = None) -> int:
|
|
354
|
+
"""Replace this process with ``command`` so it owns the terminal, its signals, and its exit code.
|
|
355
|
+
|
|
356
|
+
Proxying through ``subprocess.run`` keeps Python in the foreground process group: Ctrl+C is
|
|
357
|
+
delivered to the proxy as well as to the child, which surfaces as a ``KeyboardInterrupt``
|
|
358
|
+
traceback, and a signal-killed child reports a negative return code that ``sys.exit`` turns into
|
|
359
|
+
a nonsense status (``-2`` becomes ``254`` instead of ``130``). ``exec`` removes the middleman.
|
|
360
|
+
Only POSIX execs: on Windows ``exec`` detaches the child and hands control straight back, which
|
|
361
|
+
would break the caller's wait semantics.
|
|
362
|
+
|
|
363
|
+
Returns only on failure; the caller keeps its ``int`` contract for that case.
|
|
364
|
+
"""
|
|
365
|
+
if os.name != "posix":
|
|
366
|
+
return subprocess.run(command, check=False, env=env).returncode
|
|
367
|
+
# exec discards Python's buffers, so anything already printed has to reach the fd first.
|
|
368
|
+
sys.stdout.flush()
|
|
369
|
+
sys.stderr.flush()
|
|
370
|
+
try:
|
|
371
|
+
if env is None:
|
|
372
|
+
os.execvp(command[0], command)
|
|
373
|
+
else:
|
|
374
|
+
os.execvpe(command[0], command, env)
|
|
375
|
+
except OSError as exc:
|
|
376
|
+
print(f"docker-stack: cannot execute {command[0]}: {exc}", file=sys.stderr)
|
|
377
|
+
return 127
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _exec_docker(values: List[str]) -> int:
|
|
381
|
+
"""Hand the command to the real Docker CLI, replacing this process."""
|
|
382
|
+
return _exec_process(["docker", *values])
|
|
383
|
+
|
|
384
|
+
|
|
353
385
|
def _managed_docker(arguments: List[str]) -> int:
|
|
354
386
|
values = list(arguments)
|
|
355
387
|
if values and values[0] == "--":
|
|
@@ -378,11 +410,11 @@ def _managed_docker(arguments: List[str]) -> int:
|
|
|
378
410
|
value in {"-q", "--quiet", "--format"} or value.startswith("--format=")
|
|
379
411
|
for value in command
|
|
380
412
|
):
|
|
381
|
-
return
|
|
413
|
+
return _exec_docker(values)
|
|
382
414
|
try:
|
|
383
415
|
client = discover_manager_client()
|
|
384
416
|
if client is None or not client.supports(FEATURE_CLUSTER_CONTAINER_CLI):
|
|
385
|
-
return
|
|
417
|
+
return _exec_docker(values)
|
|
386
418
|
filters = []
|
|
387
419
|
all_containers = False
|
|
388
420
|
latest = False
|
|
@@ -411,7 +443,7 @@ def _managed_docker(arguments: List[str]) -> int:
|
|
|
411
443
|
elif value.startswith("--last="):
|
|
412
444
|
limit = int(value.split("=", 1)[1])
|
|
413
445
|
else:
|
|
414
|
-
return
|
|
446
|
+
return _exec_docker(values)
|
|
415
447
|
index += 1
|
|
416
448
|
payload = client.list_containers(
|
|
417
449
|
all_containers=all_containers,
|
|
@@ -1774,7 +1806,7 @@ def open_context_shell(config_dir: Path, context_name: str) -> int:
|
|
|
1774
1806
|
env["DOCKER_CONTEXT"] = context_name
|
|
1775
1807
|
env["DOCKER_MANAGER_CONTEXT_NAME"] = context_name
|
|
1776
1808
|
shell = env.get("SHELL", "").strip() or "/bin/bash"
|
|
1777
|
-
return
|
|
1809
|
+
return _exec_process([shell, "-i"], env=env)
|
|
1778
1810
|
|
|
1779
1811
|
|
|
1780
1812
|
def active_shell_config_dir(context_name: str) -> Optional[Path]:
|
|
@@ -1805,6 +1837,29 @@ def _error_output(exc: subprocess.CalledProcessError) -> str:
|
|
|
1805
1837
|
return "\n".join(value for value in values if value)
|
|
1806
1838
|
|
|
1807
1839
|
|
|
1840
|
+
_MANAGER_VERSION_PROBE_PATTERN = re.compile(r"manager request failed \(\w+ /version\)", re.IGNORECASE)
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
def _humanize_manager_error(message: str) -> str:
|
|
1844
|
+
"""Render a manager JSON error body as prose instead of dumping the raw payload."""
|
|
1845
|
+
match = re.search(r"^(.*?: HTTP \d+): (\{.*\})$", message, re.DOTALL)
|
|
1846
|
+
if not match:
|
|
1847
|
+
return message
|
|
1848
|
+
try:
|
|
1849
|
+
body = json.loads(match.group(2))
|
|
1850
|
+
except json.JSONDecodeError:
|
|
1851
|
+
return message
|
|
1852
|
+
if not isinstance(body, dict):
|
|
1853
|
+
return message
|
|
1854
|
+
detail = str(body.get("message", "")).strip()
|
|
1855
|
+
if not detail:
|
|
1856
|
+
return message
|
|
1857
|
+
incident = str(body.get("incident_id") or body.get("trace_id") or "").strip()
|
|
1858
|
+
if incident:
|
|
1859
|
+
return f"{match.group(1)}: {detail} (incident {incident})"
|
|
1860
|
+
return f"{match.group(1)}: {detail}"
|
|
1861
|
+
|
|
1862
|
+
|
|
1808
1863
|
def _manager_deploy_suggestion(message: str) -> Optional[str]:
|
|
1809
1864
|
if GITHUB_WORKFLOW_RESTRICTED_MESSAGE in message:
|
|
1810
1865
|
return (
|
|
@@ -1824,10 +1879,16 @@ def _manager_deploy_suggestion(message: str) -> Optional[str]:
|
|
|
1824
1879
|
"by this stack. Either allow that external network in the Docker-Manager deployment rule, attach the service to "
|
|
1825
1880
|
"a stack-owned network, or relabel/recreate the existing network with the expected stack ownership before deploying."
|
|
1826
1881
|
)
|
|
1827
|
-
if "
|
|
1882
|
+
if "stack not found" in lowered:
|
|
1883
|
+
return (
|
|
1884
|
+
"Suggestion: the manager was reached but has no stack by that name in that namespace. "
|
|
1885
|
+
"Run 'docker-stack ls' to list stacks in the current namespace, or 'docker-stack ls -A' to search every namespace."
|
|
1886
|
+
)
|
|
1887
|
+
if _MANAGER_VERSION_PROBE_PATTERN.search(message):
|
|
1828
1888
|
return (
|
|
1829
|
-
"Suggestion:
|
|
1830
|
-
"
|
|
1889
|
+
"Suggestion: docker-stack could not read the manager's /version endpoint. Check that DOCKER_MANAGER_URL points "
|
|
1890
|
+
"at a reachable manager and that the Docker auth headers are set - in CI that means running the docker-stack "
|
|
1891
|
+
"setup action before this step."
|
|
1831
1892
|
)
|
|
1832
1893
|
if "docker-manager is configured" in lowered and "did not advertise required feature" in lowered:
|
|
1833
1894
|
return (
|
|
@@ -1841,7 +1902,7 @@ def _format_called_process_error(exc: subprocess.CalledProcessError) -> str:
|
|
|
1841
1902
|
output = _error_output(exc)
|
|
1842
1903
|
lines = [f"docker-stack: command failed with exit code {exc.returncode}: {_format_command(exc.cmd)}"]
|
|
1843
1904
|
if output:
|
|
1844
|
-
lines.append(output)
|
|
1905
|
+
lines.append(_humanize_manager_error(output))
|
|
1845
1906
|
suggestion = _manager_deploy_suggestion(output)
|
|
1846
1907
|
if suggestion:
|
|
1847
1908
|
lines.extend(["", suggestion])
|
|
@@ -1850,14 +1911,14 @@ def _format_called_process_error(exc: subprocess.CalledProcessError) -> str:
|
|
|
1850
1911
|
|
|
1851
1912
|
def _format_runtime_error(exc: RuntimeError) -> str:
|
|
1852
1913
|
message = str(exc).strip() or exc.__class__.__name__
|
|
1853
|
-
lines = [f"docker-stack: {message}"]
|
|
1914
|
+
lines = [f"docker-stack: {_humanize_manager_error(message)}"]
|
|
1854
1915
|
suggestion = _manager_deploy_suggestion(message)
|
|
1855
1916
|
if suggestion:
|
|
1856
1917
|
lines.extend(["", suggestion])
|
|
1857
1918
|
return "\n".join(lines)
|
|
1858
1919
|
|
|
1859
1920
|
|
|
1860
|
-
def
|
|
1921
|
+
def _run(args: List[str] = None):
|
|
1861
1922
|
parser = argparse.ArgumentParser(description="Deploy and manage Docker stacks.")
|
|
1862
1923
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1863
1924
|
|
|
@@ -2202,5 +2263,18 @@ def main(args: List[str] = None):
|
|
|
2202
2263
|
sys.exit(2)
|
|
2203
2264
|
|
|
2204
2265
|
|
|
2266
|
+
def main(args: List[str] = None):
|
|
2267
|
+
"""Entry point. Ctrl+C has to look like an interrupted shell command, not a Python crash.
|
|
2268
|
+
|
|
2269
|
+
Commands that exec into another binary never reach this handler; it covers the paths that stay
|
|
2270
|
+
in Python (cluster-rendered ``ps``, login, deploy) and any HTTP call they are blocked on.
|
|
2271
|
+
"""
|
|
2272
|
+
try:
|
|
2273
|
+
return _run(args)
|
|
2274
|
+
except KeyboardInterrupt:
|
|
2275
|
+
print(file=sys.stderr)
|
|
2276
|
+
return 130
|
|
2277
|
+
|
|
2278
|
+
|
|
2205
2279
|
if __name__ == "__main__":
|
|
2206
|
-
main()
|
|
2280
|
+
sys.exit(main())
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="docker-stack",
|
|
5
|
-
version="2.2.
|
|
5
|
+
version="2.2.4",
|
|
6
6
|
description="CLI for deploying and managing Docker stacks.",
|
|
7
7
|
long_description=open("README.md").read(), # You can include a README file to describe your package
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -12,6 +12,8 @@ import yaml
|
|
|
12
12
|
from docker_stack.cli import (
|
|
13
13
|
DOCKER_SHELL_ENDPOINT_ENV_VARS,
|
|
14
14
|
Docker,
|
|
15
|
+
_format_runtime_error,
|
|
16
|
+
_exec_process,
|
|
15
17
|
_managed_docker,
|
|
16
18
|
_read_selected_node,
|
|
17
19
|
_select_node,
|
|
@@ -200,11 +202,65 @@ def test_managed_docker_ps_forwards_global_latest_and_limit(monkeypatch):
|
|
|
200
202
|
assert calls[1]["limit"] == 3
|
|
201
203
|
|
|
202
204
|
|
|
203
|
-
def
|
|
205
|
+
def test_exec_process_replaces_the_python_process(monkeypatch):
|
|
206
|
+
calls = []
|
|
207
|
+
monkeypatch.setattr(os, "name", "posix")
|
|
208
|
+
monkeypatch.setattr("docker_stack.cli.os.execvp", lambda file, args: calls.append((file, args)))
|
|
209
|
+
|
|
210
|
+
_exec_process(["docker", "service", "logs", "-f", "svc"])
|
|
211
|
+
|
|
212
|
+
assert calls == [("docker", ["docker", "service", "logs", "-f", "svc"])]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_exec_process_passes_env_when_given(monkeypatch):
|
|
216
|
+
calls = []
|
|
217
|
+
monkeypatch.setattr(os, "name", "posix")
|
|
218
|
+
monkeypatch.setattr("docker_stack.cli.os.execvpe", lambda file, args, env: calls.append((file, args, env)))
|
|
219
|
+
|
|
220
|
+
_exec_process(["/bin/test-shell", "-i"], env={"DOCKER_CONTEXT": "office"})
|
|
221
|
+
|
|
222
|
+
assert calls == [("/bin/test-shell", ["/bin/test-shell", "-i"], {"DOCKER_CONTEXT": "office"})]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_exec_process_reports_missing_binary(monkeypatch, capsys):
|
|
226
|
+
monkeypatch.setattr(os, "name", "posix")
|
|
227
|
+
|
|
228
|
+
def missing(_file, _args):
|
|
229
|
+
raise FileNotFoundError(2, "No such file or directory")
|
|
230
|
+
|
|
231
|
+
monkeypatch.setattr("docker_stack.cli.os.execvp", missing)
|
|
232
|
+
|
|
233
|
+
assert _exec_process(["docker", "ps"]) == 127
|
|
234
|
+
assert "cannot execute docker" in capsys.readouterr().err
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_exec_process_falls_back_to_subprocess_off_posix(monkeypatch):
|
|
204
238
|
calls = []
|
|
239
|
+
monkeypatch.setattr(os, "name", "nt")
|
|
240
|
+
monkeypatch.setattr("docker_stack.cli.os.execvp", lambda *_args: pytest.fail("exec must not run off POSIX"))
|
|
205
241
|
monkeypatch.setattr(
|
|
206
242
|
"docker_stack.cli.subprocess.run",
|
|
207
|
-
lambda command, **_kwargs: calls.append(command) or SimpleNamespace(returncode=
|
|
243
|
+
lambda command, **_kwargs: calls.append(command) or SimpleNamespace(returncode=3),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert _exec_process(["docker", "ps"]) == 3
|
|
247
|
+
assert calls == [["docker", "ps"]]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_main_reports_interrupt_as_shell_exit_code(monkeypatch):
|
|
251
|
+
def interrupted(_args):
|
|
252
|
+
raise KeyboardInterrupt
|
|
253
|
+
|
|
254
|
+
monkeypatch.setattr("docker_stack.cli._run", interrupted)
|
|
255
|
+
|
|
256
|
+
assert main(["ps"]) == 130
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_managed_docker_delegates_format_to_real_docker(monkeypatch):
|
|
260
|
+
calls = []
|
|
261
|
+
monkeypatch.setattr(
|
|
262
|
+
"docker_stack.cli._exec_process",
|
|
263
|
+
lambda command, **_kwargs: calls.append(command) or 7,
|
|
208
264
|
)
|
|
209
265
|
|
|
210
266
|
assert _managed_docker(["ps", "--format", "{{.ID}}"]) == 7
|
|
@@ -224,8 +280,8 @@ def test_managed_docker_delegates_format_to_real_docker(monkeypatch):
|
|
|
224
280
|
def test_managed_docker_delegates_explicit_target_overrides(monkeypatch, arguments):
|
|
225
281
|
calls = []
|
|
226
282
|
monkeypatch.setattr(
|
|
227
|
-
"docker_stack.cli.
|
|
228
|
-
lambda command, **_kwargs: calls.append(command) or
|
|
283
|
+
"docker_stack.cli._exec_process",
|
|
284
|
+
lambda command, **_kwargs: calls.append(command) or 6,
|
|
229
285
|
)
|
|
230
286
|
monkeypatch.setattr(
|
|
231
287
|
"docker_stack.cli.discover_manager_client",
|
|
@@ -241,8 +297,8 @@ def test_managed_docker_ps_delegates_when_manager_lacks_cluster_feature(monkeypa
|
|
|
241
297
|
client = SimpleNamespace(supports=lambda _feature: False)
|
|
242
298
|
monkeypatch.setattr("docker_stack.cli.discover_manager_client", lambda: client)
|
|
243
299
|
monkeypatch.setattr(
|
|
244
|
-
"docker_stack.cli.
|
|
245
|
-
lambda command, **_kwargs: calls.append(command) or
|
|
300
|
+
"docker_stack.cli._exec_process",
|
|
301
|
+
lambda command, **_kwargs: calls.append(command) or 9,
|
|
246
302
|
)
|
|
247
303
|
|
|
248
304
|
assert _managed_docker(["ps"]) == 9
|
|
@@ -1477,18 +1533,16 @@ def test_open_context_shell_clears_endpoint_overrides(monkeypatch, tmp_path):
|
|
|
1477
1533
|
monkeypatch.setenv(key, f"parent-{key}")
|
|
1478
1534
|
monkeypatch.setenv("SHELL", "/bin/test-shell")
|
|
1479
1535
|
|
|
1480
|
-
def
|
|
1536
|
+
def fake_exec(cmd, env=None):
|
|
1481
1537
|
captured["cmd"] = cmd
|
|
1482
|
-
captured["check"] = check
|
|
1483
1538
|
captured["env"] = env
|
|
1484
|
-
return
|
|
1539
|
+
return 0
|
|
1485
1540
|
|
|
1486
|
-
monkeypatch.setattr("docker_stack.cli.
|
|
1541
|
+
monkeypatch.setattr("docker_stack.cli._exec_process", fake_exec)
|
|
1487
1542
|
|
|
1488
1543
|
assert open_context_shell(tmp_path, "office") == 0
|
|
1489
1544
|
|
|
1490
1545
|
assert captured["cmd"] == ["/bin/test-shell", "-i"]
|
|
1491
|
-
assert captured["check"] is False
|
|
1492
1546
|
assert captured["env"]["DOCKER_CONFIG"] == str(tmp_path)
|
|
1493
1547
|
assert captured["env"]["DOCKER_CONTEXT"] == "office"
|
|
1494
1548
|
for key in DOCKER_SHELL_ENDPOINT_ENV_VARS:
|
|
@@ -1573,3 +1627,23 @@ def test_context_use_preserves_auth_when_switching_to_manager(monkeypatch, capsy
|
|
|
1573
1627
|
output = capsys.readouterr().out
|
|
1574
1628
|
assert "DOCKER_CONTEXT=office" in output
|
|
1575
1629
|
assert "auth header preserved" in output
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def test_stack_not_found_error_does_not_suggest_ci_setup():
|
|
1633
|
+
formatted = _format_runtime_error(
|
|
1634
|
+
RuntimeError(
|
|
1635
|
+
"Manager request failed (GET /api/docker-stack/stacks/docker-manager/versions?namespace=infra): "
|
|
1636
|
+
'HTTP 404: {"message":"stack not found","incident_id":"d9d372ed","trace_id":"d9d372ed"}'
|
|
1637
|
+
)
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
assert "HTTP 404: stack not found (incident d9d372ed)" in formatted
|
|
1641
|
+
assert '{"message"' not in formatted
|
|
1642
|
+
assert "has no stack by that name in that namespace" in formatted
|
|
1643
|
+
assert "setup action" not in formatted
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
def test_version_probe_failure_still_suggests_manager_url_check():
|
|
1647
|
+
formatted = _format_runtime_error(RuntimeError("Manager request failed (GET /version): connection refused"))
|
|
1648
|
+
|
|
1649
|
+
assert "DOCKER_MANAGER_URL points at a reachable manager" in formatted
|
|
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
|