c015e7d045
For some reason, this doesn't build on my computer because of an issue with OpenSSL. Fixes #43
150 lines
5.7 KiB
Python
Executable File
150 lines
5.7 KiB
Python
Executable File
#!/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)
|