#!/usr/bin/env python3 from subprocess import run, PIPE, CalledProcessError from argparse import ArgumentParser import os from os import path from sys import stderr, stdout import re from enum import Enum, unique @unique class FileStatus(Enum): UNTRACKED = '?' UNMODIFIED = ' ' MODIFIED = 'M' ADDED = 'A' DELETED = 'D' RENAMED = 'R' COPIED = 'C' UNMERGED = 'U' IGNORED = '!' class GitRepository: def __init__(self, directory): if not path.isdir(directory): if not path.exists(directory): raise ValueError("Repository doesn't exist:", directory) else: raise ValueError("Repository isn't a valid directory:", directory) elif not path.isdir(path.join(directory, ".git")): raise ValueError("Directory isn't a git repository:", directory) self.directory = directory def status(self): status_lines = run( ["git", "status", "--porcelain"], check=True, stdout=PIPE, universal_newlines=True, cwd=self.directory ).stdout status = dict() for line in status_lines.splitlines(): old_status = FileStatus(line[0]) new_status = FileStatus(line[1]) file_name = line[3:] status[file_name] = (old_status, new_status) return status def is_clean(self): try: return len(self.status()) == 0 except CalledProcessError: return False def is_automatically_merging(self): return path.exists(path.join(self.directory, ".git", "rebase-apply", "applying")) def wiggle_patch(self, patch): assert self.is_clean() # By default, wiggle won't create files the patch needs, and just fails for created_file in patch.created_files: # mkdir -p $(dirname created_file) os.makedirs(path.join(self.directory, path.dirname(created_file)), exist_ok=True) # touch created_file with open(path.join(self.directory, created_file), 'a'): pass result = run(["wiggle", "-rp", path.relpath(patch.file, start=self.directory)], stderr=stderr, cwd=self.directory) for file_name, (old_status, new_status) in self.status().items(): if new_status == FileStatus.UNTRACKED and old_status == FileStatus.UNTRACKED \ and file_name.endswith(".porig"): # Remove wiggle's automatically created backup files # They're completely unessicary since the entire repo is version-controlled os.remove(path.join(self.directory, file_name)) if result.returncode == 1: return False # There were unresolved conflicts else: # Check for an unexpected error # Since conflicts were already checked for, this will only raise for unexpected errors result.check_returncode() return True # Successfully wiggled def __str__(self): return path.basename(self.directory) class PatchFile: def __init__(self, file): if not path.isfile(file): if not path.exists(file): raise ValueError("Patch file doesn't exist:", file) else: raise ValueError("Patch isn't a file:", file) self.file = file try: summary = run(["git", "apply", "--summary", file], check=True, stdout=PIPE, universal_newlines=True).stdout except CalledProcessError: raise ValueError("Invalid patch file:", file) summary_pattern = re.compile(r"\s*(create) mode \d+ (\S+)") created_files = list() for line in summary.splitlines(): match = summary_pattern.match(line) if not match: raise NotImplementedError("Don't know how to parse summary line: {}".format(line)) (action, target_file) = match.groups() if action == "create": created_files.append(target_file) self.created_files = tuple(created_files) # Immutable copy def __str__(self): return path.basename(self.file) parser = ArgumentParser(description="Wiggle the patch into the specified git repository") parser.add_argument("repo", help="The git repository to apply the patch to", type=GitRepository) parser.add_argument("patch", help="The patch to apply to the repository", type=PatchFile) parser.add_argument("--git-am", "--am", "-a", action="store_true", help="If an automatic merge is in progress, continue it after wiggling") args = parser.parse_args() repository, patch = args.repo, args.patch if not repository.is_clean(): print(repository, "isn't a clean repo!") exit(1) was_automatically_merging = False if args.git_am and repository.is_automatically_merging(): print("Automatic merge in progress, will continue applying if successful") was_automatically_merging = True if not repository.wiggle_patch(patch): print("Unresolved conflicts found while wiggling!", file=stderr) print("Manual intervention is required to fix the conflicts!", file=stderr) exit(2) if args.git_am and was_automatically_merging: assert repository.is_automatically_merging() try: print("Adding changed files to index") run(["git", "add", "."], stdout=stdout, stderr=stderr, check=True, cwd=repository.directory) print("Continuing automatic merge after successful wiggle") run(["git", "am", "--continue"], stdout=stdout, stderr=stderr, check=True, cwd=repository.directory) except CalledProcessError as e: print("Failed to continue automatic merge!", file=stderr) exit(3) else: print("Successfully Wiggled", patch, "into", repository)