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) |