3 # Copyright (C) 2005 Fredrik Kuivinen
7 sys
.path
.append('''@@GIT_PYTHON_PATH@@''')
9 import math
, random
, os
, re
, signal
, tempfile
, stat
, errno
, traceback
10 from heapq
import heappush
, heappop
13 from gitMergeCommon
import *
17 sys
.stdout
.write(' '*outputIndent
)
20 originalIndexFile
= os
.environ
.get('GIT_INDEX_FILE',
21 os
.environ
.get('GIT_DIR', '.git') + '/index')
22 temporaryIndexFile
= os
.environ
.get('GIT_DIR', '.git') + \
23 '/merge-recursive-tmp-index'
24 def setupIndex(temporary
):
26 os
.unlink(temporaryIndexFile
)
30 newIndex
= temporaryIndexFile
32 newIndex
= originalIndexFile
33 os
.environ
['GIT_INDEX_FILE'] = newIndex
35 # This is a global variable which is used in a number of places but
36 # only written to in the 'merge' function.
38 # cacheOnly == True => Don't leave any non-stage 0 entries in the cache and
39 # don't update the working directory.
40 # False => Leave unmerged entries in the cache and update
41 # the working directory.
45 # The entry point to the merge code
46 # ---------------------------------
48 def merge(h1
, h2
, branch1Name
, branch2Name
, graph
, callDepth
=0):
49 '''Merge the commits h1 and h2, return the resulting virtual
50 commit object and a flag indicating the cleaness of the merge.'''
51 assert(isinstance(h1
, Commit
) and isinstance(h2
, Commit
))
52 assert(isinstance(graph
, Graph
))
61 ca
= getCommonAncestors(graph
, h1
, h2
)
62 output('found', len(ca
), 'common ancestor(s):')
69 outputIndent
= callDepth
+1
70 [mergedCA
, dummy
] = merge(mergedCA
, h
,
71 'Temporary merge branch 1',
72 'Temporary merge branch 2',
74 outputIndent
= callDepth
75 assert(isinstance(mergedCA
, Commit
))
83 runProgram(['git-read-tree', h1
.tree()])
86 [shaRes
, clean
] = mergeTrees(h1
.tree(), h2
.tree(), mergedCA
.tree(),
87 branch1Name
, branch2Name
)
89 if clean
or cacheOnly
:
90 res
= Commit(None, [h1
, h2
], tree
=shaRes
)
97 getFilesRE
= re
.compile(r
'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re
.S
)
98 def getFilesAndDirs(tree
):
101 out
= runProgram(['git-ls-tree', '-r', '-z', '-t', tree
])
102 for l
in out
.split('\0'):
103 m
= getFilesRE
.match(l
)
105 if m
.group(2) == 'tree':
107 elif m
.group(2) == 'blob':
108 files
.add(m
.group(4))
112 # Those two global variables are used in a number of places but only
113 # written to in 'mergeTrees' and 'uniquePath'. They keep track of
114 # every file and directory in the two branches that are about to be
116 currentFileSet
= None
117 currentDirectorySet
= None
119 def mergeTrees(head
, merge
, common
, branch1Name
, branch2Name
):
120 '''Merge the trees 'head' and 'merge' with the common ancestor
121 'common'. The name of the head branch is 'branch1Name' and the name of
122 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
123 where tree is the resulting tree and cleanMerge is True iff the
126 assert(isSha(head
) and isSha(merge
) and isSha(common
))
129 output('Already uptodate!')
137 [out
, code
] = runProgram(['git-read-tree', updateArg
, '-m',
138 common
, head
, merge
], returnCode
= True)
140 die('git-read-tree:', out
)
142 [tree
, code
] = runProgram('git-write-tree', returnCode
=True)
145 global currentFileSet
, currentDirectorySet
146 [currentFileSet
, currentDirectorySet
] = getFilesAndDirs(head
)
147 [filesM
, dirsM
] = getFilesAndDirs(merge
)
148 currentFileSet
.union_update(filesM
)
149 currentDirectorySet
.union_update(dirsM
)
151 entries
= unmergedCacheEntries()
152 renamesHead
= getRenames(head
, common
, head
, merge
, entries
)
153 renamesMerge
= getRenames(merge
, common
, head
, merge
, entries
)
155 cleanMerge
= processRenames(renamesHead
, renamesMerge
,
156 branch1Name
, branch2Name
)
157 for entry
in entries
:
160 if not processEntry(entry
, branch1Name
, branch2Name
):
163 if cleanMerge
or cacheOnly
:
164 tree
= runProgram('git-write-tree').rstrip()
170 return [tree
, cleanMerge
]
172 # Low level file merging, update and removal
173 # ------------------------------------------
175 def mergeFile(oPath
, oSha
, oMode
, aPath
, aSha
, aMode
, bPath
, bSha
, bMode
,
176 branch1Name
, branch2Name
):
181 if stat
.S_IFMT(aMode
) != stat
.S_IFMT(bMode
):
183 if stat
.S_ISREG(aMode
):
190 if aSha
!= oSha
and bSha
!= oSha
:
202 elif stat
.S_ISREG(aMode
):
203 assert(stat
.S_ISREG(bMode
))
205 orig
= runProgram(['git-unpack-file', oSha
]).rstrip()
206 src1
= runProgram(['git-unpack-file', aSha
]).rstrip()
207 src2
= runProgram(['git-unpack-file', bSha
]).rstrip()
209 [out
, code
] = runProgram(['merge',
210 '-L', branch1Name
+ '/' + aPath
,
211 '-L', 'orig/' + oPath
,
212 '-L', branch2Name
+ '/' + bPath
,
213 src1
, orig
, src2
], returnCode
=True)
214 except ProgramError
, e
:
215 print >>sys
.stderr
, e
216 die("Failed to execute 'merge'. merge(1) is used as the "
217 "file-level merge tool. Is 'merge' in your path?")
219 sha
= runProgram(['git-hash-object', '-t', 'blob', '-w',
228 assert(stat
.S_ISLNK(aMode
) and stat
.S_ISLNK(bMode
))
234 return [sha
, mode
, clean
, merge
]
236 def updateFile(clean
, sha
, mode
, path
):
237 updateCache
= cacheOnly
or clean
238 updateWd
= not cacheOnly
240 return updateFileExt(sha
, mode
, path
, updateCache
, updateWd
)
242 def updateFileExt(sha
, mode
, path
, updateCache
, updateWd
):
247 pathComponents
= path
.split('/')
248 for x
in xrange(1, len(pathComponents
)):
249 p
= '/'.join(pathComponents
[0:x
])
252 createDir
= not stat
.S_ISDIR(os
.lstat(p
).st_mode
)
260 die("Couldn't create directory", p
, e
.strerror
)
262 prog
= ['git-cat-file', 'blob', sha
]
263 if stat
.S_ISREG(mode
):
272 fd
= os
.open(path
, os
.O_WRONLY | os
.O_TRUNC | os
.O_CREAT
, mode
)
273 proc
= subprocess
.Popen(prog
, stdout
=fd
)
276 elif stat
.S_ISLNK(mode
):
277 linkTarget
= runProgram(prog
)
278 os
.symlink(linkTarget
, path
)
282 if updateWd
and updateCache
:
283 runProgram(['git-update-index', '--add', '--', path
])
285 runProgram(['git-update-index', '--add', '--cacheinfo',
286 '0%o' % mode
, sha
, path
])
288 def setIndexStages(path
,
295 istring
.append("0 " + ("0" * 40) + "\t" + path
+ "\0")
297 istring
.append("%o %s %d\t%s\0" % (oMode
, oSHA1
, 1, path
))
299 istring
.append("%o %s %d\t%s\0" % (aMode
, aSHA1
, 2, path
))
301 istring
.append("%o %s %d\t%s\0" % (bMode
, bSHA1
, 3, path
))
303 runProgram(['git-update-index', '-z', '--index-info'],
304 input="".join(istring
))
306 def removeFile(clean
, path
):
307 updateCache
= cacheOnly
or clean
308 updateWd
= not cacheOnly
311 runProgram(['git-update-index', '--force-remove', '--', path
])
317 if e
.errno
!= errno
.ENOENT
and e
.errno
!= errno
.EISDIR
:
320 os
.removedirs(os
.path
.dirname(path
))
324 def uniquePath(path
, branch
):
325 def fileExists(path
):
330 if e
.errno
== errno
.ENOENT
:
335 branch
= branch
.replace('/', '_')
336 newPath
= path
+ '~' + branch
338 while newPath
in currentFileSet
or \
339 newPath
in currentDirectorySet
or \
342 newPath
= path
+ '~' + branch
+ '_' + str(suffix
)
343 currentFileSet
.add(newPath
)
346 # Cache entry management
347 # ----------------------
350 def __init__(self
, path
):
356 # Used for debugging only
358 if self
.mode
!= None:
359 m
= '0%o' % self
.mode
367 return 'sha1: ' + sha1
+ ' mode: ' + m
369 self
.stages
= [Stage(), Stage(), Stage(), Stage()]
371 self
.processed
= False
374 return 'path: ' + self
.path
+ ' stages: ' + repr([str(x
) for x
in self
.stages
])
376 class CacheEntryContainer
:
380 def add(self
, entry
):
381 self
.entries
[entry
.path
] = entry
384 return self
.entries
.get(path
)
387 return self
.entries
.itervalues()
389 unmergedRE
= re
.compile(r
'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re
.S
)
390 def unmergedCacheEntries():
391 '''Create a dictionary mapping file names to CacheEntry
392 objects. The dictionary contains one entry for every path with a
393 non-zero stage entry.'''
395 lines
= runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
398 res
= CacheEntryContainer()
400 m
= unmergedRE
.match(l
)
402 mode
= int(m
.group(1), 8)
404 stage
= int(m
.group(3))
412 e
.stages
[stage
].mode
= mode
413 e
.stages
[stage
].sha1
= sha1
415 die('Error: Merge program failed: Unexpected output from',
419 lsTreeRE
= re
.compile(r
'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re
.S
)
420 def getCacheEntry(path
, origTree
, aTree
, bTree
):
421 '''Returns a CacheEntry object which doesn't have to correspond to
422 a real cache entry in Git's index.'''
428 m
= lsTreeRE
.match(out
)
430 die('Unexpected output from git-ls-tree:', out
)
431 elif m
.group(2) == 'blob':
432 return [m
.group(3), int(m
.group(1), 8)]
436 res
= CacheEntry(path
)
438 [oSha
, oMode
] = parse(runProgram(['git-ls-tree', origTree
, '--', path
]))
439 [aSha
, aMode
] = parse(runProgram(['git-ls-tree', aTree
, '--', path
]))
440 [bSha
, bMode
] = parse(runProgram(['git-ls-tree', bTree
, '--', path
]))
442 res
.stages
[1].sha1
= oSha
443 res
.stages
[1].mode
= oMode
444 res
.stages
[2].sha1
= aSha
445 res
.stages
[2].mode
= aMode
446 res
.stages
[3].sha1
= bSha
447 res
.stages
[3].mode
= bMode
451 # Rename detection and handling
452 # -----------------------------
456 src
, srcSha
, srcMode
, srcCacheEntry
,
457 dst
, dstSha
, dstMode
, dstCacheEntry
,
461 self
.srcMode
= srcMode
462 self
.srcCacheEntry
= srcCacheEntry
465 self
.dstMode
= dstMode
466 self
.dstCacheEntry
= dstCacheEntry
469 self
.processed
= False
471 class RenameEntryContainer
:
476 def add(self
, entry
):
477 self
.entriesSrc
[entry
.srcName
] = entry
478 self
.entriesDst
[entry
.dstName
] = entry
480 def getSrc(self
, path
):
481 return self
.entriesSrc
.get(path
)
483 def getDst(self
, path
):
484 return self
.entriesDst
.get(path
)
487 return self
.entriesSrc
.itervalues()
489 parseDiffRenamesRE
= re
.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
490 def getRenames(tree
, oTree
, aTree
, bTree
, cacheEntries
):
491 '''Get information of all renames which occured between 'oTree' and
492 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
493 'bTree') to be able to associate the correct cache entries with
494 the rename information. 'tree' is always equal to either aTree or bTree.'''
496 assert(tree
== aTree
or tree
== bTree
)
497 inp
= runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
500 ret
= RenameEntryContainer()
502 recs
= inp
.split("\0")
503 recs
.pop() # remove last entry (which is '')
507 m
= parseDiffRenamesRE
.match(rec
)
510 die('Unexpected output from git-diff-tree:', rec
)
512 srcMode
= int(m
.group(1), 8)
513 dstMode
= int(m
.group(2), 8)
520 srcCacheEntry
= cacheEntries
.get(src
)
521 if not srcCacheEntry
:
522 srcCacheEntry
= getCacheEntry(src
, oTree
, aTree
, bTree
)
523 cacheEntries
.add(srcCacheEntry
)
525 dstCacheEntry
= cacheEntries
.get(dst
)
526 if not dstCacheEntry
:
527 dstCacheEntry
= getCacheEntry(dst
, oTree
, aTree
, bTree
)
528 cacheEntries
.add(dstCacheEntry
)
530 ret
.add(RenameEntry(src
, srcSha
, srcMode
, srcCacheEntry
,
531 dst
, dstSha
, dstMode
, dstCacheEntry
,
533 except StopIteration:
537 def fmtRename(src
, dst
):
538 srcPath
= src
.split('/')
539 dstPath
= dst
.split('/')
541 endIndex
= min(len(srcPath
), len(dstPath
)) - 1
542 for x
in range(0, endIndex
):
543 if srcPath
[x
] == dstPath
[x
]:
544 path
.append(srcPath
[x
])
550 return '/'.join(path
) + \
551 '/{' + '/'.join(srcPath
[endIndex
:]) + ' => ' + \
552 '/'.join(dstPath
[endIndex
:]) + '}'
554 return src
+ ' => ' + dst
556 def processRenames(renamesA
, renamesB
, branchNameA
, branchNameB
):
559 srcNames
.add(x
.srcName
)
561 srcNames
.add(x
.srcName
)
564 for path
in srcNames
:
565 if renamesA
.getSrc(path
):
568 branchName1
= branchNameA
569 branchName2
= branchNameB
573 branchName1
= branchNameB
574 branchName2
= branchNameA
576 ren1
= renames1
.getSrc(path
)
577 ren2
= renames2
.getSrc(path
)
579 ren1
.dstCacheEntry
.processed
= True
580 ren1
.srcCacheEntry
.processed
= True
585 ren1
.processed
= True
588 # Renamed in 1 and renamed in 2
589 assert(ren1
.srcName
== ren2
.srcName
)
590 ren2
.dstCacheEntry
.processed
= True
591 ren2
.processed
= True
593 if ren1
.dstName
!= ren2
.dstName
:
594 output('CONFLICT (rename/rename): Rename',
595 fmtRename(path
, ren1
.dstName
), 'in branch', branchName1
,
596 'rename', fmtRename(path
, ren2
.dstName
), 'in',
600 if ren1
.dstName
in currentDirectorySet
:
601 dstName1
= uniquePath(ren1
.dstName
, branchName1
)
602 output(ren1
.dstName
, 'is a directory in', branchName2
,
603 'adding as', dstName1
, 'instead.')
604 removeFile(False, ren1
.dstName
)
606 dstName1
= ren1
.dstName
608 if ren2
.dstName
in currentDirectorySet
:
609 dstName2
= uniquePath(ren2
.dstName
, branchName2
)
610 output(ren2
.dstName
, 'is a directory in', branchName1
,
611 'adding as', dstName2
, 'instead.')
612 removeFile(False, ren2
.dstName
)
614 dstName2
= ren2
.dstName
615 setIndexStages(dstName1
,
617 ren1
.dstSha
, ren1
.dstMode
,
619 setIndexStages(dstName2
,
622 ren2
.dstSha
, ren2
.dstMode
)
625 removeFile(True, ren1
.srcName
)
627 [resSha
, resMode
, clean
, merge
] = \
628 mergeFile(ren1
.srcName
, ren1
.srcSha
, ren1
.srcMode
,
629 ren1
.dstName
, ren1
.dstSha
, ren1
.dstMode
,
630 ren2
.dstName
, ren2
.dstSha
, ren2
.dstMode
,
631 branchName1
, branchName2
)
633 if merge
or not clean
:
634 output('Renaming', fmtRename(path
, ren1
.dstName
))
637 output('Auto-merging', ren1
.dstName
)
640 output('CONFLICT (content): merge conflict in',
645 setIndexStages(ren1
.dstName
,
646 ren1
.srcSha
, ren1
.srcMode
,
647 ren1
.dstSha
, ren1
.dstMode
,
648 ren2
.dstSha
, ren2
.dstMode
)
650 updateFile(clean
, resSha
, resMode
, ren1
.dstName
)
652 removeFile(True, ren1
.srcName
)
654 # Renamed in 1, maybe changed in 2
655 if renamesA
== renames1
:
660 srcShaOtherBranch
= ren1
.srcCacheEntry
.stages
[stage
].sha1
661 srcModeOtherBranch
= ren1
.srcCacheEntry
.stages
[stage
].mode
663 dstShaOtherBranch
= ren1
.dstCacheEntry
.stages
[stage
].sha1
664 dstModeOtherBranch
= ren1
.dstCacheEntry
.stages
[stage
].mode
668 if ren1
.dstName
in currentDirectorySet
:
669 newPath
= uniquePath(ren1
.dstName
, branchName1
)
670 output('CONFLICT (rename/directory): Rename',
671 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in', branchName1
,
672 'directory', ren1
.dstName
, 'added in', branchName2
)
673 output('Renaming', ren1
.srcName
, 'to', newPath
, 'instead')
675 removeFile(False, ren1
.dstName
)
676 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, newPath
)
677 elif srcShaOtherBranch
== None:
678 output('CONFLICT (rename/delete): Rename',
679 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in',
680 branchName1
, 'and deleted in', branchName2
)
682 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, ren1
.dstName
)
683 elif dstShaOtherBranch
:
684 newPath
= uniquePath(ren1
.dstName
, branchName2
)
685 output('CONFLICT (rename/add): Rename',
686 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in',
687 branchName1
+ '.', ren1
.dstName
, 'added in', branchName2
)
688 output('Adding as', newPath
, 'instead')
689 updateFile(False, dstShaOtherBranch
, dstModeOtherBranch
, newPath
)
692 elif renames2
.getDst(ren1
.dstName
):
693 dst2
= renames2
.getDst(ren1
.dstName
)
694 newPath1
= uniquePath(ren1
.dstName
, branchName1
)
695 newPath2
= uniquePath(dst2
.dstName
, branchName2
)
696 output('CONFLICT (rename/rename): Rename',
697 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in',
698 branchName1
+'. Rename',
699 fmtRename(dst2
.srcName
, dst2
.dstName
), 'in', branchName2
)
700 output('Renaming', ren1
.srcName
, 'to', newPath1
, 'and',
701 dst2
.srcName
, 'to', newPath2
, 'instead')
702 removeFile(False, ren1
.dstName
)
703 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, newPath1
)
704 updateFile(False, dst2
.dstSha
, dst2
.dstMode
, newPath2
)
705 dst2
.processed
= True
712 oName
, oSHA1
, oMode
= ren1
.srcName
, ren1
.srcSha
, ren1
.srcMode
713 aName
, bName
= ren1
.dstName
, ren1
.srcName
714 aSHA1
, bSHA1
= ren1
.dstSha
, srcShaOtherBranch
715 aMode
, bMode
= ren1
.dstMode
, srcModeOtherBranch
716 aBranch
, bBranch
= branchName1
, branchName2
718 if renamesA
!= renames1
:
719 aName
, bName
= bName
, aName
720 aSHA1
, bSHA1
= bSHA1
, aSHA1
721 aMode
, bMode
= bMode
, aMode
722 aBranch
, bBranch
= bBranch
, aBranch
724 [resSha
, resMode
, clean
, merge
] = \
725 mergeFile(oName
, oSHA1
, oMode
,
730 if merge
or not clean
:
731 output('Renaming', fmtRename(ren1
.srcName
, ren1
.dstName
))
734 output('Auto-merging', ren1
.dstName
)
737 output('CONFLICT (rename/modify): Merge conflict in',
742 setIndexStages(ren1
.dstName
,
747 updateFile(clean
, resSha
, resMode
, ren1
.dstName
)
751 # Per entry merge function
752 # ------------------------
754 def processEntry(entry
, branch1Name
, branch2Name
):
755 '''Merge one cache entry.'''
757 debug('processing', entry
.path
, 'clean cache:', cacheOnly
)
762 oSha
= entry
.stages
[1].sha1
763 oMode
= entry
.stages
[1].mode
764 aSha
= entry
.stages
[2].sha1
765 aMode
= entry
.stages
[2].mode
766 bSha
= entry
.stages
[3].sha1
767 bMode
= entry
.stages
[3].mode
769 assert(oSha
== None or isSha(oSha
))
770 assert(aSha
== None or isSha(aSha
))
771 assert(bSha
== None or isSha(bSha
))
773 assert(oMode
== None or type(oMode
) is int)
774 assert(aMode
== None or type(aMode
) is int)
775 assert(bMode
== None or type(bMode
) is int)
777 if (oSha
and (not aSha
or not bSha
)):
779 # Case A: Deleted in one
781 if (not aSha
and not bSha
) or \
782 (aSha
== oSha
and not bSha
) or \
783 (not aSha
and bSha
== oSha
):
784 # Deleted in both or deleted in one and unchanged in the other
786 output('Removing', path
)
787 removeFile(True, path
)
789 # Deleted in one and changed in the other
792 output('CONFLICT (delete/modify):', path
, 'deleted in',
793 branch1Name
, 'and modified in', branch2Name
+ '.',
794 'Version', branch2Name
, 'of', path
, 'left in tree.')
798 output('CONFLICT (modify/delete):', path
, 'deleted in',
799 branch2Name
, 'and modified in', branch1Name
+ '.',
800 'Version', branch1Name
, 'of', path
, 'left in tree.')
804 updateFile(False, sha
, mode
, path
)
806 elif (not oSha
and aSha
and not bSha
) or \
807 (not oSha
and not aSha
and bSha
):
809 # Case B: Added in one.
812 addBranch
= branch1Name
813 otherBranch
= branch2Name
816 conf
= 'file/directory'
818 addBranch
= branch2Name
819 otherBranch
= branch1Name
822 conf
= 'directory/file'
824 if path
in currentDirectorySet
:
826 newPath
= uniquePath(path
, addBranch
)
827 output('CONFLICT (' + conf
+ '):',
828 'There is a directory with name', path
, 'in',
829 otherBranch
+ '. Adding', path
, 'as', newPath
)
831 removeFile(False, path
)
832 updateFile(False, sha
, mode
, newPath
)
834 output('Adding', path
)
835 updateFile(True, sha
, mode
, path
)
837 elif not oSha
and aSha
and bSha
:
839 # Case C: Added in both (check for same permissions).
844 output('CONFLICT: File', path
,
845 'added identically in both branches, but permissions',
846 'conflict', '0%o' % aMode
, '->', '0%o' % bMode
)
847 output('CONFLICT: adding with permission:', '0%o' % aMode
)
849 updateFile(False, aSha
, aMode
, path
)
851 # This case is handled by git-read-tree
855 newPath1
= uniquePath(path
, branch1Name
)
856 newPath2
= uniquePath(path
, branch2Name
)
857 output('CONFLICT (add/add): File', path
,
858 'added non-identically in both branches. Adding as',
859 newPath1
, 'and', newPath2
, 'instead.')
860 removeFile(False, path
)
861 updateFile(False, aSha
, aMode
, newPath1
)
862 updateFile(False, bSha
, bMode
, newPath2
)
864 elif oSha
and aSha
and bSha
:
866 # case D: Modified in both, but differently.
868 output('Auto-merging', path
)
869 [sha
, mode
, clean
, dummy
] = \
870 mergeFile(path
, oSha
, oMode
,
873 branch1Name
, branch2Name
)
875 updateFile(True, sha
, mode
, path
)
878 output('CONFLICT (content): Merge conflict in', path
)
881 updateFile(False, sha
, mode
, path
)
883 updateFileExt(sha
, mode
, path
, updateCache
=False, updateWd
=True)
885 die("ERROR: Fatal merge failure, shouldn't happen.")
890 die('Usage:', sys
.argv
[0], ' <base>... -- <head> <remote>..')
892 # main entry point as merge strategy module
893 # The first parameters up to -- are merge bases, and the rest are heads.
894 # This strategy module figures out merge bases itself, so we only
897 if len(sys
.argv
) < 4:
900 for nextArg
in xrange(1, len(sys
.argv
)):
901 if sys
.argv
[nextArg
] == '--':
902 if len(sys
.argv
) != nextArg
+ 3:
903 die('Not handling anything other than two heads merge.')
905 h1
= firstBranch
= sys
.argv
[nextArg
+ 1]
906 h2
= secondBranch
= sys
.argv
[nextArg
+ 2]
911 print 'Merging', h1
, 'with', h2
914 h1
= runProgram(['git-rev-parse', '--verify', h1
+ '^0']).rstrip()
915 h2
= runProgram(['git-rev-parse', '--verify', h2
+ '^0']).rstrip()
917 graph
= buildGraph([h1
, h2
])
919 [dummy
, clean
] = merge(graph
.shaMap
[h1
], graph
.shaMap
[h2
],
920 firstBranch
, secondBranch
, graph
)
924 if isinstance(sys
.exc_info()[1], SystemExit):
927 traceback
.print_exc(None, sys
.stderr
)