1#!/usr/bin/python
2#
3# Copyright (C) 2005 Fredrik Kuivinen
4#
5
6import sys
7sys.path.append('''@@GIT_PYTHON_PATH@@''')
8
9import math, random, os, re, signal, tempfile, stat, errno, traceback
10from heapq import heappush, heappop
11from sets import Set
12
13from gitMergeCommon import *
14
15outputIndent = 0
16def output(*args):
17 sys.stdout.write(' '*outputIndent)
18 printList(args)
19
20originalIndexFile = os.environ.get('GIT_INDEX_FILE',
21 os.environ.get('GIT_DIR', '.git') + '/index')
22temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
23 '/merge-recursive-tmp-index'
24def setupIndex(temporary):
25 try:
26 os.unlink(temporaryIndexFile)
27 except OSError:
28 pass
29 if temporary:
30 newIndex = temporaryIndexFile
31 else:
32 newIndex = originalIndexFile
33 os.environ['GIT_INDEX_FILE'] = newIndex
34
35# This is a global variable which is used in a number of places but
36# only written to in the 'merge' function.
37
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.
42
43cacheOnly = False
44
45# The entry point to the merge code
46# ---------------------------------
47
48def 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))
53
54 global outputIndent
55
56 output('Merging:')
57 output(h1)
58 output(h2)
59 sys.stdout.flush()
60
61 ca = getCommonAncestors(graph, h1, h2)
62 output('found', len(ca), 'common ancestor(s):')
63 for x in ca:
64 output(x)
65 sys.stdout.flush()
66
67 mergedCA = ca[0]
68 for h in ca[1:]:
69 outputIndent = callDepth+1
70 [mergedCA, dummy] = merge(mergedCA, h,
71 'Temporary merge branch 1',
72 'Temporary merge branch 2',
73 graph, callDepth+1)
74 outputIndent = callDepth
75 assert(isinstance(mergedCA, Commit))
76
77 global cacheOnly
78 if callDepth == 0:
79 setupIndex(False)
80 cacheOnly = False
81 else:
82 setupIndex(True)
83 runProgram(['git-read-tree', h1.tree()])
84 cacheOnly = True
85
86 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
87 branch1Name, branch2Name)
88
89 if clean or cacheOnly:
90 res = Commit(None, [h1, h2], tree=shaRes)
91 graph.addNode(res)
92 else:
93 res = None
94
95 return [res, clean]
96
97getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
98def getFilesAndDirs(tree):
99 files = Set()
100 dirs = Set()
101 out = runProgram(['git-ls-tree', '-r', '-z', tree])
102 for l in out.split('\0'):
103 m = getFilesRE.match(l)
104 if m:
105 if m.group(2) == 'tree':
106 dirs.add(m.group(4))
107 elif m.group(2) == 'blob':
108 files.add(m.group(4))
109
110 return [files, dirs]
111
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
115# merged.
116currentFileSet = None
117currentDirectorySet = None
118
119def 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
124 merge was clean.'''
125
126 assert(isSha(head) and isSha(merge) and isSha(common))
127
128 if common == merge:
129 output('Already uptodate!')
130 return [head, True]
131
132 if cacheOnly:
133 updateArg = '-i'
134 else:
135 updateArg = '-u'
136
137 [out, code] = runProgram(['git-read-tree', updateArg, '-m',
138 common, head, merge], returnCode = True)
139 if code != 0:
140 die('git-read-tree:', out)
141
142 [tree, code] = runProgram('git-write-tree', returnCode=True)
143 tree = tree.rstrip()
144 if code != 0:
145 global currentFileSet, currentDirectorySet
146 [currentFileSet, currentDirectorySet] = getFilesAndDirs(head)
147 [filesM, dirsM] = getFilesAndDirs(merge)
148 currentFileSet.union_update(filesM)
149 currentDirectorySet.union_update(dirsM)
150
151 entries = unmergedCacheEntries()
152 renamesHead = getRenames(head, common, head, merge, entries)
153 renamesMerge = getRenames(merge, common, head, merge, entries)
154
155 cleanMerge = processRenames(renamesHead, renamesMerge,
156 branch1Name, branch2Name)
157 for entry in entries:
158 if entry.processed:
159 continue
160 if not processEntry(entry, branch1Name, branch2Name):
161 cleanMerge = False
162
163 if cleanMerge or cacheOnly:
164 tree = runProgram('git-write-tree').rstrip()
165 else:
166 tree = None
167 else:
168 cleanMerge = True
169
170 return [tree, cleanMerge]
171
172# Low level file merging, update and removal
173# ------------------------------------------
174
175def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
176 branch1Name, branch2Name):
177
178 merge = False
179 clean = True
180
181 if stat.S_IFMT(aMode) != stat.S_IFMT(bMode):
182 clean = False
183 if stat.S_ISREG(aMode):
184 mode = aMode
185 sha = aSha
186 else:
187 mode = bMode
188 sha = bSha
189 else:
190 if aSha != oSha and bSha != oSha:
191 merge = True
192
193 if aMode == oMode:
194 mode = bMode
195 else:
196 mode = aMode
197
198 if aSha == oSha:
199 sha = bSha
200 elif bSha == oSha:
201 sha = aSha
202 elif stat.S_ISREG(aMode):
203 assert(stat.S_ISREG(bMode))
204
205 orig = runProgram(['git-unpack-file', oSha]).rstrip()
206 src1 = runProgram(['git-unpack-file', aSha]).rstrip()
207 src2 = runProgram(['git-unpack-file', bSha]).rstrip()
208 [out, code] = runProgram(['merge',
209 '-L', branch1Name + '/' + aPath,
210 '-L', 'orig/' + oPath,
211 '-L', branch2Name + '/' + bPath,
212 src1, orig, src2], returnCode=True)
213
214 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
215 src1]).rstrip()
216
217 os.unlink(orig)
218 os.unlink(src1)
219 os.unlink(src2)
220
221 clean = (code == 0)
222 else:
223 assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
224 sha = aSha
225
226 if aSha != bSha:
227 clean = False
228
229 return [sha, mode, clean, merge]
230
231def updateFile(clean, sha, mode, path):
232 updateCache = cacheOnly or clean
233 updateWd = not cacheOnly
234
235 return updateFileExt(sha, mode, path, updateCache, updateWd)
236
237def updateFileExt(sha, mode, path, updateCache, updateWd):
238 if cacheOnly:
239 updateWd = False
240
241 if updateWd:
242 pathComponents = path.split('/')
243 for x in xrange(1, len(pathComponents)):
244 p = '/'.join(pathComponents[0:x])
245
246 try:
247 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
248 except:
249 createDir = True
250
251 if createDir:
252 try:
253 os.mkdir(p)
254 except OSError, e:
255 die("Couldn't create directory", p, e.strerror)
256
257 prog = ['git-cat-file', 'blob', sha]
258 if stat.S_ISREG(mode):
259 try:
260 os.unlink(path)
261 except OSError:
262 pass
263 if mode & 0100:
264 mode = 0777
265 else:
266 mode = 0666
267 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
268 proc = subprocess.Popen(prog, stdout=fd)
269 proc.wait()
270 os.close(fd)
271 elif stat.S_ISLNK(mode):
272 linkTarget = runProgram(prog)
273 os.symlink(linkTarget, path)
274 else:
275 assert(False)
276
277 if updateWd and updateCache:
278 runProgram(['git-update-index', '--add', '--', path])
279 elif updateCache:
280 runProgram(['git-update-index', '--add', '--cacheinfo',
281 '0%o' % mode, sha, path])
282
283def removeFile(clean, path):
284 updateCache = cacheOnly or clean
285 updateWd = not cacheOnly
286
287 if updateCache:
288 runProgram(['git-update-index', '--force-remove', '--', path])
289
290 if updateWd:
291 try:
292 os.unlink(path)
293 except OSError, e:
294 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
295 raise
296
297def uniquePath(path, branch):
298 def fileExists(path):
299 try:
300 os.lstat(path)
301 return True
302 except OSError, e:
303 if e.errno == errno.ENOENT:
304 return False
305 else:
306 raise
307
308 branch = branch.replace('/', '_')
309 newPath = path + '~' + branch
310 suffix = 0
311 while newPath in currentFileSet or \
312 newPath in currentDirectorySet or \
313 fileExists(newPath):
314 suffix += 1
315 newPath = path + '~' + branch + '_' + str(suffix)
316 currentFileSet.add(newPath)
317 return newPath
318
319# Cache entry management
320# ----------------------
321
322class CacheEntry:
323 def __init__(self, path):
324 class Stage:
325 def __init__(self):
326 self.sha1 = None
327 self.mode = None
328
329 # Used for debugging only
330 def __str__(self):
331 if self.mode != None:
332 m = '0%o' % self.mode
333 else:
334 m = 'None'
335
336 if self.sha1:
337 sha1 = self.sha1
338 else:
339 sha1 = 'None'
340 return 'sha1: ' + sha1 + ' mode: ' + m
341
342 self.stages = [Stage(), Stage(), Stage(), Stage()]
343 self.path = path
344 self.processed = False
345
346 def __str__(self):
347 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
348
349class CacheEntryContainer:
350 def __init__(self):
351 self.entries = {}
352
353 def add(self, entry):
354 self.entries[entry.path] = entry
355
356 def get(self, path):
357 return self.entries.get(path)
358
359 def __iter__(self):
360 return self.entries.itervalues()
361
362unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
363def unmergedCacheEntries():
364 '''Create a dictionary mapping file names to CacheEntry
365 objects. The dictionary contains one entry for every path with a
366 non-zero stage entry.'''
367
368 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
369 lines.pop()
370
371 res = CacheEntryContainer()
372 for l in lines:
373 m = unmergedRE.match(l)
374 if m:
375 mode = int(m.group(1), 8)
376 sha1 = m.group(2)
377 stage = int(m.group(3))
378 path = m.group(4)
379
380 e = res.get(path)
381 if not e:
382 e = CacheEntry(path)
383 res.add(e)
384
385 e.stages[stage].mode = mode
386 e.stages[stage].sha1 = sha1
387 else:
388 die('Error: Merge program failed: Unexpected output from',
389 'git-ls-files:', l)
390 return res
391
392lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
393def getCacheEntry(path, origTree, aTree, bTree):
394 '''Returns a CacheEntry object which doesn't have to correspond to
395 a real cache entry in Git's index.'''
396
397 def parse(out):
398 if out == '':
399 return [None, None]
400 else:
401 m = lsTreeRE.match(out)
402 if not m:
403 die('Unexpected output from git-ls-tree:', out)
404 elif m.group(2) == 'blob':
405 return [m.group(3), int(m.group(1), 8)]
406 else:
407 return [None, None]
408
409 res = CacheEntry(path)
410
411 [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
412 [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
413 [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
414
415 res.stages[1].sha1 = oSha
416 res.stages[1].mode = oMode
417 res.stages[2].sha1 = aSha
418 res.stages[2].mode = aMode
419 res.stages[3].sha1 = bSha
420 res.stages[3].mode = bMode
421
422 return res
423
424# Rename detection and handling
425# -----------------------------
426
427class RenameEntry:
428 def __init__(self,
429 src, srcSha, srcMode, srcCacheEntry,
430 dst, dstSha, dstMode, dstCacheEntry,
431 score):
432 self.srcName = src
433 self.srcSha = srcSha
434 self.srcMode = srcMode
435 self.srcCacheEntry = srcCacheEntry
436 self.dstName = dst
437 self.dstSha = dstSha
438 self.dstMode = dstMode
439 self.dstCacheEntry = dstCacheEntry
440 self.score = score
441
442 self.processed = False
443
444class RenameEntryContainer:
445 def __init__(self):
446 self.entriesSrc = {}
447 self.entriesDst = {}
448
449 def add(self, entry):
450 self.entriesSrc[entry.srcName] = entry
451 self.entriesDst[entry.dstName] = entry
452
453 def getSrc(self, path):
454 return self.entriesSrc.get(path)
455
456 def getDst(self, path):
457 return self.entriesDst.get(path)
458
459 def __iter__(self):
460 return self.entriesSrc.itervalues()
461
462parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
463def getRenames(tree, oTree, aTree, bTree, cacheEntries):
464 '''Get information of all renames which occured between 'oTree' and
465 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
466 'bTree') to be able to associate the correct cache entries with
467 the rename information. 'tree' is always equal to either aTree or bTree.'''
468
469 assert(tree == aTree or tree == bTree)
470 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
471 '-z', oTree, tree])
472
473 ret = RenameEntryContainer()
474 try:
475 recs = inp.split("\0")
476 recs.pop() # remove last entry (which is '')
477 it = recs.__iter__()
478 while True:
479 rec = it.next()
480 m = parseDiffRenamesRE.match(rec)
481
482 if not m:
483 die('Unexpected output from git-diff-tree:', rec)
484
485 srcMode = int(m.group(1), 8)
486 dstMode = int(m.group(2), 8)
487 srcSha = m.group(3)
488 dstSha = m.group(4)
489 score = m.group(5)
490 src = it.next()
491 dst = it.next()
492
493 srcCacheEntry = cacheEntries.get(src)
494 if not srcCacheEntry:
495 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
496 cacheEntries.add(srcCacheEntry)
497
498 dstCacheEntry = cacheEntries.get(dst)
499 if not dstCacheEntry:
500 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
501 cacheEntries.add(dstCacheEntry)
502
503 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
504 dst, dstSha, dstMode, dstCacheEntry,
505 score))
506 except StopIteration:
507 pass
508 return ret
509
510def fmtRename(src, dst):
511 srcPath = src.split('/')
512 dstPath = dst.split('/')
513 path = []
514 endIndex = min(len(srcPath), len(dstPath)) - 1
515 for x in range(0, endIndex):
516 if srcPath[x] == dstPath[x]:
517 path.append(srcPath[x])
518 else:
519 endIndex = x
520 break
521
522 if len(path) > 0:
523 return '/'.join(path) + \
524 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
525 '/'.join(dstPath[endIndex:]) + '}'
526 else:
527 return src + ' => ' + dst
528
529def processRenames(renamesA, renamesB, branchNameA, branchNameB):
530 srcNames = Set()
531 for x in renamesA:
532 srcNames.add(x.srcName)
533 for x in renamesB:
534 srcNames.add(x.srcName)
535
536 cleanMerge = True
537 for path in srcNames:
538 if renamesA.getSrc(path):
539 renames1 = renamesA
540 renames2 = renamesB
541 branchName1 = branchNameA
542 branchName2 = branchNameB
543 else:
544 renames1 = renamesB
545 renames2 = renamesA
546 branchName1 = branchNameB
547 branchName2 = branchNameA
548
549 ren1 = renames1.getSrc(path)
550 ren2 = renames2.getSrc(path)
551
552 ren1.dstCacheEntry.processed = True
553 ren1.srcCacheEntry.processed = True
554
555 if ren1.processed:
556 continue
557
558 ren1.processed = True
559 removeFile(True, ren1.srcName)
560 if ren2:
561 # Renamed in 1 and renamed in 2
562 assert(ren1.srcName == ren2.srcName)
563 ren2.dstCacheEntry.processed = True
564 ren2.processed = True
565
566 if ren1.dstName != ren2.dstName:
567 output('CONFLICT (rename/rename): Rename',
568 fmtRename(path, ren1.dstName), 'in branch', branchName1,
569 'rename', fmtRename(path, ren2.dstName), 'in',
570 branchName2)
571 cleanMerge = False
572
573 if ren1.dstName in currentDirectorySet:
574 dstName1 = uniquePath(ren1.dstName, branchName1)
575 output(ren1.dstName, 'is a directory in', branchName2,
576 'adding as', dstName1, 'instead.')
577 removeFile(False, ren1.dstName)
578 else:
579 dstName1 = ren1.dstName
580
581 if ren2.dstName in currentDirectorySet:
582 dstName2 = uniquePath(ren2.dstName, branchName2)
583 output(ren2.dstName, 'is a directory in', branchName1,
584 'adding as', dstName2, 'instead.')
585 removeFile(False, ren2.dstName)
586 else:
587 dstName2 = ren1.dstName
588
589 updateFile(False, ren1.dstSha, ren1.dstMode, dstName1)
590 updateFile(False, ren2.dstSha, ren2.dstMode, dstName2)
591 else:
592 [resSha, resMode, clean, merge] = \
593 mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
594 ren1.dstName, ren1.dstSha, ren1.dstMode,
595 ren2.dstName, ren2.dstSha, ren2.dstMode,
596 branchName1, branchName2)
597
598 if merge or not clean:
599 output('Renaming', fmtRename(path, ren1.dstName))
600
601 if merge:
602 output('Auto-merging', ren1.dstName)
603
604 if not clean:
605 output('CONFLICT (content): merge conflict in',
606 ren1.dstName)
607 cleanMerge = False
608
609 if not cacheOnly:
610 updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
611 updateCache=True, updateWd=False)
612 updateFile(clean, resSha, resMode, ren1.dstName)
613 else:
614 # Renamed in 1, maybe changed in 2
615 if renamesA == renames1:
616 stage = 3
617 else:
618 stage = 2
619
620 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
621 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
622
623 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
624 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
625
626 tryMerge = False
627
628 if ren1.dstName in currentDirectorySet:
629 newPath = uniquePath(ren1.dstName, branchName1)
630 output('CONFLICT (rename/directory): Rename',
631 fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,
632 'directory', ren1.dstName, 'added in', branchName2)
633 output('Renaming', ren1.srcName, 'to', newPath, 'instead')
634 cleanMerge = False
635 removeFile(False, ren1.dstName)
636 updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
637 elif srcShaOtherBranch == None:
638 output('CONFLICT (rename/delete): Rename',
639 fmtRename(ren1.srcName, ren1.dstName), 'in',
640 branchName1, 'and deleted in', branchName2)
641 cleanMerge = False
642 updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
643 elif dstShaOtherBranch:
644 newPath = uniquePath(ren1.dstName, branchName2)
645 output('CONFLICT (rename/add): Rename',
646 fmtRename(ren1.srcName, ren1.dstName), 'in',
647 branchName1 + '.', ren1.dstName, 'added in', branchName2)
648 output('Adding as', newPath, 'instead')
649 updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
650 cleanMerge = False
651 tryMerge = True
652 elif renames2.getDst(ren1.dstName):
653 dst2 = renames2.getDst(ren1.dstName)
654 newPath1 = uniquePath(ren1.dstName, branchName1)
655 newPath2 = uniquePath(dst2.dstName, branchName2)
656 output('CONFLICT (rename/rename): Rename',
657 fmtRename(ren1.srcName, ren1.dstName), 'in',
658 branchName1+'. Rename',
659 fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2)
660 output('Renaming', ren1.srcName, 'to', newPath1, 'and',
661 dst2.srcName, 'to', newPath2, 'instead')
662 removeFile(False, ren1.dstName)
663 updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
664 updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
665 dst2.processed = True
666 cleanMerge = False
667 else:
668 tryMerge = True
669
670 if tryMerge:
671 [resSha, resMode, clean, merge] = \
672 mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
673 ren1.dstName, ren1.dstSha, ren1.dstMode,
674 ren1.srcName, srcShaOtherBranch, srcModeOtherBranch,
675 branchName1, branchName2)
676
677 if merge or not clean:
678 output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
679
680 if merge:
681 output('Auto-merging', ren1.dstName)
682
683 if not clean:
684 output('CONFLICT (rename/modify): Merge conflict in',
685 ren1.dstName)
686 cleanMerge = False
687
688 if not cacheOnly:
689 updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
690 updateCache=True, updateWd=False)
691 updateFile(clean, resSha, resMode, ren1.dstName)
692
693 return cleanMerge
694
695# Per entry merge function
696# ------------------------
697
698def processEntry(entry, branch1Name, branch2Name):
699 '''Merge one cache entry.'''
700
701 debug('processing', entry.path, 'clean cache:', cacheOnly)
702
703 cleanMerge = True
704
705 path = entry.path
706 oSha = entry.stages[1].sha1
707 oMode = entry.stages[1].mode
708 aSha = entry.stages[2].sha1
709 aMode = entry.stages[2].mode
710 bSha = entry.stages[3].sha1
711 bMode = entry.stages[3].mode
712
713 assert(oSha == None or isSha(oSha))
714 assert(aSha == None or isSha(aSha))
715 assert(bSha == None or isSha(bSha))
716
717 assert(oMode == None or type(oMode) is int)
718 assert(aMode == None or type(aMode) is int)
719 assert(bMode == None or type(bMode) is int)
720
721 if (oSha and (not aSha or not bSha)):
722 #
723 # Case A: Deleted in one
724 #
725 if (not aSha and not bSha) or \
726 (aSha == oSha and not bSha) or \
727 (not aSha and bSha == oSha):
728 # Deleted in both or deleted in one and unchanged in the other
729 if aSha:
730 output('Removing', path)
731 removeFile(True, path)
732 else:
733 # Deleted in one and changed in the other
734 cleanMerge = False
735 if not aSha:
736 output('CONFLICT (delete/modify):', path, 'deleted in',
737 branch1Name, 'and modified in', branch2Name + '.',
738 'Version', branch2Name, 'of', path, 'left in tree.')
739 mode = bMode
740 sha = bSha
741 else:
742 output('CONFLICT (modify/delete):', path, 'deleted in',
743 branch2Name, 'and modified in', branch1Name + '.',
744 'Version', branch1Name, 'of', path, 'left in tree.')
745 mode = aMode
746 sha = aSha
747
748 updateFile(False, sha, mode, path)
749
750 elif (not oSha and aSha and not bSha) or \
751 (not oSha and not aSha and bSha):
752 #
753 # Case B: Added in one.
754 #
755 if aSha:
756 addBranch = branch1Name
757 otherBranch = branch2Name
758 mode = aMode
759 sha = aSha
760 conf = 'file/directory'
761 else:
762 addBranch = branch2Name
763 otherBranch = branch1Name
764 mode = bMode
765 sha = bSha
766 conf = 'directory/file'
767
768 if path in currentDirectorySet:
769 cleanMerge = False
770 newPath = uniquePath(path, addBranch)
771 output('CONFLICT (' + conf + '):',
772 'There is a directory with name', path, 'in',
773 otherBranch + '. Adding', path, 'as', newPath)
774
775 removeFile(False, path)
776 updateFile(False, sha, mode, newPath)
777 else:
778 output('Adding', path)
779 updateFile(True, sha, mode, path)
780
781 elif not oSha and aSha and bSha:
782 #
783 # Case C: Added in both (check for same permissions).
784 #
785 if aSha == bSha:
786 if aMode != bMode:
787 cleanMerge = False
788 output('CONFLICT: File', path,
789 'added identically in both branches, but permissions',
790 'conflict', '0%o' % aMode, '->', '0%o' % bMode)
791 output('CONFLICT: adding with permission:', '0%o' % aMode)
792
793 updateFile(False, aSha, aMode, path)
794 else:
795 # This case is handled by git-read-tree
796 assert(False)
797 else:
798 cleanMerge = False
799 newPath1 = uniquePath(path, branch1Name)
800 newPath2 = uniquePath(path, branch2Name)
801 output('CONFLICT (add/add): File', path,
802 'added non-identically in both branches. Adding as',
803 newPath1, 'and', newPath2, 'instead.')
804 removeFile(False, path)
805 updateFile(False, aSha, aMode, newPath1)
806 updateFile(False, bSha, bMode, newPath2)
807
808 elif oSha and aSha and bSha:
809 #
810 # case D: Modified in both, but differently.
811 #
812 output('Auto-merging', path)
813 [sha, mode, clean, dummy] = \
814 mergeFile(path, oSha, oMode,
815 path, aSha, aMode,
816 path, bSha, bMode,
817 branch1Name, branch2Name)
818 if clean:
819 updateFile(True, sha, mode, path)
820 else:
821 cleanMerge = False
822 output('CONFLICT (content): Merge conflict in', path)
823
824 if cacheOnly:
825 updateFile(False, sha, mode, path)
826 else:
827 updateFileExt(aSha, aMode, path,
828 updateCache=True, updateWd=False)
829 updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
830 else:
831 die("ERROR: Fatal merge failure, shouldn't happen.")
832
833 return cleanMerge
834
835def usage():
836 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
837
838# main entry point as merge strategy module
839# The first parameters up to -- are merge bases, and the rest are heads.
840# This strategy module figures out merge bases itself, so we only
841# get heads.
842
843if len(sys.argv) < 4:
844 usage()
845
846for nextArg in xrange(1, len(sys.argv)):
847 if sys.argv[nextArg] == '--':
848 if len(sys.argv) != nextArg + 3:
849 die('Not handling anything other than two heads merge.')
850 try:
851 h1 = firstBranch = sys.argv[nextArg + 1]
852 h2 = secondBranch = sys.argv[nextArg + 2]
853 except IndexError:
854 usage()
855 break
856
857print 'Merging', h1, 'with', h2
858
859try:
860 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
861 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
862
863 graph = buildGraph([h1, h2])
864
865 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
866 firstBranch, secondBranch, graph)
867
868 print ''
869except:
870 if isinstance(sys.exc_info()[1], SystemExit):
871 raise
872 else:
873 traceback.print_exc(None, sys.stderr)
874 sys.exit(2)
875
876if clean:
877 sys.exit(0)
878else:
879 sys.exit(1)