Gentoo Archives: gentoo-portage-dev

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

Replies