[PATCH] git on OpenBSD
[git/jrn.git] / git-merge-recursive.py
blob689f91430be5d3ed777f0e371025545450c3ec59
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 # The actual merge code
11 # ---------------------
13 originalIndexFile = os.environ.get('GIT_INDEX_FILE',
14 os.environ.get('GIT_DIR', '.git') + '/index')
15 temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
16 '/merge-recursive-tmp-index'
17 def setupIndex(temporary):
18 try:
19 os.unlink(temporaryIndexFile)
20 except OSError:
21 pass
22 if temporary:
23 newIndex = temporaryIndexFile
24 os.environ
25 else:
26 newIndex = originalIndexFile
27 os.environ['GIT_INDEX_FILE'] = newIndex
29 def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
30 '''Merge the commits h1 and h2, return the resulting virtual
31 commit object and a flag indicating the cleaness of the merge.'''
32 assert(isinstance(h1, Commit) and isinstance(h2, Commit))
33 assert(isinstance(graph, Graph))
35 def infoMsg(*args):
36 sys.stdout.write(' '*callDepth)
37 printList(args)
38 infoMsg('Merging:')
39 infoMsg(h1)
40 infoMsg(h2)
41 sys.stdout.flush()
43 ca = getCommonAncestors(graph, h1, h2)
44 infoMsg('found', len(ca), 'common ancestor(s):')
45 for x in ca:
46 infoMsg(x)
47 sys.stdout.flush()
49 Ms = ca[0]
50 for h in ca[1:]:
51 [Ms, ignore] = merge(Ms, h,
52 'Temporary shared merge branch 1',
53 'Temporary shared merge branch 2',
54 graph, callDepth+1)
55 assert(isinstance(Ms, Commit))
57 if callDepth == 0:
58 setupIndex(False)
59 cleanCache = False
60 else:
61 setupIndex(True)
62 runProgram(['git-read-tree', h1.tree()])
63 cleanCache = True
65 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
66 branch1Name, branch2Name,
67 cleanCache)
69 if clean or cleanCache:
70 res = Commit(None, [h1, h2], tree=shaRes)
71 graph.addNode(res)
72 else:
73 res = None
75 return [res, clean]
77 getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
78 def getFilesAndDirs(tree):
79 files = Set()
80 dirs = Set()
81 out = runProgram(['git-ls-tree', '-r', '-z', tree])
82 for l in out.split('\0'):
83 m = getFilesRE.match(l)
84 if m:
85 if m.group(2) == 'tree':
86 dirs.add(m.group(4))
87 elif m.group(2) == 'blob':
88 files.add(m.group(4))
90 return [files, dirs]
92 class CacheEntry:
93 def __init__(self, path):
94 class Stage:
95 def __init__(self):
96 self.sha1 = None
97 self.mode = None
99 self.stages = [Stage(), Stage(), Stage()]
100 self.path = path
102 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
103 def unmergedCacheEntries():
104 '''Create a dictionary mapping file names to CacheEntry
105 objects. The dictionary contains one entry for every path with a
106 non-zero stage entry.'''
108 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
109 lines.pop()
111 res = {}
112 for l in lines:
113 m = unmergedRE.match(l)
114 if m:
115 mode = int(m.group(1), 8)
116 sha1 = m.group(2)
117 stage = int(m.group(3)) - 1
118 path = m.group(4)
120 if res.has_key(path):
121 e = res[path]
122 else:
123 e = CacheEntry(path)
124 res[path] = e
126 e.stages[stage].mode = mode
127 e.stages[stage].sha1 = sha1
128 else:
129 die('Error: Merge program failed: Unexpected output from', \
130 'git-ls-files:', l)
131 return res
133 def mergeTrees(head, merge, common, branch1Name, branch2Name,
134 cleanCache):
135 '''Merge the trees 'head' and 'merge' with the common ancestor
136 'common'. The name of the head branch is 'branch1Name' and the name of
137 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
138 where tree is the resulting tree and cleanMerge is True iff the
139 merge was clean.'''
141 assert(isSha(head) and isSha(merge) and isSha(common))
143 if common == merge:
144 print 'Already uptodate!'
145 return [head, True]
147 if cleanCache:
148 updateArg = '-i'
149 else:
150 updateArg = '-u'
152 [out, code] = runProgram(['git-read-tree', updateArg, '-m', common, head, merge], returnCode = True)
153 if code != 0:
154 die('git-read-tree:', out)
156 cleanMerge = True
158 [tree, code] = runProgram('git-write-tree', returnCode=True)
159 tree = tree.rstrip()
160 if code != 0:
161 [files, dirs] = getFilesAndDirs(head)
162 [filesM, dirsM] = getFilesAndDirs(merge)
163 files.union_update(filesM)
164 dirs.union_update(dirsM)
166 cleanMerge = True
167 entries = unmergedCacheEntries()
168 for name in entries:
169 if not processEntry(entries[name], branch1Name, branch2Name,
170 files, dirs, cleanCache):
171 cleanMerge = False
173 if cleanMerge or cleanCache:
174 tree = runProgram('git-write-tree').rstrip()
175 else:
176 tree = None
177 else:
178 cleanMerge = True
180 return [tree, cleanMerge]
182 def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
183 '''Merge one cache entry. 'files' is a Set with the files in both of
184 the heads that we are going to merge. 'dirs' contains the
185 corresponding data for directories. If 'cleanCache' is True no
186 non-zero stages will be left in the cache for the path
187 corresponding to the entry 'entry'.'''
189 # cleanCache == True => Don't leave any non-stage 0 entries in the cache and
190 # don't update the working directory
191 # False => Leave unmerged entries and update the working directory
193 # clean == True => non-conflict case
194 # False => conflict case
196 # If cleanCache == False then the cache shouldn't be updated if clean == False
198 def updateFile(clean, sha, mode, path, onlyWd=False):
199 updateCache = not onlyWd and (cleanCache or (not cleanCache and clean))
200 updateWd = onlyWd or (not cleanCache and clean)
202 if updateWd:
203 prog = ['git-cat-file', 'blob', sha]
204 if stat.S_ISREG(mode):
205 try:
206 os.unlink(path)
207 except OSError:
208 pass
209 if mode & 0100:
210 mode = 0777
211 else:
212 mode = 0666
213 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
214 proc = subprocess.Popen(prog, stdout=fd)
215 proc.wait()
216 os.close(fd)
217 elif stat.S_ISLNK(mode):
218 linkTarget = runProgram(prog)
219 os.symlink(linkTarget, path)
220 else:
221 assert(False)
223 if updateWd and updateCache:
224 runProgram(['git-update-index', '--add', '--', path])
225 elif updateCache:
226 runProgram(['git-update-index', '--add', '--cacheinfo',
227 '0%o' % mode, sha, path])
229 def removeFile(clean, path):
230 if cleanCache or (not cleanCache and clean):
231 runProgram(['git-update-index', '--force-remove', '--', path])
233 if not cleanCache and clean:
234 try:
235 os.unlink(path)
236 except OSError, e:
237 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
238 raise
240 def uniquePath(path, branch):
241 newPath = path + '_' + branch
242 suffix = 0
243 while newPath in files or newPath in dirs:
244 suffix += 1
245 newPath = path + '_' + branch + '_' + str(suffix)
246 files.add(newPath)
247 return newPath
249 debug('processing', entry.path, 'clean cache:', cleanCache)
251 cleanMerge = True
253 path = entry.path
254 oSha = entry.stages[0].sha1
255 oMode = entry.stages[0].mode
256 aSha = entry.stages[1].sha1
257 aMode = entry.stages[1].mode
258 bSha = entry.stages[2].sha1
259 bMode = entry.stages[2].mode
261 assert(oSha == None or isSha(oSha))
262 assert(aSha == None or isSha(aSha))
263 assert(bSha == None or isSha(bSha))
265 assert(oMode == None or type(oMode) is int)
266 assert(aMode == None or type(aMode) is int)
267 assert(bMode == None or type(bMode) is int)
269 if (oSha and (not aSha or not bSha)):
271 # Case A: Deleted in one
273 if (not aSha and not bSha) or \
274 (aSha == oSha and not bSha) or \
275 (not aSha and bSha == oSha):
276 # Deleted in both or deleted in one and unchanged in the other
277 if aSha:
278 print 'Removing ' + path
279 removeFile(True, path)
280 else:
281 # Deleted in one and changed in the other
282 cleanMerge = False
283 if not aSha:
284 print 'CONFLICT (del/mod): "' + path + '" deleted in', \
285 branch1Name, 'and modified in', branch2Name, \
286 '. Version', branch2Name, ' of "' + path + \
287 '" left in tree'
288 mode = bMode
289 sha = bSha
290 else:
291 print 'CONFLICT (mod/del): "' + path + '" deleted in', \
292 branch2Name, 'and modified in', branch1Name + \
293 '. Version', branch1Name, 'of "' + path + \
294 '" left in tree'
295 mode = aMode
296 sha = aSha
298 updateFile(False, sha, mode, path)
300 elif (not oSha and aSha and not bSha) or \
301 (not oSha and not aSha and bSha):
303 # Case B: Added in one.
305 if aSha:
306 addBranch = branch1Name
307 otherBranch = branch2Name
308 mode = aMode
309 sha = aSha
310 conf = 'file/dir'
311 else:
312 addBranch = branch2Name
313 otherBranch = branch1Name
314 mode = bMode
315 sha = bSha
316 conf = 'dir/file'
318 if path in dirs:
319 cleanMerge = False
320 newPath = uniquePath(path, addBranch)
321 print 'CONFLICT (' + conf + \
322 '): There is a directory with name "' + path + '" in', \
323 otherBranch + '. Adding "' + path + '" as "' + newPath + '"'
325 removeFile(False, path)
326 path = newPath
327 else:
328 print 'Adding "' + path + '"'
330 updateFile(True, sha, mode, path)
332 elif not oSha and aSha and bSha:
334 # Case C: Added in both (check for same permissions).
336 if aSha == bSha:
337 if aMode != bMode:
338 cleanMerge = False
339 print 'CONFLICT: File "' + path + \
340 '" added identically in both branches,', \
341 'but permissions conflict', '0%o' % aMode, '->', \
342 '0%o' % bMode
343 print 'CONFLICT: adding with permission:', '0%o' % aMode
345 updateFile(False, aSha, aMode, path)
346 else:
347 # This case is handled by git-read-tree
348 assert(False)
349 else:
350 cleanMerge = False
351 newPath1 = uniquePath(path, branch1Name)
352 newPath2 = uniquePath(path, branch2Name)
353 print 'CONFLICT (add/add): File "' + path + \
354 '" added non-identically in both branches.'
355 removeFile(False, path)
356 updateFile(False, aSha, aMode, newPath1)
357 updateFile(False, bSha, bMode, newPath2)
359 elif oSha and aSha and bSha:
361 # case D: Modified in both, but differently.
363 print 'Auto-merging', path
364 orig = runProgram(['git-unpack-file', oSha]).rstrip()
365 src1 = runProgram(['git-unpack-file', aSha]).rstrip()
366 src2 = runProgram(['git-unpack-file', bSha]).rstrip()
367 [out, ret] = runProgram(['merge',
368 '-L', branch1Name + '/' + path,
369 '-L', 'orig/' + path,
370 '-L', branch2Name + '/' + path,
371 src1, orig, src2], returnCode=True)
373 if aMode == oMode:
374 mode = bMode
375 else:
376 mode = aMode
378 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
379 src1]).rstrip()
381 if ret != 0:
382 cleanMerge = False
383 print 'CONFLICT (content): Merge conflict in "' + path + '".'
385 if cleanCache:
386 updateFile(False, sha, mode, path)
387 else:
388 updateFile(True, aSha, aMode, path)
389 updateFile(False, sha, mode, path, True)
390 else:
391 updateFile(True, sha, mode, path)
393 os.unlink(orig)
394 os.unlink(src1)
395 os.unlink(src2)
396 else:
397 die("ERROR: Fatal merge failure, shouldn't happen.")
399 return cleanMerge
401 def usage():
402 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
404 # main entry point as merge strategy module
405 # The first parameters up to -- are merge bases, and the rest are heads.
406 # This strategy module figures out merge bases itself, so we only
407 # get heads.
409 if len(sys.argv) < 4:
410 usage()
412 for nextArg in xrange(1, len(sys.argv)):
413 if sys.argv[nextArg] == '--':
414 if len(sys.argv) != nextArg + 3:
415 die('Not handling anything other than two heads merge.')
416 try:
417 h1 = firstBranch = sys.argv[nextArg + 1]
418 h2 = secondBranch = sys.argv[nextArg + 2]
419 except IndexError:
420 usage()
421 break
423 print 'Merging', h1, 'with', h2
425 try:
426 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
427 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
429 graph = buildGraph([h1, h2])
431 [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
432 firstBranch, secondBranch, graph)
434 print ''
435 except:
436 if isinstance(sys.exc_info()[1], SystemExit):
437 raise
438 else:
439 traceback.print_exc(None, sys.stderr)
440 sys.exit(2)
442 if clean:
443 sys.exit(0)
444 else:
445 sys.exit(1)