Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for merge_driver_ekeyword #50

Merged
merged 8 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 2 additions & 131 deletions bin/merge-driver-ekeyword
Original file line number Diff line number Diff line change
Expand Up @@ -3,138 +3,9 @@
# Copyright 2020-2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2 or later

"""
Custom git merge driver for handling conflicts in KEYWORDS assignments

See https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver
"""

import difflib
import os
import sys
import tempfile

from typing import Optional
from collections.abc import Sequence

from gentoolkit.ekeyword import ekeyword


KeywordChanges = list[tuple[Optional[list[str]], Optional[list[str]]]]


def keyword_array(keyword_line: str) -> list[str]:
# Find indices of string inside the double-quotes
i1: int = keyword_line.find('"') + 1
i2: int = keyword_line.rfind('"')

# Split into array of KEYWORDS
return keyword_line[i1:i2].split(' ')


def keyword_line_changes(old: str, new: str) -> KeywordChanges:
a: list[str] = keyword_array(old)
b: list[str] = keyword_array(new)

s = difflib.SequenceMatcher(a=a, b=b)

changes: KeywordChanges = []
for tag, i1, i2, j1, j2 in s.get_opcodes():
if tag == 'replace':
changes.append((a[i1:i2], b[j1:j2]),)
elif tag == 'delete':
changes.append((a[i1:i2], None),)
elif tag == 'insert':
changes.append((None, b[j1:j2]),)
else:
assert tag == 'equal'
return changes


def keyword_changes(ebuild1: str, ebuild2: str) -> Optional[KeywordChanges]:
with open(ebuild1) as e1, open(ebuild2) as e2:
lines1 = e1.readlines()
lines2 = e2.readlines()

diff = difflib.unified_diff(lines1, lines2, n=0)
assert next(diff) == '--- \n'
assert next(diff) == '+++ \n'

hunk: int = 0
old: str = ''
new: str = ''

for line in diff:
if line.startswith('@@ '):
if hunk > 0:
break
hunk += 1
elif line.startswith('-'):
if old or new:
break
old = line
elif line.startswith('+'):
if not old or new:
break
new = line
else:
if 'KEYWORDS=' in old and 'KEYWORDS=' in new:
return keyword_line_changes(old, new)
return None


def apply_keyword_changes(ebuild: str, pathname: str,
changes: KeywordChanges) -> int:
result: int = 0

with tempfile.TemporaryDirectory() as tmpdir:
# ekeyword will only modify files named *.ebuild, so make a symlink
ebuild_symlink: str = os.path.join(tmpdir, os.path.basename(pathname))
os.symlink(os.path.join(os.getcwd(), ebuild), ebuild_symlink)

for removals, additions in changes:
args = []
if removals:
for rem in removals:
# Drop leading '~' and '-' characters and prepend '^'
i = 1 if rem[0] in ('~', '-') else 0
args.append('^' + rem[i:])
if additions:
args.extend(additions)
args.append(ebuild_symlink)

result = ekeyword.main(args)
if result != 0:
break

return result


def main(argv: Sequence[str]) -> int:
if len(argv) != 5:
sys.exit(-1)

O = argv[1] # %O - filename of original
A = argv[2] # %A - filename of our current version
B = argv[3] # %B - filename of the other branch's version
P = argv[4] # %P - original path of the file

# Get changes to KEYWORDS= from %O to %B
if changes := keyword_changes(O, B):
# Apply %O -> %B changes to %A
result = apply_keyword_changes(A, P, changes)
sys.exit(result)
# Get changes to KEYWORDS= from %O to %A
elif changes := keyword_changes(O, A):
# Apply %O -> %A changes to %B
result = apply_keyword_changes(B, P, changes)
sys.exit(result)
else:
try:
os.execlp("git", "git", "merge-file", "-L", "HEAD", "-L", "base", "-L", "ours", A, O, B)
except OSError:
sys.exit(-1)
from gentoolkit.merge_driver_ekeyword import merge_driver_ekeyword


if __name__ == "__main__":
main(sys.argv)
sys.exit(merge_driver_ekeyword.main(sys.argv[1:]))
18 changes: 0 additions & 18 deletions pym/gentoolkit/ekeyword/Makefile

This file was deleted.

