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 def merge(h1
, h2
, branch1Name
, branch2Name
, graph
, callDepth
=0):
14 '''Merge the commits h1 and h2, return the resulting virtual
15 commit object and a flag indicating the cleaness of the merge.'''
16 assert(isinstance(h1
, Commit
) and isinstance(h2
, Commit
))
17 assert(isinstance(graph
, Graph
))
20 sys
.stdout
.write(' '*callDepth
)
27 ca
= getCommonAncestors(graph
, h1
, h2
)
28 infoMsg('found', len(ca
), 'common ancestor(s):')
35 [Ms
, ignore
] = merge(Ms
, h
,
36 'Temporary shared merge branch 1',
37 'Temporary shared merge branch 2',
39 assert(isinstance(Ms
, Commit
))
43 runProgram(['git-read-tree', h1
.tree()])
44 runProgram(['git-update-index', '-q', '--refresh'])
45 # Use the original index if we only have one common ancestor
49 runProgram(['git-read-tree', h1
.tree()])
52 [shaRes
, clean
] = mergeTrees(h1
.tree(), h2
.tree(), Ms
.tree(),
53 branch1Name
, branch2Name
,
56 if clean
or cleanCache
:
57 res
= Commit(None, [h1
, h2
], tree
=shaRes
)
64 getFilesRE
= re
.compile('([0-9]+) ([a-z0-9]+) ([0-9a-f]{40})\t(.*)')
65 def getFilesAndDirs(tree
):
68 out
= runProgram(['git-ls-tree', '-r', '-z', tree
])
69 for l
in out
.split('\0'):
70 m
= getFilesRE
.match(l
)
72 if m
.group(2) == 'tree':
74 elif m
.group(2) == 'blob':
80 def __init__(self
, path
):
86 self
.stages
= [Stage(), Stage(), Stage()]
89 unmergedRE
= re
.compile('^([0-9]+) ([0-9a-f]{40}) ([1-3])\t(.*)$')
90 def unmergedCacheEntries():
91 '''Create a dictionary mapping file names to CacheEntry
92 objects. The dictionary contains one entry for every path with a
93 non-zero stage entry.'''
95 lines
= runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
100 m
= unmergedRE
.match(l
)
102 mode
= int(m
.group(1), 8)
104 stage
= int(m
.group(3)) - 1
107 if res
.has_key(path
):
113 e
.stages
[stage
].mode
= mode
114 e
.stages
[stage
].sha1
= sha1
116 die('Error: Merge program failed: Unexpected output from', \
120 def mergeTrees(head
, merge
, common
, branch1Name
, branch2Name
,
122 '''Merge the trees 'head' and 'merge' with the common ancestor
123 'common'. The name of the head branch is 'branch1Name' and the name of
124 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
125 where tree is the resulting tree and cleanMerge is True iff the
128 assert(isSha(head
) and isSha(merge
) and isSha(common
))
131 print 'Already uptodate!'
139 runProgram(['git-read-tree', updateArg
, '-m', common
, head
, merge
])
142 [tree
, code
] = runProgram('git-write-tree', returnCode
=True)
145 [files
, dirs
] = getFilesAndDirs(head
)
146 [filesM
, dirsM
] = getFilesAndDirs(merge
)
147 files
.union_update(filesM
)
148 dirs
.union_update(dirsM
)
151 entries
= unmergedCacheEntries()
153 if not processEntry(entries
[name
], branch1Name
, branch2Name
,
154 files
, dirs
, cleanCache
):
157 if cleanMerge
or cleanCache
:
158 tree
= runProgram('git-write-tree').rstrip()
164 return [tree
, cleanMerge
]
166 def processEntry(entry
, branch1Name
, branch2Name
, files
, dirs
, cleanCache
):
167 '''Merge one cache entry. 'files' is a Set with the files in both of
168 the heads that we are going to merge. 'dirs' contains the
169 corresponding data for directories. If 'cleanCache' is True no
170 non-zero stages will be left in the cache for the path
171 corresponding to the entry 'entry'.'''
173 # cleanCache == True => Don't leave any non-stage 0 entries in the cache and
174 # don't update the working directory
175 # False => Leave unmerged entries and update the working directory
177 # clean == True => non-conflict case
178 # False => conflict case
180 # If cleanCache == False then the cache shouldn't be updated if clean == False
182 def updateFile(clean
, sha
, mode
, path
, onlyWd
=False):
183 updateCache
= not onlyWd
and (cleanCache
or (not cleanCache
and clean
))
184 updateWd
= onlyWd
or (not cleanCache
and clean
)
187 prog
= ['git-cat-file', 'blob', sha
]
188 if stat
.S_ISREG(mode
):
197 fd
= os
.open(path
, os
.O_WRONLY | os
.O_TRUNC | os
.O_CREAT
, mode
)
198 proc
= subprocess
.Popen(prog
, stdout
=fd
)
201 elif stat
.S_ISLNK(mode
):
202 linkTarget
= runProgram(prog
)
203 os
.symlink(linkTarget
, path
)
207 if updateWd
and updateCache
:
208 runProgram(['git-update-index', '--add', '--', path
])
210 runProgram(['git-update-index', '--add', '--cacheinfo',
211 '0%o' % mode
, sha
, path
])
213 def removeFile(clean
, path
):
214 if cleanCache
or (not cleanCache
and clean
):
215 runProgram(['git-update-index', '--force-remove', '--', path
])
217 if not cleanCache
and clean
:
221 if e
.errno
!= errno
.ENOENT
and e
.errno
!= errno
.EISDIR
:
224 def uniquePath(path
, branch
):
225 newPath
= path
+ '_' + branch
227 while newPath
in files
or newPath
in dirs
:
229 newPath
= path
+ '_' + branch
+ '_' + str(suffix
)
233 debug('processing', entry
.path
, 'clean cache:', cleanCache
)
238 oSha
= entry
.stages
[0].sha1
239 oMode
= entry
.stages
[0].mode
240 aSha
= entry
.stages
[1].sha1
241 aMode
= entry
.stages
[1].mode
242 bSha
= entry
.stages
[2].sha1
243 bMode
= entry
.stages
[2].mode
245 assert(oSha
== None or isSha(oSha
))
246 assert(aSha
== None or isSha(aSha
))
247 assert(bSha
== None or isSha(bSha
))
249 assert(oMode
== None or type(oMode
) is int)
250 assert(aMode
== None or type(aMode
) is int)
251 assert(bMode
== None or type(bMode
) is int)
253 if (oSha
and (not aSha
or not bSha
)):
255 # Case A: Deleted in one
257 if (not aSha
and not bSha
) or \
258 (aSha
== oSha
and not bSha
) or \
259 (not aSha
and bSha
== oSha
):
260 # Deleted in both or deleted in one and unchanged in the other
262 print 'Removing ' + path
263 removeFile(True, path
)
265 # Deleted in one and changed in the other
268 print 'CONFLICT (del/mod): "' + path
+ '" deleted in', \
269 branch1Name
, 'and modified in', branch2Name
, \
270 '. Version', branch2Name
, ' of "' + path
+ \
275 print 'CONFLICT (mod/del): "' + path
+ '" deleted in', \
276 branch2Name
, 'and modified in', branch1Name
+ \
277 '. Version', branch1Name
, 'of "' + path
+ \
282 updateFile(False, sha
, mode
, path
)
284 elif (not oSha
and aSha
and not bSha
) or \
285 (not oSha
and not aSha
and bSha
):
287 # Case B: Added in one.
290 addBranch
= branch1Name
291 otherBranch
= branch2Name
296 addBranch
= branch2Name
297 otherBranch
= branch1Name
304 newPath
= uniquePath(path
, addBranch
)
305 print 'CONFLICT (' + conf
+ \
306 '): There is a directory with name "' + path
+ '" in', \
307 otherBranch
+ '. Adding "' + path
+ '" as "' + newPath
+ '"'
309 removeFile(False, path
)
312 print 'Adding "' + path
+ '"'
314 updateFile(True, sha
, mode
, path
)
316 elif not oSha
and aSha
and bSha
:
318 # Case C: Added in both (check for same permissions).
323 print 'CONFLICT: File "' + path
+ \
324 '" added identically in both branches,', \
325 'but permissions conflict', '0%o' % aMode
, '->', \
327 print 'CONFLICT: adding with permission:', '0%o' % aMode
329 updateFile(False, aSha
, aMode
, path
)
331 # This case is handled by git-read-tree
335 newPath1
= uniquePath(path
, branch1Name
)
336 newPath2
= uniquePath(path
, branch2Name
)
337 print 'CONFLICT (add/add): File "' + path
+ \
338 '" added non-identically in both branches.'
339 removeFile(False, path
)
340 updateFile(False, aSha
, aMode
, newPath1
)
341 updateFile(False, bSha
, bMode
, newPath2
)
343 elif oSha
and aSha
and bSha
:
345 # case D: Modified in both, but differently.
347 print 'Auto-merging', path
348 orig
= runProgram(['git-unpack-file', oSha
]).rstrip()
349 src1
= runProgram(['git-unpack-file', aSha
]).rstrip()
350 src2
= runProgram(['git-unpack-file', bSha
]).rstrip()
351 [out
, ret
] = runProgram(['merge',
352 '-L', branch1Name
+ '/' + path
,
353 '-L', 'orig/' + path
,
354 '-L', branch2Name
+ '/' + path
,
355 src1
, orig
, src2
], returnCode
=True)
362 sha
= runProgram(['git-hash-object', '-t', 'blob', '-w',
367 print 'CONFLICT (content): Merge conflict in "' + path
+ '".'
370 updateFile(False, sha
, mode
, path
)
372 updateFile(True, aSha
, aMode
, path
)
373 updateFile(False, sha
, mode
, path
, True)
375 updateFile(True, sha
, mode
, path
)
381 die("ERROR: Fatal merge failure, shouldn't happen.")
386 die('Usage:', sys
.argv
[0], ' <base>... -- <head> <remote>..')
388 # main entry point as merge strategy module
389 # The first parameters up to -- are merge bases, and the rest are heads.
390 # This strategy module figures out merge bases itself, so we only
393 if len(sys
.argv
) < 4:
396 for nextArg
in xrange(1, len(sys
.argv
)):
397 if sys
.argv
[nextArg
] == '--':
398 if len(sys
.argv
) != nextArg
+ 3:
399 die('Not handling anything other than two heads merge.')
401 h1
= firstBranch
= sys
.argv
[nextArg
+ 1]
402 h2
= secondBranch
= sys
.argv
[nextArg
+ 2]
407 print 'Merging', h1
, 'with', h2
410 h1
= runProgram(['git-rev-parse', '--verify', h1
+ '^0']).rstrip()
411 h2
= runProgram(['git-rev-parse', '--verify', h2
+ '^0']).rstrip()
413 graph
= buildGraph([h1
, h2
])
415 [res
, clean
] = merge(graph
.shaMap
[h1
], graph
.shaMap
[h2
],
416 firstBranch
, secondBranch
, graph
)
420 traceback
.print_exc(None, sys
.stderr
)