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