git-merge-recursive.pyon commit diff: --full-index (80b1e51)
   1#!/usr/bin/python
   2#
   3# Copyright (C) 2005 Fredrik Kuivinen
   4#
   5
   6import sys
   7sys.path.append('''@@GIT_PYTHON_PATH@@''')
   8
   9import math, random, os, re, signal, tempfile, stat, errno, traceback
  10from heapq import heappush, heappop
  11from sets import Set
  12
  13from gitMergeCommon import *
  14
  15outputIndent = 0
  16def output(*args):
  17    sys.stdout.write('  '*outputIndent)
  18    printList(args)
  19
  20originalIndexFile = os.environ.get('GIT_INDEX_FILE',
  21                                   os.environ.get('GIT_DIR', '.git') + '/index')
  22temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
  23                     '/merge-recursive-tmp-index'
  24def 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
  43cacheOnly = False
  44
  45# The entry point to the merge code
  46# ---------------------------------
  47
  48def 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
  97getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
  98def 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.
 116currentFileSet = None
 117currentDirectorySet = None
 118
 119def 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
 175def 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
 231def updateFile(clean, sha, mode, path):
 232    updateCache = cacheOnly or clean
 233    updateWd = not cacheOnly
 234
 235    return updateFileExt(sha, mode, path, updateCache, updateWd)
 236
 237def 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
 283def 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
 297def 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
 322class 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
 349class 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    
 362unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
 363def 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
 392lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
 393def 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
 427class 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
 444class 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
 462parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
 463def 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
 510def 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
 529def 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
 698def 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
 835def 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
 843if len(sys.argv) < 4:
 844    usage()
 845
 846for 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
 857print 'Merging', h1, 'with', h2
 858
 859try:
 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 ''
 869except:
 870    if isinstance(sys.exc_info()[1], SystemExit):
 871        raise
 872    else:
 873        traceback.print_exc(None, sys.stderr)
 874        sys.exit(2)
 875
 876if clean:
 877    sys.exit(0)
 878else:
 879    sys.exit(1)