1010import re
1111from shutil import rmtree
1212
13+ from filelock import FileLock
1314from tmt import Logger
1415from tmt ._compat .pathlib import Path
1516from tmt .utils import Command , Common , GeneralError , RunError
2122ROOT_DIR = Path (__file__ ).resolve ().parents [2 ]
2223
2324
24- def create_hash (text : str ):
25+ def _create_hash (text : str ):
2526 """Create hash of the given text that is consistent across runs."""
2627 hashed_text = hashlib .new ("sha1" , usedforsecurity = False )
2728 hashed_text .update (text .encode ())
@@ -35,9 +36,7 @@ def get_unique_clone_path(url: str) -> Path:
3536 :param url: Repository URL
3637 :return: Unique path for cloning
3738 """
38- url = url .rstrip ("/" ).removesuffix (".git" )
39- clone_dir_name = create_hash (url )
40- return ROOT_DIR / settings .CLONE_DIR_PATH / clone_dir_name
39+ return ROOT_DIR / settings .CLONE_DIR_PATH / _create_hash (url .rstrip ("/" ).removesuffix (".git" ))
4140
4241
4342def clear_tmp_dir (logger : Logger ) -> None :
@@ -59,13 +58,13 @@ def clear_tmp_dir(logger: Logger) -> None:
5958 raise GeneralError (f"Failed to clear repository clone directory '{ path } '" ) from err
6059
6160
62- def clone_repository (url : str , logger : Logger ) -> Path :
61+ def clone_repository (url : str , destination : Path , logger : Logger ) -> Path :
6362 """
6463 Clone a Git repository to a unique path.
6564
6665 :param url: Repository URL
66+ :param destination: Path to where repository should be cloned
6767 :param logger: Logger instance
68- :param ref: Optional ref to checkout
6968 :return: Path to the cloned repository
7069 :raises: GitUrlError if URL is invalid
7170 :raises: GeneralError if clone fails
@@ -74,9 +73,6 @@ def clone_repository(url: str, logger: Logger) -> Path:
7473 # Validate URL
7574 url = check_git_url (url , logger )
7675
77- # Get unique path
78- destination = get_unique_clone_path (url )
79-
8076 # Clone with retry logic
8177 git_clone (url = url , destination = destination , logger = logger )
8278
@@ -96,31 +92,42 @@ def get_git_repository(url: str, logger: Logger, ref: str | None = None) -> Path
9692 :raises: AttributeError if ref doesn't exist
9793 """
9894 destination = get_unique_clone_path (url )
99- if not destination .exists ():
100- clone_repository (url , logger )
95+ with FileLock (destination .with_name (f"{ destination .name } .lock" )):
96+ if not destination .exists ():
97+ clone_repository (url , destination , logger )
98+
99+ common = Common (logger = logger )
100+
101+ try :
102+ # Fetch remote refs
103+ _fetch_remote (common , destination , logger )
104+ except GeneralError :
105+ logger .warning ("Unable to fetch remote repository. Trying to clone again." )
106+ rmtree (destination , ignore_errors = True )
107+ clone_repository (url , destination , logger )
108+
109+ # If no ref is specified, the default branch is used
110+ ref = ref or _get_default_branch (common , destination , logger )
101111
102- common = Common (logger = logger )
112+ # If the ref is a branch, ensure it's up to date
113+ if _is_branch (common , destination , ref ):
114+ _reset_branch (common , destination , ref , logger )
103115
104- # Fetch remote refs
105- _fetch_remote (common , destination , logger )
116+ _checkout (common , destination , ref , logger )
117+
118+ return destination
106119
107- # If no ref is specified, the default branch is used
108- ref = ref or _get_default_branch (common , destination , logger )
109120
121+ def _checkout (common : Common , repo_path : Path , ref : str , logger : Logger ):
122+ repo_status = _get_repository_status (common , repo_path )
110123 try :
111- common .run (Command ("git" , "checkout" , ref ), cwd = destination )
124+ common .run (Command ("git" , "checkout" , ref ), cwd = repo_path )
112125 except RunError as err :
113126 logger .fail (f"Failed to checkout ref '{ ref } ': { err .stderr } " )
127+ if repo_status :
128+ logger .fail (f"Previous repository status:\n { repo_status } " )
114129 raise AttributeError (f"Failed to checkout ref '{ ref } '" ) from err
115130
116- _ensure_no_changes (common , destination , logger )
117-
118- # If the ref is a branch, ensure it's up to date
119- if _is_branch (common , destination , ref ):
120- _update_branch (common , destination , ref , logger )
121-
122- return destination
123-
124131
125132def _get_default_branch (common : Common , repo_path : Path , logger : Logger ) -> str :
126133 """Determine the default branch of a Git repository using a remote HEAD."""
@@ -147,6 +154,7 @@ def _get_default_branch(common: Common, repo_path: Path, logger: Logger) -> str:
147154
148155def _fetch_remote (common : Common , repo_path : Path , logger : Logger ) -> None :
149156 """Fetch updates from the remote repository."""
157+ repo_status = _get_repository_status (common , repo_path )
150158 try :
151159 # Fetch all branches and tags, prune deleted ones
152160 common .run (
@@ -163,53 +171,24 @@ def _fetch_remote(common: Common, repo_path: Path, logger: Logger) -> None:
163171 )
164172 except RunError as err :
165173 logger .fail (f"Failed to fetch remote for repository '{ repo_path } ': { err .stderr } " )
174+ if repo_status :
175+ logger .fail (f"Previous repository status:\n { repo_status } " )
166176 raise GeneralError (f"Failed to fetch remote for repository '{ repo_path } '" ) from err
167177
168178
169- def _update_branch (common : Common , repo_path : Path , branch : str , logger : Logger ) -> None :
179+ def _reset_branch (common : Common , repo_path : Path , branch : str , logger : Logger ) -> None :
170180 """Ensure the specified branch is up to date with its remote counterpart."""
181+ repo_status = _get_repository_status (common , repo_path )
171182 try :
172- common .run (Command ("git" , "show-branch" , f"origin/{ branch } " ), cwd = repo_path )
173- except RunError as err :
174- logger .fail (f"Branch '{ branch } ' does not exist in repository '{ repo_path } ': { err .stderr } " )
175- raise GeneralError (f"Branch { branch } ' does not exist in repository '{ repo_path } '" ) from err
176- try :
177- # Check if the branch is already up to date
178- common .run (Command ("git" , "diff" , "--quiet" , branch , f"origin/{ branch } " ), cwd = repo_path )
179- return
180- except RunError :
181- # Branch is not up to date, proceed with update
182- try :
183- common .run (Command ("git" , "reset" , "--hard" , f"origin/{ branch } " ), cwd = repo_path )
184- except RunError as err :
185- logger .fail (
186- f"Failed to update branch '{ branch } ' for repository '{ repo_path } ': { err .stderr } "
187- )
188- raise GeneralError (
189- f"Failed to update branch '{ branch } ' for repository '{ repo_path } '"
190- ) from err
191-
192-
193- def _ensure_no_changes (common : Common , repo_path : Path , logger : Logger ) -> None :
194- """Ensure there are no changes in the repository."""
195- try :
196- output = common .run (Command ("git" , "status" , "--porcelain" ), cwd = repo_path )
197- if not output .stdout or not output .stdout .strip ():
198- return
199- logger .warning (f"Repository '{ repo_path } ' has changes:\n { output .stdout .strip ()} " )
200- except RunError as err :
201- logger .fail (f"Failed to check repository status for '{ repo_path } ': { err .stderr } " )
202- raise GeneralError (f"Failed to check repository status for '{ repo_path } '" ) from err
203-
204- try :
205- common .run (Command ("git" , "restore" , "." ), cwd = repo_path )
206- common .run (Command ("git" , "clean" , "-fdx" ), cwd = repo_path )
183+ common .run (Command ("git" , "reset" , "--hard" , f"origin/{ branch } " ), cwd = repo_path )
207184 except RunError as err :
208185 logger .fail (
209- f"Repository ' { repo_path } ' has changes that could not be reverted : { err .stderr } "
186+ f"Failed to update branch ' { branch } ' for repository ' { repo_path } ' : { err .stderr } "
210187 )
188+ if repo_status :
189+ logger .fail (f"Previous repository status:\n { repo_status } " )
211190 raise GeneralError (
212- f"Repository ' { repo_path } ' has changes that could not be reverted "
191+ f"Failed to update branch ' { branch } ' for repository ' { repo_path } ' "
213192 ) from err
214193
215194
@@ -224,3 +203,18 @@ def _is_branch(common: Common, repo_path: Path, ref: str) -> bool:
224203 return True
225204 except RunError :
226205 return False
206+
207+
208+ def _get_repository_status (common : Common , repo_path : Path ) -> str | None :
209+ """Get the current status of a Git repository."""
210+ try :
211+ log_result = common .run (Command ("git" , "log" , "-1" ), cwd = repo_path )
212+ status_result = common .run (Command ("git" , "status" ), cwd = repo_path )
213+
214+ outputs = [output .strip () for output in (log_result .stdout , status_result .stdout ) if output ]
215+ if outputs :
216+ return "\n " .join (outputs )
217+
218+ return None
219+ except RunError :
220+ return None
0 commit comments