Merge branch 'jc/blame'
[debian-git.git] / git-merge-recursive.py
blobce8a31fda050f36b24b8dffa5ee29e7dde074963
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, ancestor=None):
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))
53 global outputIndent
55 output('Merging:')
56 output(h1)
57 output(h2)
58 sys.stdout.flush()
60 if ancestor:
61 ca = [ancestor]
62 else:
63 assert(isinstance(graph, Graph))
64 ca = getCommonAncestors(graph, h1, h2)
65 output('found', len(ca), 'common ancestor(s):')
66 for x in ca:
67 output(x)
68 sys.stdout.flush()
70 mergedCA = ca[0]
71 for h in ca[1:]:
72 outputIndent = callDepth+1
73 [mergedCA, dummy] = merge(mergedCA, h,
74 'Temporary merge branch 1',
75 'Temporary merge branch 2',
76 graph, callDepth+1)
77 outputIndent = callDepth
78 assert(isinstance(mergedCA, Commit))
80 global cacheOnly
81 if callDepth == 0:
82 setupIndex(False)
83 cacheOnly = False
84 else:
85 setupIndex(True)
86 runProgram(['git-read-tree', h1.tree()])
87 cacheOnly = True
89 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
90 branch1Name, branch2Name)
92 if graph and (clean or cacheOnly):
93 res = Commit(None, [h1, h2], tree=shaRes)
94 graph.addNode(res)
95 else:
96 res = None
98 return [res, clean]
100 getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
101 def getFilesAndDirs(tree):
102 files = Set()
103 dirs = Set()
104 out = runProgram(['git-ls-tree', '-r', '-z', '-t', tree])
105 for l in out.split('\0'):
106 m = getFilesRE.match(l)
107 if m:
108 if m.group(2) == 'tree':
109 dirs.add(m.group(4))
110 elif m.group(2) == 'blob':
111 files.add(m.group(4))
113 return [files, dirs]
115 # Those two global variables are used in a number of places but only
116 # written to in 'mergeTrees' and 'uniquePath'. They keep track of
117 # every file and directory in the two branches that are about to be
118 # merged.
119 currentFileSet = None
120 currentDirectorySet = None
122 def mergeTrees(head, merge, common, branch1Name, branch2Name):
123 '''Merge the trees 'head' and 'merge' with the common ancestor
124 'common'. The name of the head branch is 'branch1Name' and the name of
125 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
126 where tree is the resulting tree and cleanMerge is True iff the
127 merge was clean.'''
129 assert(isSha(head) and isSha(merge) and isSha(common))
131 if common == merge:
132 output('Already uptodate!')
133 return [head, True]
135 if cacheOnly:
136 updateArg = '-i'
137 else:
138 updateArg = '-u'
140 [out, code] = runProgram(['git-read-tree', updateArg, '-m',
141 common, head, merge], returnCode = True)
142 if code != 0:
143 die('git-read-tree:', out)
145 [tree, code] = runProgram('git-write-tree', returnCode=True)
146 tree = tree.rstrip()
147 if code != 0:
148 global currentFileSet, currentDirectorySet
149 [currentFileSet, currentDirectorySet] = getFilesAndDirs(head)
150 [filesM, dirsM] = getFilesAndDirs(merge)
151 currentFileSet.union_update(filesM)
152 currentDirectorySet.union_update(dirsM)
154 entries = unmergedCacheEntries()
155 renamesHead = getRenames(head, common, head, merge, entries)
156 renamesMerge = getRenames(merge, common, head, merge, entries)
158 cleanMerge = processRenames(renamesHead, renamesMerge,
159 branch1Name, branch2Name)
160 for entry in entries:
161 if entry.processed:
162 continue
163 if not processEntry(entry, branch1Name, branch2Name):
164 cleanMerge = False
166 if cleanMerge or cacheOnly:
167 tree = runProgram('git-write-tree').rstrip()
168 else:
169 tree = None
170 else:
171 cleanMerge = True
173 return [tree, cleanMerge]
175 # Low level file merging, update and removal
176 # ------------------------------------------
178 def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
179 branch1Name, branch2Name):
181 merge = False
182 clean = True
184 if stat.S_IFMT(aMode) != stat.S_IFMT(bMode):
185 clean = False
186 if stat.S_ISREG(aMode):
187 mode = aMode
188 sha = aSha
189 else:
190 mode = bMode
191 sha = bSha
192 else:
193 if aSha != oSha and bSha != oSha:
194 merge = True
196 if aMode == oMode:
197 mode = bMode
198 else:
199 mode = aMode
201 if aSha == oSha:
202 sha = bSha
203 elif bSha == oSha:
204 sha = aSha
205 elif stat.S_ISREG(aMode):
206 assert(stat.S_ISREG(bMode))
208 orig = runProgram(['git-unpack-file', oSha]).rstrip()
209 src1 = runProgram(['git-unpack-file', aSha]).rstrip()
210 src2 = runProgram(['git-unpack-file', bSha]).rstrip()
211 try:
212 [out, code] = runProgram(['merge',
213 '-L', branch1Name + '/' + aPath,
214 '-L', 'orig/' + oPath,
215 '-L', branch2Name + '/' + bPath,
216 src1, orig, src2], returnCode=True)
217 except ProgramError, e:
218 print >>sys.stderr, e
219 die("Failed to execute 'merge'. merge(1) is used as the "
220 "file-level merge tool. Is 'merge' in your path?")
222 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
223 src1]).rstrip()
225 os.unlink(orig)
226 os.unlink(src1)
227 os.unlink(src2)
229 clean = (code == 0)
230 else:
231 assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
232 sha = aSha
234 if aSha != bSha:
235 clean = False
237 return [sha, mode, clean, merge]
239 def updateFile(clean, sha, mode, path):
240 updateCache = cacheOnly or clean
241 updateWd = not cacheOnly
243 return updateFileExt(sha, mode, path, updateCache, updateWd)
245 def updateFileExt(sha, mode, path, updateCache, updateWd):
246 if cacheOnly:
247 updateWd = False
249 if updateWd:
250 pathComponents = path.split('/')
251 for x in xrange(1, len(pathComponents)):
252 p = '/'.join(pathComponents[0:x])
254 try:
255 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
256 except OSError:
257 createDir = True
259 if createDir:
260 try:
261 os.mkdir(p)
262 except OSError, e:
263 die("Couldn't create directory", p, e.strerror)
265 prog = ['git-cat-file', 'blob', sha]
266 if stat.S_ISREG(mode):
267 try:
268 os.unlink(path)
269 except OSError:
270 pass
271 if mode & 0100:
272 mode = 0777
273 else:
274 mode = 0666
275 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
276 proc = subprocess.Popen(prog, stdout=fd)
277 proc.wait()
278 os.close(fd)
279 elif stat.S_ISLNK(mode):
280 linkTarget = runProgram(prog)
281 os.symlink(linkTarget, path)
282 else:
283 assert(False)
285 if updateWd and updateCache:
286 runProgram(['git-update-index', '--add', '--', path])
287 elif updateCache:
288 runProgram(['git-update-index', '--add', '--cacheinfo',
289 '0%o' % mode, sha, path])
291 def setIndexStages(path,
292 oSHA1, oMode,
293 aSHA1, aMode,
294 bSHA1, bMode,
295 clear=True):
296 istring = []
297 if clear:
298 istring.append("0 " + ("0" * 40) + "\t" + path + "\0")
299 if oMode:
300 istring.append("%o %s %d\t%s\0" % (oMode, oSHA1, 1, path))
301 if aMode:
302 istring.append("%o %s %d\t%s\0" % (aMode, aSHA1, 2, path))
303 if bMode:
304 istring.append("%o %s %d\t%s\0" % (bMode, bSHA1, 3, path))
306 runProgram(['git-update-index', '-z', '--index-info'],
307 input="".join(istring))
309 def removeFile(clean, path):
310 updateCache = cacheOnly or clean
311 updateWd = not cacheOnly
313 if updateCache:
314 runProgram(['git-update-index', '--force-remove', '--', path])
316 if updateWd:
317 try:
318 os.unlink(path)
319 except OSError, e:
320 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
321 raise
322 try:
323 os.removedirs(os.path.dirname(path))
324 except OSError:
325 pass
327 def uniquePath(path, branch):
328 def fileExists(path):
329 try:
330 os.lstat(path)
331 return True
332 except OSError, e:
333 if e.errno == errno.ENOENT:
334 return False
335 else:
336 raise
338 branch = branch.replace('/', '_')
339 newPath = path + '~' + branch
340 suffix = 0
341 while newPath in currentFileSet or \
342 newPath in currentDirectorySet or \
343 fileExists(newPath):
344 suffix += 1
345 newPath = path + '~' + branch + '_' + str(suffix)
346 currentFileSet.add(newPath)
347 return newPath
349 # Cache entry management
350 # ----------------------
352 class CacheEntry:
353 def __init__(self, path):
354 class Stage:
355 def __init__(self):
356 self.sha1 = None
357 self.mode = None
359 # Used for debugging only
360 def __str__(self):
361 if self.mode != None:
362 m = '0%o' % self.mode
363 else:
364 m = 'None'
366 if self.sha1:
367 sha1 = self.sha1
368 else:
369 sha1 = 'None'
370 return 'sha1: ' + sha1 + ' mode: ' + m
372 self.stages = [Stage(), Stage(), Stage(), Stage()]
373 self.path = path
374 self.processed = False
376 def __str__(self):
377 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
379 class CacheEntryContainer:
380 def __init__(self):
381 self.entries = {}
383 def add(self, entry):
384 self.entries[entry.path] = entry
386 def get(self, path):
387 return self.entries.get(path)
389 def __iter__(self):
390 return self.entries.itervalues()
392 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
393 def unmergedCacheEntries():
394 '''Create a dictionary mapping file names to CacheEntry
395 objects. The dictionary contains one entry for every path with a
396 non-zero stage entry.'''
398 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
399 lines.pop()
401 res = CacheEntryContainer()
402 for l in lines:
403 m = unmergedRE.match(l)
404 if m:
405 mode = int(m.group(1), 8)
406 sha1 = m.group(2)
407 stage = int(m.group(3))
408 path = m.group(4)
410 e = res.get(path)
411 if not e:
412 e = CacheEntry(path)
413 res.add(e)
415 e.stages[stage].mode = mode
416 e.stages[stage].sha1 = sha1
417 else:
418 die('Error: Merge program failed: Unexpected output from',
419 'git-ls-files:', l)
420 return res
422 lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
423 def getCacheEntry(path, origTree, aTree, bTree):
424 '''Returns a CacheEntry object which doesn't have to correspond to
425 a real cache entry in Git's index.'''
427 def parse(out):
428 if out == '':
429 return [None, None]
430 else:
431 m = lsTreeRE.match(out)
432 if not m:
433 die('Unexpected output from git-ls-tree:', out)
434 elif m.group(2) == 'blob':
435 return [m.group(3), int(m.group(1), 8)]
436 else:
437 return [None, None]
439 res = CacheEntry(path)
441 [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
442 [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
443 [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
445 res.stages[1].sha1 = oSha
446 res.stages[1].mode = oMode
447 res.stages[2].sha1 = aSha
448 res.stages[2].mode = aMode
449 res.stages[3].sha1 = bSha
450 res.stages[3].mode = bMode
452 return res
454 # Rename detection and handling
455 # -----------------------------
457 class RenameEntry:
458 def __init__(self,
459 src, srcSha, srcMode, srcCacheEntry,
460 dst, dstSha, dstMode, dstCacheEntry,
461 score):
462 self.srcName = src
463 self.srcSha = srcSha
464 self.srcMode = srcMode
465 self.srcCacheEntry = srcCacheEntry
466 self.dstName = dst
467 self.dstSha = dstSha
468 self.dstMode = dstMode
469 self.dstCacheEntry = dstCacheEntry
470 self.score = score
472 self.processed = False
474 class RenameEntryContainer:
475 def __init__(self):
476 self.entriesSrc = {}
477 self.entriesDst = {}
479 def add(self, entry):
480 self.entriesSrc[entry.srcName] = entry
481 self.entriesDst[entry.dstName] = entry
483 def getSrc(self, path):
484 return self.entriesSrc.get(path)
486 def getDst(self, path):
487 return self.entriesDst.get(path)
489 def __iter__(self):
490 return self.entriesSrc.itervalues()
492 parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
493 def getRenames(tree, oTree, aTree, bTree, cacheEntries):
494 '''Get information of all renames which occured between 'oTree' and
495 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
496 'bTree') to be able to associate the correct cache entries with
497 the rename information. 'tree' is always equal to either aTree or bTree.'''
499 assert(tree == aTree or tree == bTree)
500 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
501 '-z', oTree, tree])
503 ret = RenameEntryContainer()
504 try:
505 recs = inp.split("\0")
506 recs.pop() # remove last entry (which is '')
507 it = recs.__iter__()
508 while True:
509 rec = it.next()
510 m = parseDiffRenamesRE.match(rec)
512 if not m:
513 die('Unexpected output from git-diff-tree:', rec)
515 srcMode = int(m.group(1), 8)
516 dstMode = int(m.group(2), 8)
517 srcSha = m.group(3)
518 dstSha = m.group(4)
519 score = m.group(5)
520 src = it.next()
521 dst = it.next()
523 srcCacheEntry = cacheEntries.get(src)
524 if not srcCacheEntry:
525 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
526 cacheEntries.add(srcCacheEntry)
528 dstCacheEntry = cacheEntries.get(dst)
529 if not dstCacheEntry:
530 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
531 cacheEntries.add(dstCacheEntry)
533 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
534 dst, dstSha, dstMode, dstCacheEntry,
535 score))
536 except StopIteration:
537 pass
538 return ret
540 def fmtRename(src, dst):
541 srcPath = src.split('/')
542 dstPath = dst.split('/')
543 path = []
544 endIndex = min(len(srcPath), len(dstPath)) - 1
545 for x in range(0, endIndex):
546 if srcPath[x] == dstPath[x]:
547 path.append(srcPath[x])
548 else:
549 endIndex = x
550 break
552 if len(path) > 0:
553 return '/'.join(path) + \
554 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
555 '/'.join(dstPath[endIndex:]) + '}'
556 else:
557 return src + ' => ' + dst
559 def processRenames(renamesA, renamesB, branchNameA, branchNameB):
560 srcNames = Set()
561 for x in renamesA:
562 srcNames.add(x.srcName)
563 for x in renamesB:
564 srcNames.add(x.srcName)
566 cleanMerge = True
567 for path in srcNames:
568 if renamesA.getSrc(path):
569 renames1 = renamesA
570 renames2 = renamesB
571 branchName1 = branchNameA
572 branchName2 = branchNameB
573 else:
574 renames1 = renamesB
575 renames2 = renamesA
576 branchName1 = branchNameB
577 branchName2 = branchNameA
579 ren1 = renames1.getSrc(path)
580 ren2 = renames2.getSrc(path)
582 ren1.dstCacheEntry.processed = True
583 ren1.srcCacheEntry.processed = True
585 if ren1.processed:
586 continue
588 ren1.processed = True
590 if ren2:
591 # Renamed in 1 and renamed in 2
592 assert(ren1.srcName == ren2.srcName)
593 ren2.dstCacheEntry.processed = True
594 ren2.processed = True
596 if ren1.dstName != ren2.dstName:
597 output('CONFLICT (rename/rename): Rename',
598 fmtRename(path, ren1.dstName), 'in branch', branchName1,
599 'rename', fmtRename(path, ren2.dstName), 'in',
600 branchName2)
601 cleanMerge = False
603 if ren1.dstName in currentDirectorySet:
604 dstName1 = uniquePath(ren1.dstName, branchName1)
605 output(ren1.dstName, 'is a directory in', branchName2,
606 'adding as', dstName1, 'instead.')
607 removeFile(False, ren1.dstName)
608 else:
609 dstName1 = ren1.dstName
611 if ren2.dstName in currentDirectorySet:
612 dstName2 = uniquePath(ren2.dstName, branchName2)
613 output(ren2.dstName, 'is a directory in', branchName1,
614 'adding as', dstName2, 'instead.')
615 removeFile(False, ren2.dstName)
616 else:
617 dstName2 = ren2.dstName
618 setIndexStages(dstName1,
619 None, None,
620 ren1.dstSha, ren1.dstMode,
621 None, None)
622 setIndexStages(dstName2,
623 None, None,
624 None, None,
625 ren2.dstSha, ren2.dstMode)
627 else:
628 removeFile(True, ren1.srcName)
630 [resSha, resMode, clean, merge] = \
631 mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
632 ren1.dstName, ren1.dstSha, ren1.dstMode,
633 ren2.dstName, ren2.dstSha, ren2.dstMode,
634 branchName1, branchName2)
636 if merge or not clean:
637 output('Renaming', fmtRename(path, ren1.dstName))
639 if merge:
640 output('Auto-merging', ren1.dstName)
642 if not clean:
643 output('CONFLICT (content): merge conflict in',
644 ren1.dstName)
645 cleanMerge = False
647 if not cacheOnly:
648 setIndexStages(ren1.dstName,
649 ren1.srcSha, ren1.srcMode,
650 ren1.dstSha, ren1.dstMode,
651 ren2.dstSha, ren2.dstMode)
653 updateFile(clean, resSha, resMode, ren1.dstName)
654 else:
655 removeFile(True, ren1.srcName)
657 # Renamed in 1, maybe changed in 2
658 if renamesA == renames1:
659 stage = 3
660 else:
661 stage = 2
663 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
664 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
666 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
667 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
669 tryMerge = False
671 if ren1.dstName in currentDirectorySet:
672 newPath = uniquePath(ren1.dstName, branchName1)
673 output('CONFLICT (rename/directory): Rename',
674 fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,
675 'directory', ren1.dstName, 'added in', branchName2)
676 output('Renaming', ren1.srcName, 'to', newPath, 'instead')
677 cleanMerge = False
678 removeFile(False, ren1.dstName)
679 updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
680 elif srcShaOtherBranch == None:
681 output('CONFLICT (rename/delete): Rename',
682 fmtRename(ren1.srcName, ren1.dstName), 'in',
683 branchName1, 'and deleted in', branchName2)
684 cleanMerge = False
685 updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
686 elif dstShaOtherBranch:
687 newPath = uniquePath(ren1.dstName, branchName2)
688 output('CONFLICT (rename/add): Rename',
689 fmtRename(ren1.srcName, ren1.dstName), 'in',
690 branchName1 + '.', ren1.dstName, 'added in', branchName2)
691 output('Adding as', newPath, 'instead')
692 updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
693 cleanMerge = False
694 tryMerge = True
695 elif renames2.getDst(ren1.dstName):
696 dst2 = renames2.getDst(ren1.dstName)
697 newPath1 = uniquePath(ren1.dstName, branchName1)
698 newPath2 = uniquePath(dst2.dstName, branchName2)
699 output('CONFLICT (rename/rename): Rename',
700 fmtRename(ren1.srcName, ren1.dstName), 'in',
701 branchName1+'. Rename',
702 fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2)
703 output('Renaming', ren1.srcName, 'to', newPath1, 'and',
704 dst2.srcName, 'to', newPath2, 'instead')
705 removeFile(False, ren1.dstName)
706 updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
707 updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
708 dst2.processed = True
709 cleanMerge = False
710 else:
711 tryMerge = True
713 if tryMerge:
715 oName, oSHA1, oMode = ren1.srcName, ren1.srcSha, ren1.srcMode
716 aName, bName = ren1.dstName, ren1.srcName
717 aSHA1, bSHA1 = ren1.dstSha, srcShaOtherBranch
718 aMode, bMode = ren1.dstMode, srcModeOtherBranch
719 aBranch, bBranch = branchName1, branchName2
721 if renamesA != renames1:
722 aName, bName = bName, aName
723 aSHA1, bSHA1 = bSHA1, aSHA1
724 aMode, bMode = bMode, aMode
725 aBranch, bBranch = bBranch, aBranch
727 [resSha, resMode, clean, merge] = \
728 mergeFile(oName, oSHA1, oMode,
729 aName, aSHA1, aMode,
730 bName, bSHA1, bMode,
731 aBranch, bBranch);
733 if merge or not clean:
734 output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
736 if merge:
737 output('Auto-merging', ren1.dstName)
739 if not clean:
740 output('CONFLICT (rename/modify): Merge conflict in',
741 ren1.dstName)
742 cleanMerge = False
744 if not cacheOnly:
745 setIndexStages(ren1.dstName,
746 oSHA1, oMode,
747 aSHA1, aMode,
748 bSHA1, bMode)
750 updateFile(clean, resSha, resMode, ren1.dstName)
752 return cleanMerge
754 # Per entry merge function
755 # ------------------------
757 def processEntry(entry, branch1Name, branch2Name):
758 '''Merge one cache entry.'''
760 debug('processing', entry.path, 'clean cache:', cacheOnly)
762 cleanMerge = True
764 path = entry.path
765 oSha = entry.stages[1].sha1
766 oMode = entry.stages[1].mode
767 aSha = entry.stages[2].sha1
768 aMode = entry.stages[2].mode
769 bSha = entry.stages[3].sha1
770 bMode = entry.stages[3].mode
772 assert(oSha == None or isSha(oSha))
773 assert(aSha == None or isSha(aSha))
774 assert(bSha == None or isSha(bSha))
776 assert(oMode == None or type(oMode) is int)
777 assert(aMode == None or type(aMode) is int)
778 assert(bMode == None or type(bMode) is int)
780 if (oSha and (not aSha or not bSha)):
782 # Case A: Deleted in one
784 if (not aSha and not bSha) or \
785 (aSha == oSha and not bSha) or \
786 (not aSha and bSha == oSha):
787 # Deleted in both or deleted in one and unchanged in the other
788 if aSha:
789 output('Removing', path)
790 removeFile(True, path)
791 else:
792 # Deleted in one and changed in the other
793 cleanMerge = False
794 if not aSha:
795 output('CONFLICT (delete/modify):', path, 'deleted in',
796 branch1Name, 'and modified in', branch2Name + '.',
797 'Version', branch2Name, 'of', path, 'left in tree.')
798 mode = bMode
799 sha = bSha
800 else:
801 output('CONFLICT (modify/delete):', path, 'deleted in',
802 branch2Name, 'and modified in', branch1Name + '.',
803 'Version', branch1Name, 'of', path, 'left in tree.')
804 mode = aMode
805 sha = aSha
807 updateFile(False, sha, mode, path)
809 elif (not oSha and aSha and not bSha) or \
810 (not oSha and not aSha and bSha):
812 # Case B: Added in one.
814 if aSha:
815 addBranch = branch1Name
816 otherBranch = branch2Name
817 mode = aMode
818 sha = aSha
819 conf = 'file/directory'
820 else:
821 addBranch = branch2Name
822 otherBranch = branch1Name
823 mode = bMode
824 sha = bSha
825 conf = 'directory/file'
827 if path in currentDirectorySet:
828 cleanMerge = False
829 newPath = uniquePath(path, addBranch)
830 output('CONFLICT (' + conf + '):',
831 'There is a directory with name', path, 'in',
832 otherBranch + '. Adding', path, 'as', newPath)
834 removeFile(False, path)
835 updateFile(False, sha, mode, newPath)
836 else:
837 output('Adding', path)
838 updateFile(True, sha, mode, path)
840 elif not oSha and aSha and bSha:
842 # Case C: Added in both (check for same permissions).
844 if aSha == bSha:
845 if aMode != bMode:
846 cleanMerge = False
847 output('CONFLICT: File', path,
848 'added identically in both branches, but permissions',
849 'conflict', '0%o' % aMode, '->', '0%o' % bMode)
850 output('CONFLICT: adding with permission:', '0%o' % aMode)
852 updateFile(False, aSha, aMode, path)
853 else:
854 # This case is handled by git-read-tree
855 assert(False)
856 else:
857 cleanMerge = False
858 newPath1 = uniquePath(path, branch1Name)
859 newPath2 = uniquePath(path, branch2Name)
860 output('CONFLICT (add/add): File', path,
861 'added non-identically in both branches. Adding as',
862 newPath1, 'and', newPath2, 'instead.')
863 removeFile(False, path)
864 updateFile(False, aSha, aMode, newPath1)
865 updateFile(False, bSha, bMode, newPath2)
867 elif oSha and aSha and bSha:
869 # case D: Modified in both, but differently.
871 output('Auto-merging', path)
872 [sha, mode, clean, dummy] = \
873 mergeFile(path, oSha, oMode,
874 path, aSha, aMode,
875 path, bSha, bMode,
876 branch1Name, branch2Name)
877 if clean:
878 updateFile(True, sha, mode, path)
879 else:
880 cleanMerge = False
881 output('CONFLICT (content): Merge conflict in', path)
883 if cacheOnly:
884 updateFile(False, sha, mode, path)
885 else:
886 updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
887 else:
888 die("ERROR: Fatal merge failure, shouldn't happen.")
890 return cleanMerge
892 def usage():
893 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
895 # main entry point as merge strategy module
896 # The first parameters up to -- are merge bases, and the rest are heads.
898 if len(sys.argv) < 4:
899 usage()
901 bases = []
902 for nextArg in xrange(1, len(sys.argv)):
903 if sys.argv[nextArg] == '--':
904 if len(sys.argv) != nextArg + 3:
905 die('Not handling anything other than two heads merge.')
906 try:
907 h1 = firstBranch = sys.argv[nextArg + 1]
908 h2 = secondBranch = sys.argv[nextArg + 2]
909 except IndexError:
910 usage()
911 break
912 else:
913 bases.append(sys.argv[nextArg])
915 print 'Merging', h1, 'with', h2
917 try:
918 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
919 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
921 if len(bases) == 1:
922 base = runProgram(['git-rev-parse', '--verify',
923 bases[0] + '^0']).rstrip()
924 ancestor = Commit(base, None)
925 [dummy, clean] = merge(Commit(h1, None), Commit(h2, None),
926 firstBranch, secondBranch, None, 0,
927 ancestor)
928 else:
929 graph = buildGraph([h1, h2])
930 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
931 firstBranch, secondBranch, graph)
933 print ''
934 except:
935 if isinstance(sys.exc_info()[1], SystemExit):
936 raise
937 else:
938 traceback.print_exc(None, sys.stderr)
939 sys.exit(2)
941 if clean:
942 sys.exit(0)
943 else:
944 sys.exit(1)