Skip to content

Commit 357007a

Browse files
authored
Fix git repository handling (#22)
This PR fixes how tmt web handles git repositories. A cloned local repository is now properly updated when changes occur in the remote repository. Also, when no ref is specified, the default branch will now be used instead of reusing the ref from the previous checkout. Fixes #21 Related: https://issues.redhat.com/browse/TFT-3576
1 parent e1d221c commit 357007a

File tree

2 files changed

+94
-21
lines changed

2 files changed

+94
-21
lines changed

src/tmt_web/utils/git_handler.py

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
It uses tmt's Git utilities for robust clone operations with retry logic.
77
"""
88

9+
import re
910
from shutil import rmtree
1011

1112
from tmt import Logger
@@ -50,7 +51,7 @@ def clear_tmp_dir(logger: Logger) -> None:
5051
raise GeneralError(f"Failed to clear repository clone directory '{path}'") from err
5152

5253

53-
def clone_repository(url: str, logger: Logger, ref: str | None = None) -> Path:
54+
def clone_repository(url: str, logger: Logger) -> Path:
5455
"""
5556
Clone a Git repository to a unique path.
5657
@@ -71,15 +72,6 @@ def clone_repository(url: str, logger: Logger, ref: str | None = None) -> Path:
7172
# Clone with retry logic
7273
git_clone(url=url, destination=destination, logger=logger)
7374

74-
# If ref provided, checkout after clone
75-
if ref:
76-
common = Common(logger=logger)
77-
try:
78-
common.run(Command("git", "checkout", ref), cwd=destination)
79-
except RunError as err:
80-
logger.fail(f"Failed to checkout ref '{ref}'")
81-
raise AttributeError(f"Failed to checkout ref '{ref}': {err}") from err
82-
8375
return destination
8476

8577

@@ -92,17 +84,94 @@ def get_git_repository(url: str, logger: Logger, ref: str | None = None) -> Path
9284
:param ref: Optional ref to checkout
9385
:return: Path to the cloned repository
9486
:raises: GitUrlError if URL is invalid
95-
:raises: GeneralError if clone fails
87+
:raises: GeneralError if cloning, fetching, or updating a branch fails
9688
:raises: AttributeError if ref doesn't exist
9789
"""
9890
destination = get_unique_clone_path(url)
9991
if not destination.exists():
100-
clone_repository(url, logger, ref)
101-
elif ref:
102-
common = Common(logger=logger)
92+
clone_repository(url, logger)
93+
94+
common = Common(logger=logger)
95+
96+
# Fetch remote refs
97+
_fetch_remote(common, destination, logger)
98+
99+
# If no ref is specified, the default branch is used
100+
ref = ref or _get_default_branch(common, destination, logger)
101+
102+
try:
103+
common.run(Command("git", "checkout", ref), cwd=destination)
104+
except RunError as err:
105+
logger.fail(f"Failed to checkout ref '{ref}'")
106+
raise AttributeError(f"Failed to checkout ref '{ref}'") from err
107+
108+
# If the ref is a branch, ensure it's up to date
109+
if _is_branch(common, destination, ref):
110+
_update_branch(common, destination, ref, logger)
111+
112+
return destination
113+
114+
115+
def _get_default_branch(common: Common, repo_path: Path, logger: Logger) -> str:
116+
"""Determine the default branch of a Git repository using a remote HEAD."""
117+
try:
118+
output = common.run(
119+
Command("git", "symbolic-ref", "refs/remotes/origin/HEAD"), cwd=repo_path
120+
)
121+
if output.stdout:
122+
match = re.search(r"refs/remotes/origin/(.*)", output.stdout.strip())
123+
if match:
124+
return match.group(1)
125+
126+
logger.fail(f"Failed to determine default branch for repository '{repo_path}'")
127+
raise GeneralError(f"Failed to determine default branch for repository '{repo_path}'")
128+
129+
except RunError as err:
130+
logger.fail(f"Failed to determine default branch for repository '{repo_path}'")
131+
raise GeneralError(
132+
f"Failed to determine default branch for repository '{repo_path}'"
133+
) from err
134+
135+
136+
def _fetch_remote(common: Common, repo_path: Path, logger: Logger) -> None:
137+
"""Fetch updates from the remote repository."""
138+
try:
139+
common.run(Command("git", "fetch"), cwd=repo_path)
140+
except RunError as err:
141+
logger.fail(f"Failed to fetch remote for repository '{repo_path}'")
142+
raise GeneralError(f"Failed to fetch remote for repository '{repo_path}'") from err
143+
144+
145+
def _update_branch(common: Common, repo_path: Path, branch: str, logger: Logger) -> None:
146+
"""Ensure the specified branch is up to date with its remote counterpart."""
147+
try:
148+
common.run(Command("git", "show-branch", f"origin/{branch}"), cwd=repo_path)
149+
except RunError as err:
150+
logger.fail(f"Branch '{branch}' does not exist in repository '{repo_path}'")
151+
raise GeneralError(f"Branch {branch}' does not exist in repository '{repo_path}'") from err
152+
try:
153+
# Check if the branch is already up to date
154+
common.run(Command("git", "diff", "--quiet", branch, f"origin/{branch}"), cwd=repo_path)
155+
return
156+
except RunError:
157+
# Branch is not up to date, proceed with update
103158
try:
104-
common.run(Command("git", "checkout", ref), cwd=destination)
159+
common.run(Command("git", "reset", "--hard", f"origin/{branch}"), cwd=repo_path)
105160
except RunError as err:
106-
logger.fail(f"Failed to checkout ref '{ref}'")
107-
raise AttributeError(f"Failed to checkout ref '{ref}': {err}") from err
108-
return destination
161+
logger.fail(f"Failed to update branch '{branch}' for repository '{repo_path}'")
162+
raise GeneralError(
163+
f"Failed to update branch '{branch}' for repository '{repo_path}'"
164+
) from err
165+
166+
167+
def _is_branch(common: Common, repo_path: Path, ref: str) -> bool:
168+
"""
169+
Check if the given ref is a branch in the Git repository.
170+
171+
:return: True if the ref is a branch, False otherwise.
172+
"""
173+
try:
174+
common.run(Command("git", "show-ref", "-q", "--verify", f"refs/heads/{ref}"), cwd=repo_path)
175+
return True
176+
except RunError:
177+
return False

tests/unit/test_git_handler.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55
import tmt
6-
from tmt.utils import Command, GeneralError, GitUrlError, RunError
6+
from tmt.utils import GeneralError, GitUrlError, RunError
77

88
from tmt_web import settings
99
from tmt_web.utils import git_handler
@@ -121,8 +121,12 @@ def test_get_git_repository_existing_checkout_error(self, mocker, logger):
121121
assert path.exists()
122122

123123
# Mock checkout to fail
124-
cmd = Command("git", "checkout", "invalid-branch")
125-
mocker.patch("tmt.utils.Command.run", side_effect=RunError("Command failed", cmd, 1))
124+
def side_effect(cmd, *args, **kwargs):
125+
if cmd._command == ["git", "checkout", "invalid-branch"]:
126+
raise RunError("Command failed", cmd, 1)
127+
return mocker.DEFAULT
128+
129+
mocker.patch("tmt.utils.Command.run", side_effect=side_effect, autospec=True)
126130

127131
# Try to get same repo with invalid ref
128132
with pytest.raises(AttributeError, match="Failed to checkout ref"):

0 commit comments

Comments
 (0)