mirror of
https://github.com/yogstation13/Yogstation.git
synced 2025-02-26 09:04:50 +00:00
Merge pull request #37976 from AutomaticFrenzy/patch/dmi-merger
Add a Python script to fix DMI conflicts
This commit is contained in:
committed by
yogstation13-bot
parent
39974df42a
commit
a7fa4ee38e
12
.gitattributes
vendored
12
.gitattributes
vendored
@@ -1,12 +1,6 @@
|
|||||||
# dmm map merger hook
|
# merger hooks, run tools/hooks/install.bat or install.sh to set up
|
||||||
# needs additional setup, see tools/mapmerge/install.txt
|
*.dmm merge=dmm
|
||||||
*.dmm merge=merge-dmm
|
*.dmi merge=dmi
|
||||||
|
|
||||||
# dmi icon merger hook
|
|
||||||
# needs additional setup, see tools/dmitool/merging.txt
|
|
||||||
*.dmi merge=merge-dmi
|
|
||||||
|
|
||||||
# force changelog merging to use union
|
# force changelog merging to use union
|
||||||
html/changelog.html merge=union
|
html/changelog.html merge=union
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ automatically by GUI tools like TortoiseGit and GitHub for Windows, but
|
|||||||
## Current Hooks
|
## Current Hooks
|
||||||
|
|
||||||
* **Pre-commit**: Runs [mapmerge2] on changed maps, if any.
|
* **Pre-commit**: Runs [mapmerge2] on changed maps, if any.
|
||||||
|
* **DMI merger**: Attempts to [fix icon conflicts] when performing a git merge.
|
||||||
|
If it succeeds, the file is marked merged. If it fails, it logs what states
|
||||||
|
are still in conflict and adds them to the .dmi file, where the desired
|
||||||
|
resolution can be chosen.
|
||||||
|
|
||||||
## Adding New Hooks
|
## Adding New Hooks
|
||||||
|
|
||||||
@@ -34,3 +38,4 @@ version of Python is found.
|
|||||||
[merge drivers]: https://git-scm.com/docs/gitattributes#_performing_a_three_way_merge
|
[merge drivers]: https://git-scm.com/docs/gitattributes#_performing_a_three_way_merge
|
||||||
[Git for Windows]: https://gitforwindows.org/
|
[Git for Windows]: https://gitforwindows.org/
|
||||||
[mapmerge2]: ../mapmerge2/README.md
|
[mapmerge2]: ../mapmerge2/README.md
|
||||||
|
[fix icon conflicts]: ../mapmerge2/merge_driver_dmi.py
|
||||||
|
|||||||
2
tools/hooks/dmi.merge
Normal file
2
tools/hooks/dmi.merge
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exec tools/hooks/python.sh -m merge_driver_dmi "$@"
|
||||||
@@ -10,5 +10,7 @@ for %%f in (*.merge) do (
|
|||||||
|
|
||||||
driver = tools/hooks/%%f %%P %%O %%A %%B %%L >> ..\..\.git\config
|
driver = tools/hooks/%%f %%P %%O %%A %%B %%L >> ..\..\.git\config
|
||||||
)
|
)
|
||||||
|
echo Installing Python dependencies
|
||||||
|
python -m pip install -r ..\mapmerge2\requirements.txt
|
||||||
echo Done
|
echo Done
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
for f in *.hook; do
|
for f in *.hook; do
|
||||||
@@ -9,4 +10,6 @@ for f in *.merge; do
|
|||||||
echo Installing merge driver: ${f%.merge}
|
echo Installing merge driver: ${f%.merge}
|
||||||
git config --replace-all merge.${f%.merge}.driver "tools/hooks/$f %P %O %A %B %L"
|
git config --replace-all merge.${f%.merge}.driver "tools/hooks/$f %P %O %A %B %L"
|
||||||
done
|
done
|
||||||
|
echo Installing Python dependencies
|
||||||
|
./python.sh -m pip install -r ../mapmerge2/requirements.txt
|
||||||
echo "Done"
|
echo "Done"
|
||||||
|
|||||||
253
tools/mapmerge2/dmi.py
Normal file
253
tools/mapmerge2/dmi.py
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# Tools for working with modern DreamMaker icon files (PNGs + metadata)
|
||||||
|
|
||||||
|
import math
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.PngImagePlugin import PngInfo
|
||||||
|
|
||||||
|
DEFAULT_SIZE = 32, 32
|
||||||
|
LOOP_UNLIMITED = 0
|
||||||
|
LOOP_ONCE = 1
|
||||||
|
|
||||||
|
NORTH = 1
|
||||||
|
SOUTH = 2
|
||||||
|
EAST = 4
|
||||||
|
WEST = 8
|
||||||
|
SOUTHEAST = SOUTH|EAST
|
||||||
|
SOUTHWEST = SOUTH|WEST
|
||||||
|
NORTHEAST = NORTH|EAST
|
||||||
|
NORTHWEST = NORTH|WEST
|
||||||
|
|
||||||
|
CARDINALS = [NORTH, SOUTH, EAST, WEST]
|
||||||
|
DIR_ORDER = [SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST]
|
||||||
|
DIR_NAMES = {
|
||||||
|
'SOUTH': SOUTH,
|
||||||
|
'NORTH': NORTH,
|
||||||
|
'EAST': EAST,
|
||||||
|
'WEST': WEST,
|
||||||
|
'SOUTHEAST': SOUTHEAST,
|
||||||
|
'SOUTHWEST': SOUTHWEST,
|
||||||
|
'NORTHEAST': NORTHEAST,
|
||||||
|
'NORTHWEST': NORTHWEST,
|
||||||
|
**{str(x): x for x in DIR_ORDER},
|
||||||
|
**{x: x for x in DIR_ORDER},
|
||||||
|
'0': SOUTH,
|
||||||
|
None: SOUTH,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dmi:
|
||||||
|
version = "4.0"
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.states = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, fname):
|
||||||
|
image = Image.open(fname)
|
||||||
|
|
||||||
|
# no metadata = regular image file
|
||||||
|
if 'Description' not in image.info:
|
||||||
|
dmi = Dmi(*image.size)
|
||||||
|
state = dmi.state("")
|
||||||
|
state.frame(image)
|
||||||
|
return dmi
|
||||||
|
|
||||||
|
# read metadata
|
||||||
|
metadata = image.info['Description']
|
||||||
|
line_iter = iter(metadata.splitlines())
|
||||||
|
assert next(line_iter) == "# BEGIN DMI"
|
||||||
|
assert next(line_iter) == f"version = {cls.version}"
|
||||||
|
|
||||||
|
dmi = Dmi(*DEFAULT_SIZE)
|
||||||
|
state = None
|
||||||
|
|
||||||
|
for line in line_iter:
|
||||||
|
if line == "# END DMI":
|
||||||
|
break
|
||||||
|
key, value = line.lstrip().split(" = ")
|
||||||
|
if key == 'width':
|
||||||
|
dmi.width = int(value)
|
||||||
|
elif key == 'height':
|
||||||
|
dmi.height = int(value)
|
||||||
|
elif key == 'state':
|
||||||
|
state = dmi.state(unescape(value))
|
||||||
|
elif key == 'dirs':
|
||||||
|
state.dirs = int(value)
|
||||||
|
elif key == 'frames':
|
||||||
|
state._nframes = int(value)
|
||||||
|
elif key == 'delay':
|
||||||
|
state.delays = [parse_num(x) for x in value.split(',')]
|
||||||
|
elif key == 'loop':
|
||||||
|
state.loop = int(value)
|
||||||
|
elif key == 'rewind':
|
||||||
|
state.rewind = parse_bool(value)
|
||||||
|
elif key == 'hotspot':
|
||||||
|
x, y, frm = [int(x) for x in value.split(',')]
|
||||||
|
state.hotspot(frm - 1, x, y)
|
||||||
|
elif key == 'movement':
|
||||||
|
state.movement = parse_bool(value)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(key)
|
||||||
|
|
||||||
|
# cut image into frames
|
||||||
|
width, height = image.size
|
||||||
|
gridwidth = width // dmi.width
|
||||||
|
i = 0
|
||||||
|
for state in dmi.states:
|
||||||
|
for frame in range(state._nframes):
|
||||||
|
for dir in range(state.dirs):
|
||||||
|
px = dmi.width * (i % gridwidth)
|
||||||
|
py = dmi.height * (i // gridwidth)
|
||||||
|
im = image.crop((px, py, px + dmi.width, py + dmi.height))
|
||||||
|
assert im.size == (dmi.width, dmi.height)
|
||||||
|
state.frames.append(im)
|
||||||
|
i += 1
|
||||||
|
state._nframes = None
|
||||||
|
|
||||||
|
return dmi
|
||||||
|
|
||||||
|
def state(self, *args, **kwargs):
|
||||||
|
s = State(self, *args, **kwargs)
|
||||||
|
self.states.append(s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_state(self):
|
||||||
|
return self.states[0]
|
||||||
|
|
||||||
|
def get_state(self, name):
|
||||||
|
for state in self.states:
|
||||||
|
if state.name == name:
|
||||||
|
return state
|
||||||
|
raise KeyError(name)
|
||||||
|
return self.default_state
|
||||||
|
|
||||||
|
def _assemble_comment(self):
|
||||||
|
comment = "# BEGIN DMI\n"
|
||||||
|
comment += f"version = {self.version}\n"
|
||||||
|
comment += f"\twidth = {self.width}\n"
|
||||||
|
comment += f"\theight = {self.height}\n"
|
||||||
|
for state in self.states:
|
||||||
|
comment += f"state = {escape(state.name)}\n"
|
||||||
|
comment += f"\tdirs = {state.dirs}\n"
|
||||||
|
comment += f"\tframes = {state.framecount}\n"
|
||||||
|
if state.framecount > 1 and len(state.delays): #any(x != 1 for x in state.delays):
|
||||||
|
comment += "\tdelay = " + ",".join(map(str, state.delays)) + "\n"
|
||||||
|
if state.loop != 0:
|
||||||
|
comment += f"\tloop = {state.loop}\n"
|
||||||
|
if state.rewind:
|
||||||
|
comment += "\trewind = 1\n"
|
||||||
|
if state.movement:
|
||||||
|
comment += "\tmovement = 1\n"
|
||||||
|
if state.hotspots and any(state.hotspots):
|
||||||
|
current = None
|
||||||
|
for i, value in enumerate(state.hotspots):
|
||||||
|
if value != current:
|
||||||
|
x, y = value
|
||||||
|
comment += f"\thotspot = {x},{y},{i + 1}\n"
|
||||||
|
current = value
|
||||||
|
comment += "# END DMI"
|
||||||
|
return comment
|
||||||
|
|
||||||
|
def to_file(self, filename, *, palette=False):
|
||||||
|
# assemble comment
|
||||||
|
comment = self._assemble_comment()
|
||||||
|
|
||||||
|
# assemble spritesheet
|
||||||
|
W, H = self.width, self.height
|
||||||
|
num_frames = sum(len(state.frames) for state in self.states)
|
||||||
|
sqrt = math.ceil(math.sqrt(num_frames))
|
||||||
|
output = Image.new('RGBA', (sqrt * W, math.ceil(num_frames / sqrt) * H))
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for state in self.states:
|
||||||
|
for frame in state.frames:
|
||||||
|
output.paste(frame, ((i % sqrt) * W, (i // sqrt) * H))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# save
|
||||||
|
pnginfo = PngInfo()
|
||||||
|
pnginfo.add_text('Description', comment, zip=True)
|
||||||
|
if palette:
|
||||||
|
output = output.convert('P')
|
||||||
|
output.save(filename, 'png', optimize=True, pnginfo=pnginfo)
|
||||||
|
|
||||||
|
class State:
|
||||||
|
def __init__(self, dmi, name, *, loop=LOOP_UNLIMITED, rewind=False, movement=False, dirs=1):
|
||||||
|
self.dmi = dmi
|
||||||
|
self.name = name
|
||||||
|
self.loop = loop
|
||||||
|
self.rewind = rewind
|
||||||
|
self.movement = movement
|
||||||
|
self.dirs = dirs
|
||||||
|
|
||||||
|
self._nframes = None # used during loading only
|
||||||
|
self.frames = []
|
||||||
|
self.delays = []
|
||||||
|
self.hotspots = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def framecount(self):
|
||||||
|
if self._nframes is not None:
|
||||||
|
return self._nframes
|
||||||
|
else:
|
||||||
|
return len(self.frames) // self.dirs
|
||||||
|
|
||||||
|
def frame(self, image, *, delay=1):
|
||||||
|
assert image.size == (self.dmi.width, self.dmi.height)
|
||||||
|
self.delays.append(delay)
|
||||||
|
self.frames.append(image)
|
||||||
|
|
||||||
|
def hotspot(self, first_frame, x, y):
|
||||||
|
if self.hotspots is None:
|
||||||
|
self.hotspots = [None] * self.framecount
|
||||||
|
for i in range(first_frame, self.framecount):
|
||||||
|
self.hotspots[i] = x, y
|
||||||
|
|
||||||
|
def _frame_index(self, frame=0, dir=None):
|
||||||
|
ofs = DIR_ORDER.index(DIR_NAMES[dir])
|
||||||
|
if ofs >= self.dirs:
|
||||||
|
ofs = 0
|
||||||
|
return frame * self.dirs + ofs
|
||||||
|
|
||||||
|
def get_frame(self, *args, **kwargs):
|
||||||
|
return self.frames[self._frame_index(*args, **kwargs)]
|
||||||
|
|
||||||
|
def escape(text):
|
||||||
|
assert '\\' not in text and '"' not in text
|
||||||
|
return f'"{text}"'
|
||||||
|
|
||||||
|
def unescape(text, quote='"'):
|
||||||
|
if text == 'null':
|
||||||
|
return None
|
||||||
|
if not (text.startswith(quote) and text.endswith(quote)):
|
||||||
|
raise ValueError(text)
|
||||||
|
text = text[1:-1]
|
||||||
|
assert '\\' not in text and quote not in text
|
||||||
|
return text
|
||||||
|
|
||||||
|
def parse_num(value):
|
||||||
|
if '.' in value:
|
||||||
|
return float(value)
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
def parse_bool(value):
|
||||||
|
if value not in ('0', '1'):
|
||||||
|
raise ValueError(value)
|
||||||
|
return value == '1'
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# test: can we load every DMI in the tree
|
||||||
|
import os
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for dirpath, dirnames, filenames in os.walk('.'):
|
||||||
|
if '.git' in dirnames:
|
||||||
|
dirnames.remove('.git')
|
||||||
|
for filename in filenames:
|
||||||
|
if filename.endswith('.dmi'):
|
||||||
|
Dmi.from_file(os.path.join(dirpath, filename))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print(f"Successfully parsed {count} dmi files")
|
||||||
177
tools/mapmerge2/merge_driver_dmi.py
Normal file
177
tools/mapmerge2/merge_driver_dmi.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import dmi
|
||||||
|
|
||||||
|
def images_equal(left, right):
|
||||||
|
if left.size != right.size:
|
||||||
|
return False
|
||||||
|
w, h = left.size
|
||||||
|
left_load, right_load = left.load(), right.load()
|
||||||
|
for y in range(0, h):
|
||||||
|
for x in range(0, w):
|
||||||
|
lpixel, rpixel = left_load[x, y], right_load[x, y]
|
||||||
|
# quietly ignore changes where both pixels are fully transparent
|
||||||
|
if lpixel != rpixel and (lpixel[3] != 0 or rpixel[3] != 0):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def states_equal(left, right):
|
||||||
|
result = True
|
||||||
|
|
||||||
|
# basic properties
|
||||||
|
for attr in ('loop', 'rewind', 'movement', 'dirs', 'delays', 'hotspots', 'framecount'):
|
||||||
|
lval, rval = getattr(left, attr), getattr(right, attr)
|
||||||
|
if lval != rval:
|
||||||
|
result = False
|
||||||
|
|
||||||
|
# frames
|
||||||
|
for (left_frame, right_frame) in zip(left.frames, right.frames):
|
||||||
|
if not images_equal(left_frame, right_frame):
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def key_of(state):
|
||||||
|
return (state.name, state.movement)
|
||||||
|
|
||||||
|
def dictify(sheet):
|
||||||
|
result = {}
|
||||||
|
for state in sheet.states:
|
||||||
|
k = key_of(state)
|
||||||
|
if k in result:
|
||||||
|
print(f" duplicate {k!r}")
|
||||||
|
result[k] = state
|
||||||
|
return result
|
||||||
|
|
||||||
|
def three_way_merge(base, left, right):
|
||||||
|
base_dims = base.width, base.height
|
||||||
|
if base_dims != (left.width, left.height) or base_dims != (right.width, right.height):
|
||||||
|
print("Dimensions have changed:")
|
||||||
|
print(f" Base: {base.width} x {base.height}")
|
||||||
|
print(f" Ours: {left.width} x {left.height}")
|
||||||
|
print(f" Theirs: {right.width} x {right.height}")
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
base_states, left_states, right_states = dictify(base), dictify(left), dictify(right)
|
||||||
|
|
||||||
|
new_left = {k: v for k, v in left_states.items() if k not in base_states}
|
||||||
|
new_right = {k: v for k, v in right_states.items() if k not in base_states}
|
||||||
|
new_both = {}
|
||||||
|
conflicts = []
|
||||||
|
for key, state in list(new_left.items()):
|
||||||
|
in_right = new_right.get(key, None)
|
||||||
|
if in_right:
|
||||||
|
if states_equal(state, in_right):
|
||||||
|
# allow it
|
||||||
|
new_both[key] = state
|
||||||
|
else:
|
||||||
|
# generate conflict states
|
||||||
|
print(f" C: {state.name!r}: added differently in both!")
|
||||||
|
state.name = f"{state.name} !CONFLICT! left"
|
||||||
|
conflicts.append(state)
|
||||||
|
in_right.name = f"{state.name} !CONFLICT! right"
|
||||||
|
conflicts.append(in_right)
|
||||||
|
# don't add it a second time
|
||||||
|
del new_left[key]
|
||||||
|
del new_right[key]
|
||||||
|
|
||||||
|
final_states = []
|
||||||
|
# add states that are currently in the base
|
||||||
|
for state in base.states:
|
||||||
|
in_left = left_states.get(key_of(state), None)
|
||||||
|
in_right = right_states.get(key_of(state), None)
|
||||||
|
left_equals = in_left and states_equal(state, in_left)
|
||||||
|
right_equals = in_right and states_equal(state, in_right)
|
||||||
|
|
||||||
|
if not in_left and not in_right:
|
||||||
|
# deleted in both left and right, it's just deleted
|
||||||
|
print(f" {state.name!r}: deleted in both")
|
||||||
|
elif not in_left:
|
||||||
|
# left deletes
|
||||||
|
print(f" {state.name!r}: deleted in left")
|
||||||
|
if not right_equals:
|
||||||
|
print(f" ... but modified in right")
|
||||||
|
final_states.append(in_right)
|
||||||
|
elif not in_right:
|
||||||
|
# right deletes
|
||||||
|
print(f" {state.name!r}: deleted in right")
|
||||||
|
if not left_equals:
|
||||||
|
print(f" ... but modified in left")
|
||||||
|
final_states.append(in_left)
|
||||||
|
elif left_equals and right_equals:
|
||||||
|
# changed in neither
|
||||||
|
#print(f"Same in both: {state.name!r}")
|
||||||
|
final_states.append(state)
|
||||||
|
elif left_equals:
|
||||||
|
# changed only in right
|
||||||
|
print(f" {state.name!r}: changed in left")
|
||||||
|
final_states.append(in_right)
|
||||||
|
elif right_equals:
|
||||||
|
# changed only in left
|
||||||
|
print(f" {state.name!r}: changed in right")
|
||||||
|
final_states.append(in_left)
|
||||||
|
elif states_equal(in_left, in_right):
|
||||||
|
# changed in both, to the same thing
|
||||||
|
print(f" {state.name!r}: changed same in both")
|
||||||
|
final_states.append(in_left) # either or
|
||||||
|
else:
|
||||||
|
# changed in both
|
||||||
|
name = state.name
|
||||||
|
print(f" C: {name!r}: changed differently in both!")
|
||||||
|
state.name = f"{name} !CONFLICT! base"
|
||||||
|
conflicts.append(state)
|
||||||
|
in_left.name = f"{name} !CONFLICT! left"
|
||||||
|
conflicts.append(in_left)
|
||||||
|
in_right.name = f"{name} !CONFLICT! right"
|
||||||
|
conflicts.append(in_right)
|
||||||
|
|
||||||
|
# add states which both left and right added the same
|
||||||
|
for key, state in new_both.items():
|
||||||
|
print(f" {state.name!r}: added same in both")
|
||||||
|
final_states.append(state)
|
||||||
|
|
||||||
|
# add states that are brand-new in the left
|
||||||
|
for key, state in new_left.items():
|
||||||
|
print(f" {state.name!r}: added in left")
|
||||||
|
final_states.append(state)
|
||||||
|
|
||||||
|
# add states that are brand-new in the right
|
||||||
|
for key, state in new_right.items():
|
||||||
|
print(f" {state.name!r}: added in right")
|
||||||
|
final_states.append(state)
|
||||||
|
|
||||||
|
final_states.extend(conflicts)
|
||||||
|
merged = dmi.Dmi(base.width, base.height)
|
||||||
|
merged.states = final_states
|
||||||
|
return len(conflicts), merged
|
||||||
|
|
||||||
|
def main(path, original, left, right):
|
||||||
|
print(f"Merging icon: {path}")
|
||||||
|
|
||||||
|
icon_orig = dmi.Dmi.from_file(original)
|
||||||
|
icon_left = dmi.Dmi.from_file(left)
|
||||||
|
icon_right = dmi.Dmi.from_file(right)
|
||||||
|
|
||||||
|
trouble, merged = three_way_merge(icon_orig, icon_left, icon_right)
|
||||||
|
if merged:
|
||||||
|
merged.to_file(left)
|
||||||
|
if trouble:
|
||||||
|
print("!!! Manual merge required!")
|
||||||
|
if merged:
|
||||||
|
print(" A best-effort merge was performed. You must edit the icon and remove all")
|
||||||
|
print(" icon states marked with !CONFLICT!, leaving only the desired icon.")
|
||||||
|
else:
|
||||||
|
print(" The icon was totally unable to be merged, you must start with one version")
|
||||||
|
print(" or the other and manually resolve the conflict.")
|
||||||
|
print(" Information about which states conflicted is listed above.")
|
||||||
|
return trouble
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) != 6:
|
||||||
|
print("DMI merge driver called with wrong number of arguments")
|
||||||
|
print(" usage: merge-driver-dmi %P %O %A %B %L")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# "left" is also the file that ought to be overwritten
|
||||||
|
_, path, original, left, right, conflict_size_marker = sys.argv
|
||||||
|
exit(main(path, original, left, right))
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pygit2==0.26.0
|
pygit2==0.26.0
|
||||||
bidict==0.13.1
|
bidict==0.13.1
|
||||||
|
Pillow=5.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user