git-ssh-sync 0.1.0__tar.gz → 0.2.0__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.
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/PKG-INFO +41 -1
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/README.md +40 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/pyproject.toml +1 -1
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/cli.py +155 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/config.py +89 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/__init__.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/branch.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/clone.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/console.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/doctor.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/errors.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/git.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/ssh.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/status.py +0 -0
- {git_ssh_sync-0.1.0 → git_ssh_sync-0.2.0}/src/git_ssh_sync/sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: git-ssh-sync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Sync Git commits through a local machine for development environments without direct GitHub or GitLab access.
|
|
5
5
|
Requires-Dist: pydantic>=2.13.4
|
|
6
6
|
Requires-Dist: pyyaml>=6.0.3
|
|
@@ -21,6 +21,8 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
`git-ssh-sync` is a CLI tool for synchronizing Git commits created in a development environment that cannot directly access GitHub/GitLab to external Git services via a local machine.
|
|
23
23
|
|
|
24
|
+
This tool is designed for niche environments where outbound network access is restricted, such as high-security enterprises and projects that only allow limited inbound communication (e.g., SSH, RDP).
|
|
25
|
+
|
|
24
26
|
This is not a file synchronization tool. It synchronizes Git objects and branches. Source editing, building, testing, and committing are performed in the development environment, while communication with GitHub/GitLab is handled by the local machine.
|
|
25
27
|
|
|
26
28
|
## Prerequisites
|
|
@@ -113,6 +115,28 @@ git-ssh-sync init myproject \
|
|
|
113
115
|
--force
|
|
114
116
|
```
|
|
115
117
|
|
|
118
|
+
You can inspect and maintain registered projects without opening the config file directly.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# List registered projects
|
|
122
|
+
git-ssh-sync config list
|
|
123
|
+
|
|
124
|
+
# Show all settings for one project
|
|
125
|
+
git-ssh-sync config show myproject
|
|
126
|
+
|
|
127
|
+
# Update selected settings
|
|
128
|
+
git-ssh-sync config set myproject \
|
|
129
|
+
--origin git@github.com:example/myproject.git \
|
|
130
|
+
--dev-host devserver \
|
|
131
|
+
--dev-path /home/user/work/myproject
|
|
132
|
+
|
|
133
|
+
# Remove a project after confirmation
|
|
134
|
+
git-ssh-sync config remove myproject
|
|
135
|
+
|
|
136
|
+
# Remove a project without an interactive prompt
|
|
137
|
+
git-ssh-sync config remove myproject --yes
|
|
138
|
+
```
|
|
139
|
+
|
|
116
140
|
## Initial Workflow
|
|
117
141
|
|
|
118
142
|
For the first time, execute configuration, clone to the development environment, and diagnostics in order.
|
|
@@ -244,6 +268,12 @@ git-ssh-sync init myproject \
|
|
|
244
268
|
--dev-user user \
|
|
245
269
|
--dev-path /home/user/work/myproject
|
|
246
270
|
|
|
271
|
+
# List registered project settings
|
|
272
|
+
git-ssh-sync config list
|
|
273
|
+
|
|
274
|
+
# Show registered project settings
|
|
275
|
+
git-ssh-sync config show myproject
|
|
276
|
+
|
|
247
277
|
# Initial clone
|
|
248
278
|
git-ssh-sync clone myproject
|
|
249
279
|
|
|
@@ -277,6 +307,16 @@ To develop this repository itself, install dependencies using `uv sync`.
|
|
|
277
307
|
uv sync
|
|
278
308
|
```
|
|
279
309
|
|
|
310
|
+
To install from TestPyPI:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
uv tool install \
|
|
314
|
+
--index-url https://test.pypi.org/simple/ \
|
|
315
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
316
|
+
--index-strategy unsafe-best-match \
|
|
317
|
+
git-ssh-sync
|
|
318
|
+
```
|
|
319
|
+
|
|
280
320
|
To execute the CLI during development, you can run it via `uv run`.
|
|
281
321
|
|
|
282
322
|
```bash
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
`git-ssh-sync` is a CLI tool for synchronizing Git commits created in a development environment that cannot directly access GitHub/GitLab to external Git services via a local machine.
|
|
12
12
|
|
|
13
|
+
This tool is designed for niche environments where outbound network access is restricted, such as high-security enterprises and projects that only allow limited inbound communication (e.g., SSH, RDP).
|
|
14
|
+
|
|
13
15
|
This is not a file synchronization tool. It synchronizes Git objects and branches. Source editing, building, testing, and committing are performed in the development environment, while communication with GitHub/GitLab is handled by the local machine.
|
|
14
16
|
|
|
15
17
|
## Prerequisites
|
|
@@ -102,6 +104,28 @@ git-ssh-sync init myproject \
|
|
|
102
104
|
--force
|
|
103
105
|
```
|
|
104
106
|
|
|
107
|
+
You can inspect and maintain registered projects without opening the config file directly.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# List registered projects
|
|
111
|
+
git-ssh-sync config list
|
|
112
|
+
|
|
113
|
+
# Show all settings for one project
|
|
114
|
+
git-ssh-sync config show myproject
|
|
115
|
+
|
|
116
|
+
# Update selected settings
|
|
117
|
+
git-ssh-sync config set myproject \
|
|
118
|
+
--origin git@github.com:example/myproject.git \
|
|
119
|
+
--dev-host devserver \
|
|
120
|
+
--dev-path /home/user/work/myproject
|
|
121
|
+
|
|
122
|
+
# Remove a project after confirmation
|
|
123
|
+
git-ssh-sync config remove myproject
|
|
124
|
+
|
|
125
|
+
# Remove a project without an interactive prompt
|
|
126
|
+
git-ssh-sync config remove myproject --yes
|
|
127
|
+
```
|
|
128
|
+
|
|
105
129
|
## Initial Workflow
|
|
106
130
|
|
|
107
131
|
For the first time, execute configuration, clone to the development environment, and diagnostics in order.
|
|
@@ -233,6 +257,12 @@ git-ssh-sync init myproject \
|
|
|
233
257
|
--dev-user user \
|
|
234
258
|
--dev-path /home/user/work/myproject
|
|
235
259
|
|
|
260
|
+
# List registered project settings
|
|
261
|
+
git-ssh-sync config list
|
|
262
|
+
|
|
263
|
+
# Show registered project settings
|
|
264
|
+
git-ssh-sync config show myproject
|
|
265
|
+
|
|
236
266
|
# Initial clone
|
|
237
267
|
git-ssh-sync clone myproject
|
|
238
268
|
|
|
@@ -266,6 +296,16 @@ To develop this repository itself, install dependencies using `uv sync`.
|
|
|
266
296
|
uv sync
|
|
267
297
|
```
|
|
268
298
|
|
|
299
|
+
To install from TestPyPI:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
uv tool install \
|
|
303
|
+
--index-url https://test.pypi.org/simple/ \
|
|
304
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
305
|
+
--index-strategy unsafe-best-match \
|
|
306
|
+
git-ssh-sync
|
|
307
|
+
```
|
|
308
|
+
|
|
269
309
|
To execute the CLI during development, you can run it via `uv run`.
|
|
270
310
|
|
|
271
311
|
```bash
|
|
@@ -4,15 +4,22 @@ from typing import Annotated
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
from rich.markup import escape
|
|
7
|
+
from rich.table import Table
|
|
7
8
|
|
|
8
9
|
from git_ssh_sync import __version__
|
|
9
10
|
from git_ssh_sync.branch import BranchError, branch_project
|
|
10
11
|
from git_ssh_sync.clone import CloneError, clone_project
|
|
11
12
|
from git_ssh_sync.config import (
|
|
12
13
|
ConfigError,
|
|
14
|
+
NoConfigUpdateError,
|
|
13
15
|
ProjectAlreadyExistsError,
|
|
16
|
+
get_project,
|
|
17
|
+
list_project_names,
|
|
14
18
|
default_config_path,
|
|
15
19
|
init_project,
|
|
20
|
+
load_config,
|
|
21
|
+
remove_project,
|
|
22
|
+
update_project,
|
|
16
23
|
)
|
|
17
24
|
from git_ssh_sync.console import console
|
|
18
25
|
from git_ssh_sync.doctor import DoctorError, doctor_project
|
|
@@ -25,6 +32,8 @@ app = typer.Typer(
|
|
|
25
32
|
help="Sync Git commits through a local machine over SSH.",
|
|
26
33
|
no_args_is_help=True,
|
|
27
34
|
)
|
|
35
|
+
config_app = typer.Typer(help="Manage registered project configuration.")
|
|
36
|
+
app.add_typer(config_app, name="config")
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
def _version_callback(value: bool) -> None:
|
|
@@ -55,6 +64,152 @@ def _not_implemented(command: str, project: str | None = None) -> None:
|
|
|
55
64
|
)
|
|
56
65
|
|
|
57
66
|
|
|
67
|
+
@config_app.command("list")
|
|
68
|
+
def config_list_command() -> None:
|
|
69
|
+
"""List registered projects."""
|
|
70
|
+
try:
|
|
71
|
+
config = load_config()
|
|
72
|
+
except ConfigError as error:
|
|
73
|
+
console.print(f"[red]{escape(str(error))}[/red]")
|
|
74
|
+
raise typer.Exit(code=1) from error
|
|
75
|
+
|
|
76
|
+
names = list_project_names(config)
|
|
77
|
+
if not names:
|
|
78
|
+
console.print("No projects configured.")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
console.print("Configured projects:")
|
|
82
|
+
for name in names:
|
|
83
|
+
project_config = config.projects[name]
|
|
84
|
+
console.print(f"- {escape(name)}")
|
|
85
|
+
console.print(f" origin: {escape(project_config.origin)}")
|
|
86
|
+
console.print(f" local repo: {escape(project_config.local.repo_path)}")
|
|
87
|
+
console.print(f" dev host: {escape(project_config.dev.host)}")
|
|
88
|
+
console.print(f" dev path: {escape(project_config.dev.work_path)}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@config_app.command("show")
|
|
92
|
+
def config_show_command(
|
|
93
|
+
project: Annotated[str, typer.Argument(help="Project name to show.")],
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Show one registered project configuration."""
|
|
96
|
+
try:
|
|
97
|
+
project_config = get_project(load_config(), project)
|
|
98
|
+
except ConfigError as error:
|
|
99
|
+
console.print(f"[red]{escape(str(error))}[/red]")
|
|
100
|
+
raise typer.Exit(code=1) from error
|
|
101
|
+
|
|
102
|
+
table = Table(title=f"Project configuration: {escape(project)}")
|
|
103
|
+
table.add_column("Section", overflow="fold")
|
|
104
|
+
table.add_column("Name", overflow="fold")
|
|
105
|
+
table.add_column("Value", overflow="fold")
|
|
106
|
+
table.add_row("project", "name", escape(project))
|
|
107
|
+
table.add_row("origin", "url", escape(project_config.origin))
|
|
108
|
+
table.add_row("local", "repo_path", escape(project_config.local.repo_path))
|
|
109
|
+
table.add_row("dev", "host", escape(project_config.dev.host))
|
|
110
|
+
table.add_row("dev", "user", escape(project_config.dev.user))
|
|
111
|
+
table.add_row("dev", "work_path", escape(project_config.dev.work_path))
|
|
112
|
+
table.add_row("dev", "cache_path", escape(project_config.dev.cache_path))
|
|
113
|
+
table.add_row("options", "sync_tags", str(project_config.options.sync_tags))
|
|
114
|
+
table.add_row("options", "lfs", str(project_config.options.lfs))
|
|
115
|
+
table.add_row("options", "submodules", str(project_config.options.submodules))
|
|
116
|
+
table.add_row("options", "ff_only", str(project_config.options.ff_only))
|
|
117
|
+
console.print(table)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@config_app.command("remove")
|
|
121
|
+
def config_remove_command(
|
|
122
|
+
project: Annotated[str, typer.Argument(help="Project name to remove.")],
|
|
123
|
+
yes: Annotated[
|
|
124
|
+
bool,
|
|
125
|
+
typer.Option("--yes", "-y", help="Remove without confirmation."),
|
|
126
|
+
] = False,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Remove a registered project configuration."""
|
|
129
|
+
if not yes and not typer.confirm(f"Remove project '{project}'?"):
|
|
130
|
+
console.print("Aborted.")
|
|
131
|
+
raise typer.Exit(code=1)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
remove_project(project)
|
|
135
|
+
except ConfigError as error:
|
|
136
|
+
console.print(f"[red]{escape(str(error))}[/red]")
|
|
137
|
+
raise typer.Exit(code=1) from error
|
|
138
|
+
|
|
139
|
+
console.print(f"Project '{project}' removed from {default_config_path()}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@config_app.command("set")
|
|
143
|
+
def config_set_command(
|
|
144
|
+
project: Annotated[str, typer.Argument(help="Project name to update.")],
|
|
145
|
+
origin: Annotated[
|
|
146
|
+
str | None,
|
|
147
|
+
typer.Option("--origin", help="Origin Git URL."),
|
|
148
|
+
] = None,
|
|
149
|
+
local_repo_path: Annotated[
|
|
150
|
+
str | None,
|
|
151
|
+
typer.Option("--local-repo-path", help="Local gateway repository path."),
|
|
152
|
+
] = None,
|
|
153
|
+
dev_host: Annotated[
|
|
154
|
+
str | None,
|
|
155
|
+
typer.Option("--dev-host", help="Development environment SSH host."),
|
|
156
|
+
] = None,
|
|
157
|
+
dev_user: Annotated[
|
|
158
|
+
str | None,
|
|
159
|
+
typer.Option("--dev-user", help="Development environment SSH user."),
|
|
160
|
+
] = None,
|
|
161
|
+
dev_path: Annotated[
|
|
162
|
+
str | None,
|
|
163
|
+
typer.Option(
|
|
164
|
+
"--dev-path", help="Development environment work repository path."
|
|
165
|
+
),
|
|
166
|
+
] = None,
|
|
167
|
+
dev_cache_path: Annotated[
|
|
168
|
+
str | None,
|
|
169
|
+
typer.Option("--dev-cache-path", help="Development cache repository path."),
|
|
170
|
+
] = None,
|
|
171
|
+
sync_tags: Annotated[
|
|
172
|
+
bool | None,
|
|
173
|
+
typer.Option("--sync-tags/--no-sync-tags", help="Enable or disable tag sync."),
|
|
174
|
+
] = None,
|
|
175
|
+
lfs: Annotated[
|
|
176
|
+
bool | None,
|
|
177
|
+
typer.Option("--lfs/--no-lfs", help="Enable or disable Git LFS handling."),
|
|
178
|
+
] = None,
|
|
179
|
+
submodules: Annotated[
|
|
180
|
+
bool | None,
|
|
181
|
+
typer.Option(
|
|
182
|
+
"--submodules/--no-submodules",
|
|
183
|
+
help="Enable or disable submodule handling.",
|
|
184
|
+
),
|
|
185
|
+
] = None,
|
|
186
|
+
ff_only: Annotated[
|
|
187
|
+
bool | None,
|
|
188
|
+
typer.Option("--ff-only/--no-ff-only", help="Enable or disable ff-only sync."),
|
|
189
|
+
] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Update one or more fields in a registered project configuration."""
|
|
192
|
+
try:
|
|
193
|
+
update_project(
|
|
194
|
+
project,
|
|
195
|
+
origin=origin,
|
|
196
|
+
local_repo_path=local_repo_path,
|
|
197
|
+
dev_host=dev_host,
|
|
198
|
+
dev_user=dev_user,
|
|
199
|
+
dev_work_path=dev_path,
|
|
200
|
+
dev_cache_path=dev_cache_path,
|
|
201
|
+
sync_tags=sync_tags,
|
|
202
|
+
lfs=lfs,
|
|
203
|
+
submodules=submodules,
|
|
204
|
+
ff_only=ff_only,
|
|
205
|
+
)
|
|
206
|
+
except (ConfigError, NoConfigUpdateError) as error:
|
|
207
|
+
console.print(f"[red]{escape(str(error))}[/red]")
|
|
208
|
+
raise typer.Exit(code=1) from error
|
|
209
|
+
|
|
210
|
+
console.print(f"Project '{project}' updated in {default_config_path()}")
|
|
211
|
+
|
|
212
|
+
|
|
58
213
|
@app.command("init")
|
|
59
214
|
def init_command(
|
|
60
215
|
project: Annotated[str, typer.Argument(help="Project name to register.")],
|
|
@@ -23,6 +23,10 @@ class ProjectNotFoundError(ConfigError):
|
|
|
23
23
|
"""Raised when a project is not registered."""
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class NoConfigUpdateError(ConfigError):
|
|
27
|
+
"""Raised when no project configuration updates were provided."""
|
|
28
|
+
|
|
29
|
+
|
|
26
30
|
def _expand_path(value: str) -> str:
|
|
27
31
|
return str(Path(value).expanduser())
|
|
28
32
|
|
|
@@ -127,6 +131,91 @@ def get_project(config: AppConfig, project: str) -> ProjectConfig:
|
|
|
127
131
|
raise ProjectNotFoundError(f"Project '{project}' is not configured.") from error
|
|
128
132
|
|
|
129
133
|
|
|
134
|
+
def list_project_names(config: AppConfig) -> list[str]:
|
|
135
|
+
"""Return configured project names sorted for stable CLI output."""
|
|
136
|
+
return sorted(config.projects)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def remove_project(
|
|
140
|
+
project: str,
|
|
141
|
+
*,
|
|
142
|
+
config_path: Path | None = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Remove a project from config.yaml."""
|
|
145
|
+
config = load_config(config_path)
|
|
146
|
+
get_project(config, project)
|
|
147
|
+
projects = dict(config.projects)
|
|
148
|
+
del projects[project]
|
|
149
|
+
save_config(config.model_copy(update={"projects": projects}), config_path)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def update_project(
|
|
153
|
+
project: str,
|
|
154
|
+
*,
|
|
155
|
+
origin: str | None = None,
|
|
156
|
+
local_repo_path: str | None = None,
|
|
157
|
+
dev_host: str | None = None,
|
|
158
|
+
dev_user: str | None = None,
|
|
159
|
+
dev_work_path: str | None = None,
|
|
160
|
+
dev_cache_path: str | None = None,
|
|
161
|
+
sync_tags: bool | None = None,
|
|
162
|
+
lfs: bool | None = None,
|
|
163
|
+
submodules: bool | None = None,
|
|
164
|
+
ff_only: bool | None = None,
|
|
165
|
+
config_path: Path | None = None,
|
|
166
|
+
) -> ProjectConfig:
|
|
167
|
+
"""Partially update an existing project in config.yaml."""
|
|
168
|
+
config = load_config(config_path)
|
|
169
|
+
current = get_project(config, project)
|
|
170
|
+
raw = current.model_dump(mode="json")
|
|
171
|
+
|
|
172
|
+
updated = False
|
|
173
|
+
if origin is not None:
|
|
174
|
+
raw["origin"] = origin
|
|
175
|
+
updated = True
|
|
176
|
+
if local_repo_path is not None:
|
|
177
|
+
raw["local"]["repo_path"] = local_repo_path
|
|
178
|
+
updated = True
|
|
179
|
+
if dev_host is not None:
|
|
180
|
+
raw["dev"]["host"] = dev_host
|
|
181
|
+
updated = True
|
|
182
|
+
if dev_user is not None:
|
|
183
|
+
raw["dev"]["user"] = dev_user
|
|
184
|
+
updated = True
|
|
185
|
+
if dev_work_path is not None:
|
|
186
|
+
raw["dev"]["work_path"] = dev_work_path
|
|
187
|
+
updated = True
|
|
188
|
+
if dev_cache_path is not None:
|
|
189
|
+
raw["dev"]["cache_path"] = dev_cache_path
|
|
190
|
+
updated = True
|
|
191
|
+
|
|
192
|
+
for key, value in {
|
|
193
|
+
"sync_tags": sync_tags,
|
|
194
|
+
"lfs": lfs,
|
|
195
|
+
"submodules": submodules,
|
|
196
|
+
"ff_only": ff_only,
|
|
197
|
+
}.items():
|
|
198
|
+
if value is not None:
|
|
199
|
+
raw["options"][key] = value
|
|
200
|
+
updated = True
|
|
201
|
+
|
|
202
|
+
if not updated:
|
|
203
|
+
raise NoConfigUpdateError("Specify at least one setting to update.")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
updated_project = ProjectConfig.model_validate(raw)
|
|
207
|
+
except ValidationError as error:
|
|
208
|
+
raise ConfigError(
|
|
209
|
+
format_validation_error_for_project(project, error)
|
|
210
|
+
) from error
|
|
211
|
+
|
|
212
|
+
save_config(
|
|
213
|
+
register_project(config, project, updated_project, force=True),
|
|
214
|
+
config_path,
|
|
215
|
+
)
|
|
216
|
+
return updated_project
|
|
217
|
+
|
|
218
|
+
|
|
130
219
|
def build_project_config(
|
|
131
220
|
project: str,
|
|
132
221
|
*,
|
|
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
|