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 |