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