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