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