3 import sys
, math
, random
, os
, re
, signal
, tempfile
, stat
, errno
4 from heapq
import heappush
, heappop
7 sys
.path
.append('@@GIT_PYTHON_PATH@@')
8 from gitMergeCommon
import *
10 alwaysWriteTree
= False
12 # The actual merge code
13 # ---------------------
15 def merge(h1
, h2
, branch1Name
, branch2Name
, graph
, callDepth
=0):
16 '''Merge the commits h1 and h2, return the resulting virtual
17 commit object and a flag indicating the cleaness of the merge.'''
18 assert(isinstance(h1
, Commit
) and isinstance(h2
, Commit
))
19 assert(isinstance(graph
, Graph
))
22 sys
.stdout
.write(' '*callDepth
)
29 ca
= getCommonAncestors(graph
, h1
, h2
)
30 infoMsg('found', len(ca
), 'common ancestor(s):')
37 [Ms
, ignore
] = merge(Ms
, h
,
38 'Temporary shared merge branch 1',
39 'Temporary shared merge branch 2',
41 assert(isinstance(Ms
, Commit
))
45 runProgram(['git-read-tree', h1
.tree()])
46 runProgram(['git-update-cache', '-q', '--refresh'])
47 # Use the original index if we only have one common ancestor
55 runProgram(['git-read-tree', h1
.tree()])
59 [shaRes
, clean
] = mergeTrees(h1
.tree(), h2
.tree(), Ms
.tree(),
60 branch1Name
, branch2Name
,
63 if clean
or alwaysWriteTree
:
64 res
= Commit(None, [h1
, h2
], tree
=shaRes
)
71 getFilesRE
= re
.compile('([0-9]+) ([a-z0-9]+) ([0-9a-f]{40})\t(.*)')
72 def getFilesAndDirs(tree
):
75 out
= runProgram(['git-ls-tree', '-r', '-z', tree
])
76 for l
in out
.split('\0'):
77 m
= getFilesRE
.match(l
)
79 if m
.group(2) == 'tree':
81 elif m
.group(2) == 'blob':
87 def __init__(self
, path
):
93 self
.stages
= [Stage(), Stage(), Stage()]
96 unmergedRE
= re
.compile('^([0-9]+) ([0-9a-f]{40}) ([1-3])\t(.*)$')
97 def unmergedCacheEntries():
98 '''Create a dictionary mapping file names to CacheEntry
99 objects. The dictionary contains one entry for every path with a
100 non-zero stage entry.'''
102 lines
= runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
107 m
= unmergedRE
.match(l
)
109 mode
= int(m
.group(1), 8)
111 stage
= int(m
.group(3)) - 1
114 if res
.has_key(path
):
120 e
.stages
[stage
].mode
= mode
121 e
.stages
[stage
].sha1
= sha1
123 print 'Error: Merge program failed: Unexpected output from', \
128 def mergeTrees(head
, merge
, common
, branch1Name
, branch2Name
,
129 cleanCache
, updateWd
):
130 '''Merge the trees 'head' and 'merge' with the common ancestor
131 'common'. The name of the head branch is 'branch1Name' and the name of
132 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
133 where tree is the resulting tree and cleanMerge is True iff the
136 assert(isSha(head
) and isSha(merge
) and isSha(common
))
139 print 'Already uptodate!'
146 runProgram(['git-read-tree', updateArg
, '-m', common
, head
, merge
])
149 [tree
, code
] = runProgram('git-write-tree', returnCode
=True)
152 [files
, dirs
] = getFilesAndDirs(head
)
153 [filesM
, dirsM
] = getFilesAndDirs(merge
)
154 files
.union_update(filesM
)
155 dirs
.union_update(dirsM
)
158 entries
= unmergedCacheEntries()
160 if not processEntry(entries
[name
], branch1Name
, branch2Name
,
161 files
, dirs
, cleanCache
, updateWd
):
164 if cleanMerge
or cleanCache
:
165 tree
= runProgram('git-write-tree').rstrip()
171 return [tree
, cleanMerge
]
173 def processEntry(entry
, branch1Name
, branch2Name
, files
, dirs
,
174 cleanCache
, updateWd
):
175 '''Merge one cache entry. 'files' is a Set with the files in both of
176 the heads that we are going to merge. 'dirs' contains the
177 corresponding data for directories. If 'cleanCache' is True no
178 non-zero stages will be left in the cache for the path
179 corresponding to the entry 'entry'.'''
181 # cleanCache == True => Don't leave any non-stage 0 entries in the cache.
182 # False => Leave unmerged entries
184 # updateWd == True => Update the working directory to correspond to the cache
185 # False => Leave the working directory unchanged
187 # clean == True => non-conflict case
188 # False => conflict case
190 # If cleanCache == False then the cache shouldn't be updated if clean == False
192 def updateFile(clean
, sha
, mode
, path
):
193 if cleanCache
or (not cleanCache
and clean
):
194 runProgram(['git-update-cache', '--add', '--cacheinfo',
195 '0%o' % mode
, sha
, path
])
198 prog
= ['git-cat-file', 'blob', sha
]
199 if stat
.S_ISREG(mode
):
208 fd
= os
.open(path
, os
.O_WRONLY | os
.O_TRUNC | os
.O_CREAT
, mode
)
209 proc
= subprocess
.Popen(prog
, stdout
=fd
)
212 elif stat
.S_ISLNK(mode
):
213 linkTarget
= runProgram(prog
)
214 os
.symlink(linkTarget
, path
)
217 runProgram(['git-update-cache', '--', path
])
219 def removeFile(clean
, path
):
220 if cleanCache
or (not cleanCache
and clean
):
221 runProgram(['git-update-cache', '--force-remove', '--', path
])
227 if e
.errno
!= errno
.ENOENT
and e
.errno
!= errno
.EISDIR
:
230 def uniquePath(path
, branch
):
231 newPath
= path
+ '_' + branch
233 while newPath
in files
or newPath
in dirs
:
235 newPath
= path
+ '_' + branch
+ '_' + str(suffix
)
239 debug('processing', entry
.path
, 'clean cache:', cleanCache
,
245 oSha
= entry
.stages
[0].sha1
246 oMode
= entry
.stages
[0].mode
247 aSha
= entry
.stages
[1].sha1
248 aMode
= entry
.stages
[1].mode
249 bSha
= entry
.stages
[2].sha1
250 bMode
= entry
.stages
[2].mode
252 assert(oSha
== None or isSha(oSha
))
253 assert(aSha
== None or isSha(aSha
))
254 assert(bSha
== None or isSha(bSha
))
256 assert(oMode
== None or type(oMode
) is int)
257 assert(aMode
== None or type(aMode
) is int)
258 assert(bMode
== None or type(bMode
) is int)
260 if (oSha
and (not aSha
or not bSha
)):
262 # Case A: Deleted in one
264 if (not aSha
and not bSha
) or \
265 (aSha
== oSha
and not bSha
) or \
266 (not aSha
and bSha
== oSha
):
267 # Deleted in both or deleted in one and unchanged in the other
269 print 'Removing ' + path
270 removeFile(True, path
)
272 # Deleted in one and changed in the other
275 print 'CONFLICT (del/mod): "' + path
+ '" deleted in', \
276 branch1Name
, 'and modified in', branch2Name
, \
277 '. Version', branch2Name
, ' of "' + path
+ \
282 print 'CONFLICT (mod/del): "' + path
+ '" deleted in', \
283 branch2Name
, 'and modified in', branch1Name
+ \
284 '. Version', branch1Name
, 'of "' + path
+ \
289 updateFile(False, sha
, mode
, path
)
291 elif (not oSha
and aSha
and not bSha
) or \
292 (not oSha
and not aSha
and bSha
):
294 # Case B: Added in one.
297 addBranch
= branch1Name
298 otherBranch
= branch2Name
303 addBranch
= branch2Name
304 otherBranch
= branch1Name
311 newPath
= uniquePath(path
, addBranch
)
312 print 'CONFLICT (' + conf
+ \
313 '): There is a directory with name "' + path
+ '" in', \
314 otherBranch
+ '. Adding "' + path
+ '" as "' + newPath
+ '"'
316 removeFile(False, path
)
319 print 'Adding "' + path
+ '"'
321 updateFile(True, sha
, mode
, path
)
323 elif not oSha
and aSha
and bSha
:
325 # Case C: Added in both (check for same permissions).
330 print 'CONFLICT: File "' + path
+ \
331 '" added identically in both branches,'
332 print 'CONFLICT: but permissions conflict', '0%o' % aMode
, \
334 print 'CONFLICT: adding with permission:', '0%o' % aMode
336 updateFile(False, aSha
, aMode
, path
)
338 # This case is handled by git-read-tree
342 newPath1
= uniquePath(path
, branch1Name
)
343 newPath2
= uniquePath(path
, branch2Name
)
344 print 'CONFLICT (add/add): File "' + path
+ \
345 '" added non-identically in both branches.', \
346 'Adding "' + newPath1
+ '" and "' + newPath2
+ '" instead.'
347 removeFile(False, path
)
348 updateFile(False, aSha
, aMode
, newPath1
)
349 updateFile(False, bSha
, bMode
, newPath2
)
351 elif oSha
and aSha
and bSha
:
353 # case D: Modified in both, but differently.
355 print 'Auto-merging', path
356 orig
= runProgram(['git-unpack-file', oSha
]).rstrip()
357 src1
= runProgram(['git-unpack-file', aSha
]).rstrip()
358 src2
= runProgram(['git-unpack-file', bSha
]).rstrip()
359 [out
, ret
] = runProgram(['merge',
360 '-L', branch1Name
+ '/' + path
,
361 '-L', 'orig/' + path
,
362 '-L', branch2Name
+ '/' + path
,
363 src1
, orig
, src2
], returnCode
=True)
370 sha
= runProgram(['git-hash-object', '-t', 'blob', '-w',
375 print 'CONFLICT (content): Merge conflict in "' + path
+ '".'
376 updateFile(False, sha
, mode
, path
)
378 updateFile(True, sha
, mode
, path
)
384 print 'ERROR: Fatal merge failure.'
385 print "ERROR: Shouldn't happen"
391 print 'Usage:', sys
.argv
[0], ' <base>... -- <head> <remote>..'
394 # main entry point as merge strategy module
395 # The first parameters up to -- are merge bases, and the rest are heads.
396 # This strategy module figures out merge bases itself, so we only
399 for nextArg
in xrange(1, len(sys
.argv
)):
400 if sys
.argv
[nextArg
] == '--':
401 if len(sys
.argv
) != nextArg
+ 3:
402 print 'Not handling anything other than two heads merge.'
405 h1
= firstBranch
= sys
.argv
[nextArg
+ 1]
406 h2
= secondBranch
= sys
.argv
[nextArg
+ 2]
411 print 'Merging', h1
, 'with', h2
412 h1
= runProgram(['git-rev-parse', '--verify', h1
+ '^0']).rstrip()
413 h2
= runProgram(['git-rev-parse', '--verify', h2
+ '^0']).rstrip()
415 graph
= buildGraph([h1
, h2
])
417 [res
, clean
] = merge(graph
.shaMap
[h1
], graph
.shaMap
[h2
],
418 firstBranch
, secondBranch
, graph
)
425 print 'Automatic merge failed, fix up by hand'