Fix a bug in gitmergeonefile.py introduced recently
[stgit] / stgit / gitmergeonefile.py
1 """Performs a 3-way merge for GIT files
2 """
3
4 __copyright__ = """
5 Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>
6
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.
10
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.
15
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
19 """
20
21 import sys, os
22 from stgit import basedir
23 from stgit.config import file_extensions, ConfigOption
24 from stgit.utils import append_string
25
26
27 class GitMergeException(Exception):
28 pass
29
30
31 #
32 # Options
33 #
34 merger = ConfigOption('stgit', 'merger')
35 keeporig = ConfigOption('stgit', 'keeporig')
36
37 #
38 # Utility functions
39 #
40 def __str2none(x):
41 if x == '':
42 return None
43 else:
44 return x
45
46 def __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
53 def __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
60 extensions = file_extensions()
61
62 if orig_hash:
63 orig = path + extensions['ancestor']
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:
68 src1 = path + extensions['current']
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:
73 src2 = path + extensions['patched']
74 tmp = __output('git-unpack-file %s' % file2_hash)
75 os.chmod(tmp, int(file2_mode, 8))
76 os.renames(tmp, src2)
77
78 def __remove_files(orig_hash, file1_hash, file2_hash):
79 """Remove any temporary files
80 """
81 if orig_hash:
82 os.remove(orig)
83 if file1_hash:
84 os.remove(src1)
85 if file2_hash:
86 os.remove(src2)
87 pass
88
89 def __conflict(path):
90 """Write the conflict file for the 'path' variable and exit
91 """
92 append_string(os.path.join(basedir.get(), 'conflicts'), path)
93
94
95 #
96 # Main algorithm
97 #
98 def merge(orig_hash, file1_hash, file2_hash,
99 path,
100 orig_mode, file1_mode, file2_mode):
101 """Three-way merge for one file algorithm
102 """
103 __checkout_files(orig_hash, file1_hash, file2_hash,
104 path,
105 orig_mode, file1_mode, file2_mode)
106
107 # file exists in origin
108 if orig_hash:
109 # modified in both
110 if file1_hash and file2_hash:
111 # if modes are the same (git-read-tree probably dealt with it)
112 if file1_hash == file2_hash:
113 if os.system('git-update-index --cacheinfo %s %s %s'
114 % (file1_mode, file1_hash, path)) != 0:
115 print >> sys.stderr, 'Error: git-update-index failed'
116 __conflict(path)
117 return 1
118 if os.system('git-checkout-index -u -f -- %s' % path):
119 print >> sys.stderr, 'Error: git-checkout-index failed'
120 __conflict(path)
121 return 1
122 if file1_mode != file2_mode:
123 print >> sys.stderr, \
124 'Error: File added in both, permissions conflict'
125 __conflict(path)
126 return 1
127 # 3-way merge
128 else:
129 merge_ok = os.system(str(merger) % {'branch1': src1,
130 'ancestor': orig,
131 'branch2': src2,
132 'output': path }) == 0
133
134 if merge_ok:
135 os.system('git-update-index -- %s' % path)
136 __remove_files(orig_hash, file1_hash, file2_hash)
137 return 0
138 else:
139 print >> sys.stderr, \
140 'Error: three-way merge tool failed for file "%s"' \
141 % path
142 # reset the cache to the first branch
143 os.system('git-update-index --cacheinfo %s %s %s'
144 % (file1_mode, file1_hash, path))
145 if str(keeporig) != 'yes':
146 __remove_files(orig_hash, file1_hash, file2_hash)
147 __conflict(path)
148 return 1
149 # file deleted in both or deleted in one and unchanged in the other
150 elif not (file1_hash or file2_hash) \
151 or file1_hash == orig_hash or file2_hash == orig_hash:
152 if os.path.exists(path):
153 os.remove(path)
154 __remove_files(orig_hash, file1_hash, file2_hash)
155 return os.system('git-update-index --remove -- %s' % path)
156 # file deleted in one and changed in the other
157 else:
158 # Do something here - we must at least merge the entry in
159 # the cache, instead of leaving it in U(nmerged) state. In
160 # fact, stg resolved does not handle that.
161
162 # Do the same thing cogito does - remove the file in any case.
163 os.system('git-update-index --remove -- %s' % path)
164
165 #if file1_hash:
166 ## file deleted upstream and changed in the patch. The
167 ## patch is probably going to move the changes
168 ## elsewhere.
169
170 #os.system('git-update-index --remove -- %s' % path)
171 #else:
172 ## file deleted in the patch and changed upstream. We
173 ## could re-delete it, but for now leave it there -
174 ## and let the user check if he still wants to remove
175 ## the file.
176
177 ## reset the cache to the first branch
178 #os.system('git-update-index --cacheinfo %s %s %s'
179 # % (file1_mode, file1_hash, path))
180 __conflict(path)
181 return 1
182
183 # file does not exist in origin
184 else:
185 # file added in both
186 if file1_hash and file2_hash:
187 # files are the same
188 if file1_hash == file2_hash:
189 if os.system('git-update-index --add --cacheinfo %s %s %s'
190 % (file1_mode, file1_hash, path)) != 0:
191 print >> sys.stderr, 'Error: git-update-index failed'
192 __conflict(path)
193 return 1
194 if os.system('git-checkout-index -u -f -- %s' % path):
195 print >> sys.stderr, 'Error: git-checkout-index failed'
196 __conflict(path)
197 return 1
198 if file1_mode != file2_mode:
199 print >> sys.stderr, \
200 'Error: File "s" added in both, ' \
201 'permissions conflict' % path
202 __conflict(path)
203 return 1
204 # files are different
205 else:
206 # reset the index to the current file
207 os.system('git-update-index -- %s' % path)
208 print >> sys.stderr, \
209 'Error: File "%s" added in branches but different' % path
210 __conflict(path)
211 return 1
212 # file added in one
213 elif file1_hash or file2_hash:
214 if file1_hash:
215 mode = file1_mode
216 obj = file1_hash
217 else:
218 mode = file2_mode
219 obj = file2_hash
220 if os.system('git-update-index --add --cacheinfo %s %s %s'
221 % (mode, obj, path)) != 0:
222 print >> sys.stderr, 'Error: git-update-index failed'
223 __conflict(path)
224 return 1
225 __remove_files(orig_hash, file1_hash, file2_hash)
226 return os.system('git-checkout-index -u -f -- %s' % path)
227
228 # Unhandled case
229 print >> sys.stderr, 'Error: Unhandled merge conflict: ' \
230 '"%s" "%s" "%s" "%s" "%s" "%s" "%s"' \
231 % (orig_hash, file1_hash, file2_hash,
232 path,
233 orig_mode, file1_mode, file2_mode)
234 __conflict(path)
235 return 1