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