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 |