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