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