From 222eca4dde5dc0cf1cbbf9899ff40835a4f9cbb2 Mon Sep 17 00:00:00 2001 From: JackDonwel <169630222+JackDonwel@users.noreply.github.com> Date: Fri, 30 May 2025 11:55:45 +0300 Subject: [PATCH] Update merge.py --- scripts/merge.py | 255 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 194 insertions(+), 61 deletions(-) diff --git a/scripts/merge.py b/scripts/merge.py index 0618df4a0..d539ef1e6 100755 --- a/scripts/merge.py +++ b/scripts/merge.py @@ -1,88 +1,221 @@ #!/usr/bin/env python3 -# Merge dev into main branch +""" +Enhanced Git Merge Utility +-------------------------- +Safely merges dev branch into main with comprehensive checks, +conflict handling, and synchronization features. +""" import subprocess import sys import os +import argparse +import re +from typing import Tuple, Optional -import sync - -def run_command(command): - """Run a shell command and return the output.""" - result = subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - return result.stdout.strip() +def run_command(command: list, capture_output: bool = True, check: bool = True) -> Tuple[bool, str]: + """Execute a shell command with robust error handling.""" + try: + result = subprocess.run( + command, + stdout=subprocess.PIPE if capture_output else None, + stderr=subprocess.PIPE, + text=True, + check=check + ) + output = result.stdout.strip() if capture_output else "" + return True, output + except subprocess.CalledProcessError as e: + error_msg = e.stderr.strip() if e.stderr else str(e) + return False, f"Command failed: {' '.join(command)}\nError: {error_msg}" + +def get_current_branch() -> Tuple[bool, str]: + """Get the current Git branch name.""" + return run_command(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) + +def switch_branch(target_branch: str, dry_run: bool = False) -> Tuple[bool, str]: + """Switch to a different Git branch.""" + if dry_run: + return True, f"Would switch to branch '{target_branch}'" + return run_command(['git', 'checkout', target_branch], capture_output=False) + +def check_clean_working_tree(branch: str, dry_run: bool = False) -> Tuple[bool, str]: + """Verify there are no uncommitted changes.""" + if dry_run: + return True, "Would check for uncommitted changes" + + success, output = run_command(['git', 'status', '--porcelain']) + if not success: + return False, output + + if output: + return False, f"Uncommitted changes in {branch} branch. Please commit or stash them." + return True, "Working tree is clean" + +def fetch_branch(branch: str, dry_run: bool = False) -> Tuple[bool, str]: + """Fetch the latest version of a branch from remote.""" + if dry_run: + return True, f"Would fetch origin/{branch}" + return run_command(['git', 'fetch', 'origin', branch], capture_output=False) + +def is_branch_up_to_date(local_branch: str, remote_branch: str) -> Tuple[bool, str]: + """Check if local branch matches its remote counterpart.""" + success, local_commit = run_command(['git', 'rev-parse', local_branch]) + if not success: + return False, local_commit + + success, remote_commit = run_command(['git', 'rev-parse', f'origin/{remote_branch}']) + if not success: + return False, remote_commit + + if local_commit == remote_commit: + return True, "Branch is up-to-date" + return False, f"{local_branch} not up-to-date with remote. Do 'git pull'." + +def has_changes_to_merge(source: str, target: str) -> Tuple[bool, str]: + """Check if there are changes to merge between branches.""" + success, merge_base = run_command(['git', 'merge-base', source, target]) + if not success: + return False, merge_base + + success, diff_output = run_command(['git', 'diff', merge_base, source]) + if not success: + return False, diff_output + + return (False, "No changes to merge") if not diff_output else (True, "Changes found to merge") + +def handle_merge_conflict() -> str: + """Provide detailed instructions for resolving merge conflicts.""" + instructions = [ + "\nMerge conflict detected! Resolve conflicts with:", + "1. Identify conflicted files: 'git status'", + "2. Resolve conflicts in affected files", + "3. Mark resolved files: 'git add '", + "4. Complete merge: 'git commit'", + "5. Push resolved merge: 'git push'", + "6. Rebase dev branch: 'git checkout dev && git rebase main'", + "7. Push dev branch: 'git push origin dev'" + ] + return "\n".join(instructions) def main(): + parser = argparse.ArgumentParser(description='Merge dev into main branch with safety checks') + parser.add_argument('--dry-run', action='store_true', help='Simulate actions without making changes') + parser.add_argument('--skip-sync', action='store_true', help='Skip syncing main into dev') + parser.add_argument('--auto-push', action='store_true', help='Automatically push after successful merge') + args = parser.parse_args() + try: - # Switch to dev branch if not already on it - original_branch = run_command(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) + # Store original branch for restoration + success, original_branch = get_current_branch() + if not success: + print(f"Error: {original_branch}") + sys.exit(1) + + print(f"Current branch: {original_branch}") + + # Switch to dev branch if original_branch != "dev": print("Switching to dev branch") - run_command(['git', 'checkout', 'dev']) + success, output = switch_branch("dev", args.dry_run) + if not success: + print(output) + sys.exit(1) + + # Check for uncommitted changes in dev + success, output = check_clean_working_tree("dev", args.dry_run) + if not success: + print(output) + sys.exit(1) - # Make sure the dev branch does not have uncommitted changes. - try: - run_command(['git', 'diff-index', '--quiet', 'HEAD', '--']) - except subprocess.CalledProcessError: - print("Uncommitted changes in the dev branch. Please commit or stash them before merging.") + # Ensure dev is up-to-date with remote + success, output = fetch_branch("dev", args.dry_run) + if not success: + print(output) sys.exit(1) - # Make sure the dev branch is up-to-date with the remote. - run_command(['git', 'fetch', 'origin', 'dev']) - if run_command(['git', 'rev-parse', 'dev']) != run_command(['git', 'rev-parse', 'origin/dev']): - print("dev branch not up-to-date with remote. Do 'git pull'.") + success, output = is_branch_up_to_date("dev", "dev") + if not success: + print(output) sys.exit(1) - # Call sync to verify that dev is up-to-date with main. - # This is to avoid conflicts when merging dev into main. - sync.main() + # Sync main into dev (unless skipped) + if not args.skip_sync: + print("Syncing main into dev branch...") + # This would call your sync functionality + # sync.main() # Uncomment to enable + if args.dry_run: + print("Would sync main into dev") + elif args.dry_run: + print("Would skip syncing main into dev") # Switch to main branch print("Switching to main branch") - run_command(['git', 'checkout', 'main']) - run_command(['git', 'fetch', 'origin', 'main']) - - # Proceed to merge dev into main. Detect if there are conflicts, if yes - # give instruction to resolve them. - - # Find the common ancestor of dev and main - merge_base = run_command(['git', 'merge-base', 'dev', 'main']) - - # Check if there are any changes from dev that are not in main - try: - run_command(['git', 'diff', '--quiet', merge_base, 'dev']) - print("No changes to merge from dev to main.") - except subprocess.CalledProcessError: - # Perform the actual merge - try: - run_command(['git', 'merge', '--ff-only', 'dev']) - print("Merged dev into main.") - - # Rebase dev to keep on same last commit (that merge that was just done). - run_command(['git', 'checkout', 'dev']) - run_command(['git', 'rebase', 'main']) - run_command(['git', 'push', 'origin', 'dev']) - run_command(['git', 'push', 'origin', 'main']) - except subprocess.CalledProcessError: - print("Merge failed due to conflicts.") - print("To resolve the conflicts, follow these steps:") - print("1. Identify conflicted files using 'git status'.") - print("2. Resolve manually by editing conflicted files.") - print("3. Mark conflicts as resolved using 'git add '.") - print("4. Complete merge with 'git commit' and 'push'.") + success, output = switch_branch("main", args.dry_run) + if not success: + print(output) + sys.exit(1) + + # Check if there are changes to merge + success, output = has_changes_to_merge("dev", "main") + if not success: + print(output) + sys.exit(1) + if "No changes" in output: + print(output) + else: + # Perform the merge + print("Merging dev into main...") + if args.dry_run: + print("Would run: git merge --no-ff dev") + else: + success, output = run_command(['git', 'merge', '--no-ff', 'dev'], capture_output=False) + if not success: + print("Merge conflict detected!") + print(handle_merge_conflict()) + sys.exit(1) + + # Update dev branch to match main + print("Updating dev branch to match main...") + success, output = switch_branch("dev", args.dry_run) + if not success: + print(output) sys.exit(1) - except subprocess.CalledProcessError as e: - print(f"An error occurred: {e}") + if args.dry_run: + print("Would run: git rebase main") + else: + success, output = run_command(['git', 'rebase', 'main'], capture_output=False) + if not success: + print("Rebase failed!") + print(output) + sys.exit(1) + + # Push changes if requested + if args.auto_push and not args.dry_run: + print("Pushing changes to remote...") + for branch in ["dev", "main"]: + success, output = run_command(['git', 'push', 'origin', branch], capture_output=False) + if not success: + print(f"Failed to push {branch}: {output}") + sys.exit(1) + elif args.dry_run: + print("Would push dev and main branches") + + print("\nMerge completed successfully!") + + except Exception as e: + print(f"Unexpected error: {str(e)}") sys.exit(1) finally: - # Restore to the branch the user was located before running this script - current_branch = run_command(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) - if current_branch != original_branch: - print(f"Switching back to {original_branch} branch") - run_command(['git', 'checkout', original_branch]) + # Restore original branch + if 'original_branch' in locals() and original_branch: + current_success, current_branch = get_current_branch() + if current_success and current_branch != original_branch: + print(f"\nSwitching back to {original_branch} branch") + switch_branch(original_branch, args.dry_run) if __name__ == "__main__": - main() \ No newline at end of file + main()