]>
Commit | Line | Data |
---|---|---|
720d150c JH |
1 | #!/usr/bin/python |
2 | ||
ace36858 | 3 | import sys, math, random, os, re, signal, tempfile, stat, errno, traceback |
720d150c JH |
4 | from heapq import heappush, heappop |
5 | from sets import Set | |
6 | ||
7 | sys.path.append('@@GIT_PYTHON_PATH@@') | |
8 | from gitMergeCommon import * | |
9 | ||
6511cce2 FK |
10 | originalIndexFile = os.environ.get('GIT_INDEX_FILE', |
11 | os.environ.get('GIT_DIR', '.git') + '/index') | |
12 | temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \ | |
13 | '/merge-recursive-tmp-index' | |
14 | def 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 | ||
33 | cacheOnly = False | |
34 | ||
35 | # The entry point to the merge code | |
36 | # --------------------------------- | |
37 | ||
720d150c JH |
38 | def 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 | 87 | getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S) |
720d150c JH |
88 | def 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. | |
106 | currentFileSet = None | |
107 | currentDirectorySet = None | |
108 | ||
109 | def 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 | ||
165 | def 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 | ||
221 | def updateFile(clean, sha, mode, path): | |
222 | updateCache = cacheOnly or clean | |
223 | updateWd = not cacheOnly | |
224 | ||
225 | return updateFileExt(sha, mode, path, updateCache, updateWd) | |
226 | ||
227 | def 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 | ||
273 | def 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 | ||
287 | def 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 |
311 | class 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 |
338 | class 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 | 351 | unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S) |
720d150c JH |
352 | def 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 |
381 | lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S) |
382 | def 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 | ||
416 | class 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 | ||
433 | class 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 | ||
451 | parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$') | |
452 | def 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 | ||
499 | def 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 |
518 | def 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 |
680 | def 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 | ||
817 | def 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 |
825 | if len(sys.argv) < 4: |
826 | usage() | |
827 | ||
720d150c JH |
828 | for 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 | ||
839 | print 'Merging', h1, 'with', h2 | |
720d150c | 840 | |
ace36858 FK |
841 | try: |
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 '' | |
851 | except: | |
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 | |
858 | if clean: | |
859 | sys.exit(0) | |
860 | else: | |
720d150c | 861 | sys.exit(1) |