git-env 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_env/__init__.py +16 -0
- git_env/cli.py +183 -0
- git_env/completions/_git-env +43 -0
- git_env/completions/git-env.bash +64 -0
- git_env/completions/git-env.fish +32 -0
- git_env/config.py +196 -0
- git_env/discovery.py +204 -0
- git_env/output.py +34 -0
- git_env/repo.py +156 -0
- git_env/shell_completions.py +91 -0
- git_env/sync.py +243 -0
- git_env-0.1.0.dist-info/METADATA +224 -0
- git_env-0.1.0.dist-info/RECORD +15 -0
- git_env-0.1.0.dist-info/WHEEL +4 -0
- git_env-0.1.0.dist-info/entry_points.txt +3 -0
git_env/sync.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Core `git env sync` logic: copy env files from the primary worktree into
|
|
2
|
+
the current linked worktree.
|
|
3
|
+
|
|
4
|
+
Implements the "Copy semantics" and "Conflict handling" rules from spec.md,
|
|
5
|
+
including line-diff summaries, `.envsync.bak` backups, and the
|
|
6
|
+
`env.sync.onConflict` modes. The safety-rail refusals beyond repository
|
|
7
|
+
detection are tracked as separate follow-up work.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import difflib
|
|
13
|
+
import filecmp
|
|
14
|
+
import fnmatch
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from .config import SyncConfig
|
|
21
|
+
from .discovery import DiscoveredFile, discover_env_files
|
|
22
|
+
from .output import Reporter
|
|
23
|
+
from .repo import Repository
|
|
24
|
+
|
|
25
|
+
#: Suffix used for the atomic-write temp file (spec: copy to `<dest>.envsync.tmp`
|
|
26
|
+
#: then `rename(2)` over the destination).
|
|
27
|
+
_TMP_SUFFIX = ".envsync.tmp"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SyncIOError(Exception):
|
|
31
|
+
"""Raised on an I/O failure during copy. Always corresponds to exit code 4."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class SyncResult:
|
|
36
|
+
"""Outcome of a sync run, used to derive the process exit code."""
|
|
37
|
+
|
|
38
|
+
synced: int = 0
|
|
39
|
+
skipped_unchanged: int = 0
|
|
40
|
+
conflicts: int = 0
|
|
41
|
+
would_change: int = 0
|
|
42
|
+
"""Files that would be copied/overwritten; only meaningful for --dry-run."""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def exit_code(self) -> int:
|
|
46
|
+
if self.conflicts:
|
|
47
|
+
return 1
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _filter_path(files: list[DiscoveredFile], path: str | None) -> list[DiscoveredFile]:
|
|
52
|
+
if path is None:
|
|
53
|
+
return files
|
|
54
|
+
prefix = Path(path)
|
|
55
|
+
return [f for f in files if prefix in (f.relative_path, *f.relative_path.parents)]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_dest(worktree_root: Path, relative_path: Path) -> Path:
|
|
59
|
+
"""Resolve `relative_path` against `worktree_root`, refusing escapes.
|
|
60
|
+
|
|
61
|
+
Guards against a pattern or symlinked source resolving outside the
|
|
62
|
+
current worktree (spec: "Never write outside the current worktree root").
|
|
63
|
+
"""
|
|
64
|
+
dest = (worktree_root / relative_path).resolve()
|
|
65
|
+
if worktree_root not in dest.parents and dest != worktree_root:
|
|
66
|
+
raise SyncIOError(
|
|
67
|
+
f"refusing to write outside the worktree: {relative_path}"
|
|
68
|
+
)
|
|
69
|
+
return dest
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _diff_summary(src: Path, dest: Path) -> str:
|
|
73
|
+
"""Describe how `dest` differs from `src`, e.g. "3 lines differ"."""
|
|
74
|
+
try:
|
|
75
|
+
src_lines = src.read_text().splitlines()
|
|
76
|
+
dest_lines = dest.read_text().splitlines()
|
|
77
|
+
except (UnicodeDecodeError, OSError):
|
|
78
|
+
return "binary files differ"
|
|
79
|
+
matcher = difflib.SequenceMatcher(a=dest_lines, b=src_lines)
|
|
80
|
+
changed = sum(
|
|
81
|
+
max(i2 - i1, j2 - j1)
|
|
82
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes()
|
|
83
|
+
if tag != "equal"
|
|
84
|
+
)
|
|
85
|
+
noun = "line" if changed == 1 else "lines"
|
|
86
|
+
return f"{changed} {noun} differ"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _backup(dest: Path) -> None:
|
|
90
|
+
"""Back up `dest`'s current content to `<dest>.envsync.bak` before an
|
|
91
|
+
overwrite (spec: "single backup, overwritten on subsequent forces").
|
|
92
|
+
"""
|
|
93
|
+
backup_path = dest.with_name(dest.name + ".envsync.bak")
|
|
94
|
+
try:
|
|
95
|
+
shutil.copyfile(dest, backup_path)
|
|
96
|
+
shutil.copymode(dest, backup_path)
|
|
97
|
+
except OSError as exc:
|
|
98
|
+
raise SyncIOError(f"failed to back up {dest}: {exc}") from exc
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _prompt_overwrite(rel: Path, diff_desc: str, reporter: Reporter) -> bool:
|
|
102
|
+
try:
|
|
103
|
+
answer = input(f"{rel}: {diff_desc}. Overwrite? [y/N] ")
|
|
104
|
+
except EOFError:
|
|
105
|
+
reporter.warn(f"{rel}: cannot prompt for input, skipping")
|
|
106
|
+
return False
|
|
107
|
+
return answer.strip().lower() in {"y", "yes"}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _atomic_copy(src: Path, dest: Path) -> None:
|
|
111
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
tmp = dest.with_name(dest.name + _TMP_SUFFIX)
|
|
113
|
+
try:
|
|
114
|
+
shutil.copyfile(src, tmp)
|
|
115
|
+
shutil.copymode(src, tmp)
|
|
116
|
+
os.replace(tmp, dest)
|
|
117
|
+
except OSError as exc:
|
|
118
|
+
try:
|
|
119
|
+
tmp.unlink(missing_ok=True)
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
raise SyncIOError(f"failed to copy {src} -> {dest}: {exc}") from exc
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _cleanup_excluded_in_linked(
|
|
126
|
+
worktree_root: Path,
|
|
127
|
+
config: SyncConfig,
|
|
128
|
+
path: str | None,
|
|
129
|
+
*,
|
|
130
|
+
dry_run: bool,
|
|
131
|
+
reporter: Reporter,
|
|
132
|
+
) -> int:
|
|
133
|
+
"""Remove files from the linked worktree that match include AND exclude patterns.
|
|
134
|
+
|
|
135
|
+
Excluded patterns are for committed templates (e.g. .env.example) that git
|
|
136
|
+
checks out into linked worktrees but that sync should never treat as env files.
|
|
137
|
+
"""
|
|
138
|
+
root = worktree_root
|
|
139
|
+
search_root = (root / path) if path else root
|
|
140
|
+
if not search_root.is_dir():
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
count = 0
|
|
144
|
+
for dirpath, dirnames, filenames in os.walk(search_root):
|
|
145
|
+
dir_path = Path(dirpath)
|
|
146
|
+
dirnames[:] = [d for d in dirnames if d != ".git"]
|
|
147
|
+
for filename in filenames:
|
|
148
|
+
if not any(fnmatch.fnmatch(filename, p) for p in config.patterns):
|
|
149
|
+
continue
|
|
150
|
+
if not any(fnmatch.fnmatch(filename, p) for p in config.exclude):
|
|
151
|
+
continue
|
|
152
|
+
abs_path = dir_path / filename
|
|
153
|
+
if abs_path.is_symlink():
|
|
154
|
+
continue
|
|
155
|
+
rel_path = abs_path.relative_to(root)
|
|
156
|
+
if dry_run:
|
|
157
|
+
reporter.detail(f"would remove {rel_path} (excluded template)")
|
|
158
|
+
else:
|
|
159
|
+
abs_path.unlink()
|
|
160
|
+
reporter.detail(f"removed {rel_path} (excluded template)")
|
|
161
|
+
count += 1
|
|
162
|
+
return count
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run_sync(
|
|
166
|
+
repo: Repository,
|
|
167
|
+
config: SyncConfig,
|
|
168
|
+
*,
|
|
169
|
+
dry_run: bool = False,
|
|
170
|
+
force: bool = False,
|
|
171
|
+
path: str | None = None,
|
|
172
|
+
reporter: Reporter,
|
|
173
|
+
) -> SyncResult:
|
|
174
|
+
"""Sync env files from `repo.primary_root` into `repo.worktree_root`."""
|
|
175
|
+
files, warnings = discover_env_files(repo.primary_root, config)
|
|
176
|
+
files = _filter_path(files, path)
|
|
177
|
+
|
|
178
|
+
for warning in warnings:
|
|
179
|
+
reporter.warn(f"{warning.relative_path}: {warning.message}")
|
|
180
|
+
|
|
181
|
+
synced = 0
|
|
182
|
+
skipped_unchanged = 0
|
|
183
|
+
conflicts = 0
|
|
184
|
+
would_change = 0
|
|
185
|
+
|
|
186
|
+
for discovered in files:
|
|
187
|
+
rel = discovered.relative_path
|
|
188
|
+
dest = _resolve_dest(repo.worktree_root, rel)
|
|
189
|
+
|
|
190
|
+
if not dest.exists():
|
|
191
|
+
if dry_run:
|
|
192
|
+
would_change += 1
|
|
193
|
+
reporter.info(f"would sync {rel}")
|
|
194
|
+
continue
|
|
195
|
+
_atomic_copy(discovered.absolute_path, dest)
|
|
196
|
+
synced += 1
|
|
197
|
+
reporter.info(f"synced {rel}")
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if filecmp.cmp(discovered.absolute_path, dest, shallow=False):
|
|
201
|
+
skipped_unchanged += 1
|
|
202
|
+
reporter.detail(f"unchanged {rel}")
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
diff_desc = _diff_summary(discovered.absolute_path, dest)
|
|
206
|
+
should_overwrite = force or config.on_conflict == "overwrite"
|
|
207
|
+
|
|
208
|
+
if not should_overwrite and config.on_conflict == "prompt" and not dry_run:
|
|
209
|
+
should_overwrite = _prompt_overwrite(rel, diff_desc, reporter)
|
|
210
|
+
|
|
211
|
+
if should_overwrite:
|
|
212
|
+
if dry_run:
|
|
213
|
+
would_change += 1
|
|
214
|
+
reporter.info(f"would overwrite {rel} ({diff_desc})")
|
|
215
|
+
continue
|
|
216
|
+
if config.backup:
|
|
217
|
+
_backup(dest)
|
|
218
|
+
_atomic_copy(discovered.absolute_path, dest)
|
|
219
|
+
synced += 1
|
|
220
|
+
reporter.info(f"synced {rel} (overwrote, {diff_desc})")
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
conflicts += 1
|
|
224
|
+
reporter.warn(f"{rel}: {diff_desc}, skipping (use --force to overwrite)")
|
|
225
|
+
|
|
226
|
+
cleaned = _cleanup_excluded_in_linked(
|
|
227
|
+
repo.worktree_root, config, path, dry_run=dry_run, reporter=reporter
|
|
228
|
+
)
|
|
229
|
+
if dry_run:
|
|
230
|
+
would_change += cleaned
|
|
231
|
+
|
|
232
|
+
total_changes = synced + (would_change if dry_run else 0)
|
|
233
|
+
reporter.info(
|
|
234
|
+
f"{total_changes if dry_run else synced} files synced, "
|
|
235
|
+
f"{skipped_unchanged} unchanged, {conflicts} conflicts skipped"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return SyncResult(
|
|
239
|
+
synced=synced,
|
|
240
|
+
skipped_unchanged=skipped_unchanged,
|
|
241
|
+
conflicts=conflicts,
|
|
242
|
+
would_change=would_change,
|
|
243
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: git-env
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sync environment files across linked git worktrees
|
|
5
|
+
Author: Alex Ward
|
|
6
|
+
Author-email: Alex Ward <alxwrd@googlemail.com>
|
|
7
|
+
Requires-Dist: arguably>=1.2.2
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
<div align="center">
|
|
12
|
+
<h1><code>git-env</code></h1>
|
|
13
|
+
<p align="center"><i>
|
|
14
|
+
Environment file syncronisation to your linked git worktrees
|
|
15
|
+
</i></p>
|
|
16
|
+
<img width="256px" src="https://github.com/alxwrd/git-env/raw/main/.github/assets/man-reading-the-mail-768.png">
|
|
17
|
+
<div align="center">
|
|
18
|
+
<a href="https://github.com/alxwrd/git-env/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/alxwrd/git-env/test.yml?branch=main&label=main"></a>
|
|
19
|
+
<a href="https://pypi.python.org/pypi/git-env"><img src="https://img.shields.io/pypi/v/git-env.svg"></a>
|
|
20
|
+
<a href="https://github.com/alxwrd/git-env/blob/main/LICENCE"><img src="https://img.shields.io/pypi/l/git-env.svg?"></a>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
Copies untracked env files to your worktrees.
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
cd ~/repos/myproject # primary worktree, has .env
|
|
31
|
+
git worktree add ~/worktrees/myproject-feature # create a linked worktree
|
|
32
|
+
cd ~/worktrees/myproject-feature
|
|
33
|
+
git env sync # copies .env from the primary
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```plain
|
|
37
|
+
$ git env sync
|
|
38
|
+
|
|
39
|
+
copied .env
|
|
40
|
+
copied .env.local
|
|
41
|
+
|
|
42
|
+
2 files synced.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Run it again later to pick up any changes made in the primary. By default,
|
|
46
|
+
`sync` won't clobber a file that has diverged locally — pass `--force` if
|
|
47
|
+
you want the primary's copy to win.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```shell
|
|
53
|
+
uv tool install git-env
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or, with pipx:
|
|
57
|
+
|
|
58
|
+
```shell
|
|
59
|
+
pipx install git-env
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This installs a `git-env` executable on your `PATH`, which git picks up
|
|
63
|
+
automatically as the `env` subcommand:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
git env --version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## Syncing
|
|
71
|
+
|
|
72
|
+
`git env sync` copies env files from the primary worktree into the linked
|
|
73
|
+
worktree you're standing in. The primary worktree is the original clone — the
|
|
74
|
+
one whose `.git` is a real directory, not a file. `sync` locates it via
|
|
75
|
+
`git rev-parse --git-common-dir`.
|
|
76
|
+
|
|
77
|
+
Files are matched by glob against the entire primary worktree tree.
|
|
78
|
+
`.gitignore` is **not** consulted (env files are normally gitignored, which is
|
|
79
|
+
the point), but a `.envsyncignore` file at the primary root is. A file that
|
|
80
|
+
doesn't exist at the destination is copied. A file that's byte-identical is
|
|
81
|
+
skipped silently. A file that differs is skipped with a warning and a one-line
|
|
82
|
+
diff summary, unless `--force` is given.
|
|
83
|
+
|
|
84
|
+
Writes are atomic: each file is written to `<dest>.envsync.tmp` then renamed
|
|
85
|
+
over the destination. Mode bits (including the executable bit) are preserved;
|
|
86
|
+
mtimes are not, so a synced file's timestamp tells you when it was synced.
|
|
87
|
+
|
|
88
|
+
`sync` must run from a **linked** worktree — it refuses to run from the
|
|
89
|
+
primary itself, and refuses on bare repositories.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
git env sync [--dry-run] [--force] [--verbose | --quiet]
|
|
93
|
+
[--pattern <glob>]... [--path <subdir>]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Flag | Short | Meaning |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `--dry-run` | `-n` | Print what would happen; change nothing. |
|
|
99
|
+
| `--force` | `-f` | Overwrite local files that differ from the primary, backing up the previous content first. |
|
|
100
|
+
| `--verbose` | `-v` | Print every file considered, including skips. |
|
|
101
|
+
| `--quiet` | `-q` | Suppress non-error output. |
|
|
102
|
+
| `--pattern <glob>` | | Override configured patterns for this run. Repeatable. |
|
|
103
|
+
| `--path <subdir>` | | Restrict the sync to a subdirectory of the worktree. |
|
|
104
|
+
|
|
105
|
+
`--verbose` and `--quiet` are mutually exclusive.
|
|
106
|
+
|
|
107
|
+
### Exit codes
|
|
108
|
+
|
|
109
|
+
| Code | Meaning |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `0` | Success, or dry-run with nothing to change. |
|
|
112
|
+
| `1` | Sync completed but one or more files were skipped due to conflicts. |
|
|
113
|
+
| `2` | Refused to run (not a git worktree, bare repo, invoked from the primary worktree, or the primary has uncommitted changes to tracked env files). |
|
|
114
|
+
| `3` | Usage error (bad flag, unknown or reserved subcommand). |
|
|
115
|
+
| `4` | I/O error while copying. |
|
|
116
|
+
|
|
117
|
+
These codes are part of the contract — scripts can rely on them.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
All keys live under `env.sync.*` and are read with standard git config
|
|
123
|
+
precedence (system → global → local → worktree):
|
|
124
|
+
|
|
125
|
+
| Key | Type | Default | Meaning |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| `env.sync.patterns` | multi-value | `.env`, `.env.*` | Globs to sync. Multi-value, so additional `git config --add` calls append rather than replace. |
|
|
128
|
+
| `env.sync.exclude` | multi-value | `.env.example`, `.env.sample`, `.env.template` | Patterns never synced, even if they match `patterns` (typically committed templates). |
|
|
129
|
+
| `env.sync.followSymlinks` | bool | `false` | Follow symlinked env files instead of skipping them. |
|
|
130
|
+
| `env.sync.maxFileSize` | int (bytes) | `1048576` | Files larger than this are skipped with a warning. |
|
|
131
|
+
| `env.sync.onConflict` | enum | `skip` | `skip`, `overwrite`, or `prompt`. |
|
|
132
|
+
| `env.sync.backup` | bool | `true` | Whether `--force` writes a `<dest>.envsync.bak` backup before overwriting. |
|
|
133
|
+
|
|
134
|
+
Set per-repo in the primary worktree's `.git/config`, or per-worktree in
|
|
135
|
+
that worktree's own config:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
git config env.sync.onConflict overwrite
|
|
139
|
+
git config --add env.sync.patterns ".env.local"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `.envsync`
|
|
143
|
+
|
|
144
|
+
An optional `key=value` file at the primary worktree root, for pinning
|
|
145
|
+
patterns into version control so the whole team gets the same defaults
|
|
146
|
+
without everyone running `git config`. Same keys as above, minus the
|
|
147
|
+
`env.sync.` prefix:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
patterns=.env
|
|
151
|
+
patterns=.env.*
|
|
152
|
+
exclude=.env.example
|
|
153
|
+
followSymlinks=false
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Repeated keys accumulate (for multi-value settings). `git config` values,
|
|
157
|
+
if set, always take precedence over `.envsync`.
|
|
158
|
+
|
|
159
|
+
### `.envsyncignore`
|
|
160
|
+
|
|
161
|
+
A `gitignore`-syntax file at the primary worktree root. Paths it matches
|
|
162
|
+
are excluded from sync regardless of `env.sync.patterns` — use it to opt a
|
|
163
|
+
specific file or directory out without changing the glob patterns themselves.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Shell completion
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
git env --install-completions bash # print a snippet for your rc file
|
|
170
|
+
git env --install-completions zsh --write # install the completion file directly
|
|
171
|
+
git env --install-completions fish
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Supported shells: `bash`, `zsh`, `fish`. Without `--write`, the command
|
|
175
|
+
prints what to add to your shell config; with `--write`, it installs the
|
|
176
|
+
completion file to a standard location for that shell.
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
## FAQ
|
|
180
|
+
|
|
181
|
+
**Why not just symlink the env files instead?**
|
|
182
|
+
A symlink means there's only ever one copy, so editing the file in a linked
|
|
183
|
+
worktree edits the primary too — that defeats the purpose of having isolated
|
|
184
|
+
worktrees in the first place (e.g. running two branches with different API
|
|
185
|
+
keys or feature flags side by side). `git env sync` gives each worktree its
|
|
186
|
+
own independent copy, seeded from the primary, that you can then diverge from
|
|
187
|
+
intentionally.
|
|
188
|
+
|
|
189
|
+
**Does this work with bare repositories?**
|
|
190
|
+
No. `git env sync` requires a primary worktree with a real working tree to
|
|
191
|
+
copy *from*. Bare repos are detected and rejected with exit code `2`.
|
|
192
|
+
|
|
193
|
+
**What about secrets in env files?**
|
|
194
|
+
`git env sync` only ever copies bytes between worktrees on your local
|
|
195
|
+
filesystem — it doesn't transmit, log, or store file contents anywhere else,
|
|
196
|
+
and it never touches git history (env files are normally gitignored and stay
|
|
197
|
+
that way). The usual rules still apply: don't commit secrets, and be mindful
|
|
198
|
+
that `--force` backups (`<dest>.envsync.bak`) leave a second copy of the
|
|
199
|
+
previous content on disk.
|
|
200
|
+
|
|
201
|
+
**Can I sync changes back from a worktree to the primary?**
|
|
202
|
+
Not yet. `sync` is one-way (primary → linked). A `git env push` for the
|
|
203
|
+
reverse direction is planned but not implemented in v1 — see Limitations.
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
## Limitations
|
|
207
|
+
|
|
208
|
+
- **One-way sync only.** `git env sync` copies primary → linked. There is no
|
|
209
|
+
bidirectional sync in v1; pushing changes from a linked worktree back to the
|
|
210
|
+
primary isn't supported yet.
|
|
211
|
+
- **Submodules aren't traversed.** Env files inside submodules are not
|
|
212
|
+
discovered or synced.
|
|
213
|
+
- **Single primary per invocation.** The tool assumes one primary worktree and
|
|
214
|
+
doesn't support syncing between two linked worktrees directly.
|
|
215
|
+
- **No format parsing.** Env files are treated as opaque bytes; `git env`
|
|
216
|
+
doesn't understand `KEY=VALUE` syntax, so it can't merge or diff values
|
|
217
|
+
semantically — only whole-file conflict detection.
|
|
218
|
+
- **`--porcelain` is reserved but not implemented.** Passing it is a usage
|
|
219
|
+
error in v1, by design, so scripts don't silently depend on output that may
|
|
220
|
+
change later.
|
|
221
|
+
|
|
222
|
+
`push`, `diff`, `status`, `list`, `edit`, and `check` are reserved for
|
|
223
|
+
future official subcommands and will error if invoked, so a third-party
|
|
224
|
+
`git-env-<name>` script doesn't collide with them later.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
git_env/__init__.py,sha256=tk97bBRWI7MgDIPVeB7TEsKQEX9g1g4xhGyR51qimRw,303
|
|
2
|
+
git_env/cli.py,sha256=uZxcICdbmwLwlAfkqWzwlFx0zusZyKxYHCp4ttLIf-g,6107
|
|
3
|
+
git_env/completions/_git-env,sha256=rKY9vn5BzLKDf4FltOEOmU3AmfWv0daFDEiNJ-PDeyk,1482
|
|
4
|
+
git_env/completions/git-env.bash,sha256=VMDt8C2o3roJ9c6wcjn1MExNwAFC3hiBOmZe_-7FICY,1758
|
|
5
|
+
git_env/completions/git-env.fish,sha256=RQJ0a10TpH_iI5-pN_Hphg_tM77ZXEvjJm8ZEwHmNKk,1691
|
|
6
|
+
git_env/config.py,sha256=6J9LKCCIb9sd4DWoEprK5McCkPi0jCogHUB4v6nQONI,6226
|
|
7
|
+
git_env/discovery.py,sha256=QPuPOs4MhL3v_atuAXi9oRBDmUxHMqsx5vKgHQsudTU,6631
|
|
8
|
+
git_env/output.py,sha256=Du4j64mVcWsURYDG_JRvq9HmZcz5TG4EH0iFuY40FW8,1137
|
|
9
|
+
git_env/repo.py,sha256=o8B4HypFlytwHiqWQqgrzf608fcAsapAQpzk0peL6Dw,5210
|
|
10
|
+
git_env/shell_completions.py,sha256=Htbb5J_jVhTcKfz1rbYyw4xDjHQ_vsDr2qSohH3ZF7U,3049
|
|
11
|
+
git_env/sync.py,sha256=m9moOKaUPfK6NmCpu9Kz-_wlQz4qjXWkq8Lej1ACqo8,7897
|
|
12
|
+
git_env-0.1.0.dist-info/WHEEL,sha256=jROcLULcdzropX2J55opKw4UHhPFREZax2XzS-Mvpxs,80
|
|
13
|
+
git_env-0.1.0.dist-info/entry_points.txt,sha256=2HJnPa1q9g0KpBhhZOlq-wI8_vozgLnrm5aLLKJPGM4,42
|
|
14
|
+
git_env-0.1.0.dist-info/METADATA,sha256=vz_a2r2YcbSdu6q1PezZWyrzHud-TZXjpJJgvAR2SrY,8677
|
|
15
|
+
git_env-0.1.0.dist-info/RECORD,,
|