git-env 0.1.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_env-0.1.0/PKG-INFO ADDED
@@ -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,214 @@
1
+ <div align="center">
2
+ <h1><code>git-env</code></h1>
3
+ <p align="center"><i>
4
+ Environment file syncronisation to your linked git worktrees
5
+ </i></p>
6
+ <img width="256px" src="https://github.com/alxwrd/git-env/raw/main/.github/assets/man-reading-the-mail-768.png">
7
+ <div align="center">
8
+ <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>
9
+ <a href="https://pypi.python.org/pypi/git-env"><img src="https://img.shields.io/pypi/v/git-env.svg"></a>
10
+ <a href="https://github.com/alxwrd/git-env/blob/main/LICENCE"><img src="https://img.shields.io/pypi/l/git-env.svg?"></a>
11
+ </div>
12
+
13
+ Copies untracked env files to your worktrees.
14
+ </div>
15
+
16
+
17
+ ## Example
18
+
19
+ ```sh
20
+ cd ~/repos/myproject # primary worktree, has .env
21
+ git worktree add ~/worktrees/myproject-feature # create a linked worktree
22
+ cd ~/worktrees/myproject-feature
23
+ git env sync # copies .env from the primary
24
+ ```
25
+
26
+ ```plain
27
+ $ git env sync
28
+
29
+ copied .env
30
+ copied .env.local
31
+
32
+ 2 files synced.
33
+ ```
34
+
35
+ Run it again later to pick up any changes made in the primary. By default,
36
+ `sync` won't clobber a file that has diverged locally — pass `--force` if
37
+ you want the primary's copy to win.
38
+
39
+
40
+ ## Installation
41
+
42
+ ```shell
43
+ uv tool install git-env
44
+ ```
45
+
46
+ Or, with pipx:
47
+
48
+ ```shell
49
+ pipx install git-env
50
+ ```
51
+
52
+ This installs a `git-env` executable on your `PATH`, which git picks up
53
+ automatically as the `env` subcommand:
54
+
55
+ ```sh
56
+ git env --version
57
+ ```
58
+
59
+
60
+ ## Syncing
61
+
62
+ `git env sync` copies env files from the primary worktree into the linked
63
+ worktree you're standing in. The primary worktree is the original clone — the
64
+ one whose `.git` is a real directory, not a file. `sync` locates it via
65
+ `git rev-parse --git-common-dir`.
66
+
67
+ Files are matched by glob against the entire primary worktree tree.
68
+ `.gitignore` is **not** consulted (env files are normally gitignored, which is
69
+ the point), but a `.envsyncignore` file at the primary root is. A file that
70
+ doesn't exist at the destination is copied. A file that's byte-identical is
71
+ skipped silently. A file that differs is skipped with a warning and a one-line
72
+ diff summary, unless `--force` is given.
73
+
74
+ Writes are atomic: each file is written to `<dest>.envsync.tmp` then renamed
75
+ over the destination. Mode bits (including the executable bit) are preserved;
76
+ mtimes are not, so a synced file's timestamp tells you when it was synced.
77
+
78
+ `sync` must run from a **linked** worktree — it refuses to run from the
79
+ primary itself, and refuses on bare repositories.
80
+
81
+ ```
82
+ git env sync [--dry-run] [--force] [--verbose | --quiet]
83
+ [--pattern <glob>]... [--path <subdir>]
84
+ ```
85
+
86
+ | Flag | Short | Meaning |
87
+ |---|---|---|
88
+ | `--dry-run` | `-n` | Print what would happen; change nothing. |
89
+ | `--force` | `-f` | Overwrite local files that differ from the primary, backing up the previous content first. |
90
+ | `--verbose` | `-v` | Print every file considered, including skips. |
91
+ | `--quiet` | `-q` | Suppress non-error output. |
92
+ | `--pattern <glob>` | | Override configured patterns for this run. Repeatable. |
93
+ | `--path <subdir>` | | Restrict the sync to a subdirectory of the worktree. |
94
+
95
+ `--verbose` and `--quiet` are mutually exclusive.
96
+
97
+ ### Exit codes
98
+
99
+ | Code | Meaning |
100
+ |---|---|
101
+ | `0` | Success, or dry-run with nothing to change. |
102
+ | `1` | Sync completed but one or more files were skipped due to conflicts. |
103
+ | `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). |
104
+ | `3` | Usage error (bad flag, unknown or reserved subcommand). |
105
+ | `4` | I/O error while copying. |
106
+
107
+ These codes are part of the contract — scripts can rely on them.
108
+
109
+
110
+ ## Configuration
111
+
112
+ All keys live under `env.sync.*` and are read with standard git config
113
+ precedence (system → global → local → worktree):
114
+
115
+ | Key | Type | Default | Meaning |
116
+ |---|---|---|---|
117
+ | `env.sync.patterns` | multi-value | `.env`, `.env.*` | Globs to sync. Multi-value, so additional `git config --add` calls append rather than replace. |
118
+ | `env.sync.exclude` | multi-value | `.env.example`, `.env.sample`, `.env.template` | Patterns never synced, even if they match `patterns` (typically committed templates). |
119
+ | `env.sync.followSymlinks` | bool | `false` | Follow symlinked env files instead of skipping them. |
120
+ | `env.sync.maxFileSize` | int (bytes) | `1048576` | Files larger than this are skipped with a warning. |
121
+ | `env.sync.onConflict` | enum | `skip` | `skip`, `overwrite`, or `prompt`. |
122
+ | `env.sync.backup` | bool | `true` | Whether `--force` writes a `<dest>.envsync.bak` backup before overwriting. |
123
+
124
+ Set per-repo in the primary worktree's `.git/config`, or per-worktree in
125
+ that worktree's own config:
126
+
127
+ ```sh
128
+ git config env.sync.onConflict overwrite
129
+ git config --add env.sync.patterns ".env.local"
130
+ ```
131
+
132
+ ### `.envsync`
133
+
134
+ An optional `key=value` file at the primary worktree root, for pinning
135
+ patterns into version control so the whole team gets the same defaults
136
+ without everyone running `git config`. Same keys as above, minus the
137
+ `env.sync.` prefix:
138
+
139
+ ```
140
+ patterns=.env
141
+ patterns=.env.*
142
+ exclude=.env.example
143
+ followSymlinks=false
144
+ ```
145
+
146
+ Repeated keys accumulate (for multi-value settings). `git config` values,
147
+ if set, always take precedence over `.envsync`.
148
+
149
+ ### `.envsyncignore`
150
+
151
+ A `gitignore`-syntax file at the primary worktree root. Paths it matches
152
+ are excluded from sync regardless of `env.sync.patterns` — use it to opt a
153
+ specific file or directory out without changing the glob patterns themselves.
154
+
155
+
156
+ ## Shell completion
157
+
158
+ ```sh
159
+ git env --install-completions bash # print a snippet for your rc file
160
+ git env --install-completions zsh --write # install the completion file directly
161
+ git env --install-completions fish
162
+ ```
163
+
164
+ Supported shells: `bash`, `zsh`, `fish`. Without `--write`, the command
165
+ prints what to add to your shell config; with `--write`, it installs the
166
+ completion file to a standard location for that shell.
167
+
168
+
169
+ ## FAQ
170
+
171
+ **Why not just symlink the env files instead?**
172
+ A symlink means there's only ever one copy, so editing the file in a linked
173
+ worktree edits the primary too — that defeats the purpose of having isolated
174
+ worktrees in the first place (e.g. running two branches with different API
175
+ keys or feature flags side by side). `git env sync` gives each worktree its
176
+ own independent copy, seeded from the primary, that you can then diverge from
177
+ intentionally.
178
+
179
+ **Does this work with bare repositories?**
180
+ No. `git env sync` requires a primary worktree with a real working tree to
181
+ copy *from*. Bare repos are detected and rejected with exit code `2`.
182
+
183
+ **What about secrets in env files?**
184
+ `git env sync` only ever copies bytes between worktrees on your local
185
+ filesystem — it doesn't transmit, log, or store file contents anywhere else,
186
+ and it never touches git history (env files are normally gitignored and stay
187
+ that way). The usual rules still apply: don't commit secrets, and be mindful
188
+ that `--force` backups (`<dest>.envsync.bak`) leave a second copy of the
189
+ previous content on disk.
190
+
191
+ **Can I sync changes back from a worktree to the primary?**
192
+ Not yet. `sync` is one-way (primary → linked). A `git env push` for the
193
+ reverse direction is planned but not implemented in v1 — see Limitations.
194
+
195
+
196
+ ## Limitations
197
+
198
+ - **One-way sync only.** `git env sync` copies primary → linked. There is no
199
+ bidirectional sync in v1; pushing changes from a linked worktree back to the
200
+ primary isn't supported yet.
201
+ - **Submodules aren't traversed.** Env files inside submodules are not
202
+ discovered or synced.
203
+ - **Single primary per invocation.** The tool assumes one primary worktree and
204
+ doesn't support syncing between two linked worktrees directly.
205
+ - **No format parsing.** Env files are treated as opaque bytes; `git env`
206
+ doesn't understand `KEY=VALUE` syntax, so it can't merge or diff values
207
+ semantically — only whole-file conflict detection.
208
+ - **`--porcelain` is reserved but not implemented.** Passing it is a usage
209
+ error in v1, by design, so scripts don't silently depend on output that may
210
+ change later.
211
+
212
+ `push`, `diff`, `status`, `list`, `edit`, and `check` are reserved for
213
+ future official subcommands and will error if invoked, so a third-party
214
+ `git-env-<name>` script doesn't collide with them later.
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "git-env"
3
+ version = "0.1.0"
4
+ description = "Sync environment files across linked git worktrees"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Alex Ward", email = "alxwrd@googlemail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "arguably>=1.2.2",
12
+ ]
13
+
14
+ [project.scripts]
15
+ git-env = "git_env:main"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.10.0,<0.11.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=9.1.1",
24
+ ]
@@ -0,0 +1,16 @@
1
+ import importlib.metadata
2
+
3
+ try:
4
+ __version__ = importlib.metadata.version("git-env")
5
+ except importlib.metadata.PackageNotFoundError:
6
+ __version__ = "0.0.0+dev"
7
+
8
+
9
+ def main() -> None:
10
+ import __main__
11
+
12
+ __main__.__version__ = __version__
13
+
14
+ from .cli import main as cli_main
15
+
16
+ cli_main()
@@ -0,0 +1,183 @@
1
+ """Top-level CLI: dispatches `git env <subcommand>` via arguably.
2
+
3
+ Exit codes are part of the spec's contract (see spec.md):
4
+ 0 success, 1 sync conflicts skipped, 2 refused to run, 3 usage error, 4 I/O error.
5
+ argparse itself always raises `SystemExit(2)` for usage errors (bad flag, unknown
6
+ subcommand), which collides with our "refused to run" code. `GitEnvExit` lets
7
+ command bodies signal an intentional exit code; any other `SystemExit(2)` reaching
8
+ `main()` therefore came from argparse and is remapped to 3.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+
15
+ import arguably
16
+
17
+ from .config import ConfigError, load_config
18
+ from .output import Reporter
19
+ from .repo import RepoError, check_primary_clean, detect_repository
20
+ from .shell_completions import SUPPORTED_SHELLS
21
+ from .shell_completions import install_completions as render_completions_install
22
+ from .sync import SyncIOError, run_sync
23
+
24
+ #: Names set aside for future official subcommands (see spec.md Extensibility).
25
+ RESERVED_SUBCOMMANDS = frozenset({"push", "diff", "status", "list", "edit", "check"})
26
+
27
+
28
+ class GitEnvExit(Exception):
29
+ """Raised by command bodies to request a specific process exit code."""
30
+
31
+ def __init__(self, code: int) -> None:
32
+ super().__init__(code)
33
+ self.code = code
34
+
35
+
36
+ @arguably.command
37
+ def __root__(
38
+ *, porcelain: bool = False, install_completions: str | None = None, write: bool = False
39
+ ) -> None:
40
+ """
41
+ git env: sync environment files across linked git worktrees.
42
+
43
+ Args:
44
+ porcelain: reserved for a future machine-readable output mode (not yet supported)
45
+ install_completions: print a completion snippet for [bash|zsh|fish]; combine with
46
+ --write to install the completion file instead of printing it
47
+ write: used with --install-completions, write the completion file to its standard
48
+ location instead of printing a snippet
49
+ """
50
+ if porcelain:
51
+ print(
52
+ "git env: --porcelain is reserved for a future version and is not yet"
53
+ " supported",
54
+ file=sys.stderr,
55
+ )
56
+ raise GitEnvExit(3)
57
+ if install_completions is not None:
58
+ if install_completions not in SUPPORTED_SHELLS:
59
+ print(
60
+ "git env: --install-completions expects one of "
61
+ f"{', '.join(SUPPORTED_SHELLS)}, got {install_completions!r}",
62
+ file=sys.stderr,
63
+ )
64
+ raise GitEnvExit(3)
65
+ print(render_completions_install(install_completions, write=write))
66
+ raise GitEnvExit(0)
67
+ if write:
68
+ print("git env: --write requires --install-completions", file=sys.stderr)
69
+ raise GitEnvExit(3)
70
+ if arguably.is_target():
71
+ arguably.error("a subcommand is required, try 'git env --help'")
72
+
73
+
74
+ @arguably.command
75
+ def sync(
76
+ *,
77
+ dry_run: bool = False,
78
+ force: bool = False,
79
+ verbose: bool = False,
80
+ quiet: bool = False,
81
+ pattern: list[str] | None = None,
82
+ path: str | None = None,
83
+ ) -> None:
84
+ """
85
+ Copy env files from the primary worktree into the current linked worktree.
86
+
87
+ Args:
88
+ dry_run: [-n] print actions, change nothing
89
+ force: [-f] overwrite local files even when they differ from the primary
90
+ verbose: [-v] print every file considered, including skips
91
+ quiet: [-q] suppress non-error output
92
+ pattern: glob to sync, repeatable; overrides configured patterns for this run
93
+ path: restrict to a subdirectory of the worktree
94
+ """
95
+ if verbose and quiet:
96
+ print("git env sync: --verbose and --quiet are mutually exclusive", file=sys.stderr)
97
+ raise GitEnvExit(3)
98
+
99
+ reporter = Reporter(verbose=verbose, quiet=quiet)
100
+
101
+ try:
102
+ repo = detect_repository()
103
+ except RepoError as exc:
104
+ reporter.error(str(exc))
105
+ raise GitEnvExit(2) from None
106
+
107
+ try:
108
+ config = load_config(repo.primary_root)
109
+ except ConfigError as exc:
110
+ reporter.error(str(exc))
111
+ raise GitEnvExit(3) from None
112
+
113
+ if pattern:
114
+ config = type(config)(**{**config.__dict__, "patterns": tuple(pattern)})
115
+
116
+ if not force:
117
+ dirty = check_primary_clean(repo.primary_root, config.patterns, config.exclude)
118
+ if dirty:
119
+ reporter.error(
120
+ "primary worktree has uncommitted changes to tracked env files: "
121
+ f"{', '.join(dirty)} (use --force to override)"
122
+ )
123
+ raise GitEnvExit(2)
124
+
125
+ try:
126
+ result = run_sync(
127
+ repo,
128
+ config,
129
+ dry_run=dry_run,
130
+ force=force,
131
+ path=path,
132
+ reporter=reporter,
133
+ )
134
+ except SyncIOError as exc:
135
+ reporter.error(str(exc))
136
+ raise GitEnvExit(4) from None
137
+
138
+ if dry_run:
139
+ raise GitEnvExit(1 if result.would_change or result.conflicts else 0)
140
+ raise GitEnvExit(result.exit_code)
141
+
142
+
143
+ def _rewrite_help_subcommand(argv: list[str]) -> list[str]:
144
+ """Translate `git env help [subcommand]` into `git env [subcommand] --help`."""
145
+ if argv and argv[0] == "help":
146
+ rest = argv[1:]
147
+ return [*rest, "--help"]
148
+ return argv
149
+
150
+
151
+ def _reject_reserved_subcommand(argv: list[str]) -> None:
152
+ """Give a clearer error than argparse's "invalid choice" for reserved names."""
153
+ for token in argv:
154
+ if token == "--":
155
+ return
156
+ if token.startswith("-"):
157
+ continue
158
+ if token in RESERVED_SUBCOMMANDS:
159
+ print(
160
+ f"git env: '{token}' is reserved for future use and is not yet"
161
+ " implemented",
162
+ file=sys.stderr,
163
+ )
164
+ raise GitEnvExit(3)
165
+ return
166
+
167
+
168
+ def main() -> None:
169
+ argv = _rewrite_help_subcommand(sys.argv[1:])
170
+ sys.argv = [sys.argv[0], *argv]
171
+ try:
172
+ _reject_reserved_subcommand(argv)
173
+ arguably.run(
174
+ name="git env",
175
+ version_flag=True,
176
+ )
177
+ except GitEnvExit as exc:
178
+ sys.exit(exc.code)
179
+ except SystemExit as exc:
180
+ code = exc.code if isinstance(exc.code, int) else 1
181
+ if code == 2:
182
+ sys.exit(3)
183
+ raise