Gentoo Archives: gentoo-commits

From: Matt Turner <mattst88@g.o>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/gentoolkit:master commit in: bin/
Date: Thu, 31 Dec 2020 22:11:08
Message-Id: 1609443857.9f01c8b098484866974407bb74680debf0d64e4f.mattst88@gentoo
1 commit: 9f01c8b098484866974407bb74680debf0d64e4f
2 Author: Matt Turner <mattst88 <AT> gentoo <DOT> org>
3 AuthorDate: Sun Dec 20 22:12:49 2020 +0000
4 Commit: Matt Turner <mattst88 <AT> gentoo <DOT> org>
5 CommitDate: Thu Dec 31 19:44:17 2020 +0000
6 URL: https://gitweb.gentoo.org/proj/gentoolkit.git/commit/?id=9f01c8b0
7
8 bin: Add merge-driver-ekeyword
9
10 Since the KEYWORDS=... assignment is a single line, git struggles to
11 handle conflicts. When rebasing a series of commits that modify the
12 KEYWORDS=... it's usually easier to throw them away and reapply on the
13 new tree than it is to manually handle conflicts during the rebase.
14
15 git allows a 'merge driver' program to handle conflicts; this program
16 handles conflicts in the KEYWORDS=... assignment. E.g., given an ebuild
17 with these keywords:
18
19 KEYWORDS="~alpha amd64 arm arm64 ~hppa ppc ppc64 x86"
20
21 One developer drops the ~alpha keyword and pushes to gentoo.git, and
22 another developer stabilizes hppa. Without this merge driver, git
23 requires the second developer to manually resolve the conflict which is
24 tedious and prone to mistakes when rebasing a long series of patches.
25 With the custom merge driver, it automatically resolves the conflict.
26
27 To use the merge driver, configure your gentoo.git as such:
28
29 gentoo.git/.git/config:
30
31 [merge "keywords"]
32 name = KEYWORDS merge driver
33 driver = merge-driver-ekeyword %O %A %B %P
34
35 gentoo.git/.git/info/attributes:
36
37 *.ebuild merge=keywords
38
39 Signed-off-by: Matt Turner <mattst88 <AT> gentoo.org>
40
41 bin/merge-driver-ekeyword | 132 ++++++++++++++++++++++++++++++++++++++++++++++
42 1 file changed, 132 insertions(+)
43
44 diff --git a/bin/merge-driver-ekeyword b/bin/merge-driver-ekeyword
45 new file mode 100755
46 index 0000000..2df83fc
47 --- /dev/null
48 +++ b/bin/merge-driver-ekeyword
49 @@ -0,0 +1,132 @@
50 +#!/usr/bin/python
51 +#
52 +# Copyright 2020 Gentoo Authors
53 +# Distributed under the terms of the GNU General Public License v2 or later
54 +
55 +"""
56 +Custom git merge driver for handling conflicts in KEYWORDS assignments
57 +
58 +See https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver
59 +"""
60 +
61 +import difflib
62 +import os
63 +import sys
64 +import tempfile
65 +
66 +from typing import List, Optional, Tuple
67 +
68 +from gentoolkit.ekeyword import ekeyword
69 +
70 +
71 +def keyword_array(keyword_line: str) -> List[str]:
72 + # Find indices of string inside the double-quotes
73 + i1: int = keyword_line.find('"') + 1
74 + i2: int = keyword_line.rfind('"')
75 +
76 + # Split into array of KEYWORDS
77 + return keyword_line[i1:i2].split(' ')
78 +
79 +
80 +def keyword_line_changes(old: str, new: str) -> List[Tuple[Optional[str],
81 + Optional[str]]]:
82 + a: List[str] = keyword_array(old)
83 + b: List[str] = keyword_array(new)
84 +
85 + s = difflib.SequenceMatcher(a=a, b=b)
86 +
87 + changes = []
88 + for tag, i1, i2, j1, j2 in s.get_opcodes():
89 + if tag == 'replace':
90 + changes.append((a[i1:i2], b[j1:j2]),)
91 + elif tag == 'delete':
92 + changes.append((a[i1:i2], None),)
93 + elif tag == 'insert':
94 + changes.append((None, b[j1:j2]),)
95 + else:
96 + assert tag == 'equal'
97 + return changes
98 +
99 +
100 +def keyword_changes(ebuild1: str, ebuild2: str) -> List[Tuple[Optional[str],
101 + Optional[str]]]:
102 + with open(ebuild1) as e1, open(ebuild2) as e2:
103 + lines1 = e1.readlines()
104 + lines2 = e2.readlines()
105 +
106 + diff = difflib.unified_diff(lines1, lines2, n=0)
107 + assert next(diff) == '--- \n'
108 + assert next(diff) == '+++ \n'
109 +
110 + hunk: int = 0
111 + old: str = ''
112 + new: str = ''
113 +
114 + for line in diff:
115 + if line.startswith('@@ '):
116 + if hunk > 0:
117 + break
118 + hunk += 1
119 + elif line.startswith('-'):
120 + if old or new:
121 + break
122 + old = line
123 + elif line.startswith('+'):
124 + if not old or new:
125 + break
126 + new = line
127 + else:
128 + if 'KEYWORDS=' in old and 'KEYWORDS=' in new:
129 + return keyword_line_changes(old, new)
130 + return None
131 +
132 +
133 +def apply_keyword_changes(ebuild: str, pathname: str,
134 + changes: List[Tuple[Optional[str],
135 + Optional[str]]]) -> int:
136 + result: int = 0
137 +
138 + with tempfile.TemporaryDirectory() as tmpdir:
139 + # ekeyword will only modify files named *.ebuild, so make a symlink
140 + ebuild_symlink: str = os.path.join(tmpdir, os.path.basename(pathname))
141 + os.symlink(os.path.join(os.getcwd(), ebuild), ebuild_symlink)
142 +
143 + for removals, additions in changes:
144 + args = []
145 + for rem in removals:
146 + # Drop leading '~' and '-' characters and prepend '^'
147 + i = 1 if rem[0] in ('~', '-') else 0
148 + args.append('^' + rem[i:])
149 + if additions:
150 + args.extend(additions)
151 + args.append(ebuild_symlink)
152 +
153 + result = ekeyword.main(args)
154 + if result != 0:
155 + break
156 +
157 + return result
158 +
159 +
160 +def main(argv):
161 + if len(argv) != 5:
162 + sys.exit(-1)
163 +
164 + O = argv[1] # %O - filename of original
165 + A = argv[2] # %A - filename of our current version
166 + B = argv[3] # %B - filename of the other branch's version
167 + P = argv[4] # %P - original path of the file
168 +
169 + # Get changes from %O to %B
170 + changes = keyword_changes(O, B)
171 + if changes:
172 + # Apply O -> B changes to A
173 + result: int = apply_keyword_changes(A, P, changes)
174 + sys.exit(result)
175 + else:
176 + result: int = os.system(f"git merge-file -L HEAD -L base -L ours {A} {O} {B}")
177 + sys.exit(0 if result == 0 else -1)
178 +
179 +
180 +if __name__ == "__main__":
181 + main(sys.argv)