diff --git a/wigglePatch.py b/wigglePatch.py new file mode 100755 index 0000000..d2a6bb6 --- /dev/null +++ b/wigglePatch.py @@ -0,0 +1,149 @@ +#!/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)