| #!/usr/bin/env python3 | |
| # Copyright (C) 2025`` The Android Open Source Project | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import subprocess | |
| import argparse | |
| import sys | |
| import os | |
| import tempfile | |
| import shutil # For shutil.which | |
| # --- Configuration --- | |
| GITHUB_REMOTE = "origin" | |
| INTERNAL_REMOTE = "goog" | |
| GITHUB_MAIN_BRANCH_NAME = "main" # e.g., main, master | |
| INTERNAL_MAIN_BRANCH_NAME = "main" | |
| BRANCH_PREFIX = "dev/" # UPDATED PREFIX | |
| GIT_NOTES_REF = "refs/notes/commits" | |
| class Colors: | |
| HEADER = '\033[95m' | |
| OKBLUE = '\033[94m' | |
| OKCYAN = '\033[96m' | |
| OKGREEN = '\033[92m' | |
| WARNING = '\033[93m' | |
| FAIL = '\033[91m' | |
| ENDC = '\033[0m' | |
| BOLD = '\033[1m' | |
| UNDERLINE = '\033[4m' | |
| def print_color(color_code, message): | |
| print(f"{color_code}{message}{Colors.ENDC}") | |
| def run_command(command_list, | |
| capture_output=False, | |
| text=True, | |
| dry_run=False, | |
| is_modifying_command=False, | |
| check_exit_code=True, | |
| cwd=None, | |
| extra_env=None, | |
| ignore_error=False): | |
| command_str = ' '.join(command_list) | |
| effective_env = os.environ.copy() | |
| if extra_env: | |
| effective_env.update(extra_env) | |
| if dry_run and is_modifying_command: | |
| print_color(Colors.OKCYAN, f"DRY-RUN: Would execute: {command_str}") | |
| return subprocess.CompletedProcess(command_list, 0, stdout="", stderr="") | |
| if dry_run: | |
| print_color(Colors.OKCYAN, f"DRY-RUN (info): Executing: {command_str}") | |
| else: | |
| print_color(Colors.OKBLUE, f"Running: {command_str}") | |
| try: | |
| process = subprocess.run( | |
| command_list, | |
| capture_output=capture_output, | |
| text=text, | |
| check=check_exit_code and | |
| not ignore_error, # only check if not ignoring errors | |
| cwd=cwd, | |
| env=effective_env) | |
| if ignore_error and process.returncode != 0: | |
| print_color( | |
| Colors.WARNING, | |
| f"Command returned non-zero exit code {process.returncode}, but error is ignored." | |
| ) | |
| return process | |
| except subprocess.CalledProcessError as e: | |
| print_color(Colors.FAIL, f"ERROR running command: {command_str}") | |
| print_color(Colors.FAIL, f" Return code: {e.returncode}") | |
| if e.stdout: | |
| print_color(Colors.FAIL, f" Stdout: {e.stdout.strip()}") | |
| if e.stderr: | |
| print_color(Colors.FAIL, f" Stderr: {e.stderr.strip()}") | |
| raise | |
| except FileNotFoundError: | |
| print_color( | |
| Colors.FAIL, | |
| f"ERROR: Command '{command_list[0]}' not found. Is it installed and in PATH?" | |
| ) | |
| raise | |
| def check_command_exists(command_name): | |
| if not shutil.which(command_name): | |
| print_color( | |
| Colors.FAIL, | |
| f"Required command '{command_name}' not found. Please install it and ensure it's in your PATH." | |
| ) | |
| sys.exit(1) | |
| def get_original_branch(): | |
| try: | |
| proc = run_command(["git", "symbolic-ref", "--short", "HEAD"], | |
| capture_output=True) | |
| return proc.stdout.strip() | |
| except subprocess.CalledProcessError: # Detached HEAD | |
| proc = run_command(["git", "rev-parse", "--short", "HEAD"], | |
| capture_output=True) | |
| return proc.stdout.strip() | |
| def pre_flight_checks(dry_run): | |
| print_color(Colors.HEADER, "--- Running Pre-flight Checks ---") | |
| check_command_exists("git") | |
| check_command_exists("gh") | |
| try: | |
| run_command(["git", "rev-parse", "--is-inside-work-tree"], | |
| capture_output=True) | |
| except subprocess.CalledProcessError: | |
| print_color(Colors.FAIL, "ERROR: Not inside a Git repository.") | |
| sys.exit(1) | |
| for remote in [GITHUB_REMOTE, INTERNAL_REMOTE]: | |
| try: | |
| run_command(["git", "remote", "get-url", remote], capture_output=True) | |
| except subprocess.CalledProcessError: | |
| print_color(Colors.FAIL, f"ERROR: Git remote '{remote}' not found.") | |
| sys.exit(1) | |
| print_color(Colors.OKGREEN, | |
| f"✓ Remotes '{GITHUB_REMOTE}' and '{INTERNAL_REMOTE}' found.") | |
| try: | |
| run_command(["gh", "auth", "status"]) # No capture, just check auth | |
| except subprocess.CalledProcessError: | |
| print_color( | |
| Colors.FAIL, | |
| "ERROR: GitHub CLI ('gh') not authenticated. Please run 'gh auth login'." | |
| ) | |
| sys.exit(1) | |
| print_color(Colors.OKGREEN, "✓ GitHub CLI ('gh') is authenticated.") | |
| git_status_proc = run_command(["git", "status", "--porcelain"], | |
| capture_output=True) | |
| if git_status_proc.stdout.strip(): | |
| print_color( | |
| Colors.WARNING, | |
| "Warning: Your Git working directory or staging area is not clean.") | |
| if not dry_run: | |
| if input("Proceed anyway? (y/N): ").lower() != 'y': | |
| print_color(Colors.FAIL, "Aborting due to unclean Git state.") | |
| sys.exit(1) | |
| else: | |
| print_color(Colors.OKGREEN, "✓ Git working directory is clean.") | |
| print_color(Colors.HEADER, "-------------------------------") | |
| def fetch_updates(dry_run): | |
| print_color( | |
| Colors.HEADER, | |
| f"--- Fetching updates for remotes: {GITHUB_REMOTE}, {INTERNAL_REMOTE} ---" | |
| ) | |
| try: | |
| run_command( | |
| ["git", "remote", "update", GITHUB_REMOTE, INTERNAL_REMOTE, "--prune"], | |
| dry_run=dry_run, | |
| is_modifying_command=True) # Modifying local remote-tracking branches | |
| print_color(Colors.OKGREEN, "✓ Remotes updated.") | |
| except subprocess.CalledProcessError: | |
| print_color(Colors.FAIL, "ERROR: Failed to update remotes.") | |
| sys.exit(1) | |
| print_color(Colors.HEADER, "------------------------------------") | |
| def fetch_notes(dry_run): | |
| print_color(Colors.HEADER, | |
| f"--- Fetching notes for remote {GITHUB_REMOTE} ---") | |
| try: | |
| run_command( | |
| ["git", "fetch", GITHUB_REMOTE, f"{GIT_NOTES_REF}:{GIT_NOTES_REF}"], | |
| dry_run=dry_run, | |
| is_modifying_command=True) # Modifying local remote-tracking branches | |
| print_color(Colors.OKGREEN, "✓ Remotes updated.") | |
| except subprocess.CalledProcessError: | |
| print_color(Colors.FAIL, "ERROR: Failed to fetch notes.") | |
| sys.exit(1) | |
| print_color(Colors.HEADER, "------------------------------------") | |
| def get_commit_details(commit_hash, dry_run): | |
| details_format = "%H%n%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%b" # Hash, Author Name, Author Email, Author Date, Committer Name, Committer Email, Committer Date, Subject, Body | |
| try: | |
| proc = run_command([ | |
| "git", "show", "--quiet", f"--format=format:{details_format}", | |
| commit_hash | |
| ], | |
| capture_output=True, | |
| dry_run=dry_run, | |
| is_modifying_command=False) | |
| parts = proc.stdout.strip().split( | |
| '\n', 8) # Adjusted split for 8 newlines -> 9 parts | |
| return { | |
| "hash": parts[0], | |
| "author_name": parts[1], | |
| "author_email": parts[2], | |
| "author_date": parts[3], | |
| "committer_name": parts[4], | |
| "committer_email": parts[5], | |
| "committer_date": parts[6], | |
| "subject": parts[7], # Subject is now a distinct part | |
| "body": parts[8] if len(parts) > 8 else "" # Body is the last part | |
| } | |
| except Exception as e: | |
| print_color(Colors.FAIL, | |
| f"Could not get details for commit {commit_hash}: {e}") | |
| return None | |
| def get_commits_to_port(dry_run): | |
| print_color( | |
| Colors.OKBLUE, | |
| f"Identifying commits in '{INTERNAL_REMOTE}/{INTERNAL_MAIN_BRANCH_NAME}' not noted or part of '{GITHUB_REMOTE}/{GITHUB_MAIN_BRANCH_NAME}'..." | |
| ) | |
| rev_list_cmd = [ | |
| "git", "rev-list", "--reverse", "--no-merges", "--right-only", | |
| "--cherry-pick", | |
| f"{GITHUB_REMOTE}/{GITHUB_MAIN_BRANCH_NAME}..{INTERNAL_REMOTE}/{INTERNAL_MAIN_BRANCH_NAME}" | |
| ] | |
| try: | |
| proc_rev_list = run_command( | |
| rev_list_cmd, | |
| capture_output=True, | |
| dry_run=dry_run, | |
| is_modifying_command=False) | |
| except subprocess.CalledProcessError: | |
| print_color(Colors.FAIL, "ERROR: Failed to list candidate commits.") | |
| return [] | |
| potential_commits = [ | |
| line for line in proc_rev_list.stdout.strip().split('\n') if line | |
| ] | |
| if not potential_commits: | |
| print_color(Colors.OKGREEN, "No new candidate commits found by rev-list.") | |
| return [] | |
| commits_to_port_list = [] | |
| for commit_hash in potential_commits: | |
| try: | |
| run_command(["git", "notes", "--ref", GIT_NOTES_REF, "show", commit_hash], | |
| capture_output=True, | |
| dry_run=dry_run, | |
| is_modifying_command=False, | |
| check_exit_code=True) # Check for notes | |
| print_color( | |
| Colors.WARNING, | |
| f"Commit {commit_hash} already has a note on '{GIT_NOTES_REF}', skipping." | |
| ) | |
| except subprocess.CalledProcessError: # No note found, which is what we want | |
| commits_to_port_list.append(commit_hash) | |
| return commits_to_port_list | |
| def main_loop(dry_run, original_branch): | |
| commits_to_port = get_commits_to_port(dry_run) | |
| if not commits_to_port: | |
| print_color(Colors.OKGREEN, "No commits to process.") | |
| return | |
| print_color(Colors.HEADER, | |
| f"Found {len(commits_to_port)} commit(s) to process:") | |
| for i, commit_hash in enumerate(commits_to_port): | |
| details = get_commit_details(commit_hash, | |
| dry_run) # Get details for display | |
| if details: | |
| print_color(Colors.OKCYAN, | |
| f" {i+1}. {commit_hash[:10]} - {details['subject']}") | |
| else: | |
| print_color( | |
| Colors.OKCYAN, | |
| f" {i+1}. {commit_hash[:10]} - (Could not fetch details for preview)" | |
| ) | |
| print("") | |
| last_successful_local_branch_base = f"{GITHUB_REMOTE}/{GITHUB_MAIN_BRANCH_NAME}" | |
| last_successful_github_branch_for_pr_base = GITHUB_MAIN_BRANCH_NAME | |
| processed_commit_count = 0 | |
| for internal_commit_hash in commits_to_port: | |
| print_color( | |
| Colors.HEADER, | |
| f"\n--- Processing internal commit: {internal_commit_hash} ---") | |
| details = get_commit_details(internal_commit_hash, dry_run) | |
| if not details: | |
| print_color( | |
| Colors.FAIL, | |
| f"Skipping commit {internal_commit_hash} due to inability to fetch details." | |
| ) | |
| continue | |
| print_color(Colors.OKGREEN, "Commit Details:") | |
| print(f" Hash: {details['hash']}") | |
| print(f" Author: {details['author_name']} <{details['author_email']}>") | |
| print(f" Date: {details['author_date']}") | |
| print(f" Subject: {details['subject']}") | |
| print(f" Body:\n{details['body']}\n") | |
| while True: | |
| action = input( | |
| f"Process this commit? [{Colors.OKGREEN}Y{Colors.ENDC}]es/[{Colors.WARNING}S{Colors.ENDC}]kip/[{Colors.FAIL}Q{Colors.ENDC}]uit: " | |
| ).lower() | |
| if action in ['y', 's', 'q']: | |
| break | |
| print_color(Colors.FAIL, "Invalid input. Please enter Y, S, or Q.") | |
| if action == 'q': | |
| print_color(Colors.FAIL, "Quitting script as per user request.") | |
| return | |
| if action == 's': | |
| print_color(Colors.WARNING, f"Skipping commit {internal_commit_hash}.") | |
| continue | |
| short_hash = details['hash'][:10] | |
| new_local_branch_name = f"{BRANCH_PREFIX}{short_hash}" | |
| try: | |
| run_command(["git", "rev-parse", "--verify", new_local_branch_name], | |
| capture_output=True, | |
| dry_run=False, | |
| is_modifying_command=False) | |
| print_color(Colors.WARNING, | |
| f"Local branch '{new_local_branch_name}' already exists.") | |
| if not dry_run: | |
| if input( | |
| f"Delete local branch '{new_local_branch_name}' and recreate? (y/N): " | |
| ).lower() != 'y': | |
| print_color( | |
| Colors.WARNING, | |
| f"Skipping commit {internal_commit_hash} due to existing local branch." | |
| ) | |
| continue | |
| run_command(["git", "branch", "-D", new_local_branch_name], | |
| dry_run=dry_run, | |
| is_modifying_command=True) | |
| except subprocess.CalledProcessError: | |
| pass | |
| print_color( | |
| Colors.OKBLUE, | |
| f"Creating new local branch '{new_local_branch_name}' based on '{last_successful_local_branch_base}'..." | |
| ) | |
| try: | |
| run_command([ | |
| "git", "checkout", "-b", new_local_branch_name, | |
| last_successful_local_branch_base | |
| ], | |
| dry_run=dry_run, | |
| is_modifying_command=True) | |
| except subprocess.CalledProcessError: | |
| print_color( | |
| Colors.FAIL, | |
| f"ERROR: Failed to create or checkout branch '{new_local_branch_name}'. Stopping." | |
| ) | |
| return | |
| print_color( | |
| Colors.OKBLUE, | |
| f"Cherry-picking internal commit '{internal_commit_hash}' (using -x)..." | |
| ) | |
| try: | |
| run_command(["git", "cherry-pick", "-x", internal_commit_hash], | |
| dry_run=dry_run, | |
| is_modifying_command=True) | |
| except subprocess.CalledProcessError: | |
| if dry_run: | |
| print_color( | |
| Colors.WARNING, | |
| "DRY-RUN: Cherry-pick would have been attempted. If it failed, user would be prompted to resolve." | |
| ) | |
| else: | |
| print_color( | |
| Colors.FAIL, | |
| f"CHERRY-PICK FAILED for {internal_commit_hash} onto {new_local_branch_name}." | |
| ) | |
| print_color(Colors.WARNING, | |
| "Please resolve the conflicts in another terminal.") | |
| print_color( | |
| Colors.WARNING, | |
| "Steps: 1. Fix files. 2. 'git add <resolved_files>'. 3. 'git cherry-pick --continue'." | |
| ) | |
| print_color( | |
| Colors.WARNING, | |
| "Alternatively, to give up on this commit: 'git cherry-pick --abort'." | |
| ) | |
| while True: | |
| conflict_choice = input( | |
| "Type 'c' when resolved and continued, or 'a' to abort this cherry-pick: " | |
| ).lower() | |
| if conflict_choice == 'c': | |
| status_proc = run_command(["git", "status", "--porcelain"], | |
| capture_output=True, | |
| dry_run=False, | |
| is_modifying_command=False) | |
| if not ("U " in status_proc.stdout or "AA " in status_proc.stdout or | |
| "AM" in status_proc.stdout or | |
| "AU " in status_proc.stdout): # Check for unmerged paths | |
| am_path = os.path.join( | |
| run_command(["git", "rev-parse", "--git-dir"], | |
| capture_output=True).stdout.strip(), | |
| "CHERRY_PICK_HEAD") | |
| if not os.path.exists( | |
| am_path): # Check if CHERRY_PICK_HEAD is gone | |
| print_color( | |
| Colors.OKGREEN, | |
| "✓ Cherry-pick conflicts assumed resolved and continued.") | |
| break | |
| print_color( | |
| Colors.FAIL, | |
| "It seems the cherry-pick is still in progress or not properly continued." | |
| ) | |
| print_color( | |
| Colors.FAIL, | |
| "Please ensure 'git cherry-pick --continue' was successful (no conflicts remaining)." | |
| ) | |
| elif conflict_choice == 'a': | |
| print_color(Colors.WARNING, | |
| f"Aborting cherry-pick for {internal_commit_hash}...") | |
| run_command(["git", "cherry-pick", "--abort"], | |
| dry_run=False, | |
| is_modifying_command=True) | |
| print_color( | |
| Colors.WARNING, | |
| f"Cherry-pick aborted. Cleaning up branch '{new_local_branch_name}'." | |
| ) | |
| run_command(["git", "checkout", last_successful_local_branch_base], | |
| dry_run=False, | |
| is_modifying_command=True) | |
| run_command(["git", "branch", "-D", new_local_branch_name], | |
| dry_run=False, | |
| is_modifying_command=True) | |
| print_color( | |
| Colors.FAIL, | |
| "SCRIPT STOPPED due to aborted cherry-pick. The chain is broken." | |
| ) | |
| return | |
| else: | |
| print_color(Colors.FAIL, "Invalid choice. Type 'c' or 'a'.") | |
| print_color(Colors.OKGREEN, | |
| f"✓ Cherry-pick successful for {internal_commit_hash}.") | |
| print_color( | |
| Colors.OKBLUE, | |
| f"Pushing new branch '{new_local_branch_name}' to '{GITHUB_REMOTE}'...") | |
| try: | |
| run_command([ | |
| "git", "push", "--set-upstream", GITHUB_REMOTE, new_local_branch_name | |
| ], | |
| dry_run=dry_run, | |
| is_modifying_command=True) | |
| except subprocess.CalledProcessError: | |
| print_color( | |
| Colors.FAIL, | |
| f"ERROR: Failed to push branch '{new_local_branch_name}'. Stopping.") | |
| return | |
| print_color(Colors.OKGREEN, f"✓ Branch '{new_local_branch_name}' pushed.") | |
| pr_title = f"{details['subject']}" | |
| pr_body_content = f"""Port of internal commit. | |
| **Original commit message:** | |
| Subject: {details['subject']} | |
| {details['body']} | |
| --- | |
| Original internal commit hash: {details['hash']} | |
| Cherry-picked with `-x`. | |
| This PR is part of a stack. Base branch: {last_successful_github_branch_for_pr_base} | |
| """ | |
| print_color(Colors.OKBLUE, "Creating Pull Request on GitHub...") | |
| print(f" Title: {pr_title}") | |
| print(f" Base: {last_successful_github_branch_for_pr_base}") | |
| print(f" Head: {new_local_branch_name}") | |
| pr_url = "" | |
| try: | |
| existing_pr_proc = run_command([ | |
| "gh", "pr", "list", "--head", new_local_branch_name, "--json", "url", | |
| "--jq", ".[0].url" | |
| ], | |
| capture_output=True, | |
| dry_run=dry_run, | |
| is_modifying_command=False, | |
| check_exit_code=False) | |
| if existing_pr_proc.returncode == 0 and existing_pr_proc.stdout.strip(): | |
| pr_url = existing_pr_proc.stdout.strip() | |
| print_color( | |
| Colors.WARNING, | |
| f"PR already exists for head branch '{new_local_branch_name}': {pr_url}" | |
| ) | |
| else: | |
| if dry_run: | |
| print_color( | |
| Colors.OKCYAN, | |
| f"DRY-RUN: Would execute: gh pr create --base {last_successful_github_branch_for_pr_base} --head {new_local_branch_name} ..." | |
| ) | |
| pr_url = f"https://github.com/example/repo/pull/DRY_RUN_PR_NUM_FOR_{short_hash}" | |
| else: | |
| with tempfile.NamedTemporaryFile( | |
| mode="w", delete=False, prefix="pr_body_", | |
| suffix=".md") as tmp_body_file: | |
| tmp_body_file.write(pr_body_content) | |
| tmp_body_file_path = tmp_body_file.name | |
| try: | |
| pr_create_proc = run_command([ | |
| "gh", "pr", "create", "--base", | |
| last_successful_github_branch_for_pr_base, "--head", | |
| new_local_branch_name, "--title", pr_title, "--body-file", | |
| tmp_body_file_path | |
| ], | |
| capture_output=True, | |
| dry_run=dry_run, | |
| is_modifying_command=True) | |
| pr_url = pr_create_proc.stdout.strip() | |
| finally: | |
| os.remove(tmp_body_file_path) | |
| except subprocess.CalledProcessError as e: | |
| print_color( | |
| Colors.FAIL, | |
| f"ERROR: Failed to create Pull Request using 'gh'. Gh output:\n{e.stderr or e.stdout}" | |
| ) | |
| if not dry_run: | |
| print_color(Colors.FAIL, "Stopping.") | |
| return | |
| print_color(Colors.OKGREEN, f"✓ Pull Request created/verified: {pr_url}") | |
| print_color( | |
| Colors.OKBLUE, | |
| f"Adding git note for internal commit '{internal_commit_hash}'...") | |
| note_message = f"GitHub PR: {pr_url} (Branch: {new_local_branch_name})" | |
| try: | |
| run_command([ | |
| "git", "notes", "--ref", GIT_NOTES_REF, "add", "-f", "-m", | |
| note_message, internal_commit_hash | |
| ], | |
| dry_run=dry_run, | |
| is_modifying_command=True) | |
| print_color(Colors.OKGREEN, f"✓ Note added to {internal_commit_hash}.") | |
| except subprocess.CalledProcessError: | |
| print_color(Colors.FAIL, | |
| f"Error: Failed to add git note for {internal_commit_hash}.") | |
| last_successful_local_branch_base = new_local_branch_name | |
| last_successful_github_branch_for_pr_base = new_local_branch_name | |
| processed_commit_count += 1 | |
| print_color(Colors.HEADER, f"--- End processing {internal_commit_hash} ---") | |
| if processed_commit_count > 0: | |
| print_color(Colors.HEADER, "\n--- Pushing all local git notes ---") | |
| try: | |
| run_command(["git", "push", GITHUB_REMOTE, GIT_NOTES_REF], | |
| dry_run=dry_run, | |
| is_modifying_command=True) | |
| print_color(Colors.OKGREEN, "✓ Git notes pushed successfully.") | |
| except subprocess.CalledProcessError: | |
| print_color( | |
| Colors.FAIL, | |
| f"ERROR: Failed to push git notes. You might need to do this manually: git push {GITHUB_REMOTE} {GIT_NOTES_REF}" | |
| ) | |
| else: | |
| print_color( | |
| Colors.OKBLUE, | |
| "No commits were processed to create PRs, skipping notes push.") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Automate creation of stacked GitHub PRs from an internal branch." | |
| ) | |
| parser.add_argument( | |
| "--dry-run", | |
| action="store_true", | |
| help="Print actions without executing them (read-only commands may still run for info)." | |
| ) | |
| args = parser.parse_args() | |
| original_branch = get_original_branch() | |
| print_color(Colors.OKBLUE, f"Original branch/commit: {original_branch}") | |
| try: | |
| pre_flight_checks(args.dry_run) | |
| if not args.dry_run: | |
| fetch_updates(args.dry_run) | |
| else: | |
| print_color(Colors.OKCYAN, "DRY-RUN: Would fetch updates from remotes.") | |
| fetch_notes(args.dry_run) | |
| main_loop(args.dry_run, original_branch) | |
| except (subprocess.CalledProcessError, FileNotFoundError) as e: | |
| print_color(Colors.FAIL, f"A critical error occurred. Exiting script.") | |
| except Exception as e: | |
| print_color(Colors.FAIL, f"An unexpected error occurred: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| finally: | |
| print_color( | |
| Colors.OKBLUE, | |
| f"\nAttempting to return to original branch/commit: {original_branch}..." | |
| ) | |
| try: | |
| current_branch_after_script = get_original_branch() | |
| if current_branch_after_script != original_branch: | |
| run_command(["git", "checkout", original_branch], | |
| dry_run=False, | |
| is_modifying_command=True, | |
| ignore_error=True) | |
| print_color(Colors.OKGREEN, f"✓ Switched back to {original_branch}.") | |
| else: | |
| print_color(Colors.OKGREEN, f"✓ Already on {original_branch}.") | |
| except Exception as e: | |
| print_color( | |
| Colors.WARNING, | |
| f"Could not switch back to {original_branch}: {e}. Current branch: {get_original_branch()}" | |
| ) | |
| print_color(Colors.HEADER, "Script finished.") | |
| if __name__ == "__main__": | |
| main() |