##// END OF EJS Templates
filemerge: pull file-merging code into its own module
Matt Mackall -
r6003:7855b88b default
parent child Browse files
Show More
@@ -0,0 +1,65 b''
1 # filemerge.py - file-level merge handling for Mercurial
2 #
3 # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
7
8 from node import *
9 from i18n import _
10 import util, os, tempfile, context
11
12 def filemerge(repo, fw, fd, fo, wctx, mctx):
13 """perform a 3-way merge in the working directory
14
15 fw = original filename in the working directory
16 fd = destination filename in the working directory
17 fo = filename in other parent
18 wctx, mctx = working and merge changecontexts
19 """
20
21 def temp(prefix, ctx):
22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 data = repo.wwritedata(ctx.path(), ctx.data())
25 f = os.fdopen(fd, "wb")
26 f.write(data)
27 f.close()
28 return name
29
30 fcm = wctx.filectx(fw)
31 fcmdata = wctx.filectx(fd).data()
32 fco = mctx.filectx(fo)
33
34 if not fco.cmp(fcmdata): # files identical?
35 return None
36
37 fca = fcm.ancestor(fco)
38 if not fca:
39 fca = repo.filectx(fw, fileid=nullrev)
40 a = repo.wjoin(fd)
41 b = temp("base", fca)
42 c = temp("other", fco)
43
44 if fw != fo:
45 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
46 else:
47 repo.ui.status(_("merging %s\n") % fw)
48
49 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
50
51 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
52 or "hgmerge")
53 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
54 environ={'HG_FILE': fd,
55 'HG_MY_NODE': str(wctx.parents()[0]),
56 'HG_OTHER_NODE': str(mctx),
57 'HG_MY_ISLINK': fcm.islink(),
58 'HG_OTHER_ISLINK': fco.islink(),
59 'HG_BASE_ISLINK': fca.islink(),})
60 if r:
61 repo.ui.warn(_("merging %s failed!\n") % fd)
62
63 os.unlink(b)
64 os.unlink(c)
65 return r
@@ -1,405 +1,406 b''
1 1 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
2 2 # Published under the GNU GPL
3 3
4 4 '''
5 5 imerge - interactive merge
6 6 '''
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial.node import *
10 from mercurial import commands, cmdutil, dispatch, fancyopts, hg, merge, util
10 from mercurial import commands, cmdutil, dispatch, fancyopts
11 from mercurial import hg, filemerge, util
11 12 import os, tarfile
12 13
13 14 class InvalidStateFileException(Exception): pass
14 15
15 16 class ImergeStateFile(object):
16 17 def __init__(self, im):
17 18 self.im = im
18 19
19 20 def save(self, dest):
20 21 tf = tarfile.open(dest, 'w:gz')
21 22
22 23 st = os.path.join(self.im.path, 'status')
23 24 tf.add(st, os.path.join('.hg', 'imerge', 'status'))
24 25
25 26 for f in self.im.resolved:
26 27 (fd, fo) = self.im.conflicts[f]
27 28 abssrc = self.im.repo.wjoin(fd)
28 29 tf.add(abssrc, fd)
29 30
30 31 tf.close()
31 32
32 33 def load(self, source):
33 34 wlock = self.im.repo.wlock()
34 35 lock = self.im.repo.lock()
35 36
36 37 tf = tarfile.open(source, 'r')
37 38 contents = tf.getnames()
38 39 # tarfile normalizes path separators to '/'
39 40 statusfile = '.hg/imerge/status'
40 41 if statusfile not in contents:
41 42 raise InvalidStateFileException('no status file')
42 43
43 44 tf.extract(statusfile, self.im.repo.root)
44 45 p1, p2 = self.im.load()
45 46 if self.im.repo.dirstate.parents()[0] != p1.node():
46 47 hg.clean(self.im.repo, p1.node())
47 48 self.im.start(p2.node())
48 49 for tarinfo in tf:
49 50 tf.extract(tarinfo, self.im.repo.root)
50 51 self.im.load()
51 52
52 53 class Imerge(object):
53 54 def __init__(self, ui, repo):
54 55 self.ui = ui
55 56 self.repo = repo
56 57
57 58 self.path = repo.join('imerge')
58 59 self.opener = util.opener(self.path)
59 60
60 61 self.wctx = self.repo.workingctx()
61 62 self.conflicts = {}
62 63 self.resolved = []
63 64
64 65 def merging(self):
65 66 return len(self.wctx.parents()) > 1
66 67
67 68 def load(self):
68 69 # status format. \0-delimited file, fields are
69 70 # p1, p2, conflict count, conflict filenames, resolved filenames
70 71 # conflict filenames are tuples of localname, remoteorig, remotenew
71 72
72 73 statusfile = self.opener('status')
73 74
74 75 status = statusfile.read().split('\0')
75 76 if len(status) < 3:
76 77 raise util.Abort('invalid imerge status file')
77 78
78 79 try:
79 80 parents = [self.repo.changectx(n) for n in status[:2]]
80 81 except LookupError:
81 82 raise util.Abort('merge parent %s not in repository' % short(p))
82 83
83 84 status = status[2:]
84 85 conflicts = int(status.pop(0)) * 3
85 86 self.resolved = status[conflicts:]
86 87 for i in xrange(0, conflicts, 3):
87 88 self.conflicts[status[i]] = (status[i+1], status[i+2])
88 89
89 90 return parents
90 91
91 92 def save(self):
92 93 lock = self.repo.lock()
93 94
94 95 if not os.path.isdir(self.path):
95 96 os.mkdir(self.path)
96 97 statusfile = self.opener('status', 'wb')
97 98
98 99 out = [hex(n.node()) for n in self.wctx.parents()]
99 100 out.append(str(len(self.conflicts)))
100 101 conflicts = self.conflicts.items()
101 102 conflicts.sort()
102 103 for fw, fd_fo in conflicts:
103 104 out.append(fw)
104 105 out.extend(fd_fo)
105 106 out.extend(self.resolved)
106 107
107 108 statusfile.write('\0'.join(out))
108 109
109 110 def remaining(self):
110 111 return [f for f in self.conflicts if f not in self.resolved]
111 112
112 113 def filemerge(self, fn, interactive=True):
113 114 wlock = self.repo.wlock()
114 115
115 116 (fd, fo) = self.conflicts[fn]
116 117 p1, p2 = self.wctx.parents()
117 118
118 119 # this could be greatly improved
119 120 realmerge = os.environ.get('HGMERGE')
120 121 if not interactive:
121 122 os.environ['HGMERGE'] = 'merge'
122 123
123 124 # The filemerge ancestor algorithm does not work if self.wctx
124 125 # already has two parents (in normal merge it doesn't yet). But
125 126 # this is very dirty.
126 127 self.wctx._parents.pop()
127 128 try:
128 129 # TODO: we should probably revert the file if merge fails
129 return merge.filemerge(self.repo, fn, fd, fo, self.wctx, p2)
130 return filemerge.filemerge(self.repo, fn, fd, fo, self.wctx, p2)
130 131 finally:
131 132 self.wctx._parents.append(p2)
132 133 if realmerge:
133 134 os.environ['HGMERGE'] = realmerge
134 135 elif not interactive:
135 136 del os.environ['HGMERGE']
136 137
137 138 def start(self, rev=None):
138 _filemerge = merge.filemerge
139 def filemerge(repo, fw, fd, fo, wctx, mctx):
139 _filemerge = filemerge.filemerge
140 def filemerge_(repo, fw, fd, fo, wctx, mctx):
140 141 self.conflicts[fw] = (fd, fo)
141 142
142 merge.filemerge = filemerge
143 filemerge.filemerge = filemerge_
143 144 commands.merge(self.ui, self.repo, rev=rev)
144 merge.filemerge = _filemerge
145 filemerge.filemerge = _filemerge
145 146
146 147 self.wctx = self.repo.workingctx()
147 148 self.save()
148 149
149 150 def resume(self):
150 151 self.load()
151 152
152 153 dp = self.repo.dirstate.parents()
153 154 p1, p2 = self.wctx.parents()
154 155 if p1.node() != dp[0] or p2.node() != dp[1]:
155 156 raise util.Abort('imerge state does not match working directory')
156 157
157 158 def next(self):
158 159 remaining = self.remaining()
159 160 return remaining and remaining[0]
160 161
161 162 def resolve(self, files):
162 163 resolved = dict.fromkeys(self.resolved)
163 164 for fn in files:
164 165 if fn not in self.conflicts:
165 166 raise util.Abort('%s is not in the merge set' % fn)
166 167 resolved[fn] = True
167 168 self.resolved = resolved.keys()
168 169 self.resolved.sort()
169 170 self.save()
170 171 return 0
171 172
172 173 def unresolve(self, files):
173 174 resolved = dict.fromkeys(self.resolved)
174 175 for fn in files:
175 176 if fn not in resolved:
176 177 raise util.Abort('%s is not resolved' % fn)
177 178 del resolved[fn]
178 179 self.resolved = resolved.keys()
179 180 self.resolved.sort()
180 181 self.save()
181 182 return 0
182 183
183 184 def pickle(self, dest):
184 185 '''write current merge state to file to be resumed elsewhere'''
185 186 state = ImergeStateFile(self)
186 187 return state.save(dest)
187 188
188 189 def unpickle(self, source):
189 190 '''read merge state from file'''
190 191 state = ImergeStateFile(self)
191 192 return state.load(source)
192 193
193 194 def load(im, source):
194 195 if im.merging():
195 196 raise util.Abort('there is already a merge in progress '
196 197 '(update -C <rev> to abort it)' )
197 198 m, a, r, d = im.repo.status()[:4]
198 199 if m or a or r or d:
199 200 raise util.Abort('working directory has uncommitted changes')
200 201
201 202 rc = im.unpickle(source)
202 203 if not rc:
203 204 status(im)
204 205 return rc
205 206
206 207 def merge_(im, filename=None, auto=False):
207 208 success = True
208 209 if auto and not filename:
209 210 for fn in im.remaining():
210 211 rc = im.filemerge(fn, interactive=False)
211 212 if rc:
212 213 success = False
213 214 else:
214 215 im.resolve([fn])
215 216 if success:
216 217 im.ui.write('all conflicts resolved\n')
217 218 else:
218 219 status(im)
219 220 return 0
220 221
221 222 if not filename:
222 223 filename = im.next()
223 224 if not filename:
224 225 im.ui.write('all conflicts resolved\n')
225 226 return 0
226 227
227 228 rc = im.filemerge(filename, interactive=not auto)
228 229 if not rc:
229 230 im.resolve([filename])
230 231 if not im.next():
231 232 im.ui.write('all conflicts resolved\n')
232 233 return rc
233 234
234 235 def next(im):
235 236 n = im.next()
236 237 if n:
237 238 im.ui.write('%s\n' % n)
238 239 else:
239 240 im.ui.write('all conflicts resolved\n')
240 241 return 0
241 242
242 243 def resolve(im, *files):
243 244 if not files:
244 245 raise util.Abort('resolve requires at least one filename')
245 246 return im.resolve(files)
246 247
247 248 def save(im, dest):
248 249 return im.pickle(dest)
249 250
250 251 def status(im, **opts):
251 252 if not opts.get('resolved') and not opts.get('unresolved'):
252 253 opts['resolved'] = True
253 254 opts['unresolved'] = True
254 255
255 256 if im.ui.verbose:
256 257 p1, p2 = [short(p.node()) for p in im.wctx.parents()]
257 258 im.ui.note(_('merging %s and %s\n') % (p1, p2))
258 259
259 260 conflicts = im.conflicts.keys()
260 261 conflicts.sort()
261 262 remaining = dict.fromkeys(im.remaining())
262 263 st = []
263 264 for fn in conflicts:
264 265 if opts.get('no_status'):
265 266 mode = ''
266 267 elif fn in remaining:
267 268 mode = 'U '
268 269 else:
269 270 mode = 'R '
270 271 if ((opts.get('resolved') and fn not in remaining)
271 272 or (opts.get('unresolved') and fn in remaining)):
272 273 st.append((mode, fn))
273 274 st.sort()
274 275 for (mode, fn) in st:
275 276 if im.ui.verbose:
276 277 fo, fd = im.conflicts[fn]
277 278 if fd != fn:
278 279 fn = '%s (%s)' % (fn, fd)
279 280 im.ui.write('%s%s\n' % (mode, fn))
280 281 if opts.get('unresolved') and not remaining:
281 282 im.ui.write(_('all conflicts resolved\n'))
282 283
283 284 return 0
284 285
285 286 def unresolve(im, *files):
286 287 if not files:
287 288 raise util.Abort('unresolve requires at least one filename')
288 289 return im.unresolve(files)
289 290
290 291 subcmdtable = {
291 292 'load': (load, []),
292 293 'merge':
293 294 (merge_,
294 295 [('a', 'auto', None, _('automatically resolve if possible'))]),
295 296 'next': (next, []),
296 297 'resolve': (resolve, []),
297 298 'save': (save, []),
298 299 'status':
299 300 (status,
300 301 [('n', 'no-status', None, _('hide status prefix')),
301 302 ('', 'resolved', None, _('only show resolved conflicts')),
302 303 ('', 'unresolved', None, _('only show unresolved conflicts'))]),
303 304 'unresolve': (unresolve, [])
304 305 }
305 306
306 307 def dispatch_(im, args, opts):
307 308 def complete(s, choices):
308 309 candidates = []
309 310 for choice in choices:
310 311 if choice.startswith(s):
311 312 candidates.append(choice)
312 313 return candidates
313 314
314 315 c, args = args[0], list(args[1:])
315 316 cmd = complete(c, subcmdtable.keys())
316 317 if not cmd:
317 318 raise cmdutil.UnknownCommand('imerge ' + c)
318 319 if len(cmd) > 1:
319 320 cmd.sort()
320 321 raise cmdutil.AmbiguousCommand('imerge ' + c, cmd)
321 322 cmd = cmd[0]
322 323
323 324 func, optlist = subcmdtable[cmd]
324 325 opts = {}
325 326 try:
326 327 args = fancyopts.fancyopts(args, optlist, opts)
327 328 return func(im, *args, **opts)
328 329 except fancyopts.getopt.GetoptError, inst:
329 330 raise dispatch.ParseError('imerge', '%s: %s' % (cmd, inst))
330 331 except TypeError:
331 332 raise dispatch.ParseError('imerge', _('%s: invalid arguments') % cmd)
332 333
333 334 def imerge(ui, repo, *args, **opts):
334 335 '''interactive merge
335 336
336 337 imerge lets you split a merge into pieces. When you start a merge
337 338 with imerge, the names of all files with conflicts are recorded.
338 339 You can then merge any of these files, and if the merge is
339 340 successful, they will be marked as resolved. When all files are
340 341 resolved, the merge is complete.
341 342
342 343 If no merge is in progress, hg imerge [rev] will merge the working
343 344 directory with rev (defaulting to the other head if the repository
344 345 only has two heads). You may also resume a saved merge with
345 346 hg imerge load <file>.
346 347
347 348 If a merge is in progress, hg imerge will default to merging the
348 349 next unresolved file.
349 350
350 351 The following subcommands are available:
351 352
352 353 status:
353 354 show the current state of the merge
354 355 options:
355 356 -n --no-status: do not print the status prefix
356 357 --resolved: only print resolved conflicts
357 358 --unresolved: only print unresolved conflicts
358 359 next:
359 360 show the next unresolved file merge
360 361 merge [<file>]:
361 362 merge <file>. If the file merge is successful, the file will be
362 363 recorded as resolved. If no file is given, the next unresolved
363 364 file will be merged.
364 365 resolve <file>...:
365 366 mark files as successfully merged
366 367 unresolve <file>...:
367 368 mark files as requiring merging.
368 369 save <file>:
369 370 save the state of the merge to a file to be resumed elsewhere
370 371 load <file>:
371 372 load the state of the merge from a file created by save
372 373 '''
373 374
374 375 im = Imerge(ui, repo)
375 376
376 377 if im.merging():
377 378 im.resume()
378 379 else:
379 380 rev = opts.get('rev')
380 381 if rev and args:
381 382 raise util.Abort('please specify just one revision')
382 383
383 384 if len(args) == 2 and args[0] == 'load':
384 385 pass
385 386 else:
386 387 if args:
387 388 rev = args[0]
388 389 im.start(rev=rev)
389 390 if opts.get('auto'):
390 391 args = ['merge', '--auto']
391 392 else:
392 393 args = ['status']
393 394
394 395 if not args:
395 396 args = ['merge']
396 397
397 398 return dispatch_(im, args, opts)
398 399
399 400 cmdtable = {
400 401 '^imerge':
401 402 (imerge,
402 403 [('r', 'rev', '', _('revision to merge')),
403 404 ('a', 'auto', None, _('automatically merge where possible'))],
404 405 'hg imerge [command]')
405 406 }
@@ -1,687 +1,632 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import _
10 import errno, util, os, tempfile, context, heapq
11
12 def filemerge(repo, fw, fd, fo, wctx, mctx):
13 """perform a 3-way merge in the working directory
14
15 fw = original filename in the working directory
16 fd = destination filename in the working directory
17 fo = filename in other parent
18 wctx, mctx = working and merge changecontexts
19 """
20
21 def temp(prefix, ctx):
22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 data = repo.wwritedata(ctx.path(), ctx.data())
25 f = os.fdopen(fd, "wb")
26 f.write(data)
27 f.close()
28 return name
29
30 fcm = wctx.filectx(fw)
31 fcmdata = wctx.filectx(fd).data()
32 fco = mctx.filectx(fo)
33
34 if not fco.cmp(fcmdata): # files identical?
35 return None
36
37 fca = fcm.ancestor(fco)
38 if not fca:
39 fca = repo.filectx(fw, fileid=nullrev)
40 a = repo.wjoin(fd)
41 b = temp("base", fca)
42 c = temp("other", fco)
43
44 if fw != fo:
45 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
46 else:
47 repo.ui.status(_("merging %s\n") % fw)
48
49 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
50
51 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
52 or "hgmerge")
53 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
54 environ={'HG_FILE': fd,
55 'HG_MY_NODE': str(wctx.parents()[0]),
56 'HG_OTHER_NODE': str(mctx),
57 'HG_MY_ISLINK': fcm.islink(),
58 'HG_OTHER_ISLINK': fco.islink(),
59 'HG_BASE_ISLINK': fca.islink(),})
60 if r:
61 repo.ui.warn(_("merging %s failed!\n") % fd)
62
63 os.unlink(b)
64 os.unlink(c)
65 return r
10 import errno, util, os, heapq, filemerge
66 11
67 12 def checkunknown(wctx, mctx):
68 13 "check for collisions between unknown files and files in mctx"
69 14 man = mctx.manifest()
70 15 for f in wctx.unknown():
71 16 if f in man:
72 17 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
73 18 raise util.Abort(_("untracked file in working directory differs"
74 19 " from file in requested revision: '%s'")
75 20 % f)
76 21
77 22 def checkcollision(mctx):
78 23 "check for case folding collisions in the destination context"
79 24 folded = {}
80 25 for fn in mctx.manifest():
81 26 fold = fn.lower()
82 27 if fold in folded:
83 28 raise util.Abort(_("case-folding collision between %s and %s")
84 29 % (fn, folded[fold]))
85 30 folded[fold] = fn
86 31
87 32 def forgetremoved(wctx, mctx):
88 33 """
89 34 Forget removed files
90 35
91 36 If we're jumping between revisions (as opposed to merging), and if
92 37 neither the working directory nor the target rev has the file,
93 38 then we need to remove it from the dirstate, to prevent the
94 39 dirstate from listing the file when it is no longer in the
95 40 manifest.
96 41 """
97 42
98 43 action = []
99 44 man = mctx.manifest()
100 45 for f in wctx.deleted() + wctx.removed():
101 46 if f not in man:
102 47 action.append((f, "f"))
103 48
104 49 return action
105 50
106 51 def findcopies(repo, m1, m2, ma, limit):
107 52 """
108 53 Find moves and copies between m1 and m2 back to limit linkrev
109 54 """
110 55
111 56 def nonoverlap(d1, d2, d3):
112 57 "Return list of elements in d1 not in d2 or d3"
113 58 l = [d for d in d1 if d not in d3 and d not in d2]
114 59 l.sort()
115 60 return l
116 61
117 62 def dirname(f):
118 63 s = f.rfind("/")
119 64 if s == -1:
120 65 return ""
121 66 return f[:s]
122 67
123 68 def dirs(files):
124 69 d = {}
125 70 for f in files:
126 71 f = dirname(f)
127 72 while f not in d:
128 73 d[f] = True
129 74 f = dirname(f)
130 75 return d
131 76
132 77 wctx = repo.workingctx()
133 78
134 79 def makectx(f, n):
135 80 if len(n) == 20:
136 81 return repo.filectx(f, fileid=n)
137 82 return wctx.filectx(f)
138 83 ctx = util.cachefunc(makectx)
139 84
140 85 def findold(fctx):
141 86 "find files that path was copied from, back to linkrev limit"
142 87 old = {}
143 88 seen = {}
144 89 orig = fctx.path()
145 90 visit = [fctx]
146 91 while visit:
147 92 fc = visit.pop()
148 93 s = str(fc)
149 94 if s in seen:
150 95 continue
151 96 seen[s] = 1
152 97 if fc.path() != orig and fc.path() not in old:
153 98 old[fc.path()] = 1
154 99 if fc.rev() < limit:
155 100 continue
156 101 visit += fc.parents()
157 102
158 103 old = old.keys()
159 104 old.sort()
160 105 return old
161 106
162 107 copy = {}
163 108 fullcopy = {}
164 109 diverge = {}
165 110
166 111 def checkcopies(c, man, aman):
167 112 '''check possible copies for filectx c'''
168 113 for of in findold(c):
169 114 fullcopy[c.path()] = of # remember for dir rename detection
170 115 if of not in man: # original file not in other manifest?
171 116 if of in ma:
172 117 diverge.setdefault(of, []).append(c.path())
173 118 continue
174 119 # if the original file is unchanged on the other branch,
175 120 # no merge needed
176 121 if man[of] == aman.get(of):
177 122 continue
178 123 c2 = ctx(of, man[of])
179 124 ca = c.ancestor(c2)
180 125 if not ca: # unrelated?
181 126 continue
182 127 # named changed on only one side?
183 128 if ca.path() == c.path() or ca.path() == c2.path():
184 129 if c == ca and c2 == ca: # no merge needed, ignore copy
185 130 continue
186 131 copy[c.path()] = of
187 132
188 133 if not repo.ui.configbool("merge", "followcopies", True):
189 134 return {}, {}
190 135
191 136 # avoid silly behavior for update from empty dir
192 137 if not m1 or not m2 or not ma:
193 138 return {}, {}
194 139
195 140 repo.ui.debug(_(" searching for copies back to rev %d\n") % limit)
196 141
197 142 u1 = nonoverlap(m1, m2, ma)
198 143 u2 = nonoverlap(m2, m1, ma)
199 144
200 145 if u1:
201 146 repo.ui.debug(_(" unmatched files in local:\n %s\n")
202 147 % "\n ".join(u1))
203 148 if u2:
204 149 repo.ui.debug(_(" unmatched files in other:\n %s\n")
205 150 % "\n ".join(u2))
206 151
207 152 for f in u1:
208 153 checkcopies(ctx(f, m1[f]), m2, ma)
209 154
210 155 for f in u2:
211 156 checkcopies(ctx(f, m2[f]), m1, ma)
212 157
213 158 diverge2 = {}
214 159 for of, fl in diverge.items():
215 160 if len(fl) == 1:
216 161 del diverge[of] # not actually divergent
217 162 else:
218 163 diverge2.update(dict.fromkeys(fl)) # reverse map for below
219 164
220 165 if fullcopy:
221 166 repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n"))
222 167 for f in fullcopy:
223 168 note = ""
224 169 if f in copy: note += "*"
225 170 if f in diverge2: note += "!"
226 171 repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note))
227 172
228 173 del diverge2
229 174
230 175 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
231 176 return copy, diverge
232 177
233 178 repo.ui.debug(_(" checking for directory renames\n"))
234 179
235 180 # generate a directory move map
236 181 d1, d2 = dirs(m1), dirs(m2)
237 182 invalid = {}
238 183 dirmove = {}
239 184
240 185 # examine each file copy for a potential directory move, which is
241 186 # when all the files in a directory are moved to a new directory
242 187 for dst, src in fullcopy.items():
243 188 dsrc, ddst = dirname(src), dirname(dst)
244 189 if dsrc in invalid:
245 190 # already seen to be uninteresting
246 191 continue
247 192 elif dsrc in d1 and ddst in d1:
248 193 # directory wasn't entirely moved locally
249 194 invalid[dsrc] = True
250 195 elif dsrc in d2 and ddst in d2:
251 196 # directory wasn't entirely moved remotely
252 197 invalid[dsrc] = True
253 198 elif dsrc in dirmove and dirmove[dsrc] != ddst:
254 199 # files from the same directory moved to two different places
255 200 invalid[dsrc] = True
256 201 else:
257 202 # looks good so far
258 203 dirmove[dsrc + "/"] = ddst + "/"
259 204
260 205 for i in invalid:
261 206 if i in dirmove:
262 207 del dirmove[i]
263 208
264 209 del d1, d2, invalid
265 210
266 211 if not dirmove:
267 212 return copy, diverge
268 213
269 214 for d in dirmove:
270 215 repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d]))
271 216
272 217 # check unaccounted nonoverlapping files against directory moves
273 218 for f in u1 + u2:
274 219 if f not in fullcopy:
275 220 for d in dirmove:
276 221 if f.startswith(d):
277 222 # new file added in a directory that was moved, move it
278 223 copy[f] = dirmove[d] + f[len(d):]
279 224 repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f]))
280 225 break
281 226
282 227 return copy, diverge
283 228
284 229 def symmetricdifference(repo, rev1, rev2):
285 230 """symmetric difference of the sets of ancestors of rev1 and rev2
286 231
287 232 I.e. revisions that are ancestors of rev1 or rev2, but not both.
288 233 """
289 234 # basic idea:
290 235 # - mark rev1 and rev2 with different colors
291 236 # - walk the graph in topological order with the help of a heap;
292 237 # for each revision r:
293 238 # - if r has only one color, we want to return it
294 239 # - add colors[r] to its parents
295 240 #
296 241 # We keep track of the number of revisions in the heap that
297 242 # we may be interested in. We stop walking the graph as soon
298 243 # as this number reaches 0.
299 244 WHITE = 1
300 245 BLACK = 2
301 246 ALLCOLORS = WHITE | BLACK
302 247 colors = {rev1: WHITE, rev2: BLACK}
303 248
304 249 cl = repo.changelog
305 250
306 251 visit = [-rev1, -rev2]
307 252 heapq.heapify(visit)
308 253 n_wanted = len(visit)
309 254 ret = []
310 255
311 256 while n_wanted:
312 257 r = -heapq.heappop(visit)
313 258 wanted = colors[r] != ALLCOLORS
314 259 n_wanted -= wanted
315 260 if wanted:
316 261 ret.append(r)
317 262
318 263 for p in cl.parentrevs(r):
319 264 if p == nullrev:
320 265 continue
321 266 if p not in colors:
322 267 # first time we see p; add it to visit
323 268 n_wanted += wanted
324 269 colors[p] = colors[r]
325 270 heapq.heappush(visit, -p)
326 271 elif colors[p] != ALLCOLORS and colors[p] != colors[r]:
327 272 # at first we thought we wanted p, but now
328 273 # we know we don't really want it
329 274 n_wanted -= 1
330 275 colors[p] |= colors[r]
331 276
332 277 del colors[r]
333 278
334 279 return ret
335 280
336 281 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
337 282 """
338 283 Merge p1 and p2 with ancestor ma and generate merge action list
339 284
340 285 overwrite = whether we clobber working files
341 286 partial = function to filter file lists
342 287 """
343 288
344 289 repo.ui.note(_("resolving manifests\n"))
345 290 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
346 291 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
347 292
348 293 m1 = p1.manifest()
349 294 m2 = p2.manifest()
350 295 ma = pa.manifest()
351 296 backwards = (pa == p2)
352 297 action = []
353 298 copy = {}
354 299 diverge = {}
355 300
356 301 def fmerge(f, f2=None, fa=None):
357 302 """merge flags"""
358 303 if not f2:
359 304 f2 = f
360 305 fa = f
361 306 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
362 307 if m == n: # flags agree
363 308 return m # unchanged
364 309 if m and n: # flags are set but don't agree
365 310 if not a: # both differ from parent
366 311 r = repo.ui.prompt(
367 312 _(" conflicting flags for %s\n"
368 313 "(n)one, e(x)ec or sym(l)ink?") % f, "[nxl]", "n")
369 314 return r != "n" and r or ''
370 315 if m == a:
371 316 return n # changed from m to n
372 317 return m # changed from n to m
373 318 if m and m != a: # changed from a to m
374 319 return m
375 320 if n and n != a: # changed from a to n
376 321 return n
377 322 return '' # flag was cleared
378 323
379 324 def act(msg, m, f, *args):
380 325 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
381 326 action.append((f, m) + args)
382 327
383 328 if not (backwards or overwrite):
384 329 rev1 = p1.rev()
385 330 if rev1 is None:
386 331 # p1 is a workingctx
387 332 rev1 = p1.parents()[0].rev()
388 333 limit = min(symmetricdifference(repo, rev1, p2.rev()))
389 334 copy, diverge = findcopies(repo, m1, m2, ma, limit)
390 335
391 336 for of, fl in diverge.items():
392 337 act("divergent renames", "dr", of, fl)
393 338
394 339 copied = dict.fromkeys(copy.values())
395 340
396 341 # Compare manifests
397 342 for f, n in m1.iteritems():
398 343 if partial and not partial(f):
399 344 continue
400 345 if f in m2:
401 346 if overwrite or backwards:
402 347 rflags = m2.flags(f)
403 348 else:
404 349 rflags = fmerge(f)
405 350 # are files different?
406 351 if n != m2[f]:
407 352 a = ma.get(f, nullid)
408 353 # are we clobbering?
409 354 if overwrite:
410 355 act("clobbering", "g", f, rflags)
411 356 # or are we going back in time and clean?
412 357 elif backwards and not n[20:]:
413 358 act("reverting", "g", f, rflags)
414 359 # are both different from the ancestor?
415 360 elif n != a and m2[f] != a:
416 361 act("versions differ", "m", f, f, f, rflags, False)
417 362 # is remote's version newer?
418 363 elif m2[f] != a:
419 364 act("remote is newer", "g", f, rflags)
420 365 # local is newer, not overwrite, check mode bits
421 366 elif m1.flags(f) != rflags:
422 367 act("update permissions", "e", f, rflags)
423 368 # contents same, check mode bits
424 369 elif m1.flags(f) != rflags:
425 370 act("update permissions", "e", f, rflags)
426 371 elif f in copied:
427 372 continue
428 373 elif f in copy:
429 374 f2 = copy[f]
430 375 if f2 not in m2: # directory rename
431 376 act("remote renamed directory to " + f2, "d",
432 377 f, None, f2, m1.flags(f))
433 378 elif f2 in m1: # case 2 A,B/B/B
434 379 act("local copied to " + f2, "m",
435 380 f, f2, f, fmerge(f, f2, f2), False)
436 381 else: # case 4,21 A/B/B
437 382 act("local moved to " + f2, "m",
438 383 f, f2, f, fmerge(f, f2, f2), False)
439 384 elif f in ma:
440 385 if n != ma[f] and not overwrite:
441 386 if repo.ui.prompt(
442 387 _(" local changed %s which remote deleted\n"
443 388 "use (c)hanged version or (d)elete?") % f,
444 389 _("[cd]"), _("c")) == _("d"):
445 390 act("prompt delete", "r", f)
446 391 else:
447 392 act("other deleted", "r", f)
448 393 else:
449 394 # file is created on branch or in working directory
450 395 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
451 396 act("remote deleted", "r", f)
452 397
453 398 for f, n in m2.iteritems():
454 399 if partial and not partial(f):
455 400 continue
456 401 if f in m1:
457 402 continue
458 403 if f in copied:
459 404 continue
460 405 if f in copy:
461 406 f2 = copy[f]
462 407 if f2 not in m1: # directory rename
463 408 act("local renamed directory to " + f2, "d",
464 409 None, f, f2, m2.flags(f))
465 410 elif f2 in m2: # rename case 1, A/A,B/A
466 411 act("remote copied to " + f, "m",
467 412 f2, f, f, fmerge(f2, f, f2), False)
468 413 else: # case 3,20 A/B/A
469 414 act("remote moved to " + f, "m",
470 415 f2, f, f, fmerge(f2, f, f2), True)
471 416 elif f in ma:
472 417 if overwrite or backwards:
473 418 act("recreating", "g", f, m2.flags(f))
474 419 elif n != ma[f]:
475 420 if repo.ui.prompt(
476 421 _("remote changed %s which local deleted\n"
477 422 "use (c)hanged version or leave (d)eleted?") % f,
478 423 _("[cd]"), _("c")) == _("c"):
479 424 act("prompt recreating", "g", f, m2.flags(f))
480 425 else:
481 426 act("remote created", "g", f, m2.flags(f))
482 427
483 428 return action
484 429
485 430 def applyupdates(repo, action, wctx, mctx):
486 431 "apply the merge action list to the working directory"
487 432
488 433 updated, merged, removed, unresolved = 0, 0, 0, 0
489 434 action.sort()
490 435 # prescan for copy/renames
491 436 for a in action:
492 437 f, m = a[:2]
493 438 if m == 'm': # merge
494 439 f2, fd, flags, move = a[2:]
495 440 if f != fd:
496 441 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
497 442 repo.wwrite(fd, repo.wread(f), flags)
498 443
499 444 audit_path = util.path_auditor(repo.root)
500 445
501 446 for a in action:
502 447 f, m = a[:2]
503 448 if f and f[0] == "/":
504 449 continue
505 450 if m == "r": # remove
506 451 repo.ui.note(_("removing %s\n") % f)
507 452 audit_path(f)
508 453 try:
509 454 util.unlink(repo.wjoin(f))
510 455 except OSError, inst:
511 456 if inst.errno != errno.ENOENT:
512 457 repo.ui.warn(_("update failed to remove %s: %s!\n") %
513 458 (f, inst.strerror))
514 459 removed += 1
515 460 elif m == "m": # merge
516 461 f2, fd, flags, move = a[2:]
517 r = filemerge(repo, f, fd, f2, wctx, mctx)
462 r = filemerge.filemerge(repo, f, fd, f2, wctx, mctx)
518 463 if r > 0:
519 464 unresolved += 1
520 465 else:
521 466 if r is None:
522 467 updated += 1
523 468 else:
524 469 merged += 1
525 470 util.set_flags(repo.wjoin(fd), flags)
526 471 if f != fd and move and util.lexists(repo.wjoin(f)):
527 472 repo.ui.debug(_("removing %s\n") % f)
528 473 os.unlink(repo.wjoin(f))
529 474 elif m == "g": # get
530 475 flags = a[2]
531 476 repo.ui.note(_("getting %s\n") % f)
532 477 t = mctx.filectx(f).data()
533 478 repo.wwrite(f, t, flags)
534 479 updated += 1
535 480 elif m == "d": # directory rename
536 481 f2, fd, flags = a[2:]
537 482 if f:
538 483 repo.ui.note(_("moving %s to %s\n") % (f, fd))
539 484 t = wctx.filectx(f).data()
540 485 repo.wwrite(fd, t, flags)
541 486 util.unlink(repo.wjoin(f))
542 487 if f2:
543 488 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
544 489 t = mctx.filectx(f2).data()
545 490 repo.wwrite(fd, t, flags)
546 491 updated += 1
547 492 elif m == "dr": # divergent renames
548 493 fl = a[2]
549 494 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
550 495 for nf in fl:
551 496 repo.ui.warn(" %s\n" % nf)
552 497 elif m == "e": # exec
553 498 flags = a[2]
554 499 util.set_flags(repo.wjoin(f), flags)
555 500
556 501 return updated, merged, removed, unresolved
557 502
558 503 def recordupdates(repo, action, branchmerge):
559 504 "record merge actions to the dirstate"
560 505
561 506 for a in action:
562 507 f, m = a[:2]
563 508 if m == "r": # remove
564 509 if branchmerge:
565 510 repo.dirstate.remove(f)
566 511 else:
567 512 repo.dirstate.forget(f)
568 513 elif m == "f": # forget
569 514 repo.dirstate.forget(f)
570 515 elif m in "ge": # get or exec change
571 516 if branchmerge:
572 517 repo.dirstate.normaldirty(f)
573 518 else:
574 519 repo.dirstate.normal(f)
575 520 elif m == "m": # merge
576 521 f2, fd, flag, move = a[2:]
577 522 if branchmerge:
578 523 # We've done a branch merge, mark this file as merged
579 524 # so that we properly record the merger later
580 525 repo.dirstate.merge(fd)
581 526 if f != f2: # copy/rename
582 527 if move:
583 528 repo.dirstate.remove(f)
584 529 if f != fd:
585 530 repo.dirstate.copy(f, fd)
586 531 else:
587 532 repo.dirstate.copy(f2, fd)
588 533 else:
589 534 # We've update-merged a locally modified file, so
590 535 # we set the dirstate to emulate a normal checkout
591 536 # of that file some time in the past. Thus our
592 537 # merge will appear as a normal local file
593 538 # modification.
594 539 repo.dirstate.normallookup(fd)
595 540 if move:
596 541 repo.dirstate.forget(f)
597 542 elif m == "d": # directory rename
598 543 f2, fd, flag = a[2:]
599 544 if not f2 and f not in repo.dirstate:
600 545 # untracked file moved
601 546 continue
602 547 if branchmerge:
603 548 repo.dirstate.add(fd)
604 549 if f:
605 550 repo.dirstate.remove(f)
606 551 repo.dirstate.copy(f, fd)
607 552 if f2:
608 553 repo.dirstate.copy(f2, fd)
609 554 else:
610 555 repo.dirstate.normal(fd)
611 556 if f:
612 557 repo.dirstate.forget(f)
613 558
614 559 def update(repo, node, branchmerge, force, partial):
615 560 """
616 561 Perform a merge between the working directory and the given node
617 562
618 563 branchmerge = whether to merge between branches
619 564 force = whether to force branch merging or file overwriting
620 565 partial = a function to filter file lists (dirstate not updated)
621 566 """
622 567
623 568 wlock = repo.wlock()
624 569 try:
625 570 wc = repo.workingctx()
626 571 if node is None:
627 572 # tip of current branch
628 573 try:
629 574 node = repo.branchtags()[wc.branch()]
630 575 except KeyError:
631 576 if wc.branch() == "default": # no default branch!
632 577 node = repo.lookup("tip") # update to tip
633 578 else:
634 579 raise util.Abort(_("branch %s not found") % wc.branch())
635 580 overwrite = force and not branchmerge
636 581 forcemerge = force and branchmerge
637 582 pl = wc.parents()
638 583 p1, p2 = pl[0], repo.changectx(node)
639 584 pa = p1.ancestor(p2)
640 585 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
641 586 fastforward = False
642 587
643 588 ### check phase
644 589 if not overwrite and len(pl) > 1:
645 590 raise util.Abort(_("outstanding uncommitted merges"))
646 591 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
647 592 if branchmerge:
648 593 if p1.branch() != p2.branch() and pa != p2:
649 594 fastforward = True
650 595 else:
651 596 raise util.Abort(_("there is nothing to merge, just use "
652 597 "'hg update' or look at 'hg heads'"))
653 598 elif not (overwrite or branchmerge):
654 599 raise util.Abort(_("update spans branches, use 'hg merge' "
655 600 "or 'hg update -C' to lose changes"))
656 601 if branchmerge and not forcemerge:
657 602 if wc.files():
658 603 raise util.Abort(_("outstanding uncommitted changes"))
659 604
660 605 ### calculate phase
661 606 action = []
662 607 if not force:
663 608 checkunknown(wc, p2)
664 609 if not util.checkfolding(repo.path):
665 610 checkcollision(p2)
666 611 if not branchmerge:
667 612 action += forgetremoved(wc, p2)
668 613 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
669 614
670 615 ### apply phase
671 616 if not branchmerge: # just jump to the new rev
672 617 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
673 618 if not partial:
674 619 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
675 620
676 621 stats = applyupdates(repo, action, wc, p2)
677 622
678 623 if not partial:
679 624 recordupdates(repo, action, branchmerge)
680 625 repo.dirstate.setparents(fp1, fp2)
681 626 if not branchmerge and not fastforward:
682 627 repo.dirstate.setbranch(p2.branch())
683 628 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
684 629
685 630 return stats
686 631 finally:
687 632 del wlock
General Comments 0
You need to be logged in to leave comments. Login now