merge-recursive: Only print relevant rename messages
[git.git] / git-merge-recursive.py
1 #!/usr/bin/python
2
3 import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
4 from heapq import heappush, heappop
5 from sets import Set
6
7 sys.path.append('''@@GIT_PYTHON_PATH@@''')
8 from gitMergeCommon import *
9
10 originalIndexFile = os.environ.get('GIT_INDEX_FILE',
11                                    os.environ.get('GIT_DIR', '.git') + '/index')
12 temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
13                      '/merge-recursive-tmp-index'
14 def setupIndex(temporary):
15     try:
16         os.unlink(temporaryIndexFile)
17     except OSError:
18         pass
19     if temporary:
20         newIndex = temporaryIndexFile
21     else:
22         newIndex = originalIndexFile
23     os.environ['GIT_INDEX_FILE'] = newIndex
24
25 # This is a global variable which is used in a number of places but
26 # only written to in the 'merge' function.
27
28 # cacheOnly == True  => Don't leave any non-stage 0 entries in the cache and
29 #                       don't update the working directory.
30 #              False => Leave unmerged entries in the cache and update
31 #                       the working directory.
32
33 cacheOnly = False
34
35 # The entry point to the merge code
36 # ---------------------------------
37
38 def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
39     '''Merge the commits h1 and h2, return the resulting virtual
40     commit object and a flag indicating the cleaness of the merge.'''
41     assert(isinstance(h1, Commit) and isinstance(h2, Commit))
42     assert(isinstance(graph, Graph))
43
44     def infoMsg(*args):
45         sys.stdout.write('  '*callDepth)
46         printList(args)
47
48     infoMsg('Merging:')
49     infoMsg(h1)
50     infoMsg(h2)
51     sys.stdout.flush()
52
53     ca = getCommonAncestors(graph, h1, h2)
54     infoMsg('found', len(ca), 'common ancestor(s):')
55     for x in ca:
56         infoMsg(x)
57     sys.stdout.flush()
58
59     mergedCA = ca[0]
60     for h in ca[1:]:
61         [mergedCA, dummy] = merge(mergedCA, h,
62                                   'Temporary shared merge branch 1',
63                                   'Temporary shared merge branch 2',
64                                   graph, callDepth+1)
65         assert(isinstance(mergedCA, Commit))
66
67     global cacheOnly
68     if callDepth == 0:
69         setupIndex(False)
70         cacheOnly = False
71     else:
72         setupIndex(True)
73         runProgram(['git-read-tree', h1.tree()])
74         cacheOnly = True
75
76     [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
77                                  branch1Name, branch2Name)
78
79     if clean or cacheOnly:
80         res = Commit(None, [h1, h2], tree=shaRes)
81         graph.addNode(res)
82     else:
83         res = None
84
85     return [res, clean]
86
87 getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
88 def getFilesAndDirs(tree):
89     files = Set()
90     dirs = Set()
91     out = runProgram(['git-ls-tree', '-r', '-z', tree])
92     for l in out.split('\0'):
93         m = getFilesRE.match(l)
94         if m:
95             if m.group(2) == 'tree':
96                 dirs.add(m.group(4))
97             elif m.group(2) == 'blob':
98                 files.add(m.group(4))
99
100     return [files, dirs]
101
102 # Those two global variables are used in a number of places but only
103 # written to in 'mergeTrees' and 'uniquePath'. They keep track of
104 # every file and directory in the two branches that are about to be
105 # merged.
106 currentFileSet = None
107 currentDirectorySet = None
108
109 def mergeTrees(head, merge, common, branch1Name, branch2Name):
110     '''Merge the trees 'head' and 'merge' with the common ancestor
111     'common'. The name of the head branch is 'branch1Name' and the name of
112     the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
113     where tree is the resulting tree and cleanMerge is True iff the
114     merge was clean.'''
115     
116     assert(isSha(head) and isSha(merge) and isSha(common))
117
118     if common == merge:
119         print 'Already uptodate!'
120         return [head, True]
121
122     if cacheOnly:
123         updateArg = '-i'
124     else:
125         updateArg = '-u'
126
127     [out, code] = runProgram(['git-read-tree', updateArg, '-m',
128                                 common, head, merge], returnCode = True)
129     if code != 0:
130         die('git-read-tree:', out)
131
132     [tree, code] = runProgram('git-write-tree', returnCode=True)
133     tree = tree.rstrip()
134     if code != 0:
135         global currentFileSet, currentDirectorySet
136         [currentFileSet, currentDirectorySet] = getFilesAndDirs(head)
137         [filesM, dirsM] = getFilesAndDirs(merge)
138         currentFileSet.union_update(filesM)
139         currentDirectorySet.union_update(dirsM)
140
141         entries = unmergedCacheEntries()
142         renamesHead =  getRenames(head, common, head, merge, entries)
143         renamesMerge = getRenames(merge, common, head, merge, entries)
144
145         cleanMerge = processRenames(renamesHead, renamesMerge,
146                                     branch1Name, branch2Name)
147         for entry in entries:
148             if entry.processed:
149                 continue
150             if not processEntry(entry, branch1Name, branch2Name):
151                 cleanMerge = False
152                 
153         if cleanMerge or cacheOnly:
154             tree = runProgram('git-write-tree').rstrip()
155         else:
156             tree = None
157     else:
158         cleanMerge = True
159
160     return [tree, cleanMerge]
161
162 # Low level file merging, update and removal
163 # ------------------------------------------
164
165 MERGE_NONE = 0
166 MERGE_TRIVIAL = 1
167 MERGE_3WAY = 2
168 def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
169               branch1Name, branch2Name):
170
171     merge = MERGE_NONE
172     clean = True
173
174     if stat.S_IFMT(aMode) != stat.S_IFMT(bMode):
175         clean = False
176         if stat.S_ISREG(aMode):
177             mode = aMode
178             sha = aSha
179         else:
180             mode = bMode
181             sha = bSha
182     else:
183         if aSha != oSha and bSha != oSha:
184             merge = MERGE_TRIVIAL
185
186         if aMode == oMode:
187             mode = bMode
188         else:
189             mode = aMode
190
191         if aSha == oSha:
192             sha = bSha
193         elif bSha == oSha:
194             sha = aSha
195         elif stat.S_ISREG(aMode):
196             assert(stat.S_ISREG(bMode))
197
198             orig = runProgram(['git-unpack-file', oSha]).rstrip()
199             src1 = runProgram(['git-unpack-file', aSha]).rstrip()
200             src2 = runProgram(['git-unpack-file', bSha]).rstrip()
201             [out, code] = runProgram(['merge',
202                                       '-L', branch1Name + '/' + aPath,
203                                       '-L', 'orig/' + oPath,
204                                       '-L', branch2Name + '/' + bPath,
205                                       src1, orig, src2], returnCode=True)
206
207             sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
208                               src1]).rstrip()
209
210             os.unlink(orig)
211             os.unlink(src1)
212             os.unlink(src2)
213
214             merge = MERGE_3WAY
215             clean = (code == 0)
216         else:
217             assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
218             sha = aSha
219
220             if aSha != bSha:
221                 clean = False
222
223     return [sha, mode, clean, merge]
224
225 def updateFile(clean, sha, mode, path):
226     updateCache = cacheOnly or clean
227     updateWd = not cacheOnly
228
229     return updateFileExt(sha, mode, path, updateCache, updateWd)
230
231 def updateFileExt(sha, mode, path, updateCache, updateWd):
232     if cacheOnly:
233         updateWd = False
234
235     if updateWd:
236         pathComponents = path.split('/')
237         for x in xrange(1, len(pathComponents)):
238             p = '/'.join(pathComponents[0:x])
239
240             try:
241                 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
242             except: 
243                 createDir = True
244             
245             if createDir:
246                 try:
247                     os.mkdir(p)
248                 except OSError, e:
249                     die("Couldn't create directory", p, e.strerror)
250
251         prog = ['git-cat-file', 'blob', sha]
252         if stat.S_ISREG(mode):
253             try:
254                 os.unlink(path)
255             except OSError:
256                 pass
257             if mode & 0100:
258                 mode = 0777
259             else:
260                 mode = 0666
261             fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
262             proc = subprocess.Popen(prog, stdout=fd)
263             proc.wait()
264             os.close(fd)
265         elif stat.S_ISLNK(mode):
266             linkTarget = runProgram(prog)
267             os.symlink(linkTarget, path)
268         else:
269             assert(False)
270
271     if updateWd and updateCache:
272         runProgram(['git-update-index', '--add', '--', path])
273     elif updateCache:
274         runProgram(['git-update-index', '--add', '--cacheinfo',
275                     '0%o' % mode, sha, path])
276
277 def removeFile(clean, path):
278     updateCache = cacheOnly or clean
279     updateWd = not cacheOnly
280
281     if updateCache:
282         runProgram(['git-update-index', '--force-remove', '--', path])
283
284     if updateWd:
285         try:
286             os.unlink(path)
287         except OSError, e:
288             if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
289                 raise
290
291 def uniquePath(path, branch):
292     def fileExists(path):
293         try:
294             os.lstat(path)
295             return True
296         except OSError, e:
297             if e.errno == errno.ENOENT:
298                 return False
299             else:
300                 raise
301
302     newPath = path + '_' + branch
303     suffix = 0
304     while newPath in currentFileSet or \
305           newPath in currentDirectorySet  or \
306           fileExists(newPath):
307         suffix += 1
308         newPath = path + '_' + branch + '_' + str(suffix)
309     currentFileSet.add(newPath)
310     return newPath
311
312 # Cache entry management
313 # ----------------------
314
315 class CacheEntry:
316     def __init__(self, path):
317         class Stage:
318             def __init__(self):
319                 self.sha1 = None
320                 self.mode = None
321
322             # Used for debugging only
323             def __str__(self):
324                 if self.mode != None:
325                     m = '0%o' % self.mode
326                 else:
327                     m = 'None'
328
329                 if self.sha1:
330                     sha1 = self.sha1
331                 else:
332                     sha1 = 'None'
333                 return 'sha1: ' + sha1 + ' mode: ' + m
334         
335         self.stages = [Stage(), Stage(), Stage(), Stage()]
336         self.path = path
337         self.processed = False
338
339     def __str__(self):
340         return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
341
342 class CacheEntryContainer:
343     def __init__(self):
344         self.entries = {}
345
346     def add(self, entry):
347         self.entries[entry.path] = entry
348
349     def get(self, path):
350         return self.entries.get(path)
351
352     def __iter__(self):
353         return self.entries.itervalues()
354     
355 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
356 def unmergedCacheEntries():
357     '''Create a dictionary mapping file names to CacheEntry
358     objects. The dictionary contains one entry for every path with a
359     non-zero stage entry.'''
360
361     lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
362     lines.pop()
363
364     res = CacheEntryContainer()
365     for l in lines:
366         m = unmergedRE.match(l)
367         if m:
368             mode = int(m.group(1), 8)
369             sha1 = m.group(2)
370             stage = int(m.group(3))
371             path = m.group(4)
372
373             e = res.get(path)
374             if not e:
375                 e = CacheEntry(path)
376                 res.add(e)
377
378             e.stages[stage].mode = mode
379             e.stages[stage].sha1 = sha1
380         else:
381             die('Error: Merge program failed: Unexpected output from',
382                 'git-ls-files:', l)
383     return res
384
385 lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
386 def getCacheEntry(path, origTree, aTree, bTree):
387     '''Returns a CacheEntry object which doesn't have to correspond to
388     a real cache entry in Git's index.'''
389     
390     def parse(out):
391         if out == '':
392             return [None, None]
393         else:
394             m = lsTreeRE.match(out)
395             if not m:
396                 die('Unexpected output from git-ls-tree:', out)
397             elif m.group(2) == 'blob':
398                 return [m.group(3), int(m.group(1), 8)]
399             else:
400                 return [None, None]
401
402     res = CacheEntry(path)
403
404     [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
405     [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
406     [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
407
408     res.stages[1].sha1 = oSha
409     res.stages[1].mode = oMode
410     res.stages[2].sha1 = aSha
411     res.stages[2].mode = aMode
412     res.stages[3].sha1 = bSha
413     res.stages[3].mode = bMode
414
415     return res
416
417 # Rename detection and handling
418 # -----------------------------
419
420 class RenameEntry:
421     def __init__(self,
422                  src, srcSha, srcMode, srcCacheEntry,
423                  dst, dstSha, dstMode, dstCacheEntry,
424                  score):
425         self.srcName = src
426         self.srcSha = srcSha
427         self.srcMode = srcMode
428         self.srcCacheEntry = srcCacheEntry
429         self.dstName = dst
430         self.dstSha = dstSha
431         self.dstMode = dstMode
432         self.dstCacheEntry = dstCacheEntry
433         self.score = score
434
435         self.processed = False
436
437 class RenameEntryContainer:
438     def __init__(self):
439         self.entriesSrc = {}
440         self.entriesDst = {}
441
442     def add(self, entry):
443         self.entriesSrc[entry.srcName] = entry
444         self.entriesDst[entry.dstName] = entry
445
446     def getSrc(self, path):
447         return self.entriesSrc.get(path)
448
449     def getDst(self, path):
450         return self.entriesDst.get(path)
451
452     def __iter__(self):
453         return self.entriesSrc.itervalues()
454
455 parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
456 def getRenames(tree, oTree, aTree, bTree, cacheEntries):
457     '''Get information of all renames which occured between 'oTree' and
458     'tree'. We need the three trees in the merge ('oTree', 'aTree' and
459     'bTree') to be able to associate the correct cache entries with
460     the rename information. 'tree' is always equal to either aTree or bTree.'''
461
462     assert(tree == aTree or tree == bTree)
463     inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
464                       '-z', oTree, tree])
465
466     ret = RenameEntryContainer()
467     try:
468         recs = inp.split("\0")
469         recs.pop() # remove last entry (which is '')
470         it = recs.__iter__()
471         while True:
472             rec = it.next()
473             m = parseDiffRenamesRE.match(rec)
474
475             if not m:
476                 die('Unexpected output from git-diff-tree:', rec)
477
478             srcMode = int(m.group(1), 8)
479             dstMode = int(m.group(2), 8)
480             srcSha = m.group(3)
481             dstSha = m.group(4)
482             score = m.group(5)
483             src = it.next()
484             dst = it.next()
485
486             srcCacheEntry = cacheEntries.get(src)
487             if not srcCacheEntry:
488                 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
489                 cacheEntries.add(srcCacheEntry)
490
491             dstCacheEntry = cacheEntries.get(dst)
492             if not dstCacheEntry:
493                 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
494                 cacheEntries.add(dstCacheEntry)
495
496             ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
497                                 dst, dstSha, dstMode, dstCacheEntry,
498                                 score))
499     except StopIteration:
500         pass
501     return ret
502
503 def fmtRename(src, dst):
504     srcPath = src.split('/')
505     dstPath = dst.split('/')
506     path = []
507     endIndex = min(len(srcPath), len(dstPath)) - 1
508     for x in range(0, endIndex):
509         if srcPath[x] == dstPath[x]:
510             path.append(srcPath[x])
511         else:
512             endIndex = x
513             break
514
515     if len(path) > 0:
516         return '/'.join(path) + \
517                '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
518                '/'.join(dstPath[endIndex:]) + '}'
519     else:
520         return src + ' => ' + dst
521
522 def processRenames(renamesA, renamesB, branchNameA, branchNameB):
523     srcNames = Set()
524     for x in renamesA:
525         srcNames.add(x.srcName)
526     for x in renamesB:
527         srcNames.add(x.srcName)
528
529     cleanMerge = True
530     for path in srcNames:
531         if renamesA.getSrc(path):
532             renames1 = renamesA
533             renames2 = renamesB
534             branchName1 = branchNameA
535             branchName2 = branchNameB
536         else:
537             renames1 = renamesB
538             renames2 = renamesA
539             branchName1 = branchNameB
540             branchName2 = branchNameA
541         
542         ren1 = renames1.getSrc(path)
543         ren2 = renames2.getSrc(path)
544
545         ren1.dstCacheEntry.processed = True
546         ren1.srcCacheEntry.processed = True
547
548         if ren1.processed:
549             continue
550
551         ren1.processed = True
552         removeFile(True, ren1.srcName)
553         if ren2:
554             # Renamed in 1 and renamed in 2
555             assert(ren1.srcName == ren2.srcName)
556             ren2.dstCacheEntry.processed = True
557             ren2.processed = True
558
559             if ren1.dstName != ren2.dstName:
560                 print 'CONFLICT (rename/rename): Rename', \
561                       fmtRename(path, ren1.dstName), 'in branch', branchName1, \
562                       'rename', fmtRename(path, ren2.dstName), 'in', branchName2
563                 cleanMerge = False
564
565                 if ren1.dstName in currentDirectorySet:
566                     dstName1 = uniquePath(ren1.dstName, branchName1)
567                     print ren1.dstName, 'is a directory in', branchName2, \
568                           'adding as', dstName1, 'instead.'
569                     removeFile(False, ren1.dstName)
570                 else:
571                     dstName1 = ren1.dstName
572
573                 if ren2.dstName in currentDirectorySet:
574                     dstName2 = uniquePath(ren2.dstName, branchName2)
575                     print ren2.dstName, 'is a directory in', branchName1, \
576                           'adding as', dstName2, 'instead.'
577                     removeFile(False, ren2.dstName)
578                 else:
579                     dstName2 = ren1.dstName
580
581                 updateFile(False, ren1.dstSha, ren1.dstMode, dstName1)
582                 updateFile(False, ren2.dstSha, ren2.dstMode, dstName2)
583             else:
584                 [resSha, resMode, clean, merge] = \
585                          mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
586                                    ren1.dstName, ren1.dstSha, ren1.dstMode,
587                                    ren2.dstName, ren2.dstSha, ren2.dstMode,
588                                    branchName1, branchName2)
589
590                 if merge or not clean:
591                     print 'Renaming', fmtRename(path, ren1.dstName)
592
593                 if merge == MERGE_3WAY:
594                     print 'Auto-merging', ren1.dstName
595
596                 if not clean:
597                     print 'CONFLICT (content): merge conflict in', ren1.dstName
598                     cleanMerge = False
599
600                     if not cacheOnly:
601                         updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
602                                       updateCache=True, updateWd=False)
603                 updateFile(clean, resSha, resMode, ren1.dstName)
604         else:
605             # Renamed in 1, maybe changed in 2
606             if renamesA == renames1:
607                 stage = 3
608             else:
609                 stage = 2
610                 
611             srcShaOtherBranch  = ren1.srcCacheEntry.stages[stage].sha1
612             srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
613
614             dstShaOtherBranch  = ren1.dstCacheEntry.stages[stage].sha1
615             dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
616
617             tryMerge = False
618             
619             if ren1.dstName in currentDirectorySet:
620                 newPath = uniquePath(ren1.dstName, branchName1)
621                 print 'CONFLICT (rename/directory): Rename', \
622                       fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,\
623                       'directory', ren1.dstName, 'added in', branchName2
624                 print 'Renaming', ren1.srcName, 'to', newPath, 'instead'
625                 cleanMerge = False
626                 removeFile(False, ren1.dstName)
627                 updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
628             elif srcShaOtherBranch == None:
629                 print 'CONFLICT (rename/delete): Rename', \
630                       fmtRename(ren1.srcName, ren1.dstName), 'in', \
631                       branchName1, 'and deleted in', branchName2
632                 cleanMerge = False
633                 updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
634             elif dstShaOtherBranch:
635                 newPath = uniquePath(ren1.dstName, branchName2)
636                 print 'CONFLICT (rename/add): Rename', \
637                       fmtRename(ren1.srcName, ren1.dstName), 'in', \
638                       branchName1 + '.', ren1.dstName, 'added in', branchName2
639                 print 'Adding as', newPath, 'instead'
640                 updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
641                 cleanMerge = False
642                 tryMerge = True
643             elif renames2.getDst(ren1.dstName):
644                 dst2 = renames2.getDst(ren1.dstName)
645                 newPath1 = uniquePath(ren1.dstName, branchName1)
646                 newPath2 = uniquePath(dst2.dstName, branchName2)
647                 print 'CONFLICT (rename/rename): Rename', \
648                       fmtRename(ren1.srcName, ren1.dstName), 'in', \
649                       branchName1+'. Rename', \
650                       fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2
651                 print 'Renaming', ren1.srcName, 'to', newPath1, 'and', \
652                       dst2.srcName, 'to', newPath2, 'instead'
653                 removeFile(False, ren1.dstName)
654                 updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
655                 updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
656                 dst2.processed = True
657                 cleanMerge = False
658             else:
659                 tryMerge = True
660
661             if tryMerge:
662                 [resSha, resMode, clean, merge] = \
663                          mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
664                                    ren1.dstName, ren1.dstSha, ren1.dstMode,
665                                    ren1.srcName, srcShaOtherBranch, srcModeOtherBranch,
666                                    branchName1, branchName2)
667
668                 if merge or not clean:
669                     print 'Renaming', fmtRename(ren1.srcName, ren1.dstName)
670
671                 if merge == MERGE_3WAY:
672                     print 'Auto-merging', ren1.dstName
673
674                 if not clean:
675                     print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName
676                     cleanMerge = False
677
678                     if not cacheOnly:
679                         updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
680                                       updateCache=True, updateWd=False)
681                 updateFile(clean, resSha, resMode, ren1.dstName)
682
683     return cleanMerge
684
685 # Per entry merge function
686 # ------------------------
687
688 def processEntry(entry, branch1Name, branch2Name):
689     '''Merge one cache entry.'''
690
691     debug('processing', entry.path, 'clean cache:', cacheOnly)
692
693     cleanMerge = True
694
695     path = entry.path
696     oSha = entry.stages[1].sha1
697     oMode = entry.stages[1].mode
698     aSha = entry.stages[2].sha1
699     aMode = entry.stages[2].mode
700     bSha = entry.stages[3].sha1
701     bMode = entry.stages[3].mode
702
703     assert(oSha == None or isSha(oSha))
704     assert(aSha == None or isSha(aSha))
705     assert(bSha == None or isSha(bSha))
706
707     assert(oMode == None or type(oMode) is int)
708     assert(aMode == None or type(aMode) is int)
709     assert(bMode == None or type(bMode) is int)
710
711     if (oSha and (not aSha or not bSha)):
712     #
713     # Case A: Deleted in one
714     #
715         if (not aSha     and not bSha) or \
716            (aSha == oSha and not bSha) or \
717            (not aSha     and bSha == oSha):
718     # Deleted in both or deleted in one and unchanged in the other
719             if aSha:
720                 print 'Removing', path
721             removeFile(True, path)
722         else:
723     # Deleted in one and changed in the other
724             cleanMerge = False
725             if not aSha:
726                 print 'CONFLICT (delete/modify):', path, 'deleted in', \
727                       branch1Name, 'and modified in', branch2Name + '.', \
728                       'Version', branch2Name, 'of', path, 'left in tree.'
729                 mode = bMode
730                 sha = bSha
731             else:
732                 print 'CONFLICT (modify/delete):', path, 'deleted in', \
733                       branch2Name, 'and modified in', branch1Name + '.', \
734                       'Version', branch1Name, 'of', path, 'left in tree.'
735                 mode = aMode
736                 sha = aSha
737
738             updateFile(False, sha, mode, path)
739
740     elif (not oSha and aSha     and not bSha) or \
741          (not oSha and not aSha and bSha):
742     #
743     # Case B: Added in one.
744     #
745         if aSha:
746             addBranch = branch1Name
747             otherBranch = branch2Name
748             mode = aMode
749             sha = aSha
750             conf = 'file/directory'
751         else:
752             addBranch = branch2Name
753             otherBranch = branch1Name
754             mode = bMode
755             sha = bSha
756             conf = 'directory/file'
757     
758         if path in currentDirectorySet:
759             cleanMerge = False
760             newPath = uniquePath(path, addBranch)
761             print 'CONFLICT (' + conf + '):', \
762                   'There is a directory with name', path, 'in', \
763                   otherBranch + '. Adding', path, 'as', newPath
764
765             removeFile(False, path)
766             updateFile(False, sha, mode, newPath)
767         else:
768             print 'Adding', path
769             updateFile(True, sha, mode, path)
770     
771     elif not oSha and aSha and bSha:
772     #
773     # Case C: Added in both (check for same permissions).
774     #
775         if aSha == bSha:
776             if aMode != bMode:
777                 cleanMerge = False
778                 print 'CONFLICT: File', path, \
779                       'added identically in both branches, but permissions', \
780                       'conflict', '0%o' % aMode, '->', '0%o' % bMode
781                 print 'CONFLICT: adding with permission:', '0%o' % aMode
782
783                 updateFile(False, aSha, aMode, path)
784             else:
785                 # This case is handled by git-read-tree
786                 assert(False)
787         else:
788             cleanMerge = False
789             newPath1 = uniquePath(path, branch1Name)
790             newPath2 = uniquePath(path, branch2Name)
791             print 'CONFLICT (add/add): File', path, \
792                   'added non-identically in both branches. Adding as', \
793                   newPath1, 'and', newPath2, 'instead.'
794             removeFile(False, path)
795             updateFile(False, aSha, aMode, newPath1)
796             updateFile(False, bSha, bMode, newPath2)
797
798     elif oSha and aSha and bSha:
799     #
800     # case D: Modified in both, but differently.
801     #
802         print 'Auto-merging', path
803         [sha, mode, clean, dummy] = \
804               mergeFile(path, oSha, oMode,
805                         path, aSha, aMode,
806                         path, bSha, bMode,
807                         branch1Name, branch2Name)
808         if clean:
809             updateFile(True, sha, mode, path)
810         else:
811             cleanMerge = False
812             print 'CONFLICT (content): Merge conflict in', path
813
814             if cacheOnly:
815                 updateFile(False, sha, mode, path)
816             else:
817                 updateFileExt(aSha, aMode, path,
818                               updateCache=True, updateWd=False)
819                 updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
820     else:
821         die("ERROR: Fatal merge failure, shouldn't happen.")
822
823     return cleanMerge
824
825 def usage():
826     die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
827
828 # main entry point as merge strategy module
829 # The first parameters up to -- are merge bases, and the rest are heads.
830 # This strategy module figures out merge bases itself, so we only
831 # get heads.
832
833 if len(sys.argv) < 4:
834     usage()
835
836 for nextArg in xrange(1, len(sys.argv)):
837     if sys.argv[nextArg] == '--':
838         if len(sys.argv) != nextArg + 3:
839             die('Not handling anything other than two heads merge.')
840         try:
841             h1 = firstBranch = sys.argv[nextArg + 1]
842             h2 = secondBranch = sys.argv[nextArg + 2]
843         except IndexError:
844             usage()
845         break
846
847 print 'Merging', h1, 'with', h2
848
849 try:
850     h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
851     h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
852
853     graph = buildGraph([h1, h2])
854
855     [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
856                            firstBranch, secondBranch, graph)
857
858     print ''
859 except:
860     if isinstance(sys.exc_info()[1], SystemExit):
861         raise
862     else:
863         traceback.print_exc(None, sys.stderr)
864         sys.exit(2)
865
866 if clean:
867     sys.exit(0)
868 else:
869     sys.exit(1)