]> git.ipfire.org Git - thirdparty/git.git/blob - git-merge-recursive.py
Merge branch 'master'
[thirdparty/git.git] / git-merge-recursive.py
1 #!/usr/bin/python
2
3 import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
4 from heapq import heappush, heappop
5 from sets import Set
6
7 sys.path.append('''@@GIT_PYTHON_PATH@@''')
8 from gitMergeCommon import *
9
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
24
25 # This is a global variable which is used in a number of places but
26 # only written to in the 'merge' function.
27
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.
32
33 cacheOnly = False
34
35 # The entry point to the merge code
36 # ---------------------------------
37
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))
43
44 def infoMsg(*args):
45 sys.stdout.write(' '*callDepth)
46 printList(args)
47
48 infoMsg('Merging:')
49 infoMsg(h1)
50 infoMsg(h2)
51 sys.stdout.flush()
52
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()
58
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))
66
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
75
76 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
77 branch1Name, branch2Name)
78
79 if clean or cacheOnly:
80 res = Commit(None, [h1, h2], tree=shaRes)
81 graph.addNode(res)
82 else:
83 res = None
84
85 return [res, clean]
86
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))
99
100 return [files, dirs]
101
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
108
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.'''
115
116 assert(isSha(head) and isSha(merge) and isSha(common))
117
118 if common == merge:
119 print 'Already uptodate!'
120 return [head, True]
121
122 if cacheOnly:
123 updateArg = '-i'
124 else:
125 updateArg = '-u'
126
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)
131
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)
140
141 entries = unmergedCacheEntries()
142 renamesHead = getRenames(head, common, head, merge, entries)
143 renamesMerge = getRenames(merge, common, head, merge, entries)
144
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
152
153 if cleanMerge or cacheOnly:
154 tree = runProgram('git-write-tree').rstrip()
155 else:
156 tree = None
157 else:
158 cleanMerge = True
159
160 return [tree, cleanMerge]
161
162 # Low level file merging, update and removal
163 # ------------------------------------------
164
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):
170
171 merge = MERGE_NONE
172 clean = True
173
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
185
186 if aMode == oMode:
187 mode = bMode
188 else:
189 mode = aMode
190
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))
197
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)
206
207 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
208 src1]).rstrip()
209
210 os.unlink(orig)
211 os.unlink(src1)
212 os.unlink(src2)
213
214 merge = MERGE_3WAY
215 clean = (code == 0)
216 else:
217 assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
218 sha = aSha
219
220 if aSha != bSha:
221 clean = False
222
223 return [sha, mode, clean, merge]
224
225 def updateFile(clean, sha, mode, path):
226 updateCache = cacheOnly or clean
227 updateWd = not cacheOnly
228
229 return updateFileExt(sha, mode, path, updateCache, updateWd)
230
231 def updateFileExt(sha, mode, path, updateCache, updateWd):
232 if cacheOnly:
233 updateWd = False
234
235 if updateWd:
236 pathComponents = path.split('/')
237 for x in xrange(1, len(pathComponents)):
238 p = '/'.join(pathComponents[0:x])
239
240 try:
241 createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
242 except:
243 createDir = True
244
245 if createDir:
246 try:
247 os.mkdir(p)
248 except OSError, e:
249 die("Couldn't create directory", p, e.strerror)
250
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)
270
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])
276
277 def removeFile(clean, path):
278 updateCache = cacheOnly or clean
279 updateWd = not cacheOnly
280
281 if updateCache:
282 runProgram(['git-update-index', '--force-remove', '--', path])
283
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
290
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
301
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
311
312 # Cache entry management
313 # ----------------------
314
315 class CacheEntry:
316 def __init__(self, path):
317 class Stage:
318 def __init__(self):
319 self.sha1 = None
320 self.mode = None
321
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'
328
329 if self.sha1:
330 sha1 = self.sha1
331 else:
332 sha1 = 'None'
333 return 'sha1: ' + sha1 + ' mode: ' + m
334
335 self.stages = [Stage(), Stage(), Stage(), Stage()]
336 self.path = path
337 self.processed = False
338
339 def __str__(self):
340 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
341
342 class CacheEntryContainer:
343 def __init__(self):
344 self.entries = {}
345
346 def add(self, entry):
347 self.entries[entry.path] = entry
348
349 def get(self, path):
350 return self.entries.get(path)
351
352 def __iter__(self):
353 return self.entries.itervalues()
354
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.'''
360
361 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
362 lines.pop()
363
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)
372
373 e = res.get(path)
374 if not e:
375 e = CacheEntry(path)
376 res.add(e)
377
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
384
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.'''
389
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]
401
402 res = CacheEntry(path)
403
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]))
407
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
414
415 return res
416
417 # Rename detection and handling
418 # -----------------------------
419
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
434
435 self.processed = False
436
437 class RenameEntryContainer:
438 def __init__(self):
439 self.entriesSrc = {}
440 self.entriesDst = {}
441
442 def add(self, entry):
443 self.entriesSrc[entry.srcName] = entry
444 self.entriesDst[entry.dstName] = entry
445
446 def getSrc(self, path):
447 return self.entriesSrc.get(path)
448
449 def getDst(self, path):
450 return self.entriesDst.get(path)
451
452 def __iter__(self):
453 return self.entriesSrc.itervalues()
454
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.'''
461
462 assert(tree == aTree or tree == bTree)
463 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
464 '-z', oTree, tree])
465
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)
474
475 if not m:
476 die('Unexpected output from git-diff-tree:', rec)
477
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()
485
486 srcCacheEntry = cacheEntries.get(src)
487 if not srcCacheEntry:
488 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
489 cacheEntries.add(srcCacheEntry)
490
491 dstCacheEntry = cacheEntries.get(dst)
492 if not dstCacheEntry:
493 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
494 cacheEntries.add(dstCacheEntry)
495
496 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
497 dst, dstSha, dstMode, dstCacheEntry,
498 score))
499 except StopIteration:
500 pass
501 return ret
502
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
514
515 if len(path) > 0:
516 return '/'.join(path) + \
517 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
518 '/'.join(dstPath[endIndex:]) + '}'
519 else:
520 return src + ' => ' + dst
521
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)
528
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
541
542 ren1 = renames1.getSrc(path)
543 ren2 = renames2.getSrc(path)
544
545 ren1.dstCacheEntry.processed = True
546 ren1.srcCacheEntry.processed = True
547
548 if ren1.processed:
549 continue
550
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
558
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
564
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
572
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
580
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)
589
590 if merge or not clean:
591 print 'Renaming', fmtRename(path, ren1.dstName)
592
593 if merge == MERGE_3WAY:
594 print 'Auto-merging', ren1.dstName
595
596 if not clean:
597 print 'CONFLICT (content): merge conflict in', ren1.dstName
598 cleanMerge = False
599
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
610
611 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
612 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
613
614 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
615 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
616
617 tryMerge = False
618
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
660
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)
667
668 if merge or not clean:
669 print 'Renaming', fmtRename(ren1.srcName, ren1.dstName)
670
671 if merge == MERGE_3WAY:
672 print 'Auto-merging', ren1.dstName
673
674 if not clean:
675 print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName
676 cleanMerge = False
677
678 if not cacheOnly:
679 updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
680 updateCache=True, updateWd=False)
681 updateFile(clean, resSha, resMode, ren1.dstName)
682
683 return cleanMerge
684
685 # Per entry merge function
686 # ------------------------
687
688 def processEntry(entry, branch1Name, branch2Name):
689 '''Merge one cache entry.'''
690
691 debug('processing', entry.path, 'clean cache:', cacheOnly)
692
693 cleanMerge = True
694
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
702
703 assert(oSha == None or isSha(oSha))
704 assert(aSha == None or isSha(aSha))
705 assert(bSha == None or isSha(bSha))
706
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)
710
711 if (oSha and (not aSha or not bSha)):
712 #
713 # Case A: Deleted in one
714 #
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
737
738 updateFile(False, sha, mode, path)
739
740 elif (not oSha and aSha and not bSha) or \
741 (not oSha and not aSha and bSha):
742 #
743 # Case B: Added in one.
744 #
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'
757
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
764
765 removeFile(False, path)
766 updateFile(False, sha, mode, newPath)
767 else:
768 print 'Adding', path
769 updateFile(True, sha, mode, path)
770
771 elif not oSha and aSha and bSha:
772 #
773 # Case C: Added in both (check for same permissions).
774 #
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
782
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)
797
798 elif oSha and aSha and bSha:
799 #
800 # case D: Modified in both, but differently.
801 #
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
813
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.")
822
823 return cleanMerge
824
825 def usage():
826 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
827
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.
832
833 if len(sys.argv) < 4:
834 usage()
835
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
846
847 print 'Merging', h1, 'with', h2
848
849 try:
850 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
851 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
852
853 graph = buildGraph([h1, h2])
854
855 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
856 firstBranch, secondBranch, graph)
857
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)
865
866 if clean:
867 sys.exit(0)
868 else:
869 sys.exit(1)