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.
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