##// END OF EJS Templates
origpath: move from cmdutil to scmutil...
Siddharth Agarwal -
r27651:07fc2f21 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,552 +1,552
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''High-level command function for lfconvert, plus the cmdtable.'''
10 10
11 11 import os, errno
12 12 import shutil
13 13
14 14 from mercurial import util, match as match_, hg, node, context, error, \
15 15 cmdutil, scmutil, commands
16 16 from mercurial.i18n import _
17 17 from mercurial.lock import release
18 18
19 19 from hgext.convert import convcmd
20 20 from hgext.convert import filemap
21 21
22 22 import lfutil
23 23 import basestore
24 24
25 25 # -- Commands ----------------------------------------------------------
26 26
27 27 cmdtable = {}
28 28 command = cmdutil.command(cmdtable)
29 29
30 30 @command('lfconvert',
31 31 [('s', 'size', '',
32 32 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
33 33 ('', 'to-normal', False,
34 34 _('convert from a largefiles repo to a normal repo')),
35 35 ],
36 36 _('hg lfconvert SOURCE DEST [FILE ...]'),
37 37 norepo=True,
38 38 inferrepo=True)
39 39 def lfconvert(ui, src, dest, *pats, **opts):
40 40 '''convert a normal repository to a largefiles repository
41 41
42 42 Convert repository SOURCE to a new repository DEST, identical to
43 43 SOURCE except that certain files will be converted as largefiles:
44 44 specifically, any file that matches any PATTERN *or* whose size is
45 45 above the minimum size threshold is converted as a largefile. The
46 46 size used to determine whether or not to track a file as a
47 47 largefile is the size of the first version of the file. The
48 48 minimum size can be specified either with --size or in
49 49 configuration as ``largefiles.size``.
50 50
51 51 After running this command you will need to make sure that
52 52 largefiles is enabled anywhere you intend to push the new
53 53 repository.
54 54
55 55 Use --to-normal to convert largefiles back to normal files; after
56 56 this, the DEST repository can be used without largefiles at all.'''
57 57
58 58 if opts['to_normal']:
59 59 tolfile = False
60 60 else:
61 61 tolfile = True
62 62 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
63 63
64 64 if not hg.islocal(src):
65 65 raise error.Abort(_('%s is not a local Mercurial repo') % src)
66 66 if not hg.islocal(dest):
67 67 raise error.Abort(_('%s is not a local Mercurial repo') % dest)
68 68
69 69 rsrc = hg.repository(ui, src)
70 70 ui.status(_('initializing destination %s\n') % dest)
71 71 rdst = hg.repository(ui, dest, create=True)
72 72
73 73 success = False
74 74 dstwlock = dstlock = None
75 75 try:
76 76 # Get a list of all changesets in the source. The easy way to do this
77 77 # is to simply walk the changelog, using changelog.nodesbetween().
78 78 # Take a look at mercurial/revlog.py:639 for more details.
79 79 # Use a generator instead of a list to decrease memory usage
80 80 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
81 81 rsrc.heads())[0])
82 82 revmap = {node.nullid: node.nullid}
83 83 if tolfile:
84 84 # Lock destination to prevent modification while it is converted to.
85 85 # Don't need to lock src because we are just reading from its
86 86 # history which can't change.
87 87 dstwlock = rdst.wlock()
88 88 dstlock = rdst.lock()
89 89
90 90 lfiles = set()
91 91 normalfiles = set()
92 92 if not pats:
93 93 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
94 94 if pats:
95 95 matcher = match_.match(rsrc.root, '', list(pats))
96 96 else:
97 97 matcher = None
98 98
99 99 lfiletohash = {}
100 100 for ctx in ctxs:
101 101 ui.progress(_('converting revisions'), ctx.rev(),
102 102 unit=_('revision'), total=rsrc['tip'].rev())
103 103 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
104 104 lfiles, normalfiles, matcher, size, lfiletohash)
105 105 ui.progress(_('converting revisions'), None)
106 106
107 107 if os.path.exists(rdst.wjoin(lfutil.shortname)):
108 108 shutil.rmtree(rdst.wjoin(lfutil.shortname))
109 109
110 110 for f in lfiletohash.keys():
111 111 if os.path.isfile(rdst.wjoin(f)):
112 112 os.unlink(rdst.wjoin(f))
113 113 try:
114 114 os.removedirs(os.path.dirname(rdst.wjoin(f)))
115 115 except OSError:
116 116 pass
117 117
118 118 # If there were any files converted to largefiles, add largefiles
119 119 # to the destination repository's requirements.
120 120 if lfiles:
121 121 rdst.requirements.add('largefiles')
122 122 rdst._writerequirements()
123 123 else:
124 124 class lfsource(filemap.filemap_source):
125 125 def __init__(self, ui, source):
126 126 super(lfsource, self).__init__(ui, source, None)
127 127 self.filemapper.rename[lfutil.shortname] = '.'
128 128
129 129 def getfile(self, name, rev):
130 130 realname, realrev = rev
131 131 f = super(lfsource, self).getfile(name, rev)
132 132
133 133 if (not realname.startswith(lfutil.shortnameslash)
134 134 or f[0] is None):
135 135 return f
136 136
137 137 # Substitute in the largefile data for the hash
138 138 hash = f[0].strip()
139 139 path = lfutil.findfile(rsrc, hash)
140 140
141 141 if path is None:
142 142 raise error.Abort(_("missing largefile for '%s' in %s")
143 143 % (realname, realrev))
144 144 fp = open(path, 'rb')
145 145
146 146 try:
147 147 return (fp.read(), f[1])
148 148 finally:
149 149 fp.close()
150 150
151 151 class converter(convcmd.converter):
152 152 def __init__(self, ui, source, dest, revmapfile, opts):
153 153 src = lfsource(ui, source)
154 154
155 155 super(converter, self).__init__(ui, src, dest, revmapfile,
156 156 opts)
157 157
158 158 found, missing = downloadlfiles(ui, rsrc)
159 159 if missing != 0:
160 160 raise error.Abort(_("all largefiles must be present locally"))
161 161
162 162 orig = convcmd.converter
163 163 convcmd.converter = converter
164 164
165 165 try:
166 166 convcmd.convert(ui, src, dest)
167 167 finally:
168 168 convcmd.converter = orig
169 169 success = True
170 170 finally:
171 171 if tolfile:
172 172 rdst.dirstate.clear()
173 173 release(dstlock, dstwlock)
174 174 if not success:
175 175 # we failed, remove the new directory
176 176 shutil.rmtree(rdst.root)
177 177
178 178 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
179 179 matcher, size, lfiletohash):
180 180 # Convert src parents to dst parents
181 181 parents = _convertparents(ctx, revmap)
182 182
183 183 # Generate list of changed files
184 184 files = _getchangedfiles(ctx, parents)
185 185
186 186 dstfiles = []
187 187 for f in files:
188 188 if f not in lfiles and f not in normalfiles:
189 189 islfile = _islfile(f, ctx, matcher, size)
190 190 # If this file was renamed or copied then copy
191 191 # the largefile-ness of its predecessor
192 192 if f in ctx.manifest():
193 193 fctx = ctx.filectx(f)
194 194 renamed = fctx.renamed()
195 195 renamedlfile = renamed and renamed[0] in lfiles
196 196 islfile |= renamedlfile
197 197 if 'l' in fctx.flags():
198 198 if renamedlfile:
199 199 raise error.Abort(
200 200 _('renamed/copied largefile %s becomes symlink')
201 201 % f)
202 202 islfile = False
203 203 if islfile:
204 204 lfiles.add(f)
205 205 else:
206 206 normalfiles.add(f)
207 207
208 208 if f in lfiles:
209 209 dstfiles.append(lfutil.standin(f))
210 210 # largefile in manifest if it has not been removed/renamed
211 211 if f in ctx.manifest():
212 212 fctx = ctx.filectx(f)
213 213 if 'l' in fctx.flags():
214 214 renamed = fctx.renamed()
215 215 if renamed and renamed[0] in lfiles:
216 216 raise error.Abort(_('largefile %s becomes symlink') % f)
217 217
218 218 # largefile was modified, update standins
219 219 m = util.sha1('')
220 220 m.update(ctx[f].data())
221 221 hash = m.hexdigest()
222 222 if f not in lfiletohash or lfiletohash[f] != hash:
223 223 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
224 224 executable = 'x' in ctx[f].flags()
225 225 lfutil.writestandin(rdst, lfutil.standin(f), hash,
226 226 executable)
227 227 lfiletohash[f] = hash
228 228 else:
229 229 # normal file
230 230 dstfiles.append(f)
231 231
232 232 def getfilectx(repo, memctx, f):
233 233 if lfutil.isstandin(f):
234 234 # if the file isn't in the manifest then it was removed
235 235 # or renamed, raise IOError to indicate this
236 236 srcfname = lfutil.splitstandin(f)
237 237 try:
238 238 fctx = ctx.filectx(srcfname)
239 239 except error.LookupError:
240 240 return None
241 241 renamed = fctx.renamed()
242 242 if renamed:
243 243 # standin is always a largefile because largefile-ness
244 244 # doesn't change after rename or copy
245 245 renamed = lfutil.standin(renamed[0])
246 246
247 247 return context.memfilectx(repo, f, lfiletohash[srcfname] + '\n',
248 248 'l' in fctx.flags(), 'x' in fctx.flags(),
249 249 renamed)
250 250 else:
251 251 return _getnormalcontext(repo, ctx, f, revmap)
252 252
253 253 # Commit
254 254 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
255 255
256 256 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
257 257 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
258 258 getfilectx, ctx.user(), ctx.date(), ctx.extra())
259 259 ret = rdst.commitctx(mctx)
260 260 lfutil.copyalltostore(rdst, ret)
261 261 rdst.setparents(ret)
262 262 revmap[ctx.node()] = rdst.changelog.tip()
263 263
264 264 # Generate list of changed files
265 265 def _getchangedfiles(ctx, parents):
266 266 files = set(ctx.files())
267 267 if node.nullid not in parents:
268 268 mc = ctx.manifest()
269 269 mp1 = ctx.parents()[0].manifest()
270 270 mp2 = ctx.parents()[1].manifest()
271 271 files |= (set(mp1) | set(mp2)) - set(mc)
272 272 for f in mc:
273 273 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
274 274 files.add(f)
275 275 return files
276 276
277 277 # Convert src parents to dst parents
278 278 def _convertparents(ctx, revmap):
279 279 parents = []
280 280 for p in ctx.parents():
281 281 parents.append(revmap[p.node()])
282 282 while len(parents) < 2:
283 283 parents.append(node.nullid)
284 284 return parents
285 285
286 286 # Get memfilectx for a normal file
287 287 def _getnormalcontext(repo, ctx, f, revmap):
288 288 try:
289 289 fctx = ctx.filectx(f)
290 290 except error.LookupError:
291 291 return None
292 292 renamed = fctx.renamed()
293 293 if renamed:
294 294 renamed = renamed[0]
295 295
296 296 data = fctx.data()
297 297 if f == '.hgtags':
298 298 data = _converttags (repo.ui, revmap, data)
299 299 return context.memfilectx(repo, f, data, 'l' in fctx.flags(),
300 300 'x' in fctx.flags(), renamed)
301 301
302 302 # Remap tag data using a revision map
303 303 def _converttags(ui, revmap, data):
304 304 newdata = []
305 305 for line in data.splitlines():
306 306 try:
307 307 id, name = line.split(' ', 1)
308 308 except ValueError:
309 309 ui.warn(_('skipping incorrectly formatted tag %s\n')
310 310 % line)
311 311 continue
312 312 try:
313 313 newid = node.bin(id)
314 314 except TypeError:
315 315 ui.warn(_('skipping incorrectly formatted id %s\n')
316 316 % id)
317 317 continue
318 318 try:
319 319 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
320 320 name))
321 321 except KeyError:
322 322 ui.warn(_('no mapping for id %s\n') % id)
323 323 continue
324 324 return ''.join(newdata)
325 325
326 326 def _islfile(file, ctx, matcher, size):
327 327 '''Return true if file should be considered a largefile, i.e.
328 328 matcher matches it or it is larger than size.'''
329 329 # never store special .hg* files as largefiles
330 330 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
331 331 return False
332 332 if matcher and matcher(file):
333 333 return True
334 334 try:
335 335 return ctx.filectx(file).size() >= size * 1024 * 1024
336 336 except error.LookupError:
337 337 return False
338 338
339 339 def uploadlfiles(ui, rsrc, rdst, files):
340 340 '''upload largefiles to the central store'''
341 341
342 342 if not files:
343 343 return
344 344
345 345 store = basestore._openstore(rsrc, rdst, put=True)
346 346
347 347 at = 0
348 348 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
349 349 retval = store.exists(files)
350 350 files = filter(lambda h: not retval[h], files)
351 351 ui.debug("%d largefiles need to be uploaded\n" % len(files))
352 352
353 353 for hash in files:
354 354 ui.progress(_('uploading largefiles'), at, unit='largefile',
355 355 total=len(files))
356 356 source = lfutil.findfile(rsrc, hash)
357 357 if not source:
358 358 raise error.Abort(_('largefile %s missing from store'
359 359 ' (needs to be uploaded)') % hash)
360 360 # XXX check for errors here
361 361 store.put(source, hash)
362 362 at += 1
363 363 ui.progress(_('uploading largefiles'), None)
364 364
365 365 def verifylfiles(ui, repo, all=False, contents=False):
366 366 '''Verify that every largefile revision in the current changeset
367 367 exists in the central store. With --contents, also verify that
368 368 the contents of each local largefile file revision are correct (SHA-1 hash
369 369 matches the revision ID). With --all, check every changeset in
370 370 this repository.'''
371 371 if all:
372 372 revs = repo.revs('all()')
373 373 else:
374 374 revs = ['.']
375 375
376 376 store = basestore._openstore(repo)
377 377 return store.verify(revs, contents=contents)
378 378
379 379 def cachelfiles(ui, repo, node, filelist=None):
380 380 '''cachelfiles ensures that all largefiles needed by the specified revision
381 381 are present in the repository's largefile cache.
382 382
383 383 returns a tuple (cached, missing). cached is the list of files downloaded
384 384 by this operation; missing is the list of files that were needed but could
385 385 not be found.'''
386 386 lfiles = lfutil.listlfiles(repo, node)
387 387 if filelist:
388 388 lfiles = set(lfiles) & set(filelist)
389 389 toget = []
390 390
391 391 for lfile in lfiles:
392 392 try:
393 393 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
394 394 except IOError as err:
395 395 if err.errno == errno.ENOENT:
396 396 continue # node must be None and standin wasn't found in wctx
397 397 raise
398 398 if not lfutil.findfile(repo, expectedhash):
399 399 toget.append((lfile, expectedhash))
400 400
401 401 if toget:
402 402 store = basestore._openstore(repo)
403 403 ret = store.get(toget)
404 404 return ret
405 405
406 406 return ([], [])
407 407
408 408 def downloadlfiles(ui, repo, rev=None):
409 409 matchfn = scmutil.match(repo[None],
410 410 [repo.wjoin(lfutil.shortname)], {})
411 411 def prepare(ctx, fns):
412 412 pass
413 413 totalsuccess = 0
414 414 totalmissing = 0
415 415 if rev != []: # walkchangerevs on empty list would return all revs
416 416 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
417 417 prepare):
418 418 success, missing = cachelfiles(ui, repo, ctx.node())
419 419 totalsuccess += len(success)
420 420 totalmissing += len(missing)
421 421 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
422 422 if totalmissing > 0:
423 423 ui.status(_("%d largefiles failed to download\n") % totalmissing)
424 424 return totalsuccess, totalmissing
425 425
426 426 def updatelfiles(ui, repo, filelist=None, printmessage=None,
427 427 normallookup=False):
428 428 '''Update largefiles according to standins in the working directory
429 429
430 430 If ``printmessage`` is other than ``None``, it means "print (or
431 431 ignore, for false) message forcibly".
432 432 '''
433 433 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
434 434 wlock = repo.wlock()
435 435 try:
436 436 lfdirstate = lfutil.openlfdirstate(ui, repo)
437 437 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
438 438
439 439 if filelist is not None:
440 440 filelist = set(filelist)
441 441 lfiles = [f for f in lfiles if f in filelist]
442 442
443 443 update = {}
444 444 updated, removed = 0, 0
445 445 for lfile in lfiles:
446 446 abslfile = repo.wjoin(lfile)
447 abslfileorig = cmdutil.origpath(ui, repo, abslfile)
447 abslfileorig = scmutil.origpath(ui, repo, abslfile)
448 448 absstandin = repo.wjoin(lfutil.standin(lfile))
449 absstandinorig = cmdutil.origpath(ui, repo, absstandin)
449 absstandinorig = scmutil.origpath(ui, repo, absstandin)
450 450 if os.path.exists(absstandin):
451 451 if (os.path.exists(absstandinorig) and
452 452 os.path.exists(abslfile)):
453 453 shutil.copyfile(abslfile, abslfileorig)
454 454 util.unlinkpath(absstandinorig)
455 455 expecthash = lfutil.readstandin(repo, lfile)
456 456 if expecthash != '':
457 457 if lfile not in repo[None]: # not switched to normal file
458 458 util.unlinkpath(abslfile, ignoremissing=True)
459 459 # use normallookup() to allocate an entry in largefiles
460 460 # dirstate to prevent lfilesrepo.status() from reporting
461 461 # missing files as removed.
462 462 lfdirstate.normallookup(lfile)
463 463 update[lfile] = expecthash
464 464 else:
465 465 # Remove lfiles for which the standin is deleted, unless the
466 466 # lfile is added to the repository again. This happens when a
467 467 # largefile is converted back to a normal file: the standin
468 468 # disappears, but a new (normal) file appears as the lfile.
469 469 if (os.path.exists(abslfile) and
470 470 repo.dirstate.normalize(lfile) not in repo[None]):
471 471 util.unlinkpath(abslfile)
472 472 removed += 1
473 473
474 474 # largefile processing might be slow and be interrupted - be prepared
475 475 lfdirstate.write()
476 476
477 477 if lfiles:
478 478 statuswriter(_('getting changed largefiles\n'))
479 479 cachelfiles(ui, repo, None, lfiles)
480 480
481 481 for lfile in lfiles:
482 482 update1 = 0
483 483
484 484 expecthash = update.get(lfile)
485 485 if expecthash:
486 486 if not lfutil.copyfromcache(repo, expecthash, lfile):
487 487 # failed ... but already removed and set to normallookup
488 488 continue
489 489 # Synchronize largefile dirstate to the last modified
490 490 # time of the file
491 491 lfdirstate.normal(lfile)
492 492 update1 = 1
493 493
494 494 # copy the state of largefile standin from the repository's
495 495 # dirstate to its state in the lfdirstate.
496 496 abslfile = repo.wjoin(lfile)
497 497 absstandin = repo.wjoin(lfutil.standin(lfile))
498 498 if os.path.exists(absstandin):
499 499 mode = os.stat(absstandin).st_mode
500 500 if mode != os.stat(abslfile).st_mode:
501 501 os.chmod(abslfile, mode)
502 502 update1 = 1
503 503
504 504 updated += update1
505 505
506 506 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
507 507
508 508 lfdirstate.write()
509 509 if lfiles:
510 510 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
511 511 removed))
512 512 finally:
513 513 wlock.release()
514 514
515 515 @command('lfpull',
516 516 [('r', 'rev', [], _('pull largefiles for these revisions'))
517 517 ] + commands.remoteopts,
518 518 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
519 519 def lfpull(ui, repo, source="default", **opts):
520 520 """pull largefiles for the specified revisions from the specified source
521 521
522 522 Pull largefiles that are referenced from local changesets but missing
523 523 locally, pulling from a remote repository to the local cache.
524 524
525 525 If SOURCE is omitted, the 'default' path will be used.
526 526 See :hg:`help urls` for more information.
527 527
528 528 .. container:: verbose
529 529
530 530 Some examples:
531 531
532 532 - pull largefiles for all branch heads::
533 533
534 534 hg lfpull -r "head() and not closed()"
535 535
536 536 - pull largefiles on the default branch::
537 537
538 538 hg lfpull -r "branch(default)"
539 539 """
540 540 repo.lfpullsource = source
541 541
542 542 revs = opts.get('rev', [])
543 543 if not revs:
544 544 raise error.Abort(_('no revisions specified'))
545 545 revs = scmutil.revrange(repo, revs)
546 546
547 547 numcached = 0
548 548 for rev in revs:
549 549 ui.note(_('pulling largefiles for revision %s\n') % rev)
550 550 (cached, missing) = cachelfiles(ui, repo, rev)
551 551 numcached += len(cached)
552 552 ui.status(_("%d largefiles cached\n") % numcached)
@@ -1,3609 +1,3609
1 1 # mq.py - patch queues for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''manage a stack of patches
9 9
10 10 This extension lets you work with a stack of patches in a Mercurial
11 11 repository. It manages two stacks of patches - all known patches, and
12 12 applied patches (subset of known patches).
13 13
14 14 Known patches are represented as patch files in the .hg/patches
15 15 directory. Applied patches are both patch files and changesets.
16 16
17 17 Common tasks (use :hg:`help command` for more details)::
18 18
19 19 create new patch qnew
20 20 import existing patch qimport
21 21
22 22 print patch series qseries
23 23 print applied patches qapplied
24 24
25 25 add known patch to applied stack qpush
26 26 remove patch from applied stack qpop
27 27 refresh contents of top applied patch qrefresh
28 28
29 29 By default, mq will automatically use git patches when required to
30 30 avoid losing file mode changes, copy records, binary files or empty
31 31 files creations or deletions. This behavior can be configured with::
32 32
33 33 [mq]
34 34 git = auto/keep/yes/no
35 35
36 36 If set to 'keep', mq will obey the [diff] section configuration while
37 37 preserving existing git patches upon qrefresh. If set to 'yes' or
38 38 'no', mq will override the [diff] section and always generate git or
39 39 regular patches, possibly losing data in the second case.
40 40
41 41 It may be desirable for mq changesets to be kept in the secret phase (see
42 42 :hg:`help phases`), which can be enabled with the following setting::
43 43
44 44 [mq]
45 45 secret = True
46 46
47 47 You will by default be managing a patch queue named "patches". You can
48 48 create other, independent patch queues with the :hg:`qqueue` command.
49 49
50 50 If the working directory contains uncommitted files, qpush, qpop and
51 51 qgoto abort immediately. If -f/--force is used, the changes are
52 52 discarded. Setting::
53 53
54 54 [mq]
55 55 keepchanges = True
56 56
57 57 make them behave as if --keep-changes were passed, and non-conflicting
58 58 local changes will be tolerated and preserved. If incompatible options
59 59 such as -f/--force or --exact are passed, this setting is ignored.
60 60
61 61 This extension used to provide a strip command. This command now lives
62 62 in the strip extension.
63 63 '''
64 64
65 65 from mercurial.i18n import _
66 66 from mercurial.node import bin, hex, short, nullid, nullrev
67 67 from mercurial.lock import release
68 68 from mercurial import commands, cmdutil, hg, scmutil, util, revset
69 69 from mercurial import extensions, error, phases
70 70 from mercurial import patch as patchmod
71 71 from mercurial import lock as lockmod
72 72 from mercurial import localrepo
73 73 from mercurial import subrepo
74 74 import os, re, errno, shutil
75 75
76 76 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
77 77
78 78 cmdtable = {}
79 79 command = cmdutil.command(cmdtable)
80 80 # Note for extension authors: ONLY specify testedwith = 'internal' for
81 81 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
82 82 # be specifying the version(s) of Mercurial they are tested with, or
83 83 # leave the attribute unspecified.
84 84 testedwith = 'internal'
85 85
86 86 # force load strip extension formerly included in mq and import some utility
87 87 try:
88 88 stripext = extensions.find('strip')
89 89 except KeyError:
90 90 # note: load is lazy so we could avoid the try-except,
91 91 # but I (marmoute) prefer this explicit code.
92 92 class dummyui(object):
93 93 def debug(self, msg):
94 94 pass
95 95 stripext = extensions.load(dummyui(), 'strip', '')
96 96
97 97 strip = stripext.strip
98 98 checksubstate = stripext.checksubstate
99 99 checklocalchanges = stripext.checklocalchanges
100 100
101 101
102 102 # Patch names looks like unix-file names.
103 103 # They must be joinable with queue directory and result in the patch path.
104 104 normname = util.normpath
105 105
106 106 class statusentry(object):
107 107 def __init__(self, node, name):
108 108 self.node, self.name = node, name
109 109 def __repr__(self):
110 110 return hex(self.node) + ':' + self.name
111 111
112 112 # The order of the headers in 'hg export' HG patches:
113 113 HGHEADERS = [
114 114 # '# HG changeset patch',
115 115 '# User ',
116 116 '# Date ',
117 117 '# ',
118 118 '# Branch ',
119 119 '# Node ID ',
120 120 '# Parent ', # can occur twice for merges - but that is not relevant for mq
121 121 ]
122 122 # The order of headers in plain 'mail style' patches:
123 123 PLAINHEADERS = {
124 124 'from': 0,
125 125 'date': 1,
126 126 'subject': 2,
127 127 }
128 128
129 129 def inserthgheader(lines, header, value):
130 130 """Assuming lines contains a HG patch header, add a header line with value.
131 131 >>> try: inserthgheader([], '# Date ', 'z')
132 132 ... except ValueError, inst: print "oops"
133 133 oops
134 134 >>> inserthgheader(['# HG changeset patch'], '# Date ', 'z')
135 135 ['# HG changeset patch', '# Date z']
136 136 >>> inserthgheader(['# HG changeset patch', ''], '# Date ', 'z')
137 137 ['# HG changeset patch', '# Date z', '']
138 138 >>> inserthgheader(['# HG changeset patch', '# User y'], '# Date ', 'z')
139 139 ['# HG changeset patch', '# User y', '# Date z']
140 140 >>> inserthgheader(['# HG changeset patch', '# Date x', '# User y'],
141 141 ... '# User ', 'z')
142 142 ['# HG changeset patch', '# Date x', '# User z']
143 143 >>> inserthgheader(['# HG changeset patch', '# Date y'], '# Date ', 'z')
144 144 ['# HG changeset patch', '# Date z']
145 145 >>> inserthgheader(['# HG changeset patch', '', '# Date y'], '# Date ', 'z')
146 146 ['# HG changeset patch', '# Date z', '', '# Date y']
147 147 >>> inserthgheader(['# HG changeset patch', '# Parent y'], '# Date ', 'z')
148 148 ['# HG changeset patch', '# Date z', '# Parent y']
149 149 """
150 150 start = lines.index('# HG changeset patch') + 1
151 151 newindex = HGHEADERS.index(header)
152 152 bestpos = len(lines)
153 153 for i in range(start, len(lines)):
154 154 line = lines[i]
155 155 if not line.startswith('# '):
156 156 bestpos = min(bestpos, i)
157 157 break
158 158 for lineindex, h in enumerate(HGHEADERS):
159 159 if line.startswith(h):
160 160 if lineindex == newindex:
161 161 lines[i] = header + value
162 162 return lines
163 163 if lineindex > newindex:
164 164 bestpos = min(bestpos, i)
165 165 break # next line
166 166 lines.insert(bestpos, header + value)
167 167 return lines
168 168
169 169 def insertplainheader(lines, header, value):
170 170 """For lines containing a plain patch header, add a header line with value.
171 171 >>> insertplainheader([], 'Date', 'z')
172 172 ['Date: z']
173 173 >>> insertplainheader([''], 'Date', 'z')
174 174 ['Date: z', '']
175 175 >>> insertplainheader(['x'], 'Date', 'z')
176 176 ['Date: z', '', 'x']
177 177 >>> insertplainheader(['From: y', 'x'], 'Date', 'z')
178 178 ['From: y', 'Date: z', '', 'x']
179 179 >>> insertplainheader([' date : x', ' from : y', ''], 'From', 'z')
180 180 [' date : x', 'From: z', '']
181 181 >>> insertplainheader(['', 'Date: y'], 'Date', 'z')
182 182 ['Date: z', '', 'Date: y']
183 183 >>> insertplainheader(['foo: bar', 'DATE: z', 'x'], 'From', 'y')
184 184 ['From: y', 'foo: bar', 'DATE: z', '', 'x']
185 185 """
186 186 newprio = PLAINHEADERS[header.lower()]
187 187 bestpos = len(lines)
188 188 for i, line in enumerate(lines):
189 189 if ':' in line:
190 190 lheader = line.split(':', 1)[0].strip().lower()
191 191 lprio = PLAINHEADERS.get(lheader, newprio + 1)
192 192 if lprio == newprio:
193 193 lines[i] = '%s: %s' % (header, value)
194 194 return lines
195 195 if lprio > newprio and i < bestpos:
196 196 bestpos = i
197 197 else:
198 198 if line:
199 199 lines.insert(i, '')
200 200 if i < bestpos:
201 201 bestpos = i
202 202 break
203 203 lines.insert(bestpos, '%s: %s' % (header, value))
204 204 return lines
205 205
206 206 class patchheader(object):
207 207 def __init__(self, pf, plainmode=False):
208 208 def eatdiff(lines):
209 209 while lines:
210 210 l = lines[-1]
211 211 if (l.startswith("diff -") or
212 212 l.startswith("Index:") or
213 213 l.startswith("===========")):
214 214 del lines[-1]
215 215 else:
216 216 break
217 217 def eatempty(lines):
218 218 while lines:
219 219 if not lines[-1].strip():
220 220 del lines[-1]
221 221 else:
222 222 break
223 223
224 224 message = []
225 225 comments = []
226 226 user = None
227 227 date = None
228 228 parent = None
229 229 format = None
230 230 subject = None
231 231 branch = None
232 232 nodeid = None
233 233 diffstart = 0
234 234
235 235 for line in file(pf):
236 236 line = line.rstrip()
237 237 if (line.startswith('diff --git')
238 238 or (diffstart and line.startswith('+++ '))):
239 239 diffstart = 2
240 240 break
241 241 diffstart = 0 # reset
242 242 if line.startswith("--- "):
243 243 diffstart = 1
244 244 continue
245 245 elif format == "hgpatch":
246 246 # parse values when importing the result of an hg export
247 247 if line.startswith("# User "):
248 248 user = line[7:]
249 249 elif line.startswith("# Date "):
250 250 date = line[7:]
251 251 elif line.startswith("# Parent "):
252 252 parent = line[9:].lstrip() # handle double trailing space
253 253 elif line.startswith("# Branch "):
254 254 branch = line[9:]
255 255 elif line.startswith("# Node ID "):
256 256 nodeid = line[10:]
257 257 elif not line.startswith("# ") and line:
258 258 message.append(line)
259 259 format = None
260 260 elif line == '# HG changeset patch':
261 261 message = []
262 262 format = "hgpatch"
263 263 elif (format != "tagdone" and (line.startswith("Subject: ") or
264 264 line.startswith("subject: "))):
265 265 subject = line[9:]
266 266 format = "tag"
267 267 elif (format != "tagdone" and (line.startswith("From: ") or
268 268 line.startswith("from: "))):
269 269 user = line[6:]
270 270 format = "tag"
271 271 elif (format != "tagdone" and (line.startswith("Date: ") or
272 272 line.startswith("date: "))):
273 273 date = line[6:]
274 274 format = "tag"
275 275 elif format == "tag" and line == "":
276 276 # when looking for tags (subject: from: etc) they
277 277 # end once you find a blank line in the source
278 278 format = "tagdone"
279 279 elif message or line:
280 280 message.append(line)
281 281 comments.append(line)
282 282
283 283 eatdiff(message)
284 284 eatdiff(comments)
285 285 # Remember the exact starting line of the patch diffs before consuming
286 286 # empty lines, for external use by TortoiseHg and others
287 287 self.diffstartline = len(comments)
288 288 eatempty(message)
289 289 eatempty(comments)
290 290
291 291 # make sure message isn't empty
292 292 if format and format.startswith("tag") and subject:
293 293 message.insert(0, subject)
294 294
295 295 self.message = message
296 296 self.comments = comments
297 297 self.user = user
298 298 self.date = date
299 299 self.parent = parent
300 300 # nodeid and branch are for external use by TortoiseHg and others
301 301 self.nodeid = nodeid
302 302 self.branch = branch
303 303 self.haspatch = diffstart > 1
304 304 self.plainmode = (plainmode or
305 305 '# HG changeset patch' not in self.comments and
306 306 any(c.startswith('Date: ') or
307 307 c.startswith('From: ')
308 308 for c in self.comments))
309 309
310 310 def setuser(self, user):
311 311 try:
312 312 inserthgheader(self.comments, '# User ', user)
313 313 except ValueError:
314 314 if self.plainmode:
315 315 insertplainheader(self.comments, 'From', user)
316 316 else:
317 317 tmp = ['# HG changeset patch', '# User ' + user]
318 318 self.comments = tmp + self.comments
319 319 self.user = user
320 320
321 321 def setdate(self, date):
322 322 try:
323 323 inserthgheader(self.comments, '# Date ', date)
324 324 except ValueError:
325 325 if self.plainmode:
326 326 insertplainheader(self.comments, 'Date', date)
327 327 else:
328 328 tmp = ['# HG changeset patch', '# Date ' + date]
329 329 self.comments = tmp + self.comments
330 330 self.date = date
331 331
332 332 def setparent(self, parent):
333 333 try:
334 334 inserthgheader(self.comments, '# Parent ', parent)
335 335 except ValueError:
336 336 if not self.plainmode:
337 337 tmp = ['# HG changeset patch', '# Parent ' + parent]
338 338 self.comments = tmp + self.comments
339 339 self.parent = parent
340 340
341 341 def setmessage(self, message):
342 342 if self.comments:
343 343 self._delmsg()
344 344 self.message = [message]
345 345 if message:
346 346 if self.plainmode and self.comments and self.comments[-1]:
347 347 self.comments.append('')
348 348 self.comments.append(message)
349 349
350 350 def __str__(self):
351 351 s = '\n'.join(self.comments).rstrip()
352 352 if not s:
353 353 return ''
354 354 return s + '\n\n'
355 355
356 356 def _delmsg(self):
357 357 '''Remove existing message, keeping the rest of the comments fields.
358 358 If comments contains 'subject: ', message will prepend
359 359 the field and a blank line.'''
360 360 if self.message:
361 361 subj = 'subject: ' + self.message[0].lower()
362 362 for i in xrange(len(self.comments)):
363 363 if subj == self.comments[i].lower():
364 364 del self.comments[i]
365 365 self.message = self.message[2:]
366 366 break
367 367 ci = 0
368 368 for mi in self.message:
369 369 while mi != self.comments[ci]:
370 370 ci += 1
371 371 del self.comments[ci]
372 372
373 373 def newcommit(repo, phase, *args, **kwargs):
374 374 """helper dedicated to ensure a commit respect mq.secret setting
375 375
376 376 It should be used instead of repo.commit inside the mq source for operation
377 377 creating new changeset.
378 378 """
379 379 repo = repo.unfiltered()
380 380 if phase is None:
381 381 if repo.ui.configbool('mq', 'secret', False):
382 382 phase = phases.secret
383 383 if phase is not None:
384 384 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
385 385 allowemptybackup = repo.ui.backupconfig('ui', 'allowemptycommit')
386 386 try:
387 387 if phase is not None:
388 388 repo.ui.setconfig('phases', 'new-commit', phase, 'mq')
389 389 repo.ui.setconfig('ui', 'allowemptycommit', True)
390 390 return repo.commit(*args, **kwargs)
391 391 finally:
392 392 repo.ui.restoreconfig(allowemptybackup)
393 393 if phase is not None:
394 394 repo.ui.restoreconfig(phasebackup)
395 395
396 396 class AbortNoCleanup(error.Abort):
397 397 pass
398 398
399 399 def makepatchname(existing, title, fallbackname):
400 400 """Return a suitable filename for title, adding a suffix to make
401 401 it unique in the existing list"""
402 402 namebase = re.sub('[\s\W_]+', '_', title.lower()).strip('_')
403 403 if not namebase:
404 404 namebase = fallbackname
405 405 name = namebase
406 406 i = 0
407 407 while name in existing:
408 408 i += 1
409 409 name = '%s__%s' % (namebase, i)
410 410 return name
411 411
412 412 class queue(object):
413 413 def __init__(self, ui, baseui, path, patchdir=None):
414 414 self.basepath = path
415 415 try:
416 416 fh = open(os.path.join(path, 'patches.queue'))
417 417 cur = fh.read().rstrip()
418 418 fh.close()
419 419 if not cur:
420 420 curpath = os.path.join(path, 'patches')
421 421 else:
422 422 curpath = os.path.join(path, 'patches-' + cur)
423 423 except IOError:
424 424 curpath = os.path.join(path, 'patches')
425 425 self.path = patchdir or curpath
426 426 self.opener = scmutil.opener(self.path)
427 427 self.ui = ui
428 428 self.baseui = baseui
429 429 self.applieddirty = False
430 430 self.seriesdirty = False
431 431 self.added = []
432 432 self.seriespath = "series"
433 433 self.statuspath = "status"
434 434 self.guardspath = "guards"
435 435 self.activeguards = None
436 436 self.guardsdirty = False
437 437 # Handle mq.git as a bool with extended values
438 438 try:
439 439 gitmode = ui.configbool('mq', 'git', None)
440 440 if gitmode is None:
441 441 raise error.ConfigError
442 442 if gitmode:
443 443 self.gitmode = 'yes'
444 444 else:
445 445 self.gitmode = 'no'
446 446 except error.ConfigError:
447 447 # let's have check-config ignore the type mismatch
448 448 self.gitmode = ui.config(r'mq', 'git', 'auto').lower()
449 449 # deprecated config: mq.plain
450 450 self.plainmode = ui.configbool('mq', 'plain', False)
451 451 self.checkapplied = True
452 452
453 453 @util.propertycache
454 454 def applied(self):
455 455 def parselines(lines):
456 456 for l in lines:
457 457 entry = l.split(':', 1)
458 458 if len(entry) > 1:
459 459 n, name = entry
460 460 yield statusentry(bin(n), name)
461 461 elif l.strip():
462 462 self.ui.warn(_('malformated mq status line: %s\n') % entry)
463 463 # else we ignore empty lines
464 464 try:
465 465 lines = self.opener.read(self.statuspath).splitlines()
466 466 return list(parselines(lines))
467 467 except IOError as e:
468 468 if e.errno == errno.ENOENT:
469 469 return []
470 470 raise
471 471
472 472 @util.propertycache
473 473 def fullseries(self):
474 474 try:
475 475 return self.opener.read(self.seriespath).splitlines()
476 476 except IOError as e:
477 477 if e.errno == errno.ENOENT:
478 478 return []
479 479 raise
480 480
481 481 @util.propertycache
482 482 def series(self):
483 483 self.parseseries()
484 484 return self.series
485 485
486 486 @util.propertycache
487 487 def seriesguards(self):
488 488 self.parseseries()
489 489 return self.seriesguards
490 490
491 491 def invalidate(self):
492 492 for a in 'applied fullseries series seriesguards'.split():
493 493 if a in self.__dict__:
494 494 delattr(self, a)
495 495 self.applieddirty = False
496 496 self.seriesdirty = False
497 497 self.guardsdirty = False
498 498 self.activeguards = None
499 499
500 500 def diffopts(self, opts=None, patchfn=None):
501 501 diffopts = patchmod.diffopts(self.ui, opts)
502 502 if self.gitmode == 'auto':
503 503 diffopts.upgrade = True
504 504 elif self.gitmode == 'keep':
505 505 pass
506 506 elif self.gitmode in ('yes', 'no'):
507 507 diffopts.git = self.gitmode == 'yes'
508 508 else:
509 509 raise error.Abort(_('mq.git option can be auto/keep/yes/no'
510 510 ' got %s') % self.gitmode)
511 511 if patchfn:
512 512 diffopts = self.patchopts(diffopts, patchfn)
513 513 return diffopts
514 514
515 515 def patchopts(self, diffopts, *patches):
516 516 """Return a copy of input diff options with git set to true if
517 517 referenced patch is a git patch and should be preserved as such.
518 518 """
519 519 diffopts = diffopts.copy()
520 520 if not diffopts.git and self.gitmode == 'keep':
521 521 for patchfn in patches:
522 522 patchf = self.opener(patchfn, 'r')
523 523 # if the patch was a git patch, refresh it as a git patch
524 524 for line in patchf:
525 525 if line.startswith('diff --git'):
526 526 diffopts.git = True
527 527 break
528 528 patchf.close()
529 529 return diffopts
530 530
531 531 def join(self, *p):
532 532 return os.path.join(self.path, *p)
533 533
534 534 def findseries(self, patch):
535 535 def matchpatch(l):
536 536 l = l.split('#', 1)[0]
537 537 return l.strip() == patch
538 538 for index, l in enumerate(self.fullseries):
539 539 if matchpatch(l):
540 540 return index
541 541 return None
542 542
543 543 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
544 544
545 545 def parseseries(self):
546 546 self.series = []
547 547 self.seriesguards = []
548 548 for l in self.fullseries:
549 549 h = l.find('#')
550 550 if h == -1:
551 551 patch = l
552 552 comment = ''
553 553 elif h == 0:
554 554 continue
555 555 else:
556 556 patch = l[:h]
557 557 comment = l[h:]
558 558 patch = patch.strip()
559 559 if patch:
560 560 if patch in self.series:
561 561 raise error.Abort(_('%s appears more than once in %s') %
562 562 (patch, self.join(self.seriespath)))
563 563 self.series.append(patch)
564 564 self.seriesguards.append(self.guard_re.findall(comment))
565 565
566 566 def checkguard(self, guard):
567 567 if not guard:
568 568 return _('guard cannot be an empty string')
569 569 bad_chars = '# \t\r\n\f'
570 570 first = guard[0]
571 571 if first in '-+':
572 572 return (_('guard %r starts with invalid character: %r') %
573 573 (guard, first))
574 574 for c in bad_chars:
575 575 if c in guard:
576 576 return _('invalid character in guard %r: %r') % (guard, c)
577 577
578 578 def setactive(self, guards):
579 579 for guard in guards:
580 580 bad = self.checkguard(guard)
581 581 if bad:
582 582 raise error.Abort(bad)
583 583 guards = sorted(set(guards))
584 584 self.ui.debug('active guards: %s\n' % ' '.join(guards))
585 585 self.activeguards = guards
586 586 self.guardsdirty = True
587 587
588 588 def active(self):
589 589 if self.activeguards is None:
590 590 self.activeguards = []
591 591 try:
592 592 guards = self.opener.read(self.guardspath).split()
593 593 except IOError as err:
594 594 if err.errno != errno.ENOENT:
595 595 raise
596 596 guards = []
597 597 for i, guard in enumerate(guards):
598 598 bad = self.checkguard(guard)
599 599 if bad:
600 600 self.ui.warn('%s:%d: %s\n' %
601 601 (self.join(self.guardspath), i + 1, bad))
602 602 else:
603 603 self.activeguards.append(guard)
604 604 return self.activeguards
605 605
606 606 def setguards(self, idx, guards):
607 607 for g in guards:
608 608 if len(g) < 2:
609 609 raise error.Abort(_('guard %r too short') % g)
610 610 if g[0] not in '-+':
611 611 raise error.Abort(_('guard %r starts with invalid char') % g)
612 612 bad = self.checkguard(g[1:])
613 613 if bad:
614 614 raise error.Abort(bad)
615 615 drop = self.guard_re.sub('', self.fullseries[idx])
616 616 self.fullseries[idx] = drop + ''.join([' #' + g for g in guards])
617 617 self.parseseries()
618 618 self.seriesdirty = True
619 619
620 620 def pushable(self, idx):
621 621 if isinstance(idx, str):
622 622 idx = self.series.index(idx)
623 623 patchguards = self.seriesguards[idx]
624 624 if not patchguards:
625 625 return True, None
626 626 guards = self.active()
627 627 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
628 628 if exactneg:
629 629 return False, repr(exactneg[0])
630 630 pos = [g for g in patchguards if g[0] == '+']
631 631 exactpos = [g for g in pos if g[1:] in guards]
632 632 if pos:
633 633 if exactpos:
634 634 return True, repr(exactpos[0])
635 635 return False, ' '.join(map(repr, pos))
636 636 return True, ''
637 637
638 638 def explainpushable(self, idx, all_patches=False):
639 639 if all_patches:
640 640 write = self.ui.write
641 641 else:
642 642 write = self.ui.warn
643 643
644 644 if all_patches or self.ui.verbose:
645 645 if isinstance(idx, str):
646 646 idx = self.series.index(idx)
647 647 pushable, why = self.pushable(idx)
648 648 if all_patches and pushable:
649 649 if why is None:
650 650 write(_('allowing %s - no guards in effect\n') %
651 651 self.series[idx])
652 652 else:
653 653 if not why:
654 654 write(_('allowing %s - no matching negative guards\n') %
655 655 self.series[idx])
656 656 else:
657 657 write(_('allowing %s - guarded by %s\n') %
658 658 (self.series[idx], why))
659 659 if not pushable:
660 660 if why:
661 661 write(_('skipping %s - guarded by %s\n') %
662 662 (self.series[idx], why))
663 663 else:
664 664 write(_('skipping %s - no matching guards\n') %
665 665 self.series[idx])
666 666
667 667 def savedirty(self):
668 668 def writelist(items, path):
669 669 fp = self.opener(path, 'w')
670 670 for i in items:
671 671 fp.write("%s\n" % i)
672 672 fp.close()
673 673 if self.applieddirty:
674 674 writelist(map(str, self.applied), self.statuspath)
675 675 self.applieddirty = False
676 676 if self.seriesdirty:
677 677 writelist(self.fullseries, self.seriespath)
678 678 self.seriesdirty = False
679 679 if self.guardsdirty:
680 680 writelist(self.activeguards, self.guardspath)
681 681 self.guardsdirty = False
682 682 if self.added:
683 683 qrepo = self.qrepo()
684 684 if qrepo:
685 685 qrepo[None].add(f for f in self.added if f not in qrepo[None])
686 686 self.added = []
687 687
688 688 def removeundo(self, repo):
689 689 undo = repo.sjoin('undo')
690 690 if not os.path.exists(undo):
691 691 return
692 692 try:
693 693 os.unlink(undo)
694 694 except OSError as inst:
695 695 self.ui.warn(_('error removing undo: %s\n') % str(inst))
696 696
697 697 def backup(self, repo, files, copy=False):
698 698 # backup local changes in --force case
699 699 for f in sorted(files):
700 700 absf = repo.wjoin(f)
701 701 if os.path.lexists(absf):
702 702 self.ui.note(_('saving current version of %s as %s\n') %
703 (f, cmdutil.origpath(self.ui, repo, f)))
704
705 absorig = cmdutil.origpath(self.ui, repo, absf)
703 (f, scmutil.origpath(self.ui, repo, f)))
704
705 absorig = scmutil.origpath(self.ui, repo, absf)
706 706 if copy:
707 707 util.copyfile(absf, absorig)
708 708 else:
709 709 util.rename(absf, absorig)
710 710
711 711 def printdiff(self, repo, diffopts, node1, node2=None, files=None,
712 712 fp=None, changes=None, opts={}):
713 713 stat = opts.get('stat')
714 714 m = scmutil.match(repo[node1], files, opts)
715 715 cmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m,
716 716 changes, stat, fp)
717 717
718 718 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
719 719 # first try just applying the patch
720 720 (err, n) = self.apply(repo, [patch], update_status=False,
721 721 strict=True, merge=rev)
722 722
723 723 if err == 0:
724 724 return (err, n)
725 725
726 726 if n is None:
727 727 raise error.Abort(_("apply failed for patch %s") % patch)
728 728
729 729 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
730 730
731 731 # apply failed, strip away that rev and merge.
732 732 hg.clean(repo, head)
733 733 strip(self.ui, repo, [n], update=False, backup=False)
734 734
735 735 ctx = repo[rev]
736 736 ret = hg.merge(repo, rev)
737 737 if ret:
738 738 raise error.Abort(_("update returned %d") % ret)
739 739 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
740 740 if n is None:
741 741 raise error.Abort(_("repo commit failed"))
742 742 try:
743 743 ph = patchheader(mergeq.join(patch), self.plainmode)
744 744 except Exception:
745 745 raise error.Abort(_("unable to read %s") % patch)
746 746
747 747 diffopts = self.patchopts(diffopts, patch)
748 748 patchf = self.opener(patch, "w")
749 749 comments = str(ph)
750 750 if comments:
751 751 patchf.write(comments)
752 752 self.printdiff(repo, diffopts, head, n, fp=patchf)
753 753 patchf.close()
754 754 self.removeundo(repo)
755 755 return (0, n)
756 756
757 757 def qparents(self, repo, rev=None):
758 758 """return the mq handled parent or p1
759 759
760 760 In some case where mq get himself in being the parent of a merge the
761 761 appropriate parent may be p2.
762 762 (eg: an in progress merge started with mq disabled)
763 763
764 764 If no parent are managed by mq, p1 is returned.
765 765 """
766 766 if rev is None:
767 767 (p1, p2) = repo.dirstate.parents()
768 768 if p2 == nullid:
769 769 return p1
770 770 if not self.applied:
771 771 return None
772 772 return self.applied[-1].node
773 773 p1, p2 = repo.changelog.parents(rev)
774 774 if p2 != nullid and p2 in [x.node for x in self.applied]:
775 775 return p2
776 776 return p1
777 777
778 778 def mergepatch(self, repo, mergeq, series, diffopts):
779 779 if not self.applied:
780 780 # each of the patches merged in will have two parents. This
781 781 # can confuse the qrefresh, qdiff, and strip code because it
782 782 # needs to know which parent is actually in the patch queue.
783 783 # so, we insert a merge marker with only one parent. This way
784 784 # the first patch in the queue is never a merge patch
785 785 #
786 786 pname = ".hg.patches.merge.marker"
787 787 n = newcommit(repo, None, '[mq]: merge marker', force=True)
788 788 self.removeundo(repo)
789 789 self.applied.append(statusentry(n, pname))
790 790 self.applieddirty = True
791 791
792 792 head = self.qparents(repo)
793 793
794 794 for patch in series:
795 795 patch = mergeq.lookup(patch, strict=True)
796 796 if not patch:
797 797 self.ui.warn(_("patch %s does not exist\n") % patch)
798 798 return (1, None)
799 799 pushable, reason = self.pushable(patch)
800 800 if not pushable:
801 801 self.explainpushable(patch, all_patches=True)
802 802 continue
803 803 info = mergeq.isapplied(patch)
804 804 if not info:
805 805 self.ui.warn(_("patch %s is not applied\n") % patch)
806 806 return (1, None)
807 807 rev = info[1]
808 808 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
809 809 if head:
810 810 self.applied.append(statusentry(head, patch))
811 811 self.applieddirty = True
812 812 if err:
813 813 return (err, head)
814 814 self.savedirty()
815 815 return (0, head)
816 816
817 817 def patch(self, repo, patchfile):
818 818 '''Apply patchfile to the working directory.
819 819 patchfile: name of patch file'''
820 820 files = set()
821 821 try:
822 822 fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
823 823 files=files, eolmode=None)
824 824 return (True, list(files), fuzz)
825 825 except Exception as inst:
826 826 self.ui.note(str(inst) + '\n')
827 827 if not self.ui.verbose:
828 828 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
829 829 self.ui.traceback()
830 830 return (False, list(files), False)
831 831
832 832 def apply(self, repo, series, list=False, update_status=True,
833 833 strict=False, patchdir=None, merge=None, all_files=None,
834 834 tobackup=None, keepchanges=False):
835 835 wlock = lock = tr = None
836 836 try:
837 837 wlock = repo.wlock()
838 838 lock = repo.lock()
839 839 tr = repo.transaction("qpush")
840 840 try:
841 841 ret = self._apply(repo, series, list, update_status,
842 842 strict, patchdir, merge, all_files=all_files,
843 843 tobackup=tobackup, keepchanges=keepchanges)
844 844 tr.close()
845 845 self.savedirty()
846 846 return ret
847 847 except AbortNoCleanup:
848 848 tr.close()
849 849 self.savedirty()
850 850 raise
851 851 except: # re-raises
852 852 try:
853 853 tr.abort()
854 854 finally:
855 855 self.invalidate()
856 856 raise
857 857 finally:
858 858 release(tr, lock, wlock)
859 859 self.removeundo(repo)
860 860
861 861 def _apply(self, repo, series, list=False, update_status=True,
862 862 strict=False, patchdir=None, merge=None, all_files=None,
863 863 tobackup=None, keepchanges=False):
864 864 """returns (error, hash)
865 865
866 866 error = 1 for unable to read, 2 for patch failed, 3 for patch
867 867 fuzz. tobackup is None or a set of files to backup before they
868 868 are modified by a patch.
869 869 """
870 870 # TODO unify with commands.py
871 871 if not patchdir:
872 872 patchdir = self.path
873 873 err = 0
874 874 n = None
875 875 for patchname in series:
876 876 pushable, reason = self.pushable(patchname)
877 877 if not pushable:
878 878 self.explainpushable(patchname, all_patches=True)
879 879 continue
880 880 self.ui.status(_("applying %s\n") % patchname)
881 881 pf = os.path.join(patchdir, patchname)
882 882
883 883 try:
884 884 ph = patchheader(self.join(patchname), self.plainmode)
885 885 except IOError:
886 886 self.ui.warn(_("unable to read %s\n") % patchname)
887 887 err = 1
888 888 break
889 889
890 890 message = ph.message
891 891 if not message:
892 892 # The commit message should not be translated
893 893 message = "imported patch %s\n" % patchname
894 894 else:
895 895 if list:
896 896 # The commit message should not be translated
897 897 message.append("\nimported patch %s" % patchname)
898 898 message = '\n'.join(message)
899 899
900 900 if ph.haspatch:
901 901 if tobackup:
902 902 touched = patchmod.changedfiles(self.ui, repo, pf)
903 903 touched = set(touched) & tobackup
904 904 if touched and keepchanges:
905 905 raise AbortNoCleanup(
906 906 _("conflicting local changes found"),
907 907 hint=_("did you forget to qrefresh?"))
908 908 self.backup(repo, touched, copy=True)
909 909 tobackup = tobackup - touched
910 910 (patcherr, files, fuzz) = self.patch(repo, pf)
911 911 if all_files is not None:
912 912 all_files.update(files)
913 913 patcherr = not patcherr
914 914 else:
915 915 self.ui.warn(_("patch %s is empty\n") % patchname)
916 916 patcherr, files, fuzz = 0, [], 0
917 917
918 918 if merge and files:
919 919 # Mark as removed/merged and update dirstate parent info
920 920 removed = []
921 921 merged = []
922 922 for f in files:
923 923 if os.path.lexists(repo.wjoin(f)):
924 924 merged.append(f)
925 925 else:
926 926 removed.append(f)
927 927 repo.dirstate.beginparentchange()
928 928 for f in removed:
929 929 repo.dirstate.remove(f)
930 930 for f in merged:
931 931 repo.dirstate.merge(f)
932 932 p1, p2 = repo.dirstate.parents()
933 933 repo.setparents(p1, merge)
934 934 repo.dirstate.endparentchange()
935 935
936 936 if all_files and '.hgsubstate' in all_files:
937 937 wctx = repo[None]
938 938 pctx = repo['.']
939 939 overwrite = False
940 940 mergedsubstate = subrepo.submerge(repo, pctx, wctx, wctx,
941 941 overwrite)
942 942 files += mergedsubstate.keys()
943 943
944 944 match = scmutil.matchfiles(repo, files or [])
945 945 oldtip = repo['tip']
946 946 n = newcommit(repo, None, message, ph.user, ph.date, match=match,
947 947 force=True)
948 948 if repo['tip'] == oldtip:
949 949 raise error.Abort(_("qpush exactly duplicates child changeset"))
950 950 if n is None:
951 951 raise error.Abort(_("repository commit failed"))
952 952
953 953 if update_status:
954 954 self.applied.append(statusentry(n, patchname))
955 955
956 956 if patcherr:
957 957 self.ui.warn(_("patch failed, rejects left in working "
958 958 "directory\n"))
959 959 err = 2
960 960 break
961 961
962 962 if fuzz and strict:
963 963 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
964 964 err = 3
965 965 break
966 966 return (err, n)
967 967
968 968 def _cleanup(self, patches, numrevs, keep=False):
969 969 if not keep:
970 970 r = self.qrepo()
971 971 if r:
972 972 r[None].forget(patches)
973 973 for p in patches:
974 974 try:
975 975 os.unlink(self.join(p))
976 976 except OSError as inst:
977 977 if inst.errno != errno.ENOENT:
978 978 raise
979 979
980 980 qfinished = []
981 981 if numrevs:
982 982 qfinished = self.applied[:numrevs]
983 983 del self.applied[:numrevs]
984 984 self.applieddirty = True
985 985
986 986 unknown = []
987 987
988 988 for (i, p) in sorted([(self.findseries(p), p) for p in patches],
989 989 reverse=True):
990 990 if i is not None:
991 991 del self.fullseries[i]
992 992 else:
993 993 unknown.append(p)
994 994
995 995 if unknown:
996 996 if numrevs:
997 997 rev = dict((entry.name, entry.node) for entry in qfinished)
998 998 for p in unknown:
999 999 msg = _('revision %s refers to unknown patches: %s\n')
1000 1000 self.ui.warn(msg % (short(rev[p]), p))
1001 1001 else:
1002 1002 msg = _('unknown patches: %s\n')
1003 1003 raise error.Abort(''.join(msg % p for p in unknown))
1004 1004
1005 1005 self.parseseries()
1006 1006 self.seriesdirty = True
1007 1007 return [entry.node for entry in qfinished]
1008 1008
1009 1009 def _revpatches(self, repo, revs):
1010 1010 firstrev = repo[self.applied[0].node].rev()
1011 1011 patches = []
1012 1012 for i, rev in enumerate(revs):
1013 1013
1014 1014 if rev < firstrev:
1015 1015 raise error.Abort(_('revision %d is not managed') % rev)
1016 1016
1017 1017 ctx = repo[rev]
1018 1018 base = self.applied[i].node
1019 1019 if ctx.node() != base:
1020 1020 msg = _('cannot delete revision %d above applied patches')
1021 1021 raise error.Abort(msg % rev)
1022 1022
1023 1023 patch = self.applied[i].name
1024 1024 for fmt in ('[mq]: %s', 'imported patch %s'):
1025 1025 if ctx.description() == fmt % patch:
1026 1026 msg = _('patch %s finalized without changeset message\n')
1027 1027 repo.ui.status(msg % patch)
1028 1028 break
1029 1029
1030 1030 patches.append(patch)
1031 1031 return patches
1032 1032
1033 1033 def finish(self, repo, revs):
1034 1034 # Manually trigger phase computation to ensure phasedefaults is
1035 1035 # executed before we remove the patches.
1036 1036 repo._phasecache
1037 1037 patches = self._revpatches(repo, sorted(revs))
1038 1038 qfinished = self._cleanup(patches, len(patches))
1039 1039 if qfinished and repo.ui.configbool('mq', 'secret', False):
1040 1040 # only use this logic when the secret option is added
1041 1041 oldqbase = repo[qfinished[0]]
1042 1042 tphase = repo.ui.config('phases', 'new-commit', phases.draft)
1043 1043 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
1044 1044 tr = repo.transaction('qfinish')
1045 1045 try:
1046 1046 phases.advanceboundary(repo, tr, tphase, qfinished)
1047 1047 tr.close()
1048 1048 finally:
1049 1049 tr.release()
1050 1050
1051 1051 def delete(self, repo, patches, opts):
1052 1052 if not patches and not opts.get('rev'):
1053 1053 raise error.Abort(_('qdelete requires at least one revision or '
1054 1054 'patch name'))
1055 1055
1056 1056 realpatches = []
1057 1057 for patch in patches:
1058 1058 patch = self.lookup(patch, strict=True)
1059 1059 info = self.isapplied(patch)
1060 1060 if info:
1061 1061 raise error.Abort(_("cannot delete applied patch %s") % patch)
1062 1062 if patch not in self.series:
1063 1063 raise error.Abort(_("patch %s not in series file") % patch)
1064 1064 if patch not in realpatches:
1065 1065 realpatches.append(patch)
1066 1066
1067 1067 numrevs = 0
1068 1068 if opts.get('rev'):
1069 1069 if not self.applied:
1070 1070 raise error.Abort(_('no patches applied'))
1071 1071 revs = scmutil.revrange(repo, opts.get('rev'))
1072 1072 revs.sort()
1073 1073 revpatches = self._revpatches(repo, revs)
1074 1074 realpatches += revpatches
1075 1075 numrevs = len(revpatches)
1076 1076
1077 1077 self._cleanup(realpatches, numrevs, opts.get('keep'))
1078 1078
1079 1079 def checktoppatch(self, repo):
1080 1080 '''check that working directory is at qtip'''
1081 1081 if self.applied:
1082 1082 top = self.applied[-1].node
1083 1083 patch = self.applied[-1].name
1084 1084 if repo.dirstate.p1() != top:
1085 1085 raise error.Abort(_("working directory revision is not qtip"))
1086 1086 return top, patch
1087 1087 return None, None
1088 1088
1089 1089 def putsubstate2changes(self, substatestate, changes):
1090 1090 for files in changes[:3]:
1091 1091 if '.hgsubstate' in files:
1092 1092 return # already listed up
1093 1093 # not yet listed up
1094 1094 if substatestate in 'a?':
1095 1095 changes[1].append('.hgsubstate')
1096 1096 elif substatestate in 'r':
1097 1097 changes[2].append('.hgsubstate')
1098 1098 else: # modified
1099 1099 changes[0].append('.hgsubstate')
1100 1100
1101 1101 def checklocalchanges(self, repo, force=False, refresh=True):
1102 1102 excsuffix = ''
1103 1103 if refresh:
1104 1104 excsuffix = ', qrefresh first'
1105 1105 # plain versions for i18n tool to detect them
1106 1106 _("local changes found, qrefresh first")
1107 1107 _("local changed subrepos found, qrefresh first")
1108 1108 return checklocalchanges(repo, force, excsuffix)
1109 1109
1110 1110 _reserved = ('series', 'status', 'guards', '.', '..')
1111 1111 def checkreservedname(self, name):
1112 1112 if name in self._reserved:
1113 1113 raise error.Abort(_('"%s" cannot be used as the name of a patch')
1114 1114 % name)
1115 1115 for prefix in ('.hg', '.mq'):
1116 1116 if name.startswith(prefix):
1117 1117 raise error.Abort(_('patch name cannot begin with "%s"')
1118 1118 % prefix)
1119 1119 for c in ('#', ':', '\r', '\n'):
1120 1120 if c in name:
1121 1121 raise error.Abort(_('%r cannot be used in the name of a patch')
1122 1122 % c)
1123 1123
1124 1124 def checkpatchname(self, name, force=False):
1125 1125 self.checkreservedname(name)
1126 1126 if not force and os.path.exists(self.join(name)):
1127 1127 if os.path.isdir(self.join(name)):
1128 1128 raise error.Abort(_('"%s" already exists as a directory')
1129 1129 % name)
1130 1130 else:
1131 1131 raise error.Abort(_('patch "%s" already exists') % name)
1132 1132
1133 1133 def checkkeepchanges(self, keepchanges, force):
1134 1134 if force and keepchanges:
1135 1135 raise error.Abort(_('cannot use both --force and --keep-changes'))
1136 1136
1137 1137 def new(self, repo, patchfn, *pats, **opts):
1138 1138 """options:
1139 1139 msg: a string or a no-argument function returning a string
1140 1140 """
1141 1141 msg = opts.get('msg')
1142 1142 edit = opts.get('edit')
1143 1143 editform = opts.get('editform', 'mq.qnew')
1144 1144 user = opts.get('user')
1145 1145 date = opts.get('date')
1146 1146 if date:
1147 1147 date = util.parsedate(date)
1148 1148 diffopts = self.diffopts({'git': opts.get('git')})
1149 1149 if opts.get('checkname', True):
1150 1150 self.checkpatchname(patchfn)
1151 1151 inclsubs = checksubstate(repo)
1152 1152 if inclsubs:
1153 1153 substatestate = repo.dirstate['.hgsubstate']
1154 1154 if opts.get('include') or opts.get('exclude') or pats:
1155 1155 # detect missing files in pats
1156 1156 def badfn(f, msg):
1157 1157 if f != '.hgsubstate': # .hgsubstate is auto-created
1158 1158 raise error.Abort('%s: %s' % (f, msg))
1159 1159 match = scmutil.match(repo[None], pats, opts, badfn=badfn)
1160 1160 changes = repo.status(match=match)
1161 1161 else:
1162 1162 changes = self.checklocalchanges(repo, force=True)
1163 1163 commitfiles = list(inclsubs)
1164 1164 for files in changes[:3]:
1165 1165 commitfiles.extend(files)
1166 1166 match = scmutil.matchfiles(repo, commitfiles)
1167 1167 if len(repo[None].parents()) > 1:
1168 1168 raise error.Abort(_('cannot manage merge changesets'))
1169 1169 self.checktoppatch(repo)
1170 1170 insert = self.fullseriesend()
1171 1171 wlock = repo.wlock()
1172 1172 try:
1173 1173 try:
1174 1174 # if patch file write fails, abort early
1175 1175 p = self.opener(patchfn, "w")
1176 1176 except IOError as e:
1177 1177 raise error.Abort(_('cannot write patch "%s": %s')
1178 1178 % (patchfn, e.strerror))
1179 1179 try:
1180 1180 defaultmsg = "[mq]: %s" % patchfn
1181 1181 editor = cmdutil.getcommiteditor(editform=editform)
1182 1182 if edit:
1183 1183 def finishdesc(desc):
1184 1184 if desc.rstrip():
1185 1185 return desc
1186 1186 else:
1187 1187 return defaultmsg
1188 1188 # i18n: this message is shown in editor with "HG: " prefix
1189 1189 extramsg = _('Leave message empty to use default message.')
1190 1190 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1191 1191 extramsg=extramsg,
1192 1192 editform=editform)
1193 1193 commitmsg = msg
1194 1194 else:
1195 1195 commitmsg = msg or defaultmsg
1196 1196
1197 1197 n = newcommit(repo, None, commitmsg, user, date, match=match,
1198 1198 force=True, editor=editor)
1199 1199 if n is None:
1200 1200 raise error.Abort(_("repo commit failed"))
1201 1201 try:
1202 1202 self.fullseries[insert:insert] = [patchfn]
1203 1203 self.applied.append(statusentry(n, patchfn))
1204 1204 self.parseseries()
1205 1205 self.seriesdirty = True
1206 1206 self.applieddirty = True
1207 1207 nctx = repo[n]
1208 1208 ph = patchheader(self.join(patchfn), self.plainmode)
1209 1209 if user:
1210 1210 ph.setuser(user)
1211 1211 if date:
1212 1212 ph.setdate('%s %s' % date)
1213 1213 ph.setparent(hex(nctx.p1().node()))
1214 1214 msg = nctx.description().strip()
1215 1215 if msg == defaultmsg.strip():
1216 1216 msg = ''
1217 1217 ph.setmessage(msg)
1218 1218 p.write(str(ph))
1219 1219 if commitfiles:
1220 1220 parent = self.qparents(repo, n)
1221 1221 if inclsubs:
1222 1222 self.putsubstate2changes(substatestate, changes)
1223 1223 chunks = patchmod.diff(repo, node1=parent, node2=n,
1224 1224 changes=changes, opts=diffopts)
1225 1225 for chunk in chunks:
1226 1226 p.write(chunk)
1227 1227 p.close()
1228 1228 r = self.qrepo()
1229 1229 if r:
1230 1230 r[None].add([patchfn])
1231 1231 except: # re-raises
1232 1232 repo.rollback()
1233 1233 raise
1234 1234 except Exception:
1235 1235 patchpath = self.join(patchfn)
1236 1236 try:
1237 1237 os.unlink(patchpath)
1238 1238 except OSError:
1239 1239 self.ui.warn(_('error unlinking %s\n') % patchpath)
1240 1240 raise
1241 1241 self.removeundo(repo)
1242 1242 finally:
1243 1243 release(wlock)
1244 1244
1245 1245 def isapplied(self, patch):
1246 1246 """returns (index, rev, patch)"""
1247 1247 for i, a in enumerate(self.applied):
1248 1248 if a.name == patch:
1249 1249 return (i, a.node, a.name)
1250 1250 return None
1251 1251
1252 1252 # if the exact patch name does not exist, we try a few
1253 1253 # variations. If strict is passed, we try only #1
1254 1254 #
1255 1255 # 1) a number (as string) to indicate an offset in the series file
1256 1256 # 2) a unique substring of the patch name was given
1257 1257 # 3) patchname[-+]num to indicate an offset in the series file
1258 1258 def lookup(self, patch, strict=False):
1259 1259 def partialname(s):
1260 1260 if s in self.series:
1261 1261 return s
1262 1262 matches = [x for x in self.series if s in x]
1263 1263 if len(matches) > 1:
1264 1264 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
1265 1265 for m in matches:
1266 1266 self.ui.warn(' %s\n' % m)
1267 1267 return None
1268 1268 if matches:
1269 1269 return matches[0]
1270 1270 if self.series and self.applied:
1271 1271 if s == 'qtip':
1272 1272 return self.series[self.seriesend(True) - 1]
1273 1273 if s == 'qbase':
1274 1274 return self.series[0]
1275 1275 return None
1276 1276
1277 1277 if patch in self.series:
1278 1278 return patch
1279 1279
1280 1280 if not os.path.isfile(self.join(patch)):
1281 1281 try:
1282 1282 sno = int(patch)
1283 1283 except (ValueError, OverflowError):
1284 1284 pass
1285 1285 else:
1286 1286 if -len(self.series) <= sno < len(self.series):
1287 1287 return self.series[sno]
1288 1288
1289 1289 if not strict:
1290 1290 res = partialname(patch)
1291 1291 if res:
1292 1292 return res
1293 1293 minus = patch.rfind('-')
1294 1294 if minus >= 0:
1295 1295 res = partialname(patch[:minus])
1296 1296 if res:
1297 1297 i = self.series.index(res)
1298 1298 try:
1299 1299 off = int(patch[minus + 1:] or 1)
1300 1300 except (ValueError, OverflowError):
1301 1301 pass
1302 1302 else:
1303 1303 if i - off >= 0:
1304 1304 return self.series[i - off]
1305 1305 plus = patch.rfind('+')
1306 1306 if plus >= 0:
1307 1307 res = partialname(patch[:plus])
1308 1308 if res:
1309 1309 i = self.series.index(res)
1310 1310 try:
1311 1311 off = int(patch[plus + 1:] or 1)
1312 1312 except (ValueError, OverflowError):
1313 1313 pass
1314 1314 else:
1315 1315 if i + off < len(self.series):
1316 1316 return self.series[i + off]
1317 1317 raise error.Abort(_("patch %s not in series") % patch)
1318 1318
1319 1319 def push(self, repo, patch=None, force=False, list=False, mergeq=None,
1320 1320 all=False, move=False, exact=False, nobackup=False,
1321 1321 keepchanges=False):
1322 1322 self.checkkeepchanges(keepchanges, force)
1323 1323 diffopts = self.diffopts()
1324 1324 wlock = repo.wlock()
1325 1325 try:
1326 1326 heads = []
1327 1327 for hs in repo.branchmap().itervalues():
1328 1328 heads.extend(hs)
1329 1329 if not heads:
1330 1330 heads = [nullid]
1331 1331 if repo.dirstate.p1() not in heads and not exact:
1332 1332 self.ui.status(_("(working directory not at a head)\n"))
1333 1333
1334 1334 if not self.series:
1335 1335 self.ui.warn(_('no patches in series\n'))
1336 1336 return 0
1337 1337
1338 1338 # Suppose our series file is: A B C and the current 'top'
1339 1339 # patch is B. qpush C should be performed (moving forward)
1340 1340 # qpush B is a NOP (no change) qpush A is an error (can't
1341 1341 # go backwards with qpush)
1342 1342 if patch:
1343 1343 patch = self.lookup(patch)
1344 1344 info = self.isapplied(patch)
1345 1345 if info and info[0] >= len(self.applied) - 1:
1346 1346 self.ui.warn(
1347 1347 _('qpush: %s is already at the top\n') % patch)
1348 1348 return 0
1349 1349
1350 1350 pushable, reason = self.pushable(patch)
1351 1351 if pushable:
1352 1352 if self.series.index(patch) < self.seriesend():
1353 1353 raise error.Abort(
1354 1354 _("cannot push to a previous patch: %s") % patch)
1355 1355 else:
1356 1356 if reason:
1357 1357 reason = _('guarded by %s') % reason
1358 1358 else:
1359 1359 reason = _('no matching guards')
1360 1360 self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason))
1361 1361 return 1
1362 1362 elif all:
1363 1363 patch = self.series[-1]
1364 1364 if self.isapplied(patch):
1365 1365 self.ui.warn(_('all patches are currently applied\n'))
1366 1366 return 0
1367 1367
1368 1368 # Following the above example, starting at 'top' of B:
1369 1369 # qpush should be performed (pushes C), but a subsequent
1370 1370 # qpush without an argument is an error (nothing to
1371 1371 # apply). This allows a loop of "...while hg qpush..." to
1372 1372 # work as it detects an error when done
1373 1373 start = self.seriesend()
1374 1374 if start == len(self.series):
1375 1375 self.ui.warn(_('patch series already fully applied\n'))
1376 1376 return 1
1377 1377 if not force and not keepchanges:
1378 1378 self.checklocalchanges(repo, refresh=self.applied)
1379 1379
1380 1380 if exact:
1381 1381 if keepchanges:
1382 1382 raise error.Abort(
1383 1383 _("cannot use --exact and --keep-changes together"))
1384 1384 if move:
1385 1385 raise error.Abort(_('cannot use --exact and --move '
1386 1386 'together'))
1387 1387 if self.applied:
1388 1388 raise error.Abort(_('cannot push --exact with applied '
1389 1389 'patches'))
1390 1390 root = self.series[start]
1391 1391 target = patchheader(self.join(root), self.plainmode).parent
1392 1392 if not target:
1393 1393 raise error.Abort(
1394 1394 _("%s does not have a parent recorded") % root)
1395 1395 if not repo[target] == repo['.']:
1396 1396 hg.update(repo, target)
1397 1397
1398 1398 if move:
1399 1399 if not patch:
1400 1400 raise error.Abort(_("please specify the patch to move"))
1401 1401 for fullstart, rpn in enumerate(self.fullseries):
1402 1402 # strip markers for patch guards
1403 1403 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1404 1404 break
1405 1405 for i, rpn in enumerate(self.fullseries[fullstart:]):
1406 1406 # strip markers for patch guards
1407 1407 if self.guard_re.split(rpn, 1)[0] == patch:
1408 1408 break
1409 1409 index = fullstart + i
1410 1410 assert index < len(self.fullseries)
1411 1411 fullpatch = self.fullseries[index]
1412 1412 del self.fullseries[index]
1413 1413 self.fullseries.insert(fullstart, fullpatch)
1414 1414 self.parseseries()
1415 1415 self.seriesdirty = True
1416 1416
1417 1417 self.applieddirty = True
1418 1418 if start > 0:
1419 1419 self.checktoppatch(repo)
1420 1420 if not patch:
1421 1421 patch = self.series[start]
1422 1422 end = start + 1
1423 1423 else:
1424 1424 end = self.series.index(patch, start) + 1
1425 1425
1426 1426 tobackup = set()
1427 1427 if (not nobackup and force) or keepchanges:
1428 1428 status = self.checklocalchanges(repo, force=True)
1429 1429 if keepchanges:
1430 1430 tobackup.update(status.modified + status.added +
1431 1431 status.removed + status.deleted)
1432 1432 else:
1433 1433 tobackup.update(status.modified + status.added)
1434 1434
1435 1435 s = self.series[start:end]
1436 1436 all_files = set()
1437 1437 try:
1438 1438 if mergeq:
1439 1439 ret = self.mergepatch(repo, mergeq, s, diffopts)
1440 1440 else:
1441 1441 ret = self.apply(repo, s, list, all_files=all_files,
1442 1442 tobackup=tobackup, keepchanges=keepchanges)
1443 1443 except AbortNoCleanup:
1444 1444 raise
1445 1445 except: # re-raises
1446 1446 self.ui.warn(_('cleaning up working directory...\n'))
1447 1447 cmdutil.revert(self.ui, repo, repo['.'],
1448 1448 repo.dirstate.parents(), no_backup=True)
1449 1449 # only remove unknown files that we know we touched or
1450 1450 # created while patching
1451 1451 for f in all_files:
1452 1452 if f not in repo.dirstate:
1453 1453 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1454 1454 self.ui.warn(_('done\n'))
1455 1455 raise
1456 1456
1457 1457 if not self.applied:
1458 1458 return ret[0]
1459 1459 top = self.applied[-1].name
1460 1460 if ret[0] and ret[0] > 1:
1461 1461 msg = _("errors during apply, please fix and qrefresh %s\n")
1462 1462 self.ui.write(msg % top)
1463 1463 else:
1464 1464 self.ui.write(_("now at: %s\n") % top)
1465 1465 return ret[0]
1466 1466
1467 1467 finally:
1468 1468 wlock.release()
1469 1469
1470 1470 def pop(self, repo, patch=None, force=False, update=True, all=False,
1471 1471 nobackup=False, keepchanges=False):
1472 1472 self.checkkeepchanges(keepchanges, force)
1473 1473 wlock = repo.wlock()
1474 1474 try:
1475 1475 if patch:
1476 1476 # index, rev, patch
1477 1477 info = self.isapplied(patch)
1478 1478 if not info:
1479 1479 patch = self.lookup(patch)
1480 1480 info = self.isapplied(patch)
1481 1481 if not info:
1482 1482 raise error.Abort(_("patch %s is not applied") % patch)
1483 1483
1484 1484 if not self.applied:
1485 1485 # Allow qpop -a to work repeatedly,
1486 1486 # but not qpop without an argument
1487 1487 self.ui.warn(_("no patches applied\n"))
1488 1488 return not all
1489 1489
1490 1490 if all:
1491 1491 start = 0
1492 1492 elif patch:
1493 1493 start = info[0] + 1
1494 1494 else:
1495 1495 start = len(self.applied) - 1
1496 1496
1497 1497 if start >= len(self.applied):
1498 1498 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
1499 1499 return
1500 1500
1501 1501 if not update:
1502 1502 parents = repo.dirstate.parents()
1503 1503 rr = [x.node for x in self.applied]
1504 1504 for p in parents:
1505 1505 if p in rr:
1506 1506 self.ui.warn(_("qpop: forcing dirstate update\n"))
1507 1507 update = True
1508 1508 else:
1509 1509 parents = [p.node() for p in repo[None].parents()]
1510 1510 needupdate = False
1511 1511 for entry in self.applied[start:]:
1512 1512 if entry.node in parents:
1513 1513 needupdate = True
1514 1514 break
1515 1515 update = needupdate
1516 1516
1517 1517 tobackup = set()
1518 1518 if update:
1519 1519 s = self.checklocalchanges(repo, force=force or keepchanges)
1520 1520 if force:
1521 1521 if not nobackup:
1522 1522 tobackup.update(s.modified + s.added)
1523 1523 elif keepchanges:
1524 1524 tobackup.update(s.modified + s.added +
1525 1525 s.removed + s.deleted)
1526 1526
1527 1527 self.applieddirty = True
1528 1528 end = len(self.applied)
1529 1529 rev = self.applied[start].node
1530 1530
1531 1531 try:
1532 1532 heads = repo.changelog.heads(rev)
1533 1533 except error.LookupError:
1534 1534 node = short(rev)
1535 1535 raise error.Abort(_('trying to pop unknown node %s') % node)
1536 1536
1537 1537 if heads != [self.applied[-1].node]:
1538 1538 raise error.Abort(_("popping would remove a revision not "
1539 1539 "managed by this patch queue"))
1540 1540 if not repo[self.applied[-1].node].mutable():
1541 1541 raise error.Abort(
1542 1542 _("popping would remove a public revision"),
1543 1543 hint=_('see "hg help phases" for details'))
1544 1544
1545 1545 # we know there are no local changes, so we can make a simplified
1546 1546 # form of hg.update.
1547 1547 if update:
1548 1548 qp = self.qparents(repo, rev)
1549 1549 ctx = repo[qp]
1550 1550 m, a, r, d = repo.status(qp, '.')[:4]
1551 1551 if d:
1552 1552 raise error.Abort(_("deletions found between repo revs"))
1553 1553
1554 1554 tobackup = set(a + m + r) & tobackup
1555 1555 if keepchanges and tobackup:
1556 1556 raise error.Abort(_("local changes found, qrefresh first"))
1557 1557 self.backup(repo, tobackup)
1558 1558 repo.dirstate.beginparentchange()
1559 1559 for f in a:
1560 1560 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1561 1561 repo.dirstate.drop(f)
1562 1562 for f in m + r:
1563 1563 fctx = ctx[f]
1564 1564 repo.wwrite(f, fctx.data(), fctx.flags())
1565 1565 repo.dirstate.normal(f)
1566 1566 repo.setparents(qp, nullid)
1567 1567 repo.dirstate.endparentchange()
1568 1568 for patch in reversed(self.applied[start:end]):
1569 1569 self.ui.status(_("popping %s\n") % patch.name)
1570 1570 del self.applied[start:end]
1571 1571 strip(self.ui, repo, [rev], update=False, backup=False)
1572 1572 for s, state in repo['.'].substate.items():
1573 1573 repo['.'].sub(s).get(state)
1574 1574 if self.applied:
1575 1575 self.ui.write(_("now at: %s\n") % self.applied[-1].name)
1576 1576 else:
1577 1577 self.ui.write(_("patch queue now empty\n"))
1578 1578 finally:
1579 1579 wlock.release()
1580 1580
1581 1581 def diff(self, repo, pats, opts):
1582 1582 top, patch = self.checktoppatch(repo)
1583 1583 if not top:
1584 1584 self.ui.write(_("no patches applied\n"))
1585 1585 return
1586 1586 qp = self.qparents(repo, top)
1587 1587 if opts.get('reverse'):
1588 1588 node1, node2 = None, qp
1589 1589 else:
1590 1590 node1, node2 = qp, None
1591 1591 diffopts = self.diffopts(opts, patch)
1592 1592 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1593 1593
1594 1594 def refresh(self, repo, pats=None, **opts):
1595 1595 if not self.applied:
1596 1596 self.ui.write(_("no patches applied\n"))
1597 1597 return 1
1598 1598 msg = opts.get('msg', '').rstrip()
1599 1599 edit = opts.get('edit')
1600 1600 editform = opts.get('editform', 'mq.qrefresh')
1601 1601 newuser = opts.get('user')
1602 1602 newdate = opts.get('date')
1603 1603 if newdate:
1604 1604 newdate = '%d %d' % util.parsedate(newdate)
1605 1605 wlock = repo.wlock()
1606 1606
1607 1607 try:
1608 1608 self.checktoppatch(repo)
1609 1609 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1610 1610 if repo.changelog.heads(top) != [top]:
1611 1611 raise error.Abort(_("cannot qrefresh a revision with children"))
1612 1612 if not repo[top].mutable():
1613 1613 raise error.Abort(_("cannot qrefresh public revision"),
1614 1614 hint=_('see "hg help phases" for details'))
1615 1615
1616 1616 cparents = repo.changelog.parents(top)
1617 1617 patchparent = self.qparents(repo, top)
1618 1618
1619 1619 inclsubs = checksubstate(repo, hex(patchparent))
1620 1620 if inclsubs:
1621 1621 substatestate = repo.dirstate['.hgsubstate']
1622 1622
1623 1623 ph = patchheader(self.join(patchfn), self.plainmode)
1624 1624 diffopts = self.diffopts({'git': opts.get('git')}, patchfn)
1625 1625 if newuser:
1626 1626 ph.setuser(newuser)
1627 1627 if newdate:
1628 1628 ph.setdate(newdate)
1629 1629 ph.setparent(hex(patchparent))
1630 1630
1631 1631 # only commit new patch when write is complete
1632 1632 patchf = self.opener(patchfn, 'w', atomictemp=True)
1633 1633
1634 1634 # update the dirstate in place, strip off the qtip commit
1635 1635 # and then commit.
1636 1636 #
1637 1637 # this should really read:
1638 1638 # mm, dd, aa = repo.status(top, patchparent)[:3]
1639 1639 # but we do it backwards to take advantage of manifest/changelog
1640 1640 # caching against the next repo.status call
1641 1641 mm, aa, dd = repo.status(patchparent, top)[:3]
1642 1642 changes = repo.changelog.read(top)
1643 1643 man = repo.manifest.read(changes[0])
1644 1644 aaa = aa[:]
1645 1645 matchfn = scmutil.match(repo[None], pats, opts)
1646 1646 # in short mode, we only diff the files included in the
1647 1647 # patch already plus specified files
1648 1648 if opts.get('short'):
1649 1649 # if amending a patch, we start with existing
1650 1650 # files plus specified files - unfiltered
1651 1651 match = scmutil.matchfiles(repo, mm + aa + dd + matchfn.files())
1652 1652 # filter with include/exclude options
1653 1653 matchfn = scmutil.match(repo[None], opts=opts)
1654 1654 else:
1655 1655 match = scmutil.matchall(repo)
1656 1656 m, a, r, d = repo.status(match=match)[:4]
1657 1657 mm = set(mm)
1658 1658 aa = set(aa)
1659 1659 dd = set(dd)
1660 1660
1661 1661 # we might end up with files that were added between
1662 1662 # qtip and the dirstate parent, but then changed in the
1663 1663 # local dirstate. in this case, we want them to only
1664 1664 # show up in the added section
1665 1665 for x in m:
1666 1666 if x not in aa:
1667 1667 mm.add(x)
1668 1668 # we might end up with files added by the local dirstate that
1669 1669 # were deleted by the patch. In this case, they should only
1670 1670 # show up in the changed section.
1671 1671 for x in a:
1672 1672 if x in dd:
1673 1673 dd.remove(x)
1674 1674 mm.add(x)
1675 1675 else:
1676 1676 aa.add(x)
1677 1677 # make sure any files deleted in the local dirstate
1678 1678 # are not in the add or change column of the patch
1679 1679 forget = []
1680 1680 for x in d + r:
1681 1681 if x in aa:
1682 1682 aa.remove(x)
1683 1683 forget.append(x)
1684 1684 continue
1685 1685 else:
1686 1686 mm.discard(x)
1687 1687 dd.add(x)
1688 1688
1689 1689 m = list(mm)
1690 1690 r = list(dd)
1691 1691 a = list(aa)
1692 1692
1693 1693 # create 'match' that includes the files to be recommitted.
1694 1694 # apply matchfn via repo.status to ensure correct case handling.
1695 1695 cm, ca, cr, cd = repo.status(patchparent, match=matchfn)[:4]
1696 1696 allmatches = set(cm + ca + cr + cd)
1697 1697 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1698 1698
1699 1699 files = set(inclsubs)
1700 1700 for x in refreshchanges:
1701 1701 files.update(x)
1702 1702 match = scmutil.matchfiles(repo, files)
1703 1703
1704 1704 bmlist = repo[top].bookmarks()
1705 1705
1706 1706 dsguard = None
1707 1707 try:
1708 1708 dsguard = cmdutil.dirstateguard(repo, 'mq.refresh')
1709 1709 if diffopts.git or diffopts.upgrade:
1710 1710 copies = {}
1711 1711 for dst in a:
1712 1712 src = repo.dirstate.copied(dst)
1713 1713 # during qfold, the source file for copies may
1714 1714 # be removed. Treat this as a simple add.
1715 1715 if src is not None and src in repo.dirstate:
1716 1716 copies.setdefault(src, []).append(dst)
1717 1717 repo.dirstate.add(dst)
1718 1718 # remember the copies between patchparent and qtip
1719 1719 for dst in aaa:
1720 1720 f = repo.file(dst)
1721 1721 src = f.renamed(man[dst])
1722 1722 if src:
1723 1723 copies.setdefault(src[0], []).extend(
1724 1724 copies.get(dst, []))
1725 1725 if dst in a:
1726 1726 copies[src[0]].append(dst)
1727 1727 # we can't copy a file created by the patch itself
1728 1728 if dst in copies:
1729 1729 del copies[dst]
1730 1730 for src, dsts in copies.iteritems():
1731 1731 for dst in dsts:
1732 1732 repo.dirstate.copy(src, dst)
1733 1733 else:
1734 1734 for dst in a:
1735 1735 repo.dirstate.add(dst)
1736 1736 # Drop useless copy information
1737 1737 for f in list(repo.dirstate.copies()):
1738 1738 repo.dirstate.copy(None, f)
1739 1739 for f in r:
1740 1740 repo.dirstate.remove(f)
1741 1741 # if the patch excludes a modified file, mark that
1742 1742 # file with mtime=0 so status can see it.
1743 1743 mm = []
1744 1744 for i in xrange(len(m) - 1, -1, -1):
1745 1745 if not matchfn(m[i]):
1746 1746 mm.append(m[i])
1747 1747 del m[i]
1748 1748 for f in m:
1749 1749 repo.dirstate.normal(f)
1750 1750 for f in mm:
1751 1751 repo.dirstate.normallookup(f)
1752 1752 for f in forget:
1753 1753 repo.dirstate.drop(f)
1754 1754
1755 1755 user = ph.user or changes[1]
1756 1756
1757 1757 oldphase = repo[top].phase()
1758 1758
1759 1759 # assumes strip can roll itself back if interrupted
1760 1760 repo.setparents(*cparents)
1761 1761 self.applied.pop()
1762 1762 self.applieddirty = True
1763 1763 strip(self.ui, repo, [top], update=False, backup=False)
1764 1764 dsguard.close()
1765 1765 finally:
1766 1766 release(dsguard)
1767 1767
1768 1768 try:
1769 1769 # might be nice to attempt to roll back strip after this
1770 1770
1771 1771 defaultmsg = "[mq]: %s" % patchfn
1772 1772 editor = cmdutil.getcommiteditor(editform=editform)
1773 1773 if edit:
1774 1774 def finishdesc(desc):
1775 1775 if desc.rstrip():
1776 1776 ph.setmessage(desc)
1777 1777 return desc
1778 1778 return defaultmsg
1779 1779 # i18n: this message is shown in editor with "HG: " prefix
1780 1780 extramsg = _('Leave message empty to use default message.')
1781 1781 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1782 1782 extramsg=extramsg,
1783 1783 editform=editform)
1784 1784 message = msg or "\n".join(ph.message)
1785 1785 elif not msg:
1786 1786 if not ph.message:
1787 1787 message = defaultmsg
1788 1788 else:
1789 1789 message = "\n".join(ph.message)
1790 1790 else:
1791 1791 message = msg
1792 1792 ph.setmessage(msg)
1793 1793
1794 1794 # Ensure we create a new changeset in the same phase than
1795 1795 # the old one.
1796 1796 lock = tr = None
1797 1797 try:
1798 1798 lock = repo.lock()
1799 1799 tr = repo.transaction('mq')
1800 1800 n = newcommit(repo, oldphase, message, user, ph.date,
1801 1801 match=match, force=True, editor=editor)
1802 1802 # only write patch after a successful commit
1803 1803 c = [list(x) for x in refreshchanges]
1804 1804 if inclsubs:
1805 1805 self.putsubstate2changes(substatestate, c)
1806 1806 chunks = patchmod.diff(repo, patchparent,
1807 1807 changes=c, opts=diffopts)
1808 1808 comments = str(ph)
1809 1809 if comments:
1810 1810 patchf.write(comments)
1811 1811 for chunk in chunks:
1812 1812 patchf.write(chunk)
1813 1813 patchf.close()
1814 1814
1815 1815 marks = repo._bookmarks
1816 1816 for bm in bmlist:
1817 1817 marks[bm] = n
1818 1818 marks.recordchange(tr)
1819 1819 tr.close()
1820 1820
1821 1821 self.applied.append(statusentry(n, patchfn))
1822 1822 finally:
1823 1823 lockmod.release(lock, tr)
1824 1824 except: # re-raises
1825 1825 ctx = repo[cparents[0]]
1826 1826 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
1827 1827 self.savedirty()
1828 1828 self.ui.warn(_('qrefresh interrupted while patch was popped! '
1829 1829 '(revert --all, qpush to recover)\n'))
1830 1830 raise
1831 1831 finally:
1832 1832 wlock.release()
1833 1833 self.removeundo(repo)
1834 1834
1835 1835 def init(self, repo, create=False):
1836 1836 if not create and os.path.isdir(self.path):
1837 1837 raise error.Abort(_("patch queue directory already exists"))
1838 1838 try:
1839 1839 os.mkdir(self.path)
1840 1840 except OSError as inst:
1841 1841 if inst.errno != errno.EEXIST or not create:
1842 1842 raise
1843 1843 if create:
1844 1844 return self.qrepo(create=True)
1845 1845
1846 1846 def unapplied(self, repo, patch=None):
1847 1847 if patch and patch not in self.series:
1848 1848 raise error.Abort(_("patch %s is not in series file") % patch)
1849 1849 if not patch:
1850 1850 start = self.seriesend()
1851 1851 else:
1852 1852 start = self.series.index(patch) + 1
1853 1853 unapplied = []
1854 1854 for i in xrange(start, len(self.series)):
1855 1855 pushable, reason = self.pushable(i)
1856 1856 if pushable:
1857 1857 unapplied.append((i, self.series[i]))
1858 1858 self.explainpushable(i)
1859 1859 return unapplied
1860 1860
1861 1861 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1862 1862 summary=False):
1863 1863 def displayname(pfx, patchname, state):
1864 1864 if pfx:
1865 1865 self.ui.write(pfx)
1866 1866 if summary:
1867 1867 ph = patchheader(self.join(patchname), self.plainmode)
1868 1868 if ph.message:
1869 1869 msg = ph.message[0]
1870 1870 else:
1871 1871 msg = ''
1872 1872
1873 1873 if self.ui.formatted():
1874 1874 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
1875 1875 if width > 0:
1876 1876 msg = util.ellipsis(msg, width)
1877 1877 else:
1878 1878 msg = ''
1879 1879 self.ui.write(patchname, label='qseries.' + state)
1880 1880 self.ui.write(': ')
1881 1881 self.ui.write(msg, label='qseries.message.' + state)
1882 1882 else:
1883 1883 self.ui.write(patchname, label='qseries.' + state)
1884 1884 self.ui.write('\n')
1885 1885
1886 1886 applied = set([p.name for p in self.applied])
1887 1887 if length is None:
1888 1888 length = len(self.series) - start
1889 1889 if not missing:
1890 1890 if self.ui.verbose:
1891 1891 idxwidth = len(str(start + length - 1))
1892 1892 for i in xrange(start, start + length):
1893 1893 patch = self.series[i]
1894 1894 if patch in applied:
1895 1895 char, state = 'A', 'applied'
1896 1896 elif self.pushable(i)[0]:
1897 1897 char, state = 'U', 'unapplied'
1898 1898 else:
1899 1899 char, state = 'G', 'guarded'
1900 1900 pfx = ''
1901 1901 if self.ui.verbose:
1902 1902 pfx = '%*d %s ' % (idxwidth, i, char)
1903 1903 elif status and status != char:
1904 1904 continue
1905 1905 displayname(pfx, patch, state)
1906 1906 else:
1907 1907 msng_list = []
1908 1908 for root, dirs, files in os.walk(self.path):
1909 1909 d = root[len(self.path) + 1:]
1910 1910 for f in files:
1911 1911 fl = os.path.join(d, f)
1912 1912 if (fl not in self.series and
1913 1913 fl not in (self.statuspath, self.seriespath,
1914 1914 self.guardspath)
1915 1915 and not fl.startswith('.')):
1916 1916 msng_list.append(fl)
1917 1917 for x in sorted(msng_list):
1918 1918 pfx = self.ui.verbose and ('D ') or ''
1919 1919 displayname(pfx, x, 'missing')
1920 1920
1921 1921 def issaveline(self, l):
1922 1922 if l.name == '.hg.patches.save.line':
1923 1923 return True
1924 1924
1925 1925 def qrepo(self, create=False):
1926 1926 ui = self.baseui.copy()
1927 1927 if create or os.path.isdir(self.join(".hg")):
1928 1928 return hg.repository(ui, path=self.path, create=create)
1929 1929
1930 1930 def restore(self, repo, rev, delete=None, qupdate=None):
1931 1931 desc = repo[rev].description().strip()
1932 1932 lines = desc.splitlines()
1933 1933 i = 0
1934 1934 datastart = None
1935 1935 series = []
1936 1936 applied = []
1937 1937 qpp = None
1938 1938 for i, line in enumerate(lines):
1939 1939 if line == 'Patch Data:':
1940 1940 datastart = i + 1
1941 1941 elif line.startswith('Dirstate:'):
1942 1942 l = line.rstrip()
1943 1943 l = l[10:].split(' ')
1944 1944 qpp = [bin(x) for x in l]
1945 1945 elif datastart is not None:
1946 1946 l = line.rstrip()
1947 1947 n, name = l.split(':', 1)
1948 1948 if n:
1949 1949 applied.append(statusentry(bin(n), name))
1950 1950 else:
1951 1951 series.append(l)
1952 1952 if datastart is None:
1953 1953 self.ui.warn(_("no saved patch data found\n"))
1954 1954 return 1
1955 1955 self.ui.warn(_("restoring status: %s\n") % lines[0])
1956 1956 self.fullseries = series
1957 1957 self.applied = applied
1958 1958 self.parseseries()
1959 1959 self.seriesdirty = True
1960 1960 self.applieddirty = True
1961 1961 heads = repo.changelog.heads()
1962 1962 if delete:
1963 1963 if rev not in heads:
1964 1964 self.ui.warn(_("save entry has children, leaving it alone\n"))
1965 1965 else:
1966 1966 self.ui.warn(_("removing save entry %s\n") % short(rev))
1967 1967 pp = repo.dirstate.parents()
1968 1968 if rev in pp:
1969 1969 update = True
1970 1970 else:
1971 1971 update = False
1972 1972 strip(self.ui, repo, [rev], update=update, backup=False)
1973 1973 if qpp:
1974 1974 self.ui.warn(_("saved queue repository parents: %s %s\n") %
1975 1975 (short(qpp[0]), short(qpp[1])))
1976 1976 if qupdate:
1977 1977 self.ui.status(_("updating queue directory\n"))
1978 1978 r = self.qrepo()
1979 1979 if not r:
1980 1980 self.ui.warn(_("unable to load queue repository\n"))
1981 1981 return 1
1982 1982 hg.clean(r, qpp[0])
1983 1983
1984 1984 def save(self, repo, msg=None):
1985 1985 if not self.applied:
1986 1986 self.ui.warn(_("save: no patches applied, exiting\n"))
1987 1987 return 1
1988 1988 if self.issaveline(self.applied[-1]):
1989 1989 self.ui.warn(_("status is already saved\n"))
1990 1990 return 1
1991 1991
1992 1992 if not msg:
1993 1993 msg = _("hg patches saved state")
1994 1994 else:
1995 1995 msg = "hg patches: " + msg.rstrip('\r\n')
1996 1996 r = self.qrepo()
1997 1997 if r:
1998 1998 pp = r.dirstate.parents()
1999 1999 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
2000 2000 msg += "\n\nPatch Data:\n"
2001 2001 msg += ''.join('%s\n' % x for x in self.applied)
2002 2002 msg += ''.join(':%s\n' % x for x in self.fullseries)
2003 2003 n = repo.commit(msg, force=True)
2004 2004 if not n:
2005 2005 self.ui.warn(_("repo commit failed\n"))
2006 2006 return 1
2007 2007 self.applied.append(statusentry(n, '.hg.patches.save.line'))
2008 2008 self.applieddirty = True
2009 2009 self.removeundo(repo)
2010 2010
2011 2011 def fullseriesend(self):
2012 2012 if self.applied:
2013 2013 p = self.applied[-1].name
2014 2014 end = self.findseries(p)
2015 2015 if end is None:
2016 2016 return len(self.fullseries)
2017 2017 return end + 1
2018 2018 return 0
2019 2019
2020 2020 def seriesend(self, all_patches=False):
2021 2021 """If all_patches is False, return the index of the next pushable patch
2022 2022 in the series, or the series length. If all_patches is True, return the
2023 2023 index of the first patch past the last applied one.
2024 2024 """
2025 2025 end = 0
2026 2026 def nextpatch(start):
2027 2027 if all_patches or start >= len(self.series):
2028 2028 return start
2029 2029 for i in xrange(start, len(self.series)):
2030 2030 p, reason = self.pushable(i)
2031 2031 if p:
2032 2032 return i
2033 2033 self.explainpushable(i)
2034 2034 return len(self.series)
2035 2035 if self.applied:
2036 2036 p = self.applied[-1].name
2037 2037 try:
2038 2038 end = self.series.index(p)
2039 2039 except ValueError:
2040 2040 return 0
2041 2041 return nextpatch(end + 1)
2042 2042 return nextpatch(end)
2043 2043
2044 2044 def appliedname(self, index):
2045 2045 pname = self.applied[index].name
2046 2046 if not self.ui.verbose:
2047 2047 p = pname
2048 2048 else:
2049 2049 p = str(self.series.index(pname)) + " " + pname
2050 2050 return p
2051 2051
2052 2052 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
2053 2053 force=None, git=False):
2054 2054 def checkseries(patchname):
2055 2055 if patchname in self.series:
2056 2056 raise error.Abort(_('patch %s is already in the series file')
2057 2057 % patchname)
2058 2058
2059 2059 if rev:
2060 2060 if files:
2061 2061 raise error.Abort(_('option "-r" not valid when importing '
2062 2062 'files'))
2063 2063 rev = scmutil.revrange(repo, rev)
2064 2064 rev.sort(reverse=True)
2065 2065 elif not files:
2066 2066 raise error.Abort(_('no files or revisions specified'))
2067 2067 if (len(files) > 1 or len(rev) > 1) and patchname:
2068 2068 raise error.Abort(_('option "-n" not valid when importing multiple '
2069 2069 'patches'))
2070 2070 imported = []
2071 2071 if rev:
2072 2072 # If mq patches are applied, we can only import revisions
2073 2073 # that form a linear path to qbase.
2074 2074 # Otherwise, they should form a linear path to a head.
2075 2075 heads = repo.changelog.heads(repo.changelog.node(rev.first()))
2076 2076 if len(heads) > 1:
2077 2077 raise error.Abort(_('revision %d is the root of more than one '
2078 2078 'branch') % rev.last())
2079 2079 if self.applied:
2080 2080 base = repo.changelog.node(rev.first())
2081 2081 if base in [n.node for n in self.applied]:
2082 2082 raise error.Abort(_('revision %d is already managed')
2083 2083 % rev.first())
2084 2084 if heads != [self.applied[-1].node]:
2085 2085 raise error.Abort(_('revision %d is not the parent of '
2086 2086 'the queue') % rev.first())
2087 2087 base = repo.changelog.rev(self.applied[0].node)
2088 2088 lastparent = repo.changelog.parentrevs(base)[0]
2089 2089 else:
2090 2090 if heads != [repo.changelog.node(rev.first())]:
2091 2091 raise error.Abort(_('revision %d has unmanaged children')
2092 2092 % rev.first())
2093 2093 lastparent = None
2094 2094
2095 2095 diffopts = self.diffopts({'git': git})
2096 2096 tr = repo.transaction('qimport')
2097 2097 try:
2098 2098 for r in rev:
2099 2099 if not repo[r].mutable():
2100 2100 raise error.Abort(_('revision %d is not mutable') % r,
2101 2101 hint=_('see "hg help phases" '
2102 2102 'for details'))
2103 2103 p1, p2 = repo.changelog.parentrevs(r)
2104 2104 n = repo.changelog.node(r)
2105 2105 if p2 != nullrev:
2106 2106 raise error.Abort(_('cannot import merge revision %d')
2107 2107 % r)
2108 2108 if lastparent and lastparent != r:
2109 2109 raise error.Abort(_('revision %d is not the parent of '
2110 2110 '%d')
2111 2111 % (r, lastparent))
2112 2112 lastparent = p1
2113 2113
2114 2114 if not patchname:
2115 2115 patchname = makepatchname(self.fullseries,
2116 2116 repo[r].description().split('\n', 1)[0],
2117 2117 '%d.diff' % r)
2118 2118 checkseries(patchname)
2119 2119 self.checkpatchname(patchname, force)
2120 2120 self.fullseries.insert(0, patchname)
2121 2121
2122 2122 patchf = self.opener(patchname, "w")
2123 2123 cmdutil.export(repo, [n], fp=patchf, opts=diffopts)
2124 2124 patchf.close()
2125 2125
2126 2126 se = statusentry(n, patchname)
2127 2127 self.applied.insert(0, se)
2128 2128
2129 2129 self.added.append(patchname)
2130 2130 imported.append(patchname)
2131 2131 patchname = None
2132 2132 if rev and repo.ui.configbool('mq', 'secret', False):
2133 2133 # if we added anything with --rev, move the secret root
2134 2134 phases.retractboundary(repo, tr, phases.secret, [n])
2135 2135 self.parseseries()
2136 2136 self.applieddirty = True
2137 2137 self.seriesdirty = True
2138 2138 tr.close()
2139 2139 finally:
2140 2140 tr.release()
2141 2141
2142 2142 for i, filename in enumerate(files):
2143 2143 if existing:
2144 2144 if filename == '-':
2145 2145 raise error.Abort(_('-e is incompatible with import from -')
2146 2146 )
2147 2147 filename = normname(filename)
2148 2148 self.checkreservedname(filename)
2149 2149 if util.url(filename).islocal():
2150 2150 originpath = self.join(filename)
2151 2151 if not os.path.isfile(originpath):
2152 2152 raise error.Abort(
2153 2153 _("patch %s does not exist") % filename)
2154 2154
2155 2155 if patchname:
2156 2156 self.checkpatchname(patchname, force)
2157 2157
2158 2158 self.ui.write(_('renaming %s to %s\n')
2159 2159 % (filename, patchname))
2160 2160 util.rename(originpath, self.join(patchname))
2161 2161 else:
2162 2162 patchname = filename
2163 2163
2164 2164 else:
2165 2165 if filename == '-' and not patchname:
2166 2166 raise error.Abort(_('need --name to import a patch from -'))
2167 2167 elif not patchname:
2168 2168 patchname = normname(os.path.basename(filename.rstrip('/')))
2169 2169 self.checkpatchname(patchname, force)
2170 2170 try:
2171 2171 if filename == '-':
2172 2172 text = self.ui.fin.read()
2173 2173 else:
2174 2174 fp = hg.openpath(self.ui, filename)
2175 2175 text = fp.read()
2176 2176 fp.close()
2177 2177 except (OSError, IOError):
2178 2178 raise error.Abort(_("unable to read file %s") % filename)
2179 2179 patchf = self.opener(patchname, "w")
2180 2180 patchf.write(text)
2181 2181 patchf.close()
2182 2182 if not force:
2183 2183 checkseries(patchname)
2184 2184 if patchname not in self.series:
2185 2185 index = self.fullseriesend() + i
2186 2186 self.fullseries[index:index] = [patchname]
2187 2187 self.parseseries()
2188 2188 self.seriesdirty = True
2189 2189 self.ui.warn(_("adding %s to series file\n") % patchname)
2190 2190 self.added.append(patchname)
2191 2191 imported.append(patchname)
2192 2192 patchname = None
2193 2193
2194 2194 self.removeundo(repo)
2195 2195 return imported
2196 2196
2197 2197 def fixkeepchangesopts(ui, opts):
2198 2198 if (not ui.configbool('mq', 'keepchanges') or opts.get('force')
2199 2199 or opts.get('exact')):
2200 2200 return opts
2201 2201 opts = dict(opts)
2202 2202 opts['keep_changes'] = True
2203 2203 return opts
2204 2204
2205 2205 @command("qdelete|qremove|qrm",
2206 2206 [('k', 'keep', None, _('keep patch file')),
2207 2207 ('r', 'rev', [],
2208 2208 _('stop managing a revision (DEPRECATED)'), _('REV'))],
2209 2209 _('hg qdelete [-k] [PATCH]...'))
2210 2210 def delete(ui, repo, *patches, **opts):
2211 2211 """remove patches from queue
2212 2212
2213 2213 The patches must not be applied, and at least one patch is required. Exact
2214 2214 patch identifiers must be given. With -k/--keep, the patch files are
2215 2215 preserved in the patch directory.
2216 2216
2217 2217 To stop managing a patch and move it into permanent history,
2218 2218 use the :hg:`qfinish` command."""
2219 2219 q = repo.mq
2220 2220 q.delete(repo, patches, opts)
2221 2221 q.savedirty()
2222 2222 return 0
2223 2223
2224 2224 @command("qapplied",
2225 2225 [('1', 'last', None, _('show only the preceding applied patch'))
2226 2226 ] + seriesopts,
2227 2227 _('hg qapplied [-1] [-s] [PATCH]'))
2228 2228 def applied(ui, repo, patch=None, **opts):
2229 2229 """print the patches already applied
2230 2230
2231 2231 Returns 0 on success."""
2232 2232
2233 2233 q = repo.mq
2234 2234
2235 2235 if patch:
2236 2236 if patch not in q.series:
2237 2237 raise error.Abort(_("patch %s is not in series file") % patch)
2238 2238 end = q.series.index(patch) + 1
2239 2239 else:
2240 2240 end = q.seriesend(True)
2241 2241
2242 2242 if opts.get('last') and not end:
2243 2243 ui.write(_("no patches applied\n"))
2244 2244 return 1
2245 2245 elif opts.get('last') and end == 1:
2246 2246 ui.write(_("only one patch applied\n"))
2247 2247 return 1
2248 2248 elif opts.get('last'):
2249 2249 start = end - 2
2250 2250 end = 1
2251 2251 else:
2252 2252 start = 0
2253 2253
2254 2254 q.qseries(repo, length=end, start=start, status='A',
2255 2255 summary=opts.get('summary'))
2256 2256
2257 2257
2258 2258 @command("qunapplied",
2259 2259 [('1', 'first', None, _('show only the first patch'))] + seriesopts,
2260 2260 _('hg qunapplied [-1] [-s] [PATCH]'))
2261 2261 def unapplied(ui, repo, patch=None, **opts):
2262 2262 """print the patches not yet applied
2263 2263
2264 2264 Returns 0 on success."""
2265 2265
2266 2266 q = repo.mq
2267 2267 if patch:
2268 2268 if patch not in q.series:
2269 2269 raise error.Abort(_("patch %s is not in series file") % patch)
2270 2270 start = q.series.index(patch) + 1
2271 2271 else:
2272 2272 start = q.seriesend(True)
2273 2273
2274 2274 if start == len(q.series) and opts.get('first'):
2275 2275 ui.write(_("all patches applied\n"))
2276 2276 return 1
2277 2277
2278 2278 if opts.get('first'):
2279 2279 length = 1
2280 2280 else:
2281 2281 length = None
2282 2282 q.qseries(repo, start=start, length=length, status='U',
2283 2283 summary=opts.get('summary'))
2284 2284
2285 2285 @command("qimport",
2286 2286 [('e', 'existing', None, _('import file in patch directory')),
2287 2287 ('n', 'name', '',
2288 2288 _('name of patch file'), _('NAME')),
2289 2289 ('f', 'force', None, _('overwrite existing files')),
2290 2290 ('r', 'rev', [],
2291 2291 _('place existing revisions under mq control'), _('REV')),
2292 2292 ('g', 'git', None, _('use git extended diff format')),
2293 2293 ('P', 'push', None, _('qpush after importing'))],
2294 2294 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'))
2295 2295 def qimport(ui, repo, *filename, **opts):
2296 2296 """import a patch or existing changeset
2297 2297
2298 2298 The patch is inserted into the series after the last applied
2299 2299 patch. If no patches have been applied, qimport prepends the patch
2300 2300 to the series.
2301 2301
2302 2302 The patch will have the same name as its source file unless you
2303 2303 give it a new one with -n/--name.
2304 2304
2305 2305 You can register an existing patch inside the patch directory with
2306 2306 the -e/--existing flag.
2307 2307
2308 2308 With -f/--force, an existing patch of the same name will be
2309 2309 overwritten.
2310 2310
2311 2311 An existing changeset may be placed under mq control with -r/--rev
2312 2312 (e.g. qimport --rev . -n patch will place the current revision
2313 2313 under mq control). With -g/--git, patches imported with --rev will
2314 2314 use the git diff format. See the diffs help topic for information
2315 2315 on why this is important for preserving rename/copy information
2316 2316 and permission changes. Use :hg:`qfinish` to remove changesets
2317 2317 from mq control.
2318 2318
2319 2319 To import a patch from standard input, pass - as the patch file.
2320 2320 When importing from standard input, a patch name must be specified
2321 2321 using the --name flag.
2322 2322
2323 2323 To import an existing patch while renaming it::
2324 2324
2325 2325 hg qimport -e existing-patch -n new-name
2326 2326
2327 2327 Returns 0 if import succeeded.
2328 2328 """
2329 2329 lock = repo.lock() # cause this may move phase
2330 2330 try:
2331 2331 q = repo.mq
2332 2332 try:
2333 2333 imported = q.qimport(
2334 2334 repo, filename, patchname=opts.get('name'),
2335 2335 existing=opts.get('existing'), force=opts.get('force'),
2336 2336 rev=opts.get('rev'), git=opts.get('git'))
2337 2337 finally:
2338 2338 q.savedirty()
2339 2339 finally:
2340 2340 lock.release()
2341 2341
2342 2342 if imported and opts.get('push') and not opts.get('rev'):
2343 2343 return q.push(repo, imported[-1])
2344 2344 return 0
2345 2345
2346 2346 def qinit(ui, repo, create):
2347 2347 """initialize a new queue repository
2348 2348
2349 2349 This command also creates a series file for ordering patches, and
2350 2350 an mq-specific .hgignore file in the queue repository, to exclude
2351 2351 the status and guards files (these contain mostly transient state).
2352 2352
2353 2353 Returns 0 if initialization succeeded."""
2354 2354 q = repo.mq
2355 2355 r = q.init(repo, create)
2356 2356 q.savedirty()
2357 2357 if r:
2358 2358 if not os.path.exists(r.wjoin('.hgignore')):
2359 2359 fp = r.wvfs('.hgignore', 'w')
2360 2360 fp.write('^\\.hg\n')
2361 2361 fp.write('^\\.mq\n')
2362 2362 fp.write('syntax: glob\n')
2363 2363 fp.write('status\n')
2364 2364 fp.write('guards\n')
2365 2365 fp.close()
2366 2366 if not os.path.exists(r.wjoin('series')):
2367 2367 r.wvfs('series', 'w').close()
2368 2368 r[None].add(['.hgignore', 'series'])
2369 2369 commands.add(ui, r)
2370 2370 return 0
2371 2371
2372 2372 @command("^qinit",
2373 2373 [('c', 'create-repo', None, _('create queue repository'))],
2374 2374 _('hg qinit [-c]'))
2375 2375 def init(ui, repo, **opts):
2376 2376 """init a new queue repository (DEPRECATED)
2377 2377
2378 2378 The queue repository is unversioned by default. If
2379 2379 -c/--create-repo is specified, qinit will create a separate nested
2380 2380 repository for patches (qinit -c may also be run later to convert
2381 2381 an unversioned patch repository into a versioned one). You can use
2382 2382 qcommit to commit changes to this queue repository.
2383 2383
2384 2384 This command is deprecated. Without -c, it's implied by other relevant
2385 2385 commands. With -c, use :hg:`init --mq` instead."""
2386 2386 return qinit(ui, repo, create=opts.get('create_repo'))
2387 2387
2388 2388 @command("qclone",
2389 2389 [('', 'pull', None, _('use pull protocol to copy metadata')),
2390 2390 ('U', 'noupdate', None,
2391 2391 _('do not update the new working directories')),
2392 2392 ('', 'uncompressed', None,
2393 2393 _('use uncompressed transfer (fast over LAN)')),
2394 2394 ('p', 'patches', '',
2395 2395 _('location of source patch repository'), _('REPO')),
2396 2396 ] + commands.remoteopts,
2397 2397 _('hg qclone [OPTION]... SOURCE [DEST]'),
2398 2398 norepo=True)
2399 2399 def clone(ui, source, dest=None, **opts):
2400 2400 '''clone main and patch repository at same time
2401 2401
2402 2402 If source is local, destination will have no patches applied. If
2403 2403 source is remote, this command can not check if patches are
2404 2404 applied in source, so cannot guarantee that patches are not
2405 2405 applied in destination. If you clone remote repository, be sure
2406 2406 before that it has no patches applied.
2407 2407
2408 2408 Source patch repository is looked for in <src>/.hg/patches by
2409 2409 default. Use -p <url> to change.
2410 2410
2411 2411 The patch directory must be a nested Mercurial repository, as
2412 2412 would be created by :hg:`init --mq`.
2413 2413
2414 2414 Return 0 on success.
2415 2415 '''
2416 2416 def patchdir(repo):
2417 2417 """compute a patch repo url from a repo object"""
2418 2418 url = repo.url()
2419 2419 if url.endswith('/'):
2420 2420 url = url[:-1]
2421 2421 return url + '/.hg/patches'
2422 2422
2423 2423 # main repo (destination and sources)
2424 2424 if dest is None:
2425 2425 dest = hg.defaultdest(source)
2426 2426 sr = hg.peer(ui, opts, ui.expandpath(source))
2427 2427
2428 2428 # patches repo (source only)
2429 2429 if opts.get('patches'):
2430 2430 patchespath = ui.expandpath(opts.get('patches'))
2431 2431 else:
2432 2432 patchespath = patchdir(sr)
2433 2433 try:
2434 2434 hg.peer(ui, opts, patchespath)
2435 2435 except error.RepoError:
2436 2436 raise error.Abort(_('versioned patch repository not found'
2437 2437 ' (see init --mq)'))
2438 2438 qbase, destrev = None, None
2439 2439 if sr.local():
2440 2440 repo = sr.local()
2441 2441 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2442 2442 qbase = repo.mq.applied[0].node
2443 2443 if not hg.islocal(dest):
2444 2444 heads = set(repo.heads())
2445 2445 destrev = list(heads.difference(repo.heads(qbase)))
2446 2446 destrev.append(repo.changelog.parents(qbase)[0])
2447 2447 elif sr.capable('lookup'):
2448 2448 try:
2449 2449 qbase = sr.lookup('qbase')
2450 2450 except error.RepoError:
2451 2451 pass
2452 2452
2453 2453 ui.note(_('cloning main repository\n'))
2454 2454 sr, dr = hg.clone(ui, opts, sr.url(), dest,
2455 2455 pull=opts.get('pull'),
2456 2456 rev=destrev,
2457 2457 update=False,
2458 2458 stream=opts.get('uncompressed'))
2459 2459
2460 2460 ui.note(_('cloning patch repository\n'))
2461 2461 hg.clone(ui, opts, opts.get('patches') or patchdir(sr), patchdir(dr),
2462 2462 pull=opts.get('pull'), update=not opts.get('noupdate'),
2463 2463 stream=opts.get('uncompressed'))
2464 2464
2465 2465 if dr.local():
2466 2466 repo = dr.local()
2467 2467 if qbase:
2468 2468 ui.note(_('stripping applied patches from destination '
2469 2469 'repository\n'))
2470 2470 strip(ui, repo, [qbase], update=False, backup=None)
2471 2471 if not opts.get('noupdate'):
2472 2472 ui.note(_('updating destination repository\n'))
2473 2473 hg.update(repo, repo.changelog.tip())
2474 2474
2475 2475 @command("qcommit|qci",
2476 2476 commands.table["^commit|ci"][1],
2477 2477 _('hg qcommit [OPTION]... [FILE]...'),
2478 2478 inferrepo=True)
2479 2479 def commit(ui, repo, *pats, **opts):
2480 2480 """commit changes in the queue repository (DEPRECATED)
2481 2481
2482 2482 This command is deprecated; use :hg:`commit --mq` instead."""
2483 2483 q = repo.mq
2484 2484 r = q.qrepo()
2485 2485 if not r:
2486 2486 raise error.Abort('no queue repository')
2487 2487 commands.commit(r.ui, r, *pats, **opts)
2488 2488
2489 2489 @command("qseries",
2490 2490 [('m', 'missing', None, _('print patches not in series')),
2491 2491 ] + seriesopts,
2492 2492 _('hg qseries [-ms]'))
2493 2493 def series(ui, repo, **opts):
2494 2494 """print the entire series file
2495 2495
2496 2496 Returns 0 on success."""
2497 2497 repo.mq.qseries(repo, missing=opts.get('missing'),
2498 2498 summary=opts.get('summary'))
2499 2499 return 0
2500 2500
2501 2501 @command("qtop", seriesopts, _('hg qtop [-s]'))
2502 2502 def top(ui, repo, **opts):
2503 2503 """print the name of the current patch
2504 2504
2505 2505 Returns 0 on success."""
2506 2506 q = repo.mq
2507 2507 if q.applied:
2508 2508 t = q.seriesend(True)
2509 2509 else:
2510 2510 t = 0
2511 2511
2512 2512 if t:
2513 2513 q.qseries(repo, start=t - 1, length=1, status='A',
2514 2514 summary=opts.get('summary'))
2515 2515 else:
2516 2516 ui.write(_("no patches applied\n"))
2517 2517 return 1
2518 2518
2519 2519 @command("qnext", seriesopts, _('hg qnext [-s]'))
2520 2520 def next(ui, repo, **opts):
2521 2521 """print the name of the next pushable patch
2522 2522
2523 2523 Returns 0 on success."""
2524 2524 q = repo.mq
2525 2525 end = q.seriesend()
2526 2526 if end == len(q.series):
2527 2527 ui.write(_("all patches applied\n"))
2528 2528 return 1
2529 2529 q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
2530 2530
2531 2531 @command("qprev", seriesopts, _('hg qprev [-s]'))
2532 2532 def prev(ui, repo, **opts):
2533 2533 """print the name of the preceding applied patch
2534 2534
2535 2535 Returns 0 on success."""
2536 2536 q = repo.mq
2537 2537 l = len(q.applied)
2538 2538 if l == 1:
2539 2539 ui.write(_("only one patch applied\n"))
2540 2540 return 1
2541 2541 if not l:
2542 2542 ui.write(_("no patches applied\n"))
2543 2543 return 1
2544 2544 idx = q.series.index(q.applied[-2].name)
2545 2545 q.qseries(repo, start=idx, length=1, status='A',
2546 2546 summary=opts.get('summary'))
2547 2547
2548 2548 def setupheaderopts(ui, opts):
2549 2549 if not opts.get('user') and opts.get('currentuser'):
2550 2550 opts['user'] = ui.username()
2551 2551 if not opts.get('date') and opts.get('currentdate'):
2552 2552 opts['date'] = "%d %d" % util.makedate()
2553 2553
2554 2554 @command("^qnew",
2555 2555 [('e', 'edit', None, _('invoke editor on commit messages')),
2556 2556 ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')),
2557 2557 ('g', 'git', None, _('use git extended diff format')),
2558 2558 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2559 2559 ('u', 'user', '',
2560 2560 _('add "From: <USER>" to patch'), _('USER')),
2561 2561 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2562 2562 ('d', 'date', '',
2563 2563 _('add "Date: <DATE>" to patch'), _('DATE'))
2564 2564 ] + commands.walkopts + commands.commitopts,
2565 2565 _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'),
2566 2566 inferrepo=True)
2567 2567 def new(ui, repo, patch, *args, **opts):
2568 2568 """create a new patch
2569 2569
2570 2570 qnew creates a new patch on top of the currently-applied patch (if
2571 2571 any). The patch will be initialized with any outstanding changes
2572 2572 in the working directory. You may also use -I/--include,
2573 2573 -X/--exclude, and/or a list of files after the patch name to add
2574 2574 only changes to matching files to the new patch, leaving the rest
2575 2575 as uncommitted modifications.
2576 2576
2577 2577 -u/--user and -d/--date can be used to set the (given) user and
2578 2578 date, respectively. -U/--currentuser and -D/--currentdate set user
2579 2579 to current user and date to current date.
2580 2580
2581 2581 -e/--edit, -m/--message or -l/--logfile set the patch header as
2582 2582 well as the commit message. If none is specified, the header is
2583 2583 empty and the commit message is '[mq]: PATCH'.
2584 2584
2585 2585 Use the -g/--git option to keep the patch in the git extended diff
2586 2586 format. Read the diffs help topic for more information on why this
2587 2587 is important for preserving permission changes and copy/rename
2588 2588 information.
2589 2589
2590 2590 Returns 0 on successful creation of a new patch.
2591 2591 """
2592 2592 msg = cmdutil.logmessage(ui, opts)
2593 2593 q = repo.mq
2594 2594 opts['msg'] = msg
2595 2595 setupheaderopts(ui, opts)
2596 2596 q.new(repo, patch, *args, **opts)
2597 2597 q.savedirty()
2598 2598 return 0
2599 2599
2600 2600 @command("^qrefresh",
2601 2601 [('e', 'edit', None, _('invoke editor on commit messages')),
2602 2602 ('g', 'git', None, _('use git extended diff format')),
2603 2603 ('s', 'short', None,
2604 2604 _('refresh only files already in the patch and specified files')),
2605 2605 ('U', 'currentuser', None,
2606 2606 _('add/update author field in patch with current user')),
2607 2607 ('u', 'user', '',
2608 2608 _('add/update author field in patch with given user'), _('USER')),
2609 2609 ('D', 'currentdate', None,
2610 2610 _('add/update date field in patch with current date')),
2611 2611 ('d', 'date', '',
2612 2612 _('add/update date field in patch with given date'), _('DATE'))
2613 2613 ] + commands.walkopts + commands.commitopts,
2614 2614 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'),
2615 2615 inferrepo=True)
2616 2616 def refresh(ui, repo, *pats, **opts):
2617 2617 """update the current patch
2618 2618
2619 2619 If any file patterns are provided, the refreshed patch will
2620 2620 contain only the modifications that match those patterns; the
2621 2621 remaining modifications will remain in the working directory.
2622 2622
2623 2623 If -s/--short is specified, files currently included in the patch
2624 2624 will be refreshed just like matched files and remain in the patch.
2625 2625
2626 2626 If -e/--edit is specified, Mercurial will start your configured editor for
2627 2627 you to enter a message. In case qrefresh fails, you will find a backup of
2628 2628 your message in ``.hg/last-message.txt``.
2629 2629
2630 2630 hg add/remove/copy/rename work as usual, though you might want to
2631 2631 use git-style patches (-g/--git or [diff] git=1) to track copies
2632 2632 and renames. See the diffs help topic for more information on the
2633 2633 git diff format.
2634 2634
2635 2635 Returns 0 on success.
2636 2636 """
2637 2637 q = repo.mq
2638 2638 message = cmdutil.logmessage(ui, opts)
2639 2639 setupheaderopts(ui, opts)
2640 2640 wlock = repo.wlock()
2641 2641 try:
2642 2642 ret = q.refresh(repo, pats, msg=message, **opts)
2643 2643 q.savedirty()
2644 2644 return ret
2645 2645 finally:
2646 2646 wlock.release()
2647 2647
2648 2648 @command("^qdiff",
2649 2649 commands.diffopts + commands.diffopts2 + commands.walkopts,
2650 2650 _('hg qdiff [OPTION]... [FILE]...'),
2651 2651 inferrepo=True)
2652 2652 def diff(ui, repo, *pats, **opts):
2653 2653 """diff of the current patch and subsequent modifications
2654 2654
2655 2655 Shows a diff which includes the current patch as well as any
2656 2656 changes which have been made in the working directory since the
2657 2657 last refresh (thus showing what the current patch would become
2658 2658 after a qrefresh).
2659 2659
2660 2660 Use :hg:`diff` if you only want to see the changes made since the
2661 2661 last qrefresh, or :hg:`export qtip` if you want to see changes
2662 2662 made by the current patch without including changes made since the
2663 2663 qrefresh.
2664 2664
2665 2665 Returns 0 on success.
2666 2666 """
2667 2667 repo.mq.diff(repo, pats, opts)
2668 2668 return 0
2669 2669
2670 2670 @command('qfold',
2671 2671 [('e', 'edit', None, _('invoke editor on commit messages')),
2672 2672 ('k', 'keep', None, _('keep folded patch files')),
2673 2673 ] + commands.commitopts,
2674 2674 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'))
2675 2675 def fold(ui, repo, *files, **opts):
2676 2676 """fold the named patches into the current patch
2677 2677
2678 2678 Patches must not yet be applied. Each patch will be successively
2679 2679 applied to the current patch in the order given. If all the
2680 2680 patches apply successfully, the current patch will be refreshed
2681 2681 with the new cumulative patch, and the folded patches will be
2682 2682 deleted. With -k/--keep, the folded patch files will not be
2683 2683 removed afterwards.
2684 2684
2685 2685 The header for each folded patch will be concatenated with the
2686 2686 current patch header, separated by a line of ``* * *``.
2687 2687
2688 2688 Returns 0 on success."""
2689 2689 q = repo.mq
2690 2690 if not files:
2691 2691 raise error.Abort(_('qfold requires at least one patch name'))
2692 2692 if not q.checktoppatch(repo)[0]:
2693 2693 raise error.Abort(_('no patches applied'))
2694 2694 q.checklocalchanges(repo)
2695 2695
2696 2696 message = cmdutil.logmessage(ui, opts)
2697 2697
2698 2698 parent = q.lookup('qtip')
2699 2699 patches = []
2700 2700 messages = []
2701 2701 for f in files:
2702 2702 p = q.lookup(f)
2703 2703 if p in patches or p == parent:
2704 2704 ui.warn(_('skipping already folded patch %s\n') % p)
2705 2705 if q.isapplied(p):
2706 2706 raise error.Abort(_('qfold cannot fold already applied patch %s')
2707 2707 % p)
2708 2708 patches.append(p)
2709 2709
2710 2710 for p in patches:
2711 2711 if not message:
2712 2712 ph = patchheader(q.join(p), q.plainmode)
2713 2713 if ph.message:
2714 2714 messages.append(ph.message)
2715 2715 pf = q.join(p)
2716 2716 (patchsuccess, files, fuzz) = q.patch(repo, pf)
2717 2717 if not patchsuccess:
2718 2718 raise error.Abort(_('error folding patch %s') % p)
2719 2719
2720 2720 if not message:
2721 2721 ph = patchheader(q.join(parent), q.plainmode)
2722 2722 message = ph.message
2723 2723 for msg in messages:
2724 2724 if msg:
2725 2725 if message:
2726 2726 message.append('* * *')
2727 2727 message.extend(msg)
2728 2728 message = '\n'.join(message)
2729 2729
2730 2730 diffopts = q.patchopts(q.diffopts(), *patches)
2731 2731 wlock = repo.wlock()
2732 2732 try:
2733 2733 q.refresh(repo, msg=message, git=diffopts.git, edit=opts.get('edit'),
2734 2734 editform='mq.qfold')
2735 2735 q.delete(repo, patches, opts)
2736 2736 q.savedirty()
2737 2737 finally:
2738 2738 wlock.release()
2739 2739
2740 2740 @command("qgoto",
2741 2741 [('', 'keep-changes', None,
2742 2742 _('tolerate non-conflicting local changes')),
2743 2743 ('f', 'force', None, _('overwrite any local changes')),
2744 2744 ('', 'no-backup', None, _('do not save backup copies of files'))],
2745 2745 _('hg qgoto [OPTION]... PATCH'))
2746 2746 def goto(ui, repo, patch, **opts):
2747 2747 '''push or pop patches until named patch is at top of stack
2748 2748
2749 2749 Returns 0 on success.'''
2750 2750 opts = fixkeepchangesopts(ui, opts)
2751 2751 q = repo.mq
2752 2752 patch = q.lookup(patch)
2753 2753 nobackup = opts.get('no_backup')
2754 2754 keepchanges = opts.get('keep_changes')
2755 2755 if q.isapplied(patch):
2756 2756 ret = q.pop(repo, patch, force=opts.get('force'), nobackup=nobackup,
2757 2757 keepchanges=keepchanges)
2758 2758 else:
2759 2759 ret = q.push(repo, patch, force=opts.get('force'), nobackup=nobackup,
2760 2760 keepchanges=keepchanges)
2761 2761 q.savedirty()
2762 2762 return ret
2763 2763
2764 2764 @command("qguard",
2765 2765 [('l', 'list', None, _('list all patches and guards')),
2766 2766 ('n', 'none', None, _('drop all guards'))],
2767 2767 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'))
2768 2768 def guard(ui, repo, *args, **opts):
2769 2769 '''set or print guards for a patch
2770 2770
2771 2771 Guards control whether a patch can be pushed. A patch with no
2772 2772 guards is always pushed. A patch with a positive guard ("+foo") is
2773 2773 pushed only if the :hg:`qselect` command has activated it. A patch with
2774 2774 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
2775 2775 has activated it.
2776 2776
2777 2777 With no arguments, print the currently active guards.
2778 2778 With arguments, set guards for the named patch.
2779 2779
2780 2780 .. note::
2781 2781
2782 2782 Specifying negative guards now requires '--'.
2783 2783
2784 2784 To set guards on another patch::
2785 2785
2786 2786 hg qguard other.patch -- +2.6.17 -stable
2787 2787
2788 2788 Returns 0 on success.
2789 2789 '''
2790 2790 def status(idx):
2791 2791 guards = q.seriesguards[idx] or ['unguarded']
2792 2792 if q.series[idx] in applied:
2793 2793 state = 'applied'
2794 2794 elif q.pushable(idx)[0]:
2795 2795 state = 'unapplied'
2796 2796 else:
2797 2797 state = 'guarded'
2798 2798 label = 'qguard.patch qguard.%s qseries.%s' % (state, state)
2799 2799 ui.write('%s: ' % ui.label(q.series[idx], label))
2800 2800
2801 2801 for i, guard in enumerate(guards):
2802 2802 if guard.startswith('+'):
2803 2803 ui.write(guard, label='qguard.positive')
2804 2804 elif guard.startswith('-'):
2805 2805 ui.write(guard, label='qguard.negative')
2806 2806 else:
2807 2807 ui.write(guard, label='qguard.unguarded')
2808 2808 if i != len(guards) - 1:
2809 2809 ui.write(' ')
2810 2810 ui.write('\n')
2811 2811 q = repo.mq
2812 2812 applied = set(p.name for p in q.applied)
2813 2813 patch = None
2814 2814 args = list(args)
2815 2815 if opts.get('list'):
2816 2816 if args or opts.get('none'):
2817 2817 raise error.Abort(_('cannot mix -l/--list with options or '
2818 2818 'arguments'))
2819 2819 for i in xrange(len(q.series)):
2820 2820 status(i)
2821 2821 return
2822 2822 if not args or args[0][0:1] in '-+':
2823 2823 if not q.applied:
2824 2824 raise error.Abort(_('no patches applied'))
2825 2825 patch = q.applied[-1].name
2826 2826 if patch is None and args[0][0:1] not in '-+':
2827 2827 patch = args.pop(0)
2828 2828 if patch is None:
2829 2829 raise error.Abort(_('no patch to work with'))
2830 2830 if args or opts.get('none'):
2831 2831 idx = q.findseries(patch)
2832 2832 if idx is None:
2833 2833 raise error.Abort(_('no patch named %s') % patch)
2834 2834 q.setguards(idx, args)
2835 2835 q.savedirty()
2836 2836 else:
2837 2837 status(q.series.index(q.lookup(patch)))
2838 2838
2839 2839 @command("qheader", [], _('hg qheader [PATCH]'))
2840 2840 def header(ui, repo, patch=None):
2841 2841 """print the header of the topmost or specified patch
2842 2842
2843 2843 Returns 0 on success."""
2844 2844 q = repo.mq
2845 2845
2846 2846 if patch:
2847 2847 patch = q.lookup(patch)
2848 2848 else:
2849 2849 if not q.applied:
2850 2850 ui.write(_('no patches applied\n'))
2851 2851 return 1
2852 2852 patch = q.lookup('qtip')
2853 2853 ph = patchheader(q.join(patch), q.plainmode)
2854 2854
2855 2855 ui.write('\n'.join(ph.message) + '\n')
2856 2856
2857 2857 def lastsavename(path):
2858 2858 (directory, base) = os.path.split(path)
2859 2859 names = os.listdir(directory)
2860 2860 namere = re.compile("%s.([0-9]+)" % base)
2861 2861 maxindex = None
2862 2862 maxname = None
2863 2863 for f in names:
2864 2864 m = namere.match(f)
2865 2865 if m:
2866 2866 index = int(m.group(1))
2867 2867 if maxindex is None or index > maxindex:
2868 2868 maxindex = index
2869 2869 maxname = f
2870 2870 if maxname:
2871 2871 return (os.path.join(directory, maxname), maxindex)
2872 2872 return (None, None)
2873 2873
2874 2874 def savename(path):
2875 2875 (last, index) = lastsavename(path)
2876 2876 if last is None:
2877 2877 index = 0
2878 2878 newpath = path + ".%d" % (index + 1)
2879 2879 return newpath
2880 2880
2881 2881 @command("^qpush",
2882 2882 [('', 'keep-changes', None,
2883 2883 _('tolerate non-conflicting local changes')),
2884 2884 ('f', 'force', None, _('apply on top of local changes')),
2885 2885 ('e', 'exact', None,
2886 2886 _('apply the target patch to its recorded parent')),
2887 2887 ('l', 'list', None, _('list patch name in commit text')),
2888 2888 ('a', 'all', None, _('apply all patches')),
2889 2889 ('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
2890 2890 ('n', 'name', '',
2891 2891 _('merge queue name (DEPRECATED)'), _('NAME')),
2892 2892 ('', 'move', None,
2893 2893 _('reorder patch series and apply only the patch')),
2894 2894 ('', 'no-backup', None, _('do not save backup copies of files'))],
2895 2895 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
2896 2896 def push(ui, repo, patch=None, **opts):
2897 2897 """push the next patch onto the stack
2898 2898
2899 2899 By default, abort if the working directory contains uncommitted
2900 2900 changes. With --keep-changes, abort only if the uncommitted files
2901 2901 overlap with patched files. With -f/--force, backup and patch over
2902 2902 uncommitted changes.
2903 2903
2904 2904 Return 0 on success.
2905 2905 """
2906 2906 q = repo.mq
2907 2907 mergeq = None
2908 2908
2909 2909 opts = fixkeepchangesopts(ui, opts)
2910 2910 if opts.get('merge'):
2911 2911 if opts.get('name'):
2912 2912 newpath = repo.join(opts.get('name'))
2913 2913 else:
2914 2914 newpath, i = lastsavename(q.path)
2915 2915 if not newpath:
2916 2916 ui.warn(_("no saved queues found, please use -n\n"))
2917 2917 return 1
2918 2918 mergeq = queue(ui, repo.baseui, repo.path, newpath)
2919 2919 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2920 2920 ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
2921 2921 mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
2922 2922 exact=opts.get('exact'), nobackup=opts.get('no_backup'),
2923 2923 keepchanges=opts.get('keep_changes'))
2924 2924 return ret
2925 2925
2926 2926 @command("^qpop",
2927 2927 [('a', 'all', None, _('pop all patches')),
2928 2928 ('n', 'name', '',
2929 2929 _('queue name to pop (DEPRECATED)'), _('NAME')),
2930 2930 ('', 'keep-changes', None,
2931 2931 _('tolerate non-conflicting local changes')),
2932 2932 ('f', 'force', None, _('forget any local changes to patched files')),
2933 2933 ('', 'no-backup', None, _('do not save backup copies of files'))],
2934 2934 _('hg qpop [-a] [-f] [PATCH | INDEX]'))
2935 2935 def pop(ui, repo, patch=None, **opts):
2936 2936 """pop the current patch off the stack
2937 2937
2938 2938 Without argument, pops off the top of the patch stack. If given a
2939 2939 patch name, keeps popping off patches until the named patch is at
2940 2940 the top of the stack.
2941 2941
2942 2942 By default, abort if the working directory contains uncommitted
2943 2943 changes. With --keep-changes, abort only if the uncommitted files
2944 2944 overlap with patched files. With -f/--force, backup and discard
2945 2945 changes made to such files.
2946 2946
2947 2947 Return 0 on success.
2948 2948 """
2949 2949 opts = fixkeepchangesopts(ui, opts)
2950 2950 localupdate = True
2951 2951 if opts.get('name'):
2952 2952 q = queue(ui, repo.baseui, repo.path, repo.join(opts.get('name')))
2953 2953 ui.warn(_('using patch queue: %s\n') % q.path)
2954 2954 localupdate = False
2955 2955 else:
2956 2956 q = repo.mq
2957 2957 ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
2958 2958 all=opts.get('all'), nobackup=opts.get('no_backup'),
2959 2959 keepchanges=opts.get('keep_changes'))
2960 2960 q.savedirty()
2961 2961 return ret
2962 2962
2963 2963 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'))
2964 2964 def rename(ui, repo, patch, name=None, **opts):
2965 2965 """rename a patch
2966 2966
2967 2967 With one argument, renames the current patch to PATCH1.
2968 2968 With two arguments, renames PATCH1 to PATCH2.
2969 2969
2970 2970 Returns 0 on success."""
2971 2971 q = repo.mq
2972 2972 if not name:
2973 2973 name = patch
2974 2974 patch = None
2975 2975
2976 2976 if patch:
2977 2977 patch = q.lookup(patch)
2978 2978 else:
2979 2979 if not q.applied:
2980 2980 ui.write(_('no patches applied\n'))
2981 2981 return
2982 2982 patch = q.lookup('qtip')
2983 2983 absdest = q.join(name)
2984 2984 if os.path.isdir(absdest):
2985 2985 name = normname(os.path.join(name, os.path.basename(patch)))
2986 2986 absdest = q.join(name)
2987 2987 q.checkpatchname(name)
2988 2988
2989 2989 ui.note(_('renaming %s to %s\n') % (patch, name))
2990 2990 i = q.findseries(patch)
2991 2991 guards = q.guard_re.findall(q.fullseries[i])
2992 2992 q.fullseries[i] = name + ''.join([' #' + g for g in guards])
2993 2993 q.parseseries()
2994 2994 q.seriesdirty = True
2995 2995
2996 2996 info = q.isapplied(patch)
2997 2997 if info:
2998 2998 q.applied[info[0]] = statusentry(info[1], name)
2999 2999 q.applieddirty = True
3000 3000
3001 3001 destdir = os.path.dirname(absdest)
3002 3002 if not os.path.isdir(destdir):
3003 3003 os.makedirs(destdir)
3004 3004 util.rename(q.join(patch), absdest)
3005 3005 r = q.qrepo()
3006 3006 if r and patch in r.dirstate:
3007 3007 wctx = r[None]
3008 3008 wlock = r.wlock()
3009 3009 try:
3010 3010 if r.dirstate[patch] == 'a':
3011 3011 r.dirstate.drop(patch)
3012 3012 r.dirstate.add(name)
3013 3013 else:
3014 3014 wctx.copy(patch, name)
3015 3015 wctx.forget([patch])
3016 3016 finally:
3017 3017 wlock.release()
3018 3018
3019 3019 q.savedirty()
3020 3020
3021 3021 @command("qrestore",
3022 3022 [('d', 'delete', None, _('delete save entry')),
3023 3023 ('u', 'update', None, _('update queue working directory'))],
3024 3024 _('hg qrestore [-d] [-u] REV'))
3025 3025 def restore(ui, repo, rev, **opts):
3026 3026 """restore the queue state saved by a revision (DEPRECATED)
3027 3027
3028 3028 This command is deprecated, use :hg:`rebase` instead."""
3029 3029 rev = repo.lookup(rev)
3030 3030 q = repo.mq
3031 3031 q.restore(repo, rev, delete=opts.get('delete'),
3032 3032 qupdate=opts.get('update'))
3033 3033 q.savedirty()
3034 3034 return 0
3035 3035
3036 3036 @command("qsave",
3037 3037 [('c', 'copy', None, _('copy patch directory')),
3038 3038 ('n', 'name', '',
3039 3039 _('copy directory name'), _('NAME')),
3040 3040 ('e', 'empty', None, _('clear queue status file')),
3041 3041 ('f', 'force', None, _('force copy'))] + commands.commitopts,
3042 3042 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'))
3043 3043 def save(ui, repo, **opts):
3044 3044 """save current queue state (DEPRECATED)
3045 3045
3046 3046 This command is deprecated, use :hg:`rebase` instead."""
3047 3047 q = repo.mq
3048 3048 message = cmdutil.logmessage(ui, opts)
3049 3049 ret = q.save(repo, msg=message)
3050 3050 if ret:
3051 3051 return ret
3052 3052 q.savedirty() # save to .hg/patches before copying
3053 3053 if opts.get('copy'):
3054 3054 path = q.path
3055 3055 if opts.get('name'):
3056 3056 newpath = os.path.join(q.basepath, opts.get('name'))
3057 3057 if os.path.exists(newpath):
3058 3058 if not os.path.isdir(newpath):
3059 3059 raise error.Abort(_('destination %s exists and is not '
3060 3060 'a directory') % newpath)
3061 3061 if not opts.get('force'):
3062 3062 raise error.Abort(_('destination %s exists, '
3063 3063 'use -f to force') % newpath)
3064 3064 else:
3065 3065 newpath = savename(path)
3066 3066 ui.warn(_("copy %s to %s\n") % (path, newpath))
3067 3067 util.copyfiles(path, newpath)
3068 3068 if opts.get('empty'):
3069 3069 del q.applied[:]
3070 3070 q.applieddirty = True
3071 3071 q.savedirty()
3072 3072 return 0
3073 3073
3074 3074
3075 3075 @command("qselect",
3076 3076 [('n', 'none', None, _('disable all guards')),
3077 3077 ('s', 'series', None, _('list all guards in series file')),
3078 3078 ('', 'pop', None, _('pop to before first guarded applied patch')),
3079 3079 ('', 'reapply', None, _('pop, then reapply patches'))],
3080 3080 _('hg qselect [OPTION]... [GUARD]...'))
3081 3081 def select(ui, repo, *args, **opts):
3082 3082 '''set or print guarded patches to push
3083 3083
3084 3084 Use the :hg:`qguard` command to set or print guards on patch, then use
3085 3085 qselect to tell mq which guards to use. A patch will be pushed if
3086 3086 it has no guards or any positive guards match the currently
3087 3087 selected guard, but will not be pushed if any negative guards
3088 3088 match the current guard. For example::
3089 3089
3090 3090 qguard foo.patch -- -stable (negative guard)
3091 3091 qguard bar.patch +stable (positive guard)
3092 3092 qselect stable
3093 3093
3094 3094 This activates the "stable" guard. mq will skip foo.patch (because
3095 3095 it has a negative match) but push bar.patch (because it has a
3096 3096 positive match).
3097 3097
3098 3098 With no arguments, prints the currently active guards.
3099 3099 With one argument, sets the active guard.
3100 3100
3101 3101 Use -n/--none to deactivate guards (no other arguments needed).
3102 3102 When no guards are active, patches with positive guards are
3103 3103 skipped and patches with negative guards are pushed.
3104 3104
3105 3105 qselect can change the guards on applied patches. It does not pop
3106 3106 guarded patches by default. Use --pop to pop back to the last
3107 3107 applied patch that is not guarded. Use --reapply (which implies
3108 3108 --pop) to push back to the current patch afterwards, but skip
3109 3109 guarded patches.
3110 3110
3111 3111 Use -s/--series to print a list of all guards in the series file
3112 3112 (no other arguments needed). Use -v for more information.
3113 3113
3114 3114 Returns 0 on success.'''
3115 3115
3116 3116 q = repo.mq
3117 3117 guards = q.active()
3118 3118 pushable = lambda i: q.pushable(q.applied[i].name)[0]
3119 3119 if args or opts.get('none'):
3120 3120 old_unapplied = q.unapplied(repo)
3121 3121 old_guarded = [i for i in xrange(len(q.applied)) if not pushable(i)]
3122 3122 q.setactive(args)
3123 3123 q.savedirty()
3124 3124 if not args:
3125 3125 ui.status(_('guards deactivated\n'))
3126 3126 if not opts.get('pop') and not opts.get('reapply'):
3127 3127 unapplied = q.unapplied(repo)
3128 3128 guarded = [i for i in xrange(len(q.applied)) if not pushable(i)]
3129 3129 if len(unapplied) != len(old_unapplied):
3130 3130 ui.status(_('number of unguarded, unapplied patches has '
3131 3131 'changed from %d to %d\n') %
3132 3132 (len(old_unapplied), len(unapplied)))
3133 3133 if len(guarded) != len(old_guarded):
3134 3134 ui.status(_('number of guarded, applied patches has changed '
3135 3135 'from %d to %d\n') %
3136 3136 (len(old_guarded), len(guarded)))
3137 3137 elif opts.get('series'):
3138 3138 guards = {}
3139 3139 noguards = 0
3140 3140 for gs in q.seriesguards:
3141 3141 if not gs:
3142 3142 noguards += 1
3143 3143 for g in gs:
3144 3144 guards.setdefault(g, 0)
3145 3145 guards[g] += 1
3146 3146 if ui.verbose:
3147 3147 guards['NONE'] = noguards
3148 3148 guards = guards.items()
3149 3149 guards.sort(key=lambda x: x[0][1:])
3150 3150 if guards:
3151 3151 ui.note(_('guards in series file:\n'))
3152 3152 for guard, count in guards:
3153 3153 ui.note('%2d ' % count)
3154 3154 ui.write(guard, '\n')
3155 3155 else:
3156 3156 ui.note(_('no guards in series file\n'))
3157 3157 else:
3158 3158 if guards:
3159 3159 ui.note(_('active guards:\n'))
3160 3160 for g in guards:
3161 3161 ui.write(g, '\n')
3162 3162 else:
3163 3163 ui.write(_('no active guards\n'))
3164 3164 reapply = opts.get('reapply') and q.applied and q.applied[-1].name
3165 3165 popped = False
3166 3166 if opts.get('pop') or opts.get('reapply'):
3167 3167 for i in xrange(len(q.applied)):
3168 3168 if not pushable(i):
3169 3169 ui.status(_('popping guarded patches\n'))
3170 3170 popped = True
3171 3171 if i == 0:
3172 3172 q.pop(repo, all=True)
3173 3173 else:
3174 3174 q.pop(repo, q.applied[i - 1].name)
3175 3175 break
3176 3176 if popped:
3177 3177 try:
3178 3178 if reapply:
3179 3179 ui.status(_('reapplying unguarded patches\n'))
3180 3180 q.push(repo, reapply)
3181 3181 finally:
3182 3182 q.savedirty()
3183 3183
3184 3184 @command("qfinish",
3185 3185 [('a', 'applied', None, _('finish all applied changesets'))],
3186 3186 _('hg qfinish [-a] [REV]...'))
3187 3187 def finish(ui, repo, *revrange, **opts):
3188 3188 """move applied patches into repository history
3189 3189
3190 3190 Finishes the specified revisions (corresponding to applied
3191 3191 patches) by moving them out of mq control into regular repository
3192 3192 history.
3193 3193
3194 3194 Accepts a revision range or the -a/--applied option. If --applied
3195 3195 is specified, all applied mq revisions are removed from mq
3196 3196 control. Otherwise, the given revisions must be at the base of the
3197 3197 stack of applied patches.
3198 3198
3199 3199 This can be especially useful if your changes have been applied to
3200 3200 an upstream repository, or if you are about to push your changes
3201 3201 to upstream.
3202 3202
3203 3203 Returns 0 on success.
3204 3204 """
3205 3205 if not opts.get('applied') and not revrange:
3206 3206 raise error.Abort(_('no revisions specified'))
3207 3207 elif opts.get('applied'):
3208 3208 revrange = ('qbase::qtip',) + revrange
3209 3209
3210 3210 q = repo.mq
3211 3211 if not q.applied:
3212 3212 ui.status(_('no patches applied\n'))
3213 3213 return 0
3214 3214
3215 3215 revs = scmutil.revrange(repo, revrange)
3216 3216 if repo['.'].rev() in revs and repo[None].files():
3217 3217 ui.warn(_('warning: uncommitted changes in the working directory\n'))
3218 3218 # queue.finish may changes phases but leave the responsibility to lock the
3219 3219 # repo to the caller to avoid deadlock with wlock. This command code is
3220 3220 # responsibility for this locking.
3221 3221 lock = repo.lock()
3222 3222 try:
3223 3223 q.finish(repo, revs)
3224 3224 q.savedirty()
3225 3225 finally:
3226 3226 lock.release()
3227 3227 return 0
3228 3228
3229 3229 @command("qqueue",
3230 3230 [('l', 'list', False, _('list all available queues')),
3231 3231 ('', 'active', False, _('print name of active queue')),
3232 3232 ('c', 'create', False, _('create new queue')),
3233 3233 ('', 'rename', False, _('rename active queue')),
3234 3234 ('', 'delete', False, _('delete reference to queue')),
3235 3235 ('', 'purge', False, _('delete queue, and remove patch dir')),
3236 3236 ],
3237 3237 _('[OPTION] [QUEUE]'))
3238 3238 def qqueue(ui, repo, name=None, **opts):
3239 3239 '''manage multiple patch queues
3240 3240
3241 3241 Supports switching between different patch queues, as well as creating
3242 3242 new patch queues and deleting existing ones.
3243 3243
3244 3244 Omitting a queue name or specifying -l/--list will show you the registered
3245 3245 queues - by default the "normal" patches queue is registered. The currently
3246 3246 active queue will be marked with "(active)". Specifying --active will print
3247 3247 only the name of the active queue.
3248 3248
3249 3249 To create a new queue, use -c/--create. The queue is automatically made
3250 3250 active, except in the case where there are applied patches from the
3251 3251 currently active queue in the repository. Then the queue will only be
3252 3252 created and switching will fail.
3253 3253
3254 3254 To delete an existing queue, use --delete. You cannot delete the currently
3255 3255 active queue.
3256 3256
3257 3257 Returns 0 on success.
3258 3258 '''
3259 3259 q = repo.mq
3260 3260 _defaultqueue = 'patches'
3261 3261 _allqueues = 'patches.queues'
3262 3262 _activequeue = 'patches.queue'
3263 3263
3264 3264 def _getcurrent():
3265 3265 cur = os.path.basename(q.path)
3266 3266 if cur.startswith('patches-'):
3267 3267 cur = cur[8:]
3268 3268 return cur
3269 3269
3270 3270 def _noqueues():
3271 3271 try:
3272 3272 fh = repo.vfs(_allqueues, 'r')
3273 3273 fh.close()
3274 3274 except IOError:
3275 3275 return True
3276 3276
3277 3277 return False
3278 3278
3279 3279 def _getqueues():
3280 3280 current = _getcurrent()
3281 3281
3282 3282 try:
3283 3283 fh = repo.vfs(_allqueues, 'r')
3284 3284 queues = [queue.strip() for queue in fh if queue.strip()]
3285 3285 fh.close()
3286 3286 if current not in queues:
3287 3287 queues.append(current)
3288 3288 except IOError:
3289 3289 queues = [_defaultqueue]
3290 3290
3291 3291 return sorted(queues)
3292 3292
3293 3293 def _setactive(name):
3294 3294 if q.applied:
3295 3295 raise error.Abort(_('new queue created, but cannot make active '
3296 3296 'as patches are applied'))
3297 3297 _setactivenocheck(name)
3298 3298
3299 3299 def _setactivenocheck(name):
3300 3300 fh = repo.vfs(_activequeue, 'w')
3301 3301 if name != 'patches':
3302 3302 fh.write(name)
3303 3303 fh.close()
3304 3304
3305 3305 def _addqueue(name):
3306 3306 fh = repo.vfs(_allqueues, 'a')
3307 3307 fh.write('%s\n' % (name,))
3308 3308 fh.close()
3309 3309
3310 3310 def _queuedir(name):
3311 3311 if name == 'patches':
3312 3312 return repo.join('patches')
3313 3313 else:
3314 3314 return repo.join('patches-' + name)
3315 3315
3316 3316 def _validname(name):
3317 3317 for n in name:
3318 3318 if n in ':\\/.':
3319 3319 return False
3320 3320 return True
3321 3321
3322 3322 def _delete(name):
3323 3323 if name not in existing:
3324 3324 raise error.Abort(_('cannot delete queue that does not exist'))
3325 3325
3326 3326 current = _getcurrent()
3327 3327
3328 3328 if name == current:
3329 3329 raise error.Abort(_('cannot delete currently active queue'))
3330 3330
3331 3331 fh = repo.vfs('patches.queues.new', 'w')
3332 3332 for queue in existing:
3333 3333 if queue == name:
3334 3334 continue
3335 3335 fh.write('%s\n' % (queue,))
3336 3336 fh.close()
3337 3337 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3338 3338
3339 3339 if not name or opts.get('list') or opts.get('active'):
3340 3340 current = _getcurrent()
3341 3341 if opts.get('active'):
3342 3342 ui.write('%s\n' % (current,))
3343 3343 return
3344 3344 for queue in _getqueues():
3345 3345 ui.write('%s' % (queue,))
3346 3346 if queue == current and not ui.quiet:
3347 3347 ui.write(_(' (active)\n'))
3348 3348 else:
3349 3349 ui.write('\n')
3350 3350 return
3351 3351
3352 3352 if not _validname(name):
3353 3353 raise error.Abort(
3354 3354 _('invalid queue name, may not contain the characters ":\\/."'))
3355 3355
3356 3356 existing = _getqueues()
3357 3357
3358 3358 if opts.get('create'):
3359 3359 if name in existing:
3360 3360 raise error.Abort(_('queue "%s" already exists') % name)
3361 3361 if _noqueues():
3362 3362 _addqueue(_defaultqueue)
3363 3363 _addqueue(name)
3364 3364 _setactive(name)
3365 3365 elif opts.get('rename'):
3366 3366 current = _getcurrent()
3367 3367 if name == current:
3368 3368 raise error.Abort(_('can\'t rename "%s" to its current name')
3369 3369 % name)
3370 3370 if name in existing:
3371 3371 raise error.Abort(_('queue "%s" already exists') % name)
3372 3372
3373 3373 olddir = _queuedir(current)
3374 3374 newdir = _queuedir(name)
3375 3375
3376 3376 if os.path.exists(newdir):
3377 3377 raise error.Abort(_('non-queue directory "%s" already exists') %
3378 3378 newdir)
3379 3379
3380 3380 fh = repo.vfs('patches.queues.new', 'w')
3381 3381 for queue in existing:
3382 3382 if queue == current:
3383 3383 fh.write('%s\n' % (name,))
3384 3384 if os.path.exists(olddir):
3385 3385 util.rename(olddir, newdir)
3386 3386 else:
3387 3387 fh.write('%s\n' % (queue,))
3388 3388 fh.close()
3389 3389 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3390 3390 _setactivenocheck(name)
3391 3391 elif opts.get('delete'):
3392 3392 _delete(name)
3393 3393 elif opts.get('purge'):
3394 3394 if name in existing:
3395 3395 _delete(name)
3396 3396 qdir = _queuedir(name)
3397 3397 if os.path.exists(qdir):
3398 3398 shutil.rmtree(qdir)
3399 3399 else:
3400 3400 if name not in existing:
3401 3401 raise error.Abort(_('use --create to create a new queue'))
3402 3402 _setactive(name)
3403 3403
3404 3404 def mqphasedefaults(repo, roots):
3405 3405 """callback used to set mq changeset as secret when no phase data exists"""
3406 3406 if repo.mq.applied:
3407 3407 if repo.ui.configbool('mq', 'secret', False):
3408 3408 mqphase = phases.secret
3409 3409 else:
3410 3410 mqphase = phases.draft
3411 3411 qbase = repo[repo.mq.applied[0].node]
3412 3412 roots[mqphase].add(qbase.node())
3413 3413 return roots
3414 3414
3415 3415 def reposetup(ui, repo):
3416 3416 class mqrepo(repo.__class__):
3417 3417 @localrepo.unfilteredpropertycache
3418 3418 def mq(self):
3419 3419 return queue(self.ui, self.baseui, self.path)
3420 3420
3421 3421 def invalidateall(self):
3422 3422 super(mqrepo, self).invalidateall()
3423 3423 if localrepo.hasunfilteredcache(self, 'mq'):
3424 3424 # recreate mq in case queue path was changed
3425 3425 delattr(self.unfiltered(), 'mq')
3426 3426
3427 3427 def abortifwdirpatched(self, errmsg, force=False):
3428 3428 if self.mq.applied and self.mq.checkapplied and not force:
3429 3429 parents = self.dirstate.parents()
3430 3430 patches = [s.node for s in self.mq.applied]
3431 3431 if parents[0] in patches or parents[1] in patches:
3432 3432 raise error.Abort(errmsg)
3433 3433
3434 3434 def commit(self, text="", user=None, date=None, match=None,
3435 3435 force=False, editor=False, extra={}):
3436 3436 self.abortifwdirpatched(
3437 3437 _('cannot commit over an applied mq patch'),
3438 3438 force)
3439 3439
3440 3440 return super(mqrepo, self).commit(text, user, date, match, force,
3441 3441 editor, extra)
3442 3442
3443 3443 def checkpush(self, pushop):
3444 3444 if self.mq.applied and self.mq.checkapplied and not pushop.force:
3445 3445 outapplied = [e.node for e in self.mq.applied]
3446 3446 if pushop.revs:
3447 3447 # Assume applied patches have no non-patch descendants and
3448 3448 # are not on remote already. Filtering any changeset not
3449 3449 # pushed.
3450 3450 heads = set(pushop.revs)
3451 3451 for node in reversed(outapplied):
3452 3452 if node in heads:
3453 3453 break
3454 3454 else:
3455 3455 outapplied.pop()
3456 3456 # looking for pushed and shared changeset
3457 3457 for node in outapplied:
3458 3458 if self[node].phase() < phases.secret:
3459 3459 raise error.Abort(_('source has mq patches applied'))
3460 3460 # no non-secret patches pushed
3461 3461 super(mqrepo, self).checkpush(pushop)
3462 3462
3463 3463 def _findtags(self):
3464 3464 '''augment tags from base class with patch tags'''
3465 3465 result = super(mqrepo, self)._findtags()
3466 3466
3467 3467 q = self.mq
3468 3468 if not q.applied:
3469 3469 return result
3470 3470
3471 3471 mqtags = [(patch.node, patch.name) for patch in q.applied]
3472 3472
3473 3473 try:
3474 3474 # for now ignore filtering business
3475 3475 self.unfiltered().changelog.rev(mqtags[-1][0])
3476 3476 except error.LookupError:
3477 3477 self.ui.warn(_('mq status file refers to unknown node %s\n')
3478 3478 % short(mqtags[-1][0]))
3479 3479 return result
3480 3480
3481 3481 # do not add fake tags for filtered revisions
3482 3482 included = self.changelog.hasnode
3483 3483 mqtags = [mqt for mqt in mqtags if included(mqt[0])]
3484 3484 if not mqtags:
3485 3485 return result
3486 3486
3487 3487 mqtags.append((mqtags[-1][0], 'qtip'))
3488 3488 mqtags.append((mqtags[0][0], 'qbase'))
3489 3489 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
3490 3490 tags = result[0]
3491 3491 for patch in mqtags:
3492 3492 if patch[1] in tags:
3493 3493 self.ui.warn(_('tag %s overrides mq patch of the same '
3494 3494 'name\n') % patch[1])
3495 3495 else:
3496 3496 tags[patch[1]] = patch[0]
3497 3497
3498 3498 return result
3499 3499
3500 3500 if repo.local():
3501 3501 repo.__class__ = mqrepo
3502 3502
3503 3503 repo._phasedefaults.append(mqphasedefaults)
3504 3504
3505 3505 def mqimport(orig, ui, repo, *args, **kwargs):
3506 3506 if (util.safehasattr(repo, 'abortifwdirpatched')
3507 3507 and not kwargs.get('no_commit', False)):
3508 3508 repo.abortifwdirpatched(_('cannot import over an applied patch'),
3509 3509 kwargs.get('force'))
3510 3510 return orig(ui, repo, *args, **kwargs)
3511 3511
3512 3512 def mqinit(orig, ui, *args, **kwargs):
3513 3513 mq = kwargs.pop('mq', None)
3514 3514
3515 3515 if not mq:
3516 3516 return orig(ui, *args, **kwargs)
3517 3517
3518 3518 if args:
3519 3519 repopath = args[0]
3520 3520 if not hg.islocal(repopath):
3521 3521 raise error.Abort(_('only a local queue repository '
3522 3522 'may be initialized'))
3523 3523 else:
3524 3524 repopath = cmdutil.findrepo(os.getcwd())
3525 3525 if not repopath:
3526 3526 raise error.Abort(_('there is no Mercurial repository here '
3527 3527 '(.hg not found)'))
3528 3528 repo = hg.repository(ui, repopath)
3529 3529 return qinit(ui, repo, True)
3530 3530
3531 3531 def mqcommand(orig, ui, repo, *args, **kwargs):
3532 3532 """Add --mq option to operate on patch repository instead of main"""
3533 3533
3534 3534 # some commands do not like getting unknown options
3535 3535 mq = kwargs.pop('mq', None)
3536 3536
3537 3537 if not mq:
3538 3538 return orig(ui, repo, *args, **kwargs)
3539 3539
3540 3540 q = repo.mq
3541 3541 r = q.qrepo()
3542 3542 if not r:
3543 3543 raise error.Abort(_('no queue repository'))
3544 3544 return orig(r.ui, r, *args, **kwargs)
3545 3545
3546 3546 def summaryhook(ui, repo):
3547 3547 q = repo.mq
3548 3548 m = []
3549 3549 a, u = len(q.applied), len(q.unapplied(repo))
3550 3550 if a:
3551 3551 m.append(ui.label(_("%d applied"), 'qseries.applied') % a)
3552 3552 if u:
3553 3553 m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u)
3554 3554 if m:
3555 3555 # i18n: column positioning for "hg summary"
3556 3556 ui.write(_("mq: %s\n") % ', '.join(m))
3557 3557 else:
3558 3558 # i18n: column positioning for "hg summary"
3559 3559 ui.note(_("mq: (empty queue)\n"))
3560 3560
3561 3561 revsetpredicate = revset.extpredicate()
3562 3562
3563 3563 @revsetpredicate('mq()')
3564 3564 def revsetmq(repo, subset, x):
3565 3565 """Changesets managed by MQ.
3566 3566 """
3567 3567 revset.getargs(x, 0, 0, _("mq takes no arguments"))
3568 3568 applied = set([repo[r.node].rev() for r in repo.mq.applied])
3569 3569 return revset.baseset([r for r in subset if r in applied])
3570 3570
3571 3571 # tell hggettext to extract docstrings from these functions:
3572 3572 i18nfunctions = [revsetmq]
3573 3573
3574 3574 def extsetup(ui):
3575 3575 # Ensure mq wrappers are called first, regardless of extension load order by
3576 3576 # NOT wrapping in uisetup() and instead deferring to init stage two here.
3577 3577 mqopt = [('', 'mq', None, _("operate on patch repository"))]
3578 3578
3579 3579 extensions.wrapcommand(commands.table, 'import', mqimport)
3580 3580 cmdutil.summaryhooks.add('mq', summaryhook)
3581 3581
3582 3582 entry = extensions.wrapcommand(commands.table, 'init', mqinit)
3583 3583 entry[1].extend(mqopt)
3584 3584
3585 3585 nowrap = set(commands.norepo.split(" "))
3586 3586
3587 3587 def dotable(cmdtable):
3588 3588 for cmd in cmdtable.keys():
3589 3589 cmd = cmdutil.parsealiases(cmd)[0]
3590 3590 if cmd in nowrap:
3591 3591 continue
3592 3592 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
3593 3593 entry[1].extend(mqopt)
3594 3594
3595 3595 dotable(commands.table)
3596 3596
3597 3597 for extname, extmodule in extensions.extensions():
3598 3598 if extmodule.__file__ != __file__:
3599 3599 dotable(getattr(extmodule, 'cmdtable', {}))
3600 3600
3601 3601 revsetpredicate.setup()
3602 3602
3603 3603 colortable = {'qguard.negative': 'red',
3604 3604 'qguard.positive': 'yellow',
3605 3605 'qguard.unguarded': 'green',
3606 3606 'qseries.applied': 'blue bold underline',
3607 3607 'qseries.guarded': 'black bold',
3608 3608 'qseries.missing': 'red bold',
3609 3609 'qseries.unapplied': 'black bold'}
@@ -1,854 +1,854
1 1 # shelve.py - save/restore working directory state
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """save and restore changes to the working directory
9 9
10 10 The "hg shelve" command saves changes made to the working directory
11 11 and reverts those changes, resetting the working directory to a clean
12 12 state.
13 13
14 14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 15 shelve". Changes can be restored even after updating to a different
16 16 parent, in which case Mercurial's merge machinery will resolve any
17 17 conflicts if necessary.
18 18
19 19 You can have more than one shelved change outstanding at a time; each
20 20 shelved change has a distinct name. For details, see the help for "hg
21 21 shelve".
22 22 """
23 23
24 24 import collections
25 25 import itertools
26 26 from mercurial.i18n import _
27 27 from mercurial.node import nullid, nullrev, bin, hex
28 28 from mercurial import changegroup, cmdutil, scmutil, phases, commands
29 29 from mercurial import error, hg, mdiff, merge, patch, repair, util
30 30 from mercurial import templatefilters, exchange, bundlerepo, bundle2
31 31 from mercurial import lock as lockmod
32 32 from hgext import rebase
33 33 import errno
34 34
35 35 cmdtable = {}
36 36 command = cmdutil.command(cmdtable)
37 37 # Note for extension authors: ONLY specify testedwith = 'internal' for
38 38 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
39 39 # be specifying the version(s) of Mercurial they are tested with, or
40 40 # leave the attribute unspecified.
41 41 testedwith = 'internal'
42 42
43 43 backupdir = 'shelve-backup'
44 44
45 45 class shelvedfile(object):
46 46 """Helper for the file storing a single shelve
47 47
48 48 Handles common functions on shelve files (.hg/.patch) using
49 49 the vfs layer"""
50 50 def __init__(self, repo, name, filetype=None):
51 51 self.repo = repo
52 52 self.name = name
53 53 self.vfs = scmutil.vfs(repo.join('shelved'))
54 54 self.backupvfs = scmutil.vfs(repo.join(backupdir))
55 55 self.ui = self.repo.ui
56 56 if filetype:
57 57 self.fname = name + '.' + filetype
58 58 else:
59 59 self.fname = name
60 60
61 61 def exists(self):
62 62 return self.vfs.exists(self.fname)
63 63
64 64 def filename(self):
65 65 return self.vfs.join(self.fname)
66 66
67 67 def backupfilename(self):
68 68 def gennames(base):
69 69 yield base
70 70 base, ext = base.rsplit('.', 1)
71 71 for i in itertools.count(1):
72 72 yield '%s-%d.%s' % (base, i, ext)
73 73
74 74 name = self.backupvfs.join(self.fname)
75 75 for n in gennames(name):
76 76 if not self.backupvfs.exists(n):
77 77 return n
78 78
79 79 def movetobackup(self):
80 80 if not self.backupvfs.isdir():
81 81 self.backupvfs.makedir()
82 82 util.rename(self.filename(), self.backupfilename())
83 83
84 84 def stat(self):
85 85 return self.vfs.stat(self.fname)
86 86
87 87 def opener(self, mode='rb'):
88 88 try:
89 89 return self.vfs(self.fname, mode)
90 90 except IOError as err:
91 91 if err.errno != errno.ENOENT:
92 92 raise
93 93 raise error.Abort(_("shelved change '%s' not found") % self.name)
94 94
95 95 def applybundle(self):
96 96 fp = self.opener()
97 97 try:
98 98 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
99 99 if not isinstance(gen, bundle2.unbundle20):
100 100 gen.apply(self.repo, 'unshelve',
101 101 'bundle:' + self.vfs.join(self.fname),
102 102 targetphase=phases.secret)
103 103 if isinstance(gen, bundle2.unbundle20):
104 104 bundle2.applybundle(self.repo, gen,
105 105 self.repo.currenttransaction(),
106 106 source='unshelve',
107 107 url='bundle:' + self.vfs.join(self.fname))
108 108 finally:
109 109 fp.close()
110 110
111 111 def bundlerepo(self):
112 112 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
113 113 self.vfs.join(self.fname))
114 114 def writebundle(self, bases, node):
115 115 btype = 'HG10BZ'
116 116 cgversion = '01'
117 117 compression = None
118 118 if 'generaldelta' in self.repo.requirements:
119 119 btype = 'HG20'
120 120 cgversion = '02'
121 121 compression = 'BZ'
122 122
123 123 cg = changegroup.changegroupsubset(self.repo, bases, [node], 'shelve',
124 124 version=cgversion)
125 125 changegroup.writebundle(self.ui, cg, self.fname, btype, self.vfs,
126 126 compression=compression)
127 127
128 128 class shelvedstate(object):
129 129 """Handle persistence during unshelving operations.
130 130
131 131 Handles saving and restoring a shelved state. Ensures that different
132 132 versions of a shelved state are possible and handles them appropriately.
133 133 """
134 134 _version = 1
135 135 _filename = 'shelvedstate'
136 136
137 137 @classmethod
138 138 def load(cls, repo):
139 139 fp = repo.vfs(cls._filename)
140 140 try:
141 141 version = int(fp.readline().strip())
142 142
143 143 if version != cls._version:
144 144 raise error.Abort(_('this version of shelve is incompatible '
145 145 'with the version used in this repo'))
146 146 name = fp.readline().strip()
147 147 wctx = fp.readline().strip()
148 148 pendingctx = fp.readline().strip()
149 149 parents = [bin(h) for h in fp.readline().split()]
150 150 stripnodes = [bin(h) for h in fp.readline().split()]
151 151 finally:
152 152 fp.close()
153 153
154 154 obj = cls()
155 155 obj.name = name
156 156 obj.wctx = repo[bin(wctx)]
157 157 obj.pendingctx = repo[bin(pendingctx)]
158 158 obj.parents = parents
159 159 obj.stripnodes = stripnodes
160 160
161 161 return obj
162 162
163 163 @classmethod
164 164 def save(cls, repo, name, originalwctx, pendingctx, stripnodes):
165 165 fp = repo.vfs(cls._filename, 'wb')
166 166 fp.write('%i\n' % cls._version)
167 167 fp.write('%s\n' % name)
168 168 fp.write('%s\n' % hex(originalwctx.node()))
169 169 fp.write('%s\n' % hex(pendingctx.node()))
170 170 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
171 171 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
172 172 fp.close()
173 173
174 174 @classmethod
175 175 def clear(cls, repo):
176 176 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
177 177
178 178 def cleanupoldbackups(repo):
179 179 vfs = scmutil.vfs(repo.join(backupdir))
180 180 maxbackups = repo.ui.configint('shelve', 'maxbackups', 10)
181 181 hgfiles = [f for f in vfs.listdir() if f.endswith('.hg')]
182 182 hgfiles = sorted([(vfs.stat(f).st_mtime, f) for f in hgfiles])
183 183 if 0 < maxbackups and maxbackups < len(hgfiles):
184 184 bordermtime = hgfiles[-maxbackups][0]
185 185 else:
186 186 bordermtime = None
187 187 for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
188 188 if mtime == bordermtime:
189 189 # keep it, because timestamp can't decide exact order of backups
190 190 continue
191 191 base = f[:-3]
192 192 for ext in 'hg patch'.split():
193 193 try:
194 194 vfs.unlink(base + '.' + ext)
195 195 except OSError as err:
196 196 if err.errno != errno.ENOENT:
197 197 raise
198 198
199 199 def _aborttransaction(repo):
200 200 '''Abort current transaction for shelve/unshelve, but keep dirstate
201 201 '''
202 202 backupname = 'dirstate.shelve'
203 203 dirstatebackup = None
204 204 try:
205 205 # create backup of (un)shelved dirstate, because aborting transaction
206 206 # should restore dirstate to one at the beginning of the
207 207 # transaction, which doesn't include the result of (un)shelving
208 208 fp = repo.vfs.open(backupname, "w")
209 209 dirstatebackup = backupname
210 210 # clearing _dirty/_dirtypl of dirstate by _writedirstate below
211 211 # is unintentional. but it doesn't cause problem in this case,
212 212 # because no code path refers them until transaction is aborted.
213 213 repo.dirstate._writedirstate(fp) # write in-memory changes forcibly
214 214
215 215 tr = repo.currenttransaction()
216 216 tr.abort()
217 217
218 218 # restore to backuped dirstate
219 219 repo.vfs.rename(dirstatebackup, 'dirstate')
220 220 dirstatebackup = None
221 221 finally:
222 222 if dirstatebackup:
223 223 repo.vfs.unlink(dirstatebackup)
224 224
225 225 def createcmd(ui, repo, pats, opts):
226 226 """subcommand that creates a new shelve"""
227 227 wlock = repo.wlock()
228 228 try:
229 229 cmdutil.checkunfinished(repo)
230 230 return _docreatecmd(ui, repo, pats, opts)
231 231 finally:
232 232 lockmod.release(wlock)
233 233
234 234 def _docreatecmd(ui, repo, pats, opts):
235 235 def mutableancestors(ctx):
236 236 """return all mutable ancestors for ctx (included)
237 237
238 238 Much faster than the revset ancestors(ctx) & draft()"""
239 239 seen = set([nullrev])
240 240 visit = collections.deque()
241 241 visit.append(ctx)
242 242 while visit:
243 243 ctx = visit.popleft()
244 244 yield ctx.node()
245 245 for parent in ctx.parents():
246 246 rev = parent.rev()
247 247 if rev not in seen:
248 248 seen.add(rev)
249 249 if parent.mutable():
250 250 visit.append(parent)
251 251
252 252 wctx = repo[None]
253 253 parents = wctx.parents()
254 254 if len(parents) > 1:
255 255 raise error.Abort(_('cannot shelve while merging'))
256 256 parent = parents[0]
257 257
258 258 # we never need the user, so we use a generic user for all shelve operations
259 259 user = 'shelve@localhost'
260 260 label = repo._activebookmark or parent.branch() or 'default'
261 261
262 262 # slashes aren't allowed in filenames, therefore we rename it
263 263 label = label.replace('/', '_')
264 264
265 265 def gennames():
266 266 yield label
267 267 for i in xrange(1, 100):
268 268 yield '%s-%02d' % (label, i)
269 269
270 270 def commitfunc(ui, repo, message, match, opts):
271 271 hasmq = util.safehasattr(repo, 'mq')
272 272 if hasmq:
273 273 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
274 274 backup = repo.ui.backupconfig('phases', 'new-commit')
275 275 try:
276 276 repo.ui. setconfig('phases', 'new-commit', phases.secret)
277 277 editor = cmdutil.getcommiteditor(editform='shelve.shelve', **opts)
278 278 return repo.commit(message, user, opts.get('date'), match,
279 279 editor=editor)
280 280 finally:
281 281 repo.ui.restoreconfig(backup)
282 282 if hasmq:
283 283 repo.mq.checkapplied = saved
284 284
285 285 if parent.node() != nullid:
286 286 desc = "changes to: %s" % parent.description().split('\n', 1)[0]
287 287 else:
288 288 desc = '(changes in empty repository)'
289 289
290 290 if not opts['message']:
291 291 opts['message'] = desc
292 292
293 293 name = opts['name']
294 294
295 295 lock = tr = None
296 296 try:
297 297 lock = repo.lock()
298 298
299 299 # use an uncommitted transaction to generate the bundle to avoid
300 300 # pull races. ensure we don't print the abort message to stderr.
301 301 tr = repo.transaction('commit', report=lambda x: None)
302 302
303 303 if name:
304 304 if shelvedfile(repo, name, 'hg').exists():
305 305 raise error.Abort(_("a shelved change named '%s' already exists"
306 306 ) % name)
307 307 else:
308 308 for n in gennames():
309 309 if not shelvedfile(repo, n, 'hg').exists():
310 310 name = n
311 311 break
312 312 else:
313 313 raise error.Abort(_("too many shelved changes named '%s'") %
314 314 label)
315 315
316 316 # ensure we are not creating a subdirectory or a hidden file
317 317 if '/' in name or '\\' in name:
318 318 raise error.Abort(_('shelved change names may not contain slashes'))
319 319 if name.startswith('.'):
320 320 raise error.Abort(_("shelved change names may not start with '.'"))
321 321 interactive = opts.get('interactive', False)
322 322
323 323 def interactivecommitfunc(ui, repo, *pats, **opts):
324 324 match = scmutil.match(repo['.'], pats, {})
325 325 message = opts['message']
326 326 return commitfunc(ui, repo, message, match, opts)
327 327 if not interactive:
328 328 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
329 329 else:
330 330 node = cmdutil.dorecord(ui, repo, interactivecommitfunc, None,
331 331 False, cmdutil.recordfilter, *pats, **opts)
332 332 if not node:
333 333 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
334 334 if stat.deleted:
335 335 ui.status(_("nothing changed (%d missing files, see "
336 336 "'hg status')\n") % len(stat.deleted))
337 337 else:
338 338 ui.status(_("nothing changed\n"))
339 339 return 1
340 340
341 341 bases = list(mutableancestors(repo[node]))
342 342 shelvedfile(repo, name, 'hg').writebundle(bases, node)
343 343 cmdutil.export(repo, [node],
344 344 fp=shelvedfile(repo, name, 'patch').opener('wb'),
345 345 opts=mdiff.diffopts(git=True))
346 346
347 347
348 348 if ui.formatted():
349 349 desc = util.ellipsis(desc, ui.termwidth())
350 350 ui.status(_('shelved as %s\n') % name)
351 351 hg.update(repo, parent.node())
352 352
353 353 _aborttransaction(repo)
354 354 finally:
355 355 lockmod.release(tr, lock)
356 356
357 357 def cleanupcmd(ui, repo):
358 358 """subcommand that deletes all shelves"""
359 359
360 360 wlock = None
361 361 try:
362 362 wlock = repo.wlock()
363 363 for (name, _type) in repo.vfs.readdir('shelved'):
364 364 suffix = name.rsplit('.', 1)[-1]
365 365 if suffix in ('hg', 'patch'):
366 366 shelvedfile(repo, name).movetobackup()
367 367 cleanupoldbackups(repo)
368 368 finally:
369 369 lockmod.release(wlock)
370 370
371 371 def deletecmd(ui, repo, pats):
372 372 """subcommand that deletes a specific shelve"""
373 373 if not pats:
374 374 raise error.Abort(_('no shelved changes specified!'))
375 375 wlock = repo.wlock()
376 376 try:
377 377 for name in pats:
378 378 for suffix in 'hg patch'.split():
379 379 shelvedfile(repo, name, suffix).movetobackup()
380 380 cleanupoldbackups(repo)
381 381 except OSError as err:
382 382 if err.errno != errno.ENOENT:
383 383 raise
384 384 raise error.Abort(_("shelved change '%s' not found") % name)
385 385 finally:
386 386 lockmod.release(wlock)
387 387
388 388 def listshelves(repo):
389 389 """return all shelves in repo as list of (time, filename)"""
390 390 try:
391 391 names = repo.vfs.readdir('shelved')
392 392 except OSError as err:
393 393 if err.errno != errno.ENOENT:
394 394 raise
395 395 return []
396 396 info = []
397 397 for (name, _type) in names:
398 398 pfx, sfx = name.rsplit('.', 1)
399 399 if not pfx or sfx != 'patch':
400 400 continue
401 401 st = shelvedfile(repo, name).stat()
402 402 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
403 403 return sorted(info, reverse=True)
404 404
405 405 def listcmd(ui, repo, pats, opts):
406 406 """subcommand that displays the list of shelves"""
407 407 pats = set(pats)
408 408 width = 80
409 409 if not ui.plain():
410 410 width = ui.termwidth()
411 411 namelabel = 'shelve.newest'
412 412 for mtime, name in listshelves(repo):
413 413 sname = util.split(name)[1]
414 414 if pats and sname not in pats:
415 415 continue
416 416 ui.write(sname, label=namelabel)
417 417 namelabel = 'shelve.name'
418 418 if ui.quiet:
419 419 ui.write('\n')
420 420 continue
421 421 ui.write(' ' * (16 - len(sname)))
422 422 used = 16
423 423 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
424 424 ui.write(age, label='shelve.age')
425 425 ui.write(' ' * (12 - len(age)))
426 426 used += 12
427 427 fp = open(name + '.patch', 'rb')
428 428 try:
429 429 while True:
430 430 line = fp.readline()
431 431 if not line:
432 432 break
433 433 if not line.startswith('#'):
434 434 desc = line.rstrip()
435 435 if ui.formatted():
436 436 desc = util.ellipsis(desc, width - used)
437 437 ui.write(desc)
438 438 break
439 439 ui.write('\n')
440 440 if not (opts['patch'] or opts['stat']):
441 441 continue
442 442 difflines = fp.readlines()
443 443 if opts['patch']:
444 444 for chunk, label in patch.difflabel(iter, difflines):
445 445 ui.write(chunk, label=label)
446 446 if opts['stat']:
447 447 for chunk, label in patch.diffstatui(difflines, width=width,
448 448 git=True):
449 449 ui.write(chunk, label=label)
450 450 finally:
451 451 fp.close()
452 452
453 453 def singlepatchcmds(ui, repo, pats, opts, subcommand):
454 454 """subcommand that displays a single shelf"""
455 455 if len(pats) != 1:
456 456 raise error.Abort(_("--%s expects a single shelf") % subcommand)
457 457 shelfname = pats[0]
458 458
459 459 if not shelvedfile(repo, shelfname, 'patch').exists():
460 460 raise error.Abort(_("cannot find shelf %s") % shelfname)
461 461
462 462 listcmd(ui, repo, pats, opts)
463 463
464 464 def checkparents(repo, state):
465 465 """check parent while resuming an unshelve"""
466 466 if state.parents != repo.dirstate.parents():
467 467 raise error.Abort(_('working directory parents do not match unshelve '
468 468 'state'))
469 469
470 470 def pathtofiles(repo, files):
471 471 cwd = repo.getcwd()
472 472 return [repo.pathto(f, cwd) for f in files]
473 473
474 474 def unshelveabort(ui, repo, state, opts):
475 475 """subcommand that abort an in-progress unshelve"""
476 476 lock = None
477 477 try:
478 478 checkparents(repo, state)
479 479
480 480 util.rename(repo.join('unshelverebasestate'),
481 481 repo.join('rebasestate'))
482 482 try:
483 483 rebase.rebase(ui, repo, **{
484 484 'abort' : True
485 485 })
486 486 except Exception:
487 487 util.rename(repo.join('rebasestate'),
488 488 repo.join('unshelverebasestate'))
489 489 raise
490 490
491 491 lock = repo.lock()
492 492
493 493 mergefiles(ui, repo, state.wctx, state.pendingctx)
494 494
495 495 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
496 496 finally:
497 497 shelvedstate.clear(repo)
498 498 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
499 499 lockmod.release(lock)
500 500
501 501 def mergefiles(ui, repo, wctx, shelvectx):
502 502 """updates to wctx and merges the changes from shelvectx into the
503 503 dirstate."""
504 504 oldquiet = ui.quiet
505 505 try:
506 506 ui.quiet = True
507 507 hg.update(repo, wctx.node())
508 508 files = []
509 509 files.extend(shelvectx.files())
510 510 files.extend(shelvectx.parents()[0].files())
511 511
512 512 # revert will overwrite unknown files, so move them out of the way
513 513 for file in repo.status(unknown=True).unknown:
514 514 if file in files:
515 util.rename(file, cmdutil.origpath(ui, repo, file))
515 util.rename(file, scmutil.origpath(ui, repo, file))
516 516 ui.pushbuffer(True)
517 517 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
518 518 *pathtofiles(repo, files),
519 519 **{'no_backup': True})
520 520 ui.popbuffer()
521 521 finally:
522 522 ui.quiet = oldquiet
523 523
524 524 def unshelvecleanup(ui, repo, name, opts):
525 525 """remove related files after an unshelve"""
526 526 if not opts['keep']:
527 527 for filetype in 'hg patch'.split():
528 528 shelvedfile(repo, name, filetype).movetobackup()
529 529 cleanupoldbackups(repo)
530 530
531 531 def unshelvecontinue(ui, repo, state, opts):
532 532 """subcommand to continue an in-progress unshelve"""
533 533 # We're finishing off a merge. First parent is our original
534 534 # parent, second is the temporary "fake" commit we're unshelving.
535 535 lock = None
536 536 try:
537 537 checkparents(repo, state)
538 538 ms = merge.mergestate.read(repo)
539 539 if [f for f in ms if ms[f] == 'u']:
540 540 raise error.Abort(
541 541 _("unresolved conflicts, can't continue"),
542 542 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
543 543
544 544 lock = repo.lock()
545 545
546 546 util.rename(repo.join('unshelverebasestate'),
547 547 repo.join('rebasestate'))
548 548 try:
549 549 rebase.rebase(ui, repo, **{
550 550 'continue' : True
551 551 })
552 552 except Exception:
553 553 util.rename(repo.join('rebasestate'),
554 554 repo.join('unshelverebasestate'))
555 555 raise
556 556
557 557 shelvectx = repo['tip']
558 558 if not shelvectx in state.pendingctx.children():
559 559 # rebase was a no-op, so it produced no child commit
560 560 shelvectx = state.pendingctx
561 561 else:
562 562 # only strip the shelvectx if the rebase produced it
563 563 state.stripnodes.append(shelvectx.node())
564 564
565 565 mergefiles(ui, repo, state.wctx, shelvectx)
566 566
567 567 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
568 568 shelvedstate.clear(repo)
569 569 unshelvecleanup(ui, repo, state.name, opts)
570 570 ui.status(_("unshelve of '%s' complete\n") % state.name)
571 571 finally:
572 572 lockmod.release(lock)
573 573
574 574 @command('unshelve',
575 575 [('a', 'abort', None,
576 576 _('abort an incomplete unshelve operation')),
577 577 ('c', 'continue', None,
578 578 _('continue an incomplete unshelve operation')),
579 579 ('k', 'keep', None,
580 580 _('keep shelve after unshelving')),
581 581 ('t', 'tool', '', _('specify merge tool')),
582 582 ('', 'date', '',
583 583 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
584 584 _('hg unshelve [SHELVED]'))
585 585 def unshelve(ui, repo, *shelved, **opts):
586 586 """restore a shelved change to the working directory
587 587
588 588 This command accepts an optional name of a shelved change to
589 589 restore. If none is given, the most recent shelved change is used.
590 590
591 591 If a shelved change is applied successfully, the bundle that
592 592 contains the shelved changes is moved to a backup location
593 593 (.hg/shelve-backup).
594 594
595 595 Since you can restore a shelved change on top of an arbitrary
596 596 commit, it is possible that unshelving will result in a conflict
597 597 between your changes and the commits you are unshelving onto. If
598 598 this occurs, you must resolve the conflict, then use
599 599 ``--continue`` to complete the unshelve operation. (The bundle
600 600 will not be moved until you successfully complete the unshelve.)
601 601
602 602 (Alternatively, you can use ``--abort`` to abandon an unshelve
603 603 that causes a conflict. This reverts the unshelved changes, and
604 604 leaves the bundle in place.)
605 605
606 606 After a successful unshelve, the shelved changes are stored in a
607 607 backup directory. Only the N most recent backups are kept. N
608 608 defaults to 10 but can be overridden using the ``shelve.maxbackups``
609 609 configuration option.
610 610
611 611 .. container:: verbose
612 612
613 613 Timestamp in seconds is used to decide order of backups. More
614 614 than ``maxbackups`` backups are kept, if same timestamp
615 615 prevents from deciding exact order of them, for safety.
616 616 """
617 617 wlock = repo.wlock()
618 618 try:
619 619 return _dounshelve(ui, repo, *shelved, **opts)
620 620 finally:
621 621 lockmod.release(wlock)
622 622
623 623 def _dounshelve(ui, repo, *shelved, **opts):
624 624 abortf = opts['abort']
625 625 continuef = opts['continue']
626 626 if not abortf and not continuef:
627 627 cmdutil.checkunfinished(repo)
628 628
629 629 if abortf or continuef:
630 630 if abortf and continuef:
631 631 raise error.Abort(_('cannot use both abort and continue'))
632 632 if shelved:
633 633 raise error.Abort(_('cannot combine abort/continue with '
634 634 'naming a shelved change'))
635 635 if abortf and opts.get('tool', False):
636 636 ui.warn(_('tool option will be ignored\n'))
637 637
638 638 try:
639 639 state = shelvedstate.load(repo)
640 640 except IOError as err:
641 641 if err.errno != errno.ENOENT:
642 642 raise
643 643 raise error.Abort(_('no unshelve operation underway'))
644 644
645 645 if abortf:
646 646 return unshelveabort(ui, repo, state, opts)
647 647 elif continuef:
648 648 return unshelvecontinue(ui, repo, state, opts)
649 649 elif len(shelved) > 1:
650 650 raise error.Abort(_('can only unshelve one change at a time'))
651 651 elif not shelved:
652 652 shelved = listshelves(repo)
653 653 if not shelved:
654 654 raise error.Abort(_('no shelved changes to apply!'))
655 655 basename = util.split(shelved[0][1])[1]
656 656 ui.status(_("unshelving change '%s'\n") % basename)
657 657 else:
658 658 basename = shelved[0]
659 659
660 660 if not shelvedfile(repo, basename, 'patch').exists():
661 661 raise error.Abort(_("shelved change '%s' not found") % basename)
662 662
663 663 oldquiet = ui.quiet
664 664 lock = tr = None
665 665 forcemerge = ui.backupconfig('ui', 'forcemerge')
666 666 try:
667 667 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'unshelve')
668 668 lock = repo.lock()
669 669
670 670 tr = repo.transaction('unshelve', report=lambda x: None)
671 671 oldtiprev = len(repo)
672 672
673 673 pctx = repo['.']
674 674 tmpwctx = pctx
675 675 # The goal is to have a commit structure like so:
676 676 # ...-> pctx -> tmpwctx -> shelvectx
677 677 # where tmpwctx is an optional commit with the user's pending changes
678 678 # and shelvectx is the unshelved changes. Then we merge it all down
679 679 # to the original pctx.
680 680
681 681 # Store pending changes in a commit
682 682 s = repo.status()
683 683 if s.modified or s.added or s.removed or s.deleted:
684 684 ui.status(_("temporarily committing pending changes "
685 685 "(restore with 'hg unshelve --abort')\n"))
686 686 def commitfunc(ui, repo, message, match, opts):
687 687 hasmq = util.safehasattr(repo, 'mq')
688 688 if hasmq:
689 689 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
690 690
691 691 backup = repo.ui.backupconfig('phases', 'new-commit')
692 692 try:
693 693 repo.ui.setconfig('phases', 'new-commit', phases.secret)
694 694 return repo.commit(message, 'shelve@localhost',
695 695 opts.get('date'), match)
696 696 finally:
697 697 repo.ui.restoreconfig(backup)
698 698 if hasmq:
699 699 repo.mq.checkapplied = saved
700 700
701 701 tempopts = {}
702 702 tempopts['message'] = "pending changes temporary commit"
703 703 tempopts['date'] = opts.get('date')
704 704 ui.quiet = True
705 705 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
706 706 tmpwctx = repo[node]
707 707
708 708 ui.quiet = True
709 709 shelvedfile(repo, basename, 'hg').applybundle()
710 710
711 711 ui.quiet = oldquiet
712 712
713 713 shelvectx = repo['tip']
714 714
715 715 # If the shelve is not immediately on top of the commit
716 716 # we'll be merging with, rebase it to be on top.
717 717 if tmpwctx.node() != shelvectx.parents()[0].node():
718 718 ui.status(_('rebasing shelved changes\n'))
719 719 try:
720 720 rebase.rebase(ui, repo, **{
721 721 'rev' : [shelvectx.rev()],
722 722 'dest' : str(tmpwctx.rev()),
723 723 'keep' : True,
724 724 'tool' : opts.get('tool', ''),
725 725 })
726 726 except error.InterventionRequired:
727 727 tr.close()
728 728
729 729 stripnodes = [repo.changelog.node(rev)
730 730 for rev in xrange(oldtiprev, len(repo))]
731 731 shelvedstate.save(repo, basename, pctx, tmpwctx, stripnodes)
732 732
733 733 util.rename(repo.join('rebasestate'),
734 734 repo.join('unshelverebasestate'))
735 735 raise error.InterventionRequired(
736 736 _("unresolved conflicts (see 'hg resolve', then "
737 737 "'hg unshelve --continue')"))
738 738
739 739 # refresh ctx after rebase completes
740 740 shelvectx = repo['tip']
741 741
742 742 if not shelvectx in tmpwctx.children():
743 743 # rebase was a no-op, so it produced no child commit
744 744 shelvectx = tmpwctx
745 745
746 746 mergefiles(ui, repo, pctx, shelvectx)
747 747 shelvedstate.clear(repo)
748 748
749 749 # The transaction aborting will strip all the commits for us,
750 750 # but it doesn't update the inmemory structures, so addchangegroup
751 751 # hooks still fire and try to operate on the missing commits.
752 752 # Clean up manually to prevent this.
753 753 repo.unfiltered().changelog.strip(oldtiprev, tr)
754 754
755 755 unshelvecleanup(ui, repo, basename, opts)
756 756
757 757 _aborttransaction(repo)
758 758 finally:
759 759 ui.quiet = oldquiet
760 760 if tr:
761 761 tr.release()
762 762 lockmod.release(lock)
763 763 ui.restoreconfig(forcemerge)
764 764
765 765 @command('shelve',
766 766 [('A', 'addremove', None,
767 767 _('mark new/missing files as added/removed before shelving')),
768 768 ('', 'cleanup', None,
769 769 _('delete all shelved changes')),
770 770 ('', 'date', '',
771 771 _('shelve with the specified commit date'), _('DATE')),
772 772 ('d', 'delete', None,
773 773 _('delete the named shelved change(s)')),
774 774 ('e', 'edit', False,
775 775 _('invoke editor on commit messages')),
776 776 ('l', 'list', None,
777 777 _('list current shelves')),
778 778 ('m', 'message', '',
779 779 _('use text as shelve message'), _('TEXT')),
780 780 ('n', 'name', '',
781 781 _('use the given name for the shelved commit'), _('NAME')),
782 782 ('p', 'patch', None,
783 783 _('show patch')),
784 784 ('i', 'interactive', None,
785 785 _('interactive mode, only works while creating a shelve')),
786 786 ('', 'stat', None,
787 787 _('output diffstat-style summary of changes'))] + commands.walkopts,
788 788 _('hg shelve [OPTION]... [FILE]...'))
789 789 def shelvecmd(ui, repo, *pats, **opts):
790 790 '''save and set aside changes from the working directory
791 791
792 792 Shelving takes files that "hg status" reports as not clean, saves
793 793 the modifications to a bundle (a shelved change), and reverts the
794 794 files so that their state in the working directory becomes clean.
795 795
796 796 To restore these changes to the working directory, using "hg
797 797 unshelve"; this will work even if you switch to a different
798 798 commit.
799 799
800 800 When no files are specified, "hg shelve" saves all not-clean
801 801 files. If specific files or directories are named, only changes to
802 802 those files are shelved.
803 803
804 804 Each shelved change has a name that makes it easier to find later.
805 805 The name of a shelved change defaults to being based on the active
806 806 bookmark, or if there is no active bookmark, the current named
807 807 branch. To specify a different name, use ``--name``.
808 808
809 809 To see a list of existing shelved changes, use the ``--list``
810 810 option. For each shelved change, this will print its name, age,
811 811 and description; use ``--patch`` or ``--stat`` for more details.
812 812
813 813 To delete specific shelved changes, use ``--delete``. To delete
814 814 all shelved changes, use ``--cleanup``.
815 815 '''
816 816 allowables = [
817 817 ('addremove', set(['create'])), # 'create' is pseudo action
818 818 ('cleanup', set(['cleanup'])),
819 819 # ('date', set(['create'])), # ignored for passing '--date "0 0"' in tests
820 820 ('delete', set(['delete'])),
821 821 ('edit', set(['create'])),
822 822 ('list', set(['list'])),
823 823 ('message', set(['create'])),
824 824 ('name', set(['create'])),
825 825 ('patch', set(['patch', 'list'])),
826 826 ('stat', set(['stat', 'list'])),
827 827 ]
828 828 def checkopt(opt):
829 829 if opts[opt]:
830 830 for i, allowable in allowables:
831 831 if opts[i] and opt not in allowable:
832 832 raise error.Abort(_("options '--%s' and '--%s' may not be "
833 833 "used together") % (opt, i))
834 834 return True
835 835 if checkopt('cleanup'):
836 836 if pats:
837 837 raise error.Abort(_("cannot specify names when using '--cleanup'"))
838 838 return cleanupcmd(ui, repo)
839 839 elif checkopt('delete'):
840 840 return deletecmd(ui, repo, pats)
841 841 elif checkopt('list'):
842 842 return listcmd(ui, repo, pats, opts)
843 843 elif checkopt('patch'):
844 844 return singlepatchcmds(ui, repo, pats, opts, subcommand='patch')
845 845 elif checkopt('stat'):
846 846 return singlepatchcmds(ui, repo, pats, opts, subcommand='stat')
847 847 else:
848 848 return createcmd(ui, repo, pats, opts)
849 849
850 850 def extsetup(ui):
851 851 cmdutil.unfinishedstates.append(
852 852 [shelvedstate._filename, False, False,
853 853 _('unshelve already in progress'),
854 854 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
@@ -1,3431 +1,3411
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import hex, bin, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile, cStringIO
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import repair, graphmod, revset, phases, obsolete, pathutil
14 14 import changelog
15 15 import bookmarks
16 16 import encoding
17 17 import formatter
18 18 import crecord as crecordmod
19 19 import lock as lockmod
20 20
21 21 def ishunk(x):
22 22 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
23 23 return isinstance(x, hunkclasses)
24 24
25 25 def newandmodified(chunks, originalchunks):
26 26 newlyaddedandmodifiedfiles = set()
27 27 for chunk in chunks:
28 28 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
29 29 originalchunks:
30 30 newlyaddedandmodifiedfiles.add(chunk.header.filename())
31 31 return newlyaddedandmodifiedfiles
32 32
33 33 def parsealiases(cmd):
34 34 return cmd.lstrip("^").split("|")
35 35
36 36 def setupwrapcolorwrite(ui):
37 37 # wrap ui.write so diff output can be labeled/colorized
38 38 def wrapwrite(orig, *args, **kw):
39 39 label = kw.pop('label', '')
40 40 for chunk, l in patch.difflabel(lambda: args):
41 41 orig(chunk, label=label + l)
42 42
43 43 oldwrite = ui.write
44 44 def wrap(*args, **kwargs):
45 45 return wrapwrite(oldwrite, *args, **kwargs)
46 46 setattr(ui, 'write', wrap)
47 47 return oldwrite
48 48
49 49 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
50 50 if usecurses:
51 51 if testfile:
52 52 recordfn = crecordmod.testdecorator(testfile,
53 53 crecordmod.testchunkselector)
54 54 else:
55 55 recordfn = crecordmod.chunkselector
56 56
57 57 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
58 58
59 59 else:
60 60 return patch.filterpatch(ui, originalhunks, operation)
61 61
62 62 def recordfilter(ui, originalhunks, operation=None):
63 63 """ Prompts the user to filter the originalhunks and return a list of
64 64 selected hunks.
65 65 *operation* is used for ui purposes to indicate the user
66 66 what kind of filtering they are doing: reverting, committing, shelving, etc.
67 67 *operation* has to be a translated string.
68 68 """
69 69 usecurses = crecordmod.checkcurses(ui)
70 70 testfile = ui.config('experimental', 'crecordtest', None)
71 71 oldwrite = setupwrapcolorwrite(ui)
72 72 try:
73 73 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
74 74 testfile, operation)
75 75 finally:
76 76 ui.write = oldwrite
77 77 return newchunks, newopts
78 78
79 79 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
80 80 filterfn, *pats, **opts):
81 81 import merge as mergemod
82 82
83 83 if not ui.interactive():
84 84 if cmdsuggest:
85 85 msg = _('running non-interactively, use %s instead') % cmdsuggest
86 86 else:
87 87 msg = _('running non-interactively')
88 88 raise error.Abort(msg)
89 89
90 90 # make sure username is set before going interactive
91 91 if not opts.get('user'):
92 92 ui.username() # raise exception, username not provided
93 93
94 94 def recordfunc(ui, repo, message, match, opts):
95 95 """This is generic record driver.
96 96
97 97 Its job is to interactively filter local changes, and
98 98 accordingly prepare working directory into a state in which the
99 99 job can be delegated to a non-interactive commit command such as
100 100 'commit' or 'qrefresh'.
101 101
102 102 After the actual job is done by non-interactive command, the
103 103 working directory is restored to its original state.
104 104
105 105 In the end we'll record interesting changes, and everything else
106 106 will be left in place, so the user can continue working.
107 107 """
108 108
109 109 checkunfinished(repo, commit=True)
110 110 merge = len(repo[None].parents()) > 1
111 111 if merge:
112 112 raise error.Abort(_('cannot partially commit a merge '
113 113 '(use "hg commit" instead)'))
114 114
115 115 status = repo.status(match=match)
116 116 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
117 117 diffopts.nodates = True
118 118 diffopts.git = True
119 119 diffopts.showfunc = True
120 120 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
121 121 originalchunks = patch.parsepatch(originaldiff)
122 122
123 123 # 1. filter patch, so we have intending-to apply subset of it
124 124 try:
125 125 chunks, newopts = filterfn(ui, originalchunks)
126 126 except patch.PatchError as err:
127 127 raise error.Abort(_('error parsing patch: %s') % err)
128 128 opts.update(newopts)
129 129
130 130 # We need to keep a backup of files that have been newly added and
131 131 # modified during the recording process because there is a previous
132 132 # version without the edit in the workdir
133 133 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
134 134 contenders = set()
135 135 for h in chunks:
136 136 try:
137 137 contenders.update(set(h.files()))
138 138 except AttributeError:
139 139 pass
140 140
141 141 changed = status.modified + status.added + status.removed
142 142 newfiles = [f for f in changed if f in contenders]
143 143 if not newfiles:
144 144 ui.status(_('no changes to record\n'))
145 145 return 0
146 146
147 147 modified = set(status.modified)
148 148
149 149 # 2. backup changed files, so we can restore them in the end
150 150
151 151 if backupall:
152 152 tobackup = changed
153 153 else:
154 154 tobackup = [f for f in newfiles if f in modified or f in \
155 155 newlyaddedandmodifiedfiles]
156 156 backups = {}
157 157 if tobackup:
158 158 backupdir = repo.join('record-backups')
159 159 try:
160 160 os.mkdir(backupdir)
161 161 except OSError as err:
162 162 if err.errno != errno.EEXIST:
163 163 raise
164 164 try:
165 165 # backup continues
166 166 for f in tobackup:
167 167 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
168 168 dir=backupdir)
169 169 os.close(fd)
170 170 ui.debug('backup %r as %r\n' % (f, tmpname))
171 171 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
172 172 backups[f] = tmpname
173 173
174 174 fp = cStringIO.StringIO()
175 175 for c in chunks:
176 176 fname = c.filename()
177 177 if fname in backups:
178 178 c.write(fp)
179 179 dopatch = fp.tell()
180 180 fp.seek(0)
181 181
182 182 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
183 183 # 3a. apply filtered patch to clean repo (clean)
184 184 if backups:
185 185 # Equivalent to hg.revert
186 186 m = scmutil.matchfiles(repo, backups.keys())
187 187 mergemod.update(repo, repo.dirstate.p1(),
188 188 False, True, matcher=m)
189 189
190 190 # 3b. (apply)
191 191 if dopatch:
192 192 try:
193 193 ui.debug('applying patch\n')
194 194 ui.debug(fp.getvalue())
195 195 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
196 196 except patch.PatchError as err:
197 197 raise error.Abort(str(err))
198 198 del fp
199 199
200 200 # 4. We prepared working directory according to filtered
201 201 # patch. Now is the time to delegate the job to
202 202 # commit/qrefresh or the like!
203 203
204 204 # Make all of the pathnames absolute.
205 205 newfiles = [repo.wjoin(nf) for nf in newfiles]
206 206 return commitfunc(ui, repo, *newfiles, **opts)
207 207 finally:
208 208 # 5. finally restore backed-up files
209 209 try:
210 210 dirstate = repo.dirstate
211 211 for realname, tmpname in backups.iteritems():
212 212 ui.debug('restoring %r to %r\n' % (tmpname, realname))
213 213
214 214 if dirstate[realname] == 'n':
215 215 # without normallookup, restoring timestamp
216 216 # may cause partially committed files
217 217 # to be treated as unmodified
218 218 dirstate.normallookup(realname)
219 219
220 220 # copystat=True here and above are a hack to trick any
221 221 # editors that have f open that we haven't modified them.
222 222 #
223 223 # Also note that this racy as an editor could notice the
224 224 # file's mtime before we've finished writing it.
225 225 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
226 226 os.unlink(tmpname)
227 227 if tobackup:
228 228 os.rmdir(backupdir)
229 229 except OSError:
230 230 pass
231 231
232 232 def recordinwlock(ui, repo, message, match, opts):
233 233 wlock = repo.wlock()
234 234 try:
235 235 return recordfunc(ui, repo, message, match, opts)
236 236 finally:
237 237 wlock.release()
238 238
239 239 return commit(ui, repo, recordinwlock, pats, opts)
240 240
241 241 def findpossible(cmd, table, strict=False):
242 242 """
243 243 Return cmd -> (aliases, command table entry)
244 244 for each matching command.
245 245 Return debug commands (or their aliases) only if no normal command matches.
246 246 """
247 247 choice = {}
248 248 debugchoice = {}
249 249
250 250 if cmd in table:
251 251 # short-circuit exact matches, "log" alias beats "^log|history"
252 252 keys = [cmd]
253 253 else:
254 254 keys = table.keys()
255 255
256 256 allcmds = []
257 257 for e in keys:
258 258 aliases = parsealiases(e)
259 259 allcmds.extend(aliases)
260 260 found = None
261 261 if cmd in aliases:
262 262 found = cmd
263 263 elif not strict:
264 264 for a in aliases:
265 265 if a.startswith(cmd):
266 266 found = a
267 267 break
268 268 if found is not None:
269 269 if aliases[0].startswith("debug") or found.startswith("debug"):
270 270 debugchoice[found] = (aliases, table[e])
271 271 else:
272 272 choice[found] = (aliases, table[e])
273 273
274 274 if not choice and debugchoice:
275 275 choice = debugchoice
276 276
277 277 return choice, allcmds
278 278
279 279 def findcmd(cmd, table, strict=True):
280 280 """Return (aliases, command table entry) for command string."""
281 281 choice, allcmds = findpossible(cmd, table, strict)
282 282
283 283 if cmd in choice:
284 284 return choice[cmd]
285 285
286 286 if len(choice) > 1:
287 287 clist = choice.keys()
288 288 clist.sort()
289 289 raise error.AmbiguousCommand(cmd, clist)
290 290
291 291 if choice:
292 292 return choice.values()[0]
293 293
294 294 raise error.UnknownCommand(cmd, allcmds)
295 295
296 296 def findrepo(p):
297 297 while not os.path.isdir(os.path.join(p, ".hg")):
298 298 oldp, p = p, os.path.dirname(p)
299 299 if p == oldp:
300 300 return None
301 301
302 302 return p
303 303
304 304 def bailifchanged(repo, merge=True):
305 305 if merge and repo.dirstate.p2() != nullid:
306 306 raise error.Abort(_('outstanding uncommitted merge'))
307 307 modified, added, removed, deleted = repo.status()[:4]
308 308 if modified or added or removed or deleted:
309 309 raise error.Abort(_('uncommitted changes'))
310 310 ctx = repo[None]
311 311 for s in sorted(ctx.substate):
312 312 ctx.sub(s).bailifchanged()
313 313
314 314 def logmessage(ui, opts):
315 315 """ get the log message according to -m and -l option """
316 316 message = opts.get('message')
317 317 logfile = opts.get('logfile')
318 318
319 319 if message and logfile:
320 320 raise error.Abort(_('options --message and --logfile are mutually '
321 321 'exclusive'))
322 322 if not message and logfile:
323 323 try:
324 324 if logfile == '-':
325 325 message = ui.fin.read()
326 326 else:
327 327 message = '\n'.join(util.readfile(logfile).splitlines())
328 328 except IOError as inst:
329 329 raise error.Abort(_("can't read commit message '%s': %s") %
330 330 (logfile, inst.strerror))
331 331 return message
332 332
333 333 def mergeeditform(ctxorbool, baseformname):
334 334 """return appropriate editform name (referencing a committemplate)
335 335
336 336 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
337 337 merging is committed.
338 338
339 339 This returns baseformname with '.merge' appended if it is a merge,
340 340 otherwise '.normal' is appended.
341 341 """
342 342 if isinstance(ctxorbool, bool):
343 343 if ctxorbool:
344 344 return baseformname + ".merge"
345 345 elif 1 < len(ctxorbool.parents()):
346 346 return baseformname + ".merge"
347 347
348 348 return baseformname + ".normal"
349 349
350 350 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
351 351 editform='', **opts):
352 352 """get appropriate commit message editor according to '--edit' option
353 353
354 354 'finishdesc' is a function to be called with edited commit message
355 355 (= 'description' of the new changeset) just after editing, but
356 356 before checking empty-ness. It should return actual text to be
357 357 stored into history. This allows to change description before
358 358 storing.
359 359
360 360 'extramsg' is a extra message to be shown in the editor instead of
361 361 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
362 362 is automatically added.
363 363
364 364 'editform' is a dot-separated list of names, to distinguish
365 365 the purpose of commit text editing.
366 366
367 367 'getcommiteditor' returns 'commitforceeditor' regardless of
368 368 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
369 369 they are specific for usage in MQ.
370 370 """
371 371 if edit or finishdesc or extramsg:
372 372 return lambda r, c, s: commitforceeditor(r, c, s,
373 373 finishdesc=finishdesc,
374 374 extramsg=extramsg,
375 375 editform=editform)
376 376 elif editform:
377 377 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
378 378 else:
379 379 return commiteditor
380 380
381 381 def loglimit(opts):
382 382 """get the log limit according to option -l/--limit"""
383 383 limit = opts.get('limit')
384 384 if limit:
385 385 try:
386 386 limit = int(limit)
387 387 except ValueError:
388 388 raise error.Abort(_('limit must be a positive integer'))
389 389 if limit <= 0:
390 390 raise error.Abort(_('limit must be positive'))
391 391 else:
392 392 limit = None
393 393 return limit
394 394
395 395 def makefilename(repo, pat, node, desc=None,
396 396 total=None, seqno=None, revwidth=None, pathname=None):
397 397 node_expander = {
398 398 'H': lambda: hex(node),
399 399 'R': lambda: str(repo.changelog.rev(node)),
400 400 'h': lambda: short(node),
401 401 'm': lambda: re.sub('[^\w]', '_', str(desc))
402 402 }
403 403 expander = {
404 404 '%': lambda: '%',
405 405 'b': lambda: os.path.basename(repo.root),
406 406 }
407 407
408 408 try:
409 409 if node:
410 410 expander.update(node_expander)
411 411 if node:
412 412 expander['r'] = (lambda:
413 413 str(repo.changelog.rev(node)).zfill(revwidth or 0))
414 414 if total is not None:
415 415 expander['N'] = lambda: str(total)
416 416 if seqno is not None:
417 417 expander['n'] = lambda: str(seqno)
418 418 if total is not None and seqno is not None:
419 419 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
420 420 if pathname is not None:
421 421 expander['s'] = lambda: os.path.basename(pathname)
422 422 expander['d'] = lambda: os.path.dirname(pathname) or '.'
423 423 expander['p'] = lambda: pathname
424 424
425 425 newname = []
426 426 patlen = len(pat)
427 427 i = 0
428 428 while i < patlen:
429 429 c = pat[i]
430 430 if c == '%':
431 431 i += 1
432 432 c = pat[i]
433 433 c = expander[c]()
434 434 newname.append(c)
435 435 i += 1
436 436 return ''.join(newname)
437 437 except KeyError as inst:
438 438 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
439 439 inst.args[0])
440 440
441 441 class _unclosablefile(object):
442 442 def __init__(self, fp):
443 443 self._fp = fp
444 444
445 445 def close(self):
446 446 pass
447 447
448 448 def __iter__(self):
449 449 return iter(self._fp)
450 450
451 451 def __getattr__(self, attr):
452 452 return getattr(self._fp, attr)
453 453
454 454 def makefileobj(repo, pat, node=None, desc=None, total=None,
455 455 seqno=None, revwidth=None, mode='wb', modemap=None,
456 456 pathname=None):
457 457
458 458 writable = mode not in ('r', 'rb')
459 459
460 460 if not pat or pat == '-':
461 461 if writable:
462 462 fp = repo.ui.fout
463 463 else:
464 464 fp = repo.ui.fin
465 465 return _unclosablefile(fp)
466 466 if util.safehasattr(pat, 'write') and writable:
467 467 return pat
468 468 if util.safehasattr(pat, 'read') and 'r' in mode:
469 469 return pat
470 470 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
471 471 if modemap is not None:
472 472 mode = modemap.get(fn, mode)
473 473 if mode == 'wb':
474 474 modemap[fn] = 'ab'
475 475 return open(fn, mode)
476 476
477 477 def openrevlog(repo, cmd, file_, opts):
478 478 """opens the changelog, manifest, a filelog or a given revlog"""
479 479 cl = opts['changelog']
480 480 mf = opts['manifest']
481 481 dir = opts['dir']
482 482 msg = None
483 483 if cl and mf:
484 484 msg = _('cannot specify --changelog and --manifest at the same time')
485 485 elif cl and dir:
486 486 msg = _('cannot specify --changelog and --dir at the same time')
487 487 elif cl or mf:
488 488 if file_:
489 489 msg = _('cannot specify filename with --changelog or --manifest')
490 490 elif not repo:
491 491 msg = _('cannot specify --changelog or --manifest or --dir '
492 492 'without a repository')
493 493 if msg:
494 494 raise error.Abort(msg)
495 495
496 496 r = None
497 497 if repo:
498 498 if cl:
499 499 r = repo.unfiltered().changelog
500 500 elif dir:
501 501 if 'treemanifest' not in repo.requirements:
502 502 raise error.Abort(_("--dir can only be used on repos with "
503 503 "treemanifest enabled"))
504 504 dirlog = repo.dirlog(file_)
505 505 if len(dirlog):
506 506 r = dirlog
507 507 elif mf:
508 508 r = repo.manifest
509 509 elif file_:
510 510 filelog = repo.file(file_)
511 511 if len(filelog):
512 512 r = filelog
513 513 if not r:
514 514 if not file_:
515 515 raise error.CommandError(cmd, _('invalid arguments'))
516 516 if not os.path.isfile(file_):
517 517 raise error.Abort(_("revlog '%s' not found") % file_)
518 518 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
519 519 file_[:-2] + ".i")
520 520 return r
521 521
522 522 def copy(ui, repo, pats, opts, rename=False):
523 523 # called with the repo lock held
524 524 #
525 525 # hgsep => pathname that uses "/" to separate directories
526 526 # ossep => pathname that uses os.sep to separate directories
527 527 cwd = repo.getcwd()
528 528 targets = {}
529 529 after = opts.get("after")
530 530 dryrun = opts.get("dry_run")
531 531 wctx = repo[None]
532 532
533 533 def walkpat(pat):
534 534 srcs = []
535 535 if after:
536 536 badstates = '?'
537 537 else:
538 538 badstates = '?r'
539 539 m = scmutil.match(repo[None], [pat], opts, globbed=True)
540 540 for abs in repo.walk(m):
541 541 state = repo.dirstate[abs]
542 542 rel = m.rel(abs)
543 543 exact = m.exact(abs)
544 544 if state in badstates:
545 545 if exact and state == '?':
546 546 ui.warn(_('%s: not copying - file is not managed\n') % rel)
547 547 if exact and state == 'r':
548 548 ui.warn(_('%s: not copying - file has been marked for'
549 549 ' remove\n') % rel)
550 550 continue
551 551 # abs: hgsep
552 552 # rel: ossep
553 553 srcs.append((abs, rel, exact))
554 554 return srcs
555 555
556 556 # abssrc: hgsep
557 557 # relsrc: ossep
558 558 # otarget: ossep
559 559 def copyfile(abssrc, relsrc, otarget, exact):
560 560 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
561 561 if '/' in abstarget:
562 562 # We cannot normalize abstarget itself, this would prevent
563 563 # case only renames, like a => A.
564 564 abspath, absname = abstarget.rsplit('/', 1)
565 565 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
566 566 reltarget = repo.pathto(abstarget, cwd)
567 567 target = repo.wjoin(abstarget)
568 568 src = repo.wjoin(abssrc)
569 569 state = repo.dirstate[abstarget]
570 570
571 571 scmutil.checkportable(ui, abstarget)
572 572
573 573 # check for collisions
574 574 prevsrc = targets.get(abstarget)
575 575 if prevsrc is not None:
576 576 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
577 577 (reltarget, repo.pathto(abssrc, cwd),
578 578 repo.pathto(prevsrc, cwd)))
579 579 return
580 580
581 581 # check for overwrites
582 582 exists = os.path.lexists(target)
583 583 samefile = False
584 584 if exists and abssrc != abstarget:
585 585 if (repo.dirstate.normalize(abssrc) ==
586 586 repo.dirstate.normalize(abstarget)):
587 587 if not rename:
588 588 ui.warn(_("%s: can't copy - same file\n") % reltarget)
589 589 return
590 590 exists = False
591 591 samefile = True
592 592
593 593 if not after and exists or after and state in 'mn':
594 594 if not opts['force']:
595 595 ui.warn(_('%s: not overwriting - file exists\n') %
596 596 reltarget)
597 597 return
598 598
599 599 if after:
600 600 if not exists:
601 601 if rename:
602 602 ui.warn(_('%s: not recording move - %s does not exist\n') %
603 603 (relsrc, reltarget))
604 604 else:
605 605 ui.warn(_('%s: not recording copy - %s does not exist\n') %
606 606 (relsrc, reltarget))
607 607 return
608 608 elif not dryrun:
609 609 try:
610 610 if exists:
611 611 os.unlink(target)
612 612 targetdir = os.path.dirname(target) or '.'
613 613 if not os.path.isdir(targetdir):
614 614 os.makedirs(targetdir)
615 615 if samefile:
616 616 tmp = target + "~hgrename"
617 617 os.rename(src, tmp)
618 618 os.rename(tmp, target)
619 619 else:
620 620 util.copyfile(src, target)
621 621 srcexists = True
622 622 except IOError as inst:
623 623 if inst.errno == errno.ENOENT:
624 624 ui.warn(_('%s: deleted in working directory\n') % relsrc)
625 625 srcexists = False
626 626 else:
627 627 ui.warn(_('%s: cannot copy - %s\n') %
628 628 (relsrc, inst.strerror))
629 629 return True # report a failure
630 630
631 631 if ui.verbose or not exact:
632 632 if rename:
633 633 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
634 634 else:
635 635 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
636 636
637 637 targets[abstarget] = abssrc
638 638
639 639 # fix up dirstate
640 640 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
641 641 dryrun=dryrun, cwd=cwd)
642 642 if rename and not dryrun:
643 643 if not after and srcexists and not samefile:
644 644 util.unlinkpath(repo.wjoin(abssrc))
645 645 wctx.forget([abssrc])
646 646
647 647 # pat: ossep
648 648 # dest ossep
649 649 # srcs: list of (hgsep, hgsep, ossep, bool)
650 650 # return: function that takes hgsep and returns ossep
651 651 def targetpathfn(pat, dest, srcs):
652 652 if os.path.isdir(pat):
653 653 abspfx = pathutil.canonpath(repo.root, cwd, pat)
654 654 abspfx = util.localpath(abspfx)
655 655 if destdirexists:
656 656 striplen = len(os.path.split(abspfx)[0])
657 657 else:
658 658 striplen = len(abspfx)
659 659 if striplen:
660 660 striplen += len(os.sep)
661 661 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
662 662 elif destdirexists:
663 663 res = lambda p: os.path.join(dest,
664 664 os.path.basename(util.localpath(p)))
665 665 else:
666 666 res = lambda p: dest
667 667 return res
668 668
669 669 # pat: ossep
670 670 # dest ossep
671 671 # srcs: list of (hgsep, hgsep, ossep, bool)
672 672 # return: function that takes hgsep and returns ossep
673 673 def targetpathafterfn(pat, dest, srcs):
674 674 if matchmod.patkind(pat):
675 675 # a mercurial pattern
676 676 res = lambda p: os.path.join(dest,
677 677 os.path.basename(util.localpath(p)))
678 678 else:
679 679 abspfx = pathutil.canonpath(repo.root, cwd, pat)
680 680 if len(abspfx) < len(srcs[0][0]):
681 681 # A directory. Either the target path contains the last
682 682 # component of the source path or it does not.
683 683 def evalpath(striplen):
684 684 score = 0
685 685 for s in srcs:
686 686 t = os.path.join(dest, util.localpath(s[0])[striplen:])
687 687 if os.path.lexists(t):
688 688 score += 1
689 689 return score
690 690
691 691 abspfx = util.localpath(abspfx)
692 692 striplen = len(abspfx)
693 693 if striplen:
694 694 striplen += len(os.sep)
695 695 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
696 696 score = evalpath(striplen)
697 697 striplen1 = len(os.path.split(abspfx)[0])
698 698 if striplen1:
699 699 striplen1 += len(os.sep)
700 700 if evalpath(striplen1) > score:
701 701 striplen = striplen1
702 702 res = lambda p: os.path.join(dest,
703 703 util.localpath(p)[striplen:])
704 704 else:
705 705 # a file
706 706 if destdirexists:
707 707 res = lambda p: os.path.join(dest,
708 708 os.path.basename(util.localpath(p)))
709 709 else:
710 710 res = lambda p: dest
711 711 return res
712 712
713 713 pats = scmutil.expandpats(pats)
714 714 if not pats:
715 715 raise error.Abort(_('no source or destination specified'))
716 716 if len(pats) == 1:
717 717 raise error.Abort(_('no destination specified'))
718 718 dest = pats.pop()
719 719 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
720 720 if not destdirexists:
721 721 if len(pats) > 1 or matchmod.patkind(pats[0]):
722 722 raise error.Abort(_('with multiple sources, destination must be an '
723 723 'existing directory'))
724 724 if util.endswithsep(dest):
725 725 raise error.Abort(_('destination %s is not a directory') % dest)
726 726
727 727 tfn = targetpathfn
728 728 if after:
729 729 tfn = targetpathafterfn
730 730 copylist = []
731 731 for pat in pats:
732 732 srcs = walkpat(pat)
733 733 if not srcs:
734 734 continue
735 735 copylist.append((tfn(pat, dest, srcs), srcs))
736 736 if not copylist:
737 737 raise error.Abort(_('no files to copy'))
738 738
739 739 errors = 0
740 740 for targetpath, srcs in copylist:
741 741 for abssrc, relsrc, exact in srcs:
742 742 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
743 743 errors += 1
744 744
745 745 if errors:
746 746 ui.warn(_('(consider using --after)\n'))
747 747
748 748 return errors != 0
749 749
750 750 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
751 751 runargs=None, appendpid=False):
752 752 '''Run a command as a service.'''
753 753
754 754 def writepid(pid):
755 755 if opts['pid_file']:
756 756 if appendpid:
757 757 mode = 'a'
758 758 else:
759 759 mode = 'w'
760 760 fp = open(opts['pid_file'], mode)
761 761 fp.write(str(pid) + '\n')
762 762 fp.close()
763 763
764 764 if opts['daemon'] and not opts['daemon_pipefds']:
765 765 # Signal child process startup with file removal
766 766 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
767 767 os.close(lockfd)
768 768 try:
769 769 if not runargs:
770 770 runargs = util.hgcmd() + sys.argv[1:]
771 771 runargs.append('--daemon-pipefds=%s' % lockpath)
772 772 # Don't pass --cwd to the child process, because we've already
773 773 # changed directory.
774 774 for i in xrange(1, len(runargs)):
775 775 if runargs[i].startswith('--cwd='):
776 776 del runargs[i]
777 777 break
778 778 elif runargs[i].startswith('--cwd'):
779 779 del runargs[i:i + 2]
780 780 break
781 781 def condfn():
782 782 return not os.path.exists(lockpath)
783 783 pid = util.rundetached(runargs, condfn)
784 784 if pid < 0:
785 785 raise error.Abort(_('child process failed to start'))
786 786 writepid(pid)
787 787 finally:
788 788 try:
789 789 os.unlink(lockpath)
790 790 except OSError as e:
791 791 if e.errno != errno.ENOENT:
792 792 raise
793 793 if parentfn:
794 794 return parentfn(pid)
795 795 else:
796 796 return
797 797
798 798 if initfn:
799 799 initfn()
800 800
801 801 if not opts['daemon']:
802 802 writepid(os.getpid())
803 803
804 804 if opts['daemon_pipefds']:
805 805 lockpath = opts['daemon_pipefds']
806 806 try:
807 807 os.setsid()
808 808 except AttributeError:
809 809 pass
810 810 os.unlink(lockpath)
811 811 util.hidewindow()
812 812 sys.stdout.flush()
813 813 sys.stderr.flush()
814 814
815 815 nullfd = os.open(os.devnull, os.O_RDWR)
816 816 logfilefd = nullfd
817 817 if logfile:
818 818 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
819 819 os.dup2(nullfd, 0)
820 820 os.dup2(logfilefd, 1)
821 821 os.dup2(logfilefd, 2)
822 822 if nullfd not in (0, 1, 2):
823 823 os.close(nullfd)
824 824 if logfile and logfilefd not in (0, 1, 2):
825 825 os.close(logfilefd)
826 826
827 827 if runfn:
828 828 return runfn()
829 829
830 830 ## facility to let extension process additional data into an import patch
831 831 # list of identifier to be executed in order
832 832 extrapreimport = [] # run before commit
833 833 extrapostimport = [] # run after commit
834 834 # mapping from identifier to actual import function
835 835 #
836 836 # 'preimport' are run before the commit is made and are provided the following
837 837 # arguments:
838 838 # - repo: the localrepository instance,
839 839 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
840 840 # - extra: the future extra dictionary of the changeset, please mutate it,
841 841 # - opts: the import options.
842 842 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
843 843 # mutation of in memory commit and more. Feel free to rework the code to get
844 844 # there.
845 845 extrapreimportmap = {}
846 846 # 'postimport' are run after the commit is made and are provided the following
847 847 # argument:
848 848 # - ctx: the changectx created by import.
849 849 extrapostimportmap = {}
850 850
851 851 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
852 852 """Utility function used by commands.import to import a single patch
853 853
854 854 This function is explicitly defined here to help the evolve extension to
855 855 wrap this part of the import logic.
856 856
857 857 The API is currently a bit ugly because it a simple code translation from
858 858 the import command. Feel free to make it better.
859 859
860 860 :hunk: a patch (as a binary string)
861 861 :parents: nodes that will be parent of the created commit
862 862 :opts: the full dict of option passed to the import command
863 863 :msgs: list to save commit message to.
864 864 (used in case we need to save it when failing)
865 865 :updatefunc: a function that update a repo to a given node
866 866 updatefunc(<repo>, <node>)
867 867 """
868 868 # avoid cycle context -> subrepo -> cmdutil
869 869 import context
870 870 extractdata = patch.extract(ui, hunk)
871 871 tmpname = extractdata.get('filename')
872 872 message = extractdata.get('message')
873 873 user = opts.get('user') or extractdata.get('user')
874 874 date = opts.get('date') or extractdata.get('date')
875 875 branch = extractdata.get('branch')
876 876 nodeid = extractdata.get('nodeid')
877 877 p1 = extractdata.get('p1')
878 878 p2 = extractdata.get('p2')
879 879
880 880 nocommit = opts.get('no_commit')
881 881 importbranch = opts.get('import_branch')
882 882 update = not opts.get('bypass')
883 883 strip = opts["strip"]
884 884 prefix = opts["prefix"]
885 885 sim = float(opts.get('similarity') or 0)
886 886 if not tmpname:
887 887 return (None, None, False)
888 888
889 889 rejects = False
890 890
891 891 try:
892 892 cmdline_message = logmessage(ui, opts)
893 893 if cmdline_message:
894 894 # pickup the cmdline msg
895 895 message = cmdline_message
896 896 elif message:
897 897 # pickup the patch msg
898 898 message = message.strip()
899 899 else:
900 900 # launch the editor
901 901 message = None
902 902 ui.debug('message:\n%s\n' % message)
903 903
904 904 if len(parents) == 1:
905 905 parents.append(repo[nullid])
906 906 if opts.get('exact'):
907 907 if not nodeid or not p1:
908 908 raise error.Abort(_('not a Mercurial patch'))
909 909 p1 = repo[p1]
910 910 p2 = repo[p2 or nullid]
911 911 elif p2:
912 912 try:
913 913 p1 = repo[p1]
914 914 p2 = repo[p2]
915 915 # Without any options, consider p2 only if the
916 916 # patch is being applied on top of the recorded
917 917 # first parent.
918 918 if p1 != parents[0]:
919 919 p1 = parents[0]
920 920 p2 = repo[nullid]
921 921 except error.RepoError:
922 922 p1, p2 = parents
923 923 if p2.node() == nullid:
924 924 ui.warn(_("warning: import the patch as a normal revision\n"
925 925 "(use --exact to import the patch as a merge)\n"))
926 926 else:
927 927 p1, p2 = parents
928 928
929 929 n = None
930 930 if update:
931 931 if p1 != parents[0]:
932 932 updatefunc(repo, p1.node())
933 933 if p2 != parents[1]:
934 934 repo.setparents(p1.node(), p2.node())
935 935
936 936 if opts.get('exact') or importbranch:
937 937 repo.dirstate.setbranch(branch or 'default')
938 938
939 939 partial = opts.get('partial', False)
940 940 files = set()
941 941 try:
942 942 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
943 943 files=files, eolmode=None, similarity=sim / 100.0)
944 944 except patch.PatchError as e:
945 945 if not partial:
946 946 raise error.Abort(str(e))
947 947 if partial:
948 948 rejects = True
949 949
950 950 files = list(files)
951 951 if nocommit:
952 952 if message:
953 953 msgs.append(message)
954 954 else:
955 955 if opts.get('exact') or p2:
956 956 # If you got here, you either use --force and know what
957 957 # you are doing or used --exact or a merge patch while
958 958 # being updated to its first parent.
959 959 m = None
960 960 else:
961 961 m = scmutil.matchfiles(repo, files or [])
962 962 editform = mergeeditform(repo[None], 'import.normal')
963 963 if opts.get('exact'):
964 964 editor = None
965 965 else:
966 966 editor = getcommiteditor(editform=editform, **opts)
967 967 allowemptyback = repo.ui.backupconfig('ui', 'allowemptycommit')
968 968 extra = {}
969 969 for idfunc in extrapreimport:
970 970 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
971 971 try:
972 972 if partial:
973 973 repo.ui.setconfig('ui', 'allowemptycommit', True)
974 974 n = repo.commit(message, user,
975 975 date, match=m,
976 976 editor=editor, extra=extra)
977 977 for idfunc in extrapostimport:
978 978 extrapostimportmap[idfunc](repo[n])
979 979 finally:
980 980 repo.ui.restoreconfig(allowemptyback)
981 981 else:
982 982 if opts.get('exact') or importbranch:
983 983 branch = branch or 'default'
984 984 else:
985 985 branch = p1.branch()
986 986 store = patch.filestore()
987 987 try:
988 988 files = set()
989 989 try:
990 990 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
991 991 files, eolmode=None)
992 992 except patch.PatchError as e:
993 993 raise error.Abort(str(e))
994 994 if opts.get('exact'):
995 995 editor = None
996 996 else:
997 997 editor = getcommiteditor(editform='import.bypass')
998 998 memctx = context.makememctx(repo, (p1.node(), p2.node()),
999 999 message,
1000 1000 user,
1001 1001 date,
1002 1002 branch, files, store,
1003 1003 editor=editor)
1004 1004 n = memctx.commit()
1005 1005 finally:
1006 1006 store.close()
1007 1007 if opts.get('exact') and nocommit:
1008 1008 # --exact with --no-commit is still useful in that it does merge
1009 1009 # and branch bits
1010 1010 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1011 1011 elif opts.get('exact') and hex(n) != nodeid:
1012 1012 raise error.Abort(_('patch is damaged or loses information'))
1013 1013 msg = _('applied to working directory')
1014 1014 if n:
1015 1015 # i18n: refers to a short changeset id
1016 1016 msg = _('created %s') % short(n)
1017 1017 return (msg, n, rejects)
1018 1018 finally:
1019 1019 os.unlink(tmpname)
1020 1020
1021 1021 # facility to let extensions include additional data in an exported patch
1022 1022 # list of identifiers to be executed in order
1023 1023 extraexport = []
1024 1024 # mapping from identifier to actual export function
1025 1025 # function as to return a string to be added to the header or None
1026 1026 # it is given two arguments (sequencenumber, changectx)
1027 1027 extraexportmap = {}
1028 1028
1029 1029 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1030 1030 opts=None, match=None):
1031 1031 '''export changesets as hg patches.'''
1032 1032
1033 1033 total = len(revs)
1034 1034 revwidth = max([len(str(rev)) for rev in revs])
1035 1035 filemode = {}
1036 1036
1037 1037 def single(rev, seqno, fp):
1038 1038 ctx = repo[rev]
1039 1039 node = ctx.node()
1040 1040 parents = [p.node() for p in ctx.parents() if p]
1041 1041 branch = ctx.branch()
1042 1042 if switch_parent:
1043 1043 parents.reverse()
1044 1044
1045 1045 if parents:
1046 1046 prev = parents[0]
1047 1047 else:
1048 1048 prev = nullid
1049 1049
1050 1050 shouldclose = False
1051 1051 if not fp and len(template) > 0:
1052 1052 desc_lines = ctx.description().rstrip().split('\n')
1053 1053 desc = desc_lines[0] #Commit always has a first line.
1054 1054 fp = makefileobj(repo, template, node, desc=desc, total=total,
1055 1055 seqno=seqno, revwidth=revwidth, mode='wb',
1056 1056 modemap=filemode)
1057 1057 shouldclose = True
1058 1058 if fp and not getattr(fp, 'name', '<unnamed>').startswith('<'):
1059 1059 repo.ui.note("%s\n" % fp.name)
1060 1060
1061 1061 if not fp:
1062 1062 write = repo.ui.write
1063 1063 else:
1064 1064 def write(s, **kw):
1065 1065 fp.write(s)
1066 1066
1067 1067 write("# HG changeset patch\n")
1068 1068 write("# User %s\n" % ctx.user())
1069 1069 write("# Date %d %d\n" % ctx.date())
1070 1070 write("# %s\n" % util.datestr(ctx.date()))
1071 1071 if branch and branch != 'default':
1072 1072 write("# Branch %s\n" % branch)
1073 1073 write("# Node ID %s\n" % hex(node))
1074 1074 write("# Parent %s\n" % hex(prev))
1075 1075 if len(parents) > 1:
1076 1076 write("# Parent %s\n" % hex(parents[1]))
1077 1077
1078 1078 for headerid in extraexport:
1079 1079 header = extraexportmap[headerid](seqno, ctx)
1080 1080 if header is not None:
1081 1081 write('# %s\n' % header)
1082 1082 write(ctx.description().rstrip())
1083 1083 write("\n\n")
1084 1084
1085 1085 for chunk, label in patch.diffui(repo, prev, node, match, opts=opts):
1086 1086 write(chunk, label=label)
1087 1087
1088 1088 if shouldclose:
1089 1089 fp.close()
1090 1090
1091 1091 for seqno, rev in enumerate(revs):
1092 1092 single(rev, seqno + 1, fp)
1093 1093
1094 1094 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1095 1095 changes=None, stat=False, fp=None, prefix='',
1096 1096 root='', listsubrepos=False):
1097 1097 '''show diff or diffstat.'''
1098 1098 if fp is None:
1099 1099 write = ui.write
1100 1100 else:
1101 1101 def write(s, **kw):
1102 1102 fp.write(s)
1103 1103
1104 1104 if root:
1105 1105 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1106 1106 else:
1107 1107 relroot = ''
1108 1108 if relroot != '':
1109 1109 # XXX relative roots currently don't work if the root is within a
1110 1110 # subrepo
1111 1111 uirelroot = match.uipath(relroot)
1112 1112 relroot += '/'
1113 1113 for matchroot in match.files():
1114 1114 if not matchroot.startswith(relroot):
1115 1115 ui.warn(_('warning: %s not inside relative root %s\n') % (
1116 1116 match.uipath(matchroot), uirelroot))
1117 1117
1118 1118 if stat:
1119 1119 diffopts = diffopts.copy(context=0)
1120 1120 width = 80
1121 1121 if not ui.plain():
1122 1122 width = ui.termwidth()
1123 1123 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1124 1124 prefix=prefix, relroot=relroot)
1125 1125 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1126 1126 width=width,
1127 1127 git=diffopts.git):
1128 1128 write(chunk, label=label)
1129 1129 else:
1130 1130 for chunk, label in patch.diffui(repo, node1, node2, match,
1131 1131 changes, diffopts, prefix=prefix,
1132 1132 relroot=relroot):
1133 1133 write(chunk, label=label)
1134 1134
1135 1135 if listsubrepos:
1136 1136 ctx1 = repo[node1]
1137 1137 ctx2 = repo[node2]
1138 1138 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1139 1139 tempnode2 = node2
1140 1140 try:
1141 1141 if node2 is not None:
1142 1142 tempnode2 = ctx2.substate[subpath][1]
1143 1143 except KeyError:
1144 1144 # A subrepo that existed in node1 was deleted between node1 and
1145 1145 # node2 (inclusive). Thus, ctx2's substate won't contain that
1146 1146 # subpath. The best we can do is to ignore it.
1147 1147 tempnode2 = None
1148 1148 submatch = matchmod.narrowmatcher(subpath, match)
1149 1149 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1150 1150 stat=stat, fp=fp, prefix=prefix)
1151 1151
1152 1152 class changeset_printer(object):
1153 1153 '''show changeset information when templating not requested.'''
1154 1154
1155 1155 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1156 1156 self.ui = ui
1157 1157 self.repo = repo
1158 1158 self.buffered = buffered
1159 1159 self.matchfn = matchfn
1160 1160 self.diffopts = diffopts
1161 1161 self.header = {}
1162 1162 self.hunk = {}
1163 1163 self.lastheader = None
1164 1164 self.footer = None
1165 1165
1166 1166 def flush(self, ctx):
1167 1167 rev = ctx.rev()
1168 1168 if rev in self.header:
1169 1169 h = self.header[rev]
1170 1170 if h != self.lastheader:
1171 1171 self.lastheader = h
1172 1172 self.ui.write(h)
1173 1173 del self.header[rev]
1174 1174 if rev in self.hunk:
1175 1175 self.ui.write(self.hunk[rev])
1176 1176 del self.hunk[rev]
1177 1177 return 1
1178 1178 return 0
1179 1179
1180 1180 def close(self):
1181 1181 if self.footer:
1182 1182 self.ui.write(self.footer)
1183 1183
1184 1184 def show(self, ctx, copies=None, matchfn=None, **props):
1185 1185 if self.buffered:
1186 1186 self.ui.pushbuffer(labeled=True)
1187 1187 self._show(ctx, copies, matchfn, props)
1188 1188 self.hunk[ctx.rev()] = self.ui.popbuffer()
1189 1189 else:
1190 1190 self._show(ctx, copies, matchfn, props)
1191 1191
1192 1192 def _show(self, ctx, copies, matchfn, props):
1193 1193 '''show a single changeset or file revision'''
1194 1194 changenode = ctx.node()
1195 1195 rev = ctx.rev()
1196 1196 if self.ui.debugflag:
1197 1197 hexfunc = hex
1198 1198 else:
1199 1199 hexfunc = short
1200 1200 # as of now, wctx.node() and wctx.rev() return None, but we want to
1201 1201 # show the same values as {node} and {rev} templatekw
1202 1202 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1203 1203
1204 1204 if self.ui.quiet:
1205 1205 self.ui.write("%d:%s\n" % revnode, label='log.node')
1206 1206 return
1207 1207
1208 1208 date = util.datestr(ctx.date())
1209 1209
1210 1210 # i18n: column positioning for "hg log"
1211 1211 self.ui.write(_("changeset: %d:%s\n") % revnode,
1212 1212 label='log.changeset changeset.%s' % ctx.phasestr())
1213 1213
1214 1214 # branches are shown first before any other names due to backwards
1215 1215 # compatibility
1216 1216 branch = ctx.branch()
1217 1217 # don't show the default branch name
1218 1218 if branch != 'default':
1219 1219 # i18n: column positioning for "hg log"
1220 1220 self.ui.write(_("branch: %s\n") % branch,
1221 1221 label='log.branch')
1222 1222
1223 1223 for name, ns in self.repo.names.iteritems():
1224 1224 # branches has special logic already handled above, so here we just
1225 1225 # skip it
1226 1226 if name == 'branches':
1227 1227 continue
1228 1228 # we will use the templatename as the color name since those two
1229 1229 # should be the same
1230 1230 for name in ns.names(self.repo, changenode):
1231 1231 self.ui.write(ns.logfmt % name,
1232 1232 label='log.%s' % ns.colorname)
1233 1233 if self.ui.debugflag:
1234 1234 # i18n: column positioning for "hg log"
1235 1235 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1236 1236 label='log.phase')
1237 1237 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1238 1238 label = 'log.parent changeset.%s' % pctx.phasestr()
1239 1239 # i18n: column positioning for "hg log"
1240 1240 self.ui.write(_("parent: %d:%s\n")
1241 1241 % (pctx.rev(), hexfunc(pctx.node())),
1242 1242 label=label)
1243 1243
1244 1244 if self.ui.debugflag and rev is not None:
1245 1245 mnode = ctx.manifestnode()
1246 1246 # i18n: column positioning for "hg log"
1247 1247 self.ui.write(_("manifest: %d:%s\n") %
1248 1248 (self.repo.manifest.rev(mnode), hex(mnode)),
1249 1249 label='ui.debug log.manifest')
1250 1250 # i18n: column positioning for "hg log"
1251 1251 self.ui.write(_("user: %s\n") % ctx.user(),
1252 1252 label='log.user')
1253 1253 # i18n: column positioning for "hg log"
1254 1254 self.ui.write(_("date: %s\n") % date,
1255 1255 label='log.date')
1256 1256
1257 1257 if self.ui.debugflag:
1258 1258 files = ctx.p1().status(ctx)[:3]
1259 1259 for key, value in zip([# i18n: column positioning for "hg log"
1260 1260 _("files:"),
1261 1261 # i18n: column positioning for "hg log"
1262 1262 _("files+:"),
1263 1263 # i18n: column positioning for "hg log"
1264 1264 _("files-:")], files):
1265 1265 if value:
1266 1266 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1267 1267 label='ui.debug log.files')
1268 1268 elif ctx.files() and self.ui.verbose:
1269 1269 # i18n: column positioning for "hg log"
1270 1270 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1271 1271 label='ui.note log.files')
1272 1272 if copies and self.ui.verbose:
1273 1273 copies = ['%s (%s)' % c for c in copies]
1274 1274 # i18n: column positioning for "hg log"
1275 1275 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1276 1276 label='ui.note log.copies')
1277 1277
1278 1278 extra = ctx.extra()
1279 1279 if extra and self.ui.debugflag:
1280 1280 for key, value in sorted(extra.items()):
1281 1281 # i18n: column positioning for "hg log"
1282 1282 self.ui.write(_("extra: %s=%s\n")
1283 1283 % (key, value.encode('string_escape')),
1284 1284 label='ui.debug log.extra')
1285 1285
1286 1286 description = ctx.description().strip()
1287 1287 if description:
1288 1288 if self.ui.verbose:
1289 1289 self.ui.write(_("description:\n"),
1290 1290 label='ui.note log.description')
1291 1291 self.ui.write(description,
1292 1292 label='ui.note log.description')
1293 1293 self.ui.write("\n\n")
1294 1294 else:
1295 1295 # i18n: column positioning for "hg log"
1296 1296 self.ui.write(_("summary: %s\n") %
1297 1297 description.splitlines()[0],
1298 1298 label='log.summary')
1299 1299 self.ui.write("\n")
1300 1300
1301 1301 self.showpatch(ctx, matchfn)
1302 1302
1303 1303 def showpatch(self, ctx, matchfn):
1304 1304 if not matchfn:
1305 1305 matchfn = self.matchfn
1306 1306 if matchfn:
1307 1307 stat = self.diffopts.get('stat')
1308 1308 diff = self.diffopts.get('patch')
1309 1309 diffopts = patch.diffallopts(self.ui, self.diffopts)
1310 1310 node = ctx.node()
1311 1311 prev = ctx.p1().node()
1312 1312 if stat:
1313 1313 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1314 1314 match=matchfn, stat=True)
1315 1315 if diff:
1316 1316 if stat:
1317 1317 self.ui.write("\n")
1318 1318 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1319 1319 match=matchfn, stat=False)
1320 1320 self.ui.write("\n")
1321 1321
1322 1322 class jsonchangeset(changeset_printer):
1323 1323 '''format changeset information.'''
1324 1324
1325 1325 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1326 1326 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1327 1327 self.cache = {}
1328 1328 self._first = True
1329 1329
1330 1330 def close(self):
1331 1331 if not self._first:
1332 1332 self.ui.write("\n]\n")
1333 1333 else:
1334 1334 self.ui.write("[]\n")
1335 1335
1336 1336 def _show(self, ctx, copies, matchfn, props):
1337 1337 '''show a single changeset or file revision'''
1338 1338 rev = ctx.rev()
1339 1339 if rev is None:
1340 1340 jrev = jnode = 'null'
1341 1341 else:
1342 1342 jrev = str(rev)
1343 1343 jnode = '"%s"' % hex(ctx.node())
1344 1344 j = encoding.jsonescape
1345 1345
1346 1346 if self._first:
1347 1347 self.ui.write("[\n {")
1348 1348 self._first = False
1349 1349 else:
1350 1350 self.ui.write(",\n {")
1351 1351
1352 1352 if self.ui.quiet:
1353 1353 self.ui.write('\n "rev": %s' % jrev)
1354 1354 self.ui.write(',\n "node": %s' % jnode)
1355 1355 self.ui.write('\n }')
1356 1356 return
1357 1357
1358 1358 self.ui.write('\n "rev": %s' % jrev)
1359 1359 self.ui.write(',\n "node": %s' % jnode)
1360 1360 self.ui.write(',\n "branch": "%s"' % j(ctx.branch()))
1361 1361 self.ui.write(',\n "phase": "%s"' % ctx.phasestr())
1362 1362 self.ui.write(',\n "user": "%s"' % j(ctx.user()))
1363 1363 self.ui.write(',\n "date": [%d, %d]' % ctx.date())
1364 1364 self.ui.write(',\n "desc": "%s"' % j(ctx.description()))
1365 1365
1366 1366 self.ui.write(',\n "bookmarks": [%s]' %
1367 1367 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1368 1368 self.ui.write(',\n "tags": [%s]' %
1369 1369 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1370 1370 self.ui.write(',\n "parents": [%s]' %
1371 1371 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1372 1372
1373 1373 if self.ui.debugflag:
1374 1374 if rev is None:
1375 1375 jmanifestnode = 'null'
1376 1376 else:
1377 1377 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1378 1378 self.ui.write(',\n "manifest": %s' % jmanifestnode)
1379 1379
1380 1380 self.ui.write(',\n "extra": {%s}' %
1381 1381 ", ".join('"%s": "%s"' % (j(k), j(v))
1382 1382 for k, v in ctx.extra().items()))
1383 1383
1384 1384 files = ctx.p1().status(ctx)
1385 1385 self.ui.write(',\n "modified": [%s]' %
1386 1386 ", ".join('"%s"' % j(f) for f in files[0]))
1387 1387 self.ui.write(',\n "added": [%s]' %
1388 1388 ", ".join('"%s"' % j(f) for f in files[1]))
1389 1389 self.ui.write(',\n "removed": [%s]' %
1390 1390 ", ".join('"%s"' % j(f) for f in files[2]))
1391 1391
1392 1392 elif self.ui.verbose:
1393 1393 self.ui.write(',\n "files": [%s]' %
1394 1394 ", ".join('"%s"' % j(f) for f in ctx.files()))
1395 1395
1396 1396 if copies:
1397 1397 self.ui.write(',\n "copies": {%s}' %
1398 1398 ", ".join('"%s": "%s"' % (j(k), j(v))
1399 1399 for k, v in copies))
1400 1400
1401 1401 matchfn = self.matchfn
1402 1402 if matchfn:
1403 1403 stat = self.diffopts.get('stat')
1404 1404 diff = self.diffopts.get('patch')
1405 1405 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1406 1406 node, prev = ctx.node(), ctx.p1().node()
1407 1407 if stat:
1408 1408 self.ui.pushbuffer()
1409 1409 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1410 1410 match=matchfn, stat=True)
1411 1411 self.ui.write(',\n "diffstat": "%s"' % j(self.ui.popbuffer()))
1412 1412 if diff:
1413 1413 self.ui.pushbuffer()
1414 1414 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1415 1415 match=matchfn, stat=False)
1416 1416 self.ui.write(',\n "diff": "%s"' % j(self.ui.popbuffer()))
1417 1417
1418 1418 self.ui.write("\n }")
1419 1419
1420 1420 class changeset_templater(changeset_printer):
1421 1421 '''format changeset information.'''
1422 1422
1423 1423 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1424 1424 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1425 1425 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1426 1426 defaulttempl = {
1427 1427 'parent': '{rev}:{node|formatnode} ',
1428 1428 'manifest': '{rev}:{node|formatnode}',
1429 1429 'file_copy': '{name} ({source})',
1430 1430 'extra': '{key}={value|stringescape}'
1431 1431 }
1432 1432 # filecopy is preserved for compatibility reasons
1433 1433 defaulttempl['filecopy'] = defaulttempl['file_copy']
1434 1434 self.t = templater.templater(mapfile, {'formatnode': formatnode},
1435 1435 cache=defaulttempl)
1436 1436 if tmpl:
1437 1437 self.t.cache['changeset'] = tmpl
1438 1438
1439 1439 self.cache = {}
1440 1440
1441 1441 # find correct templates for current mode
1442 1442 tmplmodes = [
1443 1443 (True, None),
1444 1444 (self.ui.verbose, 'verbose'),
1445 1445 (self.ui.quiet, 'quiet'),
1446 1446 (self.ui.debugflag, 'debug'),
1447 1447 ]
1448 1448
1449 1449 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1450 1450 'docheader': '', 'docfooter': ''}
1451 1451 for mode, postfix in tmplmodes:
1452 1452 for t in self._parts:
1453 1453 cur = t
1454 1454 if postfix:
1455 1455 cur += "_" + postfix
1456 1456 if mode and cur in self.t:
1457 1457 self._parts[t] = cur
1458 1458
1459 1459 if self._parts['docheader']:
1460 1460 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1461 1461
1462 1462 def close(self):
1463 1463 if self._parts['docfooter']:
1464 1464 if not self.footer:
1465 1465 self.footer = ""
1466 1466 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1467 1467 return super(changeset_templater, self).close()
1468 1468
1469 1469 def _show(self, ctx, copies, matchfn, props):
1470 1470 '''show a single changeset or file revision'''
1471 1471 props = props.copy()
1472 1472 props.update(templatekw.keywords)
1473 1473 props['templ'] = self.t
1474 1474 props['ctx'] = ctx
1475 1475 props['repo'] = self.repo
1476 1476 props['revcache'] = {'copies': copies}
1477 1477 props['cache'] = self.cache
1478 1478
1479 1479 try:
1480 1480 # write header
1481 1481 if self._parts['header']:
1482 1482 h = templater.stringify(self.t(self._parts['header'], **props))
1483 1483 if self.buffered:
1484 1484 self.header[ctx.rev()] = h
1485 1485 else:
1486 1486 if self.lastheader != h:
1487 1487 self.lastheader = h
1488 1488 self.ui.write(h)
1489 1489
1490 1490 # write changeset metadata, then patch if requested
1491 1491 key = self._parts['changeset']
1492 1492 self.ui.write(templater.stringify(self.t(key, **props)))
1493 1493 self.showpatch(ctx, matchfn)
1494 1494
1495 1495 if self._parts['footer']:
1496 1496 if not self.footer:
1497 1497 self.footer = templater.stringify(
1498 1498 self.t(self._parts['footer'], **props))
1499 1499 except KeyError as inst:
1500 1500 msg = _("%s: no key named '%s'")
1501 1501 raise error.Abort(msg % (self.t.mapfile, inst.args[0]))
1502 1502 except SyntaxError as inst:
1503 1503 raise error.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1504 1504
1505 1505 def gettemplate(ui, tmpl, style):
1506 1506 """
1507 1507 Find the template matching the given template spec or style.
1508 1508 """
1509 1509
1510 1510 # ui settings
1511 1511 if not tmpl and not style: # template are stronger than style
1512 1512 tmpl = ui.config('ui', 'logtemplate')
1513 1513 if tmpl:
1514 1514 try:
1515 1515 tmpl = templater.unquotestring(tmpl)
1516 1516 except SyntaxError:
1517 1517 pass
1518 1518 return tmpl, None
1519 1519 else:
1520 1520 style = util.expandpath(ui.config('ui', 'style', ''))
1521 1521
1522 1522 if not tmpl and style:
1523 1523 mapfile = style
1524 1524 if not os.path.split(mapfile)[0]:
1525 1525 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1526 1526 or templater.templatepath(mapfile))
1527 1527 if mapname:
1528 1528 mapfile = mapname
1529 1529 return None, mapfile
1530 1530
1531 1531 if not tmpl:
1532 1532 return None, None
1533 1533
1534 1534 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1535 1535
1536 1536 def show_changeset(ui, repo, opts, buffered=False):
1537 1537 """show one changeset using template or regular display.
1538 1538
1539 1539 Display format will be the first non-empty hit of:
1540 1540 1. option 'template'
1541 1541 2. option 'style'
1542 1542 3. [ui] setting 'logtemplate'
1543 1543 4. [ui] setting 'style'
1544 1544 If all of these values are either the unset or the empty string,
1545 1545 regular display via changeset_printer() is done.
1546 1546 """
1547 1547 # options
1548 1548 matchfn = None
1549 1549 if opts.get('patch') or opts.get('stat'):
1550 1550 matchfn = scmutil.matchall(repo)
1551 1551
1552 1552 if opts.get('template') == 'json':
1553 1553 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1554 1554
1555 1555 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1556 1556
1557 1557 if not tmpl and not mapfile:
1558 1558 return changeset_printer(ui, repo, matchfn, opts, buffered)
1559 1559
1560 1560 try:
1561 1561 t = changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile,
1562 1562 buffered)
1563 1563 except SyntaxError as inst:
1564 1564 raise error.Abort(inst.args[0])
1565 1565 return t
1566 1566
1567 1567 def showmarker(ui, marker):
1568 1568 """utility function to display obsolescence marker in a readable way
1569 1569
1570 1570 To be used by debug function."""
1571 1571 ui.write(hex(marker.precnode()))
1572 1572 for repl in marker.succnodes():
1573 1573 ui.write(' ')
1574 1574 ui.write(hex(repl))
1575 1575 ui.write(' %X ' % marker.flags())
1576 1576 parents = marker.parentnodes()
1577 1577 if parents is not None:
1578 1578 ui.write('{%s} ' % ', '.join(hex(p) for p in parents))
1579 1579 ui.write('(%s) ' % util.datestr(marker.date()))
1580 1580 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1581 1581 sorted(marker.metadata().items())
1582 1582 if t[0] != 'date')))
1583 1583 ui.write('\n')
1584 1584
1585 1585 def finddate(ui, repo, date):
1586 1586 """Find the tipmost changeset that matches the given date spec"""
1587 1587
1588 1588 df = util.matchdate(date)
1589 1589 m = scmutil.matchall(repo)
1590 1590 results = {}
1591 1591
1592 1592 def prep(ctx, fns):
1593 1593 d = ctx.date()
1594 1594 if df(d[0]):
1595 1595 results[ctx.rev()] = d
1596 1596
1597 1597 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1598 1598 rev = ctx.rev()
1599 1599 if rev in results:
1600 1600 ui.status(_("found revision %s from %s\n") %
1601 1601 (rev, util.datestr(results[rev])))
1602 1602 return str(rev)
1603 1603
1604 1604 raise error.Abort(_("revision matching date not found"))
1605 1605
1606 1606 def increasingwindows(windowsize=8, sizelimit=512):
1607 1607 while True:
1608 1608 yield windowsize
1609 1609 if windowsize < sizelimit:
1610 1610 windowsize *= 2
1611 1611
1612 1612 class FileWalkError(Exception):
1613 1613 pass
1614 1614
1615 1615 def walkfilerevs(repo, match, follow, revs, fncache):
1616 1616 '''Walks the file history for the matched files.
1617 1617
1618 1618 Returns the changeset revs that are involved in the file history.
1619 1619
1620 1620 Throws FileWalkError if the file history can't be walked using
1621 1621 filelogs alone.
1622 1622 '''
1623 1623 wanted = set()
1624 1624 copies = []
1625 1625 minrev, maxrev = min(revs), max(revs)
1626 1626 def filerevgen(filelog, last):
1627 1627 """
1628 1628 Only files, no patterns. Check the history of each file.
1629 1629
1630 1630 Examines filelog entries within minrev, maxrev linkrev range
1631 1631 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1632 1632 tuples in backwards order
1633 1633 """
1634 1634 cl_count = len(repo)
1635 1635 revs = []
1636 1636 for j in xrange(0, last + 1):
1637 1637 linkrev = filelog.linkrev(j)
1638 1638 if linkrev < minrev:
1639 1639 continue
1640 1640 # only yield rev for which we have the changelog, it can
1641 1641 # happen while doing "hg log" during a pull or commit
1642 1642 if linkrev >= cl_count:
1643 1643 break
1644 1644
1645 1645 parentlinkrevs = []
1646 1646 for p in filelog.parentrevs(j):
1647 1647 if p != nullrev:
1648 1648 parentlinkrevs.append(filelog.linkrev(p))
1649 1649 n = filelog.node(j)
1650 1650 revs.append((linkrev, parentlinkrevs,
1651 1651 follow and filelog.renamed(n)))
1652 1652
1653 1653 return reversed(revs)
1654 1654 def iterfiles():
1655 1655 pctx = repo['.']
1656 1656 for filename in match.files():
1657 1657 if follow:
1658 1658 if filename not in pctx:
1659 1659 raise error.Abort(_('cannot follow file not in parent '
1660 1660 'revision: "%s"') % filename)
1661 1661 yield filename, pctx[filename].filenode()
1662 1662 else:
1663 1663 yield filename, None
1664 1664 for filename_node in copies:
1665 1665 yield filename_node
1666 1666
1667 1667 for file_, node in iterfiles():
1668 1668 filelog = repo.file(file_)
1669 1669 if not len(filelog):
1670 1670 if node is None:
1671 1671 # A zero count may be a directory or deleted file, so
1672 1672 # try to find matching entries on the slow path.
1673 1673 if follow:
1674 1674 raise error.Abort(
1675 1675 _('cannot follow nonexistent file: "%s"') % file_)
1676 1676 raise FileWalkError("Cannot walk via filelog")
1677 1677 else:
1678 1678 continue
1679 1679
1680 1680 if node is None:
1681 1681 last = len(filelog) - 1
1682 1682 else:
1683 1683 last = filelog.rev(node)
1684 1684
1685 1685 # keep track of all ancestors of the file
1686 1686 ancestors = set([filelog.linkrev(last)])
1687 1687
1688 1688 # iterate from latest to oldest revision
1689 1689 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1690 1690 if not follow:
1691 1691 if rev > maxrev:
1692 1692 continue
1693 1693 else:
1694 1694 # Note that last might not be the first interesting
1695 1695 # rev to us:
1696 1696 # if the file has been changed after maxrev, we'll
1697 1697 # have linkrev(last) > maxrev, and we still need
1698 1698 # to explore the file graph
1699 1699 if rev not in ancestors:
1700 1700 continue
1701 1701 # XXX insert 1327 fix here
1702 1702 if flparentlinkrevs:
1703 1703 ancestors.update(flparentlinkrevs)
1704 1704
1705 1705 fncache.setdefault(rev, []).append(file_)
1706 1706 wanted.add(rev)
1707 1707 if copied:
1708 1708 copies.append(copied)
1709 1709
1710 1710 return wanted
1711 1711
1712 1712 class _followfilter(object):
1713 1713 def __init__(self, repo, onlyfirst=False):
1714 1714 self.repo = repo
1715 1715 self.startrev = nullrev
1716 1716 self.roots = set()
1717 1717 self.onlyfirst = onlyfirst
1718 1718
1719 1719 def match(self, rev):
1720 1720 def realparents(rev):
1721 1721 if self.onlyfirst:
1722 1722 return self.repo.changelog.parentrevs(rev)[0:1]
1723 1723 else:
1724 1724 return filter(lambda x: x != nullrev,
1725 1725 self.repo.changelog.parentrevs(rev))
1726 1726
1727 1727 if self.startrev == nullrev:
1728 1728 self.startrev = rev
1729 1729 return True
1730 1730
1731 1731 if rev > self.startrev:
1732 1732 # forward: all descendants
1733 1733 if not self.roots:
1734 1734 self.roots.add(self.startrev)
1735 1735 for parent in realparents(rev):
1736 1736 if parent in self.roots:
1737 1737 self.roots.add(rev)
1738 1738 return True
1739 1739 else:
1740 1740 # backwards: all parents
1741 1741 if not self.roots:
1742 1742 self.roots.update(realparents(self.startrev))
1743 1743 if rev in self.roots:
1744 1744 self.roots.remove(rev)
1745 1745 self.roots.update(realparents(rev))
1746 1746 return True
1747 1747
1748 1748 return False
1749 1749
1750 1750 def walkchangerevs(repo, match, opts, prepare):
1751 1751 '''Iterate over files and the revs in which they changed.
1752 1752
1753 1753 Callers most commonly need to iterate backwards over the history
1754 1754 in which they are interested. Doing so has awful (quadratic-looking)
1755 1755 performance, so we use iterators in a "windowed" way.
1756 1756
1757 1757 We walk a window of revisions in the desired order. Within the
1758 1758 window, we first walk forwards to gather data, then in the desired
1759 1759 order (usually backwards) to display it.
1760 1760
1761 1761 This function returns an iterator yielding contexts. Before
1762 1762 yielding each context, the iterator will first call the prepare
1763 1763 function on each context in the window in forward order.'''
1764 1764
1765 1765 follow = opts.get('follow') or opts.get('follow_first')
1766 1766 revs = _logrevs(repo, opts)
1767 1767 if not revs:
1768 1768 return []
1769 1769 wanted = set()
1770 1770 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1771 1771 opts.get('removed'))
1772 1772 fncache = {}
1773 1773 change = repo.changectx
1774 1774
1775 1775 # First step is to fill wanted, the set of revisions that we want to yield.
1776 1776 # When it does not induce extra cost, we also fill fncache for revisions in
1777 1777 # wanted: a cache of filenames that were changed (ctx.files()) and that
1778 1778 # match the file filtering conditions.
1779 1779
1780 1780 if match.always():
1781 1781 # No files, no patterns. Display all revs.
1782 1782 wanted = revs
1783 1783 elif not slowpath:
1784 1784 # We only have to read through the filelog to find wanted revisions
1785 1785
1786 1786 try:
1787 1787 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1788 1788 except FileWalkError:
1789 1789 slowpath = True
1790 1790
1791 1791 # We decided to fall back to the slowpath because at least one
1792 1792 # of the paths was not a file. Check to see if at least one of them
1793 1793 # existed in history, otherwise simply return
1794 1794 for path in match.files():
1795 1795 if path == '.' or path in repo.store:
1796 1796 break
1797 1797 else:
1798 1798 return []
1799 1799
1800 1800 if slowpath:
1801 1801 # We have to read the changelog to match filenames against
1802 1802 # changed files
1803 1803
1804 1804 if follow:
1805 1805 raise error.Abort(_('can only follow copies/renames for explicit '
1806 1806 'filenames'))
1807 1807
1808 1808 # The slow path checks files modified in every changeset.
1809 1809 # This is really slow on large repos, so compute the set lazily.
1810 1810 class lazywantedset(object):
1811 1811 def __init__(self):
1812 1812 self.set = set()
1813 1813 self.revs = set(revs)
1814 1814
1815 1815 # No need to worry about locality here because it will be accessed
1816 1816 # in the same order as the increasing window below.
1817 1817 def __contains__(self, value):
1818 1818 if value in self.set:
1819 1819 return True
1820 1820 elif not value in self.revs:
1821 1821 return False
1822 1822 else:
1823 1823 self.revs.discard(value)
1824 1824 ctx = change(value)
1825 1825 matches = filter(match, ctx.files())
1826 1826 if matches:
1827 1827 fncache[value] = matches
1828 1828 self.set.add(value)
1829 1829 return True
1830 1830 return False
1831 1831
1832 1832 def discard(self, value):
1833 1833 self.revs.discard(value)
1834 1834 self.set.discard(value)
1835 1835
1836 1836 wanted = lazywantedset()
1837 1837
1838 1838 # it might be worthwhile to do this in the iterator if the rev range
1839 1839 # is descending and the prune args are all within that range
1840 1840 for rev in opts.get('prune', ()):
1841 1841 rev = repo[rev].rev()
1842 1842 ff = _followfilter(repo)
1843 1843 stop = min(revs[0], revs[-1])
1844 1844 for x in xrange(rev, stop - 1, -1):
1845 1845 if ff.match(x):
1846 1846 wanted = wanted - [x]
1847 1847
1848 1848 # Now that wanted is correctly initialized, we can iterate over the
1849 1849 # revision range, yielding only revisions in wanted.
1850 1850 def iterate():
1851 1851 if follow and match.always():
1852 1852 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1853 1853 def want(rev):
1854 1854 return ff.match(rev) and rev in wanted
1855 1855 else:
1856 1856 def want(rev):
1857 1857 return rev in wanted
1858 1858
1859 1859 it = iter(revs)
1860 1860 stopiteration = False
1861 1861 for windowsize in increasingwindows():
1862 1862 nrevs = []
1863 1863 for i in xrange(windowsize):
1864 1864 rev = next(it, None)
1865 1865 if rev is None:
1866 1866 stopiteration = True
1867 1867 break
1868 1868 elif want(rev):
1869 1869 nrevs.append(rev)
1870 1870 for rev in sorted(nrevs):
1871 1871 fns = fncache.get(rev)
1872 1872 ctx = change(rev)
1873 1873 if not fns:
1874 1874 def fns_generator():
1875 1875 for f in ctx.files():
1876 1876 if match(f):
1877 1877 yield f
1878 1878 fns = fns_generator()
1879 1879 prepare(ctx, fns)
1880 1880 for rev in nrevs:
1881 1881 yield change(rev)
1882 1882
1883 1883 if stopiteration:
1884 1884 break
1885 1885
1886 1886 return iterate()
1887 1887
1888 1888 def _makefollowlogfilematcher(repo, files, followfirst):
1889 1889 # When displaying a revision with --patch --follow FILE, we have
1890 1890 # to know which file of the revision must be diffed. With
1891 1891 # --follow, we want the names of the ancestors of FILE in the
1892 1892 # revision, stored in "fcache". "fcache" is populated by
1893 1893 # reproducing the graph traversal already done by --follow revset
1894 1894 # and relating linkrevs to file names (which is not "correct" but
1895 1895 # good enough).
1896 1896 fcache = {}
1897 1897 fcacheready = [False]
1898 1898 pctx = repo['.']
1899 1899
1900 1900 def populate():
1901 1901 for fn in files:
1902 1902 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1903 1903 for c in i:
1904 1904 fcache.setdefault(c.linkrev(), set()).add(c.path())
1905 1905
1906 1906 def filematcher(rev):
1907 1907 if not fcacheready[0]:
1908 1908 # Lazy initialization
1909 1909 fcacheready[0] = True
1910 1910 populate()
1911 1911 return scmutil.matchfiles(repo, fcache.get(rev, []))
1912 1912
1913 1913 return filematcher
1914 1914
1915 1915 def _makenofollowlogfilematcher(repo, pats, opts):
1916 1916 '''hook for extensions to override the filematcher for non-follow cases'''
1917 1917 return None
1918 1918
1919 1919 def _makelogrevset(repo, pats, opts, revs):
1920 1920 """Return (expr, filematcher) where expr is a revset string built
1921 1921 from log options and file patterns or None. If --stat or --patch
1922 1922 are not passed filematcher is None. Otherwise it is a callable
1923 1923 taking a revision number and returning a match objects filtering
1924 1924 the files to be detailed when displaying the revision.
1925 1925 """
1926 1926 opt2revset = {
1927 1927 'no_merges': ('not merge()', None),
1928 1928 'only_merges': ('merge()', None),
1929 1929 '_ancestors': ('ancestors(%(val)s)', None),
1930 1930 '_fancestors': ('_firstancestors(%(val)s)', None),
1931 1931 '_descendants': ('descendants(%(val)s)', None),
1932 1932 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1933 1933 '_matchfiles': ('_matchfiles(%(val)s)', None),
1934 1934 'date': ('date(%(val)r)', None),
1935 1935 'branch': ('branch(%(val)r)', ' or '),
1936 1936 '_patslog': ('filelog(%(val)r)', ' or '),
1937 1937 '_patsfollow': ('follow(%(val)r)', ' or '),
1938 1938 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1939 1939 'keyword': ('keyword(%(val)r)', ' or '),
1940 1940 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1941 1941 'user': ('user(%(val)r)', ' or '),
1942 1942 }
1943 1943
1944 1944 opts = dict(opts)
1945 1945 # follow or not follow?
1946 1946 follow = opts.get('follow') or opts.get('follow_first')
1947 1947 if opts.get('follow_first'):
1948 1948 followfirst = 1
1949 1949 else:
1950 1950 followfirst = 0
1951 1951 # --follow with FILE behavior depends on revs...
1952 1952 it = iter(revs)
1953 1953 startrev = it.next()
1954 1954 followdescendants = startrev < next(it, startrev)
1955 1955
1956 1956 # branch and only_branch are really aliases and must be handled at
1957 1957 # the same time
1958 1958 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1959 1959 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1960 1960 # pats/include/exclude are passed to match.match() directly in
1961 1961 # _matchfiles() revset but walkchangerevs() builds its matcher with
1962 1962 # scmutil.match(). The difference is input pats are globbed on
1963 1963 # platforms without shell expansion (windows).
1964 1964 wctx = repo[None]
1965 1965 match, pats = scmutil.matchandpats(wctx, pats, opts)
1966 1966 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1967 1967 opts.get('removed'))
1968 1968 if not slowpath:
1969 1969 for f in match.files():
1970 1970 if follow and f not in wctx:
1971 1971 # If the file exists, it may be a directory, so let it
1972 1972 # take the slow path.
1973 1973 if os.path.exists(repo.wjoin(f)):
1974 1974 slowpath = True
1975 1975 continue
1976 1976 else:
1977 1977 raise error.Abort(_('cannot follow file not in parent '
1978 1978 'revision: "%s"') % f)
1979 1979 filelog = repo.file(f)
1980 1980 if not filelog:
1981 1981 # A zero count may be a directory or deleted file, so
1982 1982 # try to find matching entries on the slow path.
1983 1983 if follow:
1984 1984 raise error.Abort(
1985 1985 _('cannot follow nonexistent file: "%s"') % f)
1986 1986 slowpath = True
1987 1987
1988 1988 # We decided to fall back to the slowpath because at least one
1989 1989 # of the paths was not a file. Check to see if at least one of them
1990 1990 # existed in history - in that case, we'll continue down the
1991 1991 # slowpath; otherwise, we can turn off the slowpath
1992 1992 if slowpath:
1993 1993 for path in match.files():
1994 1994 if path == '.' or path in repo.store:
1995 1995 break
1996 1996 else:
1997 1997 slowpath = False
1998 1998
1999 1999 fpats = ('_patsfollow', '_patsfollowfirst')
2000 2000 fnopats = (('_ancestors', '_fancestors'),
2001 2001 ('_descendants', '_fdescendants'))
2002 2002 if slowpath:
2003 2003 # See walkchangerevs() slow path.
2004 2004 #
2005 2005 # pats/include/exclude cannot be represented as separate
2006 2006 # revset expressions as their filtering logic applies at file
2007 2007 # level. For instance "-I a -X a" matches a revision touching
2008 2008 # "a" and "b" while "file(a) and not file(b)" does
2009 2009 # not. Besides, filesets are evaluated against the working
2010 2010 # directory.
2011 2011 matchargs = ['r:', 'd:relpath']
2012 2012 for p in pats:
2013 2013 matchargs.append('p:' + p)
2014 2014 for p in opts.get('include', []):
2015 2015 matchargs.append('i:' + p)
2016 2016 for p in opts.get('exclude', []):
2017 2017 matchargs.append('x:' + p)
2018 2018 matchargs = ','.join(('%r' % p) for p in matchargs)
2019 2019 opts['_matchfiles'] = matchargs
2020 2020 if follow:
2021 2021 opts[fnopats[0][followfirst]] = '.'
2022 2022 else:
2023 2023 if follow:
2024 2024 if pats:
2025 2025 # follow() revset interprets its file argument as a
2026 2026 # manifest entry, so use match.files(), not pats.
2027 2027 opts[fpats[followfirst]] = list(match.files())
2028 2028 else:
2029 2029 op = fnopats[followdescendants][followfirst]
2030 2030 opts[op] = 'rev(%d)' % startrev
2031 2031 else:
2032 2032 opts['_patslog'] = list(pats)
2033 2033
2034 2034 filematcher = None
2035 2035 if opts.get('patch') or opts.get('stat'):
2036 2036 # When following files, track renames via a special matcher.
2037 2037 # If we're forced to take the slowpath it means we're following
2038 2038 # at least one pattern/directory, so don't bother with rename tracking.
2039 2039 if follow and not match.always() and not slowpath:
2040 2040 # _makefollowlogfilematcher expects its files argument to be
2041 2041 # relative to the repo root, so use match.files(), not pats.
2042 2042 filematcher = _makefollowlogfilematcher(repo, match.files(),
2043 2043 followfirst)
2044 2044 else:
2045 2045 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2046 2046 if filematcher is None:
2047 2047 filematcher = lambda rev: match
2048 2048
2049 2049 expr = []
2050 2050 for op, val in sorted(opts.iteritems()):
2051 2051 if not val:
2052 2052 continue
2053 2053 if op not in opt2revset:
2054 2054 continue
2055 2055 revop, andor = opt2revset[op]
2056 2056 if '%(val)' not in revop:
2057 2057 expr.append(revop)
2058 2058 else:
2059 2059 if not isinstance(val, list):
2060 2060 e = revop % {'val': val}
2061 2061 else:
2062 2062 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2063 2063 expr.append(e)
2064 2064
2065 2065 if expr:
2066 2066 expr = '(' + ' and '.join(expr) + ')'
2067 2067 else:
2068 2068 expr = None
2069 2069 return expr, filematcher
2070 2070
2071 2071 def _logrevs(repo, opts):
2072 2072 # Default --rev value depends on --follow but --follow behavior
2073 2073 # depends on revisions resolved from --rev...
2074 2074 follow = opts.get('follow') or opts.get('follow_first')
2075 2075 if opts.get('rev'):
2076 2076 revs = scmutil.revrange(repo, opts['rev'])
2077 2077 elif follow and repo.dirstate.p1() == nullid:
2078 2078 revs = revset.baseset()
2079 2079 elif follow:
2080 2080 revs = repo.revs('reverse(:.)')
2081 2081 else:
2082 2082 revs = revset.spanset(repo)
2083 2083 revs.reverse()
2084 2084 return revs
2085 2085
2086 2086 def getgraphlogrevs(repo, pats, opts):
2087 2087 """Return (revs, expr, filematcher) where revs is an iterable of
2088 2088 revision numbers, expr is a revset string built from log options
2089 2089 and file patterns or None, and used to filter 'revs'. If --stat or
2090 2090 --patch are not passed filematcher is None. Otherwise it is a
2091 2091 callable taking a revision number and returning a match objects
2092 2092 filtering the files to be detailed when displaying the revision.
2093 2093 """
2094 2094 limit = loglimit(opts)
2095 2095 revs = _logrevs(repo, opts)
2096 2096 if not revs:
2097 2097 return revset.baseset(), None, None
2098 2098 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2099 2099 if opts.get('rev'):
2100 2100 # User-specified revs might be unsorted, but don't sort before
2101 2101 # _makelogrevset because it might depend on the order of revs
2102 2102 revs.sort(reverse=True)
2103 2103 if expr:
2104 2104 # Revset matchers often operate faster on revisions in changelog
2105 2105 # order, because most filters deal with the changelog.
2106 2106 revs.reverse()
2107 2107 matcher = revset.match(repo.ui, expr)
2108 2108 # Revset matches can reorder revisions. "A or B" typically returns
2109 2109 # returns the revision matching A then the revision matching B. Sort
2110 2110 # again to fix that.
2111 2111 revs = matcher(repo, revs)
2112 2112 revs.sort(reverse=True)
2113 2113 if limit is not None:
2114 2114 limitedrevs = []
2115 2115 for idx, rev in enumerate(revs):
2116 2116 if idx >= limit:
2117 2117 break
2118 2118 limitedrevs.append(rev)
2119 2119 revs = revset.baseset(limitedrevs)
2120 2120
2121 2121 return revs, expr, filematcher
2122 2122
2123 2123 def getlogrevs(repo, pats, opts):
2124 2124 """Return (revs, expr, filematcher) where revs is an iterable of
2125 2125 revision numbers, expr is a revset string built from log options
2126 2126 and file patterns or None, and used to filter 'revs'. If --stat or
2127 2127 --patch are not passed filematcher is None. Otherwise it is a
2128 2128 callable taking a revision number and returning a match objects
2129 2129 filtering the files to be detailed when displaying the revision.
2130 2130 """
2131 2131 limit = loglimit(opts)
2132 2132 revs = _logrevs(repo, opts)
2133 2133 if not revs:
2134 2134 return revset.baseset([]), None, None
2135 2135 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2136 2136 if expr:
2137 2137 # Revset matchers often operate faster on revisions in changelog
2138 2138 # order, because most filters deal with the changelog.
2139 2139 if not opts.get('rev'):
2140 2140 revs.reverse()
2141 2141 matcher = revset.match(repo.ui, expr)
2142 2142 # Revset matches can reorder revisions. "A or B" typically returns
2143 2143 # returns the revision matching A then the revision matching B. Sort
2144 2144 # again to fix that.
2145 2145 revs = matcher(repo, revs)
2146 2146 if not opts.get('rev'):
2147 2147 revs.sort(reverse=True)
2148 2148 if limit is not None:
2149 2149 limitedrevs = []
2150 2150 for idx, r in enumerate(revs):
2151 2151 if limit <= idx:
2152 2152 break
2153 2153 limitedrevs.append(r)
2154 2154 revs = revset.baseset(limitedrevs)
2155 2155
2156 2156 return revs, expr, filematcher
2157 2157
2158 2158 def _graphnodeformatter(ui, displayer):
2159 2159 spec = ui.config('ui', 'graphnodetemplate')
2160 2160 if not spec:
2161 2161 return templatekw.showgraphnode # fast path for "{graphnode}"
2162 2162
2163 2163 templ = formatter.gettemplater(ui, 'graphnode', spec)
2164 2164 cache = {}
2165 2165 if isinstance(displayer, changeset_templater):
2166 2166 cache = displayer.cache # reuse cache of slow templates
2167 2167 props = templatekw.keywords.copy()
2168 2168 props['templ'] = templ
2169 2169 props['cache'] = cache
2170 2170 def formatnode(repo, ctx):
2171 2171 props['ctx'] = ctx
2172 2172 props['repo'] = repo
2173 2173 props['revcache'] = {}
2174 2174 return templater.stringify(templ('graphnode', **props))
2175 2175 return formatnode
2176 2176
2177 2177 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2178 2178 filematcher=None):
2179 2179 formatnode = _graphnodeformatter(ui, displayer)
2180 2180 seen, state = [], graphmod.asciistate()
2181 2181 for rev, type, ctx, parents in dag:
2182 2182 char = formatnode(repo, ctx)
2183 2183 copies = None
2184 2184 if getrenamed and ctx.rev():
2185 2185 copies = []
2186 2186 for fn in ctx.files():
2187 2187 rename = getrenamed(fn, ctx.rev())
2188 2188 if rename:
2189 2189 copies.append((fn, rename[0]))
2190 2190 revmatchfn = None
2191 2191 if filematcher is not None:
2192 2192 revmatchfn = filematcher(ctx.rev())
2193 2193 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2194 2194 lines = displayer.hunk.pop(rev).split('\n')
2195 2195 if not lines[-1]:
2196 2196 del lines[-1]
2197 2197 displayer.flush(ctx)
2198 2198 edges = edgefn(type, char, lines, seen, rev, parents)
2199 2199 for type, char, lines, coldata in edges:
2200 2200 graphmod.ascii(ui, state, type, char, lines, coldata)
2201 2201 displayer.close()
2202 2202
2203 2203 def graphlog(ui, repo, *pats, **opts):
2204 2204 # Parameters are identical to log command ones
2205 2205 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2206 2206 revdag = graphmod.dagwalker(repo, revs)
2207 2207
2208 2208 getrenamed = None
2209 2209 if opts.get('copies'):
2210 2210 endrev = None
2211 2211 if opts.get('rev'):
2212 2212 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2213 2213 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2214 2214 displayer = show_changeset(ui, repo, opts, buffered=True)
2215 2215 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2216 2216 filematcher)
2217 2217
2218 2218 def checkunsupportedgraphflags(pats, opts):
2219 2219 for op in ["newest_first"]:
2220 2220 if op in opts and opts[op]:
2221 2221 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2222 2222 % op.replace("_", "-"))
2223 2223
2224 2224 def graphrevs(repo, nodes, opts):
2225 2225 limit = loglimit(opts)
2226 2226 nodes.reverse()
2227 2227 if limit is not None:
2228 2228 nodes = nodes[:limit]
2229 2229 return graphmod.nodes(repo, nodes)
2230 2230
2231 2231 def add(ui, repo, match, prefix, explicitonly, **opts):
2232 2232 join = lambda f: os.path.join(prefix, f)
2233 2233 bad = []
2234 2234
2235 2235 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2236 2236 names = []
2237 2237 wctx = repo[None]
2238 2238 cca = None
2239 2239 abort, warn = scmutil.checkportabilityalert(ui)
2240 2240 if abort or warn:
2241 2241 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2242 2242
2243 2243 badmatch = matchmod.badmatch(match, badfn)
2244 2244 dirstate = repo.dirstate
2245 2245 # We don't want to just call wctx.walk here, since it would return a lot of
2246 2246 # clean files, which we aren't interested in and takes time.
2247 2247 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2248 2248 True, False, full=False)):
2249 2249 exact = match.exact(f)
2250 2250 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2251 2251 if cca:
2252 2252 cca(f)
2253 2253 names.append(f)
2254 2254 if ui.verbose or not exact:
2255 2255 ui.status(_('adding %s\n') % match.rel(f))
2256 2256
2257 2257 for subpath in sorted(wctx.substate):
2258 2258 sub = wctx.sub(subpath)
2259 2259 try:
2260 2260 submatch = matchmod.narrowmatcher(subpath, match)
2261 2261 if opts.get('subrepos'):
2262 2262 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2263 2263 else:
2264 2264 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2265 2265 except error.LookupError:
2266 2266 ui.status(_("skipping missing subrepository: %s\n")
2267 2267 % join(subpath))
2268 2268
2269 2269 if not opts.get('dry_run'):
2270 2270 rejected = wctx.add(names, prefix)
2271 2271 bad.extend(f for f in rejected if f in match.files())
2272 2272 return bad
2273 2273
2274 2274 def forget(ui, repo, match, prefix, explicitonly):
2275 2275 join = lambda f: os.path.join(prefix, f)
2276 2276 bad = []
2277 2277 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2278 2278 wctx = repo[None]
2279 2279 forgot = []
2280 2280
2281 2281 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2282 2282 forget = sorted(s[0] + s[1] + s[3] + s[6])
2283 2283 if explicitonly:
2284 2284 forget = [f for f in forget if match.exact(f)]
2285 2285
2286 2286 for subpath in sorted(wctx.substate):
2287 2287 sub = wctx.sub(subpath)
2288 2288 try:
2289 2289 submatch = matchmod.narrowmatcher(subpath, match)
2290 2290 subbad, subforgot = sub.forget(submatch, prefix)
2291 2291 bad.extend([subpath + '/' + f for f in subbad])
2292 2292 forgot.extend([subpath + '/' + f for f in subforgot])
2293 2293 except error.LookupError:
2294 2294 ui.status(_("skipping missing subrepository: %s\n")
2295 2295 % join(subpath))
2296 2296
2297 2297 if not explicitonly:
2298 2298 for f in match.files():
2299 2299 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2300 2300 if f not in forgot:
2301 2301 if repo.wvfs.exists(f):
2302 2302 # Don't complain if the exact case match wasn't given.
2303 2303 # But don't do this until after checking 'forgot', so
2304 2304 # that subrepo files aren't normalized, and this op is
2305 2305 # purely from data cached by the status walk above.
2306 2306 if repo.dirstate.normalize(f) in repo.dirstate:
2307 2307 continue
2308 2308 ui.warn(_('not removing %s: '
2309 2309 'file is already untracked\n')
2310 2310 % match.rel(f))
2311 2311 bad.append(f)
2312 2312
2313 2313 for f in forget:
2314 2314 if ui.verbose or not match.exact(f):
2315 2315 ui.status(_('removing %s\n') % match.rel(f))
2316 2316
2317 2317 rejected = wctx.forget(forget, prefix)
2318 2318 bad.extend(f for f in rejected if f in match.files())
2319 2319 forgot.extend(f for f in forget if f not in rejected)
2320 2320 return bad, forgot
2321 2321
2322 2322 def files(ui, ctx, m, fm, fmt, subrepos):
2323 2323 rev = ctx.rev()
2324 2324 ret = 1
2325 2325 ds = ctx.repo().dirstate
2326 2326
2327 2327 for f in ctx.matches(m):
2328 2328 if rev is None and ds[f] == 'r':
2329 2329 continue
2330 2330 fm.startitem()
2331 2331 if ui.verbose:
2332 2332 fc = ctx[f]
2333 2333 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2334 2334 fm.data(abspath=f)
2335 2335 fm.write('path', fmt, m.rel(f))
2336 2336 ret = 0
2337 2337
2338 2338 for subpath in sorted(ctx.substate):
2339 2339 def matchessubrepo(subpath):
2340 2340 return (m.always() or m.exact(subpath)
2341 2341 or any(f.startswith(subpath + '/') for f in m.files()))
2342 2342
2343 2343 if subrepos or matchessubrepo(subpath):
2344 2344 sub = ctx.sub(subpath)
2345 2345 try:
2346 2346 submatch = matchmod.narrowmatcher(subpath, m)
2347 2347 if sub.printfiles(ui, submatch, fm, fmt, subrepos) == 0:
2348 2348 ret = 0
2349 2349 except error.LookupError:
2350 2350 ui.status(_("skipping missing subrepository: %s\n")
2351 2351 % m.abs(subpath))
2352 2352
2353 2353 return ret
2354 2354
2355 2355 def remove(ui, repo, m, prefix, after, force, subrepos):
2356 2356 join = lambda f: os.path.join(prefix, f)
2357 2357 ret = 0
2358 2358 s = repo.status(match=m, clean=True)
2359 2359 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2360 2360
2361 2361 wctx = repo[None]
2362 2362
2363 2363 for subpath in sorted(wctx.substate):
2364 2364 def matchessubrepo(matcher, subpath):
2365 2365 if matcher.exact(subpath):
2366 2366 return True
2367 2367 for f in matcher.files():
2368 2368 if f.startswith(subpath):
2369 2369 return True
2370 2370 return False
2371 2371
2372 2372 if subrepos or matchessubrepo(m, subpath):
2373 2373 sub = wctx.sub(subpath)
2374 2374 try:
2375 2375 submatch = matchmod.narrowmatcher(subpath, m)
2376 2376 if sub.removefiles(submatch, prefix, after, force, subrepos):
2377 2377 ret = 1
2378 2378 except error.LookupError:
2379 2379 ui.status(_("skipping missing subrepository: %s\n")
2380 2380 % join(subpath))
2381 2381
2382 2382 # warn about failure to delete explicit files/dirs
2383 2383 deleteddirs = util.dirs(deleted)
2384 2384 for f in m.files():
2385 2385 def insubrepo():
2386 2386 for subpath in wctx.substate:
2387 2387 if f.startswith(subpath):
2388 2388 return True
2389 2389 return False
2390 2390
2391 2391 isdir = f in deleteddirs or wctx.hasdir(f)
2392 2392 if f in repo.dirstate or isdir or f == '.' or insubrepo():
2393 2393 continue
2394 2394
2395 2395 if repo.wvfs.exists(f):
2396 2396 if repo.wvfs.isdir(f):
2397 2397 ui.warn(_('not removing %s: no tracked files\n')
2398 2398 % m.rel(f))
2399 2399 else:
2400 2400 ui.warn(_('not removing %s: file is untracked\n')
2401 2401 % m.rel(f))
2402 2402 # missing files will generate a warning elsewhere
2403 2403 ret = 1
2404 2404
2405 2405 if force:
2406 2406 list = modified + deleted + clean + added
2407 2407 elif after:
2408 2408 list = deleted
2409 2409 for f in modified + added + clean:
2410 2410 ui.warn(_('not removing %s: file still exists\n') % m.rel(f))
2411 2411 ret = 1
2412 2412 else:
2413 2413 list = deleted + clean
2414 2414 for f in modified:
2415 2415 ui.warn(_('not removing %s: file is modified (use -f'
2416 2416 ' to force removal)\n') % m.rel(f))
2417 2417 ret = 1
2418 2418 for f in added:
2419 2419 ui.warn(_('not removing %s: file has been marked for add'
2420 2420 ' (use forget to undo)\n') % m.rel(f))
2421 2421 ret = 1
2422 2422
2423 2423 for f in sorted(list):
2424 2424 if ui.verbose or not m.exact(f):
2425 2425 ui.status(_('removing %s\n') % m.rel(f))
2426 2426
2427 2427 wlock = repo.wlock()
2428 2428 try:
2429 2429 if not after:
2430 2430 for f in list:
2431 2431 if f in added:
2432 2432 continue # we never unlink added files on remove
2433 2433 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2434 2434 repo[None].forget(list)
2435 2435 finally:
2436 2436 wlock.release()
2437 2437
2438 2438 return ret
2439 2439
2440 2440 def cat(ui, repo, ctx, matcher, prefix, **opts):
2441 2441 err = 1
2442 2442
2443 2443 def write(path):
2444 2444 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2445 2445 pathname=os.path.join(prefix, path))
2446 2446 data = ctx[path].data()
2447 2447 if opts.get('decode'):
2448 2448 data = repo.wwritedata(path, data)
2449 2449 fp.write(data)
2450 2450 fp.close()
2451 2451
2452 2452 # Automation often uses hg cat on single files, so special case it
2453 2453 # for performance to avoid the cost of parsing the manifest.
2454 2454 if len(matcher.files()) == 1 and not matcher.anypats():
2455 2455 file = matcher.files()[0]
2456 2456 mf = repo.manifest
2457 2457 mfnode = ctx.manifestnode()
2458 2458 if mfnode and mf.find(mfnode, file)[0]:
2459 2459 write(file)
2460 2460 return 0
2461 2461
2462 2462 # Don't warn about "missing" files that are really in subrepos
2463 2463 def badfn(path, msg):
2464 2464 for subpath in ctx.substate:
2465 2465 if path.startswith(subpath):
2466 2466 return
2467 2467 matcher.bad(path, msg)
2468 2468
2469 2469 for abs in ctx.walk(matchmod.badmatch(matcher, badfn)):
2470 2470 write(abs)
2471 2471 err = 0
2472 2472
2473 2473 for subpath in sorted(ctx.substate):
2474 2474 sub = ctx.sub(subpath)
2475 2475 try:
2476 2476 submatch = matchmod.narrowmatcher(subpath, matcher)
2477 2477
2478 2478 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2479 2479 **opts):
2480 2480 err = 0
2481 2481 except error.RepoLookupError:
2482 2482 ui.status(_("skipping missing subrepository: %s\n")
2483 2483 % os.path.join(prefix, subpath))
2484 2484
2485 2485 return err
2486 2486
2487 2487 def commit(ui, repo, commitfunc, pats, opts):
2488 2488 '''commit the specified files or all outstanding changes'''
2489 2489 date = opts.get('date')
2490 2490 if date:
2491 2491 opts['date'] = util.parsedate(date)
2492 2492 message = logmessage(ui, opts)
2493 2493 matcher = scmutil.match(repo[None], pats, opts)
2494 2494
2495 2495 # extract addremove carefully -- this function can be called from a command
2496 2496 # that doesn't support addremove
2497 2497 if opts.get('addremove'):
2498 2498 if scmutil.addremove(repo, matcher, "", opts) != 0:
2499 2499 raise error.Abort(
2500 2500 _("failed to mark all new/missing files as added/removed"))
2501 2501
2502 2502 return commitfunc(ui, repo, message, matcher, opts)
2503 2503
2504 2504 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2505 2505 # avoid cycle context -> subrepo -> cmdutil
2506 2506 import context
2507 2507
2508 2508 # amend will reuse the existing user if not specified, but the obsolete
2509 2509 # marker creation requires that the current user's name is specified.
2510 2510 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2511 2511 ui.username() # raise exception if username not set
2512 2512
2513 2513 ui.note(_('amending changeset %s\n') % old)
2514 2514 base = old.p1()
2515 2515 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2516 2516
2517 2517 wlock = lock = newid = None
2518 2518 try:
2519 2519 wlock = repo.wlock()
2520 2520 lock = repo.lock()
2521 2521 tr = repo.transaction('amend')
2522 2522 try:
2523 2523 # See if we got a message from -m or -l, if not, open the editor
2524 2524 # with the message of the changeset to amend
2525 2525 message = logmessage(ui, opts)
2526 2526 # ensure logfile does not conflict with later enforcement of the
2527 2527 # message. potential logfile content has been processed by
2528 2528 # `logmessage` anyway.
2529 2529 opts.pop('logfile')
2530 2530 # First, do a regular commit to record all changes in the working
2531 2531 # directory (if there are any)
2532 2532 ui.callhooks = False
2533 2533 activebookmark = repo._activebookmark
2534 2534 try:
2535 2535 repo._activebookmark = None
2536 2536 opts['message'] = 'temporary amend commit for %s' % old
2537 2537 node = commit(ui, repo, commitfunc, pats, opts)
2538 2538 finally:
2539 2539 repo._activebookmark = activebookmark
2540 2540 ui.callhooks = True
2541 2541 ctx = repo[node]
2542 2542
2543 2543 # Participating changesets:
2544 2544 #
2545 2545 # node/ctx o - new (intermediate) commit that contains changes
2546 2546 # | from working dir to go into amending commit
2547 2547 # | (or a workingctx if there were no changes)
2548 2548 # |
2549 2549 # old o - changeset to amend
2550 2550 # |
2551 2551 # base o - parent of amending changeset
2552 2552
2553 2553 # Update extra dict from amended commit (e.g. to preserve graft
2554 2554 # source)
2555 2555 extra.update(old.extra())
2556 2556
2557 2557 # Also update it from the intermediate commit or from the wctx
2558 2558 extra.update(ctx.extra())
2559 2559
2560 2560 if len(old.parents()) > 1:
2561 2561 # ctx.files() isn't reliable for merges, so fall back to the
2562 2562 # slower repo.status() method
2563 2563 files = set([fn for st in repo.status(base, old)[:3]
2564 2564 for fn in st])
2565 2565 else:
2566 2566 files = set(old.files())
2567 2567
2568 2568 # Second, we use either the commit we just did, or if there were no
2569 2569 # changes the parent of the working directory as the version of the
2570 2570 # files in the final amend commit
2571 2571 if node:
2572 2572 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2573 2573
2574 2574 user = ctx.user()
2575 2575 date = ctx.date()
2576 2576 # Recompute copies (avoid recording a -> b -> a)
2577 2577 copied = copies.pathcopies(base, ctx)
2578 2578 if old.p2:
2579 2579 copied.update(copies.pathcopies(old.p2(), ctx))
2580 2580
2581 2581 # Prune files which were reverted by the updates: if old
2582 2582 # introduced file X and our intermediate commit, node,
2583 2583 # renamed that file, then those two files are the same and
2584 2584 # we can discard X from our list of files. Likewise if X
2585 2585 # was deleted, it's no longer relevant
2586 2586 files.update(ctx.files())
2587 2587
2588 2588 def samefile(f):
2589 2589 if f in ctx.manifest():
2590 2590 a = ctx.filectx(f)
2591 2591 if f in base.manifest():
2592 2592 b = base.filectx(f)
2593 2593 return (not a.cmp(b)
2594 2594 and a.flags() == b.flags())
2595 2595 else:
2596 2596 return False
2597 2597 else:
2598 2598 return f not in base.manifest()
2599 2599 files = [f for f in files if not samefile(f)]
2600 2600
2601 2601 def filectxfn(repo, ctx_, path):
2602 2602 try:
2603 2603 fctx = ctx[path]
2604 2604 flags = fctx.flags()
2605 2605 mctx = context.memfilectx(repo,
2606 2606 fctx.path(), fctx.data(),
2607 2607 islink='l' in flags,
2608 2608 isexec='x' in flags,
2609 2609 copied=copied.get(path))
2610 2610 return mctx
2611 2611 except KeyError:
2612 2612 return None
2613 2613 else:
2614 2614 ui.note(_('copying changeset %s to %s\n') % (old, base))
2615 2615
2616 2616 # Use version of files as in the old cset
2617 2617 def filectxfn(repo, ctx_, path):
2618 2618 try:
2619 2619 return old.filectx(path)
2620 2620 except KeyError:
2621 2621 return None
2622 2622
2623 2623 user = opts.get('user') or old.user()
2624 2624 date = opts.get('date') or old.date()
2625 2625 editform = mergeeditform(old, 'commit.amend')
2626 2626 editor = getcommiteditor(editform=editform, **opts)
2627 2627 if not message:
2628 2628 editor = getcommiteditor(edit=True, editform=editform)
2629 2629 message = old.description()
2630 2630
2631 2631 pureextra = extra.copy()
2632 2632 if 'amend_source' in pureextra:
2633 2633 del pureextra['amend_source']
2634 2634 pureoldextra = old.extra()
2635 2635 if 'amend_source' in pureoldextra:
2636 2636 del pureoldextra['amend_source']
2637 2637 extra['amend_source'] = old.hex()
2638 2638
2639 2639 new = context.memctx(repo,
2640 2640 parents=[base.node(), old.p2().node()],
2641 2641 text=message,
2642 2642 files=files,
2643 2643 filectxfn=filectxfn,
2644 2644 user=user,
2645 2645 date=date,
2646 2646 extra=extra,
2647 2647 editor=editor)
2648 2648
2649 2649 newdesc = changelog.stripdesc(new.description())
2650 2650 if ((not node)
2651 2651 and newdesc == old.description()
2652 2652 and user == old.user()
2653 2653 and date == old.date()
2654 2654 and pureextra == pureoldextra):
2655 2655 # nothing changed. continuing here would create a new node
2656 2656 # anyway because of the amend_source noise.
2657 2657 #
2658 2658 # This not what we expect from amend.
2659 2659 return old.node()
2660 2660
2661 2661 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2662 2662 try:
2663 2663 if opts.get('secret'):
2664 2664 commitphase = 'secret'
2665 2665 else:
2666 2666 commitphase = old.phase()
2667 2667 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2668 2668 newid = repo.commitctx(new)
2669 2669 finally:
2670 2670 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2671 2671 if newid != old.node():
2672 2672 # Reroute the working copy parent to the new changeset
2673 2673 repo.setparents(newid, nullid)
2674 2674
2675 2675 # Move bookmarks from old parent to amend commit
2676 2676 bms = repo.nodebookmarks(old.node())
2677 2677 if bms:
2678 2678 marks = repo._bookmarks
2679 2679 for bm in bms:
2680 2680 ui.debug('moving bookmarks %r from %s to %s\n' %
2681 2681 (marks, old.hex(), hex(newid)))
2682 2682 marks[bm] = newid
2683 2683 marks.recordchange(tr)
2684 2684 #commit the whole amend process
2685 2685 if createmarkers:
2686 2686 # mark the new changeset as successor of the rewritten one
2687 2687 new = repo[newid]
2688 2688 obs = [(old, (new,))]
2689 2689 if node:
2690 2690 obs.append((ctx, ()))
2691 2691
2692 2692 obsolete.createmarkers(repo, obs)
2693 2693 tr.close()
2694 2694 finally:
2695 2695 tr.release()
2696 2696 if not createmarkers and newid != old.node():
2697 2697 # Strip the intermediate commit (if there was one) and the amended
2698 2698 # commit
2699 2699 if node:
2700 2700 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2701 2701 ui.note(_('stripping amended changeset %s\n') % old)
2702 2702 repair.strip(ui, repo, old.node(), topic='amend-backup')
2703 2703 finally:
2704 2704 lockmod.release(lock, wlock)
2705 2705 return newid
2706 2706
2707 2707 def commiteditor(repo, ctx, subs, editform=''):
2708 2708 if ctx.description():
2709 2709 return ctx.description()
2710 2710 return commitforceeditor(repo, ctx, subs, editform=editform,
2711 2711 unchangedmessagedetection=True)
2712 2712
2713 2713 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2714 2714 editform='', unchangedmessagedetection=False):
2715 2715 if not extramsg:
2716 2716 extramsg = _("Leave message empty to abort commit.")
2717 2717
2718 2718 forms = [e for e in editform.split('.') if e]
2719 2719 forms.insert(0, 'changeset')
2720 2720 templatetext = None
2721 2721 while forms:
2722 2722 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2723 2723 if tmpl:
2724 2724 templatetext = committext = buildcommittemplate(
2725 2725 repo, ctx, subs, extramsg, tmpl)
2726 2726 break
2727 2727 forms.pop()
2728 2728 else:
2729 2729 committext = buildcommittext(repo, ctx, subs, extramsg)
2730 2730
2731 2731 # run editor in the repository root
2732 2732 olddir = os.getcwd()
2733 2733 os.chdir(repo.root)
2734 2734
2735 2735 # make in-memory changes visible to external process
2736 2736 tr = repo.currenttransaction()
2737 2737 repo.dirstate.write(tr)
2738 2738 pending = tr and tr.writepending() and repo.root
2739 2739
2740 2740 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2741 2741 editform=editform, pending=pending)
2742 2742 text = re.sub("(?m)^HG:.*(\n|$)", "", editortext)
2743 2743 os.chdir(olddir)
2744 2744
2745 2745 if finishdesc:
2746 2746 text = finishdesc(text)
2747 2747 if not text.strip():
2748 2748 raise error.Abort(_("empty commit message"))
2749 2749 if unchangedmessagedetection and editortext == templatetext:
2750 2750 raise error.Abort(_("commit message unchanged"))
2751 2751
2752 2752 return text
2753 2753
2754 2754 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2755 2755 ui = repo.ui
2756 2756 tmpl, mapfile = gettemplate(ui, tmpl, None)
2757 2757
2758 2758 try:
2759 2759 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2760 2760 except SyntaxError as inst:
2761 2761 raise error.Abort(inst.args[0])
2762 2762
2763 2763 for k, v in repo.ui.configitems('committemplate'):
2764 2764 if k != 'changeset':
2765 2765 t.t.cache[k] = v
2766 2766
2767 2767 if not extramsg:
2768 2768 extramsg = '' # ensure that extramsg is string
2769 2769
2770 2770 ui.pushbuffer()
2771 2771 t.show(ctx, extramsg=extramsg)
2772 2772 return ui.popbuffer()
2773 2773
2774 2774 def hgprefix(msg):
2775 2775 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2776 2776
2777 2777 def buildcommittext(repo, ctx, subs, extramsg):
2778 2778 edittext = []
2779 2779 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2780 2780 if ctx.description():
2781 2781 edittext.append(ctx.description())
2782 2782 edittext.append("")
2783 2783 edittext.append("") # Empty line between message and comments.
2784 2784 edittext.append(hgprefix(_("Enter commit message."
2785 2785 " Lines beginning with 'HG:' are removed.")))
2786 2786 edittext.append(hgprefix(extramsg))
2787 2787 edittext.append("HG: --")
2788 2788 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2789 2789 if ctx.p2():
2790 2790 edittext.append(hgprefix(_("branch merge")))
2791 2791 if ctx.branch():
2792 2792 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2793 2793 if bookmarks.isactivewdirparent(repo):
2794 2794 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2795 2795 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2796 2796 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2797 2797 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2798 2798 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2799 2799 if not added and not modified and not removed:
2800 2800 edittext.append(hgprefix(_("no files changed")))
2801 2801 edittext.append("")
2802 2802
2803 2803 return "\n".join(edittext)
2804 2804
2805 2805 def commitstatus(repo, node, branch, bheads=None, opts=None):
2806 2806 if opts is None:
2807 2807 opts = {}
2808 2808 ctx = repo[node]
2809 2809 parents = ctx.parents()
2810 2810
2811 2811 if (not opts.get('amend') and bheads and node not in bheads and not
2812 2812 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2813 2813 repo.ui.status(_('created new head\n'))
2814 2814 # The message is not printed for initial roots. For the other
2815 2815 # changesets, it is printed in the following situations:
2816 2816 #
2817 2817 # Par column: for the 2 parents with ...
2818 2818 # N: null or no parent
2819 2819 # B: parent is on another named branch
2820 2820 # C: parent is a regular non head changeset
2821 2821 # H: parent was a branch head of the current branch
2822 2822 # Msg column: whether we print "created new head" message
2823 2823 # In the following, it is assumed that there already exists some
2824 2824 # initial branch heads of the current branch, otherwise nothing is
2825 2825 # printed anyway.
2826 2826 #
2827 2827 # Par Msg Comment
2828 2828 # N N y additional topo root
2829 2829 #
2830 2830 # B N y additional branch root
2831 2831 # C N y additional topo head
2832 2832 # H N n usual case
2833 2833 #
2834 2834 # B B y weird additional branch root
2835 2835 # C B y branch merge
2836 2836 # H B n merge with named branch
2837 2837 #
2838 2838 # C C y additional head from merge
2839 2839 # C H n merge with a head
2840 2840 #
2841 2841 # H H n head merge: head count decreases
2842 2842
2843 2843 if not opts.get('close_branch'):
2844 2844 for r in parents:
2845 2845 if r.closesbranch() and r.branch() == branch:
2846 2846 repo.ui.status(_('reopening closed branch head %d\n') % r)
2847 2847
2848 2848 if repo.ui.debugflag:
2849 2849 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2850 2850 elif repo.ui.verbose:
2851 2851 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2852 2852
2853 2853 def revert(ui, repo, ctx, parents, *pats, **opts):
2854 2854 parent, p2 = parents
2855 2855 node = ctx.node()
2856 2856
2857 2857 mf = ctx.manifest()
2858 2858 if node == p2:
2859 2859 parent = p2
2860 2860 if node == parent:
2861 2861 pmf = mf
2862 2862 else:
2863 2863 pmf = None
2864 2864
2865 2865 # need all matching names in dirstate and manifest of target rev,
2866 2866 # so have to walk both. do not print errors if files exist in one
2867 2867 # but not other. in both cases, filesets should be evaluated against
2868 2868 # workingctx to get consistent result (issue4497). this means 'set:**'
2869 2869 # cannot be used to select missing files from target rev.
2870 2870
2871 2871 # `names` is a mapping for all elements in working copy and target revision
2872 2872 # The mapping is in the form:
2873 2873 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2874 2874 names = {}
2875 2875
2876 2876 wlock = repo.wlock()
2877 2877 try:
2878 2878 ## filling of the `names` mapping
2879 2879 # walk dirstate to fill `names`
2880 2880
2881 2881 interactive = opts.get('interactive', False)
2882 2882 wctx = repo[None]
2883 2883 m = scmutil.match(wctx, pats, opts)
2884 2884
2885 2885 # we'll need this later
2886 2886 targetsubs = sorted(s for s in wctx.substate if m(s))
2887 2887
2888 2888 if not m.always():
2889 2889 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2890 2890 names[abs] = m.rel(abs), m.exact(abs)
2891 2891
2892 2892 # walk target manifest to fill `names`
2893 2893
2894 2894 def badfn(path, msg):
2895 2895 if path in names:
2896 2896 return
2897 2897 if path in ctx.substate:
2898 2898 return
2899 2899 path_ = path + '/'
2900 2900 for f in names:
2901 2901 if f.startswith(path_):
2902 2902 return
2903 2903 ui.warn("%s: %s\n" % (m.rel(path), msg))
2904 2904
2905 2905 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2906 2906 if abs not in names:
2907 2907 names[abs] = m.rel(abs), m.exact(abs)
2908 2908
2909 2909 # Find status of all file in `names`.
2910 2910 m = scmutil.matchfiles(repo, names)
2911 2911
2912 2912 changes = repo.status(node1=node, match=m,
2913 2913 unknown=True, ignored=True, clean=True)
2914 2914 else:
2915 2915 changes = repo.status(node1=node, match=m)
2916 2916 for kind in changes:
2917 2917 for abs in kind:
2918 2918 names[abs] = m.rel(abs), m.exact(abs)
2919 2919
2920 2920 m = scmutil.matchfiles(repo, names)
2921 2921
2922 2922 modified = set(changes.modified)
2923 2923 added = set(changes.added)
2924 2924 removed = set(changes.removed)
2925 2925 _deleted = set(changes.deleted)
2926 2926 unknown = set(changes.unknown)
2927 2927 unknown.update(changes.ignored)
2928 2928 clean = set(changes.clean)
2929 2929 modadded = set()
2930 2930
2931 2931 # split between files known in target manifest and the others
2932 2932 smf = set(mf)
2933 2933
2934 2934 # determine the exact nature of the deleted changesets
2935 2935 deladded = _deleted - smf
2936 2936 deleted = _deleted - deladded
2937 2937
2938 2938 # We need to account for the state of the file in the dirstate,
2939 2939 # even when we revert against something else than parent. This will
2940 2940 # slightly alter the behavior of revert (doing back up or not, delete
2941 2941 # or just forget etc).
2942 2942 if parent == node:
2943 2943 dsmodified = modified
2944 2944 dsadded = added
2945 2945 dsremoved = removed
2946 2946 # store all local modifications, useful later for rename detection
2947 2947 localchanges = dsmodified | dsadded
2948 2948 modified, added, removed = set(), set(), set()
2949 2949 else:
2950 2950 changes = repo.status(node1=parent, match=m)
2951 2951 dsmodified = set(changes.modified)
2952 2952 dsadded = set(changes.added)
2953 2953 dsremoved = set(changes.removed)
2954 2954 # store all local modifications, useful later for rename detection
2955 2955 localchanges = dsmodified | dsadded
2956 2956
2957 2957 # only take into account for removes between wc and target
2958 2958 clean |= dsremoved - removed
2959 2959 dsremoved &= removed
2960 2960 # distinct between dirstate remove and other
2961 2961 removed -= dsremoved
2962 2962
2963 2963 modadded = added & dsmodified
2964 2964 added -= modadded
2965 2965
2966 2966 # tell newly modified apart.
2967 2967 dsmodified &= modified
2968 2968 dsmodified |= modified & dsadded # dirstate added may needs backup
2969 2969 modified -= dsmodified
2970 2970
2971 2971 # We need to wait for some post-processing to update this set
2972 2972 # before making the distinction. The dirstate will be used for
2973 2973 # that purpose.
2974 2974 dsadded = added
2975 2975
2976 2976 # in case of merge, files that are actually added can be reported as
2977 2977 # modified, we need to post process the result
2978 2978 if p2 != nullid:
2979 2979 if pmf is None:
2980 2980 # only need parent manifest in the merge case,
2981 2981 # so do not read by default
2982 2982 pmf = repo[parent].manifest()
2983 2983 mergeadd = dsmodified - set(pmf)
2984 2984 dsadded |= mergeadd
2985 2985 dsmodified -= mergeadd
2986 2986
2987 2987 # if f is a rename, update `names` to also revert the source
2988 2988 cwd = repo.getcwd()
2989 2989 for f in localchanges:
2990 2990 src = repo.dirstate.copied(f)
2991 2991 # XXX should we check for rename down to target node?
2992 2992 if src and src not in names and repo.dirstate[src] == 'r':
2993 2993 dsremoved.add(src)
2994 2994 names[src] = (repo.pathto(src, cwd), True)
2995 2995
2996 2996 # distinguish between file to forget and the other
2997 2997 added = set()
2998 2998 for abs in dsadded:
2999 2999 if repo.dirstate[abs] != 'a':
3000 3000 added.add(abs)
3001 3001 dsadded -= added
3002 3002
3003 3003 for abs in deladded:
3004 3004 if repo.dirstate[abs] == 'a':
3005 3005 dsadded.add(abs)
3006 3006 deladded -= dsadded
3007 3007
3008 3008 # For files marked as removed, we check if an unknown file is present at
3009 3009 # the same path. If a such file exists it may need to be backed up.
3010 3010 # Making the distinction at this stage helps have simpler backup
3011 3011 # logic.
3012 3012 removunk = set()
3013 3013 for abs in removed:
3014 3014 target = repo.wjoin(abs)
3015 3015 if os.path.lexists(target):
3016 3016 removunk.add(abs)
3017 3017 removed -= removunk
3018 3018
3019 3019 dsremovunk = set()
3020 3020 for abs in dsremoved:
3021 3021 target = repo.wjoin(abs)
3022 3022 if os.path.lexists(target):
3023 3023 dsremovunk.add(abs)
3024 3024 dsremoved -= dsremovunk
3025 3025
3026 3026 # action to be actually performed by revert
3027 3027 # (<list of file>, message>) tuple
3028 3028 actions = {'revert': ([], _('reverting %s\n')),
3029 3029 'add': ([], _('adding %s\n')),
3030 3030 'remove': ([], _('removing %s\n')),
3031 3031 'drop': ([], _('removing %s\n')),
3032 3032 'forget': ([], _('forgetting %s\n')),
3033 3033 'undelete': ([], _('undeleting %s\n')),
3034 3034 'noop': (None, _('no changes needed to %s\n')),
3035 3035 'unknown': (None, _('file not managed: %s\n')),
3036 3036 }
3037 3037
3038 3038 # "constant" that convey the backup strategy.
3039 3039 # All set to `discard` if `no-backup` is set do avoid checking
3040 3040 # no_backup lower in the code.
3041 3041 # These values are ordered for comparison purposes
3042 3042 backup = 2 # unconditionally do backup
3043 3043 check = 1 # check if the existing file differs from target
3044 3044 discard = 0 # never do backup
3045 3045 if opts.get('no_backup'):
3046 3046 backup = check = discard
3047 3047
3048 3048 backupanddel = actions['remove']
3049 3049 if not opts.get('no_backup'):
3050 3050 backupanddel = actions['drop']
3051 3051
3052 3052 disptable = (
3053 3053 # dispatch table:
3054 3054 # file state
3055 3055 # action
3056 3056 # make backup
3057 3057
3058 3058 ## Sets that results that will change file on disk
3059 3059 # Modified compared to target, no local change
3060 3060 (modified, actions['revert'], discard),
3061 3061 # Modified compared to target, but local file is deleted
3062 3062 (deleted, actions['revert'], discard),
3063 3063 # Modified compared to target, local change
3064 3064 (dsmodified, actions['revert'], backup),
3065 3065 # Added since target
3066 3066 (added, actions['remove'], discard),
3067 3067 # Added in working directory
3068 3068 (dsadded, actions['forget'], discard),
3069 3069 # Added since target, have local modification
3070 3070 (modadded, backupanddel, backup),
3071 3071 # Added since target but file is missing in working directory
3072 3072 (deladded, actions['drop'], discard),
3073 3073 # Removed since target, before working copy parent
3074 3074 (removed, actions['add'], discard),
3075 3075 # Same as `removed` but an unknown file exists at the same path
3076 3076 (removunk, actions['add'], check),
3077 3077 # Removed since targe, marked as such in working copy parent
3078 3078 (dsremoved, actions['undelete'], discard),
3079 3079 # Same as `dsremoved` but an unknown file exists at the same path
3080 3080 (dsremovunk, actions['undelete'], check),
3081 3081 ## the following sets does not result in any file changes
3082 3082 # File with no modification
3083 3083 (clean, actions['noop'], discard),
3084 3084 # Existing file, not tracked anywhere
3085 3085 (unknown, actions['unknown'], discard),
3086 3086 )
3087 3087
3088 3088 for abs, (rel, exact) in sorted(names.items()):
3089 3089 # target file to be touch on disk (relative to cwd)
3090 3090 target = repo.wjoin(abs)
3091 3091 # search the entry in the dispatch table.
3092 3092 # if the file is in any of these sets, it was touched in the working
3093 3093 # directory parent and we are sure it needs to be reverted.
3094 3094 for table, (xlist, msg), dobackup in disptable:
3095 3095 if abs not in table:
3096 3096 continue
3097 3097 if xlist is not None:
3098 3098 xlist.append(abs)
3099 3099 if dobackup and (backup <= dobackup
3100 3100 or wctx[abs].cmp(ctx[abs])):
3101 bakname = origpath(ui, repo, rel)
3101 bakname = scmutil.origpath(ui, repo, rel)
3102 3102 ui.note(_('saving current version of %s as %s\n') %
3103 3103 (rel, bakname))
3104 3104 if not opts.get('dry_run'):
3105 3105 if interactive:
3106 3106 util.copyfile(target, bakname)
3107 3107 else:
3108 3108 util.rename(target, bakname)
3109 3109 if ui.verbose or not exact:
3110 3110 if not isinstance(msg, basestring):
3111 3111 msg = msg(abs)
3112 3112 ui.status(msg % rel)
3113 3113 elif exact:
3114 3114 ui.warn(msg % rel)
3115 3115 break
3116 3116
3117 3117 if not opts.get('dry_run'):
3118 3118 needdata = ('revert', 'add', 'undelete')
3119 3119 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3120 3120 _performrevert(repo, parents, ctx, actions, interactive)
3121 3121
3122 3122 if targetsubs:
3123 3123 # Revert the subrepos on the revert list
3124 3124 for sub in targetsubs:
3125 3125 try:
3126 3126 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3127 3127 except KeyError:
3128 3128 raise error.Abort("subrepository '%s' does not exist in %s!"
3129 3129 % (sub, short(ctx.node())))
3130 3130 finally:
3131 3131 wlock.release()
3132 3132
3133 def origpath(ui, repo, filepath):
3134 '''customize where .orig files are created
3135
3136 Fetch user defined path from config file: [ui] origbackuppath = <path>
3137 Fall back to default (filepath) if not specified
3138 '''
3139 origbackuppath = ui.config('ui', 'origbackuppath', None)
3140 if origbackuppath is None:
3141 return filepath + ".orig"
3142
3143 filepathfromroot = os.path.relpath(filepath, start=repo.root)
3144 fullorigpath = repo.wjoin(origbackuppath, filepathfromroot)
3145
3146 origbackupdir = repo.vfs.dirname(fullorigpath)
3147 if not repo.vfs.exists(origbackupdir):
3148 ui.note(_('creating directory: %s\n') % origbackupdir)
3149 util.makedirs(origbackupdir)
3150
3151 return fullorigpath + ".orig"
3152
3153 3133 def _revertprefetch(repo, ctx, *files):
3154 3134 """Let extension changing the storage layer prefetch content"""
3155 3135 pass
3156 3136
3157 3137 def _performrevert(repo, parents, ctx, actions, interactive=False):
3158 3138 """function that actually perform all the actions computed for revert
3159 3139
3160 3140 This is an independent function to let extension to plug in and react to
3161 3141 the imminent revert.
3162 3142
3163 3143 Make sure you have the working directory locked when calling this function.
3164 3144 """
3165 3145 parent, p2 = parents
3166 3146 node = ctx.node()
3167 3147 def checkout(f):
3168 3148 fc = ctx[f]
3169 3149 repo.wwrite(f, fc.data(), fc.flags())
3170 3150
3171 3151 audit_path = pathutil.pathauditor(repo.root)
3172 3152 for f in actions['forget'][0]:
3173 3153 repo.dirstate.drop(f)
3174 3154 for f in actions['remove'][0]:
3175 3155 audit_path(f)
3176 3156 try:
3177 3157 util.unlinkpath(repo.wjoin(f))
3178 3158 except OSError:
3179 3159 pass
3180 3160 repo.dirstate.remove(f)
3181 3161 for f in actions['drop'][0]:
3182 3162 audit_path(f)
3183 3163 repo.dirstate.remove(f)
3184 3164
3185 3165 normal = None
3186 3166 if node == parent:
3187 3167 # We're reverting to our parent. If possible, we'd like status
3188 3168 # to report the file as clean. We have to use normallookup for
3189 3169 # merges to avoid losing information about merged/dirty files.
3190 3170 if p2 != nullid:
3191 3171 normal = repo.dirstate.normallookup
3192 3172 else:
3193 3173 normal = repo.dirstate.normal
3194 3174
3195 3175 newlyaddedandmodifiedfiles = set()
3196 3176 if interactive:
3197 3177 # Prompt the user for changes to revert
3198 3178 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3199 3179 m = scmutil.match(ctx, torevert, {})
3200 3180 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3201 3181 diffopts.nodates = True
3202 3182 diffopts.git = True
3203 3183 reversehunks = repo.ui.configbool('experimental',
3204 3184 'revertalternateinteractivemode',
3205 3185 True)
3206 3186 if reversehunks:
3207 3187 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3208 3188 else:
3209 3189 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3210 3190 originalchunks = patch.parsepatch(diff)
3211 3191
3212 3192 try:
3213 3193
3214 3194 chunks, opts = recordfilter(repo.ui, originalchunks)
3215 3195 if reversehunks:
3216 3196 chunks = patch.reversehunks(chunks)
3217 3197
3218 3198 except patch.PatchError as err:
3219 3199 raise error.Abort(_('error parsing patch: %s') % err)
3220 3200
3221 3201 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3222 3202 # Apply changes
3223 3203 fp = cStringIO.StringIO()
3224 3204 for c in chunks:
3225 3205 c.write(fp)
3226 3206 dopatch = fp.tell()
3227 3207 fp.seek(0)
3228 3208 if dopatch:
3229 3209 try:
3230 3210 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3231 3211 except patch.PatchError as err:
3232 3212 raise error.Abort(str(err))
3233 3213 del fp
3234 3214 else:
3235 3215 for f in actions['revert'][0]:
3236 3216 checkout(f)
3237 3217 if normal:
3238 3218 normal(f)
3239 3219
3240 3220 for f in actions['add'][0]:
3241 3221 # Don't checkout modified files, they are already created by the diff
3242 3222 if f not in newlyaddedandmodifiedfiles:
3243 3223 checkout(f)
3244 3224 repo.dirstate.add(f)
3245 3225
3246 3226 normal = repo.dirstate.normallookup
3247 3227 if node == parent and p2 == nullid:
3248 3228 normal = repo.dirstate.normal
3249 3229 for f in actions['undelete'][0]:
3250 3230 checkout(f)
3251 3231 normal(f)
3252 3232
3253 3233 copied = copies.pathcopies(repo[parent], ctx)
3254 3234
3255 3235 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3256 3236 if f in copied:
3257 3237 repo.dirstate.copy(copied[f], f)
3258 3238
3259 3239 def command(table):
3260 3240 """Returns a function object to be used as a decorator for making commands.
3261 3241
3262 3242 This function receives a command table as its argument. The table should
3263 3243 be a dict.
3264 3244
3265 3245 The returned function can be used as a decorator for adding commands
3266 3246 to that command table. This function accepts multiple arguments to define
3267 3247 a command.
3268 3248
3269 3249 The first argument is the command name.
3270 3250
3271 3251 The options argument is an iterable of tuples defining command arguments.
3272 3252 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3273 3253
3274 3254 The synopsis argument defines a short, one line summary of how to use the
3275 3255 command. This shows up in the help output.
3276 3256
3277 3257 The norepo argument defines whether the command does not require a
3278 3258 local repository. Most commands operate against a repository, thus the
3279 3259 default is False.
3280 3260
3281 3261 The optionalrepo argument defines whether the command optionally requires
3282 3262 a local repository.
3283 3263
3284 3264 The inferrepo argument defines whether to try to find a repository from the
3285 3265 command line arguments. If True, arguments will be examined for potential
3286 3266 repository locations. See ``findrepo()``. If a repository is found, it
3287 3267 will be used.
3288 3268 """
3289 3269 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3290 3270 inferrepo=False):
3291 3271 def decorator(func):
3292 3272 if synopsis:
3293 3273 table[name] = func, list(options), synopsis
3294 3274 else:
3295 3275 table[name] = func, list(options)
3296 3276
3297 3277 if norepo:
3298 3278 # Avoid import cycle.
3299 3279 import commands
3300 3280 commands.norepo += ' %s' % ' '.join(parsealiases(name))
3301 3281
3302 3282 if optionalrepo:
3303 3283 import commands
3304 3284 commands.optionalrepo += ' %s' % ' '.join(parsealiases(name))
3305 3285
3306 3286 if inferrepo:
3307 3287 import commands
3308 3288 commands.inferrepo += ' %s' % ' '.join(parsealiases(name))
3309 3289
3310 3290 return func
3311 3291 return decorator
3312 3292
3313 3293 return cmd
3314 3294
3315 3295 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3316 3296 # commands.outgoing. "missing" is "missing" of the result of
3317 3297 # "findcommonoutgoing()"
3318 3298 outgoinghooks = util.hooks()
3319 3299
3320 3300 # a list of (ui, repo) functions called by commands.summary
3321 3301 summaryhooks = util.hooks()
3322 3302
3323 3303 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3324 3304 #
3325 3305 # functions should return tuple of booleans below, if 'changes' is None:
3326 3306 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3327 3307 #
3328 3308 # otherwise, 'changes' is a tuple of tuples below:
3329 3309 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3330 3310 # - (desturl, destbranch, destpeer, outgoing)
3331 3311 summaryremotehooks = util.hooks()
3332 3312
3333 3313 # A list of state files kept by multistep operations like graft.
3334 3314 # Since graft cannot be aborted, it is considered 'clearable' by update.
3335 3315 # note: bisect is intentionally excluded
3336 3316 # (state file, clearable, allowcommit, error, hint)
3337 3317 unfinishedstates = [
3338 3318 ('graftstate', True, False, _('graft in progress'),
3339 3319 _("use 'hg graft --continue' or 'hg update' to abort")),
3340 3320 ('updatestate', True, False, _('last update was interrupted'),
3341 3321 _("use 'hg update' to get a consistent checkout"))
3342 3322 ]
3343 3323
3344 3324 def checkunfinished(repo, commit=False):
3345 3325 '''Look for an unfinished multistep operation, like graft, and abort
3346 3326 if found. It's probably good to check this right before
3347 3327 bailifchanged().
3348 3328 '''
3349 3329 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3350 3330 if commit and allowcommit:
3351 3331 continue
3352 3332 if repo.vfs.exists(f):
3353 3333 raise error.Abort(msg, hint=hint)
3354 3334
3355 3335 def clearunfinished(repo):
3356 3336 '''Check for unfinished operations (as above), and clear the ones
3357 3337 that are clearable.
3358 3338 '''
3359 3339 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3360 3340 if not clearable and repo.vfs.exists(f):
3361 3341 raise error.Abort(msg, hint=hint)
3362 3342 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3363 3343 if clearable and repo.vfs.exists(f):
3364 3344 util.unlink(repo.join(f))
3365 3345
3366 3346 afterresolvedstates = [
3367 3347 ('graftstate',
3368 3348 _('hg graft --continue')),
3369 3349 ]
3370 3350
3371 3351 def checkafterresolved(repo):
3372 3352 contmsg = _("continue: %s\n")
3373 3353 for f, msg in afterresolvedstates:
3374 3354 if repo.vfs.exists(f):
3375 3355 repo.ui.warn(contmsg % msg)
3376 3356 return
3377 3357 repo.ui.note(contmsg % _("hg commit"))
3378 3358
3379 3359 class dirstateguard(object):
3380 3360 '''Restore dirstate at unexpected failure.
3381 3361
3382 3362 At the construction, this class does:
3383 3363
3384 3364 - write current ``repo.dirstate`` out, and
3385 3365 - save ``.hg/dirstate`` into the backup file
3386 3366
3387 3367 This restores ``.hg/dirstate`` from backup file, if ``release()``
3388 3368 is invoked before ``close()``.
3389 3369
3390 3370 This just removes the backup file at ``close()`` before ``release()``.
3391 3371 '''
3392 3372
3393 3373 def __init__(self, repo, name):
3394 3374 self._repo = repo
3395 3375 self._suffix = '.backup.%s.%d' % (name, id(self))
3396 3376 repo.dirstate._savebackup(repo.currenttransaction(), self._suffix)
3397 3377 self._active = True
3398 3378 self._closed = False
3399 3379
3400 3380 def __del__(self):
3401 3381 if self._active: # still active
3402 3382 # this may occur, even if this class is used correctly:
3403 3383 # for example, releasing other resources like transaction
3404 3384 # may raise exception before ``dirstateguard.release`` in
3405 3385 # ``release(tr, ....)``.
3406 3386 self._abort()
3407 3387
3408 3388 def close(self):
3409 3389 if not self._active: # already inactivated
3410 3390 msg = (_("can't close already inactivated backup: dirstate%s")
3411 3391 % self._suffix)
3412 3392 raise error.Abort(msg)
3413 3393
3414 3394 self._repo.dirstate._clearbackup(self._repo.currenttransaction(),
3415 3395 self._suffix)
3416 3396 self._active = False
3417 3397 self._closed = True
3418 3398
3419 3399 def _abort(self):
3420 3400 self._repo.dirstate._restorebackup(self._repo.currenttransaction(),
3421 3401 self._suffix)
3422 3402 self._active = False
3423 3403
3424 3404 def release(self):
3425 3405 if not self._closed:
3426 3406 if not self._active: # already inactivated
3427 3407 msg = (_("can't release already inactivated backup:"
3428 3408 " dirstate%s")
3429 3409 % self._suffix)
3430 3410 raise error.Abort(msg)
3431 3411 self._abort()
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now