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