comparison hgext/imerge.py @ 5044:ed68c8c31c9a

Automated merge with http://hg.intevation.org/mercurial/crew
author Bryan O'Sullivan <bos@serpentine.com>
date Wed, 01 Aug 2007 12:03:50 -0700
parents 58006f8b8275
children ec70fd08e16c
comparison
equal deleted inserted replaced
5043:8b1ee1f59b3c 5044:ed68c8c31c9a
1 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
2 # Published under the GNU GPL
3
4 '''
5 imerge - interactive merge
6 '''
7
8 from mercurial.i18n import _
9 from mercurial.node import *
10 from mercurial import commands, cmdutil, hg, merge, util
11 import os, tarfile
12
13 class InvalidStateFileException(Exception): pass
14
15 class ImergeStateFile(object):
16 def __init__(self, im):
17 self.im = im
18
19 def save(self, dest):
20 tf = tarfile.open(dest, 'w:gz')
21
22 st = os.path.join(self.im.path, 'status')
23 tf.add(st, os.path.join('.hg', 'imerge', 'status'))
24
25 for f in self.im.resolved:
26 abssrc = self.im.repo.wjoin(f)
27 tf.add(abssrc, f)
28
29 tf.close()
30
31 def load(self, source):
32 wlock = self.im.repo.wlock()
33 lock = self.im.repo.lock()
34
35 tf = tarfile.open(source, 'r')
36 contents = tf.getnames()
37 statusfile = os.path.join('.hg', 'imerge', 'status')
38 if statusfile not in contents:
39 raise InvalidStateFileException('no status file')
40
41 tf.extract(statusfile, self.im.repo.root)
42 self.im.load()
43 p1 = self.im.parents[0].node()
44 p2 = self.im.parents[1].node()
45 if self.im.repo.dirstate.parents()[0] != p1:
46 hg.clean(self.im.repo, self.im.parents[0].node())
47 self.im.start(p2)
48 tf.extractall(self.im.repo.root)
49 self.im.load()
50
51 class Imerge(object):
52 def __init__(self, ui, repo):
53 self.ui = ui
54 self.repo = repo
55
56 self.path = repo.join('imerge')
57 self.opener = util.opener(self.path)
58
59 self.parents = [self.repo.changectx(n)
60 for n in self.repo.dirstate.parents()]
61 self.conflicts = {}
62 self.resolved = []
63
64 def merging(self):
65 return self.parents[1].node() != nullid
66
67 def load(self):
68 # status format. \0-delimited file, fields are
69 # p1, p2, conflict count, conflict filenames, resolved filenames
70 # conflict filenames are pairs of localname, remotename
71
72 statusfile = self.opener('status')
73
74 status = statusfile.read().split('\0')
75 if len(status) < 3:
76 raise util.Abort('invalid imerge status file')
77
78 try:
79 self.parents = [self.repo.changectx(n) for n in status[:2]]
80 except LookupError:
81 raise util.Abort('merge parent %s not in repository' % short(p))
82
83 status = status[2:]
84 conflicts = int(status.pop(0)) * 2
85 self.resolved = status[conflicts:]
86 for i in xrange(0, conflicts, 2):
87 self.conflicts[status[i]] = status[i+1]
88
89 def save(self):
90 lock = self.repo.lock()
91
92 if not os.path.isdir(self.path):
93 os.mkdir(self.path)
94 fd = self.opener('status', 'wb')
95
96 out = [hex(n.node()) for n in self.parents]
97 out.append(str(len(self.conflicts)))
98 for f in sorted(self.conflicts):
99 out.append(f)
100 out.append(self.conflicts[f])
101 out.extend(self.resolved)
102
103 fd.write('\0'.join(out))
104
105 def remaining(self):
106 return [f for f in self.conflicts if f not in self.resolved]
107
108 def filemerge(self, fn):
109 wlock = self.repo.wlock()
110
111 fo = self.conflicts[fn]
112 return merge.filemerge(self.repo, fn, fo, self.parents[0],
113 self.parents[1])
114
115 def start(self, rev=None):
116 _filemerge = merge.filemerge
117 def filemerge(repo, fw, fo, wctx, mctx):
118 self.conflicts[fw] = fo
119
120 merge.filemerge = filemerge
121 commands.merge(self.ui, self.repo, rev=rev)
122 merge.filemerge = _filemerge
123
124 self.parents = [self.repo.changectx(n)
125 for n in self.repo.dirstate.parents()]
126 self.save()
127
128 def resume(self):
129 self.load()
130
131 dp = self.repo.dirstate.parents()
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')
134
135 def status(self):
136 self.ui.write('merging %s and %s\n' % \
137 (short(self.parents[0].node()),
138 short(self.parents[1].node())))
139
140 if self.resolved:
141 self.ui.write('resolved:\n')
142 for fn in self.resolved:
143 self.ui.write(' %s\n' % fn)
144 remaining = [f for f in self.conflicts if f not in self.resolved]
145 if remaining:
146 self.ui.write('remaining:\n')
147 for fn in remaining:
148 fo = self.conflicts[fn]
149 if fn == fo:
150 self.ui.write(' %s\n' % (fn,))
151 else:
152 self.ui.write(' %s (%s)\n' % (fn, fo))
153 else:
154 self.ui.write('all conflicts resolved\n')
155
156 def next(self):
157 remaining = self.remaining()
158 return remaining and remaining[0]
159
160 def resolve(self, files):
161 resolved = dict.fromkeys(self.resolved)
162 for fn in files:
163 if fn not in self.conflicts:
164 raise util.Abort('%s is not in the merge set' % fn)
165 resolved[fn] = True
166 self.resolved = sorted(resolved)
167 self.save()
168 return 0
169
170 def unresolve(self, files):
171 resolved = dict.fromkeys(self.resolved)
172 for fn in files:
173 if fn not in resolved:
174 raise util.Abort('%s is not resolved' % fn)
175 del resolved[fn]
176 self.resolved = sorted(resolved)
177 self.save()
178 return 0
179
180 def pickle(self, dest):
181 '''write current merge state to file to be resumed elsewhere'''
182 state = ImergeStateFile(self)
183 return state.save(dest)
184
185 def unpickle(self, source):
186 '''read merge state from file'''
187 state = ImergeStateFile(self)
188 return state.load(source)
189
190 def load(im, source):
191 if im.merging():
192 raise util.Abort('there is already a merge in progress '
193 '(update -C <rev> to abort it)' )
194 m, a, r, d = im.repo.status()[:4]
195 if m or a or r or d:
196 raise util.Abort('working directory has uncommitted changes')
197
198 rc = im.unpickle(source)
199 if not rc:
200 im.status()
201 return rc
202
203 def merge_(im, filename=None):
204 if not filename:
205 filename = im.next()
206 if not filename:
207 im.ui.write('all conflicts resolved\n')
208 return 0
209
210 rc = im.filemerge(filename)
211 if not rc:
212 im.resolve([filename])
213 if not im.next():
214 im.ui.write('all conflicts resolved\n')
215 return 0
216 return rc
217
218 def next(im):
219 n = im.next()
220 if n:
221 im.ui.write('%s\n' % n)
222 else:
223 im.ui.write('all conflicts resolved\n')
224 return 0
225
226 def resolve(im, *files):
227 if not files:
228 raise util.Abort('resolve requires at least one filename')
229 return im.resolve(files)
230
231 def save(im, dest):
232 return im.pickle(dest)
233
234 def status(im):
235 im.status()
236 return 0
237
238 def unresolve(im, *files):
239 if not files:
240 raise util.Abort('unresolve requires at least one filename')
241 return im.unresolve(files)
242
243 subcmdtable = {
244 'load': load,
245 'merge': merge_,
246 'next': next,
247 'resolve': resolve,
248 'save': save,
249 'status': status,
250 'unresolve': unresolve
251 }
252
253 def dispatch(im, args, opts):
254 def complete(s, choices):
255 candidates = []
256 for choice in choices:
257 if choice.startswith(s):
258 candidates.append(choice)
259 return candidates
260
261 c, args = args[0], args[1:]
262 cmd = complete(c, subcmdtable.keys())
263 if not cmd:
264 raise cmdutil.UnknownCommand('imerge ' + c)
265 if len(cmd) > 1:
266 raise cmdutil.AmbiguousCommand('imerge ' + c, sorted(cmd))
267 cmd = cmd[0]
268
269 func = subcmdtable[cmd]
270 try:
271 return func(im, *args)
272 except TypeError:
273 raise cmdutil.ParseError('imerge', '%s: invalid arguments' % cmd)
274
275 def imerge(ui, repo, *args, **opts):
276 '''interactive merge
277
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.
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
282 resolved, the merge is complete.
283
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
286 only has two heads). You may also resume a saved merge with
287 hg imerge load <file>.
288
289 If a merge is in progress, hg imerge will default to merging the
290 next unresolved file.
291
292 The following subcommands are available:
293
294 status:
295 show the current state of the merge
296 next:
297 show the next unresolved file merge
298 merge [<file>]:
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
301 file will be merged.
302 resolve <file>...:
303 mark files as successfully merged
304 unresolve <file>...:
305 mark files as requiring merging.
306 save <file>:
307 save the state of the merge to a file to be resumed elsewhere
308 load <file>:
309 load the state of the merge from a file created by save
310 '''
311
312 im = Imerge(ui, repo)
313
314 if im.merging():
315 im.resume()
316 else:
317 rev = opts.get('rev')
318 if rev and args:
319 raise util.Abort('please specify just one revision')
320
321 if len(args) == 2 and args[0] == 'load':
322 pass
323 else:
324 if args:
325 rev = args[0]
326 im.start(rev=rev)
327 args = ['status']
328
329 if not args:
330 args = ['merge']
331
332 return dispatch(im, args, opts)
333
334 cmdtable = {
335 '^imerge':
336 (imerge,
337 [('r', 'rev', '', _('revision to merge'))], 'hg imerge [command]')
338 }