forked from TacoSpigot/TacoSpigot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwigglePatch.py
executable file
·149 lines (132 loc) · 5.74 KB
/
wigglePatch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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)