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/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
+ [![日本語](https://img.shields.io/badge/lang-日本語-blue)](README.ja.md) [![English](https://img.shields.io/badge/lang-English-brightgreen)](README.md)
13
+
14
+ # git-ssh-sync
15
+
16
+ [![CI](https://github.com/devgamesan/git-ssh-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/devgamesan/git-ssh-sync/actions/workflows/ci.yml)
17
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
18
+ ![Python](https://img.shields.io/badge/python-3.12+-blue.svg)
19
+ ![Release](https://img.shields.io/github/v/release/devgamesan/git-ssh-sync)
20
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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)