chiark / gitweb /
Cleanup parent information on stgit branch deletion.
[stgit] / stgit / gitmergeonefile.py
CommitLineData
3659ef88
CM
1"""Performs a 3-way merge for GIT files
2"""
3
4__copyright__ = """
5Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
21import sys, os
170f576b 22from stgit import basedir
f7ed76a9 23from stgit.config import config, file_extensions, ConfigOption
3659ef88
CM
24from stgit.utils import append_string
25
26
27class GitMergeException(Exception):
28 pass
29
30
31#
32# Options
33#
eee7283e
CM
34merger = ConfigOption('stgit', 'merger')
35keeporig = ConfigOption('stgit', 'keeporig')
3659ef88
CM
36
37#
38# Utility functions
39#
40def __str2none(x):
41 if x == '':
42 return None
43 else:
44 return x
45
46def __output(cmd):
47 f = os.popen(cmd, 'r')
48 string = f.readline().rstrip()
49 if f.close():
50 raise GitMergeException, 'Error: failed to execute "%s"' % cmd
51 return string
52
53def __checkout_files(orig_hash, file1_hash, file2_hash,
54 path,
55 orig_mode, file1_mode, file2_mode):
56 """Check out the files passed as arguments
57 """
58 global orig, src1, src2
59
1e075406
CM
60 extensions = file_extensions()
61
3659ef88 62 if orig_hash:
1e075406 63 orig = path + extensions['ancestor']
3659ef88
CM
64 tmp = __output('git-unpack-file %s' % orig_hash)
65 os.chmod(tmp, int(orig_mode, 8))
66 os.renames(tmp, orig)
67 if file1_hash:
1e075406 68 src1 = path + extensions['current']
3659ef88
CM
69 tmp = __output('git-unpack-file %s' % file1_hash)
70 os.chmod(tmp, int(file1_mode, 8))
71 os.renames(tmp, src1)
72 if file2_hash:
1e075406 73 src2 = path + extensions['patched']
3659ef88
CM
74 tmp = __output('git-unpack-file %s' % file2_hash)
75 os.chmod(tmp, int(file2_mode, 8))
76 os.renames(tmp, src2)
77
8d415553
CM
78 if file1_hash and not os.path.exists(path):
79 # the current file might be removed by GIT when it is a new
80 # file added in both branches. Just re-generate it
81 tmp = __output('git-unpack-file %s' % file1_hash)
82 os.chmod(tmp, int(file1_mode, 8))
83 os.renames(tmp, path)
84
3659ef88
CM
85def __remove_files(orig_hash, file1_hash, file2_hash):
86 """Remove any temporary files
87 """
88 if orig_hash:
89 os.remove(orig)
90 if file1_hash:
91 os.remove(src1)
92 if file2_hash:
93 os.remove(src2)
3659ef88 94
3659ef88
CM
95def __conflict(path):
96 """Write the conflict file for the 'path' variable and exit
97 """
170f576b 98 append_string(os.path.join(basedir.get(), 'conflicts'), path)
3659ef88
CM
99
100
f7ed76a9
CM
101def interactive_merge(filename):
102 """Run the interactive merger on the given file. Note that the
103 index should not have any conflicts.
104 """
f7ed76a9
CM
105 extensions = file_extensions()
106
107 ancestor = filename + extensions['ancestor']
108 current = filename + extensions['current']
109 patched = filename + extensions['patched']
110
b6e961f2
CM
111 if os.path.isfile(ancestor):
112 three_way = True
113 files_dict = {'branch1': current,
114 'ancestor': ancestor,
115 'branch2': patched,
116 'output': filename}
117 imerger = config.get('stgit.i3merge')
118 else:
119 three_way = False
120 files_dict = {'branch1': current,
121 'branch2': patched,
122 'output': filename}
123 imerger = config.get('stgit.i2merge')
124
125 if not imerger:
126 raise GitMergeException, 'No interactive merge command configured'
127
128 # check whether we have all the files for the merge
129 for fn in [filename, current, patched]:
f7ed76a9
CM
130 if not os.path.isfile(fn):
131 raise GitMergeException, \
b6e961f2 132 'Cannot run the interactive merge: "%s" missing' % fn
f7ed76a9
CM
133
134 mtime = os.path.getmtime(filename)
135
b6e961f2 136 print 'Trying the interractive %s merge' % \
73e3a6ff 137 (three_way and 'three-way' or 'two-way')
f7ed76a9 138
b6e961f2 139 err = os.system(imerger % files_dict)
f7ed76a9
CM
140 if err != 0:
141 raise GitMergeException, 'The interactive merge failed: %d' % err
142 if not os.path.isfile(filename):
143 raise GitMergeException, 'The "%s" file is missing' % filename
144 if mtime == os.path.getmtime(filename):
145 raise GitMergeException, 'The "%s" file was not modified' % filename
146
147
3659ef88
CM
148#
149# Main algorithm
150#
151def merge(orig_hash, file1_hash, file2_hash,
152 path,
153 orig_mode, file1_mode, file2_mode):
154 """Three-way merge for one file algorithm
155 """
156 __checkout_files(orig_hash, file1_hash, file2_hash,
157 path,
158 orig_mode, file1_mode, file2_mode)
159
160 # file exists in origin
161 if orig_hash:
162 # modified in both
163 if file1_hash and file2_hash:
164 # if modes are the same (git-read-tree probably dealt with it)
165 if file1_hash == file2_hash:
166 if os.system('git-update-index --cacheinfo %s %s %s'
167 % (file1_mode, file1_hash, path)) != 0:
168 print >> sys.stderr, 'Error: git-update-index failed'
169 __conflict(path)
170 return 1
171 if os.system('git-checkout-index -u -f -- %s' % path):
172 print >> sys.stderr, 'Error: git-checkout-index failed'
173 __conflict(path)
174 return 1
175 if file1_mode != file2_mode:
176 print >> sys.stderr, \
177 'Error: File added in both, permissions conflict'
178 __conflict(path)
179 return 1
180 # 3-way merge
181 else:
5b99888b
CM
182 merge_ok = os.system(str(merger) % {'branch1': src1,
183 'ancestor': orig,
184 'branch2': src2,
185 'output': path }) == 0
3659ef88
CM
186
187 if merge_ok:
188 os.system('git-update-index -- %s' % path)
189 __remove_files(orig_hash, file1_hash, file2_hash)
190 return 0
191 else:
192 print >> sys.stderr, \
193 'Error: three-way merge tool failed for file "%s"' \
194 % path
195 # reset the cache to the first branch
196 os.system('git-update-index --cacheinfo %s %s %s'
197 % (file1_mode, file1_hash, path))
f7ed76a9 198
c73e63b7 199 if config.get('stgit.autoimerge') == 'yes':
f7ed76a9
CM
200 try:
201 interactive_merge(path)
202 except GitMergeException, ex:
203 # interactive merge failed
204 print >> sys.stderr, str(ex)
205 if str(keeporig) != 'yes':
206 __remove_files(orig_hash, file1_hash,
207 file2_hash)
208 __conflict(path)
209 return 1
210 # successful interactive merge
0f4eba6a 211 os.system('git-update-index -- %s' % path)
3659ef88 212 __remove_files(orig_hash, file1_hash, file2_hash)
f7ed76a9
CM
213 return 0
214 else:
215 # no interactive merge, just mark it as conflict
216 if str(keeporig) != 'yes':
217 __remove_files(orig_hash, file1_hash, file2_hash)
218 __conflict(path)
219 return 1
220
3659ef88
CM
221 # file deleted in both or deleted in one and unchanged in the other
222 elif not (file1_hash or file2_hash) \
223 or file1_hash == orig_hash or file2_hash == orig_hash:
224 if os.path.exists(path):
225 os.remove(path)
226 __remove_files(orig_hash, file1_hash, file2_hash)
227 return os.system('git-update-index --remove -- %s' % path)
228 # file deleted in one and changed in the other
229 else:
230 # Do something here - we must at least merge the entry in
231 # the cache, instead of leaving it in U(nmerged) state. In
232 # fact, stg resolved does not handle that.
233
234 # Do the same thing cogito does - remove the file in any case.
235 os.system('git-update-index --remove -- %s' % path)
236
237 #if file1_hash:
238 ## file deleted upstream and changed in the patch. The
239 ## patch is probably going to move the changes
240 ## elsewhere.
241
242 #os.system('git-update-index --remove -- %s' % path)
243 #else:
244 ## file deleted in the patch and changed upstream. We
245 ## could re-delete it, but for now leave it there -
246 ## and let the user check if he still wants to remove
247 ## the file.
248
249 ## reset the cache to the first branch
250 #os.system('git-update-index --cacheinfo %s %s %s'
251 # % (file1_mode, file1_hash, path))
252 __conflict(path)
253 return 1
254
255 # file does not exist in origin
256 else:
257 # file added in both
258 if file1_hash and file2_hash:
259 # files are the same
260 if file1_hash == file2_hash:
261 if os.system('git-update-index --add --cacheinfo %s %s %s'
262 % (file1_mode, file1_hash, path)) != 0:
263 print >> sys.stderr, 'Error: git-update-index failed'
264 __conflict(path)
265 return 1
266 if os.system('git-checkout-index -u -f -- %s' % path):
267 print >> sys.stderr, 'Error: git-checkout-index failed'
268 __conflict(path)
269 return 1
270 if file1_mode != file2_mode:
271 print >> sys.stderr, \
272 'Error: File "s" added in both, ' \
273 'permissions conflict' % path
274 __conflict(path)
275 return 1
b6e961f2 276 # files added in both but different
3659ef88
CM
277 else:
278 print >> sys.stderr, \
279 'Error: File "%s" added in branches but different' % path
b6e961f2
CM
280 # reset the cache to the first branch
281 os.system('git-update-index --cacheinfo %s %s %s'
282 % (file1_mode, file1_hash, path))
283
284 if config.get('stgit.autoimerge') == 'yes':
285 try:
286 interactive_merge(path)
287 except GitMergeException, ex:
288 # interactive merge failed
289 print >> sys.stderr, str(ex)
290 if str(keeporig) != 'yes':
291 __remove_files(orig_hash, file1_hash,
292 file2_hash)
293 __conflict(path)
294 return 1
295 # successful interactive merge
296 os.system('git-update-index -- %s' % path)
297 __remove_files(orig_hash, file1_hash, file2_hash)
298 return 0
299 else:
300 # no interactive merge, just mark it as conflict
301 if str(keeporig) != 'yes':
302 __remove_files(orig_hash, file1_hash, file2_hash)
303 __conflict(path)
304 return 1
3659ef88
CM
305 # file added in one
306 elif file1_hash or file2_hash:
307 if file1_hash:
308 mode = file1_mode
309 obj = file1_hash
310 else:
311 mode = file2_mode
312 obj = file2_hash
313 if os.system('git-update-index --add --cacheinfo %s %s %s'
314 % (mode, obj, path)) != 0:
315 print >> sys.stderr, 'Error: git-update-index failed'
316 __conflict(path)
317 return 1
318 __remove_files(orig_hash, file1_hash, file2_hash)
319 return os.system('git-checkout-index -u -f -- %s' % path)
320
321 # Unhandled case
322 print >> sys.stderr, 'Error: Unhandled merge conflict: ' \
323 '"%s" "%s" "%s" "%s" "%s" "%s" "%s"' \
324 % (orig_hash, file1_hash, file2_hash,
325 path,
326 orig_mode, file1_mode, file2_mode)
327 __conflict(path)
328 return 1