From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from lists.gentoo.org (pigeon.gentoo.org [208.92.234.80]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by finch.gentoo.org (Postfix) with ESMTPS id BEBF61382C5 for ; Thu, 31 Dec 2020 22:11:08 +0000 (UTC) Received: from pigeon.gentoo.org (localhost [127.0.0.1]) by pigeon.gentoo.org (Postfix) with SMTP id EB1A6E090F; Thu, 31 Dec 2020 22:11:07 +0000 (UTC) Received: from smtp.gentoo.org (smtp.gentoo.org [140.211.166.183]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by pigeon.gentoo.org (Postfix) with ESMTPS id C1BF2E090F for ; Thu, 31 Dec 2020 22:11:07 +0000 (UTC) Received: from oystercatcher.gentoo.org (oystercatcher.gentoo.org [148.251.78.52]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.gentoo.org (Postfix) with ESMTPS id 95748340E93 for ; Thu, 31 Dec 2020 22:11:06 +0000 (UTC) Received: from localhost.localdomain (localhost [IPv6:::1]) by oystercatcher.gentoo.org (Postfix) with ESMTP id 07D54CC for ; Thu, 31 Dec 2020 22:11:05 +0000 (UTC) From: "Matt Turner" To: gentoo-commits@lists.gentoo.org Content-Transfer-Encoding: 8bit Content-type: text/plain; charset=UTF-8 Reply-To: gentoo-dev@lists.gentoo.org, "Matt Turner" Message-ID: <1609443857.9f01c8b098484866974407bb74680debf0d64e4f.mattst88@gentoo> Subject: [gentoo-commits] proj/gentoolkit:master commit in: bin/ X-VCS-Repository: proj/gentoolkit X-VCS-Files: bin/merge-driver-ekeyword X-VCS-Directories: bin/ X-VCS-Committer: mattst88 X-VCS-Committer-Name: Matt Turner X-VCS-Revision: 9f01c8b098484866974407bb74680debf0d64e4f X-VCS-Branch: master Date: Thu, 31 Dec 2020 22:11:05 +0000 (UTC) Precedence: bulk List-Post: List-Help: List-Unsubscribe: List-Subscribe: List-Id: Gentoo Linux mail X-BeenThere: gentoo-commits@lists.gentoo.org X-Auto-Response-Suppress: DR, RN, NRN, OOF, AutoReply X-Archives-Salt: cc4eb9a8-aecf-4c8d-9ba2-0d8b62e519b6 X-Archives-Hash: 7b595097b3a8a59a8a5efdb95c416ba1 commit: 9f01c8b098484866974407bb74680debf0d64e4f Author: Matt Turner gentoo org> AuthorDate: Sun Dec 20 22:12:49 2020 +0000 Commit: Matt Turner gentoo org> CommitDate: Thu Dec 31 19:44:17 2020 +0000 URL: https://gitweb.gentoo.org/proj/gentoolkit.git/commit/?id=9f01c8b0 bin: Add merge-driver-ekeyword Since the KEYWORDS=... assignment is a single line, git struggles to handle conflicts. When rebasing a series of commits that modify the KEYWORDS=... it's usually easier to throw them away and reapply on the new tree than it is to manually handle conflicts during the rebase. git allows a 'merge driver' program to handle conflicts; this program handles conflicts in the KEYWORDS=... assignment. E.g., given an ebuild with these keywords: KEYWORDS="~alpha amd64 arm arm64 ~hppa ppc ppc64 x86" One developer drops the ~alpha keyword and pushes to gentoo.git, and another developer stabilizes hppa. Without this merge driver, git requires the second developer to manually resolve the conflict which is tedious and prone to mistakes when rebasing a long series of patches. With the custom merge driver, it automatically resolves the conflict. To use the merge driver, configure your gentoo.git as such: gentoo.git/.git/config: [merge "keywords"] name = KEYWORDS merge driver driver = merge-driver-ekeyword %O %A %B %P gentoo.git/.git/info/attributes: *.ebuild merge=keywords Signed-off-by: Matt Turner gentoo.org> bin/merge-driver-ekeyword | 132 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/bin/merge-driver-ekeyword b/bin/merge-driver-ekeyword new file mode 100755 index 0000000..2df83fc --- /dev/null +++ b/bin/merge-driver-ekeyword @@ -0,0 +1,132 @@ +#!/usr/bin/python +# +# Copyright 2020 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 List, Optional, Tuple + +from gentoolkit.ekeyword import ekeyword + + +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) -> List[Tuple[Optional[str], + Optional[str]]]: + a: List[str] = keyword_array(old) + b: List[str] = keyword_array(new) + + s = difflib.SequenceMatcher(a=a, b=b) + + changes = [] + 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) -> List[Tuple[Optional[str], + Optional[str]]]: + 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: List[Tuple[Optional[str], + Optional[str]]]) -> 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 = [] + 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): + 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 from %O to %B + changes = keyword_changes(O, B) + if changes: + # Apply O -> B changes to A + result: int = apply_keyword_changes(A, P, changes) + sys.exit(result) + else: + result: int = os.system(f"git merge-file -L HEAD -L base -L ours {A} {O} {B}") + sys.exit(0 if result == 0 else -1) + + +if __name__ == "__main__": + main(sys.argv)