git-env 0.3.0__tar.gz → 0.4.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.3.0 → git_env-0.4.0}/PKG-INFO +7 -12
- {git_env-0.3.0 → git_env-0.4.0}/README.md +6 -10
- {git_env-0.3.0 → git_env-0.4.0}/pyproject.toml +2 -4
- git_env-0.4.0/src/git_env/_argparser.py +84 -0
- git_env-0.4.0/src/git_env/cli.py +120 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/discovery.py +2 -6
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/sync.py +3 -7
- git_env-0.3.0/src/git_env/cli.py +0 -183
- {git_env-0.3.0 → git_env-0.4.0}/LICENCE +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/__init__.py +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/completions/_git-env +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/completions/git-env.bash +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/completions/git-env.fish +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/config.py +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/output.py +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/repo.py +0 -0
- {git_env-0.3.0 → git_env-0.4.0}/src/git_env/shell_completions.py +0 -0
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-env
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Sync environment files across linked git worktrees
|
|
5
5
|
Author: Alex Ward
|
|
6
6
|
Author-email: Alex Ward <alxwrd@googlemail.com>
|
|
7
7
|
License-Expression: MIT
|
|
8
8
|
License-File: LICENCE
|
|
9
|
-
Requires-Dist: arguably>=1.2.2
|
|
10
9
|
Requires-Python: >=3.10
|
|
11
10
|
Description-Content-Type: text/markdown
|
|
12
11
|
|
|
13
12
|
<div align="center">
|
|
14
13
|
<h1><code>git-env</code></h1>
|
|
15
14
|
<p align="center"><i>
|
|
16
|
-
|
|
15
|
+
Copies untracked env files to your worktrees
|
|
17
16
|
</i></p>
|
|
18
17
|
<img width="256px" src="https://github.com/alxwrd/git-env/raw/main/.github/assets/man-reading-the-mail-768.png">
|
|
19
18
|
<div align="center">
|
|
20
|
-
<a href="https://github.com/alxwrd/git-env/actions/workflows/
|
|
21
|
-
<a href="https://pypi.python.org/pypi/git-env"><img src="https://img.shields.io/pypi/v/git-env
|
|
22
|
-
<a href="https://github.com/alxwrd/git-env/blob/main/LICENCE"><img src="https://img.shields.io/pypi/l/git-env
|
|
19
|
+
<a href="https://github.com/alxwrd/git-env/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/alxwrd/git-env/ci.yml?branch=main&label=main"></a>
|
|
20
|
+
<a href="https://pypi.python.org/pypi/git-env"><img src="https://img.shields.io/pypi/v/git-env"></a>
|
|
21
|
+
<a href="https://github.com/alxwrd/git-env/blob/main/LICENCE"><img src="https://img.shields.io/pypi/l/git-env"></a>
|
|
23
22
|
</div>
|
|
24
|
-
|
|
25
|
-
Copies untracked env files to your worktrees.
|
|
26
23
|
</div>
|
|
27
24
|
|
|
28
25
|
|
|
@@ -37,10 +34,8 @@ git env sync # copies .env from the primary
|
|
|
37
34
|
|
|
38
35
|
```plain
|
|
39
36
|
$ git env sync
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
1 file synced.
|
|
37
|
+
synced .env
|
|
38
|
+
2 files synced, 0 unchanged, 0 conflicts skipped
|
|
44
39
|
```
|
|
45
40
|
|
|
46
41
|
Run it again later to pick up any changes made in the primary. By default,
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1><code>git-env</code></h1>
|
|
3
3
|
<p align="center"><i>
|
|
4
|
-
|
|
4
|
+
Copies untracked env files to your worktrees
|
|
5
5
|
</i></p>
|
|
6
6
|
<img width="256px" src="https://github.com/alxwrd/git-env/raw/main/.github/assets/man-reading-the-mail-768.png">
|
|
7
7
|
<div align="center">
|
|
8
|
-
<a href="https://github.com/alxwrd/git-env/actions/workflows/
|
|
9
|
-
<a href="https://pypi.python.org/pypi/git-env"><img src="https://img.shields.io/pypi/v/git-env
|
|
10
|
-
<a href="https://github.com/alxwrd/git-env/blob/main/LICENCE"><img src="https://img.shields.io/pypi/l/git-env
|
|
8
|
+
<a href="https://github.com/alxwrd/git-env/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/alxwrd/git-env/ci.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"></a>
|
|
10
|
+
<a href="https://github.com/alxwrd/git-env/blob/main/LICENCE"><img src="https://img.shields.io/pypi/l/git-env"></a>
|
|
11
11
|
</div>
|
|
12
|
-
|
|
13
|
-
Copies untracked env files to your worktrees.
|
|
14
12
|
</div>
|
|
15
13
|
|
|
16
14
|
|
|
@@ -25,10 +23,8 @@ git env sync # copies .env from the primary
|
|
|
25
23
|
|
|
26
24
|
```plain
|
|
27
25
|
$ git env sync
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
1 file synced.
|
|
26
|
+
synced .env
|
|
27
|
+
2 files synced, 0 unchanged, 0 conflicts skipped
|
|
32
28
|
```
|
|
33
29
|
|
|
34
30
|
Run it again later to pick up any changes made in the primary. By default,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "git-env"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Sync environment files across linked git worktrees"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -9,9 +9,7 @@ authors = [
|
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENCE"]
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
|
-
dependencies = [
|
|
13
|
-
"arguably>=1.2.2",
|
|
14
|
-
]
|
|
12
|
+
dependencies = []
|
|
15
13
|
|
|
16
14
|
[project.scripts]
|
|
17
15
|
git-env = "git_env:main"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
from .shell_completions import SUPPORTED_SHELLS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitEnvExit(Exception):
|
|
10
|
+
"""Raised by command bodies to request a specific process exit code."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, code: int) -> None:
|
|
13
|
+
super().__init__(code)
|
|
14
|
+
self.code = code
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def rewrite_help_subcommand(argv: list[str]) -> list[str]:
|
|
18
|
+
"""Translate `git env help [subcommand]` into `git env [subcommand] --help`."""
|
|
19
|
+
if argv and argv[0] == "help":
|
|
20
|
+
return [*argv[1:], "--help"]
|
|
21
|
+
return argv
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
prog="git env",
|
|
27
|
+
description="sync environment files across linked git worktrees",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--version", action="version", version=f"%(prog)s {version('git-env')}"
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--porcelain",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="reserved for a future machine-readable output mode (not yet supported)",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--install-completions",
|
|
40
|
+
choices=SUPPORTED_SHELLS,
|
|
41
|
+
metavar="{" + "|".join(SUPPORTED_SHELLS) + "}",
|
|
42
|
+
help="print a completion snippet for the given shell; combine with --write to install",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--write",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="used with --install-completions: write the file instead of printing",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-v",
|
|
51
|
+
"--verbose",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="print every file considered, including skips",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-q", "--quiet", action="store_true", help="suppress non-error output"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
sub = parser.add_subparsers(dest="command")
|
|
60
|
+
|
|
61
|
+
p_sync = sub.add_parser(
|
|
62
|
+
"sync",
|
|
63
|
+
help="copy env files from primary worktree into current linked worktree",
|
|
64
|
+
)
|
|
65
|
+
p_sync.add_argument(
|
|
66
|
+
"-n", "--dry-run", action="store_true", help="print actions, change nothing"
|
|
67
|
+
)
|
|
68
|
+
p_sync.add_argument(
|
|
69
|
+
"-f",
|
|
70
|
+
"--force",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="overwrite local files even when they differ from primary",
|
|
73
|
+
)
|
|
74
|
+
p_sync.add_argument(
|
|
75
|
+
"--pattern",
|
|
76
|
+
action="append",
|
|
77
|
+
metavar="GLOB",
|
|
78
|
+
help="glob to sync, repeatable; overrides configured patterns",
|
|
79
|
+
)
|
|
80
|
+
p_sync.add_argument(
|
|
81
|
+
"--path", metavar="PATH", help="restrict to a subdirectory of the worktree"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return parser
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Top-level CLI: dispatches `git env <subcommand>` via argparse.
|
|
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
|
+
from ._argparser import GitEnvExit, build_parser, rewrite_help_subcommand
|
|
16
|
+
from .config import ConfigError, load_config
|
|
17
|
+
from .output import Reporter
|
|
18
|
+
from .repo import RepoError, check_primary_clean, detect_repository
|
|
19
|
+
from .shell_completions import install_completions as render_completions_install
|
|
20
|
+
from .sync import SyncIOError, run_sync
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sync(
|
|
24
|
+
*,
|
|
25
|
+
pattern: list[str] | None = None,
|
|
26
|
+
path: str | None = None,
|
|
27
|
+
dry_run: bool = False,
|
|
28
|
+
force: bool = False,
|
|
29
|
+
reporter: Reporter,
|
|
30
|
+
) -> None:
|
|
31
|
+
try:
|
|
32
|
+
repo = detect_repository()
|
|
33
|
+
except RepoError as exc:
|
|
34
|
+
reporter.error(str(exc))
|
|
35
|
+
raise GitEnvExit(2) from None
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
config = load_config(repo.primary_root)
|
|
39
|
+
except ConfigError as exc:
|
|
40
|
+
reporter.error(str(exc))
|
|
41
|
+
raise GitEnvExit(3) from None
|
|
42
|
+
|
|
43
|
+
if pattern:
|
|
44
|
+
config = type(config)(**{**config.__dict__, "patterns": tuple(pattern)})
|
|
45
|
+
|
|
46
|
+
if not force:
|
|
47
|
+
dirty = check_primary_clean(repo.primary_root, config.patterns, config.exclude)
|
|
48
|
+
if dirty:
|
|
49
|
+
reporter.error(
|
|
50
|
+
"primary worktree has uncommitted changes to tracked env files: "
|
|
51
|
+
f"{', '.join(dirty)} (use --force to override)"
|
|
52
|
+
)
|
|
53
|
+
raise GitEnvExit(2)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
result = run_sync(
|
|
57
|
+
repo,
|
|
58
|
+
config,
|
|
59
|
+
dry_run=dry_run,
|
|
60
|
+
force=force,
|
|
61
|
+
path=path,
|
|
62
|
+
reporter=reporter,
|
|
63
|
+
)
|
|
64
|
+
except SyncIOError as exc:
|
|
65
|
+
reporter.error(str(exc))
|
|
66
|
+
raise GitEnvExit(4) from None
|
|
67
|
+
|
|
68
|
+
if dry_run:
|
|
69
|
+
raise GitEnvExit(1 if result.would_change or result.conflicts else 0)
|
|
70
|
+
raise GitEnvExit(result.exit_code)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main() -> None:
|
|
74
|
+
argv = rewrite_help_subcommand(sys.argv[1:])
|
|
75
|
+
try:
|
|
76
|
+
parser = build_parser()
|
|
77
|
+
args = parser.parse_args(argv)
|
|
78
|
+
|
|
79
|
+
if args.porcelain:
|
|
80
|
+
print(
|
|
81
|
+
"git env: --porcelain is reserved for a future version and is not yet"
|
|
82
|
+
" supported",
|
|
83
|
+
file=sys.stderr,
|
|
84
|
+
)
|
|
85
|
+
raise GitEnvExit(3)
|
|
86
|
+
if args.install_completions is not None:
|
|
87
|
+
print(
|
|
88
|
+
render_completions_install(args.install_completions, write=args.write)
|
|
89
|
+
)
|
|
90
|
+
raise GitEnvExit(0)
|
|
91
|
+
if args.write:
|
|
92
|
+
print("git env: --write requires --install-completions", file=sys.stderr)
|
|
93
|
+
raise GitEnvExit(3)
|
|
94
|
+
if args.verbose and args.quiet:
|
|
95
|
+
print(
|
|
96
|
+
"git env: --verbose and --quiet are mutually exclusive", file=sys.stderr
|
|
97
|
+
)
|
|
98
|
+
raise GitEnvExit(3)
|
|
99
|
+
|
|
100
|
+
reporter = Reporter(verbose=args.verbose, quiet=args.quiet)
|
|
101
|
+
|
|
102
|
+
match args.command:
|
|
103
|
+
case None:
|
|
104
|
+
parser.error("a subcommand is required, try 'git env --help'")
|
|
105
|
+
case "sync":
|
|
106
|
+
sync(
|
|
107
|
+
pattern=args.pattern,
|
|
108
|
+
path=args.path,
|
|
109
|
+
dry_run=args.dry_run,
|
|
110
|
+
force=args.force,
|
|
111
|
+
reporter=reporter,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
except GitEnvExit as exc:
|
|
115
|
+
sys.exit(exc.code)
|
|
116
|
+
except SystemExit as exc:
|
|
117
|
+
code = exc.code if isinstance(exc.code, int) else 1
|
|
118
|
+
if code == 2:
|
|
119
|
+
sys.exit(3)
|
|
120
|
+
raise
|
|
@@ -102,10 +102,6 @@ class _IgnoreMatcher:
|
|
|
102
102
|
ignored = not negate
|
|
103
103
|
return ignored
|
|
104
104
|
|
|
105
|
-
@property
|
|
106
|
-
def empty(self) -> bool:
|
|
107
|
-
return not self._rules
|
|
108
|
-
|
|
109
105
|
|
|
110
106
|
def _load_ignore_matcher(primary_root: Path) -> _IgnoreMatcher:
|
|
111
107
|
ignore_file = primary_root / ".envsyncignore"
|
|
@@ -148,7 +144,7 @@ def discover_env_files(
|
|
|
148
144
|
continue
|
|
149
145
|
if child.is_symlink() and not config.follow_symlinks:
|
|
150
146
|
continue
|
|
151
|
-
if
|
|
147
|
+
if ignore.matches(rel_child_posix, is_dir=True):
|
|
152
148
|
continue
|
|
153
149
|
kept_dirnames.append(dirname)
|
|
154
150
|
dirnames[:] = kept_dirnames
|
|
@@ -162,7 +158,7 @@ def discover_env_files(
|
|
|
162
158
|
continue
|
|
163
159
|
if _matches_any(filename, config.exclude):
|
|
164
160
|
continue
|
|
165
|
-
if
|
|
161
|
+
if ignore.matches(rel_posix, is_dir=False):
|
|
166
162
|
continue
|
|
167
163
|
|
|
168
164
|
is_symlink = abs_path.is_symlink()
|
|
@@ -48,12 +48,6 @@ class SyncResult:
|
|
|
48
48
|
return 0
|
|
49
49
|
|
|
50
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
51
|
|
|
58
52
|
def _resolve_dest(worktree_root: Path, relative_path: Path) -> Path:
|
|
59
53
|
"""Resolve `relative_path` against `worktree_root`, refusing escapes.
|
|
@@ -173,7 +167,9 @@ def run_sync(
|
|
|
173
167
|
) -> SyncResult:
|
|
174
168
|
"""Sync env files from `repo.primary_root` into `repo.worktree_root`."""
|
|
175
169
|
files, warnings = discover_env_files(repo.primary_root, config)
|
|
176
|
-
|
|
170
|
+
if path is not None:
|
|
171
|
+
prefix = Path(path)
|
|
172
|
+
files = [f for f in files if prefix in (f.relative_path, *f.relative_path.parents)]
|
|
177
173
|
|
|
178
174
|
for warning in warnings:
|
|
179
175
|
reporter.warn(f"{warning.relative_path}: {warning.message}")
|
git_env-0.3.0/src/git_env/cli.py
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|