]> git.ipfire.org Git - thirdparty/git.git/blame - git-merge-recursive.py
short circuit out of a few places where we would allocate zero bytes
[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,
286 bSHA1, bMode):
157dc073
JH
287 runProgram(['git-update-index', '-z', '--index-info'],
288 input="0 " + ("0" * 40) + "\t" + path + "\0" + \
289 "%o %s %d\t%s\0" % (oMode, oSHA1, 1, path) + \
290 "%o %s %d\t%s\0" % (aMode, aSHA1, 2, path) + \
291 "%o %s %d\t%s\0" % (bMode, bSHA1, 3, path))
28e77a81 292
af215114
FK
293def removeFile(clean, path):
294 updateCache = cacheOnly or clean
295 updateWd = not cacheOnly
296
297 if updateCache:
298 runProgram(['git-update-index', '--force-remove', '--', path])
299
300 if updateWd:
301 try:
302 os.unlink(path)
303 except OSError, e:
304 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
305 raise
80e21a9e
JH
306 try:
307 os.removedirs(os.path.dirname(path))
a6322d07 308 except OSError:
80e21a9e 309 pass
af215114
FK
310
311def uniquePath(path, branch):
312 def fileExists(path):
313 try:
314 os.lstat(path)
315 return True
316 except OSError, e:
317 if e.errno == errno.ENOENT:
318 return False
319 else:
320 raise
321
186f855f 322 branch = branch.replace('/', '_')
e9af60c8 323 newPath = path + '~' + branch
af215114
FK
324 suffix = 0
325 while newPath in currentFileSet or \
326 newPath in currentDirectorySet or \
327 fileExists(newPath):
328 suffix += 1
e9af60c8 329 newPath = path + '~' + branch + '_' + str(suffix)
af215114
FK
330 currentFileSet.add(newPath)
331 return newPath
332
333# Cache entry management
334# ----------------------
335
720d150c
JH
336class CacheEntry:
337 def __init__(self, path):
338 class Stage:
339 def __init__(self):
340 self.sha1 = None
341 self.mode = None
af215114
FK
342
343 # Used for debugging only
344 def __str__(self):
345 if self.mode != None:
346 m = '0%o' % self.mode
347 else:
348 m = 'None'
349
350 if self.sha1:
351 sha1 = self.sha1
352 else:
353 sha1 = 'None'
354 return 'sha1: ' + sha1 + ' mode: ' + m
720d150c 355
af215114 356 self.stages = [Stage(), Stage(), Stage(), Stage()]
720d150c 357 self.path = path
af215114
FK
358 self.processed = False
359
360 def __str__(self):
361 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
720d150c 362
af215114
FK
363class CacheEntryContainer:
364 def __init__(self):
365 self.entries = {}
366
367 def add(self, entry):
368 self.entries[entry.path] = entry
369
370 def get(self, path):
371 return self.entries.get(path)
372
373 def __iter__(self):
374 return self.entries.itervalues()
375
74376a68 376unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
720d150c
JH
377def unmergedCacheEntries():
378 '''Create a dictionary mapping file names to CacheEntry
379 objects. The dictionary contains one entry for every path with a
380 non-zero stage entry.'''
381
382 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
383 lines.pop()
384
af215114 385 res = CacheEntryContainer()
720d150c
JH
386 for l in lines:
387 m = unmergedRE.match(l)
388 if m:
389 mode = int(m.group(1), 8)
390 sha1 = m.group(2)
af215114 391 stage = int(m.group(3))
720d150c
JH
392 path = m.group(4)
393
af215114
FK
394 e = res.get(path)
395 if not e:
720d150c 396 e = CacheEntry(path)
af215114
FK
397 res.add(e)
398
720d150c
JH
399 e.stages[stage].mode = mode
400 e.stages[stage].sha1 = sha1
401 else:
af215114 402 die('Error: Merge program failed: Unexpected output from',
654291a2 403 'git-ls-files:', l)
720d150c
JH
404 return res
405
af215114
FK
406lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
407def getCacheEntry(path, origTree, aTree, bTree):
408 '''Returns a CacheEntry object which doesn't have to correspond to
409 a real cache entry in Git's index.'''
720d150c 410
af215114
FK
411 def parse(out):
412 if out == '':
413 return [None, None]
414 else:
415 m = lsTreeRE.match(out)
416 if not m:
417 die('Unexpected output from git-ls-tree:', out)
418 elif m.group(2) == 'blob':
419 return [m.group(3), int(m.group(1), 8)]
420 else:
421 return [None, None]
720d150c 422
af215114 423 res = CacheEntry(path)
720d150c 424
af215114
FK
425 [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
426 [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
427 [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
d9a23fa6 428
af215114
FK
429 res.stages[1].sha1 = oSha
430 res.stages[1].mode = oMode
431 res.stages[2].sha1 = aSha
432 res.stages[2].mode = aMode
433 res.stages[3].sha1 = bSha
434 res.stages[3].mode = bMode
0bed1899 435
af215114 436 return res
720d150c 437
af215114
FK
438# Rename detection and handling
439# -----------------------------
440
441class RenameEntry:
442 def __init__(self,
443 src, srcSha, srcMode, srcCacheEntry,
444 dst, dstSha, dstMode, dstCacheEntry,
445 score):
446 self.srcName = src
447 self.srcSha = srcSha
448 self.srcMode = srcMode
449 self.srcCacheEntry = srcCacheEntry
450 self.dstName = dst
451 self.dstSha = dstSha
452 self.dstMode = dstMode
453 self.dstCacheEntry = dstCacheEntry
454 self.score = score
455
456 self.processed = False
457
458class RenameEntryContainer:
459 def __init__(self):
460 self.entriesSrc = {}
461 self.entriesDst = {}
462
463 def add(self, entry):
464 self.entriesSrc[entry.srcName] = entry
465 self.entriesDst[entry.dstName] = entry
466
467 def getSrc(self, path):
468 return self.entriesSrc.get(path)
469
470 def getDst(self, path):
471 return self.entriesDst.get(path)
472
473 def __iter__(self):
474 return self.entriesSrc.itervalues()
475
476parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
477def getRenames(tree, oTree, aTree, bTree, cacheEntries):
478 '''Get information of all renames which occured between 'oTree' and
479 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
480 'bTree') to be able to associate the correct cache entries with
481 the rename information. 'tree' is always equal to either aTree or bTree.'''
482
483 assert(tree == aTree or tree == bTree)
484 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
485 '-z', oTree, tree])
486
487 ret = RenameEntryContainer()
488 try:
489 recs = inp.split("\0")
490 recs.pop() # remove last entry (which is '')
491 it = recs.__iter__()
492 while True:
493 rec = it.next()
494 m = parseDiffRenamesRE.match(rec)
495
496 if not m:
497 die('Unexpected output from git-diff-tree:', rec)
498
499 srcMode = int(m.group(1), 8)
500 dstMode = int(m.group(2), 8)
501 srcSha = m.group(3)
502 dstSha = m.group(4)
503 score = m.group(5)
504 src = it.next()
505 dst = it.next()
506
507 srcCacheEntry = cacheEntries.get(src)
508 if not srcCacheEntry:
509 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
510 cacheEntries.add(srcCacheEntry)
511
512 dstCacheEntry = cacheEntries.get(dst)
513 if not dstCacheEntry:
514 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
515 cacheEntries.add(dstCacheEntry)
516
517 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
518 dst, dstSha, dstMode, dstCacheEntry,
519 score))
520 except StopIteration:
521 pass
522 return ret
523
524def fmtRename(src, dst):
525 srcPath = src.split('/')
526 dstPath = dst.split('/')
527 path = []
528 endIndex = min(len(srcPath), len(dstPath)) - 1
529 for x in range(0, endIndex):
530 if srcPath[x] == dstPath[x]:
531 path.append(srcPath[x])
720d150c 532 else:
af215114
FK
533 endIndex = x
534 break
535
536 if len(path) > 0:
537 return '/'.join(path) + \
538 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
539 '/'.join(dstPath[endIndex:]) + '}'
720d150c 540 else:
af215114 541 return src + ' => ' + dst
720d150c 542
af215114
FK
543def processRenames(renamesA, renamesB, branchNameA, branchNameB):
544 srcNames = Set()
545 for x in renamesA:
546 srcNames.add(x.srcName)
547 for x in renamesB:
548 srcNames.add(x.srcName)
720d150c 549
af215114
FK
550 cleanMerge = True
551 for path in srcNames:
552 if renamesA.getSrc(path):
553 renames1 = renamesA
554 renames2 = renamesB
555 branchName1 = branchNameA
556 branchName2 = branchNameB
557 else:
558 renames1 = renamesB
559 renames2 = renamesA
560 branchName1 = branchNameB
561 branchName2 = branchNameA
562
563 ren1 = renames1.getSrc(path)
564 ren2 = renames2.getSrc(path)
565
566 ren1.dstCacheEntry.processed = True
567 ren1.srcCacheEntry.processed = True
568
569 if ren1.processed:
570 continue
571
572 ren1.processed = True
573 removeFile(True, ren1.srcName)
574 if ren2:
575 # Renamed in 1 and renamed in 2
576 assert(ren1.srcName == ren2.srcName)
577 ren2.dstCacheEntry.processed = True
578 ren2.processed = True
579
580 if ren1.dstName != ren2.dstName:
46e65174
FK
581 output('CONFLICT (rename/rename): Rename',
582 fmtRename(path, ren1.dstName), 'in branch', branchName1,
583 'rename', fmtRename(path, ren2.dstName), 'in',
584 branchName2)
af215114 585 cleanMerge = False
720d150c 586
af215114
FK
587 if ren1.dstName in currentDirectorySet:
588 dstName1 = uniquePath(ren1.dstName, branchName1)
46e65174
FK
589 output(ren1.dstName, 'is a directory in', branchName2,
590 'adding as', dstName1, 'instead.')
af215114
FK
591 removeFile(False, ren1.dstName)
592 else:
593 dstName1 = ren1.dstName
720d150c 594
af215114
FK
595 if ren2.dstName in currentDirectorySet:
596 dstName2 = uniquePath(ren2.dstName, branchName2)
46e65174
FK
597 output(ren2.dstName, 'is a directory in', branchName1,
598 'adding as', dstName2, 'instead.')
af215114
FK
599 removeFile(False, ren2.dstName)
600 else:
601 dstName2 = ren1.dstName
720d150c 602
28e77a81
JH
603 # NEEDSWORK: place dstNameA at stage 2 and dstNameB at stage 3
604 # What about other stages???
af215114
FK
605 updateFile(False, ren1.dstSha, ren1.dstMode, dstName1)
606 updateFile(False, ren2.dstSha, ren2.dstMode, dstName2)
607 else:
af215114
FK
608 [resSha, resMode, clean, merge] = \
609 mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
610 ren1.dstName, ren1.dstSha, ren1.dstMode,
611 ren2.dstName, ren2.dstSha, ren2.dstMode,
612 branchName1, branchName2)
613
d1745afa 614 if merge or not clean:
46e65174 615 output('Renaming', fmtRename(path, ren1.dstName))
d1745afa 616
c8a4f5e5 617 if merge:
46e65174 618 output('Auto-merging', ren1.dstName)
af215114
FK
619
620 if not clean:
46e65174
FK
621 output('CONFLICT (content): merge conflict in',
622 ren1.dstName)
af215114
FK
623 cleanMerge = False
624
625 if not cacheOnly:
28e77a81
JH
626 setIndexStages(ren1.dstName,
627 ren1.srcSha, ren1.srcMode,
628 ren1.dstSha, ren1.dstMode,
629 ren2.dstSha, ren2.dstMode)
630
af215114
FK
631 updateFile(clean, resSha, resMode, ren1.dstName)
632 else:
633 # Renamed in 1, maybe changed in 2
634 if renamesA == renames1:
635 stage = 3
636 else:
637 stage = 2
638
639 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
640 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
641
642 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
643 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
644
645 tryMerge = False
646
647 if ren1.dstName in currentDirectorySet:
648 newPath = uniquePath(ren1.dstName, branchName1)
46e65174
FK
649 output('CONFLICT (rename/directory): Rename',
650 fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,
651 'directory', ren1.dstName, 'added in', branchName2)
652 output('Renaming', ren1.srcName, 'to', newPath, 'instead')
af215114
FK
653 cleanMerge = False
654 removeFile(False, ren1.dstName)
655 updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
656 elif srcShaOtherBranch == None:
46e65174
FK
657 output('CONFLICT (rename/delete): Rename',
658 fmtRename(ren1.srcName, ren1.dstName), 'in',
659 branchName1, 'and deleted in', branchName2)
af215114
FK
660 cleanMerge = False
661 updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
662 elif dstShaOtherBranch:
663 newPath = uniquePath(ren1.dstName, branchName2)
46e65174
FK
664 output('CONFLICT (rename/add): Rename',
665 fmtRename(ren1.srcName, ren1.dstName), 'in',
666 branchName1 + '.', ren1.dstName, 'added in', branchName2)
667 output('Adding as', newPath, 'instead')
af215114
FK
668 updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
669 cleanMerge = False
670 tryMerge = True
671 elif renames2.getDst(ren1.dstName):
672 dst2 = renames2.getDst(ren1.dstName)
673 newPath1 = uniquePath(ren1.dstName, branchName1)
674 newPath2 = uniquePath(dst2.dstName, branchName2)
46e65174
FK
675 output('CONFLICT (rename/rename): Rename',
676 fmtRename(ren1.srcName, ren1.dstName), 'in',
677 branchName1+'. Rename',
678 fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2)
679 output('Renaming', ren1.srcName, 'to', newPath1, 'and',
680 dst2.srcName, 'to', newPath2, 'instead')
af215114
FK
681 removeFile(False, ren1.dstName)
682 updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
683 updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
684 dst2.processed = True
685 cleanMerge = False
686 else:
687 tryMerge = True
720d150c 688
af215114 689 if tryMerge:
28e77a81
JH
690
691 oName, oSHA1, oMode = ren1.srcName, ren1.srcSha, ren1.srcMode
692 aName, bName = ren1.dstName, ren1.srcName
693 aSHA1, bSHA1 = ren1.dstSha, srcShaOtherBranch
694 aMode, bMode = ren1.dstMode, srcModeOtherBranch
695 aBranch, bBranch = branchName1, branchName2
696
697 if renamesA != renames1:
698 aName, bName = bName, aName
699 aSHA1, bSHA1 = bSHA1, aSHA1
700 aMode, bMode = bMode, aMode
701 aBranch, bBranch = bBranch, aBranch
702
af215114 703 [resSha, resMode, clean, merge] = \
28e77a81
JH
704 mergeFile(oName, oSHA1, oMode,
705 aName, aSHA1, aMode,
706 bName, bSHA1, bMode,
707 aBranch, bBranch);
720d150c 708
d1745afa 709 if merge or not clean:
46e65174 710 output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
d1745afa 711
c8a4f5e5 712 if merge:
46e65174 713 output('Auto-merging', ren1.dstName)
d9a23fa6 714
af215114 715 if not clean:
46e65174
FK
716 output('CONFLICT (rename/modify): Merge conflict in',
717 ren1.dstName)
af215114 718 cleanMerge = False
720d150c 719
af215114 720 if not cacheOnly:
28e77a81
JH
721 setIndexStages(ren1.dstName,
722 oSHA1, oMode,
723 aSHA1, aMode,
724 bSHA1, bMode)
725
af215114 726 updateFile(clean, resSha, resMode, ren1.dstName)
720d150c 727
af215114 728 return cleanMerge
720d150c 729
af215114
FK
730# Per entry merge function
731# ------------------------
720d150c 732
af215114
FK
733def processEntry(entry, branch1Name, branch2Name):
734 '''Merge one cache entry.'''
735
736 debug('processing', entry.path, 'clean cache:', cacheOnly)
720d150c
JH
737
738 cleanMerge = True
739
740 path = entry.path
af215114
FK
741 oSha = entry.stages[1].sha1
742 oMode = entry.stages[1].mode
743 aSha = entry.stages[2].sha1
744 aMode = entry.stages[2].mode
745 bSha = entry.stages[3].sha1
746 bMode = entry.stages[3].mode
720d150c
JH
747
748 assert(oSha == None or isSha(oSha))
749 assert(aSha == None or isSha(aSha))
750 assert(bSha == None or isSha(bSha))
751
752 assert(oMode == None or type(oMode) is int)
753 assert(aMode == None or type(aMode) is int)
754 assert(bMode == None or type(bMode) is int)
755
756 if (oSha and (not aSha or not bSha)):
757 #
758 # Case A: Deleted in one
759 #
760 if (not aSha and not bSha) or \
761 (aSha == oSha and not bSha) or \
762 (not aSha and bSha == oSha):
763 # Deleted in both or deleted in one and unchanged in the other
764 if aSha:
46e65174 765 output('Removing', path)
720d150c
JH
766 removeFile(True, path)
767 else:
768 # Deleted in one and changed in the other
769 cleanMerge = False
770 if not aSha:
46e65174
FK
771 output('CONFLICT (delete/modify):', path, 'deleted in',
772 branch1Name, 'and modified in', branch2Name + '.',
773 'Version', branch2Name, 'of', path, 'left in tree.')
720d150c
JH
774 mode = bMode
775 sha = bSha
776 else:
46e65174
FK
777 output('CONFLICT (modify/delete):', path, 'deleted in',
778 branch2Name, 'and modified in', branch1Name + '.',
779 'Version', branch1Name, 'of', path, 'left in tree.')
720d150c
JH
780 mode = aMode
781 sha = aSha
782
783 updateFile(False, sha, mode, path)
af215114 784
720d150c
JH
785 elif (not oSha and aSha and not bSha) or \
786 (not oSha and not aSha and bSha):
787 #
788 # Case B: Added in one.
789 #
790 if aSha:
791 addBranch = branch1Name
792 otherBranch = branch2Name
793 mode = aMode
794 sha = aSha
af215114 795 conf = 'file/directory'
720d150c
JH
796 else:
797 addBranch = branch2Name
798 otherBranch = branch1Name
799 mode = bMode
800 sha = bSha
af215114 801 conf = 'directory/file'
720d150c 802
af215114 803 if path in currentDirectorySet:
720d150c
JH
804 cleanMerge = False
805 newPath = uniquePath(path, addBranch)
46e65174
FK
806 output('CONFLICT (' + conf + '):',
807 'There is a directory with name', path, 'in',
808 otherBranch + '. Adding', path, 'as', newPath)
720d150c
JH
809
810 removeFile(False, path)
af215114 811 updateFile(False, sha, mode, newPath)
720d150c 812 else:
46e65174 813 output('Adding', path)
af215114 814 updateFile(True, sha, mode, path)
720d150c
JH
815
816 elif not oSha and aSha and bSha:
817 #
818 # Case C: Added in both (check for same permissions).
819 #
820 if aSha == bSha:
821 if aMode != bMode:
822 cleanMerge = False
46e65174
FK
823 output('CONFLICT: File', path,
824 'added identically in both branches, but permissions',
825 'conflict', '0%o' % aMode, '->', '0%o' % bMode)
826 output('CONFLICT: adding with permission:', '0%o' % aMode)
720d150c
JH
827
828 updateFile(False, aSha, aMode, path)
829 else:
830 # This case is handled by git-read-tree
831 assert(False)
832 else:
833 cleanMerge = False
834 newPath1 = uniquePath(path, branch1Name)
835 newPath2 = uniquePath(path, branch2Name)
46e65174
FK
836 output('CONFLICT (add/add): File', path,
837 'added non-identically in both branches. Adding as',
838 newPath1, 'and', newPath2, 'instead.')
720d150c
JH
839 removeFile(False, path)
840 updateFile(False, aSha, aMode, newPath1)
841 updateFile(False, bSha, bMode, newPath2)
842
843 elif oSha and aSha and bSha:
844 #
845 # case D: Modified in both, but differently.
846 #
46e65174 847 output('Auto-merging', path)
af215114
FK
848 [sha, mode, clean, dummy] = \
849 mergeFile(path, oSha, oMode,
850 path, aSha, aMode,
851 path, bSha, bMode,
852 branch1Name, branch2Name)
853 if clean:
854 updateFile(True, sha, mode, path)
720d150c 855 else:
720d150c 856 cleanMerge = False
46e65174 857 output('CONFLICT (content): Merge conflict in', path)
d9a23fa6 858
af215114 859 if cacheOnly:
d9a23fa6
FK
860 updateFile(False, sha, mode, path)
861 else:
af215114 862 updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
720d150c 863 else:
654291a2 864 die("ERROR: Fatal merge failure, shouldn't happen.")
720d150c
JH
865
866 return cleanMerge
867
868def usage():
654291a2 869 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
720d150c
JH
870
871# main entry point as merge strategy module
872# The first parameters up to -- are merge bases, and the rest are heads.
873# This strategy module figures out merge bases itself, so we only
874# get heads.
875
206e587c
FK
876if len(sys.argv) < 4:
877 usage()
878
720d150c
JH
879for nextArg in xrange(1, len(sys.argv)):
880 if sys.argv[nextArg] == '--':
881 if len(sys.argv) != nextArg + 3:
654291a2 882 die('Not handling anything other than two heads merge.')
720d150c
JH
883 try:
884 h1 = firstBranch = sys.argv[nextArg + 1]
885 h2 = secondBranch = sys.argv[nextArg + 2]
af215114 886 except IndexError:
720d150c
JH
887 usage()
888 break
889
890print 'Merging', h1, 'with', h2
720d150c 891
ace36858
FK
892try:
893 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
894 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
720d150c 895
ace36858 896 graph = buildGraph([h1, h2])
720d150c 897
af215114
FK
898 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
899 firstBranch, secondBranch, graph)
ace36858
FK
900
901 print ''
902except:
0bed1899
FK
903 if isinstance(sys.exc_info()[1], SystemExit):
904 raise
905 else:
906 traceback.print_exc(None, sys.stderr)
907 sys.exit(2)
720d150c
JH
908
909if clean:
910 sys.exit(0)
911else:
720d150c 912 sys.exit(1)