git-ssh-sync 0.1.0__py3-none-any.whl
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.
- git_ssh_sync/__init__.py +3 -0
- git_ssh_sync/branch.py +205 -0
- git_ssh_sync/cli.py +228 -0
- git_ssh_sync/clone.py +92 -0
- git_ssh_sync/config.py +212 -0
- git_ssh_sync/console.py +5 -0
- git_ssh_sync/doctor.py +588 -0
- git_ssh_sync/errors.py +37 -0
- git_ssh_sync/git.py +186 -0
- git_ssh_sync/ssh.py +55 -0
- git_ssh_sync/status.py +228 -0
- git_ssh_sync/sync.py +415 -0
- git_ssh_sync-0.1.0.dist-info/METADATA +294 -0
- git_ssh_sync-0.1.0.dist-info/RECORD +16 -0
- git_ssh_sync-0.1.0.dist-info/WHEEL +4 -0
- git_ssh_sync-0.1.0.dist-info/entry_points.txt +3 -0
git_ssh_sync/git.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Local Git command execution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from git_ssh_sync.console import console
|
|
12
|
+
from git_ssh_sync.errors import CommandExecutionError, format_command
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CommandResult:
|
|
17
|
+
"""Completed command details for callers that need user-facing output."""
|
|
18
|
+
|
|
19
|
+
environment: str
|
|
20
|
+
command: tuple[str, ...]
|
|
21
|
+
returncode: int
|
|
22
|
+
stdout: str
|
|
23
|
+
stderr: str
|
|
24
|
+
cwd: Path | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _merged_env(env: Mapping[str, str] | None) -> dict[str, str] | None:
|
|
28
|
+
if env is None:
|
|
29
|
+
return None
|
|
30
|
+
return {**os.environ, **env}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run_command(
|
|
34
|
+
command: Sequence[str],
|
|
35
|
+
*,
|
|
36
|
+
environment: str,
|
|
37
|
+
cwd: str | Path | None = None,
|
|
38
|
+
env: Mapping[str, str] | None = None,
|
|
39
|
+
verbose: bool = False,
|
|
40
|
+
check: bool = True,
|
|
41
|
+
) -> CommandResult:
|
|
42
|
+
cwd_path = Path(cwd) if cwd is not None else None
|
|
43
|
+
command_tuple = tuple(str(part) for part in command)
|
|
44
|
+
if verbose:
|
|
45
|
+
console.print(f"$ {format_command(command_tuple)}")
|
|
46
|
+
|
|
47
|
+
completed = subprocess.run(
|
|
48
|
+
list(command_tuple),
|
|
49
|
+
cwd=cwd_path,
|
|
50
|
+
env=_merged_env(env),
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
check=False,
|
|
54
|
+
)
|
|
55
|
+
result = CommandResult(
|
|
56
|
+
environment=environment,
|
|
57
|
+
command=command_tuple,
|
|
58
|
+
returncode=completed.returncode,
|
|
59
|
+
stdout=completed.stdout,
|
|
60
|
+
stderr=completed.stderr,
|
|
61
|
+
cwd=cwd_path,
|
|
62
|
+
)
|
|
63
|
+
if check and completed.returncode != 0:
|
|
64
|
+
raise CommandExecutionError(
|
|
65
|
+
environment=environment,
|
|
66
|
+
command=command_tuple,
|
|
67
|
+
returncode=completed.returncode,
|
|
68
|
+
cwd=cwd_path,
|
|
69
|
+
stdout=completed.stdout,
|
|
70
|
+
stderr=completed.stderr,
|
|
71
|
+
)
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_git(
|
|
76
|
+
args: Sequence[str],
|
|
77
|
+
*,
|
|
78
|
+
cwd: str | Path | None = None,
|
|
79
|
+
env: Mapping[str, str] | None = None,
|
|
80
|
+
verbose: bool = False,
|
|
81
|
+
check: bool = True,
|
|
82
|
+
) -> CommandResult:
|
|
83
|
+
"""Run a local git command using argv arguments."""
|
|
84
|
+
return _run_command(
|
|
85
|
+
["git", *args],
|
|
86
|
+
environment="local",
|
|
87
|
+
cwd=cwd,
|
|
88
|
+
env=env,
|
|
89
|
+
verbose=verbose,
|
|
90
|
+
check=check,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def fetch(
|
|
95
|
+
remote: str = "origin",
|
|
96
|
+
refspecs: Sequence[str] = (),
|
|
97
|
+
*,
|
|
98
|
+
cwd: str | Path | None = None,
|
|
99
|
+
env: Mapping[str, str] | None = None,
|
|
100
|
+
verbose: bool = False,
|
|
101
|
+
) -> CommandResult:
|
|
102
|
+
"""Run `git fetch`."""
|
|
103
|
+
return run_git(["fetch", remote, *refspecs], cwd=cwd, env=env, verbose=verbose)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def push(
|
|
107
|
+
remote: str = "origin",
|
|
108
|
+
refspecs: Sequence[str] = (),
|
|
109
|
+
*,
|
|
110
|
+
cwd: str | Path | None = None,
|
|
111
|
+
env: Mapping[str, str] | None = None,
|
|
112
|
+
verbose: bool = False,
|
|
113
|
+
) -> CommandResult:
|
|
114
|
+
"""Run `git push`."""
|
|
115
|
+
return run_git(["push", remote, *refspecs], cwd=cwd, env=env, verbose=verbose)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def rev_parse(
|
|
119
|
+
revisions: Sequence[str],
|
|
120
|
+
*,
|
|
121
|
+
cwd: str | Path | None = None,
|
|
122
|
+
env: Mapping[str, str] | None = None,
|
|
123
|
+
verbose: bool = False,
|
|
124
|
+
check: bool = True,
|
|
125
|
+
) -> CommandResult:
|
|
126
|
+
"""Run `git rev-parse`."""
|
|
127
|
+
return run_git(
|
|
128
|
+
["rev-parse", *revisions], cwd=cwd, env=env, verbose=verbose, check=check
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def log_oneline(
|
|
133
|
+
revision: str = "HEAD",
|
|
134
|
+
*,
|
|
135
|
+
cwd: str | Path | None = None,
|
|
136
|
+
env: Mapping[str, str] | None = None,
|
|
137
|
+
verbose: bool = False,
|
|
138
|
+
) -> CommandResult:
|
|
139
|
+
"""Run `git log -1 --format=%h %s` for a revision."""
|
|
140
|
+
return run_git(
|
|
141
|
+
["log", "-1", "--format=%h %s", revision], cwd=cwd, env=env, verbose=verbose
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def status_porcelain(
|
|
146
|
+
*,
|
|
147
|
+
cwd: str | Path | None = None,
|
|
148
|
+
env: Mapping[str, str] | None = None,
|
|
149
|
+
verbose: bool = False,
|
|
150
|
+
) -> CommandResult:
|
|
151
|
+
"""Run `git status --porcelain`."""
|
|
152
|
+
return run_git(["status", "--porcelain"], cwd=cwd, env=env, verbose=verbose)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def merge_base(
|
|
156
|
+
left: str,
|
|
157
|
+
right: str,
|
|
158
|
+
*,
|
|
159
|
+
cwd: str | Path | None = None,
|
|
160
|
+
env: Mapping[str, str] | None = None,
|
|
161
|
+
verbose: bool = False,
|
|
162
|
+
) -> CommandResult:
|
|
163
|
+
"""Run `git merge-base`."""
|
|
164
|
+
return run_git(["merge-base", left, right], cwd=cwd, env=env, verbose=verbose)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def rev_list(
|
|
168
|
+
revisions: Sequence[str],
|
|
169
|
+
*,
|
|
170
|
+
cwd: str | Path | None = None,
|
|
171
|
+
env: Mapping[str, str] | None = None,
|
|
172
|
+
verbose: bool = False,
|
|
173
|
+
) -> CommandResult:
|
|
174
|
+
"""Run `git rev-list`."""
|
|
175
|
+
return run_git(["rev-list", *revisions], cwd=cwd, env=env, verbose=verbose)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def remote(
|
|
179
|
+
args: Sequence[str] = (),
|
|
180
|
+
*,
|
|
181
|
+
cwd: str | Path | None = None,
|
|
182
|
+
env: Mapping[str, str] | None = None,
|
|
183
|
+
verbose: bool = False,
|
|
184
|
+
) -> CommandResult:
|
|
185
|
+
"""Run `git remote`."""
|
|
186
|
+
return run_git(["remote", *args], cwd=cwd, env=env, verbose=verbose)
|
git_ssh_sync/ssh.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""SSH command execution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from git_ssh_sync.git import CommandResult, _run_command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_ssh(
|
|
13
|
+
host: str,
|
|
14
|
+
command: Sequence[str],
|
|
15
|
+
*,
|
|
16
|
+
user: str | None = None,
|
|
17
|
+
cwd: str | Path | None = None,
|
|
18
|
+
env: Mapping[str, str] | None = None,
|
|
19
|
+
verbose: bool = False,
|
|
20
|
+
check: bool = True,
|
|
21
|
+
) -> CommandResult:
|
|
22
|
+
"""Run a command on an SSH host."""
|
|
23
|
+
target = f"{user}@{host}" if user else host
|
|
24
|
+
remote_command = shlex.join(str(part) for part in command)
|
|
25
|
+
return _run_command(
|
|
26
|
+
["ssh", target, remote_command],
|
|
27
|
+
environment=f"ssh:{target}",
|
|
28
|
+
cwd=cwd,
|
|
29
|
+
env=env,
|
|
30
|
+
verbose=verbose,
|
|
31
|
+
check=check,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_remote_git(
|
|
36
|
+
host: str,
|
|
37
|
+
repo_path: str | Path,
|
|
38
|
+
args: Sequence[str],
|
|
39
|
+
*,
|
|
40
|
+
user: str | None = None,
|
|
41
|
+
cwd: str | Path | None = None,
|
|
42
|
+
env: Mapping[str, str] | None = None,
|
|
43
|
+
verbose: bool = False,
|
|
44
|
+
check: bool = True,
|
|
45
|
+
) -> CommandResult:
|
|
46
|
+
"""Run `git -C <path> ...` on an SSH host."""
|
|
47
|
+
return run_ssh(
|
|
48
|
+
host,
|
|
49
|
+
["git", "-C", str(repo_path), *args],
|
|
50
|
+
user=user,
|
|
51
|
+
cwd=cwd,
|
|
52
|
+
env=env,
|
|
53
|
+
verbose=verbose,
|
|
54
|
+
check=check,
|
|
55
|
+
)
|
git_ssh_sync/status.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Project status inspection workflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from rich.markup import escape
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from git_ssh_sync import git, ssh
|
|
13
|
+
from git_ssh_sync.config import ProjectConfig, get_project, load_config
|
|
14
|
+
from git_ssh_sync.console import console
|
|
15
|
+
from git_ssh_sync.errors import CommandExecutionError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StatusError(RuntimeError):
|
|
19
|
+
"""Raised when project status cannot be inspected."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class StatusReport:
|
|
24
|
+
"""Collected synchronization status for a configured project."""
|
|
25
|
+
|
|
26
|
+
project: str
|
|
27
|
+
origin_url: str
|
|
28
|
+
branch: str
|
|
29
|
+
origin_head: str
|
|
30
|
+
dev_host: str
|
|
31
|
+
dev_work_path: str
|
|
32
|
+
dev_branch: str
|
|
33
|
+
dev_head: str
|
|
34
|
+
dev_working_tree_clean: bool
|
|
35
|
+
origin_ahead: int
|
|
36
|
+
dev_ahead: int
|
|
37
|
+
uses_lfs: bool
|
|
38
|
+
uses_submodules: bool
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ssh_repo_url(*, host: str, user: str, repo_path: str) -> str:
|
|
42
|
+
quoted_path = quote(repo_path, safe="/~")
|
|
43
|
+
return f"ssh://{user}@{host}{quoted_path}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _clean_output(value: str) -> str:
|
|
47
|
+
return value.strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ensure_remote_work_repo(*, host: str, user: str, path: str) -> None:
|
|
51
|
+
result = ssh.run_ssh(host, ["test", "-d", path], user=user, check=False)
|
|
52
|
+
if result.returncode == 0:
|
|
53
|
+
return
|
|
54
|
+
if result.returncode == 1:
|
|
55
|
+
raise StatusError(
|
|
56
|
+
f"[{result.environment}] work repository does not exist: {path}"
|
|
57
|
+
)
|
|
58
|
+
raise CommandExecutionError(
|
|
59
|
+
environment=result.environment,
|
|
60
|
+
command=result.command,
|
|
61
|
+
returncode=result.returncode,
|
|
62
|
+
cwd=result.cwd,
|
|
63
|
+
stdout=result.stdout,
|
|
64
|
+
stderr=result.stderr,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _split_ahead_counts(output: str) -> tuple[int, int]:
|
|
69
|
+
parts = output.split()
|
|
70
|
+
if len(parts) != 2:
|
|
71
|
+
raise StatusError(f"Unexpected rev-list output: {output.strip()}")
|
|
72
|
+
return int(parts[0]), int(parts[1])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _uses_lfs(local_path: Path) -> bool:
|
|
76
|
+
result = git.run_git(["lfs", "ls-files"], cwd=local_path, check=False)
|
|
77
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
attributes_path = local_path / ".gitattributes"
|
|
81
|
+
if not attributes_path.exists():
|
|
82
|
+
return False
|
|
83
|
+
return "filter=lfs" in attributes_path.read_text(encoding="utf-8")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _uses_submodules(local_path: Path) -> bool:
|
|
87
|
+
return (local_path / ".gitmodules").exists()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def inspect_status(project: str) -> StatusReport:
|
|
91
|
+
"""Inspect configured origin and development repository status."""
|
|
92
|
+
app_config = load_config()
|
|
93
|
+
project_config = get_project(app_config, project)
|
|
94
|
+
return inspect_project_status(project, project_config)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def inspect_project_status(project: str, project_config: ProjectConfig) -> StatusReport:
|
|
98
|
+
"""Inspect a project using an already loaded configuration."""
|
|
99
|
+
local_path = Path(project_config.local.repo_path)
|
|
100
|
+
dev_host = project_config.dev.host
|
|
101
|
+
dev_user = project_config.dev.user
|
|
102
|
+
dev_work_path = project_config.dev.work_path
|
|
103
|
+
|
|
104
|
+
if not local_path.exists():
|
|
105
|
+
raise StatusError(f"[local] gateway repository does not exist: {local_path}")
|
|
106
|
+
|
|
107
|
+
git.fetch("origin", cwd=local_path)
|
|
108
|
+
ssh.run_ssh(dev_host, ["true"], user=dev_user)
|
|
109
|
+
_ensure_remote_work_repo(host=dev_host, user=dev_user, path=dev_work_path)
|
|
110
|
+
|
|
111
|
+
dev_branch = _clean_output(
|
|
112
|
+
ssh.run_remote_git(
|
|
113
|
+
dev_host, dev_work_path, ["branch", "--show-current"], user=dev_user
|
|
114
|
+
).stdout
|
|
115
|
+
)
|
|
116
|
+
if not dev_branch:
|
|
117
|
+
raise StatusError("Development work repository is in detached HEAD state.")
|
|
118
|
+
branch = dev_branch
|
|
119
|
+
|
|
120
|
+
dev_repo_url = _ssh_repo_url(host=dev_host, user=dev_user, repo_path=dev_work_path)
|
|
121
|
+
git.fetch(
|
|
122
|
+
dev_repo_url, [f"refs/heads/{branch}:refs/remotes/dev/{branch}"], cwd=local_path
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
origin_ref = f"origin/{branch}"
|
|
126
|
+
dev_ref = f"dev/{branch}"
|
|
127
|
+
origin_head = _clean_output(git.log_oneline(origin_ref, cwd=local_path).stdout)
|
|
128
|
+
dev_head = _clean_output(git.log_oneline(dev_ref, cwd=local_path).stdout)
|
|
129
|
+
|
|
130
|
+
remote_head = _clean_output(
|
|
131
|
+
ssh.run_remote_git(
|
|
132
|
+
dev_host, dev_work_path, ["log", "-1", "--format=%h %s"], user=dev_user
|
|
133
|
+
).stdout
|
|
134
|
+
)
|
|
135
|
+
remote_status = ssh.run_remote_git(
|
|
136
|
+
dev_host, dev_work_path, ["status", "--porcelain"], user=dev_user
|
|
137
|
+
)
|
|
138
|
+
origin_ahead, dev_ahead = _split_ahead_counts(
|
|
139
|
+
git.rev_list(
|
|
140
|
+
["--left-right", "--count", f"{origin_ref}...{dev_ref}"], cwd=local_path
|
|
141
|
+
).stdout
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return StatusReport(
|
|
145
|
+
project=project,
|
|
146
|
+
origin_url=project_config.origin,
|
|
147
|
+
branch=branch,
|
|
148
|
+
origin_head=origin_head,
|
|
149
|
+
dev_host=dev_host,
|
|
150
|
+
dev_work_path=dev_work_path,
|
|
151
|
+
dev_branch=dev_branch,
|
|
152
|
+
dev_head=remote_head or dev_head,
|
|
153
|
+
dev_working_tree_clean=not remote_status.stdout.strip(),
|
|
154
|
+
origin_ahead=origin_ahead,
|
|
155
|
+
dev_ahead=dev_ahead,
|
|
156
|
+
uses_lfs=_uses_lfs(local_path),
|
|
157
|
+
uses_submodules=_uses_submodules(local_path),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _recommendation(report: StatusReport) -> str:
|
|
162
|
+
if not report.dev_working_tree_clean:
|
|
163
|
+
return "Commit or stash changes on the development environment."
|
|
164
|
+
if report.origin_ahead and report.dev_ahead:
|
|
165
|
+
return f"git-ssh-sync pull {report.project}, then resolve divergence on the development environment."
|
|
166
|
+
if report.origin_ahead:
|
|
167
|
+
return f"git-ssh-sync pull {report.project}"
|
|
168
|
+
if report.dev_ahead:
|
|
169
|
+
return f"git-ssh-sync push {report.project}"
|
|
170
|
+
return "No action needed."
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _state_lines(report: StatusReport) -> list[str]:
|
|
174
|
+
lines = [
|
|
175
|
+
f"dev is ahead of origin by {report.dev_ahead} commits",
|
|
176
|
+
f"origin is ahead of dev by {report.origin_ahead} commits",
|
|
177
|
+
]
|
|
178
|
+
if not report.dev_working_tree_clean:
|
|
179
|
+
lines.append("development working tree is dirty")
|
|
180
|
+
return lines
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def print_status(report: StatusReport) -> None:
|
|
184
|
+
"""Print a Rich-formatted status report."""
|
|
185
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
186
|
+
table.add_column("section", style="bold")
|
|
187
|
+
table.add_column("field")
|
|
188
|
+
table.add_column("value")
|
|
189
|
+
|
|
190
|
+
table.add_row("Project", "name", escape(report.project))
|
|
191
|
+
table.add_row("", "", "")
|
|
192
|
+
table.add_row("Origin", "url", escape(report.origin_url))
|
|
193
|
+
table.add_row("", "branch", escape(report.branch))
|
|
194
|
+
table.add_row("", "head", escape(report.origin_head))
|
|
195
|
+
table.add_row("", "", "")
|
|
196
|
+
table.add_row("Development", "host", escape(report.dev_host))
|
|
197
|
+
table.add_row("", "work path", escape(report.dev_work_path))
|
|
198
|
+
table.add_row("", "branch", escape(report.dev_branch))
|
|
199
|
+
table.add_row("", "head", escape(report.dev_head))
|
|
200
|
+
table.add_row(
|
|
201
|
+
"",
|
|
202
|
+
"working tree",
|
|
203
|
+
"clean" if report.dev_working_tree_clean else "[yellow]dirty[/yellow]",
|
|
204
|
+
)
|
|
205
|
+
table.add_row("", "", "")
|
|
206
|
+
for index, line in enumerate(_state_lines(report)):
|
|
207
|
+
table.add_row("State" if index == 0 else "", "", escape(line))
|
|
208
|
+
table.add_row("", "", "")
|
|
209
|
+
table.add_row("Recommendation", "", escape(_recommendation(report)))
|
|
210
|
+
console.print(table)
|
|
211
|
+
|
|
212
|
+
if report.uses_lfs or report.uses_submodules:
|
|
213
|
+
console.print()
|
|
214
|
+
if report.uses_lfs:
|
|
215
|
+
console.print("[yellow]This repository appears to use Git LFS.[/yellow]")
|
|
216
|
+
console.print("Git LFS object synchronization is not supported in v0.1.")
|
|
217
|
+
console.print(
|
|
218
|
+
"Normal Git commits may sync, but LFS file contents may be missing."
|
|
219
|
+
)
|
|
220
|
+
if report.uses_submodules:
|
|
221
|
+
console.print("[yellow]This repository uses Git submodules.[/yellow]")
|
|
222
|
+
console.print("Submodule synchronization is not supported in v0.1.")
|
|
223
|
+
console.print("Register each submodule as a separate git-ssh-sync project.")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def status_project(project: str) -> None:
|
|
227
|
+
"""Inspect and print status for a configured project."""
|
|
228
|
+
print_status(inspect_status(project))
|