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 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 cleanCache
:
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 die('Error: Merge program failed: Unexpected output from', \
127 def mergeTrees(head
, merge
, common
, branch1Name
, branch2Name
,
128 cleanCache
, updateWd
):
129 '''Merge the trees 'head' and 'merge' with the common ancestor
130 'common'. The name of the head branch is 'branch1Name' and the name of
131 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
132 where tree is the resulting tree and cleanMerge is True iff the
135 assert(isSha(head
) and isSha(merge
) and isSha(common
))
138 print 'Already uptodate!'
145 runProgram(['git-read-tree', updateArg
, '-m', common
, head
, merge
])
148 [tree
, code
] = runProgram('git-write-tree', returnCode
=True)
151 [files
, dirs
] = getFilesAndDirs(head
)
152 [filesM
, dirsM
] = getFilesAndDirs(merge
)
153 files
.union_update(filesM
)
154 dirs
.union_update(dirsM
)
157 entries
= unmergedCacheEntries()
159 if not processEntry(entries
[name
], branch1Name
, branch2Name
,
160 files
, dirs
, cleanCache
, updateWd
):
163 if cleanMerge
or cleanCache
:
164 tree
= runProgram('git-write-tree').rstrip()
170 return [tree
, cleanMerge
]
172 def processEntry(entry
, branch1Name
, branch2Name
, files
, dirs
,
173 cleanCache
, updateWd
):
174 '''Merge one cache entry. 'files' is a Set with the files in both of
175 the heads that we are going to merge. 'dirs' contains the
176 corresponding data for directories. If 'cleanCache' is True no
177 non-zero stages will be left in the cache for the path
178 corresponding to the entry 'entry'.'''
180 # cleanCache == True => Don't leave any non-stage 0 entries in the cache.
181 # False => Leave unmerged entries
183 # updateWd == True => Update the working directory to correspond to the cache
184 # False => Leave the working directory unchanged
186 # clean == True => non-conflict case
187 # False => conflict case
189 # If cleanCache == False then the cache shouldn't be updated if clean == False
191 def updateFile(clean
, sha
, mode
, path
):
192 if cleanCache
or (not cleanCache
and clean
):
193 runProgram(['git-update-cache', '--add', '--cacheinfo',
194 '0%o' % mode
, sha
, path
])
197 prog
= ['git-cat-file', 'blob', sha
]
198 if stat
.S_ISREG(mode
):
207 fd
= os
.open(path
, os
.O_WRONLY | os
.O_TRUNC | os
.O_CREAT
, mode
)
208 proc
= subprocess
.Popen(prog
, stdout
=fd
)
211 elif stat
.S_ISLNK(mode
):
212 linkTarget
= runProgram(prog
)
213 os
.symlink(linkTarget
, path
)
216 runProgram(['git-update-cache', '--', path
])
218 def removeFile(clean
, path
):
219 if cleanCache
or (not cleanCache
and clean
):
220 runProgram(['git-update-cache', '--force-remove', '--', path
])
226 if e
.errno
!= errno
.ENOENT
and e
.errno
!= errno
.EISDIR
:
229 def uniquePath(path
, branch
):
230 newPath
= path
+ '_' + branch
232 while newPath
in files
or newPath
in dirs
:
234 newPath
= path
+ '_' + branch
+ '_' + str(suffix
)
238 debug('processing', entry
.path
, 'clean cache:', cleanCache
,
244 oSha
= entry
.stages
[0].sha1
245 oMode
= entry
.stages
[0].mode
246 aSha
= entry
.stages
[1].sha1
247 aMode
= entry
.stages
[1].mode
248 bSha
= entry
.stages
[2].sha1
249 bMode
= entry
.stages
[2].mode
251 assert(oSha
== None or isSha(oSha
))
252 assert(aSha
== None or isSha(aSha
))
253 assert(bSha
== None or isSha(bSha
))
255 assert(oMode
== None or type(oMode
) is int)
256 assert(aMode
== None or type(aMode
) is int)
257 assert(bMode
== None or type(bMode
) is int)
259 if (oSha
and (not aSha
or not bSha
)):
261 # Case A: Deleted in one
263 if (not aSha
and not bSha
) or \
264 (aSha
== oSha
and not bSha
) or \
265 (not aSha
and bSha
== oSha
):
266 # Deleted in both or deleted in one and unchanged in the other
268 print 'Removing ' + path
269 removeFile(True, path
)
271 # Deleted in one and changed in the other
274 print 'CONFLICT (del/mod): "' + path
+ '" deleted in', \
275 branch1Name
, 'and modified in', branch2Name
, \
276 '. Version', branch2Name
, ' of "' + path
+ \
281 print 'CONFLICT (mod/del): "' + path
+ '" deleted in', \
282 branch2Name
, 'and modified in', branch1Name
+ \
283 '. Version', branch1Name
, 'of "' + path
+ \
288 updateFile(False, sha
, mode
, path
)
290 elif (not oSha
and aSha
and not bSha
) or \
291 (not oSha
and not aSha
and bSha
):
293 # Case B: Added in one.
296 addBranch
= branch1Name
297 otherBranch
= branch2Name
302 addBranch
= branch2Name
303 otherBranch
= branch1Name
310 newPath
= uniquePath(path
, addBranch
)
311 print 'CONFLICT (' + conf
+ \
312 '): There is a directory with name "' + path
+ '" in', \
313 otherBranch
+ '. Adding "' + path
+ '" as "' + newPath
+ '"'
315 removeFile(False, path
)
318 print 'Adding "' + path
+ '"'
320 updateFile(True, sha
, mode
, path
)
322 elif not oSha
and aSha
and bSha
:
324 # Case C: Added in both (check for same permissions).
329 print 'CONFLICT: File "' + path
+ \
330 '" added identically in both branches,'
331 print 'CONFLICT: but permissions conflict', '0%o' % aMode
, \
333 print 'CONFLICT: adding with permission:', '0%o' % aMode
335 updateFile(False, aSha
, aMode
, path
)
337 # This case is handled by git-read-tree
341 newPath1
= uniquePath(path
, branch1Name
)
342 newPath2
= uniquePath(path
, branch2Name
)
343 print 'CONFLICT (add/add): File "' + path
+ \
344 '" added non-identically in both branches.', \
345 'Adding "' + newPath1
+ '" and "' + newPath2
+ '" instead.'
346 removeFile(False, path
)
347 updateFile(False, aSha
, aMode
, newPath1
)
348 updateFile(False, bSha
, bMode
, newPath2
)
350 elif oSha
and aSha
and bSha
:
352 # case D: Modified in both, but differently.
354 print 'Auto-merging', path
355 orig
= runProgram(['git-unpack-file', oSha
]).rstrip()
356 src1
= runProgram(['git-unpack-file', aSha
]).rstrip()
357 src2
= runProgram(['git-unpack-file', bSha
]).rstrip()
358 [out
, ret
] = runProgram(['merge',
359 '-L', branch1Name
+ '/' + path
,
360 '-L', 'orig/' + path
,
361 '-L', branch2Name
+ '/' + path
,
362 src1
, orig
, src2
], returnCode
=True)
369 sha
= runProgram(['git-hash-object', '-t', 'blob', '-w',
374 print 'CONFLICT (content): Merge conflict in "' + path
+ '".'
375 updateFile(False, sha
, mode
, path
)
377 updateFile(True, sha
, mode
, path
)
383 die("ERROR: Fatal merge failure, shouldn't happen.")
388 die('Usage:', sys
.argv
[0], ' <base>... -- <head> <remote>..')
390 # main entry point as merge strategy module
391 # The first parameters up to -- are merge bases, and the rest are heads.
392 # This strategy module figures out merge bases itself, so we only
395 for nextArg
in xrange(1, len(sys
.argv
)):
396 if sys
.argv
[nextArg
] == '--':
397 if len(sys
.argv
) != nextArg
+ 3:
398 die('Not handling anything other than two heads merge.')
400 h1
= firstBranch
= sys
.argv
[nextArg
+ 1]
401 h2
= secondBranch
= sys
.argv
[nextArg
+ 2]
406 print 'Merging', h1
, 'with', h2
409 h1
= runProgram(['git-rev-parse', '--verify', h1
+ '^0']).rstrip()
410 h2
= runProgram(['git-rev-parse', '--verify', h2
+ '^0']).rstrip()
412 graph
= buildGraph([h1
, h2
])
414 [res
, clean
] = merge(graph
.shaMap
[h1
], graph
.shaMap
[h2
],
415 firstBranch
, secondBranch
, graph
)
419 traceback
.print_exc(None, sys
.stderr
)
425 print 'Automatic merge failed, fix up by hand'