git-merge-recursive.pyon commit Add a minimal test for git-cherry (b91db27)
   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', '-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))
 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 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)
 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 setIndexStages(path,
 284                   oSHA1, oMode,
 285                   aSHA1, aMode,
 286                   bSHA1, bMode,
 287                   clear=True):
 288    istring = []
 289    if clear:
 290        istring.append("0 " + ("0" * 40) + "\t" + path + "\0")
 291    if oMode:
 292        istring.append("%o %s %d\t%s\0" % (oMode, oSHA1, 1, path))
 293    if aMode:
 294        istring.append("%o %s %d\t%s\0" % (aMode, aSHA1, 2, path))
 295    if bMode:
 296        istring.append("%o %s %d\t%s\0" % (bMode, bSHA1, 3, path))
 297
 298    runProgram(['git-update-index', '-z', '--index-info'],
 299               input="".join(istring))
 300
 301def removeFile(clean, path):
 302    updateCache = cacheOnly or clean
 303    updateWd = not cacheOnly
 304
 305    if updateCache:
 306        runProgram(['git-update-index', '--force-remove', '--', path])
 307
 308    if updateWd:
 309        try:
 310            os.unlink(path)
 311        except OSError, e:
 312            if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
 313                raise
 314        try:
 315            os.removedirs(os.path.dirname(path))
 316        except OSError:
 317            pass
 318
 319def uniquePath(path, branch):
 320    def fileExists(path):
 321        try:
 322            os.lstat(path)
 323            return True
 324        except OSError, e:
 325            if e.errno == errno.ENOENT:
 326                return False
 327            else:
 328                raise
 329
 330    branch = branch.replace('/', '_')
 331    newPath = path + '~' + branch
 332    suffix = 0
 333    while newPath in currentFileSet or \
 334          newPath in currentDirectorySet  or \
 335          fileExists(newPath):
 336        suffix += 1
 337        newPath = path + '~' + branch + '_' + str(suffix)
 338    currentFileSet.add(newPath)
 339    return newPath
 340
 341# Cache entry management
 342# ----------------------
 343
 344class CacheEntry:
 345    def __init__(self, path):
 346        class Stage:
 347            def __init__(self):
 348                self.sha1 = None
 349                self.mode = None
 350
 351            # Used for debugging only
 352            def __str__(self):
 353                if self.mode != None:
 354                    m = '0%o' % self.mode
 355                else:
 356                    m = 'None'
 357
 358                if self.sha1:
 359                    sha1 = self.sha1
 360                else:
 361                    sha1 = 'None'
 362                return 'sha1: ' + sha1 + ' mode: ' + m
 363        
 364        self.stages = [Stage(), Stage(), Stage(), Stage()]
 365        self.path = path
 366        self.processed = False
 367
 368    def __str__(self):
 369        return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
 370
 371class CacheEntryContainer:
 372    def __init__(self):
 373        self.entries = {}
 374
 375    def add(self, entry):
 376        self.entries[entry.path] = entry
 377
 378    def get(self, path):
 379        return self.entries.get(path)
 380
 381    def __iter__(self):
 382        return self.entries.itervalues()
 383    
 384unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
 385def unmergedCacheEntries():
 386    '''Create a dictionary mapping file names to CacheEntry
 387    objects. The dictionary contains one entry for every path with a
 388    non-zero stage entry.'''
 389
 390    lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
 391    lines.pop()
 392
 393    res = CacheEntryContainer()
 394    for l in lines:
 395        m = unmergedRE.match(l)
 396        if m:
 397            mode = int(m.group(1), 8)
 398            sha1 = m.group(2)
 399            stage = int(m.group(3))
 400            path = m.group(4)
 401
 402            e = res.get(path)
 403            if not e:
 404                e = CacheEntry(path)
 405                res.add(e)
 406
 407            e.stages[stage].mode = mode
 408            e.stages[stage].sha1 = sha1
 409        else:
 410            die('Error: Merge program failed: Unexpected output from',
 411                'git-ls-files:', l)
 412    return res
 413
 414lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
 415def getCacheEntry(path, origTree, aTree, bTree):
 416    '''Returns a CacheEntry object which doesn't have to correspond to
 417    a real cache entry in Git's index.'''
 418    
 419    def parse(out):
 420        if out == '':
 421            return [None, None]
 422        else:
 423            m = lsTreeRE.match(out)
 424            if not m:
 425                die('Unexpected output from git-ls-tree:', out)
 426            elif m.group(2) == 'blob':
 427                return [m.group(3), int(m.group(1), 8)]
 428            else:
 429                return [None, None]
 430
 431    res = CacheEntry(path)
 432
 433    [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
 434    [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
 435    [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
 436
 437    res.stages[1].sha1 = oSha
 438    res.stages[1].mode = oMode
 439    res.stages[2].sha1 = aSha
 440    res.stages[2].mode = aMode
 441    res.stages[3].sha1 = bSha
 442    res.stages[3].mode = bMode
 443
 444    return res
 445
 446# Rename detection and handling
 447# -----------------------------
 448
 449class RenameEntry:
 450    def __init__(self,
 451                 src, srcSha, srcMode, srcCacheEntry,
 452                 dst, dstSha, dstMode, dstCacheEntry,
 453                 score):
 454        self.srcName = src
 455        self.srcSha = srcSha
 456        self.srcMode = srcMode
 457        self.srcCacheEntry = srcCacheEntry
 458        self.dstName = dst
 459        self.dstSha = dstSha
 460        self.dstMode = dstMode
 461        self.dstCacheEntry = dstCacheEntry
 462        self.score = score
 463
 464        self.processed = False
 465
 466class RenameEntryContainer:
 467    def __init__(self):
 468        self.entriesSrc = {}
 469        self.entriesDst = {}
 470
 471    def add(self, entry):
 472        self.entriesSrc[entry.srcName] = entry
 473        self.entriesDst[entry.dstName] = entry
 474
 475    def getSrc(self, path):
 476        return self.entriesSrc.get(path)
 477
 478    def getDst(self, path):
 479        return self.entriesDst.get(path)
 480
 481    def __iter__(self):
 482        return self.entriesSrc.itervalues()
 483
 484parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
 485def getRenames(tree, oTree, aTree, bTree, cacheEntries):
 486    '''Get information of all renames which occured between 'oTree' and
 487    'tree'. We need the three trees in the merge ('oTree', 'aTree' and
 488    'bTree') to be able to associate the correct cache entries with
 489    the rename information. 'tree' is always equal to either aTree or bTree.'''
 490
 491    assert(tree == aTree or tree == bTree)
 492    inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
 493                      '-z', oTree, tree])
 494
 495    ret = RenameEntryContainer()
 496    try:
 497        recs = inp.split("\0")
 498        recs.pop() # remove last entry (which is '')
 499        it = recs.__iter__()
 500        while True:
 501            rec = it.next()
 502            m = parseDiffRenamesRE.match(rec)
 503
 504            if not m:
 505                die('Unexpected output from git-diff-tree:', rec)
 506
 507            srcMode = int(m.group(1), 8)
 508            dstMode = int(m.group(2), 8)
 509            srcSha = m.group(3)
 510            dstSha = m.group(4)
 511            score = m.group(5)
 512            src = it.next()
 513            dst = it.next()
 514
 515            srcCacheEntry = cacheEntries.get(src)
 516            if not srcCacheEntry:
 517                srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
 518                cacheEntries.add(srcCacheEntry)
 519
 520            dstCacheEntry = cacheEntries.get(dst)
 521            if not dstCacheEntry:
 522                dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
 523                cacheEntries.add(dstCacheEntry)
 524
 525            ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
 526                                dst, dstSha, dstMode, dstCacheEntry,
 527                                score))
 528    except StopIteration:
 529        pass
 530    return ret
 531
 532def fmtRename(src, dst):
 533    srcPath = src.split('/')
 534    dstPath = dst.split('/')
 535    path = []
 536    endIndex = min(len(srcPath), len(dstPath)) - 1
 537    for x in range(0, endIndex):
 538        if srcPath[x] == dstPath[x]:
 539            path.append(srcPath[x])
 540        else:
 541            endIndex = x
 542            break
 543
 544    if len(path) > 0:
 545        return '/'.join(path) + \
 546               '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
 547               '/'.join(dstPath[endIndex:]) + '}'
 548    else:
 549        return src + ' => ' + dst
 550
 551def processRenames(renamesA, renamesB, branchNameA, branchNameB):
 552    srcNames = Set()
 553    for x in renamesA:
 554        srcNames.add(x.srcName)
 555    for x in renamesB:
 556        srcNames.add(x.srcName)
 557
 558    cleanMerge = True
 559    for path in srcNames:
 560        if renamesA.getSrc(path):
 561            renames1 = renamesA
 562            renames2 = renamesB
 563            branchName1 = branchNameA
 564            branchName2 = branchNameB
 565        else:
 566            renames1 = renamesB
 567            renames2 = renamesA
 568            branchName1 = branchNameB
 569            branchName2 = branchNameA
 570        
 571        ren1 = renames1.getSrc(path)
 572        ren2 = renames2.getSrc(path)
 573
 574        ren1.dstCacheEntry.processed = True
 575        ren1.srcCacheEntry.processed = True
 576
 577        if ren1.processed:
 578            continue
 579
 580        ren1.processed = True
 581
 582        if ren2:
 583            # Renamed in 1 and renamed in 2
 584            assert(ren1.srcName == ren2.srcName)
 585            ren2.dstCacheEntry.processed = True
 586            ren2.processed = True
 587
 588            if ren1.dstName != ren2.dstName:
 589                output('CONFLICT (rename/rename): Rename',
 590                       fmtRename(path, ren1.dstName), 'in branch', branchName1,
 591                       'rename', fmtRename(path, ren2.dstName), 'in',
 592                       branchName2)
 593                cleanMerge = False
 594
 595                if ren1.dstName in currentDirectorySet:
 596                    dstName1 = uniquePath(ren1.dstName, branchName1)
 597                    output(ren1.dstName, 'is a directory in', branchName2,
 598                           'adding as', dstName1, 'instead.')
 599                    removeFile(False, ren1.dstName)
 600                else:
 601                    dstName1 = ren1.dstName
 602
 603                if ren2.dstName in currentDirectorySet:
 604                    dstName2 = uniquePath(ren2.dstName, branchName2)
 605                    output(ren2.dstName, 'is a directory in', branchName1,
 606                           'adding as', dstName2, 'instead.')
 607                    removeFile(False, ren2.dstName)
 608                else:
 609                    dstName2 = ren2.dstName
 610                setIndexStages(dstName1,
 611                               None, None,
 612                               ren1.dstSha, ren1.dstMode,
 613                               None, None)
 614                setIndexStages(dstName2,
 615                               None, None,
 616                               None, None,
 617                               ren2.dstSha, ren2.dstMode)
 618
 619            else:
 620                removeFile(True, ren1.srcName)
 621
 622                [resSha, resMode, clean, merge] = \
 623                         mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
 624                                   ren1.dstName, ren1.dstSha, ren1.dstMode,
 625                                   ren2.dstName, ren2.dstSha, ren2.dstMode,
 626                                   branchName1, branchName2)
 627
 628                if merge or not clean:
 629                    output('Renaming', fmtRename(path, ren1.dstName))
 630
 631                if merge:
 632                    output('Auto-merging', ren1.dstName)
 633
 634                if not clean:
 635                    output('CONFLICT (content): merge conflict in',
 636                           ren1.dstName)
 637                    cleanMerge = False
 638
 639                    if not cacheOnly:
 640                        setIndexStages(ren1.dstName,
 641                                       ren1.srcSha, ren1.srcMode,
 642                                       ren1.dstSha, ren1.dstMode,
 643                                       ren2.dstSha, ren2.dstMode)
 644
 645                updateFile(clean, resSha, resMode, ren1.dstName)
 646        else:
 647            removeFile(True, ren1.srcName)
 648
 649            # Renamed in 1, maybe changed in 2
 650            if renamesA == renames1:
 651                stage = 3
 652            else:
 653                stage = 2
 654                
 655            srcShaOtherBranch  = ren1.srcCacheEntry.stages[stage].sha1
 656            srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
 657
 658            dstShaOtherBranch  = ren1.dstCacheEntry.stages[stage].sha1
 659            dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
 660
 661            tryMerge = False
 662            
 663            if ren1.dstName in currentDirectorySet:
 664                newPath = uniquePath(ren1.dstName, branchName1)
 665                output('CONFLICT (rename/directory): Rename',
 666                       fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,
 667                       'directory', ren1.dstName, 'added in', branchName2)
 668                output('Renaming', ren1.srcName, 'to', newPath, 'instead')
 669                cleanMerge = False
 670                removeFile(False, ren1.dstName)
 671                updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
 672            elif srcShaOtherBranch == None:
 673                output('CONFLICT (rename/delete): Rename',
 674                       fmtRename(ren1.srcName, ren1.dstName), 'in',
 675                       branchName1, 'and deleted in', branchName2)
 676                cleanMerge = False
 677                updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
 678            elif dstShaOtherBranch:
 679                newPath = uniquePath(ren1.dstName, branchName2)
 680                output('CONFLICT (rename/add): Rename',
 681                       fmtRename(ren1.srcName, ren1.dstName), 'in',
 682                       branchName1 + '.', ren1.dstName, 'added in', branchName2)
 683                output('Adding as', newPath, 'instead')
 684                updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
 685                cleanMerge = False
 686                tryMerge = True
 687            elif renames2.getDst(ren1.dstName):
 688                dst2 = renames2.getDst(ren1.dstName)
 689                newPath1 = uniquePath(ren1.dstName, branchName1)
 690                newPath2 = uniquePath(dst2.dstName, branchName2)
 691                output('CONFLICT (rename/rename): Rename',
 692                       fmtRename(ren1.srcName, ren1.dstName), 'in',
 693                       branchName1+'. Rename',
 694                       fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2)
 695                output('Renaming', ren1.srcName, 'to', newPath1, 'and',
 696                       dst2.srcName, 'to', newPath2, 'instead')
 697                removeFile(False, ren1.dstName)
 698                updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
 699                updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
 700                dst2.processed = True
 701                cleanMerge = False
 702            else:
 703                tryMerge = True
 704
 705            if tryMerge:
 706
 707                oName, oSHA1, oMode = ren1.srcName, ren1.srcSha, ren1.srcMode
 708                aName, bName = ren1.dstName, ren1.srcName
 709                aSHA1, bSHA1 = ren1.dstSha, srcShaOtherBranch
 710                aMode, bMode = ren1.dstMode, srcModeOtherBranch
 711                aBranch, bBranch = branchName1, branchName2
 712
 713                if renamesA != renames1:
 714                    aName, bName = bName, aName
 715                    aSHA1, bSHA1 = bSHA1, aSHA1
 716                    aMode, bMode = bMode, aMode
 717                    aBranch, bBranch = bBranch, aBranch
 718
 719                [resSha, resMode, clean, merge] = \
 720                         mergeFile(oName, oSHA1, oMode,
 721                                   aName, aSHA1, aMode,
 722                                   bName, bSHA1, bMode,
 723                                   aBranch, bBranch);
 724
 725                if merge or not clean:
 726                    output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
 727
 728                if merge:
 729                    output('Auto-merging', ren1.dstName)
 730
 731                if not clean:
 732                    output('CONFLICT (rename/modify): Merge conflict in',
 733                           ren1.dstName)
 734                    cleanMerge = False
 735
 736                    if not cacheOnly:
 737                        setIndexStages(ren1.dstName,
 738                                       oSHA1, oMode,
 739                                       aSHA1, aMode,
 740                                       bSHA1, bMode)
 741
 742                updateFile(clean, resSha, resMode, ren1.dstName)
 743
 744    return cleanMerge
 745
 746# Per entry merge function
 747# ------------------------
 748
 749def processEntry(entry, branch1Name, branch2Name):
 750    '''Merge one cache entry.'''
 751
 752    debug('processing', entry.path, 'clean cache:', cacheOnly)
 753
 754    cleanMerge = True
 755
 756    path = entry.path
 757    oSha = entry.stages[1].sha1
 758    oMode = entry.stages[1].mode
 759    aSha = entry.stages[2].sha1
 760    aMode = entry.stages[2].mode
 761    bSha = entry.stages[3].sha1
 762    bMode = entry.stages[3].mode
 763
 764    assert(oSha == None or isSha(oSha))
 765    assert(aSha == None or isSha(aSha))
 766    assert(bSha == None or isSha(bSha))
 767
 768    assert(oMode == None or type(oMode) is int)
 769    assert(aMode == None or type(aMode) is int)
 770    assert(bMode == None or type(bMode) is int)
 771
 772    if (oSha and (not aSha or not bSha)):
 773    #
 774    # Case A: Deleted in one
 775    #
 776        if (not aSha     and not bSha) or \
 777           (aSha == oSha and not bSha) or \
 778           (not aSha     and bSha == oSha):
 779    # Deleted in both or deleted in one and unchanged in the other
 780            if aSha:
 781                output('Removing', path)
 782            removeFile(True, path)
 783        else:
 784    # Deleted in one and changed in the other
 785            cleanMerge = False
 786            if not aSha:
 787                output('CONFLICT (delete/modify):', path, 'deleted in',
 788                       branch1Name, 'and modified in', branch2Name + '.',
 789                       'Version', branch2Name, 'of', path, 'left in tree.')
 790                mode = bMode
 791                sha = bSha
 792            else:
 793                output('CONFLICT (modify/delete):', path, 'deleted in',
 794                       branch2Name, 'and modified in', branch1Name + '.',
 795                       'Version', branch1Name, 'of', path, 'left in tree.')
 796                mode = aMode
 797                sha = aSha
 798
 799            updateFile(False, sha, mode, path)
 800
 801    elif (not oSha and aSha     and not bSha) or \
 802         (not oSha and not aSha and bSha):
 803    #
 804    # Case B: Added in one.
 805    #
 806        if aSha:
 807            addBranch = branch1Name
 808            otherBranch = branch2Name
 809            mode = aMode
 810            sha = aSha
 811            conf = 'file/directory'
 812        else:
 813            addBranch = branch2Name
 814            otherBranch = branch1Name
 815            mode = bMode
 816            sha = bSha
 817            conf = 'directory/file'
 818    
 819        if path in currentDirectorySet:
 820            cleanMerge = False
 821            newPath = uniquePath(path, addBranch)
 822            output('CONFLICT (' + conf + '):',
 823                   'There is a directory with name', path, 'in',
 824                   otherBranch + '. Adding', path, 'as', newPath)
 825
 826            removeFile(False, path)
 827            updateFile(False, sha, mode, newPath)
 828        else:
 829            output('Adding', path)
 830            updateFile(True, sha, mode, path)
 831    
 832    elif not oSha and aSha and bSha:
 833    #
 834    # Case C: Added in both (check for same permissions).
 835    #
 836        if aSha == bSha:
 837            if aMode != bMode:
 838                cleanMerge = False
 839                output('CONFLICT: File', path,
 840                       'added identically in both branches, but permissions',
 841                       'conflict', '0%o' % aMode, '->', '0%o' % bMode)
 842                output('CONFLICT: adding with permission:', '0%o' % aMode)
 843
 844                updateFile(False, aSha, aMode, path)
 845            else:
 846                # This case is handled by git-read-tree
 847                assert(False)
 848        else:
 849            cleanMerge = False
 850            newPath1 = uniquePath(path, branch1Name)
 851            newPath2 = uniquePath(path, branch2Name)
 852            output('CONFLICT (add/add): File', path,
 853                   'added non-identically in both branches. Adding as',
 854                   newPath1, 'and', newPath2, 'instead.')
 855            removeFile(False, path)
 856            updateFile(False, aSha, aMode, newPath1)
 857            updateFile(False, bSha, bMode, newPath2)
 858
 859    elif oSha and aSha and bSha:
 860    #
 861    # case D: Modified in both, but differently.
 862    #
 863        output('Auto-merging', path)
 864        [sha, mode, clean, dummy] = \
 865              mergeFile(path, oSha, oMode,
 866                        path, aSha, aMode,
 867                        path, bSha, bMode,
 868                        branch1Name, branch2Name)
 869        if clean:
 870            updateFile(True, sha, mode, path)
 871        else:
 872            cleanMerge = False
 873            output('CONFLICT (content): Merge conflict in', path)
 874
 875            if cacheOnly:
 876                updateFile(False, sha, mode, path)
 877            else:
 878                updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
 879    else:
 880        die("ERROR: Fatal merge failure, shouldn't happen.")
 881
 882    return cleanMerge
 883
 884def usage():
 885    die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
 886
 887# main entry point as merge strategy module
 888# The first parameters up to -- are merge bases, and the rest are heads.
 889# This strategy module figures out merge bases itself, so we only
 890# get heads.
 891
 892if len(sys.argv) < 4:
 893    usage()
 894
 895for nextArg in xrange(1, len(sys.argv)):
 896    if sys.argv[nextArg] == '--':
 897        if len(sys.argv) != nextArg + 3:
 898            die('Not handling anything other than two heads merge.')
 899        try:
 900            h1 = firstBranch = sys.argv[nextArg + 1]
 901            h2 = secondBranch = sys.argv[nextArg + 2]
 902        except IndexError:
 903            usage()
 904        break
 905
 906print 'Merging', h1, 'with', h2
 907
 908try:
 909    h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
 910    h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
 911
 912    graph = buildGraph([h1, h2])
 913
 914    [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
 915                           firstBranch, secondBranch, graph)
 916
 917    print ''
 918except:
 919    if isinstance(sys.exc_info()[1], SystemExit):
 920        raise
 921    else:
 922        traceback.print_exc(None, sys.stderr)
 923        sys.exit(2)
 924
 925if clean:
 926    sys.exit(0)
 927else:
 928    sys.exit(1)