git-worktree-wrapper 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_worktree_wrapper-0.1.0.dist-info/METADATA +473 -0
- git_worktree_wrapper-0.1.0.dist-info/RECORD +35 -0
- git_worktree_wrapper-0.1.0.dist-info/WHEEL +4 -0
- git_worktree_wrapper-0.1.0.dist-info/entry_points.txt +2 -0
- gww/__init__.py +3 -0
- gww/actions/__init__.py +224 -0
- gww/actions/types.py +187 -0
- gww/cli/__init__.py +1 -0
- gww/cli/commands/__init__.py +1 -0
- gww/cli/commands/add.py +122 -0
- gww/cli/commands/clone.py +97 -0
- gww/cli/commands/init.py +147 -0
- gww/cli/commands/migrate.py +81 -0
- gww/cli/commands/pull.py +62 -0
- gww/cli/commands/remove.py +153 -0
- gww/cli/context.py +382 -0
- gww/cli/main.py +285 -0
- gww/config/__init__.py +1 -0
- gww/config/loader.py +305 -0
- gww/config/resolver.py +188 -0
- gww/config/validator.py +344 -0
- gww/git/__init__.py +1 -0
- gww/git/branch.py +264 -0
- gww/git/repository.py +403 -0
- gww/git/worktree.py +395 -0
- gww/migration/__init__.py +44 -0
- gww/migration/executor.py +342 -0
- gww/migration/planner.py +260 -0
- gww/template/__init__.py +1 -0
- gww/template/evaluator.py +281 -0
- gww/template/functions.py +378 -0
- gww/utils/__init__.py +1 -0
- gww/utils/shell.py +894 -0
- gww/utils/uri.py +171 -0
- gww/utils/xdg.py +71 -0
gww/config/loader.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Configuration file loading using ruamel.yaml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from ruamel.yaml import YAML
|
|
9
|
+
from ruamel.yaml.error import YAMLError
|
|
10
|
+
|
|
11
|
+
from gww.utils.xdg import get_config_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigLoadError(Exception):
|
|
15
|
+
"""Raised when config file cannot be loaded."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigNotFoundError(ConfigLoadError):
|
|
21
|
+
"""Raised when config file does not exist."""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_yaml() -> YAML:
|
|
27
|
+
"""Create a YAML instance configured for round-trip parsing.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Configured YAML instance.
|
|
31
|
+
"""
|
|
32
|
+
yaml = YAML(typ="rt") # Round-trip mode preserves comments and formatting
|
|
33
|
+
yaml.preserve_quotes = True
|
|
34
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
35
|
+
return yaml
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_config(config_path: Optional[Path] = None) -> dict[str, Any]:
|
|
39
|
+
"""Load configuration from YAML file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config_path: Path to config file. If None, uses default XDG path.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Parsed configuration dictionary.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ConfigNotFoundError: If config file does not exist.
|
|
49
|
+
ConfigLoadError: If config file cannot be parsed.
|
|
50
|
+
"""
|
|
51
|
+
if config_path is None:
|
|
52
|
+
config_path = get_config_path()
|
|
53
|
+
|
|
54
|
+
if not config_path.exists():
|
|
55
|
+
raise ConfigNotFoundError(f"Config file not found: {config_path}")
|
|
56
|
+
|
|
57
|
+
yaml = _create_yaml()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
61
|
+
data = yaml.load(f)
|
|
62
|
+
except YAMLError as e:
|
|
63
|
+
raise ConfigLoadError(f"Invalid YAML in config file {config_path}: {e}") from e
|
|
64
|
+
except OSError as e:
|
|
65
|
+
raise ConfigLoadError(f"Cannot read config file {config_path}: {e}") from e
|
|
66
|
+
|
|
67
|
+
if data is None:
|
|
68
|
+
# Empty file or only comments
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
if not isinstance(data, dict):
|
|
72
|
+
raise ConfigLoadError(
|
|
73
|
+
f"Config file must contain a mapping, got {type(data).__name__}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return dict(data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def save_config(config: dict[str, Any], config_path: Optional[Path] = None) -> Path:
|
|
80
|
+
"""Save configuration to YAML file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: Configuration dictionary to save.
|
|
84
|
+
config_path: Path to config file. If None, uses default XDG path.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Path where config was saved.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ConfigLoadError: If config file cannot be written.
|
|
91
|
+
"""
|
|
92
|
+
if config_path is None:
|
|
93
|
+
config_path = get_config_path()
|
|
94
|
+
|
|
95
|
+
# Ensure parent directory exists
|
|
96
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
yaml = _create_yaml()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
102
|
+
yaml.dump(config, f)
|
|
103
|
+
except OSError as e:
|
|
104
|
+
raise ConfigLoadError(f"Cannot write config file {config_path}: {e}") from e
|
|
105
|
+
|
|
106
|
+
return config_path
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def config_exists(config_path: Optional[Path] = None) -> bool:
|
|
110
|
+
"""Check if config file exists.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
config_path: Path to config file. If None, uses default XDG path.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if config file exists.
|
|
117
|
+
"""
|
|
118
|
+
if config_path is None:
|
|
119
|
+
config_path = get_config_path()
|
|
120
|
+
return config_path.exists()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
DEFAULT_CONFIG_TEMPLATE = """\
|
|
124
|
+
# GWW (Git Worktree Wrapper) Configuration
|
|
125
|
+
# =========================================
|
|
126
|
+
#
|
|
127
|
+
# This file configures how gww manages your git repositories and worktrees.
|
|
128
|
+
# Location: {config_path}
|
|
129
|
+
|
|
130
|
+
# Template Functions and Variables Available
|
|
131
|
+
# ===========================================
|
|
132
|
+
#
|
|
133
|
+
# SHARED FUNCTIONS (available in templates and 'when' conditions):
|
|
134
|
+
# ----------------------------------------------------------------------------------
|
|
135
|
+
#
|
|
136
|
+
# URI Functions:
|
|
137
|
+
# path(n) - URI path segment at index n (0-based, negative for reverse)
|
|
138
|
+
# Example: path(-1) returns "repo", path(0) returns "user"
|
|
139
|
+
#
|
|
140
|
+
# host() - URI hostname (string)
|
|
141
|
+
# Example: host() returns "github.com" from "https://github.com/user/repo"
|
|
142
|
+
#
|
|
143
|
+
# uri() - Full URI string (string)
|
|
144
|
+
# Example: uri() returns "https://github.com/user/repo.git"
|
|
145
|
+
#
|
|
146
|
+
# port() - URI port number (string, empty if not specified)
|
|
147
|
+
# Example: port() returns "3000" from "http://git.example.com:3000/path"
|
|
148
|
+
#
|
|
149
|
+
# protocol() - URI protocol/scheme (string)
|
|
150
|
+
# Example: protocol() returns "https", "ssh", "git"
|
|
151
|
+
#
|
|
152
|
+
# Branch Functions (require branch context, available in worktree templates):
|
|
153
|
+
# branch() - Current branch name (as-is)
|
|
154
|
+
# Example: branch() from "feature/new-ui" → "feature/new-ui"
|
|
155
|
+
#
|
|
156
|
+
# norm_branch(sep) - Branch name with "/" replaced
|
|
157
|
+
# - norm_branch(): replaces "/" with "-"
|
|
158
|
+
# - norm_branch("_"): replaces "/" with "_"
|
|
159
|
+
# Example: norm_branch() from "feature/new-ui" → "feature-new-ui"
|
|
160
|
+
#
|
|
161
|
+
# Utility Functions (template-only, not available in 'when' conditions):
|
|
162
|
+
# time_id(fmt) - Generate datetime-based identifier string
|
|
163
|
+
# The datetime is captured on first call and cached for subsequent
|
|
164
|
+
# calls within the same template evaluation session.
|
|
165
|
+
# - time_id(): default format "%Y%m%d-%H%M.%S" → "20260120-2134.03"
|
|
166
|
+
# - time_id("%Y-%m-%d"): custom format → "2026-01-20"
|
|
167
|
+
# - time_id("%H%M%S"): time only → "213403"
|
|
168
|
+
# Format codes: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
|
|
169
|
+
# Useful for creating unique branch/worktree names with timestamps.
|
|
170
|
+
#
|
|
171
|
+
# Tag Functions:
|
|
172
|
+
# tag(name) - Get tag value by name (returns empty string if not set)
|
|
173
|
+
# Tags are passed via --tag option
|
|
174
|
+
# Example: tag("env") returns "prod" if --tag env=prod was used
|
|
175
|
+
#
|
|
176
|
+
# tag_exist(name) - Check if tag exists (returns True/False)
|
|
177
|
+
# Useful in 'when' conditions for conditional routing and path templates
|
|
178
|
+
# Example: tag_exist("env") returns True if --tag env was used
|
|
179
|
+
#
|
|
180
|
+
# PROJECT-SPECIFIC FUNCTIONS (only in project 'when' conditions):
|
|
181
|
+
# ---------------------------------------------------------
|
|
182
|
+
# source_path(extra?) - Absolute path to the source repository (string),
|
|
183
|
+
# optionally joined with `extra` (which may itself be
|
|
184
|
+
# a template). The CLI populates this for project
|
|
185
|
+
# predicates; it is the same value in clone, add,
|
|
186
|
+
# and before_remove.
|
|
187
|
+
# Examples:
|
|
188
|
+
# source_path() returns "/home/user/Developer/sources/github/user/repo"
|
|
189
|
+
# source_path("local.properties")
|
|
190
|
+
# returns "/home/user/Developer/sources/github/user/repo/local.properties"
|
|
191
|
+
#
|
|
192
|
+
# current_worktree(extra?) - Absolute path to the operation target (the
|
|
193
|
+
# freshly-cloned source for after_clone; the
|
|
194
|
+
# worktree for after_add and before_remove),
|
|
195
|
+
# optionally joined with `extra`.
|
|
196
|
+
# Examples:
|
|
197
|
+
# After clone: current_worktree() returns "/home/user/Developer/sources/github/user/repo"
|
|
198
|
+
# After add: current_worktree() returns "/home/user/Developer/worktrees/github/user/repo/feature-branch"
|
|
199
|
+
#
|
|
200
|
+
# file_exists(path) - Check if file exists in source repository (returns True/False)
|
|
201
|
+
# Path is relative to source repository root
|
|
202
|
+
# Example: file_exists("package.json") checks for package.json in repo
|
|
203
|
+
#
|
|
204
|
+
# dir_exists(path) - Check if directory exists in source repository (returns True/False)
|
|
205
|
+
# Path is relative to source repository root
|
|
206
|
+
# Example: dir_exists("src") checks for src/ directory in repo
|
|
207
|
+
#
|
|
208
|
+
# path_exists(path) - Check if path exists (file or directory) in source repository
|
|
209
|
+
# Path is relative to source repository root
|
|
210
|
+
# Example: path_exists("README.md") checks for README.md in repo
|
|
211
|
+
|
|
212
|
+
# Default paths for sources (cloned repositories) and worktrees
|
|
213
|
+
default_sources: ~/Developer/sources/default/path(-2)/path(-1)
|
|
214
|
+
default_worktrees: ~/Developer/worktrees/default/path(-2)/path(-1)/norm_branch()
|
|
215
|
+
|
|
216
|
+
# Source routing rules (optional)
|
|
217
|
+
# Routes repositories to different locations based on URI conditions
|
|
218
|
+
# Uncomment and customize as needed:
|
|
219
|
+
|
|
220
|
+
# sources:
|
|
221
|
+
# github:
|
|
222
|
+
# when: '"github" in host()'
|
|
223
|
+
# sources: ~/Developer/sources/github/path(-2)/path(-1)
|
|
224
|
+
# worktrees: ~/Developer/worktrees/github/path(-2)/path(-1)/norm_branch()
|
|
225
|
+
#
|
|
226
|
+
# gitlab:
|
|
227
|
+
# when: '"gitlab" in host()'
|
|
228
|
+
# sources: ~/Developer/sources/gitlab/path(-3)/path(-2)/path(-1)
|
|
229
|
+
# worktrees: ~/Developer/worktrees/gitlab/path(-3)/path(-2)/path(-1)/norm_branch()
|
|
230
|
+
#
|
|
231
|
+
# custom:
|
|
232
|
+
# when: 'path(0) == "myorg"'
|
|
233
|
+
# sources: ~/Developer/sources/custom/path(-2)/path(-1)
|
|
234
|
+
#
|
|
235
|
+
# # Tag-based routing examples:
|
|
236
|
+
# review:
|
|
237
|
+
# when: 'tag_exist("review")
|
|
238
|
+
# sources: ~/Developer/sources/custom/path(-2)/path(-1)
|
|
239
|
+
# worktrees: ~/Developer/worktrees/review/path(-2)/path(-1)/norm_branch()
|
|
240
|
+
#
|
|
241
|
+
# # Tag-based path templates:
|
|
242
|
+
# tagged_sources:
|
|
243
|
+
# when: 'tag_exist("worktree-name")'
|
|
244
|
+
# sources: ~/Developer/sources/tag("project")/path(-2)/path(-1)
|
|
245
|
+
# worktrees: ~/Developer/worktrees/tag("project")/path(-2)/path(-1)/branch()-tag("worktree-name")
|
|
246
|
+
#
|
|
247
|
+
# # Time-based worktree names (useful for temporary or timestamped worktrees):
|
|
248
|
+
# timestamped:
|
|
249
|
+
# when: 'tag_exist("timestamp")'
|
|
250
|
+
# worktrees: ~/Developer/worktrees/path(-2)/path(-1)/norm_branch()-time_id()
|
|
251
|
+
#
|
|
252
|
+
# Actions (optional)
|
|
253
|
+
# Execute actions after clone or worktree creation based on project detection
|
|
254
|
+
#
|
|
255
|
+
# Action syntax:
|
|
256
|
+
# - command: "single string with optional template functions"
|
|
257
|
+
# - copy: ["source-template", "destination-template"] # templates are evaluated first
|
|
258
|
+
# - Template functions are evaluated first; for `command` the result is
|
|
259
|
+
# then parsed as shell arguments, for `copy` the resolved source and
|
|
260
|
+
# destination are passed to the copy action.
|
|
261
|
+
# - Commands always execute with current_worktree() as the current working directory.
|
|
262
|
+
# - Use quotes for arguments with spaces: command: "echo 'hello world'"
|
|
263
|
+
# - Available functions: current_worktree(), source_path(), tag("name"), etc.
|
|
264
|
+
#
|
|
265
|
+
# Uncomment and customize as needed:
|
|
266
|
+
|
|
267
|
+
# actions:
|
|
268
|
+
# - when: 'file_exists("local.properties")'
|
|
269
|
+
# after_clone:
|
|
270
|
+
# - copy: ["~/sources/default-local.properties", "local.properties"]
|
|
271
|
+
# after_add:
|
|
272
|
+
# - copy: ["source_path('local.properties')", "local.properties"]
|
|
273
|
+
# - command: "./setup-env.sh"
|
|
274
|
+
#
|
|
275
|
+
# # Tag-based actions:
|
|
276
|
+
# - when: not file_exists("CLAUDE.md") and tag_exist("use-claude")
|
|
277
|
+
# after_clone:
|
|
278
|
+
# - command: "claude init"
|
|
279
|
+
# after_add:
|
|
280
|
+
# - copy: ["source_path('CLAUDE.md')", "CLAUDE.md"]
|
|
281
|
+
#
|
|
282
|
+
# # Commands with template functions:
|
|
283
|
+
# - when: file_exists("CLAUDE.md") and tag_exist("use-claude") and tag_exist("review")
|
|
284
|
+
# after_add:
|
|
285
|
+
# - command: "claude -p tag('prompt') --cwd current_worktree()"
|
|
286
|
+
#
|
|
287
|
+
# # Simple command with current_worktree:
|
|
288
|
+
# - when: 'file_exists("package.json")'
|
|
289
|
+
# after_add:
|
|
290
|
+
# - command: "npm install --prefix current_worktree()"
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def get_default_config(config_path: Optional[Path] = None) -> str:
|
|
295
|
+
"""Get default configuration file content.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
config_path: Path where config will be saved (for documentation).
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Default configuration YAML content.
|
|
302
|
+
"""
|
|
303
|
+
if config_path is None:
|
|
304
|
+
config_path = get_config_path()
|
|
305
|
+
return DEFAULT_CONFIG_TEMPLATE.format(config_path=config_path)
|
gww/config/resolver.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Configuration resolver for path template evaluation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from gww.config.validator import Config, SourceRule
|
|
9
|
+
from gww.template.evaluator import TemplateError, evaluate_predicate, evaluate_template
|
|
10
|
+
from gww.template.functions import TemplateContext, create_function_registry
|
|
11
|
+
from gww.utils.uri import ParsedURI
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResolverError(Exception):
|
|
15
|
+
"""Raised when path resolution fails."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _expand_home(path: str) -> str:
|
|
21
|
+
"""Expand ~ to home directory in path string.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
path: Path string that may contain ~.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path with ~ expanded to home directory.
|
|
28
|
+
"""
|
|
29
|
+
if path.startswith("~"):
|
|
30
|
+
return str(Path(path).expanduser())
|
|
31
|
+
return path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_uri_context(uri: ParsedURI, tags: dict[str, str] = {}) -> dict[str, Any]:
|
|
35
|
+
"""Build evaluation context for URI predicates.
|
|
36
|
+
|
|
37
|
+
Uses the unified FunctionRegistry to provide shared functions:
|
|
38
|
+
- URI functions: host(), port(), protocol(), uri(), path(index)
|
|
39
|
+
- Tag functions: tag(name), tag_exist(name)
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
uri: Parsed URI object.
|
|
43
|
+
tags: Optional dictionary of tag key-value pairs.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary of context functions for predicate evaluation.
|
|
47
|
+
"""
|
|
48
|
+
context = TemplateContext(uri=uri, tags=tags)
|
|
49
|
+
return create_function_registry(context)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def find_matching_source_rule(
|
|
53
|
+
config: Config,
|
|
54
|
+
uri: ParsedURI,
|
|
55
|
+
tags: dict[str, str] = {},
|
|
56
|
+
) -> Optional[SourceRule]:
|
|
57
|
+
"""Find the first matching source rule for a URI.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config: Validated configuration.
|
|
61
|
+
uri: Parsed URI to match against.
|
|
62
|
+
tags: Optional dictionary of tag key-value pairs.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Matching SourceRule, or None if no match.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ResolverError: If predicate evaluation fails.
|
|
69
|
+
"""
|
|
70
|
+
context = _build_uri_context(uri, tags)
|
|
71
|
+
|
|
72
|
+
for name, rule in config.sources.items():
|
|
73
|
+
try:
|
|
74
|
+
if evaluate_predicate(rule.when, context):
|
|
75
|
+
return rule
|
|
76
|
+
except TemplateError as e:
|
|
77
|
+
raise ResolverError(
|
|
78
|
+
f"Error evaluating 'when' for source rule '{name}': {e}"
|
|
79
|
+
) from e
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_source_path(
|
|
85
|
+
config: Config,
|
|
86
|
+
uri: ParsedURI,
|
|
87
|
+
tags: dict[str, str] = {},
|
|
88
|
+
) -> Path:
|
|
89
|
+
"""Resolve the source checkout path for a URI.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
config: Validated configuration.
|
|
93
|
+
uri: Parsed URI for the repository.
|
|
94
|
+
tags: Optional dictionary of tag key-value pairs.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Absolute path where repository should be cloned.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ResolverError: If path resolution fails.
|
|
101
|
+
"""
|
|
102
|
+
# Find matching rule or use default
|
|
103
|
+
rule = find_matching_source_rule(config, uri, tags)
|
|
104
|
+
|
|
105
|
+
if rule and rule.sources:
|
|
106
|
+
template = rule.sources
|
|
107
|
+
else:
|
|
108
|
+
template = config.default_sources
|
|
109
|
+
|
|
110
|
+
# Create context and evaluate template
|
|
111
|
+
context = TemplateContext(uri=uri, tags=tags)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
path_str = evaluate_template(template, context)
|
|
115
|
+
except TemplateError as e:
|
|
116
|
+
raise ResolverError(f"Error evaluating source path template: {e}") from e
|
|
117
|
+
|
|
118
|
+
# Expand ~ and resolve to absolute path
|
|
119
|
+
path_str = _expand_home(path_str)
|
|
120
|
+
return Path(path_str).resolve()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def resolve_worktree_path(
|
|
124
|
+
config: Config,
|
|
125
|
+
uri: ParsedURI,
|
|
126
|
+
branch: str,
|
|
127
|
+
tags: dict[str, str] = {},
|
|
128
|
+
) -> Path:
|
|
129
|
+
"""Resolve the worktree path for a branch.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
config: Validated configuration.
|
|
133
|
+
uri: Parsed URI for the repository.
|
|
134
|
+
branch: Branch name for the worktree.
|
|
135
|
+
tags: Optional dictionary of tag key-value pairs.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Absolute path where worktree should be created.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ResolverError: If path resolution fails.
|
|
142
|
+
"""
|
|
143
|
+
# Find matching rule or use default
|
|
144
|
+
rule = find_matching_source_rule(config, uri, tags)
|
|
145
|
+
|
|
146
|
+
if rule and rule.worktrees:
|
|
147
|
+
template = rule.worktrees
|
|
148
|
+
else:
|
|
149
|
+
template = config.default_worktrees
|
|
150
|
+
|
|
151
|
+
# Create context and evaluate template
|
|
152
|
+
context = TemplateContext(
|
|
153
|
+
uri=uri,
|
|
154
|
+
branch=branch,
|
|
155
|
+
tags=tags,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
path_str = evaluate_template(template, context)
|
|
160
|
+
except TemplateError as e:
|
|
161
|
+
raise ResolverError(f"Error evaluating worktree path template: {e}") from e
|
|
162
|
+
|
|
163
|
+
# Expand ~ and resolve to absolute path
|
|
164
|
+
path_str = _expand_home(path_str)
|
|
165
|
+
return Path(path_str).resolve()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_source_path_for_worktree(
|
|
169
|
+
config: Config,
|
|
170
|
+
uri: ParsedURI,
|
|
171
|
+
tags: dict[str, str] = {},
|
|
172
|
+
) -> Path:
|
|
173
|
+
"""Get the source path that corresponds to a worktree's repository.
|
|
174
|
+
|
|
175
|
+
This is useful when working from a worktree to find its source repository.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
config: Validated configuration.
|
|
179
|
+
uri: Parsed URI for the repository.
|
|
180
|
+
tags: Optional dictionary of tag key-value pairs.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Absolute path to the source repository.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ResolverError: If path resolution fails.
|
|
187
|
+
"""
|
|
188
|
+
return resolve_source_path(config, uri, tags)
|