eeb3d24da8224a2275513918480c0b7866dfbe01
   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
  10# The actual merge code
  11# ---------------------
  12
  13def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
  14    '''Merge the commits h1 and h2, return the resulting virtual
  15    commit object and a flag indicating the cleaness of the merge.'''
  16    assert(isinstance(h1, Commit) and isinstance(h2, Commit))
  17    assert(isinstance(graph, Graph))
  18
  19    def infoMsg(*args):
  20        sys.stdout.write('  '*callDepth)
  21        printList(args)
  22    infoMsg('Merging:')
  23    infoMsg(h1)
  24    infoMsg(h2)
  25    sys.stdout.flush()
  26
  27    ca = getCommonAncestors(graph, h1, h2)
  28    infoMsg('found', len(ca), 'common ancestor(s):')
  29    for x in ca:
  30        infoMsg(x)
  31    sys.stdout.flush()
  32
  33    Ms = ca[0]
  34    for h in ca[1:]:
  35        [Ms, ignore] = merge(Ms, h,
  36                             'Temporary shared merge branch 1',
  37                             'Temporary shared merge branch 2',
  38                             graph, callDepth+1)
  39        assert(isinstance(Ms, Commit))
  40
  41    if callDepth == 0:
  42        if len(ca) > 1:
  43            runProgram(['git-read-tree', h1.tree()])
  44            runProgram(['git-update-cache', '-q', '--refresh'])
  45        # Use the original index if we only have one common ancestor
  46        
  47        cleanCache = False
  48    else:
  49        runProgram(['git-read-tree', h1.tree()])
  50        cleanCache = True
  51
  52    [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
  53                                 branch1Name, branch2Name,
  54                                 cleanCache)
  55
  56    if clean or cleanCache:
  57        res = Commit(None, [h1, h2], tree=shaRes)
  58        graph.addNode(res)
  59    else:
  60        res = None
  61
  62    return [res, clean]
  63
  64getFilesRE = re.compile('([0-9]+) ([a-z0-9]+) ([0-9a-f]{40})\t(.*)')
  65def getFilesAndDirs(tree):
  66    files = Set()
  67    dirs = Set()
  68    out = runProgram(['git-ls-tree', '-r', '-z', tree])
  69    for l in out.split('\0'):
  70        m = getFilesRE.match(l)
  71        if m:
  72            if m.group(2) == 'tree':
  73                dirs.add(m.group(4))
  74            elif m.group(2) == 'blob':
  75                files.add(m.group(4))
  76
  77    return [files, dirs]
  78
  79class CacheEntry:
  80    def __init__(self, path):
  81        class Stage:
  82            def __init__(self):
  83                self.sha1 = None
  84                self.mode = None
  85        
  86        self.stages = [Stage(), Stage(), Stage()]
  87        self.path = path
  88
  89unmergedRE = re.compile('^([0-9]+) ([0-9a-f]{40}) ([1-3])\t(.*)$')
  90def unmergedCacheEntries():
  91    '''Create a dictionary mapping file names to CacheEntry
  92    objects. The dictionary contains one entry for every path with a
  93    non-zero stage entry.'''
  94
  95    lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
  96    lines.pop()
  97
  98    res = {}
  99    for l in lines:
 100        m = unmergedRE.match(l)
 101        if m:
 102            mode = int(m.group(1), 8)
 103            sha1 = m.group(2)
 104            stage = int(m.group(3)) - 1
 105            path = m.group(4)
 106
 107            if res.has_key(path):
 108                e = res[path]
 109            else:
 110                e = CacheEntry(path)
 111                res[path] = e
 112                
 113            e.stages[stage].mode = mode
 114            e.stages[stage].sha1 = sha1
 115        else:
 116            die('Error: Merge program failed: Unexpected output from', \
 117                'git-ls-files:', l)
 118    return res
 119
 120def mergeTrees(head, merge, common, branch1Name, branch2Name,
 121               cleanCache):
 122    '''Merge the trees 'head' and 'merge' with the common ancestor
 123    'common'. The name of the head branch is 'branch1Name' and the name of
 124    the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
 125    where tree is the resulting tree and cleanMerge is True iff the
 126    merge was clean.'''
 127    
 128    assert(isSha(head) and isSha(merge) and isSha(common))
 129
 130    if common == merge:
 131        print 'Already uptodate!'
 132        return [head, True]
 133
 134    if cleanCache:
 135        updateArg = '-i'
 136    else:
 137        updateArg = '-u'
 138
 139    runProgram(['git-read-tree', updateArg, '-m', common, head, merge])
 140    cleanMerge = True
 141
 142    [tree, code] = runProgram('git-write-tree', returnCode=True)
 143    tree = tree.rstrip()
 144    if code != 0:
 145        [files, dirs] = getFilesAndDirs(head)
 146        [filesM, dirsM] = getFilesAndDirs(merge)
 147        files.union_update(filesM)
 148        dirs.union_update(dirsM)
 149        
 150        cleanMerge = True
 151        entries = unmergedCacheEntries()
 152        for name in entries:
 153            if not processEntry(entries[name], branch1Name, branch2Name,
 154                                files, dirs, cleanCache):
 155                cleanMerge = False
 156                
 157        if cleanMerge or cleanCache:
 158            tree = runProgram('git-write-tree').rstrip()
 159        else:
 160            tree = None
 161    else:
 162        cleanMerge = True
 163
 164    return [tree, cleanMerge]
 165
 166def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
 167    '''Merge one cache entry. 'files' is a Set with the files in both of
 168    the heads that we are going to merge. 'dirs' contains the
 169    corresponding data for directories. If 'cleanCache' is True no
 170    non-zero stages will be left in the cache for the path
 171    corresponding to the entry 'entry'.'''
 172
 173# cleanCache == True  => Don't leave any non-stage 0 entries in the cache and
 174#                        don't update the working directory
 175#               False => Leave unmerged entries and update the working directory
 176
 177# clean     == True  => non-conflict case
 178#              False => conflict case
 179
 180# If cleanCache == False then the cache shouldn't be updated if clean == False
 181
 182    def updateFile(clean, sha, mode, path, onlyWd=False):
 183        updateCache = not onlyWd and (cleanCache or (not cleanCache and clean))
 184        updateWd = onlyWd or (not cleanCache and clean)
 185
 186        if updateWd:
 187            prog = ['git-cat-file', 'blob', sha]
 188            if stat.S_ISREG(mode):
 189                try:
 190                    os.unlink(path)
 191                except OSError:
 192                    pass
 193                if mode & 0100:
 194                    mode = 0777
 195                else:
 196                    mode = 0666
 197                fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
 198                proc = subprocess.Popen(prog, stdout=fd)
 199                proc.wait()
 200                os.close(fd)
 201            elif stat.S_ISLNK(mode):
 202                linkTarget = runProgram(prog)
 203                os.symlink(linkTarget, path)
 204            else:
 205                assert(False)
 206
 207        if updateWd and updateCache:
 208            runProgram(['git-update-cache', '--add', '--', path])
 209        elif updateCache:
 210            runProgram(['git-update-cache', '--add', '--cacheinfo',
 211                        '0%o' % mode, sha, path])
 212
 213    def removeFile(clean, path):
 214        if cleanCache or (not cleanCache and clean):
 215            runProgram(['git-update-cache', '--force-remove', '--', path])
 216
 217        if not cleanCache and clean:
 218            try:
 219                os.unlink(path)
 220            except OSError, e:
 221                if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
 222                    raise
 223
 224    def uniquePath(path, branch):
 225        newPath = path + '_' + branch
 226        suffix = 0
 227        while newPath in files or newPath in dirs:
 228            suffix += 1
 229            newPath = path + '_' + branch + '_' + str(suffix)
 230        files.add(newPath)
 231        return newPath
 232
 233    debug('processing', entry.path, 'clean cache:', cleanCache)
 234
 235    cleanMerge = True
 236
 237    path = entry.path
 238    oSha = entry.stages[0].sha1
 239    oMode = entry.stages[0].mode
 240    aSha = entry.stages[1].sha1
 241    aMode = entry.stages[1].mode
 242    bSha = entry.stages[2].sha1
 243    bMode = entry.stages[2].mode
 244
 245    assert(oSha == None or isSha(oSha))
 246    assert(aSha == None or isSha(aSha))
 247    assert(bSha == None or isSha(bSha))
 248
 249    assert(oMode == None or type(oMode) is int)
 250    assert(aMode == None or type(aMode) is int)
 251    assert(bMode == None or type(bMode) is int)
 252
 253    if (oSha and (not aSha or not bSha)):
 254    #
 255    # Case A: Deleted in one
 256    #
 257        if (not aSha     and not bSha) or \
 258           (aSha == oSha and not bSha) or \
 259           (not aSha     and bSha == oSha):
 260    # Deleted in both or deleted in one and unchanged in the other
 261            if aSha:
 262                print 'Removing ' + path
 263            removeFile(True, path)
 264        else:
 265    # Deleted in one and changed in the other
 266            cleanMerge = False
 267            if not aSha:
 268                print 'CONFLICT (del/mod): "' + path + '" deleted in', \
 269                      branch1Name, 'and modified in', branch2Name, \
 270                      '. Version', branch2Name, ' of "' + path + \
 271                      '" left in tree'
 272                mode = bMode
 273                sha = bSha
 274            else:
 275                print 'CONFLICT (mod/del): "' + path + '" deleted in', \
 276                      branch2Name, 'and modified in', branch1Name + \
 277                      '. Version', branch1Name, 'of "' + path + \
 278                      '" left in tree'
 279                mode = aMode
 280                sha = aSha
 281
 282            updateFile(False, sha, mode, path)
 283    
 284    elif (not oSha and aSha     and not bSha) or \
 285         (not oSha and not aSha and bSha):
 286    #
 287    # Case B: Added in one.
 288    #
 289        if aSha:
 290            addBranch = branch1Name
 291            otherBranch = branch2Name
 292            mode = aMode
 293            sha = aSha
 294            conf = 'file/dir'
 295        else:
 296            addBranch = branch2Name
 297            otherBranch = branch1Name
 298            mode = bMode
 299            sha = bSha
 300            conf = 'dir/file'
 301    
 302        if path in dirs:
 303            cleanMerge = False
 304            newPath = uniquePath(path, addBranch)
 305            print 'CONFLICT (' + conf + \
 306                  '): There is a directory with name "' + path + '" in', \
 307                  otherBranch + '. Adding "' + path + '" as "' + newPath + '"'
 308
 309            removeFile(False, path)
 310            path = newPath
 311        else:
 312            print 'Adding "' + path + '"'
 313
 314        updateFile(True, sha, mode, path)
 315    
 316    elif not oSha and aSha and bSha:
 317    #
 318    # Case C: Added in both (check for same permissions).
 319    #
 320        if aSha == bSha:
 321            if aMode != bMode:
 322                cleanMerge = False
 323                print 'CONFLICT: File "' + path + \
 324                      '" added identically in both branches,', \
 325                      'but permissions conflict', '0%o' % aMode, '->', \
 326                      '0%o' % bMode
 327                print 'CONFLICT: adding with permission:', '0%o' % aMode
 328
 329                updateFile(False, aSha, aMode, path)
 330            else:
 331                # This case is handled by git-read-tree
 332                assert(False)
 333        else:
 334            cleanMerge = False
 335            newPath1 = uniquePath(path, branch1Name)
 336            newPath2 = uniquePath(path, branch2Name)
 337            print 'CONFLICT (add/add): File "' + path + \
 338                  '" added non-identically in both branches.'
 339            removeFile(False, path)
 340            updateFile(False, aSha, aMode, newPath1)
 341            updateFile(False, bSha, bMode, newPath2)
 342
 343    elif oSha and aSha and bSha:
 344    #
 345    # case D: Modified in both, but differently.
 346    #
 347        print 'Auto-merging', path 
 348        orig = runProgram(['git-unpack-file', oSha]).rstrip()
 349        src1 = runProgram(['git-unpack-file', aSha]).rstrip()
 350        src2 = runProgram(['git-unpack-file', bSha]).rstrip()
 351        [out, ret] = runProgram(['merge',
 352                                 '-L', branch1Name + '/' + path,
 353                                 '-L', 'orig/' + path,
 354                                 '-L', branch2Name + '/' + path,
 355                                 src1, orig, src2], returnCode=True)
 356
 357        if aMode == oMode:
 358            mode = bMode
 359        else:
 360            mode = aMode
 361
 362        sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
 363                          src1]).rstrip()
 364
 365        if ret != 0:
 366            cleanMerge = False
 367            print 'CONFLICT (content): Merge conflict in "' + path + '".'
 368
 369            if cleanCache:
 370                updateFile(False, sha, mode, path)
 371            else:
 372                updateFile(True, aSha, aMode, path)
 373                updateFile(False, sha, mode, path, True)
 374        else:
 375            updateFile(True, sha, mode, path)
 376
 377        os.unlink(orig)
 378        os.unlink(src1)
 379        os.unlink(src2)
 380    else:
 381        die("ERROR: Fatal merge failure, shouldn't happen.")
 382
 383    return cleanMerge
 384
 385def usage():
 386    die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
 387
 388# main entry point as merge strategy module
 389# The first parameters up to -- are merge bases, and the rest are heads.
 390# This strategy module figures out merge bases itself, so we only
 391# get heads.
 392
 393if len(sys.argv) < 4:
 394    usage()
 395
 396for nextArg in xrange(1, len(sys.argv)):
 397    if sys.argv[nextArg] == '--':
 398        if len(sys.argv) != nextArg + 3:
 399            die('Not handling anything other than two heads merge.')
 400        try:
 401            h1 = firstBranch = sys.argv[nextArg + 1]
 402            h2 = secondBranch = sys.argv[nextArg + 2]
 403        except IndexError:
 404            usage()
 405        break
 406
 407print 'Merging', h1, 'with', h2
 408
 409try:
 410    h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
 411    h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
 412
 413    graph = buildGraph([h1, h2])
 414
 415    [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
 416                         firstBranch, secondBranch, graph)
 417
 418    print ''
 419except:
 420    traceback.print_exc(None, sys.stderr)
 421    sys.exit(2)
 422
 423if clean:
 424    sys.exit(0)
 425else:
 426    sys.exit(1)