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