Code

Update the git-ls-tree documentation
[git.git] / git-merge-recursive.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2005 Fredrik Kuivinen
4 #
6 import sys
7 sys.path.append('''@@GIT_PYTHON_PATH@@''')
9 import math, random, os, re, signal, tempfile, stat, errno, traceback
10 from heapq import heappush, heappop
11 from sets import Set
13 from gitMergeCommon import *
15 outputIndent = 0
16 def output(*args):
17     sys.stdout.write('  '*outputIndent)
18     printList(args)
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
35 # This is a global variable which is used in a number of places but
36 # only written to in the 'merge' function.
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.
43 cacheOnly = False
45 # The entry point to the merge code
46 # ---------------------------------
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))
54     global outputIndent
56     output('Merging:')
57     output(h1)
58     output(h2)
59     sys.stdout.flush()
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()
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))
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
86     [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
87                                  branch1Name, branch2Name)
89     if clean or cacheOnly:
90         res = Commit(None, [h1, h2], tree=shaRes)
91         graph.addNode(res)
92     else:
93         res = None
95     return [res, clean]
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', '-t', 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))
110     return [files, dirs]
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
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))
128     if common == merge:
129         output('Already uptodate!')
130         return [head, True]
132     if cacheOnly:
133         updateArg = '-i'
134     else:
135         updateArg = '-u'
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)
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)
151         entries = unmergedCacheEntries()
152         renamesHead =  getRenames(head, common, head, merge, entries)
153         renamesMerge = getRenames(merge, common, head, merge, entries)
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
170     return [tree, cleanMerge]
172 # Low level file merging, update and removal
173 # ------------------------------------------
175 def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
176               branch1Name, branch2Name):
178     merge = False
179     clean = True
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
193         if aMode == oMode:
194             mode = bMode
195         else:
196             mode = aMode
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))
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)
214             sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
215                               src1]).rstrip()
217             os.unlink(orig)
218             os.unlink(src1)
219             os.unlink(src2)
221             clean = (code == 0)
222         else:
223             assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
224             sha = aSha
226             if aSha != bSha:
227                 clean = False
229     return [sha, mode, clean, merge]
231 def updateFile(clean, sha, mode, path):
232     updateCache = cacheOnly or clean
233     updateWd = not cacheOnly
235     return updateFileExt(sha, mode, path, updateCache, updateWd)
237 def updateFileExt(sha, mode, path, updateCache, updateWd):
238     if cacheOnly:
239         updateWd = False
241     if updateWd:
242         pathComponents = path.split('/')
243         for x in xrange(1, len(pathComponents)):
244             p = '/'.join(pathComponents[0:x])
246             try:
247                 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
248             except OSError:
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)
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)
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])
283 def removeFile(clean, path):
284     updateCache = cacheOnly or clean
285     updateWd = not cacheOnly
287     if updateCache:
288         runProgram(['git-update-index', '--force-remove', '--', path])
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         try:
297             os.removedirs(os.path.dirname(path))
298         except OSError:
299             pass
301 def uniquePath(path, branch):
302     def fileExists(path):
303         try:
304             os.lstat(path)
305             return True
306         except OSError, e:
307             if e.errno == errno.ENOENT:
308                 return False
309             else:
310                 raise
312     branch = branch.replace('/', '_')
313     newPath = path + '~' + branch
314     suffix = 0
315     while newPath in currentFileSet or \
316           newPath in currentDirectorySet  or \
317           fileExists(newPath):
318         suffix += 1
319         newPath = path + '~' + branch + '_' + str(suffix)
320     currentFileSet.add(newPath)
321     return newPath
323 # Cache entry management
324 # ----------------------
326 class CacheEntry:
327     def __init__(self, path):
328         class Stage:
329             def __init__(self):
330                 self.sha1 = None
331                 self.mode = None
333             # Used for debugging only
334             def __str__(self):
335                 if self.mode != None:
336                     m = '0%o' % self.mode
337                 else:
338                     m = 'None'
340                 if self.sha1:
341                     sha1 = self.sha1
342                 else:
343                     sha1 = 'None'
344                 return 'sha1: ' + sha1 + ' mode: ' + m
345         
346         self.stages = [Stage(), Stage(), Stage(), Stage()]
347         self.path = path
348         self.processed = False
350     def __str__(self):
351         return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
353 class CacheEntryContainer:
354     def __init__(self):
355         self.entries = {}
357     def add(self, entry):
358         self.entries[entry.path] = entry
360     def get(self, path):
361         return self.entries.get(path)
363     def __iter__(self):
364         return self.entries.itervalues()
365     
366 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
367 def unmergedCacheEntries():
368     '''Create a dictionary mapping file names to CacheEntry
369     objects. The dictionary contains one entry for every path with a
370     non-zero stage entry.'''
372     lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
373     lines.pop()
375     res = CacheEntryContainer()
376     for l in lines:
377         m = unmergedRE.match(l)
378         if m:
379             mode = int(m.group(1), 8)
380             sha1 = m.group(2)
381             stage = int(m.group(3))
382             path = m.group(4)
384             e = res.get(path)
385             if not e:
386                 e = CacheEntry(path)
387                 res.add(e)
389             e.stages[stage].mode = mode
390             e.stages[stage].sha1 = sha1
391         else:
392             die('Error: Merge program failed: Unexpected output from',
393                 'git-ls-files:', l)
394     return res
396 lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
397 def getCacheEntry(path, origTree, aTree, bTree):
398     '''Returns a CacheEntry object which doesn't have to correspond to
399     a real cache entry in Git's index.'''
400     
401     def parse(out):
402         if out == '':
403             return [None, None]
404         else:
405             m = lsTreeRE.match(out)
406             if not m:
407                 die('Unexpected output from git-ls-tree:', out)
408             elif m.group(2) == 'blob':
409                 return [m.group(3), int(m.group(1), 8)]
410             else:
411                 return [None, None]
413     res = CacheEntry(path)
415     [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
416     [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
417     [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
419     res.stages[1].sha1 = oSha
420     res.stages[1].mode = oMode
421     res.stages[2].sha1 = aSha
422     res.stages[2].mode = aMode
423     res.stages[3].sha1 = bSha
424     res.stages[3].mode = bMode
426     return res
428 # Rename detection and handling
429 # -----------------------------
431 class RenameEntry:
432     def __init__(self,
433                  src, srcSha, srcMode, srcCacheEntry,
434                  dst, dstSha, dstMode, dstCacheEntry,
435                  score):
436         self.srcName = src
437         self.srcSha = srcSha
438         self.srcMode = srcMode
439         self.srcCacheEntry = srcCacheEntry
440         self.dstName = dst
441         self.dstSha = dstSha
442         self.dstMode = dstMode
443         self.dstCacheEntry = dstCacheEntry
444         self.score = score
446         self.processed = False
448 class RenameEntryContainer:
449     def __init__(self):
450         self.entriesSrc = {}
451         self.entriesDst = {}
453     def add(self, entry):
454         self.entriesSrc[entry.srcName] = entry
455         self.entriesDst[entry.dstName] = entry
457     def getSrc(self, path):
458         return self.entriesSrc.get(path)
460     def getDst(self, path):
461         return self.entriesDst.get(path)
463     def __iter__(self):
464         return self.entriesSrc.itervalues()
466 parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
467 def getRenames(tree, oTree, aTree, bTree, cacheEntries):
468     '''Get information of all renames which occured between 'oTree' and
469     'tree'. We need the three trees in the merge ('oTree', 'aTree' and
470     'bTree') to be able to associate the correct cache entries with
471     the rename information. 'tree' is always equal to either aTree or bTree.'''
473     assert(tree == aTree or tree == bTree)
474     inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
475                       '-z', oTree, tree])
477     ret = RenameEntryContainer()
478     try:
479         recs = inp.split("\0")
480         recs.pop() # remove last entry (which is '')
481         it = recs.__iter__()
482         while True:
483             rec = it.next()
484             m = parseDiffRenamesRE.match(rec)
486             if not m:
487                 die('Unexpected output from git-diff-tree:', rec)
489             srcMode = int(m.group(1), 8)
490             dstMode = int(m.group(2), 8)
491             srcSha = m.group(3)
492             dstSha = m.group(4)
493             score = m.group(5)
494             src = it.next()
495             dst = it.next()
497             srcCacheEntry = cacheEntries.get(src)
498             if not srcCacheEntry:
499                 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
500                 cacheEntries.add(srcCacheEntry)
502             dstCacheEntry = cacheEntries.get(dst)
503             if not dstCacheEntry:
504                 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
505                 cacheEntries.add(dstCacheEntry)
507             ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
508                                 dst, dstSha, dstMode, dstCacheEntry,
509                                 score))
510     except StopIteration:
511         pass
512     return ret
514 def fmtRename(src, dst):
515     srcPath = src.split('/')
516     dstPath = dst.split('/')
517     path = []
518     endIndex = min(len(srcPath), len(dstPath)) - 1
519     for x in range(0, endIndex):
520         if srcPath[x] == dstPath[x]:
521             path.append(srcPath[x])
522         else:
523             endIndex = x
524             break
526     if len(path) > 0:
527         return '/'.join(path) + \
528                '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
529                '/'.join(dstPath[endIndex:]) + '}'
530     else:
531         return src + ' => ' + dst
533 def processRenames(renamesA, renamesB, branchNameA, branchNameB):
534     srcNames = Set()
535     for x in renamesA:
536         srcNames.add(x.srcName)
537     for x in renamesB:
538         srcNames.add(x.srcName)
540     cleanMerge = True
541     for path in srcNames:
542         if renamesA.getSrc(path):
543             renames1 = renamesA
544             renames2 = renamesB
545             branchName1 = branchNameA
546             branchName2 = branchNameB
547         else:
548             renames1 = renamesB
549             renames2 = renamesA
550             branchName1 = branchNameB
551             branchName2 = branchNameA
552         
553         ren1 = renames1.getSrc(path)
554         ren2 = renames2.getSrc(path)
556         ren1.dstCacheEntry.processed = True
557         ren1.srcCacheEntry.processed = True
559         if ren1.processed:
560             continue
562         ren1.processed = True
563         removeFile(True, ren1.srcName)
564         if ren2:
565             # Renamed in 1 and renamed in 2
566             assert(ren1.srcName == ren2.srcName)
567             ren2.dstCacheEntry.processed = True
568             ren2.processed = True
570             if ren1.dstName != ren2.dstName:
571                 output('CONFLICT (rename/rename): Rename',
572                        fmtRename(path, ren1.dstName), 'in branch', branchName1,
573                        'rename', fmtRename(path, ren2.dstName), 'in',
574                        branchName2)
575                 cleanMerge = False
577                 if ren1.dstName in currentDirectorySet:
578                     dstName1 = uniquePath(ren1.dstName, branchName1)
579                     output(ren1.dstName, 'is a directory in', branchName2,
580                            'adding as', dstName1, 'instead.')
581                     removeFile(False, ren1.dstName)
582                 else:
583                     dstName1 = ren1.dstName
585                 if ren2.dstName in currentDirectorySet:
586                     dstName2 = uniquePath(ren2.dstName, branchName2)
587                     output(ren2.dstName, 'is a directory in', branchName1,
588                            'adding as', dstName2, 'instead.')
589                     removeFile(False, ren2.dstName)
590                 else:
591                     dstName2 = ren1.dstName
593                 updateFile(False, ren1.dstSha, ren1.dstMode, dstName1)
594                 updateFile(False, ren2.dstSha, ren2.dstMode, dstName2)
595             else:
596                 [resSha, resMode, clean, merge] = \
597                          mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
598                                    ren1.dstName, ren1.dstSha, ren1.dstMode,
599                                    ren2.dstName, ren2.dstSha, ren2.dstMode,
600                                    branchName1, branchName2)
602                 if merge or not clean:
603                     output('Renaming', fmtRename(path, ren1.dstName))
605                 if merge:
606                     output('Auto-merging', ren1.dstName)
608                 if not clean:
609                     output('CONFLICT (content): merge conflict in',
610                            ren1.dstName)
611                     cleanMerge = False
613                     if not cacheOnly:
614                         updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
615                                       updateCache=True, updateWd=False)
616                 updateFile(clean, resSha, resMode, ren1.dstName)
617         else:
618             # Renamed in 1, maybe changed in 2
619             if renamesA == renames1:
620                 stage = 3
621             else:
622                 stage = 2
623                 
624             srcShaOtherBranch  = ren1.srcCacheEntry.stages[stage].sha1
625             srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
627             dstShaOtherBranch  = ren1.dstCacheEntry.stages[stage].sha1
628             dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
630             tryMerge = False
631             
632             if ren1.dstName in currentDirectorySet:
633                 newPath = uniquePath(ren1.dstName, branchName1)
634                 output('CONFLICT (rename/directory): Rename',
635                        fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,
636                        'directory', ren1.dstName, 'added in', branchName2)
637                 output('Renaming', ren1.srcName, 'to', newPath, 'instead')
638                 cleanMerge = False
639                 removeFile(False, ren1.dstName)
640                 updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
641             elif srcShaOtherBranch == None:
642                 output('CONFLICT (rename/delete): Rename',
643                        fmtRename(ren1.srcName, ren1.dstName), 'in',
644                        branchName1, 'and deleted in', branchName2)
645                 cleanMerge = False
646                 updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
647             elif dstShaOtherBranch:
648                 newPath = uniquePath(ren1.dstName, branchName2)
649                 output('CONFLICT (rename/add): Rename',
650                        fmtRename(ren1.srcName, ren1.dstName), 'in',
651                        branchName1 + '.', ren1.dstName, 'added in', branchName2)
652                 output('Adding as', newPath, 'instead')
653                 updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
654                 cleanMerge = False
655                 tryMerge = True
656             elif renames2.getDst(ren1.dstName):
657                 dst2 = renames2.getDst(ren1.dstName)
658                 newPath1 = uniquePath(ren1.dstName, branchName1)
659                 newPath2 = uniquePath(dst2.dstName, branchName2)
660                 output('CONFLICT (rename/rename): Rename',
661                        fmtRename(ren1.srcName, ren1.dstName), 'in',
662                        branchName1+'. Rename',
663                        fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2)
664                 output('Renaming', ren1.srcName, 'to', newPath1, 'and',
665                        dst2.srcName, 'to', newPath2, 'instead')
666                 removeFile(False, ren1.dstName)
667                 updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
668                 updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
669                 dst2.processed = True
670                 cleanMerge = False
671             else:
672                 tryMerge = True
674             if tryMerge:
675                 [resSha, resMode, clean, merge] = \
676                          mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
677                                    ren1.dstName, ren1.dstSha, ren1.dstMode,
678                                    ren1.srcName, srcShaOtherBranch, srcModeOtherBranch,
679                                    branchName1, branchName2)
681                 if merge or not clean:
682                     output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
684                 if merge:
685                     output('Auto-merging', ren1.dstName)
687                 if not clean:
688                     output('CONFLICT (rename/modify): Merge conflict in',
689                            ren1.dstName)
690                     cleanMerge = False
692                     if not cacheOnly:
693                         updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
694                                       updateCache=True, updateWd=False)
695                 updateFile(clean, resSha, resMode, ren1.dstName)
697     return cleanMerge
699 # Per entry merge function
700 # ------------------------
702 def processEntry(entry, branch1Name, branch2Name):
703     '''Merge one cache entry.'''
705     debug('processing', entry.path, 'clean cache:', cacheOnly)
707     cleanMerge = True
709     path = entry.path
710     oSha = entry.stages[1].sha1
711     oMode = entry.stages[1].mode
712     aSha = entry.stages[2].sha1
713     aMode = entry.stages[2].mode
714     bSha = entry.stages[3].sha1
715     bMode = entry.stages[3].mode
717     assert(oSha == None or isSha(oSha))
718     assert(aSha == None or isSha(aSha))
719     assert(bSha == None or isSha(bSha))
721     assert(oMode == None or type(oMode) is int)
722     assert(aMode == None or type(aMode) is int)
723     assert(bMode == None or type(bMode) is int)
725     if (oSha and (not aSha or not bSha)):
726     #
727     # Case A: Deleted in one
728     #
729         if (not aSha     and not bSha) or \
730            (aSha == oSha and not bSha) or \
731            (not aSha     and bSha == oSha):
732     # Deleted in both or deleted in one and unchanged in the other
733             if aSha:
734                 output('Removing', path)
735             removeFile(True, path)
736         else:
737     # Deleted in one and changed in the other
738             cleanMerge = False
739             if not aSha:
740                 output('CONFLICT (delete/modify):', path, 'deleted in',
741                        branch1Name, 'and modified in', branch2Name + '.',
742                        'Version', branch2Name, 'of', path, 'left in tree.')
743                 mode = bMode
744                 sha = bSha
745             else:
746                 output('CONFLICT (modify/delete):', path, 'deleted in',
747                        branch2Name, 'and modified in', branch1Name + '.',
748                        'Version', branch1Name, 'of', path, 'left in tree.')
749                 mode = aMode
750                 sha = aSha
752             updateFile(False, sha, mode, path)
754     elif (not oSha and aSha     and not bSha) or \
755          (not oSha and not aSha and bSha):
756     #
757     # Case B: Added in one.
758     #
759         if aSha:
760             addBranch = branch1Name
761             otherBranch = branch2Name
762             mode = aMode
763             sha = aSha
764             conf = 'file/directory'
765         else:
766             addBranch = branch2Name
767             otherBranch = branch1Name
768             mode = bMode
769             sha = bSha
770             conf = 'directory/file'
771     
772         if path in currentDirectorySet:
773             cleanMerge = False
774             newPath = uniquePath(path, addBranch)
775             output('CONFLICT (' + conf + '):',
776                    'There is a directory with name', path, 'in',
777                    otherBranch + '. Adding', path, 'as', newPath)
779             removeFile(False, path)
780             updateFile(False, sha, mode, newPath)
781         else:
782             output('Adding', path)
783             updateFile(True, sha, mode, path)
784     
785     elif not oSha and aSha and bSha:
786     #
787     # Case C: Added in both (check for same permissions).
788     #
789         if aSha == bSha:
790             if aMode != bMode:
791                 cleanMerge = False
792                 output('CONFLICT: File', path,
793                        'added identically in both branches, but permissions',
794                        'conflict', '0%o' % aMode, '->', '0%o' % bMode)
795                 output('CONFLICT: adding with permission:', '0%o' % aMode)
797                 updateFile(False, aSha, aMode, path)
798             else:
799                 # This case is handled by git-read-tree
800                 assert(False)
801         else:
802             cleanMerge = False
803             newPath1 = uniquePath(path, branch1Name)
804             newPath2 = uniquePath(path, branch2Name)
805             output('CONFLICT (add/add): File', path,
806                    'added non-identically in both branches. Adding as',
807                    newPath1, 'and', newPath2, 'instead.')
808             removeFile(False, path)
809             updateFile(False, aSha, aMode, newPath1)
810             updateFile(False, bSha, bMode, newPath2)
812     elif oSha and aSha and bSha:
813     #
814     # case D: Modified in both, but differently.
815     #
816         output('Auto-merging', path)
817         [sha, mode, clean, dummy] = \
818               mergeFile(path, oSha, oMode,
819                         path, aSha, aMode,
820                         path, bSha, bMode,
821                         branch1Name, branch2Name)
822         if clean:
823             updateFile(True, sha, mode, path)
824         else:
825             cleanMerge = False
826             output('CONFLICT (content): Merge conflict in', path)
828             if cacheOnly:
829                 updateFile(False, sha, mode, path)
830             else:
831                 updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
832     else:
833         die("ERROR: Fatal merge failure, shouldn't happen.")
835     return cleanMerge
837 def usage():
838     die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
840 # main entry point as merge strategy module
841 # The first parameters up to -- are merge bases, and the rest are heads.
842 # This strategy module figures out merge bases itself, so we only
843 # get heads.
845 if len(sys.argv) < 4:
846     usage()
848 for nextArg in xrange(1, len(sys.argv)):
849     if sys.argv[nextArg] == '--':
850         if len(sys.argv) != nextArg + 3:
851             die('Not handling anything other than two heads merge.')
852         try:
853             h1 = firstBranch = sys.argv[nextArg + 1]
854             h2 = secondBranch = sys.argv[nextArg + 2]
855         except IndexError:
856             usage()
857         break
859 print 'Merging', h1, 'with', h2
861 try:
862     h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
863     h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
865     graph = buildGraph([h1, h2])
867     [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
868                            firstBranch, secondBranch, graph)
870     print ''
871 except:
872     if isinstance(sys.exc_info()[1], SystemExit):
873         raise
874     else:
875         traceback.print_exc(None, sys.stderr)
876         sys.exit(2)
878 if clean:
879     sys.exit(0)
880 else:
881     sys.exit(1)