Skip to content

Commit 479980e

Browse files
committed
refactor: set runs-on
Signed-off-by: Gabor Boros <gabor@opencraft.com>
1 parent cadecc9 commit 479980e

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

tooling/phd/git.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
Git helper utilities for detecting repository metadata.
3+
4+
These helpers are used by the instance creation workflow to detect the
5+
current cluster repository information and pass it to cookiecutter as
6+
context, avoiding reliance on Jinja extensions that run in temporary
7+
directories.
8+
"""
9+
10+
import subprocess
11+
12+
13+
def _run_git_command(args: list[str]) -> str | None:
14+
"""
15+
Run a git command and return its standard output.
16+
17+
Args:
18+
args: Arguments to pass to the `git` executable (e.g., ["rev-parse", "HEAD"]).
19+
20+
Returns:
21+
The command's stdout with trailing whitespace stripped, or None if the
22+
command fails or git is not available.
23+
"""
24+
25+
try:
26+
result = subprocess.run(
27+
["git", *args], capture_output=True, text=True, check=True
28+
)
29+
return result.stdout.strip()
30+
except (subprocess.CalledProcessError, FileNotFoundError):
31+
return None
32+
33+
34+
def get_git_repo_url() -> str:
35+
"""
36+
Get the repository remote.origin.url as an SSH URL when possible.
37+
38+
Converts HTTPS GitHub URLs (https://github.com/owner/repo.git) to SSH form
39+
(git@github.com:owner/repo.git). If the URL cannot be determined, an empty
40+
string is returned.
41+
42+
Returns:
43+
Repository URL (SSH preferred) or an empty string when unavailable.
44+
"""
45+
46+
url = _run_git_command(["config", "--get", "remote.origin.url"]) or ""
47+
48+
if not url:
49+
return ""
50+
51+
if url.startswith("https://github.com/"):
52+
return url.replace("https://github.com/", "git@github.com:")
53+
54+
return url
55+
56+
57+
def get_git_repo_branch() -> str:
58+
"""
59+
Get the current git branch name.
60+
61+
Returns:
62+
The current branch name. Falls back to "main" when detection fails.
63+
"""
64+
65+
branch = _run_git_command(["rev-parse", "--abbrev-ref", "HEAD"]) or ""
66+
return branch if branch else "main"
67+
68+
69+
def parse_repo_owner(repo_url: str) -> str:
70+
"""
71+
Extract the repository owner/organization from a GitHub URL.
72+
73+
Supports SSH (git@github.com:owner/repo.git) and HTTPS
74+
(https://github.com/owner/repo.git) formats.
75+
76+
Args:
77+
repo_url: Repository URL in SSH or HTTPS form.
78+
79+
Returns:
80+
Repository owner/organization, or an empty string if it cannot be parsed.
81+
"""
82+
83+
if not repo_url:
84+
return ""
85+
86+
path = repo_url
87+
if repo_url.startswith("git@github.com:"):
88+
path = repo_url.split(":", 1)[1]
89+
elif repo_url.startswith("https://github.com/"):
90+
path = repo_url.split("github.com/", 1)[1]
91+
92+
segments = [s for s in path.split("/") if s]
93+
if len(segments) < 2:
94+
return ""
95+
return segments[0]
96+
97+
98+
def parse_repo_name(repo_url: str) -> str:
99+
"""
100+
Extract the repository name from a GitHub URL.
101+
102+
Args:
103+
repo_url: Repository URL in SSH or HTTPS form.
104+
105+
Returns:
106+
Repository name without the trailing ".git" suffix, or an empty string
107+
if it cannot be parsed.
108+
"""
109+
110+
if not repo_url:
111+
return ""
112+
113+
path = repo_url
114+
if repo_url.startswith("git@github.com:"):
115+
path = repo_url.split(":", 1)[1]
116+
elif repo_url.startswith("https://github.com/"):
117+
path = repo_url.split("github.com/", 1)[1]
118+
119+
segments = [s for s in path.split("/") if s]
120+
if not segments:
121+
return ""
122+
123+
repo = segments[-1]
124+
if repo.endswith(".git"):
125+
repo = repo[:-4]
126+
return repo

tooling/tests/test_git.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Unit tests for phd.git helpers.
3+
"""
4+
5+
from unittest.mock import Mock, patch
6+
7+
import pytest
8+
9+
from phd.git import (
10+
get_git_repo_branch,
11+
get_git_repo_url,
12+
parse_repo_name,
13+
parse_repo_owner,
14+
)
15+
16+
17+
class TestGitUrlDetection:
18+
@patch("subprocess.run")
19+
def test_https_to_ssh_conversion(self, mrun):
20+
mrun.return_value = Mock(stdout="https://github.com/open-craft/repo.git\n")
21+
url = get_git_repo_url()
22+
assert url == "git@github.com:open-craft/repo.git"
23+
24+
@patch("subprocess.run")
25+
def test_ssh_preserved(self, mrun):
26+
mrun.return_value = Mock(stdout="git@github.com:open-craft/repo.git\n")
27+
url = get_git_repo_url()
28+
assert url == "git@github.com:open-craft/repo.git"
29+
30+
@patch("subprocess.run", side_effect=FileNotFoundError())
31+
def test_missing_url_returns_empty(self, _mrun):
32+
assert get_git_repo_url() == ""
33+
34+
35+
class TestGitBranchDetection:
36+
@patch("subprocess.run")
37+
def test_branch_detected(self, mrun):
38+
mrun.return_value = Mock(stdout="feature-branch\n")
39+
assert get_git_repo_branch() == "feature-branch"
40+
41+
@patch("subprocess.run", side_effect=FileNotFoundError())
42+
def test_branch_fallback_main(self, _mrun):
43+
assert get_git_repo_branch() == "main"
44+
45+
46+
class TestParsing:
47+
@pytest.mark.parametrize(
48+
"url,owner",
49+
[
50+
("git@github.com:open-craft/repo.git", "open-craft"),
51+
("https://github.com/open-craft/repo.git", "open-craft"),
52+
("", ""),
53+
],
54+
)
55+
def test_parse_repo_owner(self, url, owner):
56+
assert parse_repo_owner(url) == owner
57+
58+
@pytest.mark.parametrize(
59+
"url,name",
60+
[
61+
("git@github.com:open-craft/repo.git", "repo"),
62+
("https://github.com/open-craft/repo.git", "repo"),
63+
("https://github.com/open-craft/repo", "repo"),
64+
("", ""),
65+
],
66+
)
67+
def test_parse_repo_name(self, url, name):
68+
assert parse_repo_name(url) == name

0 commit comments

Comments
 (0)