cornsnake.util_git
Functions for interacting with a Git repository. It includes functions for executing Git commands, checking out branches, handling commits, and managing Git configuration settings.
1""" 2Functions for interacting with a Git repository. It includes functions for executing Git commands, checking out branches, handling commits, and managing Git configuration settings. 3 4[Documentation](http://docs.mrseanryan.cornsnake.s3-website-eu-west-1.amazonaws.com/cornsnake/util_git.html) 5""" 6 7import os 8 9from . import config 10from . import util_date 11from . import util_file 12from . import util_list 13from . import util_print 14from . import util_log 15from . import util_proc 16 17COMMIT_PARTS_SEPARTOR = "_||_" 18 19logger = util_log.getLogger(__name__) 20 21 22def _parse_result(result): 23 """ 24 Function to parse the result obtained from executing Git commands. 25 26 Args: 27 result (str): The result string to be parsed. 28 29 Returns: 30 list: A list of cleaned parts obtained by splitting the result string. 31 """ 32 parts = result.split(COMMIT_PARTS_SEPARTOR) 33 parts_cleaned = [] 34 for part in parts: 35 parts_cleaned.append(part.replace('"', "")) 36 return parts_cleaned 37 38 39def get_branches_with_commits_in_origin_not_local(repo_dir, branches): 40 """ 41 For all the given branches, find any missing commits: commits that are on origin but not this copy of the repository. 42 """ 43 if not has_git_remote_urls(repo_dir): 44 util_print.print_warning( 45 f"Cannot check for missing commits: the local repo {repo_dir} has no remote origin" 46 ) 47 return [] 48 branches_with_missing_commits = [] 49 fetch(repo_dir) 50 for branch in branches: 51 checkout_branch(branch, repo_dir) 52 result = execute_command("rev-list", [f"HEAD..origin/{branch}"], repo_dir) 53 if result.strip(): 54 branches_with_missing_commits.append(branch) 55 return branches_with_missing_commits 56 57 58def get_current_branch(local_repo_location): 59 """ 60 Function to get the current branch of the Git repository. 61 62 Args: 63 local_repo_location (str): The local repository location. 64 65 Returns: 66 str: The name of the current branch. 67 """ 68 return execute_command( 69 "rev-parse", ["--abbrev-ref", "HEAD"], local_repo_location 70 ).strip() 71 72 73def get_last_commit_id(path_to_repo_dir, file_only=None): 74 """Get the last commit ID based on the given path to the repository directory and an optional file parameter. 75 Args: 76 path_to_repo_dir (str): The path to the repository directory. 77 file_only (str): Optional parameter to specify a specific file. 78 Returns: 79 str: The last commit ID.""" 80 # git log -n 1 --pretty=format:"%H_|\_%ad_|\_%s" --date=short BUILD.plz 81 git_args = [ 82 "log", 83 "-n", 84 "1", 85 f'--pretty=format:"%H{COMMIT_PARTS_SEPARTOR}%ad{COMMIT_PARTS_SEPARTOR}%s"', 86 "--date=short", 87 ] 88 if file_only is not None: 89 git_args += [file_only] 90 result = util_proc.run_process_and_get_output( 91 config.PATH_TO_GIT, git_args, path_to_repo_dir 92 ) 93 if config.IS_VERBOSE: 94 print(" " + result) 95 parsed = _parse_result(result) 96 commit = parsed[0] 97 return commit 98 99 100def has_git_remote_urls(repo_dir): 101 """ 102 Does the given git repository directory have any remote origin URLs. 103 """ 104 result = execute_command("remote", ["-v"], repo_dir) 105 return len(result.strip()) > 0 106 107 108def is_git_repo(repo_dir): 109 """ 110 Is the given directory a git repository (contains .git folder) 111 """ 112 path_to_git_config = os.path.join(repo_dir, ".git", "config") 113 return util_file.does_file_exist(path_to_git_config) 114 115 116def list_git_remote_urls(repo_dir): 117 """ 118 List any remote origin URLs of the given git repository directory. 119 """ 120 if not has_git_remote_urls(repo_dir): 121 return [] 122 123 remote_urls = [] 124 # git remote -v 125 result = execute_command("remote", ["-v"], repo_dir) 126 lines = result.split("\n") 127 128 for line in lines: 129 if not line.strip(): 130 continue 131 # example: 132 # origin https://git.api.mendix.com/b7a238f5-07a8-4008-83a7-b98590f40969.git/ (fetch) 133 parts = line.split() 134 if len(parts) == 3: 135 remote_urls.append(parts[1]) 136 else: 137 raise RuntimeError(f"Cannot parse output of git remote: {line}") 138 139 return remote_urls 140 141 142def get_git_remote_push_url(repo_dir): 143 """ 144 Get the remote origin *push* URL of the given git repository directory. 145 """ 146 try: 147 # git remote get-url --push origin 148 return execute_command( 149 "remote", ["get-url", "--push", "origin"], repo_dir 150 ).rstrip() 151 except Exception as e: 152 util_log.log_exception(e) 153 return "" 154 155 156def execute_command(command, git_args, working_dir): 157 """Execute a Git command with specified arguments in the given working directory. 158 Args: 159 command (str): The Git command to execute. 160 git_args (list): List of arguments for the Git command. 161 working_dir (str): The working directory to execute the command in. 162 Returns: 163 str: The output of the command.""" 164 git_args_with_command = [command] + git_args 165 return util_proc.run_process_and_get_output( 166 config.PATH_TO_GIT, git_args_with_command, working_dir 167 ) 168 169 170# execute git command with too many parameters - so we chunk 171def execute_command_in_chunks(command, git_args, git_extra_args_to_chunk, working_dir): 172 """Execute a Git command with arguments provided in chunks to avoid exceeding the parameter limit. 173 Args: 174 command (str): The Git command to execute. 175 git_args (list): List of arguments for the Git command. 176 git_extra_args_to_chunk (list): List of additional arguments to chunk and execute. 177 working_dir (str): The working directory to execute the command in.""" 178 CHUNK_SIZE = 20 179 chunks = util_list.chunk(git_extra_args_to_chunk, CHUNK_SIZE) 180 for chunk in chunks: 181 execute_command(command, git_args + chunk, working_dir) 182 183 184def fetch(working_dir): 185 """Fetch latest commits from a Git repository using the specified working directory. 186 187 Args: 188 working_dir (str): The working directory of the repository.""" 189 execute_command("fetch", [], working_dir) 190 191 192def fetch_notes(working_dir): 193 """Fetch notes from a Git repository using the specified working directory. 194 Args: 195 working_dir (str): The working directory of the repository.""" 196 execute_command( 197 "fetch", ["origin", "refs/notes/*:refs/notes/*", "--force"], working_dir 198 ) 199 200 201def checkout_branch(branch, path_to_repo_dir): 202 """Check out the specified branch in the Git repository at the given path. 203 Args: 204 branch (str): The branch to check out. 205 path_to_repo_dir (str): The path to the repository directory. 206 Returns: 207 str: The result of the checkout command.""" 208 result = execute_command("checkout", [branch], path_to_repo_dir) 209 return result 210 211 212def checkout_at_start_of_date(local_repo_location, branch, start_date): 213 """Check out the branch at the start of the specified date in the local repository location. 214 Args: 215 local_repo_location (str): The local repository location. 216 branch (str): The branch to check out. 217 start_date (str): The start date for checking out the branch.""" 218 # git rev-list -n 1 --first-parent --before="2023-12-22 00:00" main 219 last_commit = execute_command( 220 "rev-list", 221 ["-n", "1", "--first-parent", f'--before="{start_date} 00:00"', branch], 222 local_repo_location, 223 ) 224 last_commit = last_commit.strip() 225 if last_commit: 226 checkout_branch(last_commit, local_repo_location, True) 227 return True 228 return False 229 230 231def checkout_head(local_repo_location, branch): 232 """Check out the HEAD of the specified branch in the local repository location. 233 Args: 234 local_repo_location (str): The local repository location. 235 branch (str): The branch to check out.""" 236 checkout_branch(branch, local_repo_location) 237 238 239def check_has_no_changes(path_to_local_repo): 240 """Check if the local repository at the specified path has any changes. 241 Args: 242 path_to_local_repo (str): The path to the local repository directory.""" 243 # should return empty, if no changes 244 result = execute_command("status", ["--porcelain"], path_to_local_repo) 245 if len(result) > 0: 246 message = f"Local repository at {path_to_local_repo} has changes. Please ensure the local repository is clean and has no outstanding changes." 247 raise RuntimeError(message) 248 249 250def _prepare_local_clone(path_to_repo_dir, temp_git_fixer_dir, is_mirror): 251 """Prepare a local clone of the repository with the specified parameters. 252 Args: 253 path_to_repo_dir (str): The path to the repository directory. 254 temp_git_fixer_dir (str): The temporary directory for fixing Git issues. 255 is_mirror (bool): A flag indicating whether the clone is a mirror. 256 Returns: 257 str: The path to the prepared local clone directory.""" 258 local_clone_dir = os.path.join( 259 temp_git_fixer_dir, "lm" 260 ) # lm = local_mirror (keeping path short) 261 util_file.ensure_dir_exists(local_clone_dir) 262 args = [path_to_repo_dir] 263 if is_mirror: 264 args = ["--mirror"] + args 265 execute_command("clone", args, local_clone_dir) 266 267 local_repo_name = util_file.get_last_part_of_path(path_to_repo_dir) 268 if is_mirror: 269 local_repo_name += ".git" 270 else: 271 if local_repo_name.endswith(".git"): 272 local_repo_name = local_repo_name[:-4] 273 return os.path.join(local_clone_dir, local_repo_name) 274 275 276def prepare_local_full_clone(path_to_repo_dir, temp_git_fixer_dir): 277 """Prepare a full local clone of the repository using the specified paths. 278 Args: 279 path_to_repo_dir (str): The path to the repository directory. 280 temp_git_fixer_dir (str): The temporary directory for fixing Git issues. 281 Returns: 282 str: The path to the prepared full local clone directory.""" 283 return _prepare_local_clone(path_to_repo_dir, temp_git_fixer_dir, False) 284 285 286def prepare_local_mirror_clone(path_to_repo_dir, temp_git_fixer_dir): 287 """Prepare a mirror local clone of the repository using the specified paths. 288 Args: 289 path_to_repo_dir (str): The path to the repository directory. 290 temp_git_fixer_dir (str): The temporary directory for fixing Git issues. 291 Returns: 292 str: The path to the prepared mirror local clone directory.""" 293 return _prepare_local_clone(path_to_repo_dir, temp_git_fixer_dir, True) 294 295 296def gc_expire_reflog(repo_dir): 297 """Expire the reflog in the repository to aid in garbage collection. 298 Args: 299 repo_dir (str): The repository directory path.""" 300 # Expires the reflog, which helps a following 'gc prune' to remove more garbage 301 execute_command("reflog", ["expire", "--expire=now", "--all"], repo_dir) 302 303 304def gc_prune_aggressive(mirror_clone_dir): 305 """Aggressively prune the repository to remove garbage using the mirror clone directory. 306 Args: 307 mirror_clone_dir (str): The mirror clone directory path. 308 Returns: 309 str: The result of the pruning operation.""" 310 result = execute_command("gc", ["--prune=now", "--aggressive"], mirror_clone_dir) 311 return result 312 313 314def gc_prune_now(repo_dir): 315 """Prune the repository immediately to remove unnecessary objects. 316 Args: 317 repo_dir (str): The repository directory path.""" 318 execute_command("gc", ["--prune=now"], repo_dir) 319 320 321def gc_prune_safe(repo_dir): 322 """Safely prune the repository to optimize object storage. 323 Args: 324 repo_dir (str): The repository directory path.""" 325 execute_command("gc", [], repo_dir) 326 327 328def remove_origin_and_remote_branches(repo_dir): 329 """Remove the origin and remote branches from the repository. 330 Args: 331 repo_dir (str): The repository directory path.""" 332 execute_command("remote", ["remove", "origin"], repo_dir) 333 334 335def remove_tag(repo_dir, tag): 336 """Remove the specified tag from the repository. 337 Args: 338 repo_dir (str): The repository directory path. 339 tag (str): The tag to be removed.""" 340 execute_command("tag", ["-d", tag], repo_dir) 341 342 343def get_commits_after_date(repo_dir, branches, start_date): 344 """Get all commits after the specified date in the given branches of the repository. 345 Args: 346 repo_dir (str): The repository directory path. 347 branches (list): List of branches to check commits from. 348 start_date (str): The start date for filtering commits. 349 Returns: 350 list: A list of commits after the specified date.""" 351 commits = [] 352 for branch in branches: 353 checkout_branch(branch, repo_dir) 354 355 # git log --pretty=format:"%H" --after="2023-01-31" 356 day_before_cutoff = util_date.add_day_to_date(start_date, -1) 357 result = execute_command( 358 "log", ['--pretty=format:"%H"', f'--after="{day_before_cutoff}"'], repo_dir 359 ) 360 commits += result.split("\n") 361 cleaned_commits = [] 362 for commit in commits: 363 cleaned = commit.replace('"', "").strip() 364 cleaned_commits.append(cleaned) 365 return cleaned_commits 366 367 368def get_git_text_editor_or_none(local_repo_location): 369 """Get the default Git text editor or return None if not found. 370 Args: 371 local_repo_location (str): The local repository location. 372 Returns: 373 tuple: A tuple containing the path to the text editor program and its arguments, or (None, None) if not found.""" 374 path_to_text_editor = get_config("core.editor", local_repo_location) 375 if path_to_text_editor is None or len(path_to_text_editor) == 0: 376 return (None, None) 377 error_message = f"Cannot parse default git text editor ('core.editor' = '{path_to_text_editor}')" 378 # Unfortunately, value can be like '"program" command'. To be able to execute 'program', we need to split that out: 379 program = path_to_text_editor 380 args = [] 381 try: 382 if '"' in path_to_text_editor: 383 if path_to_text_editor.count('"') == 2: 384 parts = path_to_text_editor.split( 385 '"' 386 ) # '"path to exe" cmd' -> ['', 'path to exe', ' cmd'] 387 parts = util_list.strip_strings(parts) 388 program = parts[1] 389 args = parts[2:] 390 except Exception as e: 391 util_log.log_exception(e) 392 # Intentionally NOT passing exception onwards 393 if os.path.exists(program): 394 return (program, args) 395 util_print.print_custom(error_message) 396 return (None, None) 397 398 399def get_all_tags(path_to_local_repo): 400 """Get all tags from the specified local repository. 401 Args: 402 path_to_local_repo (str): The path to the local repository directory. 403 Returns: 404 list: A list of all tags in the repository.""" 405 tags = [] 406 result = execute_command("tag", [], path_to_local_repo) 407 raw_tags = result.split("\n") 408 for raw_tag in raw_tags: 409 raw_tag = raw_tag.strip() 410 if raw_tag: 411 tags.append(raw_tag) 412 return tags 413 414 415def delete_all_tags(path_to_local_repo): 416 """Delete all tags from the specified local repository. 417 Args: 418 path_to_local_repo (str): The path to the local repository directory.""" 419 tags = get_all_tags(path_to_local_repo) 420 execute_command_in_chunks( 421 "push", ["--delete", "origin", "--quiet"], tags, path_to_local_repo 422 ) 423 424 425def get_all_origin_branches(path_to_local_repo): 426 """Get all origin branches from the specified local repository. 427 Args: 428 path_to_local_repo (str): The path to the local repository directory. 429 Returns: 430 list: A list of all origin branches in the repository.""" 431 branches = [] 432 result = execute_command("branch", ["-r"], path_to_local_repo) 433 raw_branches = result.split("\n") 434 for raw_branch in raw_branches: 435 if "origin" not in raw_branch: 436 continue 437 if "->" in raw_branch: 438 continue 439 raw_branch = raw_branch.strip() 440 raw_branch = raw_branch.removeprefix("origin/") 441 if raw_branch: 442 branches.append(raw_branch) 443 return branches 444 445 446def delete_branches_except(branches_to_keep, path_to_local_repo): 447 """Delete branches in the local repository except for the specified ones to keep. 448 Args: 449 branches_to_keep (list): List of branches to retain. 450 path_to_local_repo (str): The path to the local repository directory.""" 451 # original bash: 452 # git branch -r | grep -Eo 'origin/.*' | grep -v 'origin/main$' | sed 's/origin\///' | xargs git push origin -v --delete 453 # OR this one seems more reliable: 454 # git branch -r | grep -Eo 'origin/.*' | grep -v 'origin/main$' | sed 's/origin\///' | xargs git push -d origin 455 branches = get_all_origin_branches(path_to_local_repo) 456 branches_to_delete = util_list.except_for(branches, branches_to_keep) 457 458 execute_command_in_chunks( 459 "push", ["-d", "origin"], branches_to_delete, path_to_local_repo 460 ) 461 462 463def get_config(key, path_to_local_repo): 464 """Get the Git configuration setting value for the specified key in the local repository. 465 Args: 466 key (str): The configuration key to retrieve. 467 path_to_local_repo (str): The path to the local repository directory. 468 Returns: 469 str: The value of the configuration setting for the key.""" 470 result = "" 471 try: 472 result = execute_command("config", ["--get", key], path_to_local_repo, False) 473 except RuntimeError as re: 474 # If the config key is missing, then git returns exit code 1 and empty stderr 475 if len(re.args) == 2 and re.args[1] == 1: 476 if config.IS_VERBOSE: 477 util_print.print_custom( 478 f"[ok] git config has no value for setting '{key}'" 479 ) 480 return "" 481 raise 482 return result 483 484 485def log_git_config(path_to_local_repo): 486 """Log interesting Git configuration settings that can impact text file handling. 487 Args: 488 path_to_local_repo (str): The path to the local repository directory.""" 489 interesting_settings = [ 490 "i18n.filesencoding", 491 "core.ignorecase", # Probably best set to false. 492 "core.autocrlf", # Caused encoding exception on validation (which we handle now). Probably best set to false - line-endings. 493 "diff.astextplain.textconv", 494 ] 495 496 logger.info("git config - text related settings") 497 for setting in interesting_settings: 498 value = get_config(setting, path_to_local_repo) 499 if len(value) > 0: 500 logger.info(f" {setting}={value}") 501 else: 502 logger.info(f" {setting} is not set [ok]") 503 504 505settings_best_set_to_false = [ 506 "core.ignorecase", # Probably best set to false. 507 "core.autocrlf", # Caused encoding exception on validation (which we handle now). Probably best set to false - line-endings. 508] 509 510 511def is_git_ignored(directory, filename): 512 """Check if a file is ignored by Git in the specified directory. 513 Args: 514 directory (str): The directory containing the file. 515 filename (str): The name of the file to check. 516 Returns: 517 bool: True if the file is ignored, False otherwise.""" 518 try: 519 execute_command("check-ignore", [filename], directory, False) 520 except RuntimeError as re: 521 if len(re.args) == 2: 522 exit_code = re.args[1] 523 if exit_code == 0: 524 # 0 - One or more of the provided paths is ignored. 525 return True 526 elif exit_code == 1: 527 # 1 - None of the provided paths are ignored. 528 return False 529 # 128 - A fatal error was encountered. 530 # any other exit code 531 raise 532 # 0 - One or more of the provided paths is ignored. 533 return True
40def get_branches_with_commits_in_origin_not_local(repo_dir, branches): 41 """ 42 For all the given branches, find any missing commits: commits that are on origin but not this copy of the repository. 43 """ 44 if not has_git_remote_urls(repo_dir): 45 util_print.print_warning( 46 f"Cannot check for missing commits: the local repo {repo_dir} has no remote origin" 47 ) 48 return [] 49 branches_with_missing_commits = [] 50 fetch(repo_dir) 51 for branch in branches: 52 checkout_branch(branch, repo_dir) 53 result = execute_command("rev-list", [f"HEAD..origin/{branch}"], repo_dir) 54 if result.strip(): 55 branches_with_missing_commits.append(branch) 56 return branches_with_missing_commits
For all the given branches, find any missing commits: commits that are on origin but not this copy of the repository.
59def get_current_branch(local_repo_location): 60 """ 61 Function to get the current branch of the Git repository. 62 63 Args: 64 local_repo_location (str): The local repository location. 65 66 Returns: 67 str: The name of the current branch. 68 """ 69 return execute_command( 70 "rev-parse", ["--abbrev-ref", "HEAD"], local_repo_location 71 ).strip()
Function to get the current branch of the Git repository.
Args: local_repo_location (str): The local repository location.
Returns: str: The name of the current branch.
74def get_last_commit_id(path_to_repo_dir, file_only=None): 75 """Get the last commit ID based on the given path to the repository directory and an optional file parameter. 76 Args: 77 path_to_repo_dir (str): The path to the repository directory. 78 file_only (str): Optional parameter to specify a specific file. 79 Returns: 80 str: The last commit ID.""" 81 # git log -n 1 --pretty=format:"%H_|\_%ad_|\_%s" --date=short BUILD.plz 82 git_args = [ 83 "log", 84 "-n", 85 "1", 86 f'--pretty=format:"%H{COMMIT_PARTS_SEPARTOR}%ad{COMMIT_PARTS_SEPARTOR}%s"', 87 "--date=short", 88 ] 89 if file_only is not None: 90 git_args += [file_only] 91 result = util_proc.run_process_and_get_output( 92 config.PATH_TO_GIT, git_args, path_to_repo_dir 93 ) 94 if config.IS_VERBOSE: 95 print(" " + result) 96 parsed = _parse_result(result) 97 commit = parsed[0] 98 return commit
Get the last commit ID based on the given path to the repository directory and an optional file parameter. Args: path_to_repo_dir (str): The path to the repository directory. file_only (str): Optional parameter to specify a specific file. Returns: str: The last commit ID.
101def has_git_remote_urls(repo_dir): 102 """ 103 Does the given git repository directory have any remote origin URLs. 104 """ 105 result = execute_command("remote", ["-v"], repo_dir) 106 return len(result.strip()) > 0
Does the given git repository directory have any remote origin URLs.
109def is_git_repo(repo_dir): 110 """ 111 Is the given directory a git repository (contains .git folder) 112 """ 113 path_to_git_config = os.path.join(repo_dir, ".git", "config") 114 return util_file.does_file_exist(path_to_git_config)
Is the given directory a git repository (contains .git folder)
117def list_git_remote_urls(repo_dir): 118 """ 119 List any remote origin URLs of the given git repository directory. 120 """ 121 if not has_git_remote_urls(repo_dir): 122 return [] 123 124 remote_urls = [] 125 # git remote -v 126 result = execute_command("remote", ["-v"], repo_dir) 127 lines = result.split("\n") 128 129 for line in lines: 130 if not line.strip(): 131 continue 132 # example: 133 # origin https://git.api.mendix.com/b7a238f5-07a8-4008-83a7-b98590f40969.git/ (fetch) 134 parts = line.split() 135 if len(parts) == 3: 136 remote_urls.append(parts[1]) 137 else: 138 raise RuntimeError(f"Cannot parse output of git remote: {line}") 139 140 return remote_urls
List any remote origin URLs of the given git repository directory.
143def get_git_remote_push_url(repo_dir): 144 """ 145 Get the remote origin *push* URL of the given git repository directory. 146 """ 147 try: 148 # git remote get-url --push origin 149 return execute_command( 150 "remote", ["get-url", "--push", "origin"], repo_dir 151 ).rstrip() 152 except Exception as e: 153 util_log.log_exception(e) 154 return ""
Get the remote origin push URL of the given git repository directory.
157def execute_command(command, git_args, working_dir): 158 """Execute a Git command with specified arguments in the given working directory. 159 Args: 160 command (str): The Git command to execute. 161 git_args (list): List of arguments for the Git command. 162 working_dir (str): The working directory to execute the command in. 163 Returns: 164 str: The output of the command.""" 165 git_args_with_command = [command] + git_args 166 return util_proc.run_process_and_get_output( 167 config.PATH_TO_GIT, git_args_with_command, working_dir 168 )
Execute a Git command with specified arguments in the given working directory. Args: command (str): The Git command to execute. git_args (list): List of arguments for the Git command. working_dir (str): The working directory to execute the command in. Returns: str: The output of the command.
172def execute_command_in_chunks(command, git_args, git_extra_args_to_chunk, working_dir): 173 """Execute a Git command with arguments provided in chunks to avoid exceeding the parameter limit. 174 Args: 175 command (str): The Git command to execute. 176 git_args (list): List of arguments for the Git command. 177 git_extra_args_to_chunk (list): List of additional arguments to chunk and execute. 178 working_dir (str): The working directory to execute the command in.""" 179 CHUNK_SIZE = 20 180 chunks = util_list.chunk(git_extra_args_to_chunk, CHUNK_SIZE) 181 for chunk in chunks: 182 execute_command(command, git_args + chunk, working_dir)
Execute a Git command with arguments provided in chunks to avoid exceeding the parameter limit. Args: command (str): The Git command to execute. git_args (list): List of arguments for the Git command. git_extra_args_to_chunk (list): List of additional arguments to chunk and execute. working_dir (str): The working directory to execute the command in.
185def fetch(working_dir): 186 """Fetch latest commits from a Git repository using the specified working directory. 187 188 Args: 189 working_dir (str): The working directory of the repository.""" 190 execute_command("fetch", [], working_dir)
Fetch latest commits from a Git repository using the specified working directory.
Args: working_dir (str): The working directory of the repository.
193def fetch_notes(working_dir): 194 """Fetch notes from a Git repository using the specified working directory. 195 Args: 196 working_dir (str): The working directory of the repository.""" 197 execute_command( 198 "fetch", ["origin", "refs/notes/*:refs/notes/*", "--force"], working_dir 199 )
Fetch notes from a Git repository using the specified working directory. Args: working_dir (str): The working directory of the repository.
202def checkout_branch(branch, path_to_repo_dir): 203 """Check out the specified branch in the Git repository at the given path. 204 Args: 205 branch (str): The branch to check out. 206 path_to_repo_dir (str): The path to the repository directory. 207 Returns: 208 str: The result of the checkout command.""" 209 result = execute_command("checkout", [branch], path_to_repo_dir) 210 return result
Check out the specified branch in the Git repository at the given path. Args: branch (str): The branch to check out. path_to_repo_dir (str): The path to the repository directory. Returns: str: The result of the checkout command.
213def checkout_at_start_of_date(local_repo_location, branch, start_date): 214 """Check out the branch at the start of the specified date in the local repository location. 215 Args: 216 local_repo_location (str): The local repository location. 217 branch (str): The branch to check out. 218 start_date (str): The start date for checking out the branch.""" 219 # git rev-list -n 1 --first-parent --before="2023-12-22 00:00" main 220 last_commit = execute_command( 221 "rev-list", 222 ["-n", "1", "--first-parent", f'--before="{start_date} 00:00"', branch], 223 local_repo_location, 224 ) 225 last_commit = last_commit.strip() 226 if last_commit: 227 checkout_branch(last_commit, local_repo_location, True) 228 return True 229 return False
Check out the branch at the start of the specified date in the local repository location. Args: local_repo_location (str): The local repository location. branch (str): The branch to check out. start_date (str): The start date for checking out the branch.
232def checkout_head(local_repo_location, branch): 233 """Check out the HEAD of the specified branch in the local repository location. 234 Args: 235 local_repo_location (str): The local repository location. 236 branch (str): The branch to check out.""" 237 checkout_branch(branch, local_repo_location)
Check out the HEAD of the specified branch in the local repository location. Args: local_repo_location (str): The local repository location. branch (str): The branch to check out.
240def check_has_no_changes(path_to_local_repo): 241 """Check if the local repository at the specified path has any changes. 242 Args: 243 path_to_local_repo (str): The path to the local repository directory.""" 244 # should return empty, if no changes 245 result = execute_command("status", ["--porcelain"], path_to_local_repo) 246 if len(result) > 0: 247 message = f"Local repository at {path_to_local_repo} has changes. Please ensure the local repository is clean and has no outstanding changes." 248 raise RuntimeError(message)
Check if the local repository at the specified path has any changes. Args: path_to_local_repo (str): The path to the local repository directory.
277def prepare_local_full_clone(path_to_repo_dir, temp_git_fixer_dir): 278 """Prepare a full local clone of the repository using the specified paths. 279 Args: 280 path_to_repo_dir (str): The path to the repository directory. 281 temp_git_fixer_dir (str): The temporary directory for fixing Git issues. 282 Returns: 283 str: The path to the prepared full local clone directory.""" 284 return _prepare_local_clone(path_to_repo_dir, temp_git_fixer_dir, False)
Prepare a full local clone of the repository using the specified paths. Args: path_to_repo_dir (str): The path to the repository directory. temp_git_fixer_dir (str): The temporary directory for fixing Git issues. Returns: str: The path to the prepared full local clone directory.
287def prepare_local_mirror_clone(path_to_repo_dir, temp_git_fixer_dir): 288 """Prepare a mirror local clone of the repository using the specified paths. 289 Args: 290 path_to_repo_dir (str): The path to the repository directory. 291 temp_git_fixer_dir (str): The temporary directory for fixing Git issues. 292 Returns: 293 str: The path to the prepared mirror local clone directory.""" 294 return _prepare_local_clone(path_to_repo_dir, temp_git_fixer_dir, True)
Prepare a mirror local clone of the repository using the specified paths. Args: path_to_repo_dir (str): The path to the repository directory. temp_git_fixer_dir (str): The temporary directory for fixing Git issues. Returns: str: The path to the prepared mirror local clone directory.
297def gc_expire_reflog(repo_dir): 298 """Expire the reflog in the repository to aid in garbage collection. 299 Args: 300 repo_dir (str): The repository directory path.""" 301 # Expires the reflog, which helps a following 'gc prune' to remove more garbage 302 execute_command("reflog", ["expire", "--expire=now", "--all"], repo_dir)
Expire the reflog in the repository to aid in garbage collection. Args: repo_dir (str): The repository directory path.
305def gc_prune_aggressive(mirror_clone_dir): 306 """Aggressively prune the repository to remove garbage using the mirror clone directory. 307 Args: 308 mirror_clone_dir (str): The mirror clone directory path. 309 Returns: 310 str: The result of the pruning operation.""" 311 result = execute_command("gc", ["--prune=now", "--aggressive"], mirror_clone_dir) 312 return result
Aggressively prune the repository to remove garbage using the mirror clone directory. Args: mirror_clone_dir (str): The mirror clone directory path. Returns: str: The result of the pruning operation.
315def gc_prune_now(repo_dir): 316 """Prune the repository immediately to remove unnecessary objects. 317 Args: 318 repo_dir (str): The repository directory path.""" 319 execute_command("gc", ["--prune=now"], repo_dir)
Prune the repository immediately to remove unnecessary objects. Args: repo_dir (str): The repository directory path.
322def gc_prune_safe(repo_dir): 323 """Safely prune the repository to optimize object storage. 324 Args: 325 repo_dir (str): The repository directory path.""" 326 execute_command("gc", [], repo_dir)
Safely prune the repository to optimize object storage. Args: repo_dir (str): The repository directory path.
329def remove_origin_and_remote_branches(repo_dir): 330 """Remove the origin and remote branches from the repository. 331 Args: 332 repo_dir (str): The repository directory path.""" 333 execute_command("remote", ["remove", "origin"], repo_dir)
Remove the origin and remote branches from the repository. Args: repo_dir (str): The repository directory path.
336def remove_tag(repo_dir, tag): 337 """Remove the specified tag from the repository. 338 Args: 339 repo_dir (str): The repository directory path. 340 tag (str): The tag to be removed.""" 341 execute_command("tag", ["-d", tag], repo_dir)
Remove the specified tag from the repository. Args: repo_dir (str): The repository directory path. tag (str): The tag to be removed.
344def get_commits_after_date(repo_dir, branches, start_date): 345 """Get all commits after the specified date in the given branches of the repository. 346 Args: 347 repo_dir (str): The repository directory path. 348 branches (list): List of branches to check commits from. 349 start_date (str): The start date for filtering commits. 350 Returns: 351 list: A list of commits after the specified date.""" 352 commits = [] 353 for branch in branches: 354 checkout_branch(branch, repo_dir) 355 356 # git log --pretty=format:"%H" --after="2023-01-31" 357 day_before_cutoff = util_date.add_day_to_date(start_date, -1) 358 result = execute_command( 359 "log", ['--pretty=format:"%H"', f'--after="{day_before_cutoff}"'], repo_dir 360 ) 361 commits += result.split("\n") 362 cleaned_commits = [] 363 for commit in commits: 364 cleaned = commit.replace('"', "").strip() 365 cleaned_commits.append(cleaned) 366 return cleaned_commits
Get all commits after the specified date in the given branches of the repository. Args: repo_dir (str): The repository directory path. branches (list): List of branches to check commits from. start_date (str): The start date for filtering commits. Returns: list: A list of commits after the specified date.
369def get_git_text_editor_or_none(local_repo_location): 370 """Get the default Git text editor or return None if not found. 371 Args: 372 local_repo_location (str): The local repository location. 373 Returns: 374 tuple: A tuple containing the path to the text editor program and its arguments, or (None, None) if not found.""" 375 path_to_text_editor = get_config("core.editor", local_repo_location) 376 if path_to_text_editor is None or len(path_to_text_editor) == 0: 377 return (None, None) 378 error_message = f"Cannot parse default git text editor ('core.editor' = '{path_to_text_editor}')" 379 # Unfortunately, value can be like '"program" command'. To be able to execute 'program', we need to split that out: 380 program = path_to_text_editor 381 args = [] 382 try: 383 if '"' in path_to_text_editor: 384 if path_to_text_editor.count('"') == 2: 385 parts = path_to_text_editor.split( 386 '"' 387 ) # '"path to exe" cmd' -> ['', 'path to exe', ' cmd'] 388 parts = util_list.strip_strings(parts) 389 program = parts[1] 390 args = parts[2:] 391 except Exception as e: 392 util_log.log_exception(e) 393 # Intentionally NOT passing exception onwards 394 if os.path.exists(program): 395 return (program, args) 396 util_print.print_custom(error_message) 397 return (None, None)
Get the default Git text editor or return None if not found. Args: local_repo_location (str): The local repository location. Returns: tuple: A tuple containing the path to the text editor program and its arguments, or (None, None) if not found.
426def get_all_origin_branches(path_to_local_repo): 427 """Get all origin branches from the specified local repository. 428 Args: 429 path_to_local_repo (str): The path to the local repository directory. 430 Returns: 431 list: A list of all origin branches in the repository.""" 432 branches = [] 433 result = execute_command("branch", ["-r"], path_to_local_repo) 434 raw_branches = result.split("\n") 435 for raw_branch in raw_branches: 436 if "origin" not in raw_branch: 437 continue 438 if "->" in raw_branch: 439 continue 440 raw_branch = raw_branch.strip() 441 raw_branch = raw_branch.removeprefix("origin/") 442 if raw_branch: 443 branches.append(raw_branch) 444 return branches
Get all origin branches from the specified local repository. Args: path_to_local_repo (str): The path to the local repository directory. Returns: list: A list of all origin branches in the repository.
447def delete_branches_except(branches_to_keep, path_to_local_repo): 448 """Delete branches in the local repository except for the specified ones to keep. 449 Args: 450 branches_to_keep (list): List of branches to retain. 451 path_to_local_repo (str): The path to the local repository directory.""" 452 # original bash: 453 # git branch -r | grep -Eo 'origin/.*' | grep -v 'origin/main$' | sed 's/origin\///' | xargs git push origin -v --delete 454 # OR this one seems more reliable: 455 # git branch -r | grep -Eo 'origin/.*' | grep -v 'origin/main$' | sed 's/origin\///' | xargs git push -d origin 456 branches = get_all_origin_branches(path_to_local_repo) 457 branches_to_delete = util_list.except_for(branches, branches_to_keep) 458 459 execute_command_in_chunks( 460 "push", ["-d", "origin"], branches_to_delete, path_to_local_repo 461 )
Delete branches in the local repository except for the specified ones to keep. Args: branches_to_keep (list): List of branches to retain. path_to_local_repo (str): The path to the local repository directory.
464def get_config(key, path_to_local_repo): 465 """Get the Git configuration setting value for the specified key in the local repository. 466 Args: 467 key (str): The configuration key to retrieve. 468 path_to_local_repo (str): The path to the local repository directory. 469 Returns: 470 str: The value of the configuration setting for the key.""" 471 result = "" 472 try: 473 result = execute_command("config", ["--get", key], path_to_local_repo, False) 474 except RuntimeError as re: 475 # If the config key is missing, then git returns exit code 1 and empty stderr 476 if len(re.args) == 2 and re.args[1] == 1: 477 if config.IS_VERBOSE: 478 util_print.print_custom( 479 f"[ok] git config has no value for setting '{key}'" 480 ) 481 return "" 482 raise 483 return result
Get the Git configuration setting value for the specified key in the local repository. Args: key (str): The configuration key to retrieve. path_to_local_repo (str): The path to the local repository directory. Returns: str: The value of the configuration setting for the key.
486def log_git_config(path_to_local_repo): 487 """Log interesting Git configuration settings that can impact text file handling. 488 Args: 489 path_to_local_repo (str): The path to the local repository directory.""" 490 interesting_settings = [ 491 "i18n.filesencoding", 492 "core.ignorecase", # Probably best set to false. 493 "core.autocrlf", # Caused encoding exception on validation (which we handle now). Probably best set to false - line-endings. 494 "diff.astextplain.textconv", 495 ] 496 497 logger.info("git config - text related settings") 498 for setting in interesting_settings: 499 value = get_config(setting, path_to_local_repo) 500 if len(value) > 0: 501 logger.info(f" {setting}={value}") 502 else: 503 logger.info(f" {setting} is not set [ok]")
Log interesting Git configuration settings that can impact text file handling. Args: path_to_local_repo (str): The path to the local repository directory.
512def is_git_ignored(directory, filename): 513 """Check if a file is ignored by Git in the specified directory. 514 Args: 515 directory (str): The directory containing the file. 516 filename (str): The name of the file to check. 517 Returns: 518 bool: True if the file is ignored, False otherwise.""" 519 try: 520 execute_command("check-ignore", [filename], directory, False) 521 except RuntimeError as re: 522 if len(re.args) == 2: 523 exit_code = re.args[1] 524 if exit_code == 0: 525 # 0 - One or more of the provided paths is ignored. 526 return True 527 elif exit_code == 1: 528 # 1 - None of the provided paths are ignored. 529 return False 530 # 128 - A fatal error was encountered. 531 # any other exit code 532 raise 533 # 0 - One or more of the provided paths is ignored. 534 return True
Check if a file is ignored by Git in the specified directory. Args: directory (str): The directory containing the file. filename (str): The name of the file to check. Returns: bool: True if the file is ignored, False otherwise.