Source

git / gitMergeCommon.py

#
# Copyright (C) 2005 Fredrik Kuivinen
#

import sys, re, os, traceback
from sets import Set

def die(*args):
    printList(args, sys.stderr)
    sys.exit(2)

def printList(list, file=sys.stdout):
    for x in list:
        file.write(str(x))
        file.write(' ')
    file.write('\n')

import subprocess

# Debugging machinery
# -------------------

DEBUG = 0
functionsToDebug = Set()

def addDebug(func):
    if type(func) == str:
        functionsToDebug.add(func)
    else:
        functionsToDebug.add(func.func_name)

def debug(*args):
    if DEBUG:
        funcName = traceback.extract_stack()[-2][2]
        if funcName in functionsToDebug:
            printList(args)

# Program execution
# -----------------

class ProgramError(Exception):
    def __init__(self, progStr, error):
        self.progStr = progStr
        self.error = error

    def __str__(self):
        return self.progStr + ': ' + self.error

addDebug('runProgram')
def runProgram(prog, input=None, returnCode=False, env=None, pipeOutput=True):
    debug('runProgram prog:', str(prog), 'input:', str(input))
    if type(prog) is str:
        progStr = prog
    else:
        progStr = ' '.join(prog)
    
    try:
        if pipeOutput:
            stderr = subprocess.STDOUT
            stdout = subprocess.PIPE
        else:
            stderr = None
            stdout = None
        pop = subprocess.Popen(prog,
                               shell = type(prog) is str,
                               stderr=stderr,
                               stdout=stdout,
                               stdin=subprocess.PIPE,
                               env=env)
    except OSError, e:
        debug('strerror:', e.strerror)
        raise ProgramError(progStr, e.strerror)

    if input != None:
        pop.stdin.write(input)
    pop.stdin.close()

    if pipeOutput:
        out = pop.stdout.read()
    else:
        out = ''

    code = pop.wait()
    if returnCode:
        ret = [out, code]
    else:
        ret = out
    if code != 0 and not returnCode:
        debug('error output:', out)
        debug('prog:', prog)
        raise ProgramError(progStr, out)
#    debug('output:', out.replace('\0', '\n'))
    return ret

# Code for computing common ancestors
# -----------------------------------

currentId = 0
def getUniqueId():
    global currentId
    currentId += 1
    return currentId

# The 'virtual' commit objects have SHAs which are integers
shaRE = re.compile('^[0-9a-f]{40}$')
def isSha(obj):
    return (type(obj) is str and bool(shaRE.match(obj))) or \
           (type(obj) is int and obj >= 1)

class Commit:
    def __init__(self, sha, parents, tree=None):
        self.parents = parents
        self.firstLineMsg = None
        self.children = []

        if tree:
            tree = tree.rstrip()
            assert(isSha(tree))
        self._tree = tree

        if not sha:
            self.sha = getUniqueId()
            self.virtual = True
            self.firstLineMsg = 'virtual commit'
            assert(isSha(tree))
        else:
            self.virtual = False
            self.sha = sha.rstrip()
        assert(isSha(self.sha))

    def tree(self):
        self.getInfo()
        assert(self._tree != None)
        return self._tree

    def shortInfo(self):
        self.getInfo()
        return str(self.sha) + ' ' + self.firstLineMsg

    def __str__(self):
        return self.shortInfo()

    def getInfo(self):
        if self.virtual or self.firstLineMsg != None:
            return
        else:
            info = runProgram(['git-cat-file', 'commit', self.sha])
            info = info.split('\n')
            msg = False
            for l in info:
                if msg:
                    self.firstLineMsg = l
                    break
                else:
                    if l.startswith('tree'):
                        self._tree = l[5:].rstrip()
                    elif l == '':
                        msg = True

class Graph:
    def __init__(self):
        self.commits = []
        self.shaMap = {}

    def addNode(self, node):
        assert(isinstance(node, Commit))
        self.shaMap[node.sha] = node
        self.commits.append(node)
        for p in node.parents:
            p.children.append(node)
        return node

    def reachableNodes(self, n1, n2):
        res = {}
        def traverse(n):
            res[n] = True
            for p in n.parents:
                traverse(p)

        traverse(n1)
        traverse(n2)
        return res

    def fixParents(self, node):
        for x in range(0, len(node.parents)):
            node.parents[x] = self.shaMap[node.parents[x]]

# addDebug('buildGraph')
def buildGraph(heads):
    debug('buildGraph heads:', heads)
    for h in heads:
        assert(isSha(h))

    g = Graph()

    out = runProgram(['git-rev-list', '--parents'] + heads)
    for l in out.split('\n'):
        if l == '':
            continue
        shas = l.split(' ')

        # This is a hack, we temporarily use the 'parents' attribute
        # to contain a list of SHA1:s. They are later replaced by proper
        # Commit objects.
        c = Commit(shas[0], shas[1:])

        g.commits.append(c)
        g.shaMap[c.sha] = c

    for c in g.commits:
        g.fixParents(c)

    for c in g.commits:
        for p in c.parents:
            p.children.append(c)
    return g

# Write the empty tree to the object database and return its SHA1
def writeEmptyTree():
    tmpIndex = os.environ.get('GIT_DIR', '.git') + '/merge-tmp-index'
    def delTmpIndex():
        try:
            os.unlink(tmpIndex)
        except OSError:
            pass
    delTmpIndex()
    newEnv = os.environ.copy()
    newEnv['GIT_INDEX_FILE'] = tmpIndex
    res = runProgram(['git-write-tree'], env=newEnv).rstrip()
    delTmpIndex()
    return res

def addCommonRoot(graph):
    roots = []
    for c in graph.commits:
        if len(c.parents) == 0:
            roots.append(c)

    superRoot = Commit(sha=None, parents=[], tree=writeEmptyTree())
    graph.addNode(superRoot)
    for r in roots:
        r.parents = [superRoot]
    superRoot.children = roots
    return superRoot

def getCommonAncestors(graph, commit1, commit2):
    '''Find the common ancestors for commit1 and commit2'''
    assert(isinstance(commit1, Commit) and isinstance(commit2, Commit))

    def traverse(start, set):
        stack = [start]
        while len(stack) > 0:
            el = stack.pop()
            set.add(el)
            for p in el.parents:
                if p not in set:
                    stack.append(p)
    h1Set = Set()
    h2Set = Set()
    traverse(commit1, h1Set)
    traverse(commit2, h2Set)
    shared = h1Set.intersection(h2Set)

    if len(shared) == 0:
        shared = [addCommonRoot(graph)]
        
    res = Set()

    for s in shared:
        if len([c for c in s.children if c in shared]) == 0:
            res.add(s)
    return list(res)