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/sync.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Safe pull and checkout workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from git_ssh_sync import git, ssh
|
|
9
|
+
from git_ssh_sync.config import ProjectConfig, get_project, load_config
|
|
10
|
+
from git_ssh_sync.console import console
|
|
11
|
+
from git_ssh_sync.errors import CommandExecutionError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SyncError(RuntimeError):
|
|
15
|
+
"""Raised when a sync workflow stops for a recoverable safety reason."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cache_url(*, host: str, user: str, cache_path: str) -> str:
|
|
19
|
+
quoted_path = quote(cache_path, safe="/~")
|
|
20
|
+
return f"ssh://{user}@{host}{quoted_path}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _work_url(project_config: ProjectConfig) -> str:
|
|
24
|
+
quoted_path = quote(project_config.dev.work_path, safe="/~")
|
|
25
|
+
return f"ssh://{project_config.dev.user}@{project_config.dev.host}{quoted_path}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _clean_output(value: str) -> str:
|
|
29
|
+
return value.strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ensure_gateway_repo(path: Path) -> None:
|
|
33
|
+
if not path.exists():
|
|
34
|
+
raise SyncError(f"[local] gateway repository does not exist: {path}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _origin_branch_exists(local_path: Path, branch: str) -> bool:
|
|
38
|
+
result = git.run_git(
|
|
39
|
+
["show-ref", "--verify", "--quiet", f"refs/remotes/origin/{branch}"],
|
|
40
|
+
cwd=local_path,
|
|
41
|
+
check=False,
|
|
42
|
+
)
|
|
43
|
+
if result.returncode == 0:
|
|
44
|
+
return True
|
|
45
|
+
if result.returncode == 1:
|
|
46
|
+
return False
|
|
47
|
+
raise CommandExecutionError(
|
|
48
|
+
environment=result.environment,
|
|
49
|
+
command=result.command,
|
|
50
|
+
returncode=result.returncode,
|
|
51
|
+
cwd=result.cwd,
|
|
52
|
+
stdout=result.stdout,
|
|
53
|
+
stderr=result.stderr,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_origin_branch(local_path: Path, branch: str) -> None:
|
|
58
|
+
if not _origin_branch_exists(local_path, branch):
|
|
59
|
+
raise SyncError(
|
|
60
|
+
f"Origin branch does not exist: {branch}\n\n"
|
|
61
|
+
"Run on the gateway repository:\n\n"
|
|
62
|
+
" git fetch origin"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ensure_origin_branch_missing(project: str, local_path: Path, branch: str) -> None:
|
|
67
|
+
if _origin_branch_exists(local_path, branch):
|
|
68
|
+
raise SyncError(
|
|
69
|
+
f"Origin branch already exists: {branch}\n\n"
|
|
70
|
+
"Run checkout without --base to use the existing branch:\n\n"
|
|
71
|
+
f" git-ssh-sync checkout {project} {branch}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _create_origin_branch(local_path: Path, branch: str, base_branch: str) -> None:
|
|
76
|
+
git.push(
|
|
77
|
+
"origin",
|
|
78
|
+
[f"refs/remotes/origin/{base_branch}:refs/heads/{branch}"],
|
|
79
|
+
cwd=local_path,
|
|
80
|
+
)
|
|
81
|
+
git.fetch(
|
|
82
|
+
"origin", [f"refs/heads/{branch}:refs/remotes/origin/{branch}"], cwd=local_path
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _push_origin_branch_to_cache(
|
|
87
|
+
project_config: ProjectConfig, local_path: Path, branch: str
|
|
88
|
+
) -> None:
|
|
89
|
+
remote_cache = _cache_url(
|
|
90
|
+
host=project_config.dev.host,
|
|
91
|
+
user=project_config.dev.user,
|
|
92
|
+
cache_path=project_config.dev.cache_path,
|
|
93
|
+
)
|
|
94
|
+
git.push(
|
|
95
|
+
remote_cache,
|
|
96
|
+
[f"refs/remotes/origin/{branch}:refs/heads/{branch}"],
|
|
97
|
+
cwd=local_path,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _fetch_dev_branch(project_config: ProjectConfig, branch: str) -> None:
|
|
102
|
+
ssh.run_remote_git(
|
|
103
|
+
project_config.dev.host,
|
|
104
|
+
project_config.dev.work_path,
|
|
105
|
+
["fetch", "gitsync", f"refs/heads/{branch}:refs/remotes/gitsync/{branch}"],
|
|
106
|
+
user=project_config.dev.user,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _remote_branch_exists(project_config: ProjectConfig, branch: str) -> bool:
|
|
111
|
+
result = ssh.run_remote_git(
|
|
112
|
+
project_config.dev.host,
|
|
113
|
+
project_config.dev.work_path,
|
|
114
|
+
["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
|
|
115
|
+
user=project_config.dev.user,
|
|
116
|
+
check=False,
|
|
117
|
+
)
|
|
118
|
+
if result.returncode == 0:
|
|
119
|
+
return True
|
|
120
|
+
if result.returncode == 1:
|
|
121
|
+
return False
|
|
122
|
+
raise CommandExecutionError(
|
|
123
|
+
environment=result.environment,
|
|
124
|
+
command=result.command,
|
|
125
|
+
returncode=result.returncode,
|
|
126
|
+
cwd=result.cwd,
|
|
127
|
+
stdout=result.stdout,
|
|
128
|
+
stderr=result.stderr,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _remote_current_branch(project_config: ProjectConfig) -> str:
|
|
133
|
+
result = ssh.run_remote_git(
|
|
134
|
+
project_config.dev.host,
|
|
135
|
+
project_config.dev.work_path,
|
|
136
|
+
["branch", "--show-current"],
|
|
137
|
+
user=project_config.dev.user,
|
|
138
|
+
)
|
|
139
|
+
return _clean_output(result.stdout) or "(detached)"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _remote_gitsync_url(project_config: ProjectConfig) -> str:
|
|
143
|
+
result = ssh.run_remote_git(
|
|
144
|
+
project_config.dev.host,
|
|
145
|
+
project_config.dev.work_path,
|
|
146
|
+
["remote", "get-url", "gitsync"],
|
|
147
|
+
user=project_config.dev.user,
|
|
148
|
+
)
|
|
149
|
+
return _clean_output(result.stdout)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _ensure_gitsync_remote_matches(project: str, project_config: ProjectConfig) -> None:
|
|
153
|
+
actual_url = _remote_gitsync_url(project_config)
|
|
154
|
+
expected_url = project_config.dev.cache_path
|
|
155
|
+
if actual_url == expected_url:
|
|
156
|
+
return
|
|
157
|
+
raise SyncError(
|
|
158
|
+
"Development work repository gitsync remote does not match the configured cache path.\n\n"
|
|
159
|
+
f"Project:\n {project}\n\n"
|
|
160
|
+
"Development:\n"
|
|
161
|
+
f" host: {project_config.dev.host}\n"
|
|
162
|
+
f" path: {project_config.dev.work_path}\n\n"
|
|
163
|
+
f"Configured cache path:\n {expected_url}\n\n"
|
|
164
|
+
f"Actual gitsync remote:\n {actual_url}\n\n"
|
|
165
|
+
"Update the work repo remote or recreate the project clone:\n\n"
|
|
166
|
+
f" git -C {project_config.dev.work_path} remote set-url gitsync {expected_url}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _require_remote_current_branch(project_config: ProjectConfig) -> str:
|
|
171
|
+
branch = _remote_current_branch(project_config)
|
|
172
|
+
if branch == "(detached)":
|
|
173
|
+
raise SyncError(
|
|
174
|
+
"Development work repository is in detached HEAD state.\n\n"
|
|
175
|
+
"Switch to a branch on the development environment or run:\n\n"
|
|
176
|
+
" git-ssh-sync checkout <project> <branch>"
|
|
177
|
+
)
|
|
178
|
+
return branch
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _remote_short_head(project_config: ProjectConfig) -> str:
|
|
182
|
+
result = ssh.run_remote_git(
|
|
183
|
+
project_config.dev.host,
|
|
184
|
+
project_config.dev.work_path,
|
|
185
|
+
["rev-parse", "--short", "HEAD"],
|
|
186
|
+
user=project_config.dev.user,
|
|
187
|
+
)
|
|
188
|
+
return _clean_output(result.stdout)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _remote_status(project_config: ProjectConfig) -> str:
|
|
192
|
+
result = ssh.run_remote_git(
|
|
193
|
+
project_config.dev.host,
|
|
194
|
+
project_config.dev.work_path,
|
|
195
|
+
["status", "--porcelain"],
|
|
196
|
+
user=project_config.dev.user,
|
|
197
|
+
)
|
|
198
|
+
return _clean_output(result.stdout)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _fetch_dev_branch_to_local(
|
|
202
|
+
project_config: ProjectConfig, local_path: Path, branch: str
|
|
203
|
+
) -> None:
|
|
204
|
+
git.fetch(
|
|
205
|
+
_work_url(project_config),
|
|
206
|
+
[f"refs/heads/{branch}:refs/remotes/dev/{branch}"],
|
|
207
|
+
cwd=local_path,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _dirty_error(project: str, project_config: ProjectConfig) -> SyncError:
|
|
212
|
+
return SyncError(
|
|
213
|
+
"Error: Development working tree is dirty.\n\n"
|
|
214
|
+
f"Project:\n {project}\n\n"
|
|
215
|
+
"Development:\n"
|
|
216
|
+
f" host: {project_config.dev.host}\n"
|
|
217
|
+
f" path: {project_config.dev.work_path}\n"
|
|
218
|
+
f" branch: {_remote_current_branch(project_config)}\n"
|
|
219
|
+
f" commit: {_remote_short_head(project_config)}\n\n"
|
|
220
|
+
"Commit or stash changes first."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _ensure_dev_clean(project: str, project_config: ProjectConfig) -> None:
|
|
225
|
+
if _remote_status(project_config):
|
|
226
|
+
raise _dirty_error(project, project_config)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _switch_to_branch(project_config: ProjectConfig, branch: str) -> None:
|
|
230
|
+
if _remote_branch_exists(project_config, branch):
|
|
231
|
+
ssh.run_remote_git(
|
|
232
|
+
project_config.dev.host,
|
|
233
|
+
project_config.dev.work_path,
|
|
234
|
+
["switch", branch],
|
|
235
|
+
user=project_config.dev.user,
|
|
236
|
+
)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
ssh.run_remote_git(
|
|
240
|
+
project_config.dev.host,
|
|
241
|
+
project_config.dev.work_path,
|
|
242
|
+
["switch", "--track", "-c", branch, f"gitsync/{branch}"],
|
|
243
|
+
user=project_config.dev.user,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _ensure_fast_forwardable(
|
|
248
|
+
project: str, project_config: ProjectConfig, branch: str
|
|
249
|
+
) -> None:
|
|
250
|
+
result = ssh.run_remote_git(
|
|
251
|
+
project_config.dev.host,
|
|
252
|
+
project_config.dev.work_path,
|
|
253
|
+
[
|
|
254
|
+
"merge-base",
|
|
255
|
+
"--is-ancestor",
|
|
256
|
+
f"refs/heads/{branch}",
|
|
257
|
+
f"refs/remotes/gitsync/{branch}",
|
|
258
|
+
],
|
|
259
|
+
user=project_config.dev.user,
|
|
260
|
+
check=False,
|
|
261
|
+
)
|
|
262
|
+
if result.returncode == 0:
|
|
263
|
+
return
|
|
264
|
+
if result.returncode == 1:
|
|
265
|
+
current_branch = _remote_current_branch(project_config)
|
|
266
|
+
current_commit = _remote_short_head(project_config)
|
|
267
|
+
raise SyncError(
|
|
268
|
+
f"Cannot fast-forward {branch}.\n\n"
|
|
269
|
+
f"origin/{branch} and dev/{branch} have diverged.\n\n"
|
|
270
|
+
f"Project:\n {project}\n\n"
|
|
271
|
+
"Development:\n"
|
|
272
|
+
f" host: {project_config.dev.host}\n"
|
|
273
|
+
f" path: {project_config.dev.work_path}\n"
|
|
274
|
+
f" branch: {current_branch}\n"
|
|
275
|
+
f" commit: {current_commit}\n\n"
|
|
276
|
+
"Resolve on the development environment:\n\n"
|
|
277
|
+
" git fetch gitsync\n"
|
|
278
|
+
f" git merge gitsync/{branch}\n\n"
|
|
279
|
+
"or:\n\n"
|
|
280
|
+
f" git rebase gitsync/{branch}"
|
|
281
|
+
)
|
|
282
|
+
raise CommandExecutionError(
|
|
283
|
+
environment=result.environment,
|
|
284
|
+
command=result.command,
|
|
285
|
+
returncode=result.returncode,
|
|
286
|
+
cwd=result.cwd,
|
|
287
|
+
stdout=result.stdout,
|
|
288
|
+
stderr=result.stderr,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _ensure_pushable(
|
|
293
|
+
project: str, project_config: ProjectConfig, local_path: Path, branch: str
|
|
294
|
+
) -> None:
|
|
295
|
+
result = git.run_git(
|
|
296
|
+
[
|
|
297
|
+
"merge-base",
|
|
298
|
+
"--is-ancestor",
|
|
299
|
+
f"refs/remotes/origin/{branch}",
|
|
300
|
+
f"refs/remotes/dev/{branch}",
|
|
301
|
+
],
|
|
302
|
+
cwd=local_path,
|
|
303
|
+
check=False,
|
|
304
|
+
)
|
|
305
|
+
if result.returncode == 0:
|
|
306
|
+
return
|
|
307
|
+
if result.returncode == 1:
|
|
308
|
+
current_branch = _remote_current_branch(project_config)
|
|
309
|
+
current_commit = _remote_short_head(project_config)
|
|
310
|
+
raise SyncError(
|
|
311
|
+
f"Cannot push {branch}.\n\n"
|
|
312
|
+
f"origin/{branch} has commits that are not included in dev/{branch}.\n\n"
|
|
313
|
+
f"Project:\n {project}\n\n"
|
|
314
|
+
"Development:\n"
|
|
315
|
+
f" host: {project_config.dev.host}\n"
|
|
316
|
+
f" path: {project_config.dev.work_path}\n"
|
|
317
|
+
f" branch: {current_branch}\n"
|
|
318
|
+
f" commit: {current_commit}\n\n"
|
|
319
|
+
"Run:\n\n"
|
|
320
|
+
f" git-ssh-sync pull {project}\n\n"
|
|
321
|
+
"Then resolve the branch on the development environment before pushing again."
|
|
322
|
+
)
|
|
323
|
+
raise CommandExecutionError(
|
|
324
|
+
environment=result.environment,
|
|
325
|
+
command=result.command,
|
|
326
|
+
returncode=result.returncode,
|
|
327
|
+
cwd=result.cwd,
|
|
328
|
+
stdout=result.stdout,
|
|
329
|
+
stderr=result.stderr,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _load_project(project: str) -> ProjectConfig:
|
|
334
|
+
return get_project(load_config(), project)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def pull_project(project: str) -> None:
|
|
338
|
+
"""Fetch origin changes and fast-forward the current development branch."""
|
|
339
|
+
project_config = _load_project(project)
|
|
340
|
+
local_path = Path(project_config.local.repo_path)
|
|
341
|
+
|
|
342
|
+
_ensure_gateway_repo(local_path)
|
|
343
|
+
_ensure_gitsync_remote_matches(project, project_config)
|
|
344
|
+
selected_branch = _require_remote_current_branch(project_config)
|
|
345
|
+
console.print(f"Project: {project}")
|
|
346
|
+
console.print(f"Branch: {selected_branch}")
|
|
347
|
+
console.print("Direction: origin -> development")
|
|
348
|
+
git.fetch("origin", cwd=local_path)
|
|
349
|
+
_ensure_origin_branch(local_path, selected_branch)
|
|
350
|
+
_push_origin_branch_to_cache(project_config, local_path, selected_branch)
|
|
351
|
+
_fetch_dev_branch(project_config, selected_branch)
|
|
352
|
+
|
|
353
|
+
_ensure_fast_forwardable(project, project_config, selected_branch)
|
|
354
|
+
ssh.run_remote_git(
|
|
355
|
+
project_config.dev.host,
|
|
356
|
+
project_config.dev.work_path,
|
|
357
|
+
["merge", "--ff-only", f"gitsync/{selected_branch}"],
|
|
358
|
+
user=project_config.dev.user,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def checkout_project(
|
|
363
|
+
project: str,
|
|
364
|
+
branch: str,
|
|
365
|
+
*,
|
|
366
|
+
create: bool = False,
|
|
367
|
+
base_branch: str | None = None,
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Switch the development repository to a branch from origin."""
|
|
370
|
+
project_config = _load_project(project)
|
|
371
|
+
local_path = Path(project_config.local.repo_path)
|
|
372
|
+
|
|
373
|
+
_ensure_gateway_repo(local_path)
|
|
374
|
+
_ensure_gitsync_remote_matches(project, project_config)
|
|
375
|
+
git.fetch("origin", cwd=local_path)
|
|
376
|
+
if create:
|
|
377
|
+
base = base_branch or _require_remote_current_branch(project_config)
|
|
378
|
+
_ensure_origin_branch(local_path, base)
|
|
379
|
+
_ensure_origin_branch_missing(project, local_path, branch)
|
|
380
|
+
_create_origin_branch(local_path, branch, base)
|
|
381
|
+
_ensure_origin_branch(local_path, branch)
|
|
382
|
+
_push_origin_branch_to_cache(project_config, local_path, branch)
|
|
383
|
+
_fetch_dev_branch(project_config, branch)
|
|
384
|
+
_ensure_dev_clean(project, project_config)
|
|
385
|
+
_switch_to_branch(project_config, branch)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def push_project(project: str) -> None:
|
|
389
|
+
"""Push current development branch commits to origin when it has not diverged."""
|
|
390
|
+
project_config = _load_project(project)
|
|
391
|
+
local_path = Path(project_config.local.repo_path)
|
|
392
|
+
|
|
393
|
+
_ensure_gateway_repo(local_path)
|
|
394
|
+
_ensure_gitsync_remote_matches(project, project_config)
|
|
395
|
+
selected_branch = _require_remote_current_branch(project_config)
|
|
396
|
+
console.print(f"Project: {project}")
|
|
397
|
+
console.print(f"Branch: {selected_branch}")
|
|
398
|
+
console.print("Direction: development -> origin")
|
|
399
|
+
git.fetch("origin", cwd=local_path)
|
|
400
|
+
_ensure_origin_branch(local_path, selected_branch)
|
|
401
|
+
_fetch_dev_branch_to_local(project_config, local_path, selected_branch)
|
|
402
|
+
_ensure_pushable(project, project_config, local_path, selected_branch)
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
git.push(
|
|
406
|
+
"origin",
|
|
407
|
+
[f"refs/remotes/dev/{selected_branch}:refs/heads/{selected_branch}"],
|
|
408
|
+
cwd=local_path,
|
|
409
|
+
)
|
|
410
|
+
except CommandExecutionError as error:
|
|
411
|
+
raise SyncError(
|
|
412
|
+
f"Failed to push {selected_branch} to origin.\n\n"
|
|
413
|
+
f"Project:\n {project}\n\n"
|
|
414
|
+
f"Origin push failed:\n{error}"
|
|
415
|
+
) from error
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: git-ssh-sync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sync Git commits through a local machine for development environments without direct GitHub or GitLab access.
|
|
5
|
+
Requires-Dist: pydantic>=2.13.4
|
|
6
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
7
|
+
Requires-Dist: rich>=13.7.0
|
|
8
|
+
Requires-Dist: typer>=0.12.0
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
[](README.ja.md) [](README.md)
|
|
13
|
+
|
|
14
|
+
# git-ssh-sync
|
|
15
|
+
|
|
16
|
+
[](https://github.com/devgamesan/git-ssh-sync/actions/workflows/ci.yml)
|
|
17
|
+

|
|
18
|
+

|
|
19
|
+

|
|
20
|
+
[](https://github.com/astral-sh/ruff)
|
|
21
|
+
|
|
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
|
+
|
|
24
|
+
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
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
`git-ssh-sync` assumes the following configuration:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
GitHub / GitLab
|
|
32
|
+
↑↓
|
|
33
|
+
Local machine
|
|
34
|
+
↑↓ SSH
|
|
35
|
+
Development environment
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Local machine:
|
|
39
|
+
|
|
40
|
+
- Can access GitHub / GitLab
|
|
41
|
+
- Can SSH to the development environment
|
|
42
|
+
- Has `git` and `uv` available
|
|
43
|
+
- Uses `git-ssh-sync` for commit synchronization, status checks, and diagnostics between GitHub/GitLab and the development environment
|
|
44
|
+
|
|
45
|
+
Development environment:
|
|
46
|
+
|
|
47
|
+
- Can be accessed via SSH from the local machine
|
|
48
|
+
- Cannot directly access GitHub / GitLab from the development environment
|
|
49
|
+
- Has `git` available
|
|
50
|
+
- Performs source editing, building, testing, and committing
|
|
51
|
+
- Synchronizes with GitHub/GitLab via the local machine
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
For normal use, install on your local machine using `uv tool install`.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install git-ssh-sync
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For unreleased versions or the latest repository version, install directly from GitHub.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv tool install git+https://github.com/devgamesan/git-ssh-sync.git
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
After installation, verify that the command can be executed.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git-ssh-sync --help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
First, register the project you want to synchronize.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git-ssh-sync init myproject \
|
|
79
|
+
--origin git@github.com:example/myproject.git \
|
|
80
|
+
--dev-host devserver \
|
|
81
|
+
--dev-user user \
|
|
82
|
+
--dev-path /home/user/work/myproject
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Key parameters:
|
|
86
|
+
|
|
87
|
+
- `myproject`: Project name for `git-ssh-sync`
|
|
88
|
+
- `--origin`: Repository URL on the GitHub / GitLab side
|
|
89
|
+
- `--dev-host`: SSH host of the development environment
|
|
90
|
+
- `--dev-user`: SSH user of the development environment
|
|
91
|
+
- `--dev-path`: Path to the work repository on the development environment
|
|
92
|
+
|
|
93
|
+
For `--origin`, specify a remote URL that can be used with `git clone` or `git fetch`. Main formats are:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
git@github.com:example/myproject.git
|
|
97
|
+
git@gitlab.com:example/myproject.git
|
|
98
|
+
ssh://git@github.com/example/myproject.git
|
|
99
|
+
https://github.com/example/myproject.git
|
|
100
|
+
https://gitlab.com/example/myproject.git
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
When using SSH format, prepare SSH keys and authentication settings for connecting to GitHub/GitLab on the local machine. The development environment does not connect directly to GitHub/GitLab.
|
|
104
|
+
|
|
105
|
+
To overwrite existing configuration, use `--force`.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git-ssh-sync init myproject \
|
|
109
|
+
--origin git@github.com:example/myproject.git \
|
|
110
|
+
--dev-host devserver \
|
|
111
|
+
--dev-user user \
|
|
112
|
+
--dev-path /home/user/work/myproject \
|
|
113
|
+
--force
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Initial Workflow
|
|
117
|
+
|
|
118
|
+
For the first time, execute configuration, clone to the development environment, and diagnostics in order.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git-ssh-sync init myproject \
|
|
122
|
+
--origin git@github.com:example/myproject.git \
|
|
123
|
+
--dev-host devserver \
|
|
124
|
+
--dev-user user \
|
|
125
|
+
--dev-path /home/user/work/myproject
|
|
126
|
+
git-ssh-sync clone myproject
|
|
127
|
+
git-ssh-sync doctor myproject
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`clone` creates a gateway repository on your local machine and deploys cache and work repositories on the development environment.
|
|
131
|
+
|
|
132
|
+
- Gateway repository: Relay repository on the local machine
|
|
133
|
+
- Cache repository: Bare repository on the development environment
|
|
134
|
+
- Work repository: Repository where actual editing, building, testing, and committing are performed on the development environment
|
|
135
|
+
|
|
136
|
+
Afterward, the work repository on the development environment can be used as a normal Git repository.
|
|
137
|
+
|
|
138
|
+
`doctor` checks the local environment, SSH connection, fetch/push permissions to origin, and repository deployment on the development environment. Run this not only for the first time but also when synchronization is not working properly.
|
|
139
|
+
|
|
140
|
+
## Daily Development Workflow
|
|
141
|
+
|
|
142
|
+
For daily development, `pull` from the local machine before starting work, commit normally in the development environment, and finally `push` from the local machine.
|
|
143
|
+
|
|
144
|
+
Local machine:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
git-ssh-sync pull myproject
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Development environment:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
cd ~/work/myproject
|
|
154
|
+
git status
|
|
155
|
+
git add .
|
|
156
|
+
git commit -m "Add feature"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Local machine:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git-ssh-sync push myproject
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`pull` and `push` target the current branch of the work repository on the development environment. To synchronize a different branch, switch the work repository branch with `checkout` first.
|
|
166
|
+
|
|
167
|
+
## Branch Switching Workflow
|
|
168
|
+
|
|
169
|
+
To switch to an existing branch, execute `checkout` from the local machine.
|
|
170
|
+
|
|
171
|
+
Local machine:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git-ssh-sync checkout myproject feature/foo
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
To create a new branch, use `-b`. Use `--base` together to explicitly specify the starting point.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
git-ssh-sync checkout myproject -b feature/foo --base develop
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Development environment:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cd ~/work/myproject
|
|
187
|
+
git status
|
|
188
|
+
git add .
|
|
189
|
+
git commit -m "Implement foo"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Local machine:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
git-ssh-sync push myproject
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`checkout -b feature/foo --base develop` creates `feature/foo` on origin based on `develop` from origin and switches the work repository on the development environment to that branch. If `--base` is omitted, the current branch of the work repository on the development environment is used as the starting point. If a branch with the same name already exists on origin, switch to the existing branch without `-b`.
|
|
199
|
+
|
|
200
|
+
## Status Check
|
|
201
|
+
|
|
202
|
+
Use `status` to check synchronization status.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
git-ssh-sync status myproject
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`status` displays the ahead/behind status between origin and the development environment, and the working tree status for the current branch of the work repository. Follow the displayed recommendation and execute `pull` or `push` as necessary.
|
|
209
|
+
|
|
210
|
+
To list existence status and ahead/behind for each branch, use `branch`.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
git-ssh-sync branch myproject
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Operational Rules
|
|
217
|
+
|
|
218
|
+
When using `git-ssh-sync`, following these rules makes it easier to understand the state:
|
|
219
|
+
|
|
220
|
+
- `pull` on the local machine before starting work
|
|
221
|
+
- Create commits in the development environment
|
|
222
|
+
- `push` on the local machine when work is done
|
|
223
|
+
- Check `status` when in doubt before/after synchronization
|
|
224
|
+
- Run `doctor` when concerned about connections or repository deployment
|
|
225
|
+
|
|
226
|
+
Uncommitted changes are not synchronized. If there are uncommitted changes in the working tree of the development environment, the changes themselves are not sent to the local machine or origin. Please `git add` and `git commit` changes you want to synchronize in the development environment.
|
|
227
|
+
|
|
228
|
+
`pull` updates the development environment branch only when fast-forward is possible. If origin and the development environment have diverged, automatic merge or automatic rebase is not performed.
|
|
229
|
+
|
|
230
|
+
`push` executes only when the branch on the origin side is an ancestor of the branch on the development environment side. If there are unobtained commits on origin, it stops.
|
|
231
|
+
|
|
232
|
+
When diverged, automatic resolution is not performed. Execute `pull` on the local machine, follow the displayed instructions to merge or rebase in the development environment, then `push` again.
|
|
233
|
+
|
|
234
|
+
## Common Commands
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Display help
|
|
238
|
+
git-ssh-sync --help
|
|
239
|
+
|
|
240
|
+
# Register a project
|
|
241
|
+
git-ssh-sync init myproject \
|
|
242
|
+
--origin git@github.com:example/myproject.git \
|
|
243
|
+
--dev-host devserver \
|
|
244
|
+
--dev-user user \
|
|
245
|
+
--dev-path /home/user/work/myproject
|
|
246
|
+
|
|
247
|
+
# Initial clone
|
|
248
|
+
git-ssh-sync clone myproject
|
|
249
|
+
|
|
250
|
+
# Check synchronization status
|
|
251
|
+
git-ssh-sync status myproject
|
|
252
|
+
|
|
253
|
+
# Check branch status
|
|
254
|
+
git-ssh-sync branch myproject
|
|
255
|
+
|
|
256
|
+
# Reflect changes from origin to development environment
|
|
257
|
+
git-ssh-sync pull myproject
|
|
258
|
+
|
|
259
|
+
# Reflect commits from development environment to origin
|
|
260
|
+
git-ssh-sync push myproject
|
|
261
|
+
|
|
262
|
+
# Switch development environment branch
|
|
263
|
+
git-ssh-sync checkout myproject feature/foo
|
|
264
|
+
|
|
265
|
+
# Create and switch to new branch from base branch
|
|
266
|
+
git-ssh-sync checkout myproject -b feature/foo --base develop
|
|
267
|
+
|
|
268
|
+
# Diagnostics
|
|
269
|
+
git-ssh-sync doctor myproject
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## For Developers
|
|
273
|
+
|
|
274
|
+
To develop this repository itself, install dependencies using `uv sync`.
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
uv sync
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
To execute the CLI during development, you can run it via `uv run`.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
uv run git-ssh-sync --help
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Tests are executed with the following command:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
uv run pytest
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Related Documentation
|
|
293
|
+
|
|
294
|
+
- [Specification](docs/spec.md)
|