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