1#!/usr/bin/python
2
3import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
4from heapq import heappush, heappop
5from sets import Set
6
7sys.path.append('''@@GIT_PYTHON_PATH@@''')
8from gitMergeCommon import *
9
10originalIndexFile = os.environ.get('GIT_INDEX_FILE',
11 os.environ.get('GIT_DIR', '.git') + '/index')
12temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
13 '/merge-recursive-tmp-index'
14def 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
24
25# This is a global variable which is used in a number of places but
26# only written to in the 'merge' function.
27
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.
32
33cacheOnly = False
34
35# The entry point to the merge code
36# ---------------------------------
37
38def 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))
43
44 def infoMsg(*args):
45 sys.stdout.write(' '*callDepth)
46 printList(args)
47
48 infoMsg('Merging:')
49 infoMsg(h1)
50 infoMsg(h2)
51 sys.stdout.flush()
52
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()
58
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))
66
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
75
76 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
77 branch1Name, branch2Name)
78
79 if clean or cacheOnly:
80 res = Commit(None, [h1, h2], tree=shaRes)
81 graph.addNode(res)
82 else:
83 res = None
84
85 return [res, clean]
86
87getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
88def 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))
99
100 return [files, dirs]
101
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.
106currentFileSet = None
107currentDirectorySet = None
108
109def 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.'''
115
116 assert(isSha(head) and isSha(merge) and isSha(common))
117
118 if common == merge:
119 print 'Already uptodate!'
120 return [head, True]
121
122 if cacheOnly:
123 updateArg = '-i'
124 else:
125 updateArg = '-u'
126
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)
131
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)
140
141 entries = unmergedCacheEntries()
142 renamesHead = getRenames(head, common, head, merge, entries)
143 renamesMerge = getRenames(merge, common, head, merge, entries)
144
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
152
153 if cleanMerge or cacheOnly:
154 tree = runProgram('git-write-tree').rstrip()
155 else:
156 tree = None
157 else:
158 cleanMerge = True
159
160 return [tree, cleanMerge]
161
162# Low level file merging, update and removal
163# ------------------------------------------
164
165def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
166 branch1Name, branch2Name):
167
168 merge = False
169 clean = True
170
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
182
183 if aMode == oMode:
184 mode = bMode
185 else:
186 mode = aMode
187
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))
194
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)
203
204 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
205 src1]).rstrip()
206
207 os.unlink(orig)
208 os.unlink(src1)
209 os.unlink(src2)
210
211 clean = (code == 0)
212 else:
213 assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
214 sha = aSha
215
216 if aSha != bSha:
217 clean = False
218
219 return [sha, mode, clean, merge]
220
221def updateFile(clean, sha, mode, path):
222 updateCache = cacheOnly or clean
223 updateWd = not cacheOnly
224
225 return updateFileExt(sha, mode, path, updateCache, updateWd)
226
227def updateFileExt(sha, mode, path, updateCache, updateWd):
228 if cacheOnly:
229 updateWd = False
230
231 if updateWd:
232 pathComponents = path.split('/')
233 for x in xrange(1, len(pathComponents)):
234 p = '/'.join(pathComponents[0:x])
235
236 try:
237 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
238 except:
239 createDir = True
240
241 if createDir:
242 try:
243 os.mkdir(p)
244 except OSError, e:
245 die("Couldn't create directory", p, e.strerror)
246
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)
266
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])
272
273def removeFile(clean, path):
274 updateCache = cacheOnly or clean
275 updateWd = not cacheOnly
276
277 if updateCache:
278 runProgram(['git-update-index', '--force-remove', '--', path])
279
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
286
287def 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
297
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
308
309# Cache entry management
310# ----------------------
311
312class CacheEntry:
313 def __init__(self, path):
314 class Stage:
315 def __init__(self):
316 self.sha1 = None
317 self.mode = None
318
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'
325
326 if self.sha1:
327 sha1 = self.sha1
328 else:
329 sha1 = 'None'
330 return 'sha1: ' + sha1 + ' mode: ' + m
331
332 self.stages = [Stage(), Stage(), Stage(), Stage()]
333 self.path = path
334 self.processed = False
335
336 def __str__(self):
337 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
338
339class CacheEntryContainer:
340 def __init__(self):
341 self.entries = {}
342
343 def add(self, entry):
344 self.entries[entry.path] = entry
345
346 def get(self, path):
347 return self.entries.get(path)
348
349 def __iter__(self):
350 return self.entries.itervalues()
351
352unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
353def 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.'''
357
358 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
359 lines.pop()
360
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)
369
370 e = res.get(path)
371 if not e:
372 e = CacheEntry(path)
373 res.add(e)
374
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
381
382lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
383def 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.'''
386
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]
398
399 res = CacheEntry(path)
400
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]))
404
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
411
412 return res
413
414# Rename detection and handling
415# -----------------------------
416
417class 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
431
432 self.processed = False
433
434class RenameEntryContainer:
435 def __init__(self):
436 self.entriesSrc = {}
437 self.entriesDst = {}
438
439 def add(self, entry):
440 self.entriesSrc[entry.srcName] = entry
441 self.entriesDst[entry.dstName] = entry
442
443 def getSrc(self, path):
444 return self.entriesSrc.get(path)
445
446 def getDst(self, path):
447 return self.entriesDst.get(path)
448
449 def __iter__(self):
450 return self.entriesSrc.itervalues()
451
452parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
453def 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.'''
458
459 assert(tree == aTree or tree == bTree)
460 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
461 '-z', oTree, tree])
462
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)
471
472 if not m:
473 die('Unexpected output from git-diff-tree:', rec)
474
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()
482
483 srcCacheEntry = cacheEntries.get(src)
484 if not srcCacheEntry:
485 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
486 cacheEntries.add(srcCacheEntry)
487
488 dstCacheEntry = cacheEntries.get(dst)
489 if not dstCacheEntry:
490 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
491 cacheEntries.add(dstCacheEntry)
492
493 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
494 dst, dstSha, dstMode, dstCacheEntry,
495 score))
496 except StopIteration:
497 pass
498 return ret
499
500def 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
511
512 if len(path) > 0:
513 return '/'.join(path) + \
514 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
515 '/'.join(dstPath[endIndex:]) + '}'
516 else:
517 return src + ' => ' + dst
518
519def 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)
525
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
538
539 ren1 = renames1.getSrc(path)
540 ren2 = renames2.getSrc(path)
541
542 ren1.dstCacheEntry.processed = True
543 ren1.srcCacheEntry.processed = True
544
545 if ren1.processed:
546 continue
547
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
555
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
561
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
569
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
577
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)
586
587 if merge or not clean:
588 print 'Renaming', fmtRename(path, ren1.dstName)
589
590 if merge:
591 print 'Auto-merging', ren1.dstName
592
593 if not clean:
594 print 'CONFLICT (content): merge conflict in', ren1.dstName
595 cleanMerge = False
596
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
607
608 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
609 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
610
611 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
612 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
613
614 tryMerge = False
615
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
657
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)
664
665 if merge or not clean:
666 print 'Renaming', fmtRename(ren1.srcName, ren1.dstName)
667
668 if merge:
669 print 'Auto-merging', ren1.dstName
670
671 if not clean:
672 print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName
673 cleanMerge = False
674
675 if not cacheOnly:
676 updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
677 updateCache=True, updateWd=False)
678 updateFile(clean, resSha, resMode, ren1.dstName)
679
680 return cleanMerge
681
682# Per entry merge function
683# ------------------------
684
685def processEntry(entry, branch1Name, branch2Name):
686 '''Merge one cache entry.'''
687
688 debug('processing', entry.path, 'clean cache:', cacheOnly)
689
690 cleanMerge = True
691
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
699
700 assert(oSha == None or isSha(oSha))
701 assert(aSha == None or isSha(aSha))
702 assert(bSha == None or isSha(bSha))
703
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)
707
708 if (oSha and (not aSha or not bSha)):
709 #
710 # Case A: Deleted in one
711 #
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
734
735 updateFile(False, sha, mode, path)
736
737 elif (not oSha and aSha and not bSha) or \
738 (not oSha and not aSha and bSha):
739 #
740 # Case B: Added in one.
741 #
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'
754
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
761
762 removeFile(False, path)
763 updateFile(False, sha, mode, newPath)
764 else:
765 print 'Adding', path
766 updateFile(True, sha, mode, path)
767
768 elif not oSha and aSha and bSha:
769 #
770 # Case C: Added in both (check for same permissions).
771 #
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
779
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)
794
795 elif oSha and aSha and bSha:
796 #
797 # case D: Modified in both, but differently.
798 #
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
810
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.")
819
820 return cleanMerge
821
822def usage():
823 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
824
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.
829
830if len(sys.argv) < 4:
831 usage()
832
833for 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
843
844print 'Merging', h1, 'with', h2
845
846try:
847 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
848 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
849
850 graph = buildGraph([h1, h2])
851
852 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
853 firstBranch, secondBranch, graph)
854
855 print ''
856except:
857 if isinstance(sys.exc_info()[1], SystemExit):
858 raise
859 else:
860 traceback.print_exc(None, sys.stderr)
861 sys.exit(2)
862
863if clean:
864 sys.exit(0)
865else:
866 sys.exit(1)