Staging
v0.5.1
https://github.com/git/git
Raw File
Tip revision: d5bc7eecbbb0b9f6122708bf5cd62f78ebdaafd8 authored by Junio C Hamano on 25 September 2005, 07:36:26 UTC
GIT v0.99.7d
Tip revision: d5bc7ee
git-merge-recursive.py
#!/usr/bin/python

import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
from heapq import heappush, heappop
from sets import Set

sys.path.append('@@GIT_PYTHON_PATH@@')
from gitMergeCommon import *

# The actual merge code
# ---------------------

originalIndexFile = os.environ.get('GIT_INDEX_FILE',
                                   os.environ.get('GIT_DIR', '.git') + '/index')
temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
                     '/merge-recursive-tmp-index'
def setupIndex(temporary):
    try:
        os.unlink(temporaryIndexFile)
    except OSError:
        pass
    if temporary:
        newIndex = temporaryIndexFile
        os.environ
    else:
        newIndex = originalIndexFile
    os.environ['GIT_INDEX_FILE'] = newIndex

def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
    '''Merge the commits h1 and h2, return the resulting virtual
    commit object and a flag indicating the cleaness of the merge.'''
    assert(isinstance(h1, Commit) and isinstance(h2, Commit))
    assert(isinstance(graph, Graph))

    def infoMsg(*args):
        sys.stdout.write('  '*callDepth)
        printList(args)
    infoMsg('Merging:')
    infoMsg(h1)
    infoMsg(h2)
    sys.stdout.flush()

    ca = getCommonAncestors(graph, h1, h2)
    infoMsg('found', len(ca), 'common ancestor(s):')
    for x in ca:
        infoMsg(x)
    sys.stdout.flush()

    Ms = ca[0]
    for h in ca[1:]:
        [Ms, ignore] = merge(Ms, h,
                             'Temporary shared merge branch 1',
                             'Temporary shared merge branch 2',
                             graph, callDepth+1)
        assert(isinstance(Ms, Commit))

    if callDepth == 0:
        setupIndex(False)
        cleanCache = False
    else:
        setupIndex(True)
        runProgram(['git-read-tree', h1.tree()])
        cleanCache = True

    [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
                                 branch1Name, branch2Name,
                                 cleanCache)

    if clean or cleanCache:
        res = Commit(None, [h1, h2], tree=shaRes)
        graph.addNode(res)
    else:
        res = None

    return [res, clean]

getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
def getFilesAndDirs(tree):
    files = Set()
    dirs = Set()
    out = runProgram(['git-ls-tree', '-r', '-z', tree])
    for l in out.split('\0'):
        m = getFilesRE.match(l)
        if m:
            if m.group(2) == 'tree':
                dirs.add(m.group(4))
            elif m.group(2) == 'blob':
                files.add(m.group(4))

    return [files, dirs]

class CacheEntry:
    def __init__(self, path):
        class Stage:
            def __init__(self):
                self.sha1 = None
                self.mode = None
        
        self.stages = [Stage(), Stage(), Stage()]
        self.path = path

unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
def unmergedCacheEntries():
    '''Create a dictionary mapping file names to CacheEntry
    objects. The dictionary contains one entry for every path with a
    non-zero stage entry.'''

    lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
    lines.pop()

    res = {}
    for l in lines:
        m = unmergedRE.match(l)
        if m:
            mode = int(m.group(1), 8)
            sha1 = m.group(2)
            stage = int(m.group(3)) - 1
            path = m.group(4)

            if res.has_key(path):
                e = res[path]
            else:
                e = CacheEntry(path)
                res[path] = e
                
            e.stages[stage].mode = mode
            e.stages[stage].sha1 = sha1
        else:
            die('Error: Merge program failed: Unexpected output from', \
                'git-ls-files:', l)
    return res

