git-env 0.2.0__tar.gz → 0.3.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/LICENCE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Alex Ward
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,9 +1,11 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: git-env
3
- Version: 0.2.0
3
+ Version: 0.3.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
+ License-Expression: MIT
8
+ License-File: LICENCE
7
9
  Requires-Dist: arguably>=1.2.2
8
10
  Requires-Python: >=3.10
9
11
  Description-Content-Type: text/markdown
@@ -1,11 +1,13 @@
1
1
  [project]
2
2
  name = "git-env"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Sync environment files across linked git worktrees"
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Alex Ward", email = "alxwrd@googlemail.com" }
8
8
  ]
9
+ license = "MIT"
10
+ license-files = ["LICENCE"]
9
11
  requires-python = ">=3.10"
10
12
  dependencies = [
11
13
  "arguably>=1.2.2",
@@ -6,6 +6,7 @@ local -> worktree) handled natively by `git config --get[-all]`.
6
6
  from __future__ import annotations
7
7
 
8
8
  import subprocess
9
+ from collections.abc import Callable
9
10
  from dataclasses import dataclass
10
11
  from pathlib import Path
11
12
 
@@ -89,7 +90,7 @@ def _parse_envsync_file(path: Path) -> dict[str, list[str]]:
89
90
  return values
90
91
 
91
92
 
92
- def _parse_bool(value: str, *, source: str) -> bool:
93
+ def _parse_bool(value: str, source: str) -> bool:
93
94
  lowered = value.strip().lower()
94
95
  if lowered in _TRUE_VALUES:
95
96
  return True
@@ -98,7 +99,7 @@ def _parse_bool(value: str, *, source: str) -> bool:
98
99
  raise ConfigError(f"invalid boolean value for {source}: {value!r}")
99
100
 
100
101
 
101
- def _parse_int(value: str, *, source: str) -> int:
102
+ def _parse_int(value: str, source: str) -> int:
102
103
  try:
103
104
  return int(value.strip())
104
105
  except ValueError as exc:
