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] [RFC PATCH gentoolkit] bin: Add merge-driver-ekeyword
Date: Mon, 21 Dec 2020 03:44:59
Message-Id: 20201221034452.307153-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 ~ia64 ~mips ~ppc ~ppc64 ~riscv ~s390 ~sparc ~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. With
15 the custom merge driver, it automatically resolves the conflict.
16
17 gentoo.git/.git/config:
18
19 [core]
20 ...
21 attributesfile = ~/.gitattributes
22 [merge "keywords"]
23 name = KEYWORDS merge driver
24 driver = merge-driver-ekeyword %O %A %B
25
26 ~/.gitattributes:
27
28 *.ebuild merge=keywords
29
30 Signed-off-by: Matt Turner <mattst88@g.o>
31 ---
32 One annoying wart in the program is due to the fact that ekeyword
33 won't work on any file not named *.ebuild. I make a symlink (and set up
34 an atexit handler to remove it) to work around this. I'm not sure we
35 could make ekeyword handle arbitrary filenames given its complex multi-
36 argument parameter support. git merge files are named .merge_file_XXXXX
37 according to git-unpack-file(1), so we could allow those. Thoughts?
38
39 bin/merge-driver-ekeyword | 125 ++++++++++++++++++++++++++++++++++++++
40 1 file changed, 125 insertions(+)
41 create mode 100755 bin/merge-driver-ekeyword
42
43 diff --git a/bin/merge-driver-ekeyword b/bin/merge-driver-ekeyword
44 new file mode 100755
45 index 0000000..6e645a9
46 --- /dev/null
47 +++ b/bin/merge-driver-ekeyword
48 @@ -0,0 +1,125 @@
49 +#!/usr/bin/python
50 +#
51 +# Copyright 2020 Gentoo Authors
52 +# Distributed under the terms of the GNU General Public License v2 or later
53 +
54 +"""
55 +Custom git merge driver for handling conflicts in KEYWORDS assignments
56 +
57 +See https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver
58 +"""
59 +
60 +import atexit
61 +import difflib
62 +import os
63 +import shutil
64 +import sys
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.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: break
117 + hunk += 1
118 + elif line.startswith('-'):
119 + if old or new: break
120 + old = line
121 + elif line.startswith('+'):
122 + if not old or new: break
123 + new = line
124 + else:
125 + if 'KEYWORDS=' in old and 'KEYWORDS=' in new:
126 + return keyword_line_changes(old, new)
127 + return None
128 +
129 +
130 +def apply_keyword_changes(ebuild: str,
131 + changes: List[Tuple[Optional[str],
132 + Optional[str]]]) -> int:
133 + # ekeyword will only modify files named *.ebuild, so make a symlink
134 + ebuild_symlink = ebuild + '.ebuild'
135 + os.symlink(ebuild, ebuild_symlink)
136 + atexit.register(lambda: os.remove(ebuild_symlink))
137 +
138 + for removals, additions in changes:
139 + args = []
140 + for rem in removals:
141 + # Drop leading '~' and '-' characters and prepend '^'
142 + i = 1 if rem[0] in ('~', '-') else 0
143 + args.append('^' + rem[i:])
144 + if additions:
145 + args.extend(additions)
146 + args.append(ebuild_symlink)
147 +
148 + result = ekeyword.main(args)
149 + if result != 0:
150 + return result
151 + return 0
152 +
153 +
154 +def main(argv):
155 + if len(argv) != 4:
156 + sys.exit(-1)
157 +
158 + O = argv[1] # %O - filename of original
159 + A = argv[2] # %A - filename of our current version
160 + B = argv[3] # %B - filename of the other branch's version
161 +
162 + # Get changes from %O to %B
163 + changes = keyword_changes(O, B)
164 + if not changes:
165 + sys.exit(-1)
166 +
167 + # Apply O -> B changes to A
168 + result: int = apply_keyword_changes(A, changes)
169 + sys.exit(result)
170 +
171 +
172 +if __name__ == "__main__":
173 + main(sys.argv)
174 --
175 2.26.2

Replies