http-push: add support for deleting remote branches
[git] / git-merge-recursive.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2005 Fredrik Kuivinen
4 #
5
6 import sys
7 sys.path.append('''@@GIT_PYTHON_PATH@@''')
8
9 import math, random, os, re, signal, tempfile, stat, errno, traceback
10 from heapq import heappush, heappop
11 from sets import Set
12
13 from gitMergeCommon import *
14
15 outputIndent = 0
16 def output(*args):
17     sys.stdout.write('  '*outputIndent)
18     printList(args)
19
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
34
35 # This is a global variable which is used in a number of places but
36 # only written to in the 'merge' function.
37
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.
42
43 cacheOnly = False
44
45 # The entry point to the merge code
46 # ---------------------------------
47
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))
52
53     global outputIndent
54
55     output('Merging:')
56     output(h1)
57     output(h2)
58     sys.stdout.flush()
59
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()
69
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))
79
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
88
89     [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
90                                  branch1Name, branch2Name)
91
92     if graph and (clean or cacheOnly):
93         res = Commit(None, [h1, h2], tree=shaRes)
94         graph.addNode(res)
95     else:
96         res = None
97
98     return [res, clean]
99
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))
112
113     return [files, dirs]
114
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
121
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.'''
128     
129     assert(isSha(head) and isSha(merge) and isSha(common))
130
131     if common == merge:
132         output('Already uptodate!')
133         return [head, True]
134
135     if cacheOnly:
136         updateArg = '-i'
137     else:
138         updateArg = '-u'
139
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)
144
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)
153
154         entries = unmergedCacheEntries()
155         renamesHead =  getRenames(head, common, head, merge, entries)
156         renamesMerge = getRenames(merge, common, head, merge, entries)
157
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
165                 
166         if cleanMerge or cacheOnly:
167             tree = runProgram('git-write-tree').rstrip()
168         else:
169             tree = None
170     else:
171         cleanMerge = True
172
173     return [tree, cleanMerge]
174
175 # Low level file merging, update and removal
176 # ------------------------------------------
177
178 def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
179               branch1Name, branch2Name):
180
181     merge = False
182     clean = True
183
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
195
196         if aMode == oMode:
197             mode = bMode
198         else:
199             mode = aMode
200
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))
207
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?")
221
222             sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
223                               src1]).rstrip()
224
225             os.unlink(orig)
226             os.unlink(src1)
227             os.unlink(src2)
228
229             clean = (code == 0)
230         else:
231             assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
232             sha = aSha
233
234             if aSha != bSha:
235                 clean = False
236
237     return [sha, mode, clean, merge]
238
239 def updateFile(clean, sha, mode, path):
240     updateCache = cacheOnly or clean
241     updateWd = not cacheOnly
242
243     return updateFileExt(sha, mode, path, updateCache, updateWd)
244
245 def updateFileExt(sha, mode, path, updateCache, updateWd):
246     if cacheOnly:
247         updateWd = False
248
249     if updateWd:
250         pathComponents = path.split('/')
251         for x in xrange(1, len(pathComponents)):
252             p = '/'.join(pathComponents[0:x])
253
254             try:
255                 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
256             except OSError:
257                 createDir = True
258             
259             if createDir:
260                 try:
261                     os.mkdir(p)
262                 except OSError, e:
263                     die("Couldn't create directory", p, e.strerror)
264
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)
284
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])
290
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))
305
306     runProgram(['git-update-index', '-z', '--index-info'],
307                input="".join(istring))
308
309 def removeFile(clean, path):
310     updateCache = cacheOnly or clean
311     updateWd = not cacheOnly
312
313     if updateCache:
314         runProgram(['git-update-index', '--force-remove', '--', path])
315
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
326
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
337
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
348
349 # Cache entry management
350 # ----------------------
351
352 class CacheEntry:
353     def __init__(self, path):
354         class Stage:
355             def __init__(self):
356                 self.sha1 = None
357                 self.mode = None
358
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'
365
366                 if self.sha1:
367                     sha1 = self.sha1
368                 else:
369                     sha1 = 'None'
370                 return 'sha1: ' + sha1 + ' mode: ' + m
371         
372         self.stages = [Stage(), Stage(), Stage(), Stage()]
373         self.path = path
374         self.processed = False
375
376     def __str__(self):
377         return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
378
379 class CacheEntryContainer:
380     def __init__(self):
381         self.entries = {}
382
383     def add(self, entry):
384         self.entries[entry.path] = entry
385
386     def get(self, path):
387         return self.entries.get(path)
388
389     def __iter__(self):
390         return self.entries.itervalues()
391     
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.'''
397
398     lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
399     lines.pop()
400
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)
409
410             e = res.get(path)
411             if not e:
412                 e = CacheEntry(path)
413                 res.add(e)
414
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
421
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.'''
426     
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]
438
439     res = CacheEntry(path)
440
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]))
444
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
451
452     return res
453
454 # Rename detection and handling
455 # -----------------------------
456
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
471
472         self.processed = False
473
474 class RenameEntryContainer:
475     def __init__(self):
476         self.entriesSrc = {}
477         self.entriesDst = {}
478
479     def add(self, entry):
480         self.entriesSrc[entry.srcName] = entry
481         self.entriesDst[entry.dstName] = entry
482
483     def getSrc(self, path):
484         return self.entriesSrc.get(path)
485
486     def getDst(self, path):
487         return self.entriesDst.get(path)
488
489     def __iter__(self):
490         return self.entriesSrc.itervalues()
491
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.'''
498
499     assert(tree == aTree or tree == bTree)
500     inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
501                       '-z', oTree, tree])
502
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)
511
512             if not m:
513                 die('Unexpected output from git-diff-tree:', rec)
514
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()
522
523             srcCacheEntry = cacheEntries.get(src)
524             if not srcCacheEntry:
525                 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
526                 cacheEntries.add(srcCacheEntry)
527
528             dstCacheEntry = cacheEntries.get(dst)
529             if not dstCacheEntry:
530                 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
531                 cacheEntries.add(dstCacheEntry)
532
533             ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
534                                 dst, dstSha, dstMode, dstCacheEntry,
535                                 score))
536     except StopIteration:
537         pass
538     return ret
539
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
551
552     if len(path) > 0:
553         return '/'.join(path) + \
554                '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
555                '/'.join(dstPath[endIndex:]) + '}'
556     else:
557         return src + ' => ' + dst
558
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)
565
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
578         
579         ren1 = renames1.getSrc(path)
580         ren2 = renames2.getSrc(path)
581
582         ren1.dstCacheEntry.processed = True
583         ren1.srcCacheEntry.processed = True
584
585         if ren1.processed:
586             continue
587
588         ren1.processed = True
589
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
595
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
602
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
610
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)
626
627             else:
628                 removeFile(True, ren1.srcName)
629
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)
635
636                 if merge or not clean:
637                     output('Renaming', fmtRename(path, ren1.dstName))
638
639                 if merge:
640                     output('Auto-merging', ren1.dstName)
641
642                 if not clean:
643                     output('CONFLICT (content): merge conflict in',
644                            ren1.dstName)
645                     cleanMerge = False
646
647                     if not cacheOnly:
648                         setIndexStages(ren1.dstName,
649                                        ren1.srcSha, ren1.srcMode,
650                                        ren1.dstSha, ren1.dstMode,
651                                        ren2.dstSha, ren2.dstMode)
652
653                 updateFile(clean, resSha, resMode, ren1.dstName)
654         else:
655             removeFile(True, ren1.srcName)
656
657             # Renamed in 1, maybe changed in 2
658             if renamesA == renames1:
659                 stage = 3
660             else:
661                 stage = 2
662                 
663             srcShaOtherBranch  = ren1.srcCacheEntry.stages[stage].sha1
664             srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
665
666             dstShaOtherBranch  = ren1.dstCacheEntry.stages[stage].sha1
667             dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
668
669             tryMerge = False
670             
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
712
713             if tryMerge:
714
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
720
721                 if renamesA != renames1:
722                     aName, bName = bName, aName
723                     aSHA1, bSHA1 = bSHA1, aSHA1
724                     aMode, bMode = bMode, aMode
725                     aBranch, bBranch = bBranch, aBranch
726
727                 [resSha, resMode, clean, merge] = \
728                          mergeFile(oName, oSHA1, oMode,
729                                    aName, aSHA1, aMode,
730                                    bName, bSHA1, bMode,
731                                    aBranch, bBranch);
732
733                 if merge or not clean:
734                     output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
735
736                 if merge:
737                     output('Auto-merging', ren1.dstName)
738
739                 if not clean:
740                     output('CONFLICT (rename/modify): Merge conflict in',
741                            ren1.dstName)
742                     cleanMerge = False
743
744                     if not cacheOnly:
745                         setIndexStages(ren1.dstName,
746                                        oSHA1, oMode,
747                                        aSHA1, aMode,
748                                        bSHA1, bMode)
749
750                 updateFile(clean, resSha, resMode, ren1.dstName)
751
752     return cleanMerge
753
754 # Per entry merge function
755 # ------------------------
756
757 def processEntry(entry, branch1Name, branch2Name):
758     '''Merge one cache entry.'''
759
760     debug('processing', entry.path, 'clean cache:', cacheOnly)
761
762     cleanMerge = True
763
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
771
772     assert(oSha == None or isSha(oSha))
773     assert(aSha == None or isSha(aSha))
774     assert(bSha == None or isSha(bSha))
775
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)
779
780     if (oSha and (not aSha or not bSha)):
781     #
782     # Case A: Deleted in one
783     #
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
806
807             updateFile(False, sha, mode, path)
808
809     elif (not oSha and aSha     and not bSha) or \
810          (not oSha and not aSha and bSha):
811     #
812     # Case B: Added in one.
813     #
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'
826     
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)
833
834             removeFile(False, path)
835             updateFile(False, sha, mode, newPath)
836         else:
837             output('Adding', path)
838             updateFile(True, sha, mode, path)
839     
840     elif not oSha and aSha and bSha:
841     #
842     # Case C: Added in both (check for same permissions).
843     #
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)
851
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)
866
867     elif oSha and aSha and bSha:
868     #
869     # case D: Modified in both, but differently.
870     #
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)
882
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.")
889
890     return cleanMerge
891
892 def usage():
893     die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
894
895 # main entry point as merge strategy module
896 # The first parameters up to -- are merge bases, and the rest are heads.
897
898 if len(sys.argv) < 4:
899     usage()
900
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])
914
915 print 'Merging', h1, 'with', h2
916
917 try:
918     h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
919     h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
920
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)
932
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)
940
941 if clean:
942     sys.exit(0)
943 else:
944     sys.exit(1)