Make stgit.config use git-repo-config.
[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 """
c73e63b7
YD
105 imerger = config.get('stgit.imerger')
106 if not imerger:
f7ed76a9
CM
107 raise GitMergeException, 'Configuration error: %s' % err
108
109 extensions = file_extensions()
110
111 ancestor = filename + extensions['ancestor']
112 current = filename + extensions['current']
113 patched = filename + extensions['patched']
114
115 # check whether we have all the files for a three-way merge
116 for fn in [filename, ancestor, current, patched]:
117 if not os.path.isfile(fn):
118 raise GitMergeException, \
119 'Cannot run the interactive merger: "%s" missing' % fn
120
121 mtime = os.path.getmtime(filename)
122
123 err = os.system(imerger % {'branch1': current,
124 'ancestor': ancestor,
125 'branch2': patched,
126 'output': filename})
127
128 if err != 0:
129 raise GitMergeException, 'The interactive merge failed: %d' % err
130 if not os.path.isfile(filename):
131 raise GitMergeException, 'The "%s" file is missing' % filename
132 if mtime == os.path.getmtime(filename):
133 raise GitMergeException, 'The "%s" file was not modified' % filename
134
135
3659ef88
CM
136#
137# Main algorithm
138#
139def merge(orig_hash, file1_hash, file2_hash,
140 path,
141 orig_mode, file1_mode, file2_mode):
142 """Three-way merge for one file algorithm
143 """
144 __checkout_files(orig_hash, file1_hash, file2_hash,
145 path,
146 orig_mode, file1_mode, file2_mode)
147
148 # file exists in origin
149 if orig_hash:
150 # modified in both
151 if file1_hash and file2_hash:
152 # if modes are the same (git-read-tree probably dealt with it)
153 if file1_hash == file2_hash:
154 if os.system('git-update-index --cacheinfo %s %s %s'
155 % (file1_mode, file1_hash, path)) != 0:
156 print >> sys.stderr, 'Error: git-update-index failed'
157 __conflict(path)
158 return 1
159 if os.system('git-checkout-index -u -f -- %s' % path):
160 print >> sys.stderr, 'Error: git-checkout-index failed'
161 __conflict(path)
162 return 1
163 if file1_mode != file2_mode:
164 print >> sys.stderr, \
165 'Error: File added in both, permissions conflict'
166 __conflict(path)
167 return 1
168 # 3-way merge
169 else:
5b99888b
CM
170 merge_ok = os.system(str(merger) % {'branch1': src1,
171 'ancestor': orig,
172 'branch2': src2,
173 'output': path }) == 0
3659ef88
CM
174
175 if merge_ok:
176 os.system('git-update-index -- %s' % path)
177 __remove_files(orig_hash, file1_hash, file2_hash)
178 return 0
179 else:
180 print >> sys.stderr, \
181 'Error: three-way merge tool failed for file "%s"' \
182 % path
183 # reset the cache to the first branch
184 os.system('git-update-index --cacheinfo %s %s %s'
185 % (file1_mode, file1_hash, path))
f7ed76a9 186
c73e63b7 187 if config.get('stgit.autoimerge') == 'yes':
f7ed76a9
CM
188 print >> sys.stderr, \
189 'Trying the interactive merge'
190 try:
191 interactive_merge(path)
192 except GitMergeException, ex:
193 # interactive merge failed
194 print >> sys.stderr, str(ex)
195 if str(keeporig) != 'yes':
196 __remove_files(orig_hash, file1_hash,
197 file2_hash)
198 __conflict(path)
199 return 1
200 # successful interactive merge
0f4eba6a 201 os.system('git-update-index -- %s' % path)
3659ef88 202 __remove_files(orig_hash, file1_hash, file2_hash)
f7ed76a9
CM
203 return 0
204 else:
205 # no interactive merge, just mark it as conflict
206 if str(keeporig) != 'yes':
207 __remove_files(orig_hash, file1_hash, file2_hash)
208 __conflict(path)
209 return 1
210
3659ef88
CM
211 # file deleted in both or deleted in one and unchanged in the other
212 elif not (file1_hash or file2_hash) \
213 or file1_hash == orig_hash or file2_hash == orig_hash:
214 if os.path.exists(path):
215 os.remove(path)
216 __remove_files(orig_hash, file1_hash, file2_hash)
217 return os.system('git-update-index --remove -- %s' % path)
218 # file deleted in one and changed in the other
219 else:
220 # Do something here - we must at least merge the entry in
221 # the cache, instead of leaving it in U(nmerged) state. In
222 # fact, stg resolved does not handle that.
223
224 # Do the same thing cogito does - remove the file in any case.
225 os.system('git-update-index --remove -- %s' % path)
226
227 #if file1_hash:
228 ## file deleted upstream and changed in the patch. The
229 ## patch is probably going to move the changes
230 ## elsewhere.
231
232 #os.system('git-update-index --remove -- %s' % path)
233 #else:
234 ## file deleted in the patch and changed upstream. We
235 ## could re-delete it, but for now leave it there -
236 ## and let the user check if he still wants to remove
237 ## the file.
238
239 ## reset the cache to the first branch
240 #os.system('git-update-index --cacheinfo %s %s %s'
241 # % (file1_mode, file1_hash, path))
242 __conflict(path)
243 return 1
244
245 # file does not exist in origin
246 else:
247 # file added in both
248 if file1_hash and file2_hash:
249 # files are the same
250 if file1_hash == file2_hash:
251 if os.system('git-update-index --add --cacheinfo %s %s %s'
252 % (file1_mode, file1_hash, path)) != 0:
253 print >> sys.stderr, 'Error: git-update-index failed'
254 __conflict(path)
255 return 1
256 if os.system('git-checkout-index -u -f -- %s' % path):
257 print >> sys.stderr, 'Error: git-checkout-index failed'
258 __conflict(path)
259 return 1
260 if file1_mode != file2_mode:
261 print >> sys.stderr, \
262 'Error: File "s" added in both, ' \
263 'permissions conflict' % path
264 __conflict(path)
265 return 1
266 # files are different
267 else:
56df21ed
CM
268 # reset the index to the current file
269 os.system('git-update-index -- %s' % path)
3659ef88
CM
270 print >> sys.stderr, \
271 'Error: File "%s" added in branches but different' % path
272 __conflict(path)
273 return 1
274 # file added in one
275 elif file1_hash or file2_hash:
276 if file1_hash:
277 mode = file1_mode
278 obj = file1_hash
279 else:
280 mode = file2_mode
281 obj = file2_hash
282 if os.system('git-update-index --add --cacheinfo %s %s %s'
283 % (mode, obj, path)) != 0:
284 print >> sys.stderr, 'Error: git-update-index failed'
285 __conflict(path)
286 return 1
287 __remove_files(orig_hash, file1_hash, file2_hash)
288 return os.system('git-checkout-index -u -f -- %s' % path)
289
290 # Unhandled case
291 print >> sys.stderr, 'Error: Unhandled merge conflict: ' \
292 '"%s" "%s" "%s" "%s" "%s" "%s" "%s"' \
293 % (orig_hash, file1_hash, file2_hash,
294 path,
295 orig_mode, file1_mode, file2_mode)
296 __conflict(path)
297 return 1