def mergeTrees(head, merge, common, branch1Name, branch2Name,
               cleanCache):
    '''Merge the trees 'head' and 'merge' with the common ancestor
    'common'. The name of the head branch is 'branch1Name' and the name of
    the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
    where tree is the resulting tree and cleanMerge is True iff the
    merge was clean.'''
    
    assert(isSha(head) and isSha(merge) and isSha(common))

    if common == merge:
        print 'Already uptodate!'
        return [head, True]

    if cleanCache:
        updateArg = '-i'
    else:
        updateArg = '-u'

    runProgram(['git-read-tree', updateArg, '-m', common, head, merge])
    cleanMerge = True

    [tree, code] = runProgram('git-write-tree', returnCode=True)
    tree = tree.rstrip()
    if code != 0:
        [files, dirs] = getFilesAndDirs(head)
        [filesM, dirsM] = getFilesAndDirs(merge)
        files.union_update(filesM)
        dirs.union_update(dirsM)
        
        cleanMerge = True
        entries = unmergedCacheEntries()
        for name in entries:
            if not processEntry(entries[name], branch1Name, branch2Name,
                                files, dirs, cleanCache):
                cleanMerge = False
                
        if cleanMerge or cleanCache:
            tree = runProgram('git-write-tree').rstrip()
        else:
            tree = None
    else:
        cleanMerge = True

    return [tree, cleanMerge]

def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
    '''Merge one cache entry. 'files' is a Set with the files in both of
    the heads that we are going to merge. 'dirs' contains the
    corresponding data for directories. If 'cleanCache' is True no
    non-zero stages will be left in the cache for the path
    corresponding to the entry 'entry'.'''

# cleanCache == True  => Don't leave any non-stage 0 entries in the cache and
#                        don't update the working directory
#               False => Leave unmerged entries and update the working directory

# clean     == True  => non-conflict case
#              False => conflict case

