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