1 change: 0 additions & 1 deletion pym/gentoolkit/ekeyword/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ py.install_sources(

install_data(
[
'Makefile',
'README',
'pytest.ini',
],
Expand Down
Empty file.
165 changes: 165 additions & 0 deletions pym/gentoolkit/merge_driver_ekeyword/merge_driver_ekeyword.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/python3
#
# Copyright 2020-2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2 or later

"""
Custom git merge driver for handling conflicts in KEYWORDS assignments

See https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver
"""

import difflib
import os
import shutil
import sys
import tempfile

from typing import Optional
from collections.abc import Sequence

from gentoolkit.ekeyword import ekeyword


KeywordChanges = list[tuple[Optional[list[str]], Optional[list[str]]]]


def keyword_array(keyword_line: str) -> list[str]:
# Find indices of string inside the double-quotes
i1: int = keyword_line.find('"') + 1
i2: int = keyword_line.rfind('"')

# Split into array of KEYWORDS
return keyword_line[i1:i2].split(" ")


def keyword_line_changes(old: str, new: str) -> KeywordChanges:
a: list[str] = keyword_array(old)
b: list[str] = keyword_array(new)

s = difflib.SequenceMatcher(a=a, b=b)

changes: KeywordChanges = []
for tag, i1, i2, j1, j2 in s.get_opcodes():
if tag == "replace":
changes.append(
(a[i1:i2], b[j1:j2]),
)
elif tag == "delete":
changes.append(
(a[i1:i2], None),
)
elif tag == "insert":
changes.append(
(None, b[j1:j2]),
)
else:
assert tag == "equal"
return changes


def keyword_changes(ebuild1: str, ebuild2: str) -> Optional[KeywordChanges]:
with open(ebuild1) as e1, open(ebuild2) as e2:
lines1 = e1.readlines()
lines2 = e2.readlines()

diff = difflib.unified_diff(lines1, lines2, n=0)
assert next(diff) == "--- \n"
assert next(diff) == "+++ \n"

hunk: int = 0
old: str = ""
new: str = ""

for line in diff:
if line.startswith("@@ "):
if hunk > 0:
break
hunk += 1
elif line.startswith("-"):
if old or new:
break
old = line
elif line.startswith("+"):
if not old or new:
break
new = line
else:
if "KEYWORDS=" in old and "KEYWORDS=" in new:
return keyword_line_changes(old, new)
return None


def apply_keyword_changes(ebuild: str, pathname: str, changes: KeywordChanges) -> int:
result: int = 0

with tempfile.TemporaryDirectory() as tmpdir:
# ekeyword will only modify files named *.ebuild, so make a symlink
ebuild_symlink: str = os.path.join(tmpdir, os.path.basename(pathname))
os.symlink(os.path.join(os.getcwd(), ebuild), ebuild_symlink)

for removals, additions in changes:
args = []
if removals:
for rem in removals:
# Drop leading '~' and '-' characters and prepend '^'
i = 1 if rem[0] in ("~", "-") else 0
args.append("^" + rem[i:])
if additions:
args.extend(additions)
args.append(ebuild_symlink)

result = ekeyword.main(args)
if result != 0:
break

return result


def merge_keywords(O, A, B, P) -> int:
# Get changes to KEYWORDS= from %O to %B
if changes := keyword_changes(O, B):
# Apply %O -> %B changes to %A
return apply_keyword_changes(A, P, changes)
return -1


def main(argv: Sequence[str]) -> int:
if len(argv) != 4:
return -1

O = argv[0] # %O - filename of original
A = argv[1] # %A - filename of our current version
B = argv[2] # %B - filename of the other branch's version
P = argv[3] # %P - original path of the file

if merge_keywords(O, A, B, P) == 0:
return 0

# Try in reverse
if merge_keywords(O, B, A, P) == 0:
# Merged file should be left in %A
shutil.move(B, A)
return 0

try:
os.execlp(
"git",
"git",
"merge-file",
"-L",
"HEAD",
"-L",
"base",
"-L",
"ours",
A,
O,
B,
)
except OSError:
return -1


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
48 changes: 48 additions & 0 deletions pym/gentoolkit/merge_driver_ekeyword/test_merge_driver_ekeyword.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/python
# Copyright 2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

"""Unittests for merge_driver_ekeyword"""

import itertools
import os
import pathlib
import pytest
import shutil
import tempfile

from gentoolkit.merge_driver_ekeyword import merge_driver_ekeyword


TESTDIR = pathlib.Path(__file__).parent / "tests"
TESTDIRS = [os.path.dirname(x) for x in TESTDIR.rglob("common-ancestor.ebuild")]
TESTDATA = itertools.product(TESTDIRS, (False, True))


def file_contents(filename):
with open(filename) as file:
return file.readlines()


@pytest.mark.parametrize("testdir,reverse", TESTDATA)
def test_merge(testdir, reverse):
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(testdir, tmpdir, dirs_exist_ok=True)

O = os.path.join(tmpdir, "common-ancestor.ebuild")
if reverse:
A = os.path.join(tmpdir, "B.ebuild")
B = os.path.join(tmpdir, "A.ebuild")
else:
A = os.path.join(tmpdir, "A.ebuild")
B = os.path.join(tmpdir, "B.ebuild")
P = "expected.ebuild"
expected = os.path.join(tmpdir, P)

# A.ebuild and B.ebuild can be merged iff expected.ebuild exists.
if os.path.exists(expected):
assert 0 == merge_driver_ekeyword.main([O, A, B, P])
assert file_contents(expected) == file_contents(A)
else:
assert -1 == merge_driver_ekeyword.merge_keywords(O, A, B, P)
assert -1 == merge_driver_ekeyword.merge_keywords(O, B, A, P)
Loading
Loading