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