Add test case for merge-base.
[git/gitweb.git] / git-merge-recursive.py
blob90e889c300ab16681040c27ab70c43cd741ff8a0
1 #!/usr/bin/python
3 import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
4 from heapq import heappush, heappop
5 from sets import Set
7 sys.path.append('''@@GIT_PYTHON_PATH@@''')
8 from gitMergeCommon import *
10 originalIndexFile = os.environ.get('GIT_INDEX_FILE',
11 os.environ.get('GIT_DIR', '.git') + '/index')
12 temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
13 '/merge-recursive-tmp-index'
14 def setupIndex(temporary):
15 try:
16 os.unlink(temporaryIndexFile)
17 except OSError:
18 pass
19 if temporary:
20 newIndex = temporaryIndexFile
21 else:
22 newIndex = originalIndexFile
23 os.environ['GIT_INDEX_FILE'] = newIndex
25 # This is a global variable which is used in a number of places but
26 # only written to in the 'merge' function.
28 # cacheOnly == True => Don't leave any non-stage 0 entries in the cache and
29 # don't update the working directory.
30 # False => Leave unmerged entries in the cache and update
31 # the working directory.
33 cacheOnly = False
35 # The entry point to the merge code
36 # ---------------------------------
38 def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
39 '''Merge the commits h1 and h2, return the resulting virtual
40 commit object and a flag indicating the cleaness of the merge.'''
41 assert(isinstance(h1, Commit) and isinstance(h2, Commit))
42 assert(isinstance(graph, Graph))
44 def infoMsg(*args):
45 sys.stdout.write(' '*callDepth)
46 printList(args)
48 infoMsg('Merging:')
49 infoMsg(h1)
50 infoMsg(h2)
51 sys.stdout.flush()
53 ca = getCommonAncestors(graph, h1, h2)
54 infoMsg('found', len(ca), 'common ancestor(s):')
55 for x in ca:
56 infoMsg(x)
57 sys.stdout.flush()
59 mergedCA = ca[0]
60 for h in ca[1:]:
61 [mergedCA, dummy] = merge(mergedCA, h,
62 'Temporary shared merge branch 1',
63 'Temporary shared merge branch 2',
64 graph, callDepth+1)
65 assert(isinstance(mergedCA, Commit))
67 global cacheOnly
68 if callDepth == 0:
69 setupIndex(False)
70 cacheOnly = False
71 else:
72 setupIndex(True)
73 runProgram(['git-read-tree', h1.tree()])
74 cacheOnly = True
76 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
77 branch1Name, branch2Name)
79 if clean or cacheOnly:
80 res = Commit(None, [h1, h2], tree=shaRes)
81 graph.addNode(res)
82 else:
83 res = None
85 return [res, clean]
87 getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
88 def getFilesAndDirs(tree):
89 files = Set()
90 dirs = Set()
91 out = runProgram(['git-ls-tree', '-r', '-z', tree])
92 for l in out.split('\0'):
93 m = getFilesRE.match(l)
94 if m:
95 if m.group(2) == 'tree':
96 dirs.add(m.group(4))
97 elif m.group(2) == 'blob':
98 files.add(m.group(4))
100 return [files, dirs]
102 # Those two global variables are used in a number of places but only
103 # written to in 'mergeTrees' and 'uniquePath'. They keep track of
104 # every file and directory in the two branches that are about to be
105 # merged.
106 currentFileSet = None
107 currentDirectorySet = None
109 def mergeTrees(head, merge, common, branch1Name, branch2Name):
110 '''Merge the trees 'head' and 'merge' with the common ancestor
111 'common'. The name of the head branch is 'branch1Name' and the name of
112 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
113 where tree is the resulting tree and cleanMerge is True iff the
114 merge was clean.'''
116 assert(isSha(head) and isSha(merge) and isSha(common))
118 if common == merge:
119 print 'Already uptodate!'
120 return [head, True]
122 if cacheOnly:
123 updateArg = '-i'
124 else:
125 updateArg = '-u'
127 [out, code] = runProgram(['git-read-tree', updateArg, '-m',
128 common, head, merge], returnCode = True)
129 if code != 0:
130 die('git-read-tree:', out)
132 [tree, code] = runProgram('git-write-tree', returnCode=True)
133 tree = tree.rstrip()
134 if code != 0:
135 global currentFileSet, currentDirectorySet
136 [currentFileSet, currentDirectorySet] = getFilesAndDirs(head)
137 [filesM, dirsM] = getFilesAndDirs(merge)
138 currentFileSet.union_update(filesM)
139 currentDirectorySet.union_update(dirsM)
141 entries = unmergedCacheEntries()
142 renamesHead = getRenames(head, common, head, merge, entries)
143 renamesMerge = getRenames(merge, common, head, merge, entries)
145 cleanMerge = processRenames(renamesHead, renamesMerge,
146 branch1Name, branch2Name)
147 for entry in entries:
148 if entry.processed:
149 continue
150 if not processEntry(entry, branch1Name, branch2Name):
151 cleanMerge = False
153 if cleanMerge or cacheOnly:
154 tree = runProgram('git-write-tree').rstrip()
155 else:
156 tree = None
157 else:
158 cleanMerge = True
160 return [tree, cleanMerge]
162 # Low level file merging, update and removal
163 # ------------------------------------------
165 def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
166 branch1Name, branch2Name):
168 merge = False
169 clean = True
171 if stat.S_IFMT(aMode) != stat.S_IFMT(bMode):
172 clean = False
173 if stat.S_ISREG(aMode):
174 mode = aMode
175 sha = aSha
176 else:
177 mode = bMode
178 sha = bSha
179 else:
180 if aSha != oSha and bSha != oSha:
181 merge = True
183 if aMode == oMode:
184 mode = bMode
185 else:
186 mode = aMode
188 if aSha == oSha:
189 sha = bSha
190 elif bSha == oSha:
191 sha = aSha
192 elif stat.S_ISREG(aMode):
193 assert(stat.S_ISREG(bMode))
195 orig = runProgram(['git-unpack-file', oSha]).rstrip()
196 src1 = runProgram(['git-unpack-file', aSha]).rstrip()
197 src2 = runProgram(['git-unpack-file', bSha]).rstrip()
198 [out, code] = runProgram(['merge',
199 '-L', branch1Name + '/' + aPath,
200 '-L', 'orig/' + oPath,
201 '-L', branch2Name + '/' + bPath,
202 src1, orig, src2], returnCode=True)
204 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
205 src1]).rstrip()
207 os.unlink(orig)
208 os.unlink(src1)
209 os.unlink(src2)
211 clean = (code == 0)
212 else:
213 assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
214 sha = aSha
216 if aSha != bSha:
217 clean = False
219 return [sha, mode, clean, merge]
221 def updateFile(clean, sha, mode, path):
222 updateCache = cacheOnly or clean
223 updateWd = not cacheOnly
225 return updateFileExt(sha, mode, path, updateCache, updateWd)
227 def updateFileExt(sha, mode, path, updateCache, updateWd):
228 if cacheOnly:
229 updateWd = False
231 if updateWd:
232 pathComponents = path.split('/')
233 for x in xrange(1, len(pathComponents)):
234 p = '/'.join(pathComponents[0:x])
236 try:
237 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
238 except:
239 createDir = True
241 if createDir:
242 try:
243 os.mkdir(p)
244 except OSError, e:
245 die("Couldn't create directory", p, e.strerror)
247 prog = ['git-cat-file', 'blob', sha]
248 if stat.S_ISREG(mode):
249 try:
250 os.unlink(path)
251 except OSError:
252 pass
253 if mode & 0100:
254 mode = 0777
255 else:
256 mode = 0666
257 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
258 proc = subprocess.Popen(prog, stdout=fd)
259 proc.wait()
260 os.close(fd)
261 elif stat.S_ISLNK(mode):
262 linkTarget = runProgram(prog)
263 os.symlink(linkTarget, path)
264 else:
265 assert(False)
267 if updateWd and updateCache:
268 runProgram(['git-update-index', '--add', '--', path])
269 elif updateCache:
270 runProgram(['git-update-index', '--add', '--cacheinfo',
271 '0%o' % mode, sha, path])
273 def removeFile(clean, path):
274 updateCache = cacheOnly or clean
275 updateWd = not cacheOnly
277 if updateCache:
278 runProgram(['git-update-index', '--force-remove', '--', path])
280 if updateWd:
281 try:
282 os.unlink(path)
283 except OSError, e:
284 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
285 raise
287 def uniquePath(path, branch):
288 def fileExists(path):
289 try:
290 os.lstat(path)
291 return True
292 except OSError, e:
293 if e.errno == errno.ENOENT:
294 return False
295 else:
296 raise
298 branch = branch.replace('/', '_')
299 newPath = path + '_' + branch
300 suffix = 0
301 while newPath in currentFileSet or \
302 newPath in currentDirectorySet or \
303 fileExists(newPath):
304 suffix += 1
305 newPath = path + '_' + branch + '_' + str(suffix)
306 currentFileSet.add(newPath)
307 return newPath
309 # Cache entry management
310 # ----------------------
312 class CacheEntry:
313 def __init__(self, path):
314 class Stage:
315 def __init__(self):
316 self.sha1 = None
317 self.mode = None
319 # Used for debugging only
320 def __str__(self):
321 if self.mode != None:
322 m = '0%o' % self.mode
323 else:
324 m = 'None'
326 if self.sha1:
327 sha1 = self.sha1
328 else:
329 sha1 = 'None'
330 return 'sha1: ' + sha1 + ' mode: ' + m
332 self.stages = [Stage(), Stage(), Stage(), Stage()]
333 self.path = path
334 self.processed = False
336 def __str__(self):
337 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
339 class CacheEntryContainer:
340 def __init__(self):
341 self.entries = {}
343 def add(self, entry):
344 self.entries[entry.path] = entry
346 def get(self, path):
347 return self.entries.get(path)
349 def __iter__(self):
350 return self.entries.itervalues()
352 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
353 def unmergedCacheEntries():
354 '''Create a dictionary mapping file names to CacheEntry
355 objects. The dictionary contains one entry for every path with a
356 non-zero stage entry.'''
358 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
359 lines.pop()
361 res = CacheEntryContainer()
362 for l in lines:
363 m = unmergedRE.match(l)
364 if m:
365 mode = int(m.group(1), 8)
366 sha1 = m.group(2)
367 stage = int(m.group(3))
368 path = m.group(4)
370 e = res.get(path)
371 if not e:
372 e = CacheEntry(path)
373 res.add(e)
375 e.stages[stage].mode = mode
376 e.stages[stage].sha1 = sha1
377 else:
378 die('Error: Merge program failed: Unexpected output from',
379 'git-ls-files:', l)
380 return res
382 lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
383 def getCacheEntry(path, origTree, aTree, bTree):
384 '''Returns a CacheEntry object which doesn't have to correspond to
385 a real cache entry in Git's index.'''
387 def parse(out):
388 if out == '':
389 return [None, None]
390 else:
391 m = lsTreeRE.match(out)
392 if not m:
393 die('Unexpected output from git-ls-tree:', out)
394 elif m.group(2) == 'blob':
395 return [m.group(3), int(m.group(1), 8)]
396 else:
397 return [None, None]
399 res = CacheEntry(path)
401 [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
402 [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
403 [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
405 res.stages[1].sha1 = oSha
406 res.stages[1].mode = oMode
407 res.stages[2].sha1 = aSha
408 res.stages[2].mode = aMode
409 res.stages[3].sha1 = bSha
410 res.stages[3].mode = bMode
412 return res
414 # Rename detection and handling
415 # -----------------------------
417 class RenameEntry:
418 def __init__(self,
419 src, srcSha, srcMode, srcCacheEntry,
420 dst, dstSha, dstMode, dstCacheEntry,
421 score):
422 self.srcName = src
423 self.srcSha = srcSha
424 self.srcMode = srcMode
425 self.srcCacheEntry = srcCacheEntry
426 self.dstName = dst
427 self.dstSha = dstSha
428 self.dstMode = dstMode
429 self.dstCacheEntry = dstCacheEntry
430 self.score = score
432 self.processed = False
434 class RenameEntryContainer:
435 def __init__(self):
436 self.entriesSrc = {}
437 self.entriesDst = {}
439 def add(self, entry):
440 self.entriesSrc[entry.srcName] = entry
441 self.entriesDst[entry.dstName] = entry
443 def getSrc(self, path):
444 return self.entriesSrc.get(path)
446 def getDst(self, path):
447 return self.entriesDst.get(path)
449 def __iter__(self):
450 return self.entriesSrc.itervalues()
452 parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
453 def getRenames(tree, oTree, aTree, bTree, cacheEntries):
454 '''Get information of all renames which occured between 'oTree' and
455 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
456 'bTree') to be able to associate the correct cache entries with
457 the rename information. 'tree' is always equal to either aTree or bTree.'''
459 assert(tree == aTree or tree == bTree)
460 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
461 '-z', oTree, tree])
463 ret = RenameEntryContainer()
464 try:
465 recs = inp.split("\0")
466 recs.pop() # remove last entry (which is '')
467 it = recs.__iter__()
468 while True:
469 rec = it.next()
470 m = parseDiffRenamesRE.match(rec)
472 if not m:
473 die('Unexpected output from git-diff-tree:', rec)
475 srcMode = int(m.group(1), 8)
476 dstMode = int(m.group(2), 8)
477 srcSha = m.group(3)
478 dstSha = m.group(4)
479 score = m.group(5)
480 src = it.next()
481 dst = it.next()
483 srcCacheEntry = cacheEntries.get(src)
484 if not srcCacheEntry:
485 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
486 cacheEntries.add(srcCacheEntry)
488 dstCacheEntry = cacheEntries.get(dst)
489 if not dstCacheEntry:
490 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
491 cacheEntries.add(dstCacheEntry)
493 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
494 dst, dstSha, dstMode, dstCacheEntry,
495 score))
496 except StopIteration:
497 pass
498 return ret
500 def fmtRename(src, dst):
501 srcPath = src.split('/')
502 dstPath = dst.split('/')
503 path = []
504 endIndex = min(len(srcPath), len(dstPath)) - 1
505 for x in range(0, endIndex):
506 if srcPath[x] == dstPath[x]:
507 path.append(srcPath[x])
508 else:
509 endIndex = x
510 break
512 if len(path) > 0:
513 return '/'.join(path) + \
514 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
515 '/'.join(dstPath[endIndex:]) + '}'
516 else:
517 return src + ' => ' + dst
519 def processRenames(renamesA, renamesB, branchNameA, branchNameB):
520 srcNames = Set()
521 for x in renamesA:
522 srcNames.add(x.srcName)
523 for x in renamesB:
524 srcNames.add(x.srcName)
526 cleanMerge = True
527 for path in srcNames:
528 if renamesA.getSrc(path):
529 renames1 = renamesA
530 renames2 = renamesB
531 branchName1 = branchNameA
532 branchName2 = branchNameB
533 else:
534 renames1 = renamesB
535 renames2 = renamesA
536 branchName1 = branchNameB
537 branchName2 = branchNameA
539 ren1 = renames1.getSrc(path)
540 ren2 = renames2.getSrc(path)
542 ren1.dstCacheEntry.processed = True
543 ren1.srcCacheEntry.processed = True
545 if ren1.processed:
546 continue
548 ren1.processed = True
549 removeFile(True, ren1.srcName)
550 if ren2:
551 # Renamed in 1 and renamed in 2
552 assert(ren1.srcName == ren2.srcName)
553 ren2.dstCacheEntry.processed = True
554 ren2.processed = True
556 if ren1.dstName != ren2.dstName:
557 print 'CONFLICT (rename/rename): Rename', \
558 fmtRename(path, ren1.dstName), 'in branch', branchName1, \
559 'rename', fmtRename(path, ren2.dstName), 'in', branchName2
560 cleanMerge = False
562 if ren1.dstName in currentDirectorySet:
563 dstName1 = uniquePath(ren1.dstName, branchName1)
564 print ren1.dstName, 'is a directory in', branchName2, \
565 'adding as', dstName1, 'instead.'
566 removeFile(False, ren1.dstName)
567 else:
568 dstName1 = ren1.dstName
570 if ren2.dstName in currentDirectorySet:
571 dstName2 = uniquePath(ren2.dstName, branchName2)
572 print ren2.dstName, 'is a directory in', branchName1, \
573 'adding as', dstName2, 'instead.'
574 removeFile(False, ren2.dstName)
575 else:
576 dstName2 = ren1.dstName
578 updateFile(False, ren1.dstSha, ren1.dstMode, dstName1)
579 updateFile(False, ren2.dstSha, ren2.dstMode, dstName2)
580 else:
581 [resSha, resMode, clean, merge] = \
582 mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
583 ren1.dstName, ren1.dstSha, ren1.dstMode,
584 ren2.dstName, ren2.dstSha, ren2.dstMode,
585 branchName1, branchName2)
587 if merge or not clean:
588 print 'Renaming', fmtRename(path, ren1.dstName)
590 if merge:
591 print 'Auto-merging', ren1.dstName
593 if not clean:
594 print 'CONFLICT (content): merge conflict in', ren1.dstName
595 cleanMerge = False
597 if not cacheOnly:
598 updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
599 updateCache=True, updateWd=False)
600 updateFile(clean, resSha, resMode, ren1.dstName)
601 else:
602 # Renamed in 1, maybe changed in 2
603 if renamesA == renames1:
604 stage = 3
605 else:
606 stage = 2
608 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
609 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
611 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
612 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
614 tryMerge = False
616 if ren1.dstName in currentDirectorySet:
617 newPath = uniquePath(ren1.dstName, branchName1)
618 print 'CONFLICT (rename/directory): Rename', \
619 fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,\
620 'directory', ren1.dstName, 'added in', branchName2
621 print 'Renaming', ren1.srcName, 'to', newPath, 'instead'
622 cleanMerge = False
623 removeFile(False, ren1.dstName)
624 updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
625 elif srcShaOtherBranch == None:
626 print 'CONFLICT (rename/delete): Rename', \
627 fmtRename(ren1.srcName, ren1.dstName), 'in', \
628 branchName1, 'and deleted in', branchName2
629 cleanMerge = False
630 updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
631 elif dstShaOtherBranch:
632 newPath = uniquePath(ren1.dstName, branchName2)
633 print 'CONFLICT (rename/add): Rename', \
634 fmtRename(ren1.srcName, ren1.dstName), 'in', \
635 branchName1 + '.', ren1.dstName, 'added in', branchName2
636 print 'Adding as', newPath, 'instead'
637 updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
638 cleanMerge = False
639 tryMerge = True
640 elif renames2.getDst(ren1.dstName):
641 dst2 = renames2.getDst(ren1.dstName)
642 newPath1 = uniquePath(ren1.dstName, branchName1)
643 newPath2 = uniquePath(dst2.dstName, branchName2)
644 print 'CONFLICT (rename/rename): Rename', \
645 fmtRename(ren1.srcName, ren1.dstName), 'in', \
646 branchName1+'. Rename', \
647 fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2
648 print 'Renaming', ren1.srcName, 'to', newPath1, 'and', \
649 dst2.srcName, 'to', newPath2, 'instead'
650 removeFile(False, ren1.dstName)
651 updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
652 updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
653 dst2.processed = True
654 cleanMerge = False
655 else:
656 tryMerge = True
658 if tryMerge:
659 [resSha, resMode, clean, merge] = \
660 mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
661 ren1.dstName, ren1.dstSha, ren1.dstMode,
662 ren1.srcName, srcShaOtherBranch, srcModeOtherBranch,
663 branchName1, branchName2)
665 if merge or not clean:
666 print 'Renaming', fmtRename(ren1.srcName, ren1.dstName)
668 if merge:
669 print 'Auto-merging', ren1.dstName
671 if not clean:
672 print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName
673 cleanMerge = False
675 if not cacheOnly:
676 updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
677 updateCache=True, updateWd=False)
678 updateFile(clean, resSha, resMode, ren1.dstName)
680 return cleanMerge
682 # Per entry merge function
683 # ------------------------
685 def processEntry(entry, branch1Name, branch2Name):
686 '''Merge one cache entry.'''
688 debug('processing', entry.path, 'clean cache:', cacheOnly)
690 cleanMerge = True
692 path = entry.path
693 oSha = entry.stages[1].sha1
694 oMode = entry.stages[1].mode
695 aSha = entry.stages[2].sha1
696 aMode = entry.stages[2].mode
697 bSha = entry.stages[3].sha1
698 bMode = entry.stages[3].mode
700 assert(oSha == None or isSha(oSha))
701 assert(aSha == None or isSha(aSha))
702 assert(bSha == None or isSha(bSha))
704 assert(oMode == None or type(oMode) is int)
705 assert(aMode == None or type(aMode) is int)
706 assert(bMode == None or type(bMode) is int)
708 if (oSha and (not aSha or not bSha)):
710 # Case A: Deleted in one
712 if (not aSha and not bSha) or \
713 (aSha == oSha and not bSha) or \
714 (not aSha and bSha == oSha):
715 # Deleted in both or deleted in one and unchanged in the other
716 if aSha:
717 print 'Removing', path
718 removeFile(True, path)
719 else:
720 # Deleted in one and changed in the other
721 cleanMerge = False
722 if not aSha:
723 print 'CONFLICT (delete/modify):', path, 'deleted in', \
724 branch1Name, 'and modified in', branch2Name + '.', \
725 'Version', branch2Name, 'of', path, 'left in tree.'
726 mode = bMode
727 sha = bSha
728 else:
729 print 'CONFLICT (modify/delete):', path, 'deleted in', \
730 branch2Name, 'and modified in', branch1Name + '.', \
731 'Version', branch1Name, 'of', path, 'left in tree.'
732 mode = aMode
733 sha = aSha
735 updateFile(False, sha, mode, path)
737 elif (not oSha and aSha and not bSha) or \
738 (not oSha and not aSha and bSha):
740 # Case B: Added in one.
742 if aSha:
743 addBranch = branch1Name
744 otherBranch = branch2Name
745 mode = aMode
746 sha = aSha
747 conf = 'file/directory'
748 else:
749 addBranch = branch2Name
750 otherBranch = branch1Name
751 mode = bMode
752 sha = bSha
753 conf = 'directory/file'
755 if path in currentDirectorySet:
756 cleanMerge = False
757 newPath = uniquePath(path, addBranch)
758 print 'CONFLICT (' + conf + '):', \
759 'There is a directory with name', path, 'in', \
760 otherBranch + '. Adding', path, 'as', newPath
762 removeFile(False, path)
763 updateFile(False, sha, mode, newPath)
764 else:
765 print 'Adding', path
766 updateFile(True, sha, mode, path)
768 elif not oSha and aSha and bSha:
770 # Case C: Added in both (check for same permissions).
772 if aSha == bSha:
773 if aMode != bMode:
774 cleanMerge = False
775 print 'CONFLICT: File', path, \
776 'added identically in both branches, but permissions', \
777 'conflict', '0%o' % aMode, '->', '0%o' % bMode
778 print 'CONFLICT: adding with permission:', '0%o' % aMode
780 updateFile(False, aSha, aMode, path)
781 else:
782 # This case is handled by git-read-tree
783 assert(False)
784 else:
785 cleanMerge = False
786 newPath1 = uniquePath(path, branch1Name)
787 newPath2 = uniquePath(path, branch2Name)
788 print 'CONFLICT (add/add): File', path, \
789 'added non-identically in both branches. Adding as', \
790 newPath1, 'and', newPath2, 'instead.'
791 removeFile(False, path)
792 updateFile(False, aSha, aMode, newPath1)
793 updateFile(False, bSha, bMode, newPath2)
795 elif oSha and aSha and bSha:
797 # case D: Modified in both, but differently.
799 print 'Auto-merging', path
800 [sha, mode, clean, dummy] = \
801 mergeFile(path, oSha, oMode,
802 path, aSha, aMode,
803 path, bSha, bMode,
804 branch1Name, branch2Name)
805 if clean:
806 updateFile(True, sha, mode, path)
807 else:
808 cleanMerge = False
809 print 'CONFLICT (content): Merge conflict in', path
811 if cacheOnly:
812 updateFile(False, sha, mode, path)
813 else:
814 updateFileExt(aSha, aMode, path,
815 updateCache=True, updateWd=False)
816 updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
817 else:
818 die("ERROR: Fatal merge failure, shouldn't happen.")
820 return cleanMerge
822 def usage():
823 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
825 # main entry point as merge strategy module
826 # The first parameters up to -- are merge bases, and the rest are heads.
827 # This strategy module figures out merge bases itself, so we only
828 # get heads.
830 if len(sys.argv) < 4:
831 usage()
833 for nextArg in xrange(1, len(sys.argv)):
834 if sys.argv[nextArg] == '--':
835 if len(sys.argv) != nextArg + 3:
836 die('Not handling anything other than two heads merge.')
837 try:
838 h1 = firstBranch = sys.argv[nextArg + 1]
839 h2 = secondBranch = sys.argv[nextArg + 2]
840 except IndexError:
841 usage()
842 break
844 print 'Merging', h1, 'with', h2
846 try:
847 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
848 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
850 graph = buildGraph([h1, h2])
852 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
853 firstBranch, secondBranch, graph)
855 print ''
856 except:
857 if isinstance(sys.exc_info()[1], SystemExit):
858 raise
859 else:
860 traceback.print_exc(None, sys.stderr)
861 sys.exit(2)
863 if clean:
864 sys.exit(0)
865 else:
866 sys.exit(1)