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/utils/uri.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""URI parsing utilities for git repository URLs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ParsedURI:
|
|
13
|
+
"""Parsed URI components for git repository URLs.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
uri: Original URI string.
|
|
17
|
+
protocol: Protocol/scheme (http, https, ssh, git, file).
|
|
18
|
+
host: Hostname or IP address.
|
|
19
|
+
port: Port number as string, empty if not specified.
|
|
20
|
+
path_segments: List of path segments without leading/trailing slashes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
uri: str
|
|
24
|
+
protocol: str
|
|
25
|
+
host: str
|
|
26
|
+
port: str
|
|
27
|
+
path_segments: list[str]
|
|
28
|
+
|
|
29
|
+
def path(self, index: int) -> str:
|
|
30
|
+
"""Get path segment by index.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
index: Segment index (0-based, negative for reverse indexing).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Path segment at the specified index.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
IndexError: If index is out of range.
|
|
40
|
+
"""
|
|
41
|
+
return self.path_segments[index]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Pattern for SCP-style SSH URLs: git@host:path
|
|
45
|
+
SCP_PATTERN = re.compile(
|
|
46
|
+
r"^(?P<user>[^@]+)@(?P<host>[^:]+):(?P<path>.+)$"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_uri(uri: str) -> ParsedURI:
|
|
51
|
+
"""Parse a git repository URI into its components.
|
|
52
|
+
|
|
53
|
+
Supports:
|
|
54
|
+
- Standard URLs: https://github.com/user/repo.git
|
|
55
|
+
- SSH URLs: ssh://git@github.com/user/repo.git
|
|
56
|
+
- SCP-style SSH: git@github.com:user/repo.git
|
|
57
|
+
- File URLs: file:///path/to/repo
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
uri: Git repository URI string.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
ParsedURI with extracted components.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If the URI cannot be parsed.
|
|
67
|
+
"""
|
|
68
|
+
uri = uri.strip()
|
|
69
|
+
|
|
70
|
+
if not uri:
|
|
71
|
+
raise ValueError("Empty URI")
|
|
72
|
+
|
|
73
|
+
# Try SCP-style SSH first (git@host:path)
|
|
74
|
+
scp_match = SCP_PATTERN.match(uri)
|
|
75
|
+
if scp_match:
|
|
76
|
+
host = scp_match.group("host")
|
|
77
|
+
path = scp_match.group("path")
|
|
78
|
+
path_segments = _extract_path_segments(path)
|
|
79
|
+
return ParsedURI(
|
|
80
|
+
uri=uri,
|
|
81
|
+
protocol="ssh",
|
|
82
|
+
host=host,
|
|
83
|
+
port="",
|
|
84
|
+
path_segments=path_segments,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Try standard URL parsing
|
|
88
|
+
try:
|
|
89
|
+
parsed = urlparse(uri)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise ValueError(f"Invalid URI: {uri}") from e
|
|
92
|
+
|
|
93
|
+
if not parsed.scheme:
|
|
94
|
+
raise ValueError(f"Missing protocol/scheme in URI: {uri}")
|
|
95
|
+
|
|
96
|
+
if not parsed.netloc and parsed.scheme != "file":
|
|
97
|
+
raise ValueError(f"Missing host in URI: {uri}")
|
|
98
|
+
|
|
99
|
+
# Extract protocol
|
|
100
|
+
protocol = parsed.scheme.lower()
|
|
101
|
+
|
|
102
|
+
# Extract host and port
|
|
103
|
+
host = parsed.hostname or ""
|
|
104
|
+
port = str(parsed.port) if parsed.port else ""
|
|
105
|
+
|
|
106
|
+
# Extract and clean path segments
|
|
107
|
+
path = parsed.path
|
|
108
|
+
path_segments = _extract_path_segments(path)
|
|
109
|
+
|
|
110
|
+
if not path_segments and protocol != "file":
|
|
111
|
+
raise ValueError(f"Missing path in URI: {uri}")
|
|
112
|
+
|
|
113
|
+
return ParsedURI(
|
|
114
|
+
uri=uri,
|
|
115
|
+
protocol=protocol,
|
|
116
|
+
host=host,
|
|
117
|
+
port=port,
|
|
118
|
+
path_segments=path_segments,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _extract_path_segments(path: str) -> list[str]:
|
|
123
|
+
"""Extract clean path segments from a path string.
|
|
124
|
+
|
|
125
|
+
Removes:
|
|
126
|
+
- Leading/trailing slashes
|
|
127
|
+
- Empty segments
|
|
128
|
+
- .git suffix from the last segment
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
path: Path string to parse.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of path segments.
|
|
135
|
+
"""
|
|
136
|
+
# Remove leading/trailing slashes and split
|
|
137
|
+
segments = [s for s in path.strip("/").split("/") if s]
|
|
138
|
+
|
|
139
|
+
# Remove .git suffix from last segment
|
|
140
|
+
if segments and segments[-1].endswith(".git"):
|
|
141
|
+
segments[-1] = segments[-1][:-4]
|
|
142
|
+
|
|
143
|
+
return segments
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_repo_name(uri: str) -> str:
|
|
147
|
+
"""Extract repository name from a URI.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
uri: Git repository URI.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Repository name (last path segment without .git).
|
|
154
|
+
"""
|
|
155
|
+
parsed = parse_uri(uri)
|
|
156
|
+
return parsed.path_segments[-1] if parsed.path_segments else ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_owner(uri: str) -> Optional[str]:
|
|
160
|
+
"""Extract owner/organization from a URI.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
uri: Git repository URI.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Owner name (second-to-last path segment) or None.
|
|
167
|
+
"""
|
|
168
|
+
parsed = parse_uri(uri)
|
|
169
|
+
if len(parsed.path_segments) >= 2:
|
|
170
|
+
return parsed.path_segments[-2]
|
|
171
|
+
return None
|
gww/utils/xdg.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""XDG Base Directory specification handling for cross-platform config paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
APP_NAME = "gww"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def user_config_dir(appname: str = APP_NAME) -> Path:
|
|
13
|
+
"""Return cross-platform config directory following XDG/OS conventions.
|
|
14
|
+
|
|
15
|
+
On every platform, ``$XDG_CONFIG_HOME`` is honored when set to an
|
|
16
|
+
absolute path (per the XDG Base Directory spec). Otherwise the
|
|
17
|
+
platform default is used:
|
|
18
|
+
|
|
19
|
+
- Linux: ``~/.config/{appname}``
|
|
20
|
+
- macOS: ``~/Library/Application Support/{appname}``
|
|
21
|
+
- Windows: ``%APPDATA%/{appname}`` (falling back to
|
|
22
|
+
``~/AppData/Roaming/{appname}``)
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
appname: Application name for the config subdirectory.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Path to the application config directory.
|
|
29
|
+
"""
|
|
30
|
+
home = Path.home()
|
|
31
|
+
|
|
32
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
33
|
+
if xdg and Path(xdg).is_absolute():
|
|
34
|
+
return Path(xdg) / appname
|
|
35
|
+
|
|
36
|
+
platform = sys.platform
|
|
37
|
+
|
|
38
|
+
if platform.startswith("win"):
|
|
39
|
+
base = os.environ.get("APPDATA", str(home / "AppData" / "Roaming"))
|
|
40
|
+
return Path(base) / appname
|
|
41
|
+
|
|
42
|
+
if platform == "darwin":
|
|
43
|
+
return home / "Library" / "Application Support" / appname
|
|
44
|
+
|
|
45
|
+
return home / ".config" / appname
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_config_path(appname: str = APP_NAME) -> Path:
|
|
49
|
+
"""Return full path to config file.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
appname: Application name for the config subdirectory.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to config.yml in the user config directory.
|
|
56
|
+
"""
|
|
57
|
+
return user_config_dir(appname) / "config.yml"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ensure_config_dir(appname: str = APP_NAME) -> Path:
|
|
61
|
+
"""Ensure config directory exists, creating it if necessary.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
appname: Application name for the config subdirectory.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Path to the existing or newly created config directory.
|
|
68
|
+
"""
|
|
69
|
+
config_dir = user_config_dir(appname)
|
|
70
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
return config_dir
|