Add a new merge strategy by Fredrik Kuivinen.

I really wanted to try this out, instead of asking for an adjustment
to the 'git merge' driver and waiting.  For now the new strategy is
called 'fredrik' and not in the list of default strategies to be tried.

The script wants Python 2.4 so this commit also adjusts Debian and RPM
build procecure files.

Signed-off-by: Junio C Hamano <junkio@cox.net>
This commit is contained in:
Junio C Hamano 2005-09-10 17:46:27 -07:00
parent 0f69be5376
commit 720d150c48
9 changed files with 741 additions and 7 deletions

1
.gitignore vendored
View File

@ -43,6 +43,7 @@ git-mailinfo
git-mailsplit git-mailsplit
git-merge git-merge
git-merge-base git-merge-base
git-merge-fredrik
git-merge-index git-merge-index
git-merge-octopus git-merge-octopus
git-merge-one-file git-merge-one-file

View File

@ -48,6 +48,7 @@ ALL_CFLAGS = $(CFLAGS) $(PLATFORM_DEFINES) $(DEFINES)
prefix = $(HOME) prefix = $(HOME)
bindir = $(prefix)/bin bindir = $(prefix)/bin
template_dir = $(prefix)/share/git-core/templates/ template_dir = $(prefix)/share/git-core/templates/
GIT_PYTHON_DIR = $(prefix)/share/git-core/python
# DESTDIR= # DESTDIR=
CC = gcc CC = gcc
@ -81,6 +82,9 @@ SCRIPT_PERL = \
git-archimport.perl git-cvsimport.perl git-relink.perl \ git-archimport.perl git-cvsimport.perl git-relink.perl \
git-rename.perl git-shortlog.perl git-rename.perl git-shortlog.perl
SCRIPT_PYTHON = \
git-merge-fredrik.py
# The ones that do not have to link with lcrypto nor lz. # The ones that do not have to link with lcrypto nor lz.
SIMPLE_PROGRAMS = \ SIMPLE_PROGRAMS = \
git-get-tar-commit-id git-mailinfo git-mailsplit git-stripspace \ git-get-tar-commit-id git-mailinfo git-mailsplit git-stripspace \
@ -105,6 +109,9 @@ PROGRAMS = \
git-upload-pack git-verify-pack git-write-tree \ git-upload-pack git-verify-pack git-write-tree \
$(SIMPLE_PROGRAMS) $(SIMPLE_PROGRAMS)
PYMODULES = \
gitMergeCommon.py
ifdef WITH_SEND_EMAIL ifdef WITH_SEND_EMAIL
SCRIPT_PERL += git-send-email.perl SCRIPT_PERL += git-send-email.perl
endif endif
@ -150,6 +157,9 @@ endif
ifndef PERL_PATH ifndef PERL_PATH
PERL_PATH = /usr/bin/perl PERL_PATH = /usr/bin/perl
endif endif
ifndef PYTHON_PATH
PYTHON_PATH = /usr/bin/python
endif
ifndef NO_OPENSSL ifndef NO_OPENSSL
LIB_OBJS += epoch.o LIB_OBJS += epoch.o
@ -189,7 +199,9 @@ endif
DEFINES += '-DSHA1_HEADER=$(SHA1_HEADER)' DEFINES += '-DSHA1_HEADER=$(SHA1_HEADER)'
SCRIPTS = $(patsubst %.sh,%,$(SCRIPT_SH)) \ SCRIPTS = $(patsubst %.sh,%,$(SCRIPT_SH)) \
$(patsubst %.perl,%,$(SCRIPT_PERL)) gitk $(patsubst %.perl,%,$(SCRIPT_PERL)) \
$(patsubst %.py,%,$(SCRIPT_PYTHON)) \
gitk
### Build rules ### Build rules
@ -214,6 +226,13 @@ $(patsubst %.perl,%,$(SCRIPT_PERL)) : % : %.perl
sed -e '1s|#!.*perl|#!$(PERL_PATH)|' $@.perl >$@ sed -e '1s|#!.*perl|#!$(PERL_PATH)|' $@.perl >$@
chmod +x $@ chmod +x $@
$(patsubst %.py,%,$(SCRIPT_PYTHON)) : % : %.py
rm -f $@
sed -e '1s|#!.*python|#!$(PYTHON_PATH)|' \
-e 's|@@GIT_PYTHON_PATH@@|$(GIT_PYTHON_DIR)|g' \
$@.py >$@
chmod +x $@
%.o: %.c %.o: %.c
$(CC) -o $*.o -c $(ALL_CFLAGS) $< $(CC) -o $*.o -c $(ALL_CFLAGS) $<
%.o: %.S %.o: %.S
@ -274,6 +293,8 @@ install: $(PROGRAMS) $(SCRIPTS)
$(INSTALL) git-revert $(DESTDIR)$(bindir)/git-cherry-pick $(INSTALL) git-revert $(DESTDIR)$(bindir)/git-cherry-pick
sh ./cmd-rename.sh $(DESTDIR)$(bindir) sh ./cmd-rename.sh $(DESTDIR)$(bindir)
$(MAKE) -C templates install $(MAKE) -C templates install
$(INSTALL) -m755 -d $(DESTDIR)$(GIT_PYTHON_DIR)
$(INSTALL) $(PYMODULES) $(DESTDIR)$(GIT_PYTHON_DIR)
install-doc: install-doc:
$(MAKE) -C Documentation install $(MAKE) -C Documentation install

