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