]> git.ipfire.org Git - thirdparty/git.git/blame - git-merge-recursive.py
Require tk 8.4 (RPM)
[thirdparty/git.git] / git-merge-recursive.py
CommitLineData
720d150c
JH
1#!/usr/bin/python
2
ace36858 3import sys, math, random, os, re, signal, tempfile, stat, errno, traceback
720d150c
JH
4from heapq import heappush, heappop
5from sets import Set
6
7sys.path.append('@@GIT_PYTHON_PATH@@')
8from gitMergeCommon import *
9
720d150c
JH
10# The actual merge code
11# ---------------------
12
6511cce2
FK
13originalIndexFile = os.environ.get('GIT_INDEX_FILE',
14 os.environ.get('GIT_DIR', '.git') + '/index')
15temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
16 '/merge-recursive-tmp-index'
17def setupIndex(temporary):
18 try:
19 os.unlink(temporaryIndexFile)
20 except OSError:
21 pass
22 if temporary:
23 newIndex = temporaryIndexFile
24 os.environ
25 else:
26 newIndex = originalIndexFile
27 os.environ['GIT_INDEX_FILE'] = newIndex
28
720d150c
JH
29def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
30 '''Merge the commits h1 and h2, return the resulting virtual
31 commit object and a flag indicating the cleaness of the merge.'''
32 assert(isinstance(h1, Commit) and isinstance(h2, Commit))
33 assert(isinstance(graph, Graph))
34
35 def infoMsg(*args):
36 sys.stdout.write(' '*callDepth)
37 printList(args)
38 infoMsg('Merging:')
39 infoMsg(h1)
40 infoMsg(h2)
41 sys.stdout.flush()
42
43 ca = getCommonAncestors(graph, h1, h2)
44 infoMsg('found', len(ca), 'common ancestor(s):')
45 for x in ca:
46 infoMsg(x)
47 sys.stdout.flush()
48
49 Ms = ca[0]
50 for h in ca[1:]:
51 [Ms, ignore] = merge(Ms, h,
52 'Temporary shared merge branch 1',
53 'Temporary shared merge branch 2',
54 graph, callDepth+1)
55 assert(isinstance(Ms, Commit))
56
57 if callDepth == 0:
6511cce2 58 setupIndex(False)
d9a23fa6 59 cleanCache = False
720d150c 60 else:
6511cce2 61 setupIndex(True)
720d150c 62 runProgram(['git-read-tree', h1.tree()])
720d150c
JH
63 cleanCache = True
64
65 [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
66 branch1Name, branch2Name,
d9a23fa6 67 cleanCache)
720d150c 68
8ceba720 69 if clean or cleanCache:
720d150c
JH
70 res = Commit(None, [h1, h2], tree=shaRes)
71 graph.addNode(res)
72 else:
73 res = None
74
75 return [res, clean]
76
74376a68 77getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S)
720d150c
JH
78def getFilesAndDirs(tree):
79 files = Set()
80 dirs = Set()
81 out = runProgram(['git-ls-tree', '-r', '-z', tree])
82 for l in out.split('\0'):
83 m = getFilesRE.match(l)
84 if m:
85 if m.group(2) == 'tree':
86 dirs.add(m.group(4))
87 elif m.group(2) == 'blob':
88 files.add(m.group(4))
89
90 return [files, dirs]
91
92class CacheEntry:
93 def __init__(self, path):
94 class Stage:
95 def __init__(self):
96 self.sha1 = None
97 self.mode = None
98
99 self.stages = [Stage(), Stage(), Stage()]
100 self.path = path
101
74376a68 102unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
720d150c
JH
103def unmergedCacheEntries():
104 '''Create a dictionary mapping file names to CacheEntry
105 objects. The dictionary contains one entry for every path with a
106 non-zero stage entry.'''
107
108 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
109 lines.pop()
110
111 res = {}
112 for l in lines:
113 m = unmergedRE.match(l)
114 if m:
115 mode = int(m.group(1), 8)
116 sha1 = m.group(2)
117 stage = int(m.group(3)) - 1
118 path = m.group(4)
119
120 if res.has_key(path):
121 e = res[path]
122 else:
123 e = CacheEntry(path)
124 res[path] = e
125
126 e.stages[stage].mode = mode
127 e.stages[stage].sha1 = sha1
128 else:
654291a2
FK
129 die('Error: Merge program failed: Unexpected output from', \
130 'git-ls-files:', l)
720d150c
JH
131 return res
132
133def mergeTrees(head, merge, common, branch1Name, branch2Name,
d9a23fa6 134 cleanCache):
720d150c
JH
135 '''Merge the trees 'head' and 'merge' with the common ancestor
136 'common'. The name of the head branch is 'branch1Name' and the name of
137 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
138 where tree is the resulting tree and cleanMerge is True iff the
139 merge was clean.'''
140
141 assert(isSha(head) and isSha(merge) and isSha(common))
142
143 if common == merge:
144 print 'Already uptodate!'
145 return [head, True]
146
d9a23fa6 147 if cleanCache:
720d150c 148 updateArg = '-i'
d9a23fa6
FK
149 else:
150 updateArg = '-u'
151
0bed1899
FK
152 [out, code] = runProgram(['git-read-tree', updateArg, '-m', common, head, merge], returnCode = True)
153 if code != 0:
154 die('git-read-tree:', out)
155
720d150c
JH
156 cleanMerge = True
157
158 [tree, code] = runProgram('git-write-tree', returnCode=True)
159 tree = tree.rstrip()
160 if code != 0:
161 [files, dirs] = getFilesAndDirs(head)
162 [filesM, dirsM] = getFilesAndDirs(merge)
163 files.union_update(filesM)
164 dirs.union_update(dirsM)
165
166 cleanMerge = True
167 entries = unmergedCacheEntries()
168 for name in entries:
169 if not processEntry(entries[name], branch1Name, branch2Name,
d9a23fa6 170 files, dirs, cleanCache):
720d150c
JH
171 cleanMerge = False
172
173 if cleanMerge or cleanCache:
174 tree = runProgram('git-write-tree').rstrip()
175 else:
176 tree = None
177 else:
178 cleanMerge = True
179
180 return [tree, cleanMerge]
181
d9a23fa6 182def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
720d150c
JH
183 '''Merge one cache entry. 'files' is a Set with the files in both of
184 the heads that we are going to merge. 'dirs' contains the
185 corresponding data for directories. If 'cleanCache' is True no
186 non-zero stages will be left in the cache for the path
187 corresponding to the entry 'entry'.'''
188
d9a23fa6
FK
189# cleanCache == True => Don't leave any non-stage 0 entries in the cache and
190# don't update the working directory
191# False => Leave unmerged entries and update the working directory
720d150c
JH
192
193# clean == True => non-conflict case
194# False => conflict case
195
196# If cleanCache == False then the cache shouldn't be updated if clean == False
197
d9a23fa6
FK
198 def updateFile(clean, sha, mode, path, onlyWd=False):
199 updateCache = not onlyWd and (cleanCache or (not cleanCache and clean))
200 updateWd = onlyWd or (not cleanCache and clean)
720d150c
JH
201
202 if updateWd:
203 prog = ['git-cat-file', 'blob', sha]
204 if stat.S_ISREG(mode):
205 try:
206 os.unlink(path)
207 except OSError:
208 pass
209 if mode & 0100:
210 mode = 0777
211 else:
212 mode = 0666
213 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
214 proc = subprocess.Popen(prog, stdout=fd)
215 proc.wait()
216 os.close(fd)
217 elif stat.S_ISLNK(mode):
218 linkTarget = runProgram(prog)
219 os.symlink(linkTarget, path)
220 else:
221 assert(False)
d9a23fa6
FK
222
223 if updateWd and updateCache:
87a71b65 224 runProgram(['git-update-index', '--add', '--', path])
d9a23fa6 225 elif updateCache:
87a71b65 226 runProgram(['git-update-index', '--add', '--cacheinfo',
d9a23fa6 227 '0%o' % mode, sha, path])
720d150c
JH
228
229 def removeFile(clean, path):
230 if cleanCache or (not cleanCache and clean):
87a71b65 231 runProgram(['git-update-index', '--force-remove', '--', path])
720d150c 232
d9a23fa6 233 if not cleanCache and clean:
720d150c
JH
234 try:
235 os.unlink(path)
236 except OSError, e:
237 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
238 raise
239
240 def uniquePath(path, branch):
241 newPath = path + '_' + branch
242 suffix = 0
243 while newPath in files or newPath in dirs:
244 suffix += 1
245 newPath = path + '_' + branch + '_' + str(suffix)
246 files.add(newPath)
247 return newPath
248
d9a23fa6 249 debug('processing', entry.path, 'clean cache:', cleanCache)
720d150c
JH
250
251 cleanMerge = True
252
253 path = entry.path
254 oSha = entry.stages[0].sha1
255 oMode = entry.stages[0].mode
256 aSha = entry.stages[1].sha1
257 aMode = entry.stages[1].mode
258 bSha = entry.stages[2].sha1
259 bMode = entry.stages[2].mode
260
261 assert(oSha == None or isSha(oSha))
262 assert(aSha == None or isSha(aSha))
263 assert(bSha == None or isSha(bSha))
264
265 assert(oMode == None or type(oMode) is int)
266 assert(aMode == None or type(aMode) is int)
267 assert(bMode == None or type(bMode) is int)
268
269 if (oSha and (not aSha or not bSha)):
270 #
271 # Case A: Deleted in one
272 #
273 if (not aSha and not bSha) or \
274 (aSha == oSha and not bSha) or \
275 (not aSha and bSha == oSha):
276 # Deleted in both or deleted in one and unchanged in the other
277 if aSha:
278 print 'Removing ' + path
279 removeFile(True, path)
280 else:
281 # Deleted in one and changed in the other
282 cleanMerge = False
283 if not aSha:
284 print 'CONFLICT (del/mod): "' + path + '" deleted in', \
285 branch1Name, 'and modified in', branch2Name, \
286 '. Version', branch2Name, ' of "' + path + \
287 '" left in tree'
288 mode = bMode
289 sha = bSha
290 else:
291 print 'CONFLICT (mod/del): "' + path + '" deleted in', \
292 branch2Name, 'and modified in', branch1Name + \
293 '. Version', branch1Name, 'of "' + path + \
294 '" left in tree'
295 mode = aMode
296 sha = aSha
297
298 updateFile(False, sha, mode, path)
299
300 elif (not oSha and aSha and not bSha) or \
301 (not oSha and not aSha and bSha):
302 #
303 # Case B: Added in one.
304 #
305 if aSha:
306 addBranch = branch1Name
307 otherBranch = branch2Name
308 mode = aMode
309 sha = aSha
310 conf = 'file/dir'
311 else:
312 addBranch = branch2Name
313 otherBranch = branch1Name
314 mode = bMode
315 sha = bSha
316 conf = 'dir/file'
317
318 if path in dirs:
319 cleanMerge = False
320 newPath = uniquePath(path, addBranch)
321 print 'CONFLICT (' + conf + \
322 '): There is a directory with name "' + path + '" in', \
323 otherBranch + '. Adding "' + path + '" as "' + newPath + '"'
324
325 removeFile(False, path)
326 path = newPath
327 else:
328 print 'Adding "' + path + '"'
329
330 updateFile(True, sha, mode, path)
331
332 elif not oSha and aSha and bSha:
333 #
334 # Case C: Added in both (check for same permissions).
335 #
336 if aSha == bSha:
337 if aMode != bMode:
338 cleanMerge = False
339 print 'CONFLICT: File "' + path + \
d9a23fa6
FK
340 '" added identically in both branches,', \
341 'but permissions conflict', '0%o' % aMode, '->', \
342 '0%o' % bMode
720d150c
JH
343 print 'CONFLICT: adding with permission:', '0%o' % aMode
344
345 updateFile(False, aSha, aMode, path)
346 else:
347 # This case is handled by git-read-tree
348 assert(False)
349 else:
350 cleanMerge = False
351 newPath1 = uniquePath(path, branch1Name)
352 newPath2 = uniquePath(path, branch2Name)
353 print 'CONFLICT (add/add): File "' + path + \
d9a23fa6 354 '" added non-identically in both branches.'
720d150c
JH
355 removeFile(False, path)
356 updateFile(False, aSha, aMode, newPath1)
357 updateFile(False, bSha, bMode, newPath2)
358
359 elif oSha and aSha and bSha:
360 #
361 # case D: Modified in both, but differently.
362 #
363 print 'Auto-merging', path
364 orig = runProgram(['git-unpack-file', oSha]).rstrip()
365 src1 = runProgram(['git-unpack-file', aSha]).rstrip()
366 src2 = runProgram(['git-unpack-file', bSha]).rstrip()
367 [out, ret] = runProgram(['merge',
368 '-L', branch1Name + '/' + path,
369 '-L', 'orig/' + path,
370 '-L', branch2Name + '/' + path,
371 src1, orig, src2], returnCode=True)
372
373 if aMode == oMode:
374 mode = bMode
375 else:
376 mode = aMode
377
378 sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
379 src1]).rstrip()
380
381 if ret != 0:
382 cleanMerge = False
383 print 'CONFLICT (content): Merge conflict in "' + path + '".'
d9a23fa6
FK
384
385 if cleanCache:
386 updateFile(False, sha, mode, path)
387 else:
388 updateFile(True, aSha, aMode, path)
389 updateFile(False, sha, mode, path, True)
720d150c
JH
390 else:
391 updateFile(True, sha, mode, path)
392
393 os.unlink(orig)
394 os.unlink(src1)
395 os.unlink(src2)
396 else:
654291a2 397 die("ERROR: Fatal merge failure, shouldn't happen.")
720d150c
JH
398
399 return cleanMerge
400
401def usage():
654291a2 402 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
720d150c
JH
403
404# main entry point as merge strategy module
405# The first parameters up to -- are merge bases, and the rest are heads.
406# This strategy module figures out merge bases itself, so we only
407# get heads.
408
206e587c
FK
409if len(sys.argv) < 4:
410 usage()
411
720d150c
JH
412for nextArg in xrange(1, len(sys.argv)):
413 if sys.argv[nextArg] == '--':
414 if len(sys.argv) != nextArg + 3:
654291a2 415 die('Not handling anything other than two heads merge.')
720d150c
JH
416 try:
417 h1 = firstBranch = sys.argv[nextArg + 1]
418 h2 = secondBranch = sys.argv[nextArg + 2]
419 except IndexError:
420 usage()
421 break
422
423print 'Merging', h1, 'with', h2
720d150c 424
ace36858
FK
425try:
426 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
427 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
720d150c 428
ace36858 429 graph = buildGraph([h1, h2])
720d150c 430
ace36858
FK
431 [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
432 firstBranch, secondBranch, graph)
433
434 print ''
435except:
0bed1899
FK
436 if isinstance(sys.exc_info()[1], SystemExit):
437 raise
438 else:
439 traceback.print_exc(None, sys.stderr)
440 sys.exit(2)
720d150c
JH
441
442if clean:
443 sys.exit(0)
444else:
720d150c 445 sys.exit(1)