33
44from git import Actor
55from git import Repo
6+ from git .exc import GitCommandError
7+ from git .exc import NoSuchPathError
68from git .refs .head import Head
79from git .remote import PushInfo
810from git .remote import PushInfoList
9- from git .exc import GitCommandError
10- from git .exc import NoSuchPathError
1111from typing import Any
12+ from typing import Union
1213import json
1314import logging
1415import os
16+ import re
1517import sys
1618
1719_logger = logging .getLogger (app_config .APP_NAME )
@@ -133,7 +135,7 @@ def load_target_repos(repos: list[dict]) -> list[Repo]:
133135 return result
134136
135137
136- def identity_setup (repo : Repo , actor_username : str , actor_email : str ) -> None :
138+ def identity_setup (repo : Repo , actor_username : str , actor_email : str ) -> bool :
137139 """Set up identity configuration for a GitPython repository.
138140
139141 Args:
@@ -158,37 +160,91 @@ def identity_setup(repo: Repo, actor_username: str, actor_email: str) -> None:
158160 return False
159161
160162
161- def checkout_branch (repo : Repo , branch_name : str , from_branch : str = None ) -> bool :
162- """Checkout or create a new branch in the local repository.
163+ def configure_divergent_branches_reconciliation_method (repo : Repo , rebase : bool = False , fast_forward_only : bool = False ) -> bool :
164+ """Configure the reconciliation method for handling divergent branches in a GitPython repository.
165+
166+ This function configures the reconciliation method to handle situations where the local and remote branches have diverged during pull operations.
167+
168+ Args:
169+ repo (Repo): The GitPython repository object.
170+ rebase (bool, optional): If True, set the reconciliation method to rebase. Defaults to False.
171+ fast_forward_only (bool, optional): If True, set the reconciliation method to fast-forward only. Ignored if 'rebase' is True. Defaults to False.
172+
173+ Returns:
174+ bool: True if the reconciliation method was configured successfully, False otherwise.
175+ """
176+ try :
177+ config_writer = repo .config_writer ()
178+ if fast_forward_only :
179+ _logger .debug (
180+ "Setting reconciliation method to fast-forward only.." )
181+ config_writer .set_value ('pull' , 'ff' , 'only' ).release ()
182+ elif rebase :
183+ _logger .debug ("Setting reconciliation method to rebase.." )
184+ config_writer .set_value ('pull' , 'rebase' , 'true' ).release ()
185+ else :
186+ _logger .debug ("Setting reconciliation method to merge.." )
187+ config_writer .set_value ('pull' , 'rebase' , 'false' ).release ()
188+ del (config_writer )
189+ return True
190+ except Exception as e :
191+ _logger .error (f"An error occurred while setting up reconciliation method: {
192+ str (e ).strip ()} " )
193+ return False
194+
195+
196+ def checkout_branch (repo : Repo , branch_name : str , from_branch : str = None , remote_name : str = "origin" ) -> bool :
197+ """Checkout an existing branch or create a new branch in the local repository.
198+
199+ This function checks out an existing branch or creates a new branch in the local repository.
200+ If the specified branch already exists locally, it switches to that branch.
201+ If the branch does not exist locally but exists in the remote repository, it creates a new local branch
202+ tracking the remote branch and switches to it.
203+ If the specified branch does not exist locally or remotely, it attempts to create a new branch
204+ based on the provided base branch (or the current branch if not specified).
163205
164206 Args:
165207 repo (Repo): The GitPython Repo object representing the local repository.
166208 branch_name (str): The name of the branch to checkout or create.
167209 from_branch (str, optional): The name of the base branch to create the new branch from.
168210 If None, create the new branch from the current branch. Defaults to None.
211+ remote_name (str, optional): The name of the remote repository. Defaults to "origin".
169212
170213 Returns:
171214 bool: True if the branch was successfully checked out or created, False otherwise.
172215 """
173216 try :
217+ remote_branch_name = f"{ remote_name } /{ branch_name } "
174218 if branch_name in repo .branches :
175219 _logger .info (
176- f"'{ branch_name } ' branch already exists, checking out .." )
220+ f"'{ branch_name } ' branch already exists. Switching .." )
177221 branch = repo .branches [branch_name ]
222+ elif remote_branch_name in repo .refs :
223+ _logger .info (f"'{ remote_branch_name } ' exists." )
224+ branch = repo .create_head (branch_name , commit = remote_branch_name )
225+ _logger .info (f"Branch '{ branch_name } ' set up to track '{
226+ remote_branch_name } '" )
227+ branch .set_tracking_branch (repo .refs [remote_branch_name ])
178228 else :
179229 _logger .info (f"'{ branch_name } ' doesn't exist, creating.." )
180- from_branch = from_branch or repo .active_branch .name
230+ from_branch = from_branch or get_default_branch_name (repo = repo , remote_name = remote_name )
231+ remote_from_branch = f"{ remote_name } /{ from_branch } "
181232 if from_branch in repo .branches :
182233 branch = repo .create_head (branch_name , commit = from_branch )
183234 _logger .info (f"Created new branch '{
184- branch_name } ' based on '{ from_branch } ' branch" )
235+ branch_name } ' based on '{ from_branch } ' branch. Switching.." )
236+ elif remote_from_branch in repo .refs :
237+ branch = repo .create_head (
238+ branch_name , commit = remote_from_branch )
239+ _logger .info (f"Created new branch '{
240+ branch_name } ' based on '{ remote_from_branch } ' branch. Switching.." )
185241 else :
186242 _logger .error (
187243 f"Error: '{ from_branch } ' based on branch doesn't exist" )
188244 return False
189245
190246 branch .checkout ()
191- _logger .info (f"Checked out branch '{ branch_name } ' successfully." )
247+ _logger .info (f"Switched to branch '{ branch_name } ' successfully." )
192248 return True
193249
194250 except GitCommandError as e :
@@ -244,7 +300,7 @@ def commit_changes(repo: Repo, title: str, description: str = None, author: Acto
244300
245301
246302def push_changes (repo : Repo , remote_name : str = 'origin' , remote_branch_name : str | None = None , timeout : int | None = 180 ) -> bool :
247- """Push changes to the remote repository.
303+ """Push changes to the remote repository, pulling changes from the remote branch if it exists .
248304
249305 Args:
250306 repo (Repo): The GitPython Repo object representing the local repository.
@@ -264,7 +320,8 @@ def push_changes(repo: Repo, remote_name: str = 'origin', remote_branch_name: st
264320 This function pushes changes from the active local branch to the specified remote branch.
265321 It handles various scenarios such as existing and non-existing remotes, and provides detailed logging
266322 information during the push operation. The timeout parameter allows customization of the maximum time
267- allowed for the push operation.
323+ allowed for the push operation. Before pushing, if the specified remote branch exists, it pulls changes
324+ from that branch to ensure synchronization.
268325
269326 Example:
270327 # Push changes from the active branch to the 'main' branch of the remote repository 'origin'
@@ -276,44 +333,69 @@ def push_changes(repo: Repo, remote_name: str = 'origin', remote_branch_name: st
276333 remote = repo .remotes [remote_name ]
277334 branch_name = repo .active_branch .name
278335 remote_branch_name = remote_branch_name if remote_branch_name else branch_name
279- _logger .info (f"Pushing changes to '{
280- remote_branch_name } ' branch of remote '{ remote_name } '..." )
281- result : PushInfoList = remote .push (
282- refspec = f"{ branch_name } :{ remote_branch_name } " , progress = RemoteProgressReporter (_logger ), kill_after_timeout = timeout )
283- try :
284- assert len (result ) != 0
285- VALID_PUSH_INFO_FLAGS : list [int ] = [PushInfo .FAST_FORWARD , PushInfo .NEW_HEAD ,
286- PushInfo .UP_TO_DATE , PushInfo .FORCED_UPDATE , PushInfo .NEW_TAG ]
287- for push_info in result :
288- _logger .debug ("+------------+" )
289- _logger .debug ("| Push Info: |" )
290- _logger .debug ("+------------+" )
291- _logger .debug (f"Flag: { push_info .flags } " )
292- _logger .debug (f"Local ref: { push_info .local_ref } " )
293- _logger .debug (f"Remote Ref: { push_info .remote_ref } " )
294- _logger .debug (f"Remote ref string: {
295- push_info .remote_ref_string } " )
296- _logger .debug (f"Old Commit: { push_info .old_commit } " )
297- _logger .debug (f"Summary: { push_info .summary .strip ()} " )
298- if push_info .flags not in VALID_PUSH_INFO_FLAGS :
299- if push_info .flags == PushInfo .ERROR :
300- _logger .error (
301- f"Incomplete push error: Push contains rejected heads. Check your internet connection and run in 'debug' mode to see more details." )
302- else :
303- _logger .error (
304- "Unexpected push error, maybe the remote rejected heads. Check your internet connection and run in 'debug' mode to see more details." )
305- return False
306- except AssertionError :
307- _logger .error (f"Pushing changes to remote '{
308- remote_name } ' completely failed. Check your internet connection and run in 'debug' mode to see the remote push progress." )
309- return False
310- _logger .info (f"Changes pushed successfully to '{
311- branch_name } ' branch of remote '{ remote_name } '." )
312-
313- _logger .debug (f"Setting '{ branch_name } ' upstream branch to '{ remote_name } /{
314- remote_branch_name } '.." )
315- repo .active_branch .set_tracking_branch (
316- repo .refs [f"{ remote_name } /{ remote_branch_name } " ])
336+
337+ push_is_needed = True
338+
339+ remote_refs = remote .refs
340+ if remote_branch_name in remote_refs :
341+ _logger .debug (
342+ f"'{ remote_name } /{ remote_branch_name } ' remote branch exists." )
343+ if not has_tracking_branch (repo .active_branch ):
344+ _logger .debug (
345+ f"'{ branch_name } ' has no tracking branch. Setting.." )
346+ repo .active_branch .set_tracking_branch (
347+ repo .refs [f"{ remote_name } /{ remote_branch_name } " ])
348+ _logger .debug (f"Pulling changes from '{
349+ remote_branch_name } ' branch of remote '{ remote_name } ' to '{ branch_name } '..." )
350+ remote .pull (
351+ refspec = remote_branch_name , kill_after_timeout = timeout )
352+
353+ push_is_needed = needs_push (repo = repo , branch_name = branch_name )
354+
355+ if push_is_needed :
356+ _logger .info (f"Pushing changes to '{
357+ remote_branch_name } ' branch of remote '{ remote_name } '..." )
358+ result : PushInfoList = remote .push (
359+ refspec = f"{ branch_name } :{ remote_branch_name } " , progress = RemoteProgressReporter (_logger ), kill_after_timeout = timeout )
360+
361+ try :
362+ assert len (result ) != 0
363+ VALID_PUSH_INFO_FLAGS : list [int ] = [PushInfo .FAST_FORWARD , PushInfo .NEW_HEAD ,
364+ PushInfo .UP_TO_DATE , PushInfo .FORCED_UPDATE , PushInfo .NEW_TAG ]
365+ for push_info in result :
366+ _logger .debug ("+------------+" )
367+ _logger .debug ("| Push Info: |" )
368+ _logger .debug ("+------------+" )
369+ _logger .debug (f"Flag: { push_info .flags } " )
370+ _logger .debug (f"Local ref: { push_info .local_ref } " )
371+ _logger .debug (f"Remote Ref: { push_info .remote_ref } " )
372+ _logger .debug (f"Remote ref string: {
373+ push_info .remote_ref_string } " )
374+ _logger .debug (f"Old Commit: { push_info .old_commit } " )
375+ _logger .debug (f"Summary: { push_info .summary .strip ()} " )
376+ if push_info .flags not in VALID_PUSH_INFO_FLAGS :
377+ if push_info .flags == PushInfo .ERROR :
378+ _logger .error (
379+ "Incomplete push error: Push contains rejected heads. Check your internet connection and run in 'debug' mode to see more details." )
380+ else :
381+ _logger .error (
382+ "Unexpected push error, maybe the remote rejected heads. Check your internet connection and run in 'debug' mode to see more details." )
383+ return False
384+ except AssertionError :
385+ _logger .error (f"Pushing changes to remote '{
386+ remote_name } ' completely failed. Check your internet connection and run in 'debug' mode to see the remote push progress." )
387+ return False
388+
389+ _logger .info (f"Changes pushed successfully to '{
390+ branch_name } ' branch of remote '{ remote_name } '." )
391+ if not has_tracking_branch (repo .active_branch ):
392+ _logger .debug (f"Setting '{ branch_name } ' upstream branch to '{
393+ remote_name } /{ remote_branch_name } '.." )
394+ repo .active_branch .set_tracking_branch (
395+ repo .refs [f"{ remote_name } /{ remote_branch_name } " ])
396+
397+ else :
398+ _logger .info ("Already up-to-date. Skipping.." )
317399 return True
318400 except IndexError :
319401 _logger .error (f"Error accessing remote '{
@@ -377,3 +459,24 @@ def needs_push(repo: Repo, branch_name: str | None = None) -> bool:
377459 if tracking_branch :
378460 return any (repo .iter_commits (f"{ tracking_branch .name } ..{ branch .name } " ))
379461 return False
462+
463+
464+ def get_default_branch_name (repo : Repo , remote_name : str = "origin" ) -> Union [str , None ]:
465+ """Get the default branch name of a Git repository.
466+
467+ Args:
468+ repo (Repo): The GitPython Repo object representing the local repository.
469+ remote_name (str, optional): The name of the remote repository. Defaults to "origin".
470+
471+ Returns:
472+ Union[str, None]: The name of the default branch, or None if not found.
473+ """
474+ try :
475+ show_result = repo .git .remote ("show" , remote_name )
476+ matches = re .search (r"\s*HEAD branch:\s*(.*)" , show_result )
477+ if matches :
478+ return matches .group (1 )
479+ except Exception as e :
480+ _logger .error (f"Error while querying the default branch: { e } " )
481+
482+ return None
0 commit comments