GIT 0.99.9j aka 1.0rc3
[git/jrn.git] / git-merge-recursive.py
blobd7d36aa7d11299d3a1e91dce1bc9572ac56a7c08
1 #!/usr/bin/python
3 # Copyright (C) 2005 Fredrik Kuivinen
6 import sys
7 sys.path.append('''@@GIT_PYTHON_PATH@@''')
9 import math, random, os, re, signal, tempfile, stat, errno, traceback
10 from heapq import heappush, heappop
11 from sets import Set
13 from gitMergeCommon import *
15 outputIndent = 0
16 def output(*args):
17 sys.stdout.write(' '*outputIndent)
18 printList(args)
20 originalIndexFile = os.environ.get('GIT_INDEX_FILE',
21 os.environ.get('GIT_DIR', '.git') + '/index')
22 temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
23 '/merge-recursive-tmp-index'
24 def 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
35 # This is a global variable which is used in a number of places but
36 # only written to in the 'merge' function.
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.
43 cacheOnly = False
45 # The entry point to the merge code
46 # ---------------------------------
48 def 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))
54 global outputIndent
56 output('Merging:')
57 output(h1)
58 output(h2)
59 sys.stdout.flush()
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()
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))
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
86 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
87 branch1Name, branch2Name)
89 if clean or cacheOnly:
90 res = Commit(None, [h1, h2], tree=shaRes)
91 graph.addNode(res)
92 else:
93 res = None
95 return [res, clean]
97 getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
98 def 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))
110 return [files, dirs]
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.
116 currentFileSet = None
117 currentDirectorySet = None
119 def 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.'''
126 assert(isSha(head) and isSha(merge) and isSha(common))
128 if common == merge:
129 output('Already uptodate!')
130 return [head, True]
132 if cacheOnly:
133 updateArg = '-i'
134 else:
135 updateArg = '-u'
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)
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)
151 entries = unmergedCacheEntries()
152 renamesHead = getRenames(head, common, head, merge, entries)
153 renamesMerge = getRenames(merge, common, head, merge, entries)
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
163 if cleanMerge or cacheOnly:
164 tree = runProgram('git-write-tree').rstrip()
165 else:
166 tree = None
167 else:
168 cleanMerge = True
170 return [tree, cleanMerge]
172 # Low level file merging, update and removal
173 # ------------------------------------------
175 def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
176 branch1Name, branch2Name):
178 merge = False
179 clean = True
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
193 if aMode == oMode:
194 mode = bMode
195 else:
196 mode = aMode
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))
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)
214 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
215 src1]).rstrip()
217 os.unlink(orig)
218 os.unlink(src1)
219 os.unlink(src2)
221 clean = (code == 0)
222 else:
223 assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
224 sha = aSha
226 if aSha != bSha:
227 clean = False
229 return [sha, mode, clean, merge]
231 def updateFile(clean, sha, mode, path):
232 updateCache = cacheOnly or clean
233 updateWd = not cacheOnly
235 return updateFileExt(sha, mode, path, updateCache, updateWd)
237 def updateFileExt(sha, mode, path, updateCache, updateWd):
238 if cacheOnly:
239 updateWd = False
241 if updateWd:
242 pathComponents = path.split('/')
243 for x in xrange(1, len(pathComponents)):
244 p = '/'.join(pathComponents[0:x])
246 try:
247 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
248 except:
249 createDir = True
251 if createDir:
252 try:
253 os.mkdir(p)
254 except OSError, e:
255 die("Couldn't create directory", p, e.strerror)
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)
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])
283 def removeFile(clean, path):
284 updateCache = cacheOnly or clean
285 updateWd = not cacheOnly
287 if updateCache:
288 runProgram(['git-update-index', '--force-remove', '--', path])
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
297 def 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
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
319 # Cache entry management
320 # ----------------------
322 class CacheEntry:
323 def __init__(self, path):
324 class Stage:
325 def __init__(self):
326 self.sha1 = None
327 self.mode = None
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'
336 if self.sha1:
337 sha1 = self.sha1
338 else:
339 sha1 = 'None'
340 return 'sha1: ' + sha1 + ' mode: ' + m
342 self.stages = [Stage(), Stage(), Stage(), Stage()]
343 self.path = path
344 self.processed = False
346 def __str__(self):
347 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
349 class CacheEntryContainer:
350 def __init__(self):
351 self.entries = {}
353 def add(self, entry):
354 self.entries[entry.path] = entry
356 def get(self, path):
357 return self.entries.get(path)
359 def __iter__(self):
360 return self.entries.itervalues()
362 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
363 def 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.'''
368 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
369 lines.pop()
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)
380 e = res.get(path)
381 if not e:
382 e = CacheEntry(path)
383 res.add(e)
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
392 lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
393 def 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.'''
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]
409 res = CacheEntry(path)
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]))
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
422 return res
424 # Rename detection and handling
425 # -----------------------------
427 class 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
442 self.processed = False
444 class RenameEntryContainer:
445 def __init__(self):
446 self.entriesSrc = {}
447 self.entriesDst = {}
449 def add(self, entry):
450 self.entriesSrc[entry.srcName] = entry
451 self.entriesDst[entry.dstName] = entry
453 def getSrc(self, path):
454 return self.entriesSrc.get(path)
456 def getDst(self, path):
457 return self.entriesDst.get(path)
459 def __iter__(self):
460 return self.entriesSrc.itervalues()
462 parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
463 def 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.'''
469 assert(tree == aTree or tree == bTree)
470 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
471 '-z', oTree, tree])
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)
482 if not m:
483 die('Unexpected output from git-diff-tree:', rec)
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()
493 srcCacheEntry = cacheEntries.get(src)
494 if not srcCacheEntry:
495 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
496 cacheEntries.add(srcCacheEntry)
498 dstCacheEntry = cacheEntries.get(dst)
499 if not dstCacheEntry:
500 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
501 cacheEntries.add(dstCacheEntry)
503 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
504 dst, dstSha, dstMode, dstCacheEntry,
505 score))
506 except StopIteration:
507 pass
508 return ret
510 def 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
522 if len(path) > 0:
523 return '/'.join(path) + \
524 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
525 '/'.join(dstPath[endIndex:]) + '}'
526 else:
527 return src + ' => ' + dst
529 def 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)
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
549 ren1 = renames1.getSrc(path)
550 ren2 = renames2.getSrc(path)
552 ren1.dstCacheEntry.processed = True
553 ren1.srcCacheEntry.processed = True
555 if ren1.processed:
556 continue
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
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
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
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
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)
598 if merge or not clean:
599 output('Renaming', fmtRename(path, ren1.dstName))
601 if merge:
602 output('Auto-merging', ren1.dstName)
604 if not clean:
605 output('CONFLICT (content): merge conflict in',
606 ren1.dstName)
607 cleanMerge = False
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
620 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
621 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
623 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
624 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
626 tryMerge = False
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
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)
677 if merge or not clean:
678 output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
680 if merge:
681 output('Auto-merging', ren1.dstName)
683 if not clean:
684 output('CONFLICT (rename/modify): Merge conflict in',
685 ren1.dstName)
686 cleanMerge = False
688 if not cacheOnly:
689 updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
690 updateCache=True, updateWd=False)
691 updateFile(clean, resSha, resMode, ren1.dstName)
693 return cleanMerge
695 # Per entry merge function
696 # ------------------------
698 def processEntry(entry, branch1Name, branch2Name):
699 '''Merge one cache entry.'''
701 debug('processing', entry.path, 'clean cache:', cacheOnly)
703 cleanMerge = True
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
713 assert(oSha == None or isSha(oSha))
714 assert(aSha == None or isSha(aSha))
715 assert(bSha == None or isSha(bSha))
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)
721 if (oSha and (not aSha or not bSha)):
723 # Case A: Deleted in one
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
748 updateFile(False, sha, mode, path)
750 elif (not oSha and aSha and not bSha) or \
751 (not oSha and not aSha and bSha):
753 # Case B: Added in one.
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'
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)
775 removeFile(False, path)
776 updateFile(False, sha, mode, newPath)
777 else:
778 output('Adding', path)
779 updateFile(True, sha, mode, path)
781 elif not oSha and aSha and bSha:
783 # Case C: Added in both (check for same permissions).
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)
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)
808 elif oSha and aSha and bSha:
810 # case D: Modified in both, but differently.
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)
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.")
833 return cleanMerge
835 def usage():
836 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
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.
843 if len(sys.argv) < 4:
844 usage()
846 for 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
857 print 'Merging', h1, 'with', h2
859 try:
860 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
861 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
863 graph = buildGraph([h1, h2])
865 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
866 firstBranch, secondBranch, graph)
868 print ''
869 except:
870 if isinstance(sys.exc_info()[1], SystemExit):
871 raise
872 else:
873 traceback.print_exc(None, sys.stderr)
874 sys.exit(2)
876 if clean:
877 sys.exit(0)
878 else:
879 sys.exit(1)