Gentoo Archives: gentoo-portage-dev

From: Matt Turner <mattst88@g.o>
To: gentoo-portage-dev@l.g.o
Cc: Zac Medico <zmedico@g.o>, Matt Turner <mattst88@g.o>
Subject: [gentoo-portage-dev] [PATCH gentoolkit] bin: Add merge-driver-ekeyword
Date: Thu, 31 Dec 2020 19:47:30
Message-Id: 20201231194723.177630-1-mattst88@gentoo.org
In Reply to: Re: [gentoo-portage-dev] [PATCH gentoolkit] bin: Add merge-driver-ekeyword by Zac Medico
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 v3: Address Zac's feedback: use tempfile.TemporaryDirectory
33
34 Since ekeyword prints the name of the file modified, I think using a
35 symlink with the name of the original file is better than having it
36 print .merge_file_SEd3R8.
37
38 bin/merge-driver-ekeyword | 132 ++++++++++++++++++++++++++++++++++++++
39 1 file changed, 132 insertions(+)
40 create mode 100755 bin/merge-driver-ekeyword
41
42 diff --git a/bin/merge-driver-ekeyword b/bin/merge-driver-ekeyword
43 new file mode 100755
44 index 0000000..2df83fc
45 --- /dev/null
46 +++ b/bin/merge-driver-ekeyword
47 @@ -0,0 +1,132 @@
48 +#!/usr/bin/python
49 +#
50 +# Copyright 2020 Gentoo Authors
51 +# Distributed under the terms of the GNU General Public License v2 or later
52 +
53 +"""
54 +Custom git merge driver for handling conflicts in KEYWORDS assignments
55 +
56 +See https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver
57 +"""
58 +
59 +import difflib
60 +import os
61 +import sys
62 +import tempfile
63 +
64 +from typing import List, Optional, Tuple
65 +
66 +from gentoolkit.ekeyword import ekeyword
67 +
68 +
69 +def keyword_array(keyword_line: str) -> List[str]:
70 + # Find indices of string inside the double-quotes
71 + i1: int = keyword_line.find('"') + 1
72 + i2: int = keyword_line.rfind('"')
73 +
74 + # Split into array of KEYWORDS
75 + return keyword_line[i1:i2].split(' ')
76 +
77 +
78 +def keyword_line_changes(old: str, new: str) -> List[Tuple[Optional[str],
79 + Optional[str]]]:
80 + a: List[str] = keyword_array(old)
81 + b: List[str] = keyword_array(new)
82 +
83 + s = difflib.SequenceMatcher(a=a, b=b)
84 +
85 + changes = []
86 + for tag, i1, i2, j1, j2 in s.get_opcodes():
87 + if tag == 'replace':
88 + changes.append((a[i1:i2], b[j1:j2]),)
89 + elif tag == 'delete':
90 + changes.append((a[i1:i2], None),)
91 + elif tag == 'insert':
92 + changes.append((None, b[j1:j2]),)
93 + else:
94 + assert tag == 'equal'
95 + return changes
96 +
97 +
98 +def keyword_changes(ebuild1: str, ebuild2: str) -> List[Tuple[Optional[str],
99 + Optional[str]]]:
100 + with open(ebuild1) as e1, open(ebuild2) as e2:
101 + lines1 = e1.readlines()
102 + lines2 = e2.readlines()
103 +
104 + diff = difflib.unified_diff(lines1, lines2, n=0)
105 + assert next(diff) == '--- \n'
106 + assert next(diff) == '+++ \n'
107 +
108 + hunk: int = 0
109 + old: str = ''
110 + new: str = ''
111 +
112 + for line in diff:
113 + if line.startswith('@@ '):
114 + if hunk > 0:
115 + break
116 + hunk += 1
117 + elif line.startswith('-'):
118 + if old or new:
119 + break
120 + old = line
121 + elif line.startswith('+'):
122 + if not old or new:
123 + break
124 + new = line
125 + else:
126 + if 'KEYWORDS=' in old and 'KEYWORDS=' in new:
127 + return keyword_line_changes(old, new)
128 + return None
129 +
130 +
131 +def apply_keyword_changes(ebuild: str, pathname: str,
132 + changes: List[Tuple[Optional[str],
133 + Optional[str]]]) -> int:
134 + result: int = 0
135 +
136 + with tempfile.TemporaryDirectory() as tmpdir:
137 + # ekeyword will only modify files named *.ebuild, so make a symlink
138 + ebuild_symlink: str = os.path.join(tmpdir, os.path.basename(pathname))
139 + os.symlink(os.path.join(os.getcwd(), ebuild), ebuild_symlink)
140 +
141 + for removals, additions in changes:
142 + args = []
143 + for rem in removals:
144 + # Drop leading '~' and '-' characters and prepend '^'
145 + i = 1 if rem[0] in ('~', '-') else 0
146 + args.append('^' + rem[i:])
147 + if additions:
148 + args.extend(additions)
149 + args.append(ebuild_symlink)
150 +
151 + result = ekeyword.main(args)
152 + if result != 0:
153 + break
154 +
155 + return result
156 +
157 +
158 +def main(argv):
159 + if len(argv) != 5:
160 + sys.exit(-1)
161 +
162 + O = argv[1] # %O - filename of original
163 + A = argv[2] # %A - filename of our current version
164 + B = argv[3] # %B - filename of the other branch's version
165 + P = argv[4] # %P - original path of the file
166 +
167 + # Get changes from %O to %B
168 + changes = keyword_changes(O, B)
169 + if changes:
170 + # Apply O -> B changes to A
171 + result: int = apply_keyword_changes(A, P, changes)
172 + sys.exit(result)
173 + else:
174 + result: int = os.system(f"git merge-file -L HEAD -L base -L ours {A} {O} {B}")
175 + sys.exit(0 if result == 0 else -1)
176 +
177 +
178 +if __name__ == "__main__":
179 + main(sys.argv)
180 --
181 2.26.2

Replies