@@ -116,36 +117,20 @@ def _resolve_multi(
116
117
  return list(default)
117
118
 
118
119
 
119
- def _resolve_bool(
120
- config_key: str, envsync: dict[str, list[str]], envsync_key: str, default: bool, cwd: Path
121
- ) -> bool:
120
+ def _resolve(
121
+ config_key: str,
122
+ envsync: dict[str, list[str]],
123
+ envsync_key: str,
124
+ default: str | bool | int,
125
+ cwd: Path,
126
+ parse: Callable[[str, str], str | bool | int] | None = None,
127
+ ) -> str | bool | int:
122
128
  value = _git_config_get(config_key, cwd)
123
129
  if value is not None:
124
- return _parse_bool(value, source=config_key)
130
+ return parse(value, config_key) if parse else value
125
131
  if envsync_key in envsync:
126
- return _parse_bool(envsync[envsync_key][-1], source=f".envsync:{envsync_key}")
127
- return default
128
-
129
-
130
- def _resolve_int(
131
- config_key: str, envsync: dict[str, list[str]], envsync_key: str, default: int, cwd: Path
132
- ) -> int:
133
- value = _git_config_get(config_key, cwd)
134
- if value is not None:
135
- return _parse_int(value, source=config_key)
136
- if envsync_key in envsync:
137
- return _parse_int(envsync[envsync_key][-1], source=f".envsync:{envsync_key}")
138
- return default
139
-
140
-
141
- def _resolve_str(
142
- config_key: str, envsync: dict[str, list[str]], envsync_key: str, default: str, cwd: Path
143
- ) -> str:
144
- value = _git_config_get(config_key, cwd)
145
- if value is not None:
146
- return value
147
- if envsync_key in envsync:
148
- return envsync[envsync_key][-1]
132
+ raw = envsync[envsync_key][-1]
133
+ return parse(raw, f".envsync:{envsync_key}") if parse else raw
149
134
  return default
150
135
 
151
136
 
@@ -164,17 +149,13 @@ def load_config(primary_root: Path) -> SyncConfig:
164
149
  exclude = _resolve_multi(
165
150
  "env.sync.exclude", envsync, "exclude", DEFAULT_EXCLUDE, primary_root
166
151
  )
167
- follow_symlinks = _resolve_bool(
168
- "env.sync.followSymlinks",
169
- envsync,
170
- "followSymlinks",
171
- DEFAULT_FOLLOW_SYMLINKS,
172
- primary_root,
152
+ follow_symlinks = _resolve(
153
+ "env.sync.followSymlinks", envsync, "followSymlinks", DEFAULT_FOLLOW_SYMLINKS, primary_root, _parse_bool
173
154
  )
174
- max_file_size = _resolve_int(
175
- "env.sync.maxFileSize", envsync, "maxFileSize", DEFAULT_MAX_FILE_SIZE, primary_root
155
+ max_file_size = _resolve(
156
+ "env.sync.maxFileSize", envsync, "maxFileSize", DEFAULT_MAX_FILE_SIZE, primary_root, _parse_int
176
157
  )
177
- on_conflict = _resolve_str(
158
+ on_conflict = _resolve(
178
159
  "env.sync.onConflict", envsync, "onConflict", DEFAULT_ON_CONFLICT, primary_root
179
160
  )
180
161
  if on_conflict not in VALID_ON_CONFLICT:
@@ -182,15 +163,15 @@ def load_config(primary_root: Path) -> SyncConfig:
182
163
  f"invalid env.sync.onConflict value: {on_conflict!r}"
183
164
  f" (expected one of {sorted(VALID_ON_CONFLICT)})"
184
165
  )
185
- backup = _resolve_bool(
186
- "env.sync.backup", envsync, "backup", DEFAULT_BACKUP, primary_root
166
+ backup = _resolve(
167
+ "env.sync.backup", envsync, "backup", DEFAULT_BACKUP, primary_root, _parse_bool
187
168
  )
188
169
 
189
170
  return SyncConfig(
190
171
  patterns=tuple(patterns),
191
172
  exclude=tuple(exclude),
192
- follow_symlinks=follow_symlinks,
193
- max_file_size=max_file_size,
194
- on_conflict=on_conflict,
195
- backup=backup,
173
+ follow_symlinks=bool(follow_symlinks),
174
+ max_file_size=int(max_file_size), # type: ignore[arg-type]
175
+ on_conflict=str(on_conflict),
176
+ backup=bool(backup),
196
177
  )
@@ -32,10 +32,6 @@ class DiscoveredFile:
32
32
  absolute_path: Path
33
33
  """Absolute path to the file (symlink target if followed)."""
34
34
 
35
- size: int
36
-
37
- is_symlink: bool
38
-
39
35
 
40
36
  @dataclass(frozen=True)
41
37
  class DiscoveryWarning:
@@ -195,8 +191,6 @@ def discover_env_files(
195
191
  DiscoveredFile(
196
192
  relative_path=rel_path,
197
193
  absolute_path=abs_path,
198
- size=stat_result.st_size,
199
- is_symlink=is_symlink,
200
194
  )
201
195
  )
202
196
 
@@ -9,6 +9,12 @@ from dataclasses import dataclass
9
9
  from pathlib import Path
10
10
 
11
11
 
12
+ @dataclass(frozen=True)
13
+ class ShellResult:
14
+ ok: bool
15
+ out: str
16
+
17
+
12
18
  class RepoError(Exception):
13
19
  """Raised when the current location is not a usable linked worktree.
14
20
 
@@ -33,20 +39,7 @@ class Repository:
33
39
  """Absolute path to the current (linked) worktree's root directory."""
34
40
 
35
41
 
36
- def _git(*args: str, cwd: Path | None = None) -> str:
37
- try:
38
- result = subprocess.run(
39
- ["git", *args],
40
- cwd=cwd,
41
- capture_output=True,
42
- text=True,
43
- )
44
- except FileNotFoundError as exc:
45
- raise RepoError("git executable not found on PATH") from exc
46
- return result.stdout.strip()
47
-
48
-
49
- def _git_ok(*args: str, cwd: Path | None = None) -> tuple[bool, str]:
42
+ def _git(*args: str, cwd: Path | None = None) -> ShellResult:
50
43
  try:
51
44
  result = subprocess.run(
52
45
  ["git", *args],
@@ -56,7 +49,7 @@ def _git_ok(*args: str, cwd: Path | None = None) -> tuple[bool, str]:
56
49
  )
57
50
  except FileNotFoundError as exc:
58
51
  raise RepoError("git executable not found on PATH") from exc
59
- return result.returncode == 0, result.stdout.strip()
52
+ return ShellResult(ok=result.returncode == 0, out=result.stdout.strip())
60
53
 
61
54
 
62
55
  def detect_repository(cwd: Path | None = None) -> Repository:
@@ -69,21 +62,21 @@ def detect_repository(cwd: Path | None = None) -> Repository:
69
62
  """
70
63
  cwd = (cwd or Path.cwd()).resolve()
71
64
 
72
- inside_ok, inside_out = _git_ok("rev-parse", "--is-inside-work-tree", cwd=cwd)
73
- bare_ok, bare_out = _git_ok("rev-parse", "--is-bare-repository", cwd=cwd)
65
+ inside = _git("rev-parse", "--is-inside-work-tree", cwd=cwd)
66
+ bare = _git("rev-parse", "--is-bare-repository", cwd=cwd)
74
67
 
75
68
  # A bare repo has no work tree, so --is-inside-work-tree reports "false" (not
76
69
  # an error) even when we *are* inside a git dir. Check bare-ness first so that
77
70
  # case gets its own message instead of the generic "not inside a worktree" one.
78
- if bare_ok and bare_out == "true":
71
+ if bare.ok and bare.out == "true":
79
72
  raise RepoError("bare repositories are not supported")
80
73
 
81
- if not inside_ok or inside_out != "true":
74
+ if not inside.ok or inside.out != "true":
82
75
  raise RepoError("not inside a git worktree")
83
76
 
84
- git_dir = Path(_git("rev-parse", "--path-format=absolute", "--git-dir", cwd=cwd))
77
+ git_dir = Path(_git("rev-parse", "--path-format=absolute", "--git-dir", cwd=cwd).out)
85
78
  git_common_dir = Path(
86
- _git("rev-parse", "--path-format=absolute", "--git-common-dir", cwd=cwd)
79
+ _git("rev-parse", "--path-format=absolute", "--git-common-dir", cwd=cwd).out
87
80
  )
88
81
 
89
82
  if git_dir.resolve() == git_common_dir.resolve():
@@ -95,7 +88,7 @@ def detect_repository(cwd: Path | None = None) -> Repository:
95
88
 
96
89
  primary_root = git_common_dir.resolve().parent
97
90
  worktree_root = Path(
98
- _git("rev-parse", "--path-format=absolute", "--show-toplevel", cwd=cwd)
91
+ _git("rev-parse", "--path-format=absolute", "--show-toplevel", cwd=cwd).out
99
92
  ).resolve()
100
93
 
101
94
  _check_pwd_within_worktree(worktree_root)
@@ -30,15 +30,11 @@ class CompletionTarget:
30
30
  enable_snippet: str
31
31
 
32
32
 
33
- def _xdg_data_home() -> Path:
34
- return Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share")
35
-
36
-
37
33
  def completion_target(shell: str) -> CompletionTarget:
38
34
  """Resolve the standard user-level install path and rc snippet for `shell`."""
39
35
  home = Path.home()
40
36
  if shell == "bash":
41
- path = _xdg_data_home() / "bash-completion" / "completions" / "git-env"
37
+ path = Path(os.environ.get("XDG_DATA_HOME") or home / ".local" / "share") / "bash-completion" / "completions" / "git-env"
42
38
  snippet = f'source "{path}"'
43
39
  elif shell == "zsh":
44
40
  path = home / ".zsh" / "completions" / "_git-env"
File without changes
File without changes
File without changes
File without changes
File without changes