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