3 import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
4 from heapq import heappush, heappop
7 sys.path.append('@@GIT_PYTHON_PATH@@')
8 from gitMergeCommon import *
10 # The actual merge code
11 # ---------------------
13 originalIndexFile = os.environ.get('GIT_INDEX_FILE',
14 os.environ.get('GIT_DIR', '.git') + '/index')
15 temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
16 '/merge-recursive-tmp-index'
17 def setupIndex(temporary):
19 os.unlink(temporaryIndexFile)
23 newIndex = temporaryIndexFile
26 newIndex = originalIndexFile
27 os.environ['GIT_INDEX_FILE'] = newIndex
29 def 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))
36 sys.stdout.write(' '*callDepth)
43 ca = getCommonAncestors(graph, h1, h2)
44 infoMsg('found', len(ca), 'common ancestor(s):')
51 [Ms, ignore] = merge(Ms, h,
52 'Temporary shared merge branch 1',
53 'Temporary shared merge branch 2',
55 assert(isinstance(Ms, Commit))
62 runProgram(['git-read-tree', h1.tree()])
65 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
66 branch1Name, branch2Name,
69 if clean or cleanCache:
70 res = Commit(None, [h1, h2], tree=shaRes)
77 getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
78 def getFilesAndDirs(tree):
81 out = runProgram(['git-ls-tree', '-r', '-z', tree])
82 for l in out.split('\0'):
83 m = getFilesRE.match(l)
85 if m.group(2) == 'tree':
87 elif m.group(2) == 'blob':
93 def __init__(self, path):
99 self.stages = [Stage(), Stage(), Stage()]
102 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
103 def 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.'''
108 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
113 m = unmergedRE.match(l)
115 mode = int(m.group(1), 8)
117 stage = int(m.group(3)) - 1
120 if res.has_key(path):
126 e.stages[stage].mode = mode
127 e.stages[stage].sha1 = sha1
129 die('Error: Merge program failed: Unexpected output from', \
133 def mergeTrees(head, merge, common, branch1Name, branch2Name,
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
141 assert(isSha(head) and isSha(merge) and isSha(common))
144 print 'Already uptodate!'
152 [out, code] = runProgram(['git-read-tree', updateArg, '-m', common, head, merge], returnCode = True)
154 die('git-read-tree:', out)
158 [tree, code] = runProgram('git-write-tree', returnCode=True)
161 [files, dirs] = getFilesAndDirs(head)
162 [filesM, dirsM] = getFilesAndDirs(merge)
163 files.union_update(filesM)
164 dirs.union_update(dirsM)
167 entries = unmergedCacheEntries()
169 if not processEntry(entries[name], branch1Name, branch2Name,
170 files, dirs, cleanCache):
173 if cleanMerge or cleanCache:
174 tree = runProgram('git-write-tree').rstrip()
180 return [tree, cleanMerge]
182 def 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'.'''
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
193 # clean == True => non-conflict case
194 # False => conflict case
196 # If cleanCache == False then the cache shouldn't be updated if clean == False
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)
203 prog = ['git-cat-file', 'blob', sha]
204 if stat.S_ISREG(mode):
213 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
214 proc = subprocess.Popen(prog, stdout=fd)
217 elif stat.S_ISLNK(mode):
218 linkTarget = runProgram(prog)
219 os.symlink(linkTarget, path)
223 if updateWd and updateCache:
224 runProgram(['git-update-index', '--add', '--', path])
226 runProgram(['git-update-index', '--add', '--cacheinfo',
227 '0%o' % mode, sha, path])
229 def removeFile(clean, path):
230 if cleanCache or (not cleanCache and clean):
231 runProgram(['git-update-index', '--force-remove', '--', path])
233 if not cleanCache and clean:
237 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
240 def uniquePath(path, branch):
241 newPath = path + '_' + branch
243 while newPath in files or newPath in dirs:
245 newPath = path + '_' + branch + '_' + str(suffix)
249 debug('processing', entry.path, 'clean cache:', cleanCache)
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
261 assert(oSha == None or isSha(oSha))
262 assert(aSha == None or isSha(aSha))
263 assert(bSha == None or isSha(bSha))
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)
269 if (oSha and (not aSha or not bSha)):
271 # Case A: Deleted in one
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
278 print 'Removing ' + path
279 removeFile(True, path)
281 # Deleted in one and changed in the other
284 print 'CONFLICT (del/mod): "' + path + '" deleted in', \
285 branch1Name, 'and modified in', branch2Name, \
286 '. Version', branch2Name, ' of "' + path + \
291 print 'CONFLICT (mod/del): "' + path + '" deleted in', \
292 branch2Name, 'and modified in', branch1Name + \
293 '. Version', branch1Name, 'of "' + path + \
298 updateFile(False, sha, mode, path)
300 elif (not oSha and aSha and not bSha) or \
301 (not oSha and not aSha and bSha):
303 # Case B: Added in one.
306 addBranch = branch1Name
307 otherBranch = branch2Name
312 addBranch = branch2Name
313 otherBranch = branch1Name
320 newPath = uniquePath(path, addBranch)
321 print 'CONFLICT (' + conf + \
322 '): There is a directory with name "' + path + '" in', \
323 otherBranch + '. Adding "' + path + '" as "' + newPath + '"'
325 removeFile(False, path)
328 print 'Adding "' + path + '"'
330 updateFile(True, sha, mode, path)
332 elif not oSha and aSha and bSha:
334 # Case C: Added in both (check for same permissions).
339 print 'CONFLICT: File "' + path + \
340 '" added identically in both branches,', \
341 'but permissions conflict', '0%o' % aMode, '->', \
343 print 'CONFLICT: adding with permission:', '0%o' % aMode
345 updateFile(False, aSha, aMode, path)
347 # This case is handled by git-read-tree
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)
359 elif oSha and aSha and bSha:
361 # case D: Modified in both, but differently.
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)
378 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
383 print 'CONFLICT (content): Merge conflict in "' + path + '".'
386 updateFile(False, sha, mode, path)
388 updateFile(True, aSha, aMode, path)
389 updateFile(False, sha, mode, path, True)
391 updateFile(True, sha, mode, path)
397 die("ERROR: Fatal merge failure, shouldn't happen.")
402 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
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
409 if len(sys.argv) < 4:
412 for 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.')
417 h1 = firstBranch = sys.argv[nextArg + 1]
418 h2 = secondBranch = sys.argv[nextArg + 2]
423 print 'Merging', h1, 'with', h2
426 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
427 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
429 graph = buildGraph([h1, h2])
431 [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
432 firstBranch, secondBranch, graph)
436 if isinstance(sys.exc_info()[1], SystemExit):
439 traceback.print_exc(None, sys.stderr)