2
debian/control vendored
View File

@ -8,7 +8,7 @@ Standards-Version: 3.6.1
Package: git-core Package: git-core
Architecture: any Architecture: any
Depends: ${shlibs:Depends}, ${perl:Depends}, ${misc:Depends}, patch, rcs Depends: ${shlibs:Depends}, ${perl:Depends}, ${misc:Depends}, patch, rcs
Recommends: rsync, curl, ssh, libmail-sendmail-perl, libemail-valid-perl Recommends: rsync, curl, ssh, libmail-sendmail-perl, libemail-valid-perl, python (>= 2.4.0)
Suggests: cogito Suggests: cogito
Conflicts: git, cogito (<< 0.13) Conflicts: git, cogito (<< 0.13)
Description: The git content addressable filesystem Description: The git content addressable filesystem

2
debian/rules vendored
View File

@ -41,7 +41,7 @@ MAN_DESTDIR := $(DESTDIR)/$(MANDIR)
build: debian/build-stamp build: debian/build-stamp
debian/build-stamp: debian/build-stamp:
dh_testdir dh_testdir
$(MAKE) prefix=$(PREFIX) all doc $(MAKE) prefix=$(PREFIX) PYTHON_PATH=/usr/bin/python2.4 all doc
touch debian/build-stamp touch debian/build-stamp
debian-clean: debian-clean:

View File

@ -10,7 +10,7 @@ URL: http://kernel.org/pub/software/scm/git/
Source: http://kernel.org/pub/software/scm/git/%{name}-%{version}.tar.gz Source: http://kernel.org/pub/software/scm/git/%{name}-%{version}.tar.gz
BuildRequires: zlib-devel, openssl-devel, curl-devel %{!?_without_docs:, xmlto, asciidoc > 6.0.3} BuildRequires: zlib-devel, openssl-devel, curl-devel %{!?_without_docs:, xmlto, asciidoc > 6.0.3}
BuildRoot: %{_tmppath}/%{name}-%{version}-root BuildRoot: %{_tmppath}/%{name}-%{version}-root
Requires: sh-utils, diffutils, rsync, rcs, mktemp >= 1.5 Requires: sh-utils, diffutils, rsync, rcs, python >= 2.4
%description %description
This is a stupid (but extremely fast) directory content manager. It This is a stupid (but extremely fast) directory content manager. It

426
git-merge-fredrik.py Executable file
View File

@ -0,0 +1,426 @@
#!/usr/bin/python
import sys, math, random, os, re, signal, tempfile, stat, errno
from heapq import heappush, heappop
from sets import Set
sys.path.append('@@GIT_PYTHON_PATH@@')
from gitMergeCommon import *
alwaysWriteTree = False
# The actual merge code
# ---------------------
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:
if len(ca) > 1:
runProgram(['git-read-tree', h1.tree()])
runProgram(['git-update-cache', '-q', '--refresh'])
# Use the original index if we only have one common ancestor
updateWd = True
if alwaysWriteTree:
cleanCache = True
else:
cleanCache = False
else:
runProgram(['git-read-tree', h1.tree()])
updateWd = False
cleanCache = True
[shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
branch1Name, branch2Name,
cleanCache, updateWd)
if clean or alwaysWriteTree:
res = Commit(None, [h1, h2], tree=shaRes)
graph.addNode(res)
else:
res = None
return [res, clean]
getFilesRE = re.compile('([0-9]+) ([a-z0-9]+) ([0-9a-f]{40})\t(.*)')
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('^([0-9]+) ([0-9a-f]{40}) ([1-3])\t(.*)$')
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:
print 'Error: Merge program failed: Unexpected output from', \
'git-ls-files:', l
sys.exit(2)
return res
def mergeTrees(head, merge, common, branch1Name, branch2Name,
cleanCache, updateWd):
'''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 updateWd:
updateArg = '-u'
else:
updateArg = '-i'
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, updateWd):
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, updateWd):
'''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.
# False => Leave unmerged entries
# updateWd == True => Update the working directory to correspond to the cache
# False => Leave the working directory unchanged
# 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):
if cleanCache or (not cleanCache and clean):
runProgram(['git-update-cache', '--add', '--cacheinfo',
'0%o' % mode, sha, path])
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)
runProgram(['git-update-cache', '--', path])
def removeFile(clean, path):
if cleanCache or (not cleanCache and clean):
runProgram(['git-update-cache', '--force-remove', '--', path])
if updateWd:
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,
'wd:', updateWd)
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,'
print 'CONFLICT: 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.', \
'Adding "' + newPath1 + '" and "' + newPath2 + '" instead.'
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 + '".'
updateFile(False, sha, mode, path)
else:
updateFile(True, sha, mode, path)
os.unlink(orig)
os.unlink(src1)
os.unlink(src2)
else:
print 'ERROR: Fatal merge failure.'
print "ERROR: Shouldn't happen"
sys.exit(2)
return cleanMerge
def usage():
print 'Usage:', sys.argv[0], ' <base>... -- <head> <remote>..'
sys.exit(2)
# 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.
for nextArg in xrange(1, len(sys.argv)):
if sys.argv[nextArg] == '--':
if len(sys.argv) != nextArg + 3:
print 'Not handling anything other than two heads merge.'
sys.exit(2)
try:
h1 = firstBranch = sys.argv[nextArg + 1]
h2 = secondBranch = sys.argv[nextArg + 2]
except IndexError:
usage()
break
print 'Merging', h1, 'with', h2
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 ''
if clean:
sys.exit(0)
else:
print 'Automatic merge failed, fix up by hand'
sys.exit(1)

