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