##// END OF EJS Templates
imerge: simplify 1d5ebb0d366f
Brendan Cully -
r5165:ec24bfd8 default
parent child Browse files
Show More
@@ -1,362 +1,362 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, fancyopts, hg, merge, util
10 from mercurial import commands, cmdutil, fancyopts, hg, merge, util
11 import os, tarfile
11 import os, tarfile
12
12
13 class InvalidStateFileException(Exception): pass
13 class InvalidStateFileException(Exception): pass
14
14
15 class ImergeStateFile(object):
15 class ImergeStateFile(object):
16 def __init__(self, im):
16 def __init__(self, im):
17 self.im = im
17 self.im = im
18
18
19 def save(self, dest):
19 def save(self, dest):
20 tf = tarfile.open(dest, 'w:gz')
20 tf = tarfile.open(dest, 'w:gz')
21
21
22 st = os.path.join(self.im.path, 'status')
22 st = os.path.join(self.im.path, 'status')
23 tf.add(st, os.path.join('.hg', 'imerge', 'status'))
23 tf.add(st, os.path.join('.hg', 'imerge', 'status'))
24
24
25 for f in self.im.resolved:
25 for f in self.im.resolved:
26 (fd, fo) = self.im.conflicts[f]
26 (fd, fo) = self.im.conflicts[f]
27 abssrc = self.im.repo.wjoin(fd)
27 abssrc = self.im.repo.wjoin(fd)
28 tf.add(abssrc, fd)
28 tf.add(abssrc, fd)
29
29
30 tf.close()
30 tf.close()
31
31
32 def load(self, source):
32 def load(self, source):
33 wlock = self.im.repo.wlock()
33 wlock = self.im.repo.wlock()
34 lock = self.im.repo.lock()
34 lock = self.im.repo.lock()
35
35
36 tf = tarfile.open(source, 'r')
36 tf = tarfile.open(source, 'r')
37 contents = tf.getnames()
37 contents = tf.getnames()
38 # tarfile normalizes path separators to '/'
38 # tarfile normalizes path separators to '/'
39 statusfile = '/'.join(['.hg', 'imerge', 'status'])
39 statusfile = '.hg/imerge/status'
40 if statusfile not in contents:
40 if statusfile not in contents:
41 raise InvalidStateFileException('no status file')
41 raise InvalidStateFileException('no status file')
42
42
43 tf.extract(statusfile, self.im.repo.root)
43 tf.extract(statusfile, self.im.repo.root)
44 p1, p2 = self.im.load()
44 p1, p2 = self.im.load()
45 if self.im.repo.dirstate.parents()[0] != p1.node():
45 if self.im.repo.dirstate.parents()[0] != p1.node():
46 hg.clean(self.im.repo, p1.node())
46 hg.clean(self.im.repo, p1.node())
47 self.im.start(p2.node())
47 self.im.start(p2.node())
48 for tarinfo in tf:
48 for tarinfo in tf:
49 tf.extract(tarinfo, self.im.repo.root)
49 tf.extract(tarinfo, self.im.repo.root)
50 self.im.load()
50 self.im.load()
51
51
52 class Imerge(object):
52 class Imerge(object):
53 def __init__(self, ui, repo):
53 def __init__(self, ui, repo):
54 self.ui = ui
54 self.ui = ui
55 self.repo = repo
55 self.repo = repo
56
56
57 self.path = repo.join('imerge')
57 self.path = repo.join('imerge')
58 self.opener = util.opener(self.path)
58 self.opener = util.opener(self.path)
59
59
60 self.wctx = self.repo.workingctx()
60 self.wctx = self.repo.workingctx()
61 self.conflicts = {}
61 self.conflicts = {}
62 self.resolved = []
62 self.resolved = []
63
63
64 def merging(self):
64 def merging(self):
65 return len(self.wctx.parents()) > 1
65 return len(self.wctx.parents()) > 1
66
66
67 def load(self):
67 def load(self):
68 # status format. \0-delimited file, fields are
68 # status format. \0-delimited file, fields are
69 # p1, p2, conflict count, conflict filenames, resolved filenames
69 # p1, p2, conflict count, conflict filenames, resolved filenames
70 # conflict filenames are tuples of localname, remoteorig, remotenew
70 # conflict filenames are tuples of localname, remoteorig, remotenew
71
71
72 statusfile = self.opener('status')
72 statusfile = self.opener('status')
73
73
74 status = statusfile.read().split('\0')
74 status = statusfile.read().split('\0')
75 if len(status) < 3:
75 if len(status) < 3:
76 raise util.Abort('invalid imerge status file')
76 raise util.Abort('invalid imerge status file')
77
77
78 try:
78 try:
79 parents = [self.repo.changectx(n) for n in status[:2]]
79 parents = [self.repo.changectx(n) for n in status[:2]]
80 except LookupError:
80 except LookupError:
81 raise util.Abort('merge parent %s not in repository' % short(p))
81 raise util.Abort('merge parent %s not in repository' % short(p))
82
82
83 status = status[2:]
83 status = status[2:]
84 conflicts = int(status.pop(0)) * 3
84 conflicts = int(status.pop(0)) * 3
85 self.resolved = status[conflicts:]
85 self.resolved = status[conflicts:]
86 for i in xrange(0, conflicts, 3):
86 for i in xrange(0, conflicts, 3):
87 self.conflicts[status[i]] = (status[i+1], status[i+2])
87 self.conflicts[status[i]] = (status[i+1], status[i+2])
88
88
89 return parents
89 return parents
90
90
91 def save(self):
91 def save(self):
92 lock = self.repo.lock()
92 lock = self.repo.lock()
93
93
94 if not os.path.isdir(self.path):
94 if not os.path.isdir(self.path):
95 os.mkdir(self.path)
95 os.mkdir(self.path)
96 statusfile = self.opener('status', 'wb')
96 statusfile = self.opener('status', 'wb')
97
97
98 out = [hex(n.node()) for n in self.wctx.parents()]
98 out = [hex(n.node()) for n in self.wctx.parents()]
99 out.append(str(len(self.conflicts)))
99 out.append(str(len(self.conflicts)))
100 conflicts = self.conflicts.items()
100 conflicts = self.conflicts.items()
101 conflicts.sort()
101 conflicts.sort()
102 for fw, fd_fo in conflicts:
102 for fw, fd_fo in conflicts:
103 out.append(fw)
103 out.append(fw)
104 out.extend(fd_fo)
104 out.extend(fd_fo)
105 out.extend(self.resolved)
105 out.extend(self.resolved)
106
106
107 statusfile.write('\0'.join(out))
107 statusfile.write('\0'.join(out))
108
108
109 def remaining(self):
109 def remaining(self):
110 return [f for f in self.conflicts if f not in self.resolved]
110 return [f for f in self.conflicts if f not in self.resolved]
111
111
112 def filemerge(self, fn):
112 def filemerge(self, fn):
113 wlock = self.repo.wlock()
113 wlock = self.repo.wlock()
114
114
115 (fd, fo) = self.conflicts[fn]
115 (fd, fo) = self.conflicts[fn]
116 p2 = self.wctx.parents()[1]
116 p2 = self.wctx.parents()[1]
117 return merge.filemerge(self.repo, fn, fd, fo, self.wctx, p2)
117 return merge.filemerge(self.repo, fn, fd, fo, self.wctx, p2)
118
118
119 def start(self, rev=None):
119 def start(self, rev=None):
120 _filemerge = merge.filemerge
120 _filemerge = merge.filemerge
121 def filemerge(repo, fw, fd, fo, wctx, mctx):
121 def filemerge(repo, fw, fd, fo, wctx, mctx):
122 self.conflicts[fw] = (fd, fo)
122 self.conflicts[fw] = (fd, fo)
123
123
124 merge.filemerge = filemerge
124 merge.filemerge = filemerge
125 commands.merge(self.ui, self.repo, rev=rev)
125 commands.merge(self.ui, self.repo, rev=rev)
126 merge.filemerge = _filemerge
126 merge.filemerge = _filemerge
127
127
128 self.wctx = self.repo.workingctx()
128 self.wctx = self.repo.workingctx()
129 self.save()
129 self.save()
130
130
131 def resume(self):
131 def resume(self):
132 self.load()
132 self.load()
133
133
134 dp = self.repo.dirstate.parents()
134 dp = self.repo.dirstate.parents()
135 p1, p2 = self.wctx.parents()
135 p1, p2 = self.wctx.parents()
136 if p1.node() != dp[0] or p2.node() != dp[1]:
136 if p1.node() != dp[0] or p2.node() != dp[1]:
137 raise util.Abort('imerge state does not match working directory')
137 raise util.Abort('imerge state does not match working directory')
138
138
139 def next(self):
139 def next(self):
140 remaining = self.remaining()
140 remaining = self.remaining()
141 return remaining and remaining[0]
141 return remaining and remaining[0]
142
142
143 def resolve(self, files):
143 def resolve(self, files):
144 resolved = dict.fromkeys(self.resolved)
144 resolved = dict.fromkeys(self.resolved)
145 for fn in files:
145 for fn in files:
146 if fn not in self.conflicts:
146 if fn not in self.conflicts:
147 raise util.Abort('%s is not in the merge set' % fn)
147 raise util.Abort('%s is not in the merge set' % fn)
148 resolved[fn] = True
148 resolved[fn] = True
149 self.resolved = resolved.keys()
149 self.resolved = resolved.keys()
150 self.resolved.sort()
150 self.resolved.sort()
151 self.save()
151 self.save()
152 return 0
152 return 0
153
153
154 def unresolve(self, files):
154 def unresolve(self, files):
155 resolved = dict.fromkeys(self.resolved)
155 resolved = dict.fromkeys(self.resolved)
156 for fn in files:
156 for fn in files:
157 if fn not in resolved:
157 if fn not in resolved:
158 raise util.Abort('%s is not resolved' % fn)
158 raise util.Abort('%s is not resolved' % fn)
159 del resolved[fn]
159 del resolved[fn]
160 self.resolved = resolved.keys()
160 self.resolved = resolved.keys()
161 self.resolved.sort()
161 self.resolved.sort()
162 self.save()
162 self.save()
163 return 0
163 return 0
164
164
165 def pickle(self, dest):
165 def pickle(self, dest):
166 '''write current merge state to file to be resumed elsewhere'''
166 '''write current merge state to file to be resumed elsewhere'''
167 state = ImergeStateFile(self)
167 state = ImergeStateFile(self)
168 return state.save(dest)
168 return state.save(dest)
169
169
170 def unpickle(self, source):
170 def unpickle(self, source):
171 '''read merge state from file'''
171 '''read merge state from file'''
172 state = ImergeStateFile(self)
172 state = ImergeStateFile(self)
173 return state.load(source)
173 return state.load(source)
174
174
175 def load(im, source):
175 def load(im, source):
176 if im.merging():
176 if im.merging():
177 raise util.Abort('there is already a merge in progress '
177 raise util.Abort('there is already a merge in progress '
178 '(update -C <rev> to abort it)' )
178 '(update -C <rev> to abort it)' )
179 m, a, r, d = im.repo.status()[:4]
179 m, a, r, d = im.repo.status()[:4]
180 if m or a or r or d:
180 if m or a or r or d:
181 raise util.Abort('working directory has uncommitted changes')
181 raise util.Abort('working directory has uncommitted changes')
182
182
183 rc = im.unpickle(source)
183 rc = im.unpickle(source)
184 if not rc:
184 if not rc:
185 status(im)
185 status(im)
186 return rc
186 return rc
187
187
188 def merge_(im, filename=None):
188 def merge_(im, filename=None):
189 if not filename:
189 if not filename:
190 filename = im.next()
190 filename = im.next()
191 if not filename:
191 if not filename:
192 im.ui.write('all conflicts resolved\n')
192 im.ui.write('all conflicts resolved\n')
193 return 0
193 return 0
194
194
195 rc = im.filemerge(filename)
195 rc = im.filemerge(filename)
196 if not rc:
196 if not rc:
197 im.resolve([filename])
197 im.resolve([filename])
198 if not im.next():
198 if not im.next():
199 im.ui.write('all conflicts resolved\n')
199 im.ui.write('all conflicts resolved\n')
200 return 0
200 return 0
201 return rc
201 return rc
202
202
203 def next(im):
203 def next(im):
204 n = im.next()
204 n = im.next()
205 if n:
205 if n:
206 im.ui.write('%s\n' % n)
206 im.ui.write('%s\n' % n)
207 else:
207 else:
208 im.ui.write('all conflicts resolved\n')
208 im.ui.write('all conflicts resolved\n')
209 return 0
209 return 0
210
210
211 def resolve(im, *files):
211 def resolve(im, *files):
212 if not files:
212 if not files:
213 raise util.Abort('resolve requires at least one filename')
213 raise util.Abort('resolve requires at least one filename')
214 return im.resolve(files)
214 return im.resolve(files)
215
215
216 def save(im, dest):
216 def save(im, dest):
217 return im.pickle(dest)
217 return im.pickle(dest)
218
218
219 def status(im, **opts):
219 def status(im, **opts):
220 if not opts.get('resolved') and not opts.get('unresolved'):
220 if not opts.get('resolved') and not opts.get('unresolved'):
221 opts['resolved'] = True
221 opts['resolved'] = True
222 opts['unresolved'] = True
222 opts['unresolved'] = True
223
223
224 if im.ui.verbose:
224 if im.ui.verbose:
225 p1, p2 = [short(p.node()) for p in im.wctx.parents()]
225 p1, p2 = [short(p.node()) for p in im.wctx.parents()]
226 im.ui.note(_('merging %s and %s\n') % (p1, p2))
226 im.ui.note(_('merging %s and %s\n') % (p1, p2))
227
227
228 conflicts = im.conflicts.keys()
228 conflicts = im.conflicts.keys()
229 conflicts.sort()
229 conflicts.sort()
230 remaining = dict.fromkeys(im.remaining())
230 remaining = dict.fromkeys(im.remaining())
231 st = []
231 st = []
232 for fn in conflicts:
232 for fn in conflicts:
233 if opts.get('no_status'):
233 if opts.get('no_status'):
234 mode = ''
234 mode = ''
235 elif fn in remaining:
235 elif fn in remaining:
236 mode = 'U '
236 mode = 'U '
237 else:
237 else:
238 mode = 'R '
238 mode = 'R '
239 if ((opts.get('resolved') and fn not in remaining)
239 if ((opts.get('resolved') and fn not in remaining)
240 or (opts.get('unresolved') and fn in remaining)):
240 or (opts.get('unresolved') and fn in remaining)):
241 st.append((mode, fn))
241 st.append((mode, fn))
242 st.sort()
242 st.sort()
243 for (mode, fn) in st:
243 for (mode, fn) in st:
244 if im.ui.verbose:
244 if im.ui.verbose:
245 fo, fd = im.conflicts[fn]
245 fo, fd = im.conflicts[fn]
246 if fd != fn:
246 if fd != fn:
247 fn = '%s (%s)' % (fn, fd)
247 fn = '%s (%s)' % (fn, fd)
248 im.ui.write('%s%s\n' % (mode, fn))
248 im.ui.write('%s%s\n' % (mode, fn))
249 if opts.get('unresolved') and not remaining:
249 if opts.get('unresolved') and not remaining:
250 im.ui.write(_('all conflicts resolved\n'))
250 im.ui.write(_('all conflicts resolved\n'))
251
251
252 return 0
252 return 0
253
253
254 def unresolve(im, *files):
254 def unresolve(im, *files):
255 if not files:
255 if not files:
256 raise util.Abort('unresolve requires at least one filename')
256 raise util.Abort('unresolve requires at least one filename')
257 return im.unresolve(files)
257 return im.unresolve(files)
258
258
259 subcmdtable = {
259 subcmdtable = {
260 'load': (load, []),
260 'load': (load, []),
261 'merge': (merge_, []),
261 'merge': (merge_, []),
262 'next': (next, []),
262 'next': (next, []),
263 'resolve': (resolve, []),
263 'resolve': (resolve, []),
264 'save': (save, []),
264 'save': (save, []),
265 'status': (status,
265 'status': (status,
266 [('n', 'no-status', None, _('hide status prefix')),
266 [('n', 'no-status', None, _('hide status prefix')),
267 ('', 'resolved', None, _('only show resolved conflicts')),
267 ('', 'resolved', None, _('only show resolved conflicts')),
268 ('', 'unresolved', None, _('only show unresolved conflicts'))]),
268 ('', 'unresolved', None, _('only show unresolved conflicts'))]),
269 'unresolve': (unresolve, [])
269 'unresolve': (unresolve, [])
270 }
270 }
271
271
272 def dispatch(im, args, opts):
272 def dispatch(im, args, opts):
273 def complete(s, choices):
273 def complete(s, choices):
274 candidates = []
274 candidates = []
275 for choice in choices:
275 for choice in choices:
276 if choice.startswith(s):
276 if choice.startswith(s):
277 candidates.append(choice)
277 candidates.append(choice)
278 return candidates
278 return candidates
279
279
280 c, args = args[0], list(args[1:])
280 c, args = args[0], list(args[1:])
281 cmd = complete(c, subcmdtable.keys())
281 cmd = complete(c, subcmdtable.keys())
282 if not cmd:
282 if not cmd:
283 raise cmdutil.UnknownCommand('imerge ' + c)
283 raise cmdutil.UnknownCommand('imerge ' + c)
284 if len(cmd) > 1:
284 if len(cmd) > 1:
285 cmd.sort()
285 cmd.sort()
286 raise cmdutil.AmbiguousCommand('imerge ' + c, cmd)
286 raise cmdutil.AmbiguousCommand('imerge ' + c, cmd)
287 cmd = cmd[0]
287 cmd = cmd[0]
288
288
289 func, optlist = subcmdtable[cmd]
289 func, optlist = subcmdtable[cmd]
290 opts = {}
290 opts = {}
291 try:
291 try:
292 args = fancyopts.fancyopts(args, optlist, opts)
292 args = fancyopts.fancyopts(args, optlist, opts)
293 return func(im, *args, **opts)
293 return func(im, *args, **opts)
294 except fancyopts.getopt.GetoptError, inst:
294 except fancyopts.getopt.GetoptError, inst:
295 raise cmdutil.ParseError('imerge', '%s: %s' % (cmd, inst))
295 raise cmdutil.ParseError('imerge', '%s: %s' % (cmd, inst))
296 except TypeError:
296 except TypeError:
297 raise cmdutil.ParseError('imerge', _('%s: invalid arguments') % cmd)
297 raise cmdutil.ParseError('imerge', _('%s: invalid arguments') % cmd)
298
298
299 def imerge(ui, repo, *args, **opts):
299 def imerge(ui, repo, *args, **opts):
300 '''interactive merge
300 '''interactive merge
301
301
302 imerge lets you split a merge into pieces. When you start a merge
302 imerge lets you split a merge into pieces. When you start a merge
303 with imerge, the names of all files with conflicts are recorded.
303 with imerge, the names of all files with conflicts are recorded.
304 You can then merge any of these files, and if the merge is
304 You can then merge any of these files, and if the merge is
305 successful, they will be marked as resolved. When all files are
305 successful, they will be marked as resolved. When all files are
306 resolved, the merge is complete.
306 resolved, the merge is complete.
307
307
308 If no merge is in progress, hg imerge [rev] will merge the working
308 If no merge is in progress, hg imerge [rev] will merge the working
309 directory with rev (defaulting to the other head if the repository
309 directory with rev (defaulting to the other head if the repository
310 only has two heads). You may also resume a saved merge with
310 only has two heads). You may also resume a saved merge with
311 hg imerge load <file>.
311 hg imerge load <file>.
312
312
313 If a merge is in progress, hg imerge will default to merging the
313 If a merge is in progress, hg imerge will default to merging the
314 next unresolved file.
314 next unresolved file.
315
315
316 The following subcommands are available:
316 The following subcommands are available:
317
317
318 status:
318 status:
319 show the current state of the merge
319 show the current state of the merge
320 next:
320 next:
321 show the next unresolved file merge
321 show the next unresolved file merge
322 merge [<file>]:
322 merge [<file>]:
323 merge <file>. If the file merge is successful, the file will be
323 merge <file>. If the file merge is successful, the file will be
324 recorded as resolved. If no file is given, the next unresolved
324 recorded as resolved. If no file is given, the next unresolved
325 file will be merged.
325 file will be merged.
326 resolve <file>...:
326 resolve <file>...:
327 mark files as successfully merged
327 mark files as successfully merged
328 unresolve <file>...:
328 unresolve <file>...:
329 mark files as requiring merging.
329 mark files as requiring merging.
330 save <file>:
330 save <file>:
331 save the state of the merge to a file to be resumed elsewhere
331 save the state of the merge to a file to be resumed elsewhere
332 load <file>:
332 load <file>:
333 load the state of the merge from a file created by save
333 load the state of the merge from a file created by save
334 '''
334 '''
335
335
336 im = Imerge(ui, repo)
336 im = Imerge(ui, repo)
337
337
338 if im.merging():
338 if im.merging():
339 im.resume()
339 im.resume()
340 else:
340 else:
341 rev = opts.get('rev')
341 rev = opts.get('rev')
342 if rev and args:
342 if rev and args:
343 raise util.Abort('please specify just one revision')
343 raise util.Abort('please specify just one revision')
344
344
345 if len(args) == 2 and args[0] == 'load':
345 if len(args) == 2 and args[0] == 'load':
346 pass
346 pass
347 else:
347 else:
348 if args:
348 if args:
349 rev = args[0]
349 rev = args[0]
350 im.start(rev=rev)
350 im.start(rev=rev)
351 args = ['status']
351 args = ['status']
352
352
353 if not args:
353 if not args:
354 args = ['merge']
354 args = ['merge']
355
355
356 return dispatch(im, args, opts)
356 return dispatch(im, args, opts)
357
357
358 cmdtable = {
358 cmdtable = {
359 '^imerge':
359 '^imerge':
360 (imerge,
360 (imerge,
361 [('r', 'rev', '', _('revision to merge'))], 'hg imerge [command]')
361 [('r', 'rev', '', _('revision to merge'))], 'hg imerge [command]')
362 }
362 }
General Comments 0
You need to be logged in to leave comments. Login now