View File

@ -14,7 +14,7 @@ usage () {
# all_strategies='resolve fredrik stupid octopus' # all_strategies='resolve fredrik stupid octopus'
all_strategies='resolve octopus stupid' all_strategies='fredrik octopus resolve stupid'
default_strategies='resolve octopus' default_strategies='resolve octopus'
use_strategies= use_strategies=

268
gitMergeCommon.py Normal file
View File

@ -0,0 +1,268 @@
import sys, re, os, traceback
from sets import Set
if sys.version_info[0] < 2 or \
(sys.version_info[0] == 2 and sys.version_info[1] < 4):
print 'Python version 2.4 required, found', \
str(sys.version_info[0])+'.'+str(sys.version_info[1])+'.'+ \
str(sys.version_info[2])
sys.exit(1)
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)
def printList(list):
for x in list:
sys.stdout.write(str(x))
sys.stdout.write(' ')
sys.stdout.write('\n')
# Program execution
# -----------------
class ProgramError(Exception):
def __init__(self, progStr, error):
self.progStr = progStr
self.error = 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['GIT_DIR'] + '/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)

View File

@ -12,6 +12,7 @@
static int merge = 0; static int merge = 0;
static int update = 0; static int update = 0;
static int index_only = 0;
static int head_idx = -1; static int head_idx = -1;
static int merge_size = 0; static int merge_size = 0;
@ -306,6 +307,9 @@ static void verify_uptodate(struct cache_entry *ce)
{ {
struct stat st; struct stat st;
if (index_only)
return;
if (!lstat(ce->name, &st)) { if (!lstat(ce->name, &st)) {
unsigned changed = ce_match_stat(ce, &st); unsigned changed = ce_match_stat(ce, &st);
if (!changed) if (!changed)
@ -576,7 +580,7 @@ static int read_cache_unmerged(void)
return deleted; return deleted;
} }
static const char read_tree_usage[] = "git-read-tree (<sha> | -m [-u] <sha1> [<sha2> [<sha3>]])"; static const char read_tree_usage[] = "git-read-tree (<sha> | -m [-u | -i] <sha1> [<sha2> [<sha3>]])";
static struct cache_file cache_file; static struct cache_file cache_file;
@ -595,12 +599,22 @@ int main(int argc, char **argv)
for (i = 1; i < argc; i++) { for (i = 1; i < argc; i++) {
const char *arg = argv[i]; const char *arg = argv[i];
/* "-u" means "update", meaning that a merge will update the working directory */ /* "-u" means "update", meaning that a merge will update
* the working tree.
*/
if (!strcmp(arg, "-u")) { if (!strcmp(arg, "-u")) {
update = 1; update = 1;
continue; continue;
} }
/* "-i" means "index only", meaning that a merge will
* not even look at the working tree.
*/
if (!strcmp(arg, "-i")) {
index_only = 1;
continue;
}
/* This differs from "-m" in that we'll silently ignore unmerged entries */ /* This differs from "-m" in that we'll silently ignore unmerged entries */
if (!strcmp(arg, "--reset")) { if (!strcmp(arg, "--reset")) {
if (stage || merge) if (stage || merge)
@ -628,6 +642,10 @@ int main(int argc, char **argv)
continue; continue;
} }
/* using -u and -i at the same time makes no sense */
if (1 < index_only + update)
usage(read_tree_usage);
if (get_sha1(arg, sha1) < 0) if (get_sha1(arg, sha1) < 0)
usage(read_tree_usage); usage(read_tree_usage);
if (list_tree(sha1) < 0) if (list_tree(sha1) < 0)