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