Skip to content

Commit 75b9f60

Browse files
committed
Detect branch info on detached heads during deployment
1 parent 5e6a05f commit 75b9f60

File tree

2 files changed

+57
-3
lines changed

2 files changed

+57
-3
lines changed

datajunction-clients/python/datajunction/deployment.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,9 @@ def _detect_git_branch(cwd: str | Path | None = None) -> str | None:
520520
"""
521521
Returns the current git branch name, or None if not in a git repo or
522522
git is not available.
523+
524+
In detached HEAD state (common in CI), falls back to recovering the
525+
branch from remote tracking refs (refs/remotes/origin/<branch>).
523526
"""
524527
try:
525528
result = subprocess.run(
@@ -530,7 +533,24 @@ def _detect_git_branch(cwd: str | Path | None = None) -> str | None:
530533
cwd=cwd,
531534
)
532535
branch = result.stdout.strip()
533-
return branch if branch and branch != "HEAD" else None
536+
if branch and branch != "HEAD":
537+
return branch
538+
# Detached HEAD (common in CI) — find remote branches whose tip is
539+
# exactly this commit.
540+
result2 = subprocess.run(
541+
["git", "branch", "-r", "--points-at", "HEAD"],
542+
capture_output=True,
543+
text=True,
544+
check=True,
545+
cwd=cwd,
546+
)
547+
for line in result2.stdout.splitlines():
548+
ref = line.strip()
549+
if "/" in ref: # pragma: no branch
550+
_, branch = ref.split("/", 1)
551+
if branch != "HEAD": # pragma: no branch
552+
return branch
553+
return None
534554
except (subprocess.CalledProcessError, FileNotFoundError):
535555
return None
536556

datajunction-clients/python/tests/test_deploy.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,10 +759,44 @@ def test_returns_none_when_git_not_installed(self, monkeypatch):
759759
)
760760
assert DeploymentService._detect_git_branch() is None
761761

762-
def test_returns_none_for_detached_head(self, monkeypatch):
762+
@pytest.mark.parametrize(
763+
"remote_output,expected",
764+
[
765+
(" origin/my-feature\n", "my-feature"),
766+
(" upstream/my-feature\n", "my-feature"),
767+
(" fork/my-feature\n", "my-feature"),
768+
],
769+
)
770+
def test_detached_head_falls_back_to_remote_ref(
771+
self,
772+
monkeypatch,
773+
remote_output,
774+
expected,
775+
):
776+
calls = iter(
777+
[
778+
mock.MagicMock(stdout="HEAD\n"), # git rev-parse --abbrev-ref HEAD
779+
mock.MagicMock(stdout=remote_output), # git branch -r --points-at HEAD
780+
],
781+
)
782+
monkeypatch.setattr(
783+
"datajunction.deployment.subprocess.run",
784+
lambda *a, **kw: next(calls),
785+
)
786+
assert DeploymentService._detect_git_branch() == expected
787+
788+
def test_detached_head_returns_none_when_no_remote_ref(self, monkeypatch):
789+
calls = iter(
790+
[
791+
mock.MagicMock(stdout="HEAD\n"), # git rev-parse --abbrev-ref HEAD
792+
mock.MagicMock(
793+
stdout="",
794+
), # git branch -r --points-at HEAD — no results
795+
],
796+
)
763797
monkeypatch.setattr(
764798
"datajunction.deployment.subprocess.run",
765-
lambda *a, **kw: mock.MagicMock(stdout="HEAD\n"),
799+
lambda *a, **kw: next(calls),
766800
)
767801
assert DeploymentService._detect_git_branch() is None
768802

0 commit comments

Comments
 (0)