Hi, I have cleaned up the code for the merge algorithm quite a lot. It is in a much better shape now.
The main changes compared to the previous version are: * Lots of clean up. * Some of the scripts have been renamed to better match the naming convention used in Git. * A new option ('-s') has been added to git-merge-cache * Unclean merges are detected and reported * Clean merges are committed The user interface has been changed to: git-merge-script <branch> <message> HEAD will be merged with <branch>. If the merge turns out to be clean then it will be committed with the message <message>. The working directory will always be updated to reflect the result of the merge. git-merge-cache has a new flag, '-s'. When this flag is given git-merge-cache will only run the merge-script once, instead of once per cache entry. The needed information is written to the merge-scripts stdin in the following form: <oSha> SP <aSha> SP <bSha> SP <oMode> SP <aMode> SP <bMode> SP <path> NUL where <oSha> - original file SHA1 (or empty) <aSha> - file in branch1 SHA1 (or empty) <bSha> - file in branch2 SHA1 (or empty) <oMode> - orignal file mode (or empty) <aMode> - file in branch1 mode (or empty) <bMode> - file in branch2 mode (or empty) <path> - pathname in repository SP is a single space and NUL is \0. An empty SHA1 is represented as '0000000000000000000000000000000000000000' and an empty mode is represented with '0'. This flag is nice to have when you have a merge-script which needs to keep some state between different cache entries. - Fredrik Signed-off-by: Fredrik Kuivinen <[EMAIL PROTECTED]> --- Makefile | 3 - git-merge-files-script | 188 ++++++++++++++++++++++++++++++++ git-merge-script | 94 ++++++++++++++++ gitMergeCommon.py | 86 +++++++++++++++ gitMergeCore.py | 281 ++++++++++++++++++++++++++++++++++++++++++++++++ merge-cache.c | 78 +++++++++++++ read-tree.c | 13 ++ 7 files changed, 737 insertions(+), 6 deletions(-) create mode 100755 git-merge-files-script create mode 100755 git-merge-script create mode 100644 gitMergeCommon.py create mode 100644 gitMergeCore.py diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -66,7 +66,8 @@ SCRIPTS=git git-apply-patch-script git-m git-format-patch-script git-sh-setup-script git-push-script \ git-branch-script git-parse-remote-script git-verify-tag-script \ git-ls-remote-script git-clone-dumb-http git-rename-script \ - git-request-pull-script git-bisect-script + git-request-pull-script git-bisect-script git-merge-script \ + gitMergeCommon.py gitMergeCore.py git-merge-files-script SCRIPTS += git-count-objects-script SCRIPTS += git-revert-script diff --git a/git-merge-files-script b/git-merge-files-script new file mode 100755 --- /dev/null +++ b/git-merge-files-script @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# +# This script is based on 'git-merge-one-file-script' in the Git +# distribution. +# +# This script is supposed to be executed by git-merge-cache. When +# git-merge-cache is used with the '-s' flag it will write entries to +# the executed programs stdin. The syntax of one entry is: +# +# <oSha> SP <aSha> SP <bSha> SP <oMode> SP <aMode> SP <bMode> SP <path> NUL +# +# <oSha> - original file SHA1 (or empty) +# <aSha> - file in branch1 SHA1 (or empty) +# <bSha> - file in branch2 SHA1 (or empty) +# <oMode> - orignal file mode (or empty) +# <aMode> - file in branch1 mode (or empty) +# <bMode> - file in branch2 mode (or empty) +# <path> - pathname in repository +# +# SP is a single space and NUL is \0. An empty SHA1 is represented as +# '0000000000000000000000000000000000000000' and an empty mode is +# represented with '0'. + +import sys, os, re +from gitMergeCommon import * +from sets import Set + +# TODO +# Generate a unique file name when we get a name conflict. + +# x <==> sha != '', - <==> sha == '' +# +# Case o a b +# D x x x +# A x x - +# A x - x +# A x - - +# C - x x +# B - x - +# B - - x +# - - - Doesn't happen +def processEntry(oSha, aSha, bSha, oMode, aMode, bMode, path): + global cleanMerge + debug('processing', path) + if oSha == '0'*40: + oSha = '' + if aSha == '0'*40: + aSha = '' + if bSha == '0'*40: + bSha = '' + if oMode == '0': + oMode = '' + if aMode == '0': + aMode = '' + if bMode == '0': + bMode = '' + + if (oSha != '' and (aSha == '' or bSha == '')): + # + # Case A: Deleted in one + # + if (aSha == '' and bSha == '') or \ + (aSha == oSha and bSha == '') or \ + (aSha == '' and bSha == oSha): + # Deleted in both or deleted in one and unchanged in the other + print 'Removing ' + path + runProgram(['git-update-cache', '--force-remove', '--', path]) + else: + # Deleted in one and changed in the other + cleanMerge = False + if aSha == '': + print 'CONFLICT (del/mod): "' + path + '" deleted in', \ + branch1, 'and modified in', branch2, '. Version', branch2, ' of "' + path + '" left in tree' + mode = bMode + sha = bSha + else: + print 'CONFLICT (mod/del): "' + path + '" deleted in', branch2, 'and modified in', branch1, \ + '. Version', branch1, 'of "' + path + '" left in tree' + mode = aMode + sha = aSha + runProgram(['git-update-cache', '--cacheinfo', mode, sha, path]) + + elif (oSha == '' and aSha != '' and bSha == '') or \ + (oSha == '' and aSha == '' and bSha != ''): + # + # Case B: Added in one. + # + if aSha != '': + addBranch = branch1 + otherBranch = branch2 + conf = 'file/dir' + else: + addBranch = branch2 + otherBranch = branch1 + conf = 'dir/file' + + if path in dirs: + cleanMerge = False + newPath = path + '_' + addBranch + print 'CONFLICT (' + conf + '): There is a directory with name "' + path + '" in', otherBranch, \ + '. Adding "' + path + '" as "' + newPath + '"' + runProgram(['git-update-cache', '--force-remove', '--', path]) + path = newPath + else: + print 'Adding "' + path + '"' + runProgram(['git-update-cache', '--add', '--cacheinfo', aMode+bMode, aSha+bSha, path]) + + elif 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', aMode, '->', bMode + print 'CONFLICT: adding with permission:', aMode + runProgram(['git-update-cache', '--add', '--cacheinfo', aMode, aSha, path]) + else: + # This case is handled by git-read-tree + print 'Adding', path + runProgram(['git-update-cache', '--add', '--cacheinfo', aMode, aSha, path]) + else: + cleanMerge = False + newPath1 = path + '_' + branch1 + newPath2 = path + '_' + branch2 + print 'CONFLICT (add/add): File "' + path + '" added non-identically in both branches.', \ + 'Adding "' + newPath1 + '" and "' + newPath2 + '" instead.' + runProgram(['git-update-cache', '--force-remove', '--', path]) + runProgram(['git-update-cache', '--add', + '--cacheinfo', aMode, aSha, newPath1, + '--cacheinfo', bMode, bSha, 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', os.environ['GIT_MERGE_BRANCH_1'] + '/' + path, + '-L', 'orig/' + path, + '-L', os.environ['GIT_MERGE_BRANCH_2'] + '/' + path, + src1, orig, src2], returnCode=True) + + if ret != 0: + cleanMerge = False + print 'CONFLICT (content): Merge conflict in "' + path + '".' + + if aMode == oMode: + mode = bMode + else: + mode = aMode + + sha = runProgram(['git-hash-object', '-t', 'blob', '-w', src1]) + runProgram(['git-update-cache', '--cacheinfo', aMode, sha, path]) + os.unlink(orig) + os.unlink(src1) + os.unlink(src2) + else: + print 'ERROR: Fatal merge failure.' + print "ERROR: Shouldn't happen" + sys.exit(1) + +[files, dirs] = eval(open(os.environ['GIT_MERGE_DIRS']).read()) +branch1 = os.environ['GIT_MERGE_BRANCH_1'] +branch2 = os.environ['GIT_MERGE_BRANCH_2'] +cleanMerge = True + +input = sys.stdin.read() +entries = input.split("\0") +entries.pop() # remove last entry (which is '') +entryRE = re.compile('^([0-9a-f]{40}) ([0-9a-f]{40}) ([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+) (.*)$') +debug('starting processing...', repr(entries)) +for entry in entries: + m = entryRE.match(entry) + if m: + processEntry(m.group(1), m.group(2), m.group(3), m.group(4), + m.group(5), m.group(6), m.group(7)) + else: + print sys.argv[0], 'Invalid input from merge program:', entry + sys.exit(1) + +if not cleanMerge: + sys.exit(1) + diff --git a/git-merge-script b/git-merge-script new file mode 100755 --- /dev/null +++ b/git-merge-script @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +from gitMergeCommon import * +from gitMergeCore import buildGraph, merge +import sys + +def dropHeads(): + try: + os.unlink(os.environ['GIT_DIR'] + '/MERGE_HEAD') + except OSError: + pass + + try: + os.unlink(os.environ['GIT_DIR'] + '/LAST_MERGE') + except OSError: + pass + +def writeHead(head, sha): + if sha[-1] != '\n': + sha += '\n' + + try: + f = open(os.environ['GIT_DIR'] + '/' + head, 'w') + f.write(sha) + f.close() + except IOError, e: + print 'Failed to write to', os.environ['GIT_DIR'] + '/' + head + ':', e.strerror + sys.exit(1) + return True + +def checkCleanTree(): + [out, code] = runProgram(['git-update-cache', '--refresh'], returnCode = True) + + if code != 0: + return False + out = runProgram(['git-diff-cache', '--name-only', '--cached', 'HEAD']) + if len(out) > 0: + return False + + return True + +def doCommit(msg, p1, p2): + tree = runProgram(['git-write-tree']) + tree = tree.rstrip() + + commit = runProgram(['git-commit-tree', tree, '-p', p1, '-p', p2], msg + '\n') + writeHead('HEAD', commit) + return commit.rstrip() + +if len(sys.argv) < 2: + print 'Usage:', sys.argv[0], '<branch> <msg>' + sys.exit(1) + +setupEnvironment() +repoValid() + +if not checkCleanTree(): + print 'Either the cache is out of sync or the working tree does not match HEAD.' + print 'Aborting merge.' + sys.exit(1) + +h1 = firstBranch = 'HEAD' +h2 = secondBranch = sys.argv[1] +commitMessage = sys.argv[2] + +print 'Merging', h1, 'with', h2 +h1 = runProgram(['git-rev-parse', '--revs-only', h1]).rstrip() +h2 = runProgram(['git-rev-parse', '--revs-only', h2]).rstrip() +print 'Resolved heads: h1:', h1, 'h2:', h2 + +writeHead('ORIG_HEAD', h1) +writeHead('LAST_MERGE', h2) + +print 'Building graph...' +graph = buildGraph([h1, h2]) +print 'graph done.' + +[res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2], firstBranch, secondBranch, graph) +print 'Merge result: (tree)', res.tree() + +# Checkout the merge results +runProgram(['git-read-tree', 'HEAD']) +runProgram(['git-update-cache', '--refresh']) +runProgram(['git-read-tree', '-m', '-u', 'HEAD', res.tree()]) + +if clean: + cmit = doCommit(commitMessage, h1, h2) + print 'Committed merge', cmit + print runProgram('git-diff-tree -p ORIG_HEAD ' + cmit + ' | git-apply --stat') + dropHeads() +else: + print 'Automatic merge failed, fix up by hand' + writeHead('MERGE_HEAD', h2) + sys.exit(1) diff --git a/gitMergeCommon.py b/gitMergeCommon.py new file mode 100644 --- /dev/null +++ b/gitMergeCommon.py @@ -0,0 +1,86 @@ +import sys + +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, os, sets, traceback + +DEBUG = 0 + +functionsToDebug = sets.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') + +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): + debug('runProgram prog:', str(prog), 'input:', str(input)) + if type(prog) is str: + progStr = prog + else: + progStr = ' '.join(prog) + + try: + pop = subprocess.Popen(prog, + shell = type(prog) is str, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + 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() + + out = pop.stdout.read() + 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 + +def setupEnvironment(): + if 'GIT_DIR' not in os.environ: + os.environ['GIT_DIR'] = '.git' + + if not os.environ.has_key('GIT_OBJECT_DIRECTORY'): + os.environ['GIT_OBJECT_DIRECTORY'] = os.environ['GIT_DIR'] + '/objects' + +def repoValid(): + if not (os.path.exists(os.environ['GIT_DIR']) and + os.path.exists(os.environ['GIT_DIR'] + '/refs') and + os.path.exists(os.environ['GIT_OBJECT_DIRECTORY']) and + os.path.exists(os.environ['GIT_OBJECT_DIRECTORY'] + '/00')): + print "Not a Git archive" + sys.exit(1) diff --git a/gitMergeCore.py b/gitMergeCore.py new file mode 100644 --- /dev/null +++ b/gitMergeCore.py @@ -0,0 +1,281 @@ +import sys, math, random, os, re, signal, tempfile +from gitMergeCommon import runProgram, debug, addDebug, printList +from heapq import heappush, heappop +from sets import Set + +currentId = 0 +def getUniqueId(): + global currentId + currentId += 1 + return currentId + +class Commit: + def __init__(self, sha, parents, tree=None): + self.parents = parents + self.firstLineMsg = None + self.children = [] + + if tree: + tree = tree.rstrip() + self._tree = tree + + if not sha: + self.sha = getUniqueId() + self.virtual = True + self.firstLineMsg = 'virtual commit' + assert(tree != None) + 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 findShared(h1, h2): + 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(h1, h1Set) + traverse(h2, h2Set) + + return h1Set.intersection(h2Set) + +def sharedHeads(shared): + h = Set() + + for s in shared: + if len([c for c in s.children if c in shared]) == 0: + h.add(s) + return list(h) + +getFilesRE = re.compile('([0-9]+) ([a-z0-9]+) ([0-9a-f]{40})\t(.*)') +def getFilesAndDirs(tree1, tree2): + files = Set() + dirs = Set() + def addFilesDirs(tree): + 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)) + + addFilesDirs(tree1) + addFilesDirs(tree2) + return [files, dirs] + +def merge(h1, h2, branch1Name, branch2Name, graph, indent=0, first=True): + def infoMsg(*args): + sys.stdout.write(' '*indent) + printList(args) + infoMsg('Merging:') + infoMsg(h1) + infoMsg(h2) + assert(isinstance(h1, Commit) and isinstance(h2, Commit)) + assert(isinstance(graph, Graph)) + + s = findShared(h1, h2) + if len(s) == 0: + s = [addCommonRoot(graph)] + + # infoMsg('found', len(s), 'shared commits.') + # if len(s) < 10: + # for x in s: + # infoMsg(x) + sys.stdout.flush() + s = sharedHeads(s) + infoMsg('found', len(s), 'shared head(s):') + for x in s : + infoMsg(x) + Ms = s[0] + + for h in s[1:]: + [Ms, ignore] = merge(Ms, h, branch1Name, branch2Name, graph, indent+1, False) + + filesDirs = getFilesAndDirs(h1.tree(), h2.tree()) + + infoMsg('running resolve on') + infoMsg(h1) + infoMsg(h2) + + if first: + b1 = branch1Name + b2 = branch2Name + else: + b1 = 'Temporary shared merge branch 1' + b2 = 'Temporary shared merge branch 2' + [shaRes, clean] = resolve(h1.tree(), h2.tree(), Ms.tree(), b1, b2, filesDirs) + + res = Commit(None, [h1, h2], tree=shaRes) + graph.addNode(res) + # infoMsg('merge returned: (tree)', res.tree()) + return [res, clean] + +# 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) + +def resolve(head, merge, common, b1, b2, filesDirs): + assert(isSha(head) and isSha(merge) and isSha(common)) + head = runProgram(['git-rev-parse', '--revs-only', head]).rstrip() + merge = runProgram(['git-rev-parse', '--revs-only', merge]).rstrip() + + if common == merge: + return head + + if common == head: + return merge + + runProgram(['git-read-tree', head]) + runProgram(['git-read-tree', '-i', '-m', common, head, merge]) + os.environ['GIT_MERGE_BRANCH_1'] = b1 + os.environ['GIT_MERGE_BRANCH_2'] = b2 + fdirs = tempfile.NamedTemporaryFile('w') + fdirs.write(repr(filesDirs)) + fdirs.flush() + os.environ['GIT_MERGE_DIRS'] = fdirs.name + try: + [tree, code] = runProgram('git-write-tree', returnCode=True) + tree = tree.rstrip() + if code != 0: + [out, code] = runProgram(['git-merge-cache', '-s', + 'git-merge-files-script', '-a'], + returnCode=True) + print out + if code != 0: + # Non-clean merge + clean = False + else: + clean = True + tree = runProgram('git-write-tree').rstrip() + else: + clean = True + finally: + fdirs.close() + + return [tree, clean] + diff --git a/merge-cache.c b/merge-cache.c --- a/merge-cache.c +++ b/merge-cache.c @@ -5,7 +5,7 @@ static const char *pgm = NULL; static const char *arguments[8]; -static int one_shot, quiet; +static int one_shot, quiet, use_stdin, stdin_fd, pgm_pid; static int err; static void run_program(void) @@ -37,6 +37,59 @@ static void run_program(void) } } +static void start_program(void) +{ + int pid, fds[2]; + + if (pipe(fds) < 0) + die("pipe failed: %s", strerror(errno)); + + pid = fork(); + if (pid < 0) + die("unable to fork"); + + if (!pid) { + close(fds[1]); + dup2(fds[0], 0); + close(fds[0]); + execlp(pgm, pgm, NULL); + die("unable to execute '%s': %s", pgm, strerror(errno)); + } + + pgm_pid = pid; + close(fds[0]); + stdin_fd = fds[1]; +} + +static void die_write(int fd, const char* buf, int len) +{ + if (write(fd, buf, len) != len) + die("unable to write: %s", strerror(errno)); +} + +static void write_to_program(void) +{ + int arg; + for(arg = 1; arg < 4; arg++) { + if(!strcmp("", arguments[arg])) + die_write(stdin_fd, "0000000000000000000000000000000000000000", 40); + else + die_write(stdin_fd, arguments[arg], 40); + die_write(stdin_fd, " ", 1); + } + + for(arg = 5; arg < 8; arg++) { + if(!strcmp("", arguments[arg])) + die_write(stdin_fd, "0", 1); + else + die_write(stdin_fd, arguments[arg], strlen(arguments[arg])); + die_write(stdin_fd, " ", 1); + } + + /* Make sure we write the terminating \0 too. */ + die_write(stdin_fd, arguments[4], strlen(arguments[4])+1); +} + static int merge_entry(int pos, const char *path) { int found; @@ -68,7 +121,11 @@ static int merge_entry(int pos, const ch } while (++pos < active_nr); if (!found) die("git-merge-cache: %s not in the cache", path); - run_program(); + + if(use_stdin) + write_to_program(); + else + run_program(); return found; } @@ -100,7 +157,7 @@ int main(int argc, char **argv) int i, force_file = 0; if (argc < 3) - usage("git-merge-cache [-o] [-q] <merge-program> (-a | <filename>*)"); + usage("git-merge-cache [-o] [-q] [-s] <merge-program> (-a | <filename>*)"); read_cache(); @@ -113,7 +170,14 @@ int main(int argc, char **argv) quiet = 1; i++; } + if (!strcmp(argv[i], "-s")) { + use_stdin = 1; + i++; + } pgm = argv[i++]; + if(use_stdin) + start_program(); + for (; i < argc; i++) { char *arg = argv[i]; if (!force_file && *arg == '-') { @@ -129,6 +193,14 @@ int main(int argc, char **argv) } merge_file(arg); } + + if(use_stdin) { + int status; + close(stdin_fd); + if (waitpid(pgm_pid, &status, 0) < 0 || !WIFEXITED(status) || WEXITSTATUS(status)) + err = 1; + } + if (err && !quiet) die("merge program failed"); return err; diff --git a/read-tree.c b/read-tree.c --- a/read-tree.c +++ b/read-tree.c @@ -7,6 +7,7 @@ static int stage = 0; static int update = 0; +static int ignore_working_dir = 0; static int unpack_tree(unsigned char *sha1) { @@ -80,7 +81,10 @@ static void verify_uptodate(struct cache { struct stat st; - if (!lstat(ce->name, &st)) { + if (ignore_working_dir) + return; + + if (!lstat(ce->name, &st)) { unsigned changed = ce_match_stat(ce, &st); if (!changed) return; @@ -510,7 +514,7 @@ static int read_cache_unmerged(void) 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; @@ -535,6 +539,11 @@ int main(int argc, char **argv) continue; } + if (!strcmp(arg, "-i")) { + ignore_working_dir = 1; + continue; + } + /* This differs from "-m" in that we'll silently ignore unmerged entries */ if (!strcmp(arg, "--reset")) { if (stage || merge || emu23) - To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to [EMAIL PROTECTED] More majordomo info at http://vger.kernel.org/majordomo-info.html