# If cleanCache == False then the cache shouldn't be updated if clean == False

    def updateFile(clean, sha, mode, path, onlyWd=False):
        updateCache = not onlyWd and (cleanCache or (not cleanCache and clean))
        updateWd = onlyWd or (not cleanCache and clean)

        if updateWd:
            prog = ['git-cat-file', 'blob', sha]
            if stat.S_ISREG(mode):
                try:
                    os.unlink(path)
                except OSError:
                    pass
                if mode & 0100:
                    mode = 0777
                else:
                    mode = 0666
                fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
                proc = subprocess.Popen(prog, stdout=fd)
                proc.wait()
                os.close(fd)
            elif stat.S_ISLNK(mode):
                linkTarget = runProgram(prog)
                os.symlink(linkTarget, path)
            else:
                assert(False)

        if updateWd and updateCache:
            runProgram(['git-update-index', '--add', '--', path])
        elif updateCache:
            runProgram(['git-update-index', '--add', '--cacheinfo',
                        '0%o' % mode, sha, path])

    def removeFile(clean, path):
        if cleanCache or (not cleanCache and clean):
            runProgram(['git-update-index', '--force-remove', '--', path])

        if not cleanCache and clean:
            try:
                os.unlink(path)
            except OSError, e:
                if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
                    raise

    def uniquePath(path, branch):
        newPath = path + '_' + branch
        suffix = 0
        while newPath in files or newPath in dirs:
            suffix += 1
            newPath = path + '_' + branch + '_' + str(suffix)
        files.add(newPath)
        return newPath

    debug('processing', entry.path, 'clean cache:', cleanCache)

    cleanMerge = True

    path = entry.path
    oSha = entry.stages[0].sha1
    oMode = entry.stages[0].mode
    aSha = entry.stages[1].sha1
    aMode = entry.stages[1].mode
    bSha = entry.stages[2].sha1
    bMode = entry.stages[2].mode

    assert(oSha == None or isSha(oSha))
    assert(aSha == None or isSha(aSha))
    assert(bSha == None or isSha(bSha))

    assert(oMode == None or type(oMode) is int)
    assert(aMode == None or type(aMode) is int)
    assert(bMode == None or type(bMode) is int)

    if (oSha and (not aSha or not bSha)):
    #
    # Case A: Deleted in one
    #
        if (not aSha     and not bSha) or \
           (aSha == oSha and not bSha) or \
           (not aSha     and bSha == oSha):
    # Deleted in both or deleted in one and unchanged in the other
            if aSha:
                print 'Removing ' + path
            removeFile(True, path)
        else:
    # Deleted in one and changed in the other
            cleanMerge = False
            if not aSha:
                print 'CONFLICT (del/mod): "' + path + '" deleted in', \
                      branch1Name, 'and modified in', branch2Name, \
                      '. Version', branch2Name, ' of "' + path + \
                      '" left in tree'
                mode = bMode
                sha = bSha
            else:
                print 'CONFLICT (mod/del): "' + path + '" deleted in', \
                      branch2Name, 'and modified in', branch1Name + \
                      '. Version', branch1Name, 'of "' + path + \
                      '" left in tree'
                mode = aMode
                sha = aSha

            updateFile(False, sha, mode, path)
    
    elif (not oSha and aSha     and not bSha) or \
         (not oSha and not aSha and bSha):
    #
    # Case B: Added in one.
    #
        if aSha:
            addBranch = branch1Name
            otherBranch = branch2Name
            mode = aMode
            sha = aSha
            conf = 'file/dir'
        else:
            addBranch = branch2Name
            otherBranch = branch1Name
            mode = bMode
            sha = bSha
            conf = 'dir/file'
    
        if path in dirs:
            cleanMerge = False
            newPath = uniquePath(path, addBranch)
            print 'CONFLICT (' + conf + \
                  '): There is a directory with name "' + path + '" in', \
                  otherBranch + '. Adding "' + path + '" as "' + newPath + '"'

            removeFile(False, path)
            path = newPath
        else:
            print 'Adding "' + path + '"'

        updateFile(True, sha, mode, path)
    
    elif not oSha and aSha and bSha:
    #
    # Case C: Added in both (check for same permissions).
    #
        if aSha == bSha:
            if aMode != bMode:
                cleanMerge = False
                print 'CONFLICT: File "' + path + \
                      '" added identically in both branches,', \
                      'but permissions conflict', '0%o' % aMode, '->', \
                      '0%o' % bMode
                print 'CONFLICT: adding with permission:', '0%o' % aMode

                updateFile(False, aSha, aMode, path)
            else:
                # This case is handled by git-read-tree
                assert(False)
        else:
            cleanMerge = False
            newPath1 = uniquePath(path, branch1Name)
            newPath2 = uniquePath(path, branch2Name)
            print 'CONFLICT (add/add): File "' + path + \
                  '" added non-identically in both branches.'
            removeFile(False, path)
            updateFile(False, aSha, aMode, newPath1)
            updateFile(False, bSha, bMode, newPath2)

    elif oSha and aSha and bSha:
    #
    # case D: Modified in both, but differently.
    #
        print 'Auto-merging', path 
        orig = runProgram(['git-unpack-file', oSha]).rstrip()
        src1 = runProgram(['git-unpack-file', aSha]).rstrip()
        src2 = runProgram(['git-unpack-file', bSha]).rstrip()
        [out, ret] = runProgram(['merge',
                                 '-L', branch1Name + '/' + path,
                                 '-L', 'orig/' + path,
                                 '-L', branch2Name + '/' + path,
                                 src1, orig, src2], returnCode=True)

        if aMode == oMode:
            mode = bMode
        else:
            mode = aMode

        sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
                          src1]).rstrip()

        if ret != 0:
            cleanMerge = False
            print 'CONFLICT (content): Merge conflict in "' + path + '".'

            if cleanCache:
                updateFile(False, sha, mode, path)
            else:
                updateFile(True, aSha, aMode, path)
                updateFile(False, sha, mode, path, True)
        else:
            updateFile(True, sha, mode, path)

        os.unlink(orig)
        os.unlink(src1)
        os.unlink(src2)
    else:
        die("ERROR: Fatal merge failure, shouldn't happen.")

    return cleanMerge

def usage():
    die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')

# main entry point as merge strategy module
# The first parameters up to -- are merge bases, and the rest are heads.
# This strategy module figures out merge bases itself, so we only
# get heads.

if len(sys.argv) < 4:
    usage()

for nextArg in xrange(1, len(sys.argv)):
    if sys.argv[nextArg] == '--':
        if len(sys.argv) != nextArg + 3:
            die('Not handling anything other than two heads merge.')
        try:
            h1 = firstBranch = sys.argv[nextArg + 1]
            h2 = secondBranch = sys.argv[nextArg + 2]
	except IndexError:
            usage()
        break

print 'Merging', h1, 'with', h2

try:
    h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
    h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()

    graph = buildGraph([h1, h2])

    [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
                         firstBranch, secondBranch, graph)

    print ''
except:
    traceback.print_exc(None, sys.stderr)
    sys.exit(2)

if clean:
    sys.exit(0)
else:
    sys.exit(1)
back to top