[PATCH] Introduce a 'die' function.
[git/jrn.git] / git-merge-fredrik.py
blobe4c6fe3c61ba3a02045bb7efe0f1f482c7a393d1
1 #!/usr/bin/python
3 import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
4 from heapq import heappush, heappop
5 from sets import Set
7 sys.path.append('@@GIT_PYTHON_PATH@@')
8 from gitMergeCommon import *
10 alwaysWriteTree = False
12 # The actual merge code
13 # ---------------------
15 def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
16 '''Merge the commits h1 and h2, return the resulting virtual
17 commit object and a flag indicating the cleaness of the merge.'''
18 assert(isinstance(h1, Commit) and isinstance(h2, Commit))
19 assert(isinstance(graph, Graph))
21 def infoMsg(*args):
22 sys.stdout.write(' '*callDepth)
23 printList(args)
24 infoMsg('Merging:')
25 infoMsg(h1)
26 infoMsg(h2)
27 sys.stdout.flush()
29 ca = getCommonAncestors(graph, h1, h2)
30 infoMsg('found', len(ca), 'common ancestor(s):')
31 for x in ca:
32 infoMsg(x)
33 sys.stdout.flush()
35 Ms = ca[0]
36 for h in ca[1:]:
37 [Ms, ignore] = merge(Ms, h,
38 'Temporary shared merge branch 1',
39 'Temporary shared merge branch 2',
40 graph, callDepth+1)
41 assert(isinstance(Ms, Commit))
43 if callDepth == 0:
44 if len(ca) > 1:
45 runProgram(['git-read-tree', h1.tree()])
46 runProgram(['git-update-cache', '-q', '--refresh'])
47 # Use the original index if we only have one common ancestor
49 updateWd = True
50 if alwaysWriteTree:
51 cleanCache = True
52 else:
53 cleanCache = False
54 else:
55 runProgram(['git-read-tree', h1.tree()])
56 updateWd = False
57 cleanCache = True
59 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
60 branch1Name, branch2Name,
61 cleanCache, updateWd)
63 if clean or cleanCache:
64 res = Commit(None, [h1, h2], tree=shaRes)
65 graph.addNode(res)
66 else:
67 res = None
69 return [res, clean]
71 getFilesRE = re.compile('([0-9]+) ([a-z0-9]+) ([0-9a-f]{40})\t(.*)')
72 def getFilesAndDirs(tree):
73 files = Set()
74 dirs = Set()
75 out = runProgram(['git-ls-tree', '-r', '-z', tree])
76 for l in out.split('\0'):
77 m = getFilesRE.match(l)
78 if m:
79 if m.group(2) == 'tree':
80 dirs.add(m.group(4))
81 elif m.group(2) == 'blob':
82 files.add(m.group(4))
84 return [files, dirs]
86 class CacheEntry:
87 def __init__(self, path):
88 class Stage:
89 def __init__(self):
90 self.sha1 = None
91 self.mode = None
93 self.stages = [Stage(), Stage(), Stage()]
94 self.path = path
96 unmergedRE = re.compile('^([0-9]+) ([0-9a-f]{40}) ([1-3])\t(.*)$')
97 def unmergedCacheEntries():
98 '''Create a dictionary mapping file names to CacheEntry
99 objects. The dictionary contains one entry for every path with a
100 non-zero stage entry.'''
102 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
103 lines.pop()
105 res = {}
106 for l in lines:
107 m = unmergedRE.match(l)
108 if m:
109 mode = int(m.group(1), 8)
110 sha1 = m.group(2)
111 stage = int(m.group(3)) - 1
112 path = m.group(4)
114 if res.has_key(path):
115 e = res[path]
116 else:
117 e = CacheEntry(path)
118 res[path] = e
120 e.stages[stage].mode = mode
121 e.stages[stage].sha1 = sha1
122 else:
123 die('Error: Merge program failed: Unexpected output from', \
124 'git-ls-files:', l)
125 return res
127 def mergeTrees(head, merge, common, branch1Name, branch2Name,
128 cleanCache, updateWd):
129 '''Merge the trees 'head' and 'merge' with the common ancestor
130 'common'. The name of the head branch is 'branch1Name' and the name of
131 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
132 where tree is the resulting tree and cleanMerge is True iff the
133 merge was clean.'''
135 assert(isSha(head) and isSha(merge) and isSha(common))
137 if common == merge:
138 print 'Already uptodate!'
139 return [head, True]
141 if updateWd:
142 updateArg = '-u'
143 else:
144 updateArg = '-i'
145 runProgram(['git-read-tree', updateArg, '-m', common, head, merge])
146 cleanMerge = True
148 [tree, code] = runProgram('git-write-tree', returnCode=True)
149 tree = tree.rstrip()
150 if code != 0:
151 [files, dirs] = getFilesAndDirs(head)
152 [filesM, dirsM] = getFilesAndDirs(merge)
153 files.union_update(filesM)
154 dirs.union_update(dirsM)
156 cleanMerge = True
157 entries = unmergedCacheEntries()
158 for name in entries:
159 if not processEntry(entries[name], branch1Name, branch2Name,
160 files, dirs, cleanCache, updateWd):
161 cleanMerge = False
163 if cleanMerge or cleanCache:
164 tree = runProgram('git-write-tree').rstrip()
165 else:
166 tree = None
167 else:
168 cleanMerge = True
170 return [tree, cleanMerge]
172 def processEntry(entry, branch1Name, branch2Name, files, dirs,
173 cleanCache, updateWd):
174 '''Merge one cache entry. 'files' is a Set with the files in both of
175 the heads that we are going to merge. 'dirs' contains the
176 corresponding data for directories. If 'cleanCache' is True no
177 non-zero stages will be left in the cache for the path
178 corresponding to the entry 'entry'.'''
180 # cleanCache == True => Don't leave any non-stage 0 entries in the cache.
181 # False => Leave unmerged entries
183 # updateWd == True => Update the working directory to correspond to the cache
184 # False => Leave the working directory unchanged
186 # clean == True => non-conflict case
187 # False => conflict case
189 # If cleanCache == False then the cache shouldn't be updated if clean == False
191 def updateFile(clean, sha, mode, path):
192 if cleanCache or (not cleanCache and clean):
193 runProgram(['git-update-cache', '--add', '--cacheinfo',
194 '0%o' % mode, sha, path])
196 if updateWd:
197 prog = ['git-cat-file', 'blob', sha]
198 if stat.S_ISREG(mode):
199 try:
200 os.unlink(path)
201 except OSError:
202 pass
203 if mode & 0100:
204 mode = 0777
205 else:
206 mode = 0666
207 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
208 proc = subprocess.Popen(prog, stdout=fd)
209 proc.wait()
210 os.close(fd)
211 elif stat.S_ISLNK(mode):
212 linkTarget = runProgram(prog)
213 os.symlink(linkTarget, path)
214 else:
215 assert(False)
216 runProgram(['git-update-cache', '--', path])
218 def removeFile(clean, path):
219 if cleanCache or (not cleanCache and clean):
220 runProgram(['git-update-cache', '--force-remove', '--', path])
222 if updateWd:
223 try:
224 os.unlink(path)
225 except OSError, e:
226 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
227 raise
229 def uniquePath(path, branch):
230 newPath = path + '_' + branch
231 suffix = 0
232 while newPath in files or newPath in dirs:
233 suffix += 1
234 newPath = path + '_' + branch + '_' + str(suffix)
235 files.add(newPath)
236 return newPath
238 debug('processing', entry.path, 'clean cache:', cleanCache,
239 'wd:', updateWd)
241 cleanMerge = True
243 path = entry.path
244 oSha = entry.stages[0].sha1
245 oMode = entry.stages[0].mode
246 aSha = entry.stages[1].sha1
247 aMode = entry.stages[1].mode
248 bSha = entry.stages[2].sha1
249 bMode = entry.stages[2].mode
251 assert(oSha == None or isSha(oSha))
252 assert(aSha == None or isSha(aSha))
253 assert(bSha == None or isSha(bSha))
255 assert(oMode == None or type(oMode) is int)
256 assert(aMode == None or type(aMode) is int)
257 assert(bMode == None or type(bMode) is int)
259 if (oSha and (not aSha or not bSha)):
261 # Case A: Deleted in one
263 if (not aSha and not bSha) or \
264 (aSha == oSha and not bSha) or \
265 (not aSha and bSha == oSha):
266 # Deleted in both or deleted in one and unchanged in the other
267 if aSha:
268 print 'Removing ' + path
269 removeFile(True, path)
270 else:
271 # Deleted in one and changed in the other
272 cleanMerge = False
273 if not aSha:
274 print 'CONFLICT (del/mod): "' + path + '" deleted in', \
275 branch1Name, 'and modified in', branch2Name, \
276 '. Version', branch2Name, ' of "' + path + \
277 '" left in tree'
278 mode = bMode
279 sha = bSha
280 else:
281 print 'CONFLICT (mod/del): "' + path + '" deleted in', \
282 branch2Name, 'and modified in', branch1Name + \
283 '. Version', branch1Name, 'of "' + path + \
284 '" left in tree'
285 mode = aMode
286 sha = aSha
288 updateFile(False, sha, mode, path)
290 elif (not oSha and aSha and not bSha) or \
291 (not oSha and not aSha and bSha):
293 # Case B: Added in one.
295 if aSha:
296 addBranch = branch1Name
297 otherBranch = branch2Name
298 mode = aMode
299 sha = aSha
300 conf = 'file/dir'
301 else:
302 addBranch = branch2Name
303 otherBranch = branch1Name
304 mode = bMode
305 sha = bSha
306 conf = 'dir/file'
308 if path in dirs:
309 cleanMerge = False
310 newPath = uniquePath(path, addBranch)
311 print 'CONFLICT (' + conf + \
312 '): There is a directory with name "' + path + '" in', \
313 otherBranch + '. Adding "' + path + '" as "' + newPath + '"'
315 removeFile(False, path)
316 path = newPath
317 else:
318 print 'Adding "' + path + '"'
320 updateFile(True, sha, mode, path)
322 elif not oSha and aSha and bSha:
324 # Case C: Added in both (check for same permissions).
326 if aSha == bSha:
327 if aMode != bMode:
328 cleanMerge = False
329 print 'CONFLICT: File "' + path + \
330 '" added identically in both branches,'
331 print 'CONFLICT: but permissions conflict', '0%o' % aMode, \
332 '->', '0%o' % bMode
333 print 'CONFLICT: adding with permission:', '0%o' % aMode
335 updateFile(False, aSha, aMode, path)
336 else:
337 # This case is handled by git-read-tree
338 assert(False)
339 else:
340 cleanMerge = False
341 newPath1 = uniquePath(path, branch1Name)
342 newPath2 = uniquePath(path, branch2Name)
343 print 'CONFLICT (add/add): File "' + path + \
344 '" added non-identically in both branches.', \
345 'Adding "' + newPath1 + '" and "' + newPath2 + '" instead.'
346 removeFile(False, path)
347 updateFile(False, aSha, aMode, newPath1)
348 updateFile(False, bSha, bMode, newPath2)
350 elif oSha and aSha and bSha:
352 # case D: Modified in both, but differently.
354 print 'Auto-merging', path
355 orig = runProgram(['git-unpack-file', oSha]).rstrip()
356 src1 = runProgram(['git-unpack-file', aSha]).rstrip()
357 src2 = runProgram(['git-unpack-file', bSha]).rstrip()
358 [out, ret] = runProgram(['merge',
359 '-L', branch1Name + '/' + path,
360 '-L', 'orig/' + path,
361 '-L', branch2Name + '/' + path,
362 src1, orig, src2], returnCode=True)
364 if aMode == oMode:
365 mode = bMode
366 else:
367 mode = aMode
369 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
370 src1]).rstrip()
372 if ret != 0:
373 cleanMerge = False
374 print 'CONFLICT (content): Merge conflict in "' + path + '".'
375 updateFile(False, sha, mode, path)
376 else:
377 updateFile(True, sha, mode, path)
379 os.unlink(orig)
380 os.unlink(src1)
381 os.unlink(src2)
382 else:
383 die("ERROR: Fatal merge failure, shouldn't happen.")
385 return cleanMerge
387 def usage():
388 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
390 # main entry point as merge strategy module
391 # The first parameters up to -- are merge bases, and the rest are heads.
392 # This strategy module figures out merge bases itself, so we only
393 # get heads.
395 for nextArg in xrange(1, len(sys.argv)):
396 if sys.argv[nextArg] == '--':
397 if len(sys.argv) != nextArg + 3:
398 die('Not handling anything other than two heads merge.')
399 try:
400 h1 = firstBranch = sys.argv[nextArg + 1]
401 h2 = secondBranch = sys.argv[nextArg + 2]
402 except IndexError:
403 usage()
404 break
406 print 'Merging', h1, 'with', h2
408 try:
409 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
410 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
412 graph = buildGraph([h1, h2])
414 [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
415 firstBranch, secondBranch, graph)
417 print ''
418 except:
419 traceback.print_exc(None, sys.stderr)
420 sys.exit(2)
422 if clean:
423 sys.exit(0)
424 else:
425 print 'Automatic merge failed, fix up by hand'
426 sys.exit(1)