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 |