1 """Performs a 3-way merge for GIT files
5 Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 from stgit
import basedir
23 from stgit
.config
import config
, file_extensions
, ConfigOption
24 from stgit
.utils
import append_string
27 class GitMergeException(Exception):
34 merger
= ConfigOption('stgit', 'merger')
35 keeporig
= ConfigOption('stgit', 'keeporig')
47 f
= os
.popen(cmd
, 'r')
48 string
= f
.readline().rstrip()
50 raise GitMergeException
, 'Error: failed to execute "%s"' % cmd
53 def __checkout_files(orig_hash
, file1_hash
, file2_hash
,
55 orig_mode
, file1_mode
, file2_mode
):
56 """Check out the files passed as arguments
58 global orig
, src1
, src2
60 extensions
= file_extensions()
63 orig
= path
+ extensions
['ancestor']
64 tmp
= __output('git-unpack-file %s' % orig_hash
)
65 os
.chmod(tmp
, int(orig_mode
, 8))
68 src1
= path
+ extensions
['current']
69 tmp
= __output('git-unpack-file %s' % file
1_hash
)
70 os
.chmod(tmp
, int(file1_mode
, 8))
73 src2
= path
+ extensions
['patched']
74 tmp
= __output('git-unpack-file %s' % file
2_hash
)
75 os
.chmod(tmp
, int(file2_mode
, 8))
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' % file
1_hash
)
82 os
.chmod(tmp
, int(file1_mode
, 8))
85 def __remove_files(orig_hash
, file1_hash
, file2_hash
):
86 """Remove any temporary files
96 """Write the conflict file for the 'path' variable and exit
98 append_string(os
.path
.join(basedir
.get(), 'conflicts'), path
)
101 def interactive_merge(filename
):
102 """Run the interactive merger on the given file. Note that the
103 index should not have any conflicts.
106 imerger
= config
.get('stgit', 'imerger')
107 except Exception, err
:
108 raise GitMergeException
, 'Configuration error: %s' % err
110 extensions
= file_extensions()
112 ancestor
= filename
+ extensions
['ancestor']
113 current
= filename
+ extensions
['current']
114 patched
= filename
+ extensions
['patched']
116 # check whether we have all the files for a three-way merge
117 for fn
in [filename
, ancestor
, current
, patched
]:
118 if not os
.path
.isfile(fn
):
119 raise GitMergeException
, \
120 'Cannot run the interactive merger: "%s" missing' % fn
122 mtime
= os
.path
.getmtime(filename
)
124 err
= os
.system(imerger %
{'branch1': current
,
125 'ancestor': ancestor
,
130 raise GitMergeException
, 'The interactive merge failed: %d' % err
131 if not os
.path
.isfile(filename
):
132 raise GitMergeException
, 'The "%s" file is missing' % filename
133 if mtime
== os
.path
.getmtime(filename
):
134 raise GitMergeException
, 'The "%s" file was not modified' % filename
140 def merge(orig_hash
, file1_hash
, file2_hash
,
142 orig_mode
, file1_mode
, file2_mode
):
143 """Three-way merge for one file algorithm
145 __checkout_files(orig_hash
, file1_hash
, file2_hash
,
147 orig_mode
, file1_mode
, file2_mode
)
149 # file exists in origin
152 if file1_hash
and file2_hash
:
153 # if modes are the same (git-read-tree probably dealt with it)
154 if file1_hash
== file2_hash
:
155 if os
.system('git-update-index --cacheinfo %s %s %s'
156 %
(file1_mode
, file1_hash
, path
)) != 0:
157 print >> sys
.stderr
, 'Error: git-update-index failed'
160 if os
.system('git-checkout-index -u -f -- %s' % path
):
161 print >> sys
.stderr
, 'Error: git-checkout-index failed'
164 if file1_mode
!= file2_mode
:
165 print >> sys
.stderr
, \
166 'Error: File added in both, permissions conflict'
171 merge_ok
= os
.system(str(merger
) %
{'branch1': src1
,
174 'output': path
}) == 0
177 os
.system('git-update-index -- %s' % path
)
178 __remove_files(orig_hash
, file1_hash
, file2_hash
)
181 print >> sys
.stderr
, \
182 'Error: three-way merge tool failed for file "%s"' \
184 # reset the cache to the first branch
185 os
.system('git-update-index --cacheinfo %s %s %s'
186 %
(file1_mode
, file1_hash
, path
))
188 if config
.get('stgit', 'autoimerge') == 'yes':
189 print >> sys
.stderr
, \
190 'Trying the interactive merge'
192 interactive_merge(path
)
193 except GitMergeException
, ex
:
194 # interactive merge failed
195 print >> sys
.stderr
, str(ex
)
196 if str(keeporig
) != 'yes':
197 __remove_files(orig_hash
, file1_hash
,
201 # successful interactive merge
202 os
.system('git-update-index -- %s' % path
)
203 __remove_files(orig_hash
, file1_hash
, file2_hash
)
206 # no interactive merge, just mark it as conflict
207 if str(keeporig
) != 'yes':
208 __remove_files(orig_hash
, file1_hash
, file2_hash
)
212 # file deleted in both or deleted in one and unchanged in the other
213 elif not (file1_hash
or file2_hash
) \
214 or file1_hash
== orig_hash
or file2_hash
== orig_hash
:
215 if os
.path
.exists(path
):
217 __remove_files(orig_hash
, file1_hash
, file2_hash
)
218 return os
.system('git-update-index --remove -- %s' % path
)
219 # file deleted in one and changed in the other
221 # Do something here - we must at least merge the entry in
222 # the cache, instead of leaving it in U(nmerged) state. In
223 # fact, stg resolved does not handle that.
225 # Do the same thing cogito does - remove the file in any case.
226 os
.system('git-update-index --remove -- %s' % path
)
229 ## file deleted upstream and changed in the patch. The
230 ## patch is probably going to move the changes
233 #os.system('git-update-index --remove -- %s' % path)
235 ## file deleted in the patch and changed upstream. We
236 ## could re-delete it, but for now leave it there -
237 ## and let the user check if he still wants to remove
240 ## reset the cache to the first branch
241 #os.system('git-update-index --cacheinfo %s %s %s'
242 # % (file1_mode, file1_hash, path))
246 # file does not exist in origin
249 if file1_hash
and file2_hash
:
251 if file1_hash
== file2_hash
:
252 if os
.system('git-update-index --add --cacheinfo %s %s %s'
253 %
(file1_mode
, file1_hash
, path
)) != 0:
254 print >> sys
.stderr
, 'Error: git-update-index failed'
257 if os
.system('git-checkout-index -u -f -- %s' % path
):
258 print >> sys
.stderr
, 'Error: git-checkout-index failed'
261 if file1_mode
!= file2_mode
:
262 print >> sys
.stderr
, \
263 'Error: File "s" added in both, ' \
264 'permissions conflict' % path
267 # files are different
269 # reset the index to the current file
270 os
.system('git-update-index -- %s' % path
)
271 print >> sys
.stderr
, \
272 'Error: File "%s" added in branches but different' % path
276 elif file1_hash
or file2_hash
:
283 if os
.system('git-update-index --add --cacheinfo %s %s %s'
284 %
(mode
, obj
, path
)) != 0:
285 print >> sys
.stderr
, 'Error: git-update-index failed'
288 __remove_files(orig_hash
, file1_hash
, file2_hash
)
289 return os
.system('git-checkout-index -u -f -- %s' % path
)
292 print >> sys
.stderr
, 'Error: Unhandled merge conflict: ' \
293 '"%s" "%s" "%s" "%s" "%s" "%s" "%s"' \
294 %
(orig_hash
, file1_hash
, file2_hash
,
296 orig_mode
, file1_mode
, file2_mode
)