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