##// END OF EJS Templates
strip: make --keep option not set all dirstate times to 0...
Durham Goode -
r18760:e74704c3 default
parent child Browse files
Show More
@@ -1,3587 +1,3602
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 behaviour 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
62 62 from mercurial.i18n import _
63 63 from mercurial.node import bin, hex, short, nullid, nullrev
64 64 from mercurial.lock import release
65 65 from mercurial import commands, cmdutil, hg, scmutil, util, revset
66 66 from mercurial import repair, extensions, error, phases
67 67 from mercurial import patch as patchmod
68 68 import os, re, errno, shutil
69 69
70 70 commands.norepo += " qclone"
71 71
72 72 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
73 73
74 74 cmdtable = {}
75 75 command = cmdutil.command(cmdtable)
76 76 testedwith = 'internal'
77 77
78 78 # Patch names looks like unix-file names.
79 79 # They must be joinable with queue directory and result in the patch path.
80 80 normname = util.normpath
81 81
82 82 class statusentry(object):
83 83 def __init__(self, node, name):
84 84 self.node, self.name = node, name
85 85 def __repr__(self):
86 86 return hex(self.node) + ':' + self.name
87 87
88 88 class patchheader(object):
89 89 def __init__(self, pf, plainmode=False):
90 90 def eatdiff(lines):
91 91 while lines:
92 92 l = lines[-1]
93 93 if (l.startswith("diff -") or
94 94 l.startswith("Index:") or
95 95 l.startswith("===========")):
96 96 del lines[-1]
97 97 else:
98 98 break
99 99 def eatempty(lines):
100 100 while lines:
101 101 if not lines[-1].strip():
102 102 del lines[-1]
103 103 else:
104 104 break
105 105
106 106 message = []
107 107 comments = []
108 108 user = None
109 109 date = None
110 110 parent = None
111 111 format = None
112 112 subject = None
113 113 branch = None
114 114 nodeid = None
115 115 diffstart = 0
116 116
117 117 for line in file(pf):
118 118 line = line.rstrip()
119 119 if (line.startswith('diff --git')
120 120 or (diffstart and line.startswith('+++ '))):
121 121 diffstart = 2
122 122 break
123 123 diffstart = 0 # reset
124 124 if line.startswith("--- "):
125 125 diffstart = 1
126 126 continue
127 127 elif format == "hgpatch":
128 128 # parse values when importing the result of an hg export
129 129 if line.startswith("# User "):
130 130 user = line[7:]
131 131 elif line.startswith("# Date "):
132 132 date = line[7:]
133 133 elif line.startswith("# Parent "):
134 134 parent = line[9:].lstrip()
135 135 elif line.startswith("# Branch "):
136 136 branch = line[9:]
137 137 elif line.startswith("# Node ID "):
138 138 nodeid = line[10:]
139 139 elif not line.startswith("# ") and line:
140 140 message.append(line)
141 141 format = None
142 142 elif line == '# HG changeset patch':
143 143 message = []
144 144 format = "hgpatch"
145 145 elif (format != "tagdone" and (line.startswith("Subject: ") or
146 146 line.startswith("subject: "))):
147 147 subject = line[9:]
148 148 format = "tag"
149 149 elif (format != "tagdone" and (line.startswith("From: ") or
150 150 line.startswith("from: "))):
151 151 user = line[6:]
152 152 format = "tag"
153 153 elif (format != "tagdone" and (line.startswith("Date: ") or
154 154 line.startswith("date: "))):
155 155 date = line[6:]
156 156 format = "tag"
157 157 elif format == "tag" and line == "":
158 158 # when looking for tags (subject: from: etc) they
159 159 # end once you find a blank line in the source
160 160 format = "tagdone"
161 161 elif message or line:
162 162 message.append(line)
163 163 comments.append(line)
164 164
165 165 eatdiff(message)
166 166 eatdiff(comments)
167 167 # Remember the exact starting line of the patch diffs before consuming
168 168 # empty lines, for external use by TortoiseHg and others
169 169 self.diffstartline = len(comments)
170 170 eatempty(message)
171 171 eatempty(comments)
172 172
173 173 # make sure message isn't empty
174 174 if format and format.startswith("tag") and subject:
175 175 message.insert(0, "")
176 176 message.insert(0, subject)
177 177
178 178 self.message = message
179 179 self.comments = comments
180 180 self.user = user
181 181 self.date = date
182 182 self.parent = parent
183 183 # nodeid and branch are for external use by TortoiseHg and others
184 184 self.nodeid = nodeid
185 185 self.branch = branch
186 186 self.haspatch = diffstart > 1
187 187 self.plainmode = plainmode
188 188
189 189 def setuser(self, user):
190 190 if not self.updateheader(['From: ', '# User '], user):
191 191 try:
192 192 patchheaderat = self.comments.index('# HG changeset patch')
193 193 self.comments.insert(patchheaderat + 1, '# User ' + user)
194 194 except ValueError:
195 195 if self.plainmode or self._hasheader(['Date: ']):
196 196 self.comments = ['From: ' + user] + self.comments
197 197 else:
198 198 tmp = ['# HG changeset patch', '# User ' + user, '']
199 199 self.comments = tmp + self.comments
200 200 self.user = user
201 201
202 202 def setdate(self, date):
203 203 if not self.updateheader(['Date: ', '# Date '], date):
204 204 try:
205 205 patchheaderat = self.comments.index('# HG changeset patch')
206 206 self.comments.insert(patchheaderat + 1, '# Date ' + date)
207 207 except ValueError:
208 208 if self.plainmode or self._hasheader(['From: ']):
209 209 self.comments = ['Date: ' + date] + self.comments
210 210 else:
211 211 tmp = ['# HG changeset patch', '# Date ' + date, '']
212 212 self.comments = tmp + self.comments
213 213 self.date = date
214 214
215 215 def setparent(self, parent):
216 216 if not self.updateheader(['# Parent '], parent):
217 217 try:
218 218 patchheaderat = self.comments.index('# HG changeset patch')
219 219 self.comments.insert(patchheaderat + 1, '# Parent ' + parent)
220 220 except ValueError:
221 221 pass
222 222 self.parent = parent
223 223
224 224 def setmessage(self, message):
225 225 if self.comments:
226 226 self._delmsg()
227 227 self.message = [message]
228 228 self.comments += self.message
229 229
230 230 def updateheader(self, prefixes, new):
231 231 '''Update all references to a field in the patch header.
232 232 Return whether the field is present.'''
233 233 res = False
234 234 for prefix in prefixes:
235 235 for i in xrange(len(self.comments)):
236 236 if self.comments[i].startswith(prefix):
237 237 self.comments[i] = prefix + new
238 238 res = True
239 239 break
240 240 return res
241 241
242 242 def _hasheader(self, prefixes):
243 243 '''Check if a header starts with any of the given prefixes.'''
244 244 for prefix in prefixes:
245 245 for comment in self.comments:
246 246 if comment.startswith(prefix):
247 247 return True
248 248 return False
249 249
250 250 def __str__(self):
251 251 if not self.comments:
252 252 return ''
253 253 return '\n'.join(self.comments) + '\n\n'
254 254
255 255 def _delmsg(self):
256 256 '''Remove existing message, keeping the rest of the comments fields.
257 257 If comments contains 'subject: ', message will prepend
258 258 the field and a blank line.'''
259 259 if self.message:
260 260 subj = 'subject: ' + self.message[0].lower()
261 261 for i in xrange(len(self.comments)):
262 262 if subj == self.comments[i].lower():
263 263 del self.comments[i]
264 264 self.message = self.message[2:]
265 265 break
266 266 ci = 0
267 267 for mi in self.message:
268 268 while mi != self.comments[ci]:
269 269 ci += 1
270 270 del self.comments[ci]
271 271
272 272 def newcommit(repo, phase, *args, **kwargs):
273 273 """helper dedicated to ensure a commit respect mq.secret setting
274 274
275 275 It should be used instead of repo.commit inside the mq source for operation
276 276 creating new changeset.
277 277 """
278 278 repo = repo.unfiltered()
279 279 if phase is None:
280 280 if repo.ui.configbool('mq', 'secret', False):
281 281 phase = phases.secret
282 282 if phase is not None:
283 283 backup = repo.ui.backupconfig('phases', 'new-commit')
284 284 # Marking the repository as committing an mq patch can be used
285 285 # to optimize operations like branchtags().
286 286 repo._committingpatch = True
287 287 try:
288 288 if phase is not None:
289 289 repo.ui.setconfig('phases', 'new-commit', phase)
290 290 return repo.commit(*args, **kwargs)
291 291 finally:
292 292 repo._committingpatch = False
293 293 if phase is not None:
294 294 repo.ui.restoreconfig(backup)
295 295
296 296 class AbortNoCleanup(error.Abort):
297 297 pass
298 298
299 299 class queue(object):
300 300 def __init__(self, ui, path, patchdir=None):
301 301 self.basepath = path
302 302 try:
303 303 fh = open(os.path.join(path, 'patches.queue'))
304 304 cur = fh.read().rstrip()
305 305 fh.close()
306 306 if not cur:
307 307 curpath = os.path.join(path, 'patches')
308 308 else:
309 309 curpath = os.path.join(path, 'patches-' + cur)
310 310 except IOError:
311 311 curpath = os.path.join(path, 'patches')
312 312 self.path = patchdir or curpath
313 313 self.opener = scmutil.opener(self.path)
314 314 self.ui = ui
315 315 self.applieddirty = False
316 316 self.seriesdirty = False
317 317 self.added = []
318 318 self.seriespath = "series"
319 319 self.statuspath = "status"
320 320 self.guardspath = "guards"
321 321 self.activeguards = None
322 322 self.guardsdirty = False
323 323 # Handle mq.git as a bool with extended values
324 324 try:
325 325 gitmode = ui.configbool('mq', 'git', None)
326 326 if gitmode is None:
327 327 raise error.ConfigError
328 328 self.gitmode = gitmode and 'yes' or 'no'
329 329 except error.ConfigError:
330 330 self.gitmode = ui.config('mq', 'git', 'auto').lower()
331 331 self.plainmode = ui.configbool('mq', 'plain', False)
332 332
333 333 @util.propertycache
334 334 def applied(self):
335 335 def parselines(lines):
336 336 for l in lines:
337 337 entry = l.split(':', 1)
338 338 if len(entry) > 1:
339 339 n, name = entry
340 340 yield statusentry(bin(n), name)
341 341 elif l.strip():
342 342 self.ui.warn(_('malformated mq status line: %s\n') % entry)
343 343 # else we ignore empty lines
344 344 try:
345 345 lines = self.opener.read(self.statuspath).splitlines()
346 346 return list(parselines(lines))
347 347 except IOError, e:
348 348 if e.errno == errno.ENOENT:
349 349 return []
350 350 raise
351 351
352 352 @util.propertycache
353 353 def fullseries(self):
354 354 try:
355 355 return self.opener.read(self.seriespath).splitlines()
356 356 except IOError, e:
357 357 if e.errno == errno.ENOENT:
358 358 return []
359 359 raise
360 360
361 361 @util.propertycache
362 362 def series(self):
363 363 self.parseseries()
364 364 return self.series
365 365
366 366 @util.propertycache
367 367 def seriesguards(self):
368 368 self.parseseries()
369 369 return self.seriesguards
370 370
371 371 def invalidate(self):
372 372 for a in 'applied fullseries series seriesguards'.split():
373 373 if a in self.__dict__:
374 374 delattr(self, a)
375 375 self.applieddirty = False
376 376 self.seriesdirty = False
377 377 self.guardsdirty = False
378 378 self.activeguards = None
379 379
380 380 def diffopts(self, opts={}, patchfn=None):
381 381 diffopts = patchmod.diffopts(self.ui, opts)
382 382 if self.gitmode == 'auto':
383 383 diffopts.upgrade = True
384 384 elif self.gitmode == 'keep':
385 385 pass
386 386 elif self.gitmode in ('yes', 'no'):
387 387 diffopts.git = self.gitmode == 'yes'
388 388 else:
389 389 raise util.Abort(_('mq.git option can be auto/keep/yes/no'
390 390 ' got %s') % self.gitmode)
391 391 if patchfn:
392 392 diffopts = self.patchopts(diffopts, patchfn)
393 393 return diffopts
394 394
395 395 def patchopts(self, diffopts, *patches):
396 396 """Return a copy of input diff options with git set to true if
397 397 referenced patch is a git patch and should be preserved as such.
398 398 """
399 399 diffopts = diffopts.copy()
400 400 if not diffopts.git and self.gitmode == 'keep':
401 401 for patchfn in patches:
402 402 patchf = self.opener(patchfn, 'r')
403 403 # if the patch was a git patch, refresh it as a git patch
404 404 for line in patchf:
405 405 if line.startswith('diff --git'):
406 406 diffopts.git = True
407 407 break
408 408 patchf.close()
409 409 return diffopts
410 410
411 411 def join(self, *p):
412 412 return os.path.join(self.path, *p)
413 413
414 414 def findseries(self, patch):
415 415 def matchpatch(l):
416 416 l = l.split('#', 1)[0]
417 417 return l.strip() == patch
418 418 for index, l in enumerate(self.fullseries):
419 419 if matchpatch(l):
420 420 return index
421 421 return None
422 422
423 423 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
424 424
425 425 def parseseries(self):
426 426 self.series = []
427 427 self.seriesguards = []
428 428 for l in self.fullseries:
429 429 h = l.find('#')
430 430 if h == -1:
431 431 patch = l
432 432 comment = ''
433 433 elif h == 0:
434 434 continue
435 435 else:
436 436 patch = l[:h]
437 437 comment = l[h:]
438 438 patch = patch.strip()
439 439 if patch:
440 440 if patch in self.series:
441 441 raise util.Abort(_('%s appears more than once in %s') %
442 442 (patch, self.join(self.seriespath)))
443 443 self.series.append(patch)
444 444 self.seriesguards.append(self.guard_re.findall(comment))
445 445
446 446 def checkguard(self, guard):
447 447 if not guard:
448 448 return _('guard cannot be an empty string')
449 449 bad_chars = '# \t\r\n\f'
450 450 first = guard[0]
451 451 if first in '-+':
452 452 return (_('guard %r starts with invalid character: %r') %
453 453 (guard, first))
454 454 for c in bad_chars:
455 455 if c in guard:
456 456 return _('invalid character in guard %r: %r') % (guard, c)
457 457
458 458 def setactive(self, guards):
459 459 for guard in guards:
460 460 bad = self.checkguard(guard)
461 461 if bad:
462 462 raise util.Abort(bad)
463 463 guards = sorted(set(guards))
464 464 self.ui.debug('active guards: %s\n' % ' '.join(guards))
465 465 self.activeguards = guards
466 466 self.guardsdirty = True
467 467
468 468 def active(self):
469 469 if self.activeguards is None:
470 470 self.activeguards = []
471 471 try:
472 472 guards = self.opener.read(self.guardspath).split()
473 473 except IOError, err:
474 474 if err.errno != errno.ENOENT:
475 475 raise
476 476 guards = []
477 477 for i, guard in enumerate(guards):
478 478 bad = self.checkguard(guard)
479 479 if bad:
480 480 self.ui.warn('%s:%d: %s\n' %
481 481 (self.join(self.guardspath), i + 1, bad))
482 482 else:
483 483 self.activeguards.append(guard)
484 484 return self.activeguards
485 485
486 486 def setguards(self, idx, guards):
487 487 for g in guards:
488 488 if len(g) < 2:
489 489 raise util.Abort(_('guard %r too short') % g)
490 490 if g[0] not in '-+':
491 491 raise util.Abort(_('guard %r starts with invalid char') % g)
492 492 bad = self.checkguard(g[1:])
493 493 if bad:
494 494 raise util.Abort(bad)
495 495 drop = self.guard_re.sub('', self.fullseries[idx])
496 496 self.fullseries[idx] = drop + ''.join([' #' + g for g in guards])
497 497 self.parseseries()
498 498 self.seriesdirty = True
499 499
500 500 def pushable(self, idx):
501 501 if isinstance(idx, str):
502 502 idx = self.series.index(idx)
503 503 patchguards = self.seriesguards[idx]
504 504 if not patchguards:
505 505 return True, None
506 506 guards = self.active()
507 507 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
508 508 if exactneg:
509 509 return False, repr(exactneg[0])
510 510 pos = [g for g in patchguards if g[0] == '+']
511 511 exactpos = [g for g in pos if g[1:] in guards]
512 512 if pos:
513 513 if exactpos:
514 514 return True, repr(exactpos[0])
515 515 return False, ' '.join(map(repr, pos))
516 516 return True, ''
517 517
518 518 def explainpushable(self, idx, all_patches=False):
519 519 write = all_patches and self.ui.write or self.ui.warn
520 520 if all_patches or self.ui.verbose:
521 521 if isinstance(idx, str):
522 522 idx = self.series.index(idx)
523 523 pushable, why = self.pushable(idx)
524 524 if all_patches and pushable:
525 525 if why is None:
526 526 write(_('allowing %s - no guards in effect\n') %
527 527 self.series[idx])
528 528 else:
529 529 if not why:
530 530 write(_('allowing %s - no matching negative guards\n') %
531 531 self.series[idx])
532 532 else:
533 533 write(_('allowing %s - guarded by %s\n') %
534 534 (self.series[idx], why))
535 535 if not pushable:
536 536 if why:
537 537 write(_('skipping %s - guarded by %s\n') %
538 538 (self.series[idx], why))
539 539 else:
540 540 write(_('skipping %s - no matching guards\n') %
541 541 self.series[idx])
542 542
543 543 def savedirty(self):
544 544 def writelist(items, path):
545 545 fp = self.opener(path, 'w')
546 546 for i in items:
547 547 fp.write("%s\n" % i)
548 548 fp.close()
549 549 if self.applieddirty:
550 550 writelist(map(str, self.applied), self.statuspath)
551 551 self.applieddirty = False
552 552 if self.seriesdirty:
553 553 writelist(self.fullseries, self.seriespath)
554 554 self.seriesdirty = False
555 555 if self.guardsdirty:
556 556 writelist(self.activeguards, self.guardspath)
557 557 self.guardsdirty = False
558 558 if self.added:
559 559 qrepo = self.qrepo()
560 560 if qrepo:
561 561 qrepo[None].add(f for f in self.added if f not in qrepo[None])
562 562 self.added = []
563 563
564 564 def removeundo(self, repo):
565 565 undo = repo.sjoin('undo')
566 566 if not os.path.exists(undo):
567 567 return
568 568 try:
569 569 os.unlink(undo)
570 570 except OSError, inst:
571 571 self.ui.warn(_('error removing undo: %s\n') % str(inst))
572 572
573 573 def backup(self, repo, files, copy=False):
574 574 # backup local changes in --force case
575 575 for f in sorted(files):
576 576 absf = repo.wjoin(f)
577 577 if os.path.lexists(absf):
578 578 self.ui.note(_('saving current version of %s as %s\n') %
579 579 (f, f + '.orig'))
580 580 if copy:
581 581 util.copyfile(absf, absf + '.orig')
582 582 else:
583 583 util.rename(absf, absf + '.orig')
584 584
585 585 def printdiff(self, repo, diffopts, node1, node2=None, files=None,
586 586 fp=None, changes=None, opts={}):
587 587 stat = opts.get('stat')
588 588 m = scmutil.match(repo[node1], files, opts)
589 589 cmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m,
590 590 changes, stat, fp)
591 591
592 592 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
593 593 # first try just applying the patch
594 594 (err, n) = self.apply(repo, [patch], update_status=False,
595 595 strict=True, merge=rev)
596 596
597 597 if err == 0:
598 598 return (err, n)
599 599
600 600 if n is None:
601 601 raise util.Abort(_("apply failed for patch %s") % patch)
602 602
603 603 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
604 604
605 605 # apply failed, strip away that rev and merge.
606 606 hg.clean(repo, head)
607 607 self.strip(repo, [n], update=False, backup='strip')
608 608
609 609 ctx = repo[rev]
610 610 ret = hg.merge(repo, rev)
611 611 if ret:
612 612 raise util.Abort(_("update returned %d") % ret)
613 613 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
614 614 if n is None:
615 615 raise util.Abort(_("repo commit failed"))
616 616 try:
617 617 ph = patchheader(mergeq.join(patch), self.plainmode)
618 618 except Exception:
619 619 raise util.Abort(_("unable to read %s") % patch)
620 620
621 621 diffopts = self.patchopts(diffopts, patch)
622 622 patchf = self.opener(patch, "w")
623 623 comments = str(ph)
624 624 if comments:
625 625 patchf.write(comments)
626 626 self.printdiff(repo, diffopts, head, n, fp=patchf)
627 627 patchf.close()
628 628 self.removeundo(repo)
629 629 return (0, n)
630 630
631 631 def qparents(self, repo, rev=None):
632 632 if rev is None:
633 633 (p1, p2) = repo.dirstate.parents()
634 634 if p2 == nullid:
635 635 return p1
636 636 if not self.applied:
637 637 return None
638 638 return self.applied[-1].node
639 639 p1, p2 = repo.changelog.parents(rev)
640 640 if p2 != nullid and p2 in [x.node for x in self.applied]:
641 641 return p2
642 642 return p1
643 643
644 644 def mergepatch(self, repo, mergeq, series, diffopts):
645 645 if not self.applied:
646 646 # each of the patches merged in will have two parents. This
647 647 # can confuse the qrefresh, qdiff, and strip code because it
648 648 # needs to know which parent is actually in the patch queue.
649 649 # so, we insert a merge marker with only one parent. This way
650 650 # the first patch in the queue is never a merge patch
651 651 #
652 652 pname = ".hg.patches.merge.marker"
653 653 n = newcommit(repo, None, '[mq]: merge marker', force=True)
654 654 self.removeundo(repo)
655 655 self.applied.append(statusentry(n, pname))
656 656 self.applieddirty = True
657 657
658 658 head = self.qparents(repo)
659 659
660 660 for patch in series:
661 661 patch = mergeq.lookup(patch, strict=True)
662 662 if not patch:
663 663 self.ui.warn(_("patch %s does not exist\n") % patch)
664 664 return (1, None)
665 665 pushable, reason = self.pushable(patch)
666 666 if not pushable:
667 667 self.explainpushable(patch, all_patches=True)
668 668 continue
669 669 info = mergeq.isapplied(patch)
670 670 if not info:
671 671 self.ui.warn(_("patch %s is not applied\n") % patch)
672 672 return (1, None)
673 673 rev = info[1]
674 674 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
675 675 if head:
676 676 self.applied.append(statusentry(head, patch))
677 677 self.applieddirty = True
678 678 if err:
679 679 return (err, head)
680 680 self.savedirty()
681 681 return (0, head)
682 682
683 683 def patch(self, repo, patchfile):
684 684 '''Apply patchfile to the working directory.
685 685 patchfile: name of patch file'''
686 686 files = set()
687 687 try:
688 688 fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
689 689 files=files, eolmode=None)
690 690 return (True, list(files), fuzz)
691 691 except Exception, inst:
692 692 self.ui.note(str(inst) + '\n')
693 693 if not self.ui.verbose:
694 694 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
695 695 self.ui.traceback()
696 696 return (False, list(files), False)
697 697
698 698 def apply(self, repo, series, list=False, update_status=True,
699 699 strict=False, patchdir=None, merge=None, all_files=None,
700 700 tobackup=None, keepchanges=False):
701 701 wlock = lock = tr = None
702 702 try:
703 703 wlock = repo.wlock()
704 704 lock = repo.lock()
705 705 tr = repo.transaction("qpush")
706 706 try:
707 707 ret = self._apply(repo, series, list, update_status,
708 708 strict, patchdir, merge, all_files=all_files,
709 709 tobackup=tobackup, keepchanges=keepchanges)
710 710 tr.close()
711 711 self.savedirty()
712 712 return ret
713 713 except AbortNoCleanup:
714 714 tr.close()
715 715 self.savedirty()
716 716 return 2, repo.dirstate.p1()
717 717 except: # re-raises
718 718 try:
719 719 tr.abort()
720 720 finally:
721 721 repo.invalidate()
722 722 repo.dirstate.invalidate()
723 723 self.invalidate()
724 724 raise
725 725 finally:
726 726 release(tr, lock, wlock)
727 727 self.removeundo(repo)
728 728
729 729 def _apply(self, repo, series, list=False, update_status=True,
730 730 strict=False, patchdir=None, merge=None, all_files=None,
731 731 tobackup=None, keepchanges=False):
732 732 """returns (error, hash)
733 733
734 734 error = 1 for unable to read, 2 for patch failed, 3 for patch
735 735 fuzz. tobackup is None or a set of files to backup before they
736 736 are modified by a patch.
737 737 """
738 738 # TODO unify with commands.py
739 739 if not patchdir:
740 740 patchdir = self.path
741 741 err = 0
742 742 n = None
743 743 for patchname in series:
744 744 pushable, reason = self.pushable(patchname)
745 745 if not pushable:
746 746 self.explainpushable(patchname, all_patches=True)
747 747 continue
748 748 self.ui.status(_("applying %s\n") % patchname)
749 749 pf = os.path.join(patchdir, patchname)
750 750
751 751 try:
752 752 ph = patchheader(self.join(patchname), self.plainmode)
753 753 except IOError:
754 754 self.ui.warn(_("unable to read %s\n") % patchname)
755 755 err = 1
756 756 break
757 757
758 758 message = ph.message
759 759 if not message:
760 760 # The commit message should not be translated
761 761 message = "imported patch %s\n" % patchname
762 762 else:
763 763 if list:
764 764 # The commit message should not be translated
765 765 message.append("\nimported patch %s" % patchname)
766 766 message = '\n'.join(message)
767 767
768 768 if ph.haspatch:
769 769 if tobackup:
770 770 touched = patchmod.changedfiles(self.ui, repo, pf)
771 771 touched = set(touched) & tobackup
772 772 if touched and keepchanges:
773 773 raise AbortNoCleanup(
774 774 _("local changes found, refresh first"))
775 775 self.backup(repo, touched, copy=True)
776 776 tobackup = tobackup - touched
777 777 (patcherr, files, fuzz) = self.patch(repo, pf)
778 778 if all_files is not None:
779 779 all_files.update(files)
780 780 patcherr = not patcherr
781 781 else:
782 782 self.ui.warn(_("patch %s is empty\n") % patchname)
783 783 patcherr, files, fuzz = 0, [], 0
784 784
785 785 if merge and files:
786 786 # Mark as removed/merged and update dirstate parent info
787 787 removed = []
788 788 merged = []
789 789 for f in files:
790 790 if os.path.lexists(repo.wjoin(f)):
791 791 merged.append(f)
792 792 else:
793 793 removed.append(f)
794 794 for f in removed:
795 795 repo.dirstate.remove(f)
796 796 for f in merged:
797 797 repo.dirstate.merge(f)
798 798 p1, p2 = repo.dirstate.parents()
799 799 repo.setparents(p1, merge)
800 800
801 801 match = scmutil.matchfiles(repo, files or [])
802 802 oldtip = repo['tip']
803 803 n = newcommit(repo, None, message, ph.user, ph.date, match=match,
804 804 force=True)
805 805 if repo['tip'] == oldtip:
806 806 raise util.Abort(_("qpush exactly duplicates child changeset"))
807 807 if n is None:
808 808 raise util.Abort(_("repository commit failed"))
809 809
810 810 if update_status:
811 811 self.applied.append(statusentry(n, patchname))
812 812
813 813 if patcherr:
814 814 self.ui.warn(_("patch failed, rejects left in working dir\n"))
815 815 err = 2
816 816 break
817 817
818 818 if fuzz and strict:
819 819 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
820 820 err = 3
821 821 break
822 822 return (err, n)
823 823
824 824 def _cleanup(self, patches, numrevs, keep=False):
825 825 if not keep:
826 826 r = self.qrepo()
827 827 if r:
828 828 r[None].forget(patches)
829 829 for p in patches:
830 830 try:
831 831 os.unlink(self.join(p))
832 832 except OSError, inst:
833 833 if inst.errno != errno.ENOENT:
834 834 raise
835 835
836 836 qfinished = []
837 837 if numrevs:
838 838 qfinished = self.applied[:numrevs]
839 839 del self.applied[:numrevs]
840 840 self.applieddirty = True
841 841
842 842 unknown = []
843 843
844 844 for (i, p) in sorted([(self.findseries(p), p) for p in patches],
845 845 reverse=True):
846 846 if i is not None:
847 847 del self.fullseries[i]
848 848 else:
849 849 unknown.append(p)
850 850
851 851 if unknown:
852 852 if numrevs:
853 853 rev = dict((entry.name, entry.node) for entry in qfinished)
854 854 for p in unknown:
855 855 msg = _('revision %s refers to unknown patches: %s\n')
856 856 self.ui.warn(msg % (short(rev[p]), p))
857 857 else:
858 858 msg = _('unknown patches: %s\n')
859 859 raise util.Abort(''.join(msg % p for p in unknown))
860 860
861 861 self.parseseries()
862 862 self.seriesdirty = True
863 863 return [entry.node for entry in qfinished]
864 864
865 865 def _revpatches(self, repo, revs):
866 866 firstrev = repo[self.applied[0].node].rev()
867 867 patches = []
868 868 for i, rev in enumerate(revs):
869 869
870 870 if rev < firstrev:
871 871 raise util.Abort(_('revision %d is not managed') % rev)
872 872
873 873 ctx = repo[rev]
874 874 base = self.applied[i].node
875 875 if ctx.node() != base:
876 876 msg = _('cannot delete revision %d above applied patches')
877 877 raise util.Abort(msg % rev)
878 878
879 879 patch = self.applied[i].name
880 880 for fmt in ('[mq]: %s', 'imported patch %s'):
881 881 if ctx.description() == fmt % patch:
882 882 msg = _('patch %s finalized without changeset message\n')
883 883 repo.ui.status(msg % patch)
884 884 break
885 885
886 886 patches.append(patch)
887 887 return patches
888 888
889 889 def finish(self, repo, revs):
890 890 # Manually trigger phase computation to ensure phasedefaults is
891 891 # executed before we remove the patches.
892 892 repo._phasecache
893 893 patches = self._revpatches(repo, sorted(revs))
894 894 qfinished = self._cleanup(patches, len(patches))
895 895 if qfinished and repo.ui.configbool('mq', 'secret', False):
896 896 # only use this logic when the secret option is added
897 897 oldqbase = repo[qfinished[0]]
898 898 tphase = repo.ui.config('phases', 'new-commit', phases.draft)
899 899 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
900 900 phases.advanceboundary(repo, tphase, qfinished)
901 901
902 902 def delete(self, repo, patches, opts):
903 903 if not patches and not opts.get('rev'):
904 904 raise util.Abort(_('qdelete requires at least one revision or '
905 905 'patch name'))
906 906
907 907 realpatches = []
908 908 for patch in patches:
909 909 patch = self.lookup(patch, strict=True)
910 910 info = self.isapplied(patch)
911 911 if info:
912 912 raise util.Abort(_("cannot delete applied patch %s") % patch)
913 913 if patch not in self.series:
914 914 raise util.Abort(_("patch %s not in series file") % patch)
915 915 if patch not in realpatches:
916 916 realpatches.append(patch)
917 917
918 918 numrevs = 0
919 919 if opts.get('rev'):
920 920 if not self.applied:
921 921 raise util.Abort(_('no patches applied'))
922 922 revs = scmutil.revrange(repo, opts.get('rev'))
923 923 if len(revs) > 1 and revs[0] > revs[1]:
924 924 revs.reverse()
925 925 revpatches = self._revpatches(repo, revs)
926 926 realpatches += revpatches
927 927 numrevs = len(revpatches)
928 928
929 929 self._cleanup(realpatches, numrevs, opts.get('keep'))
930 930
931 931 def checktoppatch(self, repo):
932 932 '''check that working directory is at qtip'''
933 933 if self.applied:
934 934 top = self.applied[-1].node
935 935 patch = self.applied[-1].name
936 936 if repo.dirstate.p1() != top:
937 937 raise util.Abort(_("working directory revision is not qtip"))
938 938 return top, patch
939 939 return None, None
940 940
941 941 def checksubstate(self, repo, baserev=None):
942 942 '''return list of subrepos at a different revision than substate.
943 943 Abort if any subrepos have uncommitted changes.'''
944 944 inclsubs = []
945 945 wctx = repo[None]
946 946 if baserev:
947 947 bctx = repo[baserev]
948 948 else:
949 949 bctx = wctx.parents()[0]
950 950 for s in sorted(wctx.substate):
951 951 if wctx.sub(s).dirty(True):
952 952 raise util.Abort(
953 953 _("uncommitted changes in subrepository %s") % s)
954 954 elif s not in bctx.substate or bctx.sub(s).dirty():
955 955 inclsubs.append(s)
956 956 return inclsubs
957 957
958 958 def putsubstate2changes(self, substatestate, changes):
959 959 for files in changes[:3]:
960 960 if '.hgsubstate' in files:
961 961 return # already listed up
962 962 # not yet listed up
963 963 if substatestate in 'a?':
964 964 changes[1].append('.hgsubstate')
965 965 elif substatestate in 'r':
966 966 changes[2].append('.hgsubstate')
967 967 else: # modified
968 968 changes[0].append('.hgsubstate')
969 969
970 970 def localchangesfound(self, refresh=True):
971 971 if refresh:
972 972 raise util.Abort(_("local changes found, refresh first"))
973 973 else:
974 974 raise util.Abort(_("local changes found"))
975 975
976 976 def checklocalchanges(self, repo, force=False, refresh=True):
977 977 m, a, r, d = repo.status()[:4]
978 978 if (m or a or r or d) and not force:
979 979 self.localchangesfound(refresh)
980 980 return m, a, r, d
981 981
982 982 _reserved = ('series', 'status', 'guards', '.', '..')
983 983 def checkreservedname(self, name):
984 984 if name in self._reserved:
985 985 raise util.Abort(_('"%s" cannot be used as the name of a patch')
986 986 % name)
987 987 for prefix in ('.hg', '.mq'):
988 988 if name.startswith(prefix):
989 989 raise util.Abort(_('patch name cannot begin with "%s"')
990 990 % prefix)
991 991 for c in ('#', ':'):
992 992 if c in name:
993 993 raise util.Abort(_('"%s" cannot be used in the name of a patch')
994 994 % c)
995 995
996 996 def checkpatchname(self, name, force=False):
997 997 self.checkreservedname(name)
998 998 if not force and os.path.exists(self.join(name)):
999 999 if os.path.isdir(self.join(name)):
1000 1000 raise util.Abort(_('"%s" already exists as a directory')
1001 1001 % name)
1002 1002 else:
1003 1003 raise util.Abort(_('patch "%s" already exists') % name)
1004 1004
1005 1005 def checkkeepchanges(self, keepchanges, force):
1006 1006 if force and keepchanges:
1007 1007 raise util.Abort(_('cannot use both --force and --keep-changes'))
1008 1008
1009 1009 def new(self, repo, patchfn, *pats, **opts):
1010 1010 """options:
1011 1011 msg: a string or a no-argument function returning a string
1012 1012 """
1013 1013 msg = opts.get('msg')
1014 1014 user = opts.get('user')
1015 1015 date = opts.get('date')
1016 1016 if date:
1017 1017 date = util.parsedate(date)
1018 1018 diffopts = self.diffopts({'git': opts.get('git')})
1019 1019 if opts.get('checkname', True):
1020 1020 self.checkpatchname(patchfn)
1021 1021 inclsubs = self.checksubstate(repo)
1022 1022 if inclsubs:
1023 1023 inclsubs.append('.hgsubstate')
1024 1024 substatestate = repo.dirstate['.hgsubstate']
1025 1025 if opts.get('include') or opts.get('exclude') or pats:
1026 1026 if inclsubs:
1027 1027 pats = list(pats or []) + inclsubs
1028 1028 match = scmutil.match(repo[None], pats, opts)
1029 1029 # detect missing files in pats
1030 1030 def badfn(f, msg):
1031 1031 if f != '.hgsubstate': # .hgsubstate is auto-created
1032 1032 raise util.Abort('%s: %s' % (f, msg))
1033 1033 match.bad = badfn
1034 1034 changes = repo.status(match=match)
1035 1035 m, a, r, d = changes[:4]
1036 1036 else:
1037 1037 changes = self.checklocalchanges(repo, force=True)
1038 1038 m, a, r, d = changes
1039 1039 match = scmutil.matchfiles(repo, m + a + r + inclsubs)
1040 1040 if len(repo[None].parents()) > 1:
1041 1041 raise util.Abort(_('cannot manage merge changesets'))
1042 1042 commitfiles = m + a + r
1043 1043 self.checktoppatch(repo)
1044 1044 insert = self.fullseriesend()
1045 1045 wlock = repo.wlock()
1046 1046 try:
1047 1047 try:
1048 1048 # if patch file write fails, abort early
1049 1049 p = self.opener(patchfn, "w")
1050 1050 except IOError, e:
1051 1051 raise util.Abort(_('cannot write patch "%s": %s')
1052 1052 % (patchfn, e.strerror))
1053 1053 try:
1054 1054 if self.plainmode:
1055 1055 if user:
1056 1056 p.write("From: " + user + "\n")
1057 1057 if not date:
1058 1058 p.write("\n")
1059 1059 if date:
1060 1060 p.write("Date: %d %d\n\n" % date)
1061 1061 else:
1062 1062 p.write("# HG changeset patch\n")
1063 1063 p.write("# Parent "
1064 1064 + hex(repo[None].p1().node()) + "\n")
1065 1065 if user:
1066 1066 p.write("# User " + user + "\n")
1067 1067 if date:
1068 1068 p.write("# Date %s %s\n\n" % date)
1069 1069 if util.safehasattr(msg, '__call__'):
1070 1070 msg = msg()
1071 1071 commitmsg = msg and msg or ("[mq]: %s" % patchfn)
1072 1072 n = newcommit(repo, None, commitmsg, user, date, match=match,
1073 1073 force=True)
1074 1074 if n is None:
1075 1075 raise util.Abort(_("repo commit failed"))
1076 1076 try:
1077 1077 self.fullseries[insert:insert] = [patchfn]
1078 1078 self.applied.append(statusentry(n, patchfn))
1079 1079 self.parseseries()
1080 1080 self.seriesdirty = True
1081 1081 self.applieddirty = True
1082 1082 if msg:
1083 1083 msg = msg + "\n\n"
1084 1084 p.write(msg)
1085 1085 if commitfiles:
1086 1086 parent = self.qparents(repo, n)
1087 1087 if inclsubs:
1088 1088 self.putsubstate2changes(substatestate, changes)
1089 1089 chunks = patchmod.diff(repo, node1=parent, node2=n,
1090 1090 changes=changes, opts=diffopts)
1091 1091 for chunk in chunks:
1092 1092 p.write(chunk)
1093 1093 p.close()
1094 1094 r = self.qrepo()
1095 1095 if r:
1096 1096 r[None].add([patchfn])
1097 1097 except: # re-raises
1098 1098 repo.rollback()
1099 1099 raise
1100 1100 except Exception:
1101 1101 patchpath = self.join(patchfn)
1102 1102 try:
1103 1103 os.unlink(patchpath)
1104 1104 except OSError:
1105 1105 self.ui.warn(_('error unlinking %s\n') % patchpath)
1106 1106 raise
1107 1107 self.removeundo(repo)
1108 1108 finally:
1109 1109 release(wlock)
1110 1110
1111 1111 def strip(self, repo, revs, update=True, backup="all", force=None):
1112 1112 wlock = lock = None
1113 1113 try:
1114 1114 wlock = repo.wlock()
1115 1115 lock = repo.lock()
1116 1116
1117 1117 if update:
1118 1118 self.checklocalchanges(repo, force=force, refresh=False)
1119 1119 urev = self.qparents(repo, revs[0])
1120 1120 hg.clean(repo, urev)
1121 1121 repo.dirstate.write()
1122 1122
1123 1123 repair.strip(self.ui, repo, revs, backup)
1124 1124 finally:
1125 1125 release(lock, wlock)
1126 1126
1127 1127 def isapplied(self, patch):
1128 1128 """returns (index, rev, patch)"""
1129 1129 for i, a in enumerate(self.applied):
1130 1130 if a.name == patch:
1131 1131 return (i, a.node, a.name)
1132 1132 return None
1133 1133
1134 1134 # if the exact patch name does not exist, we try a few
1135 1135 # variations. If strict is passed, we try only #1
1136 1136 #
1137 1137 # 1) a number (as string) to indicate an offset in the series file
1138 1138 # 2) a unique substring of the patch name was given
1139 1139 # 3) patchname[-+]num to indicate an offset in the series file
1140 1140 def lookup(self, patch, strict=False):
1141 1141 def partialname(s):
1142 1142 if s in self.series:
1143 1143 return s
1144 1144 matches = [x for x in self.series if s in x]
1145 1145 if len(matches) > 1:
1146 1146 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
1147 1147 for m in matches:
1148 1148 self.ui.warn(' %s\n' % m)
1149 1149 return None
1150 1150 if matches:
1151 1151 return matches[0]
1152 1152 if self.series and self.applied:
1153 1153 if s == 'qtip':
1154 1154 return self.series[self.seriesend(True) - 1]
1155 1155 if s == 'qbase':
1156 1156 return self.series[0]
1157 1157 return None
1158 1158
1159 1159 if patch in self.series:
1160 1160 return patch
1161 1161
1162 1162 if not os.path.isfile(self.join(patch)):
1163 1163 try:
1164 1164 sno = int(patch)
1165 1165 except (ValueError, OverflowError):
1166 1166 pass
1167 1167 else:
1168 1168 if -len(self.series) <= sno < len(self.series):
1169 1169 return self.series[sno]
1170 1170
1171 1171 if not strict:
1172 1172 res = partialname(patch)
1173 1173 if res:
1174 1174 return res
1175 1175 minus = patch.rfind('-')
1176 1176 if minus >= 0:
1177 1177 res = partialname(patch[:minus])
1178 1178 if res:
1179 1179 i = self.series.index(res)
1180 1180 try:
1181 1181 off = int(patch[minus + 1:] or 1)
1182 1182 except (ValueError, OverflowError):
1183 1183 pass
1184 1184 else:
1185 1185 if i - off >= 0:
1186 1186 return self.series[i - off]
1187 1187 plus = patch.rfind('+')
1188 1188 if plus >= 0:
1189 1189 res = partialname(patch[:plus])
1190 1190 if res:
1191 1191 i = self.series.index(res)
1192 1192 try:
1193 1193 off = int(patch[plus + 1:] or 1)
1194 1194 except (ValueError, OverflowError):
1195 1195 pass
1196 1196 else:
1197 1197 if i + off < len(self.series):
1198 1198 return self.series[i + off]
1199 1199 raise util.Abort(_("patch %s not in series") % patch)
1200 1200
1201 1201 def push(self, repo, patch=None, force=False, list=False, mergeq=None,
1202 1202 all=False, move=False, exact=False, nobackup=False,
1203 1203 keepchanges=False):
1204 1204 self.checkkeepchanges(keepchanges, force)
1205 1205 diffopts = self.diffopts()
1206 1206 wlock = repo.wlock()
1207 1207 try:
1208 1208 heads = []
1209 1209 for b, ls in repo.branchmap().iteritems():
1210 1210 heads += ls
1211 1211 if not heads:
1212 1212 heads = [nullid]
1213 1213 if repo.dirstate.p1() not in heads and not exact:
1214 1214 self.ui.status(_("(working directory not at a head)\n"))
1215 1215
1216 1216 if not self.series:
1217 1217 self.ui.warn(_('no patches in series\n'))
1218 1218 return 0
1219 1219
1220 1220 # Suppose our series file is: A B C and the current 'top'
1221 1221 # patch is B. qpush C should be performed (moving forward)
1222 1222 # qpush B is a NOP (no change) qpush A is an error (can't
1223 1223 # go backwards with qpush)
1224 1224 if patch:
1225 1225 patch = self.lookup(patch)
1226 1226 info = self.isapplied(patch)
1227 1227 if info and info[0] >= len(self.applied) - 1:
1228 1228 self.ui.warn(
1229 1229 _('qpush: %s is already at the top\n') % patch)
1230 1230 return 0
1231 1231
1232 1232 pushable, reason = self.pushable(patch)
1233 1233 if pushable:
1234 1234 if self.series.index(patch) < self.seriesend():
1235 1235 raise util.Abort(
1236 1236 _("cannot push to a previous patch: %s") % patch)
1237 1237 else:
1238 1238 if reason:
1239 1239 reason = _('guarded by %s') % reason
1240 1240 else:
1241 1241 reason = _('no matching guards')
1242 1242 self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason))
1243 1243 return 1
1244 1244 elif all:
1245 1245 patch = self.series[-1]
1246 1246 if self.isapplied(patch):
1247 1247 self.ui.warn(_('all patches are currently applied\n'))
1248 1248 return 0
1249 1249
1250 1250 # Following the above example, starting at 'top' of B:
1251 1251 # qpush should be performed (pushes C), but a subsequent
1252 1252 # qpush without an argument is an error (nothing to
1253 1253 # apply). This allows a loop of "...while hg qpush..." to
1254 1254 # work as it detects an error when done
1255 1255 start = self.seriesend()
1256 1256 if start == len(self.series):
1257 1257 self.ui.warn(_('patch series already fully applied\n'))
1258 1258 return 1
1259 1259 if not force and not keepchanges:
1260 1260 self.checklocalchanges(repo, refresh=self.applied)
1261 1261
1262 1262 if exact:
1263 1263 if keepchanges:
1264 1264 raise util.Abort(
1265 1265 _("cannot use --exact and --keep-changes together"))
1266 1266 if move:
1267 1267 raise util.Abort(_('cannot use --exact and --move '
1268 1268 'together'))
1269 1269 if self.applied:
1270 1270 raise util.Abort(_('cannot push --exact with applied '
1271 1271 'patches'))
1272 1272 root = self.series[start]
1273 1273 target = patchheader(self.join(root), self.plainmode).parent
1274 1274 if not target:
1275 1275 raise util.Abort(
1276 1276 _("%s does not have a parent recorded") % root)
1277 1277 if not repo[target] == repo['.']:
1278 1278 hg.update(repo, target)
1279 1279
1280 1280 if move:
1281 1281 if not patch:
1282 1282 raise util.Abort(_("please specify the patch to move"))
1283 1283 for fullstart, rpn in enumerate(self.fullseries):
1284 1284 # strip markers for patch guards
1285 1285 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1286 1286 break
1287 1287 for i, rpn in enumerate(self.fullseries[fullstart:]):
1288 1288 # strip markers for patch guards
1289 1289 if self.guard_re.split(rpn, 1)[0] == patch:
1290 1290 break
1291 1291 index = fullstart + i
1292 1292 assert index < len(self.fullseries)
1293 1293 fullpatch = self.fullseries[index]
1294 1294 del self.fullseries[index]
1295 1295 self.fullseries.insert(fullstart, fullpatch)
1296 1296 self.parseseries()
1297 1297 self.seriesdirty = True
1298 1298
1299 1299 self.applieddirty = True
1300 1300 if start > 0:
1301 1301 self.checktoppatch(repo)
1302 1302 if not patch:
1303 1303 patch = self.series[start]
1304 1304 end = start + 1
1305 1305 else:
1306 1306 end = self.series.index(patch, start) + 1
1307 1307
1308 1308 tobackup = set()
1309 1309 if (not nobackup and force) or keepchanges:
1310 1310 m, a, r, d = self.checklocalchanges(repo, force=True)
1311 1311 if keepchanges:
1312 1312 tobackup.update(m + a + r + d)
1313 1313 else:
1314 1314 tobackup.update(m + a)
1315 1315
1316 1316 s = self.series[start:end]
1317 1317 all_files = set()
1318 1318 try:
1319 1319 if mergeq:
1320 1320 ret = self.mergepatch(repo, mergeq, s, diffopts)
1321 1321 else:
1322 1322 ret = self.apply(repo, s, list, all_files=all_files,
1323 1323 tobackup=tobackup, keepchanges=keepchanges)
1324 1324 except: # re-raises
1325 1325 self.ui.warn(_('cleaning up working directory...'))
1326 1326 node = repo.dirstate.p1()
1327 1327 hg.revert(repo, node, None)
1328 1328 # only remove unknown files that we know we touched or
1329 1329 # created while patching
1330 1330 for f in all_files:
1331 1331 if f not in repo.dirstate:
1332 1332 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1333 1333 self.ui.warn(_('done\n'))
1334 1334 raise
1335 1335
1336 1336 if not self.applied:
1337 1337 return ret[0]
1338 1338 top = self.applied[-1].name
1339 1339 if ret[0] and ret[0] > 1:
1340 1340 msg = _("errors during apply, please fix and refresh %s\n")
1341 1341 self.ui.write(msg % top)
1342 1342 else:
1343 1343 self.ui.write(_("now at: %s\n") % top)
1344 1344 return ret[0]
1345 1345
1346 1346 finally:
1347 1347 wlock.release()
1348 1348
1349 1349 def pop(self, repo, patch=None, force=False, update=True, all=False,
1350 1350 nobackup=False, keepchanges=False):
1351 1351 self.checkkeepchanges(keepchanges, force)
1352 1352 wlock = repo.wlock()
1353 1353 try:
1354 1354 if patch:
1355 1355 # index, rev, patch
1356 1356 info = self.isapplied(patch)
1357 1357 if not info:
1358 1358 patch = self.lookup(patch)
1359 1359 info = self.isapplied(patch)
1360 1360 if not info:
1361 1361 raise util.Abort(_("patch %s is not applied") % patch)
1362 1362
1363 1363 if not self.applied:
1364 1364 # Allow qpop -a to work repeatedly,
1365 1365 # but not qpop without an argument
1366 1366 self.ui.warn(_("no patches applied\n"))
1367 1367 return not all
1368 1368
1369 1369 if all:
1370 1370 start = 0
1371 1371 elif patch:
1372 1372 start = info[0] + 1
1373 1373 else:
1374 1374 start = len(self.applied) - 1
1375 1375
1376 1376 if start >= len(self.applied):
1377 1377 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
1378 1378 return
1379 1379
1380 1380 if not update:
1381 1381 parents = repo.dirstate.parents()
1382 1382 rr = [x.node for x in self.applied]
1383 1383 for p in parents:
1384 1384 if p in rr:
1385 1385 self.ui.warn(_("qpop: forcing dirstate update\n"))
1386 1386 update = True
1387 1387 else:
1388 1388 parents = [p.node() for p in repo[None].parents()]
1389 1389 needupdate = False
1390 1390 for entry in self.applied[start:]:
1391 1391 if entry.node in parents:
1392 1392 needupdate = True
1393 1393 break
1394 1394 update = needupdate
1395 1395
1396 1396 tobackup = set()
1397 1397 if update:
1398 1398 m, a, r, d = self.checklocalchanges(
1399 1399 repo, force=force or keepchanges)
1400 1400 if force:
1401 1401 if not nobackup:
1402 1402 tobackup.update(m + a)
1403 1403 elif keepchanges:
1404 1404 tobackup.update(m + a + r + d)
1405 1405
1406 1406 self.applieddirty = True
1407 1407 end = len(self.applied)
1408 1408 rev = self.applied[start].node
1409 1409
1410 1410 try:
1411 1411 heads = repo.changelog.heads(rev)
1412 1412 except error.LookupError:
1413 1413 node = short(rev)
1414 1414 raise util.Abort(_('trying to pop unknown node %s') % node)
1415 1415
1416 1416 if heads != [self.applied[-1].node]:
1417 1417 raise util.Abort(_("popping would remove a revision not "
1418 1418 "managed by this patch queue"))
1419 1419 if not repo[self.applied[-1].node].mutable():
1420 1420 raise util.Abort(
1421 1421 _("popping would remove an immutable revision"),
1422 1422 hint=_('see "hg help phases" for details'))
1423 1423
1424 1424 # we know there are no local changes, so we can make a simplified
1425 1425 # form of hg.update.
1426 1426 if update:
1427 1427 qp = self.qparents(repo, rev)
1428 1428 ctx = repo[qp]
1429 1429 m, a, r, d = repo.status(qp, '.')[:4]
1430 1430 if d:
1431 1431 raise util.Abort(_("deletions found between repo revs"))
1432 1432
1433 1433 tobackup = set(a + m + r) & tobackup
1434 1434 if keepchanges and tobackup:
1435 1435 self.localchangesfound()
1436 1436 self.backup(repo, tobackup)
1437 1437
1438 1438 for f in a:
1439 1439 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1440 1440 repo.dirstate.drop(f)
1441 1441 for f in m + r:
1442 1442 fctx = ctx[f]
1443 1443 repo.wwrite(f, fctx.data(), fctx.flags())
1444 1444 repo.dirstate.normal(f)
1445 1445 repo.setparents(qp, nullid)
1446 1446 for patch in reversed(self.applied[start:end]):
1447 1447 self.ui.status(_("popping %s\n") % patch.name)
1448 1448 del self.applied[start:end]
1449 1449 self.strip(repo, [rev], update=False, backup='strip')
1450 1450 if self.applied:
1451 1451 self.ui.write(_("now at: %s\n") % self.applied[-1].name)
1452 1452 else:
1453 1453 self.ui.write(_("patch queue now empty\n"))
1454 1454 finally:
1455 1455 wlock.release()
1456 1456
1457 1457 def diff(self, repo, pats, opts):
1458 1458 top, patch = self.checktoppatch(repo)
1459 1459 if not top:
1460 1460 self.ui.write(_("no patches applied\n"))
1461 1461 return
1462 1462 qp = self.qparents(repo, top)
1463 1463 if opts.get('reverse'):
1464 1464 node1, node2 = None, qp
1465 1465 else:
1466 1466 node1, node2 = qp, None
1467 1467 diffopts = self.diffopts(opts, patch)
1468 1468 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1469 1469
1470 1470 def refresh(self, repo, pats=None, **opts):
1471 1471 if not self.applied:
1472 1472 self.ui.write(_("no patches applied\n"))
1473 1473 return 1
1474 1474 msg = opts.get('msg', '').rstrip()
1475 1475 newuser = opts.get('user')
1476 1476 newdate = opts.get('date')
1477 1477 if newdate:
1478 1478 newdate = '%d %d' % util.parsedate(newdate)
1479 1479 wlock = repo.wlock()
1480 1480
1481 1481 try:
1482 1482 self.checktoppatch(repo)
1483 1483 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1484 1484 if repo.changelog.heads(top) != [top]:
1485 1485 raise util.Abort(_("cannot refresh a revision with children"))
1486 1486 if not repo[top].mutable():
1487 1487 raise util.Abort(_("cannot refresh immutable revision"),
1488 1488 hint=_('see "hg help phases" for details'))
1489 1489
1490 1490 cparents = repo.changelog.parents(top)
1491 1491 patchparent = self.qparents(repo, top)
1492 1492
1493 1493 inclsubs = self.checksubstate(repo, hex(patchparent))
1494 1494 if inclsubs:
1495 1495 inclsubs.append('.hgsubstate')
1496 1496 substatestate = repo.dirstate['.hgsubstate']
1497 1497
1498 1498 ph = patchheader(self.join(patchfn), self.plainmode)
1499 1499 diffopts = self.diffopts({'git': opts.get('git')}, patchfn)
1500 1500 if msg:
1501 1501 ph.setmessage(msg)
1502 1502 if newuser:
1503 1503 ph.setuser(newuser)
1504 1504 if newdate:
1505 1505 ph.setdate(newdate)
1506 1506 ph.setparent(hex(patchparent))
1507 1507
1508 1508 # only commit new patch when write is complete
1509 1509 patchf = self.opener(patchfn, 'w', atomictemp=True)
1510 1510
1511 1511 comments = str(ph)
1512 1512 if comments:
1513 1513 patchf.write(comments)
1514 1514
1515 1515 # update the dirstate in place, strip off the qtip commit
1516 1516 # and then commit.
1517 1517 #
1518 1518 # this should really read:
1519 1519 # mm, dd, aa = repo.status(top, patchparent)[:3]
1520 1520 # but we do it backwards to take advantage of manifest/changelog
1521 1521 # caching against the next repo.status call
1522 1522 mm, aa, dd = repo.status(patchparent, top)[:3]
1523 1523 changes = repo.changelog.read(top)
1524 1524 man = repo.manifest.read(changes[0])
1525 1525 aaa = aa[:]
1526 1526 matchfn = scmutil.match(repo[None], pats, opts)
1527 1527 # in short mode, we only diff the files included in the
1528 1528 # patch already plus specified files
1529 1529 if opts.get('short'):
1530 1530 # if amending a patch, we start with existing
1531 1531 # files plus specified files - unfiltered
1532 1532 match = scmutil.matchfiles(repo, mm + aa + dd + matchfn.files())
1533 1533 # filter with include/exclude options
1534 1534 matchfn = scmutil.match(repo[None], opts=opts)
1535 1535 else:
1536 1536 match = scmutil.matchall(repo)
1537 1537 m, a, r, d = repo.status(match=match)[:4]
1538 1538 mm = set(mm)
1539 1539 aa = set(aa)
1540 1540 dd = set(dd)
1541 1541
1542 1542 # we might end up with files that were added between
1543 1543 # qtip and the dirstate parent, but then changed in the
1544 1544 # local dirstate. in this case, we want them to only
1545 1545 # show up in the added section
1546 1546 for x in m:
1547 1547 if x not in aa:
1548 1548 mm.add(x)
1549 1549 # we might end up with files added by the local dirstate that
1550 1550 # were deleted by the patch. In this case, they should only
1551 1551 # show up in the changed section.
1552 1552 for x in a:
1553 1553 if x in dd:
1554 1554 dd.remove(x)
1555 1555 mm.add(x)
1556 1556 else:
1557 1557 aa.add(x)
1558 1558 # make sure any files deleted in the local dirstate
1559 1559 # are not in the add or change column of the patch
1560 1560 forget = []
1561 1561 for x in d + r:
1562 1562 if x in aa:
1563 1563 aa.remove(x)
1564 1564 forget.append(x)
1565 1565 continue
1566 1566 else:
1567 1567 mm.discard(x)
1568 1568 dd.add(x)
1569 1569
1570 1570 m = list(mm)
1571 1571 r = list(dd)
1572 1572 a = list(aa)
1573 1573
1574 1574 # create 'match' that includes the files to be recommitted.
1575 1575 # apply matchfn via repo.status to ensure correct case handling.
1576 1576 cm, ca, cr, cd = repo.status(patchparent, match=matchfn)[:4]
1577 1577 allmatches = set(cm + ca + cr + cd)
1578 1578 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1579 1579
1580 1580 files = set(inclsubs)
1581 1581 for x in refreshchanges:
1582 1582 files.update(x)
1583 1583 match = scmutil.matchfiles(repo, files)
1584 1584
1585 1585 bmlist = repo[top].bookmarks()
1586 1586
1587 1587 try:
1588 1588 if diffopts.git or diffopts.upgrade:
1589 1589 copies = {}
1590 1590 for dst in a:
1591 1591 src = repo.dirstate.copied(dst)
1592 1592 # during qfold, the source file for copies may
1593 1593 # be removed. Treat this as a simple add.
1594 1594 if src is not None and src in repo.dirstate:
1595 1595 copies.setdefault(src, []).append(dst)
1596 1596 repo.dirstate.add(dst)
1597 1597 # remember the copies between patchparent and qtip
1598 1598 for dst in aaa:
1599 1599 f = repo.file(dst)
1600 1600 src = f.renamed(man[dst])
1601 1601 if src:
1602 1602 copies.setdefault(src[0], []).extend(
1603 1603 copies.get(dst, []))
1604 1604 if dst in a:
1605 1605 copies[src[0]].append(dst)
1606 1606 # we can't copy a file created by the patch itself
1607 1607 if dst in copies:
1608 1608 del copies[dst]
1609 1609 for src, dsts in copies.iteritems():
1610 1610 for dst in dsts:
1611 1611 repo.dirstate.copy(src, dst)
1612 1612 else:
1613 1613 for dst in a:
1614 1614 repo.dirstate.add(dst)
1615 1615 # Drop useless copy information
1616 1616 for f in list(repo.dirstate.copies()):
1617 1617 repo.dirstate.copy(None, f)
1618 1618 for f in r:
1619 1619 repo.dirstate.remove(f)
1620 1620 # if the patch excludes a modified file, mark that
1621 1621 # file with mtime=0 so status can see it.
1622 1622 mm = []
1623 1623 for i in xrange(len(m) - 1, -1, -1):
1624 1624 if not matchfn(m[i]):
1625 1625 mm.append(m[i])
1626 1626 del m[i]
1627 1627 for f in m:
1628 1628 repo.dirstate.normal(f)
1629 1629 for f in mm:
1630 1630 repo.dirstate.normallookup(f)
1631 1631 for f in forget:
1632 1632 repo.dirstate.drop(f)
1633 1633
1634 1634 if not msg:
1635 1635 if not ph.message:
1636 1636 message = "[mq]: %s\n" % patchfn
1637 1637 else:
1638 1638 message = "\n".join(ph.message)
1639 1639 else:
1640 1640 message = msg
1641 1641
1642 1642 user = ph.user or changes[1]
1643 1643
1644 1644 oldphase = repo[top].phase()
1645 1645
1646 1646 # assumes strip can roll itself back if interrupted
1647 1647 repo.setparents(*cparents)
1648 1648 self.applied.pop()
1649 1649 self.applieddirty = True
1650 1650 self.strip(repo, [top], update=False,
1651 1651 backup='strip')
1652 1652 except: # re-raises
1653 1653 repo.dirstate.invalidate()
1654 1654 raise
1655 1655
1656 1656 try:
1657 1657 # might be nice to attempt to roll back strip after this
1658 1658
1659 1659 # Ensure we create a new changeset in the same phase than
1660 1660 # the old one.
1661 1661 n = newcommit(repo, oldphase, message, user, ph.date,
1662 1662 match=match, force=True)
1663 1663 # only write patch after a successful commit
1664 1664 c = [list(x) for x in refreshchanges]
1665 1665 if inclsubs:
1666 1666 self.putsubstate2changes(substatestate, c)
1667 1667 chunks = patchmod.diff(repo, patchparent,
1668 1668 changes=c, opts=diffopts)
1669 1669 for chunk in chunks:
1670 1670 patchf.write(chunk)
1671 1671 patchf.close()
1672 1672
1673 1673 marks = repo._bookmarks
1674 1674 for bm in bmlist:
1675 1675 marks[bm] = n
1676 1676 marks.write()
1677 1677
1678 1678 self.applied.append(statusentry(n, patchfn))
1679 1679 except: # re-raises
1680 1680 ctx = repo[cparents[0]]
1681 1681 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
1682 1682 self.savedirty()
1683 1683 self.ui.warn(_('refresh interrupted while patch was popped! '
1684 1684 '(revert --all, qpush to recover)\n'))
1685 1685 raise
1686 1686 finally:
1687 1687 wlock.release()
1688 1688 self.removeundo(repo)
1689 1689
1690 1690 def init(self, repo, create=False):
1691 1691 if not create and os.path.isdir(self.path):
1692 1692 raise util.Abort(_("patch queue directory already exists"))
1693 1693 try:
1694 1694 os.mkdir(self.path)
1695 1695 except OSError, inst:
1696 1696 if inst.errno != errno.EEXIST or not create:
1697 1697 raise
1698 1698 if create:
1699 1699 return self.qrepo(create=True)
1700 1700
1701 1701 def unapplied(self, repo, patch=None):
1702 1702 if patch and patch not in self.series:
1703 1703 raise util.Abort(_("patch %s is not in series file") % patch)
1704 1704 if not patch:
1705 1705 start = self.seriesend()
1706 1706 else:
1707 1707 start = self.series.index(patch) + 1
1708 1708 unapplied = []
1709 1709 for i in xrange(start, len(self.series)):
1710 1710 pushable, reason = self.pushable(i)
1711 1711 if pushable:
1712 1712 unapplied.append((i, self.series[i]))
1713 1713 self.explainpushable(i)
1714 1714 return unapplied
1715 1715
1716 1716 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1717 1717 summary=False):
1718 1718 def displayname(pfx, patchname, state):
1719 1719 if pfx:
1720 1720 self.ui.write(pfx)
1721 1721 if summary:
1722 1722 ph = patchheader(self.join(patchname), self.plainmode)
1723 1723 msg = ph.message and ph.message[0] or ''
1724 1724 if self.ui.formatted():
1725 1725 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
1726 1726 if width > 0:
1727 1727 msg = util.ellipsis(msg, width)
1728 1728 else:
1729 1729 msg = ''
1730 1730 self.ui.write(patchname, label='qseries.' + state)
1731 1731 self.ui.write(': ')
1732 1732 self.ui.write(msg, label='qseries.message.' + state)
1733 1733 else:
1734 1734 self.ui.write(patchname, label='qseries.' + state)
1735 1735 self.ui.write('\n')
1736 1736
1737 1737 applied = set([p.name for p in self.applied])
1738 1738 if length is None:
1739 1739 length = len(self.series) - start
1740 1740 if not missing:
1741 1741 if self.ui.verbose:
1742 1742 idxwidth = len(str(start + length - 1))
1743 1743 for i in xrange(start, start + length):
1744 1744 patch = self.series[i]
1745 1745 if patch in applied:
1746 1746 char, state = 'A', 'applied'
1747 1747 elif self.pushable(i)[0]:
1748 1748 char, state = 'U', 'unapplied'
1749 1749 else:
1750 1750 char, state = 'G', 'guarded'
1751 1751 pfx = ''
1752 1752 if self.ui.verbose:
1753 1753 pfx = '%*d %s ' % (idxwidth, i, char)
1754 1754 elif status and status != char:
1755 1755 continue
1756 1756 displayname(pfx, patch, state)
1757 1757 else:
1758 1758 msng_list = []
1759 1759 for root, dirs, files in os.walk(self.path):
1760 1760 d = root[len(self.path) + 1:]
1761 1761 for f in files:
1762 1762 fl = os.path.join(d, f)
1763 1763 if (fl not in self.series and
1764 1764 fl not in (self.statuspath, self.seriespath,
1765 1765 self.guardspath)
1766 1766 and not fl.startswith('.')):
1767 1767 msng_list.append(fl)
1768 1768 for x in sorted(msng_list):
1769 1769 pfx = self.ui.verbose and ('D ') or ''
1770 1770 displayname(pfx, x, 'missing')
1771 1771
1772 1772 def issaveline(self, l):
1773 1773 if l.name == '.hg.patches.save.line':
1774 1774 return True
1775 1775
1776 1776 def qrepo(self, create=False):
1777 1777 ui = self.ui.copy()
1778 1778 ui.setconfig('paths', 'default', '', overlay=False)
1779 1779 ui.setconfig('paths', 'default-push', '', overlay=False)
1780 1780 if create or os.path.isdir(self.join(".hg")):
1781 1781 return hg.repository(ui, path=self.path, create=create)
1782 1782
1783 1783 def restore(self, repo, rev, delete=None, qupdate=None):
1784 1784 desc = repo[rev].description().strip()
1785 1785 lines = desc.splitlines()
1786 1786 i = 0
1787 1787 datastart = None
1788 1788 series = []
1789 1789 applied = []
1790 1790 qpp = None
1791 1791 for i, line in enumerate(lines):
1792 1792 if line == 'Patch Data:':
1793 1793 datastart = i + 1
1794 1794 elif line.startswith('Dirstate:'):
1795 1795 l = line.rstrip()
1796 1796 l = l[10:].split(' ')
1797 1797 qpp = [bin(x) for x in l]
1798 1798 elif datastart is not None:
1799 1799 l = line.rstrip()
1800 1800 n, name = l.split(':', 1)
1801 1801 if n:
1802 1802 applied.append(statusentry(bin(n), name))
1803 1803 else:
1804 1804 series.append(l)
1805 1805 if datastart is None:
1806 1806 self.ui.warn(_("no saved patch data found\n"))
1807 1807 return 1
1808 1808 self.ui.warn(_("restoring status: %s\n") % lines[0])
1809 1809 self.fullseries = series
1810 1810 self.applied = applied
1811 1811 self.parseseries()
1812 1812 self.seriesdirty = True
1813 1813 self.applieddirty = True
1814 1814 heads = repo.changelog.heads()
1815 1815 if delete:
1816 1816 if rev not in heads:
1817 1817 self.ui.warn(_("save entry has children, leaving it alone\n"))
1818 1818 else:
1819 1819 self.ui.warn(_("removing save entry %s\n") % short(rev))
1820 1820 pp = repo.dirstate.parents()
1821 1821 if rev in pp:
1822 1822 update = True
1823 1823 else:
1824 1824 update = False
1825 1825 self.strip(repo, [rev], update=update, backup='strip')
1826 1826 if qpp:
1827 1827 self.ui.warn(_("saved queue repository parents: %s %s\n") %
1828 1828 (short(qpp[0]), short(qpp[1])))
1829 1829 if qupdate:
1830 1830 self.ui.status(_("updating queue directory\n"))
1831 1831 r = self.qrepo()
1832 1832 if not r:
1833 1833 self.ui.warn(_("unable to load queue repository\n"))
1834 1834 return 1
1835 1835 hg.clean(r, qpp[0])
1836 1836
1837 1837 def save(self, repo, msg=None):
1838 1838 if not self.applied:
1839 1839 self.ui.warn(_("save: no patches applied, exiting\n"))
1840 1840 return 1
1841 1841 if self.issaveline(self.applied[-1]):
1842 1842 self.ui.warn(_("status is already saved\n"))
1843 1843 return 1
1844 1844
1845 1845 if not msg:
1846 1846 msg = _("hg patches saved state")
1847 1847 else:
1848 1848 msg = "hg patches: " + msg.rstrip('\r\n')
1849 1849 r = self.qrepo()
1850 1850 if r:
1851 1851 pp = r.dirstate.parents()
1852 1852 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
1853 1853 msg += "\n\nPatch Data:\n"
1854 1854 msg += ''.join('%s\n' % x for x in self.applied)
1855 1855 msg += ''.join(':%s\n' % x for x in self.fullseries)
1856 1856 n = repo.commit(msg, force=True)
1857 1857 if not n:
1858 1858 self.ui.warn(_("repo commit failed\n"))
1859 1859 return 1
1860 1860 self.applied.append(statusentry(n, '.hg.patches.save.line'))
1861 1861 self.applieddirty = True
1862 1862 self.removeundo(repo)
1863 1863
1864 1864 def fullseriesend(self):
1865 1865 if self.applied:
1866 1866 p = self.applied[-1].name
1867 1867 end = self.findseries(p)
1868 1868 if end is None:
1869 1869 return len(self.fullseries)
1870 1870 return end + 1
1871 1871 return 0
1872 1872
1873 1873 def seriesend(self, all_patches=False):
1874 1874 """If all_patches is False, return the index of the next pushable patch
1875 1875 in the series, or the series length. If all_patches is True, return the
1876 1876 index of the first patch past the last applied one.
1877 1877 """
1878 1878 end = 0
1879 1879 def next(start):
1880 1880 if all_patches or start >= len(self.series):
1881 1881 return start
1882 1882 for i in xrange(start, len(self.series)):
1883 1883 p, reason = self.pushable(i)
1884 1884 if p:
1885 1885 return i
1886 1886 self.explainpushable(i)
1887 1887 return len(self.series)
1888 1888 if self.applied:
1889 1889 p = self.applied[-1].name
1890 1890 try:
1891 1891 end = self.series.index(p)
1892 1892 except ValueError:
1893 1893 return 0
1894 1894 return next(end + 1)
1895 1895 return next(end)
1896 1896
1897 1897 def appliedname(self, index):
1898 1898 pname = self.applied[index].name
1899 1899 if not self.ui.verbose:
1900 1900 p = pname
1901 1901 else:
1902 1902 p = str(self.series.index(pname)) + " " + pname
1903 1903 return p
1904 1904
1905 1905 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
1906 1906 force=None, git=False):
1907 1907 def checkseries(patchname):
1908 1908 if patchname in self.series:
1909 1909 raise util.Abort(_('patch %s is already in the series file')
1910 1910 % patchname)
1911 1911
1912 1912 if rev:
1913 1913 if files:
1914 1914 raise util.Abort(_('option "-r" not valid when importing '
1915 1915 'files'))
1916 1916 rev = scmutil.revrange(repo, rev)
1917 1917 rev.sort(reverse=True)
1918 1918 elif not files:
1919 1919 raise util.Abort(_('no files or revisions specified'))
1920 1920 if (len(files) > 1 or len(rev) > 1) and patchname:
1921 1921 raise util.Abort(_('option "-n" not valid when importing multiple '
1922 1922 'patches'))
1923 1923 imported = []
1924 1924 if rev:
1925 1925 # If mq patches are applied, we can only import revisions
1926 1926 # that form a linear path to qbase.
1927 1927 # Otherwise, they should form a linear path to a head.
1928 1928 heads = repo.changelog.heads(repo.changelog.node(rev[-1]))
1929 1929 if len(heads) > 1:
1930 1930 raise util.Abort(_('revision %d is the root of more than one '
1931 1931 'branch') % rev[-1])
1932 1932 if self.applied:
1933 1933 base = repo.changelog.node(rev[0])
1934 1934 if base in [n.node for n in self.applied]:
1935 1935 raise util.Abort(_('revision %d is already managed')
1936 1936 % rev[0])
1937 1937 if heads != [self.applied[-1].node]:
1938 1938 raise util.Abort(_('revision %d is not the parent of '
1939 1939 'the queue') % rev[0])
1940 1940 base = repo.changelog.rev(self.applied[0].node)
1941 1941 lastparent = repo.changelog.parentrevs(base)[0]
1942 1942 else:
1943 1943 if heads != [repo.changelog.node(rev[0])]:
1944 1944 raise util.Abort(_('revision %d has unmanaged children')
1945 1945 % rev[0])
1946 1946 lastparent = None
1947 1947
1948 1948 diffopts = self.diffopts({'git': git})
1949 1949 for r in rev:
1950 1950 if not repo[r].mutable():
1951 1951 raise util.Abort(_('revision %d is not mutable') % r,
1952 1952 hint=_('see "hg help phases" for details'))
1953 1953 p1, p2 = repo.changelog.parentrevs(r)
1954 1954 n = repo.changelog.node(r)
1955 1955 if p2 != nullrev:
1956 1956 raise util.Abort(_('cannot import merge revision %d') % r)
1957 1957 if lastparent and lastparent != r:
1958 1958 raise util.Abort(_('revision %d is not the parent of %d')
1959 1959 % (r, lastparent))
1960 1960 lastparent = p1
1961 1961
1962 1962 if not patchname:
1963 1963 patchname = normname('%d.diff' % r)
1964 1964 checkseries(patchname)
1965 1965 self.checkpatchname(patchname, force)
1966 1966 self.fullseries.insert(0, patchname)
1967 1967
1968 1968 patchf = self.opener(patchname, "w")
1969 1969 cmdutil.export(repo, [n], fp=patchf, opts=diffopts)
1970 1970 patchf.close()
1971 1971
1972 1972 se = statusentry(n, patchname)
1973 1973 self.applied.insert(0, se)
1974 1974
1975 1975 self.added.append(patchname)
1976 1976 imported.append(patchname)
1977 1977 patchname = None
1978 1978 if rev and repo.ui.configbool('mq', 'secret', False):
1979 1979 # if we added anything with --rev, we must move the secret root
1980 1980 phases.retractboundary(repo, phases.secret, [n])
1981 1981 self.parseseries()
1982 1982 self.applieddirty = True
1983 1983 self.seriesdirty = True
1984 1984
1985 1985 for i, filename in enumerate(files):
1986 1986 if existing:
1987 1987 if filename == '-':
1988 1988 raise util.Abort(_('-e is incompatible with import from -'))
1989 1989 filename = normname(filename)
1990 1990 self.checkreservedname(filename)
1991 1991 originpath = self.join(filename)
1992 1992 if not os.path.isfile(originpath):
1993 1993 raise util.Abort(_("patch %s does not exist") % filename)
1994 1994
1995 1995 if patchname:
1996 1996 self.checkpatchname(patchname, force)
1997 1997
1998 1998 self.ui.write(_('renaming %s to %s\n')
1999 1999 % (filename, patchname))
2000 2000 util.rename(originpath, self.join(patchname))
2001 2001 else:
2002 2002 patchname = filename
2003 2003
2004 2004 else:
2005 2005 if filename == '-' and not patchname:
2006 2006 raise util.Abort(_('need --name to import a patch from -'))
2007 2007 elif not patchname:
2008 2008 patchname = normname(os.path.basename(filename.rstrip('/')))
2009 2009 self.checkpatchname(patchname, force)
2010 2010 try:
2011 2011 if filename == '-':
2012 2012 text = self.ui.fin.read()
2013 2013 else:
2014 2014 fp = hg.openpath(self.ui, filename)
2015 2015 text = fp.read()
2016 2016 fp.close()
2017 2017 except (OSError, IOError):
2018 2018 raise util.Abort(_("unable to read file %s") % filename)
2019 2019 patchf = self.opener(patchname, "w")
2020 2020 patchf.write(text)
2021 2021 patchf.close()
2022 2022 if not force:
2023 2023 checkseries(patchname)
2024 2024 if patchname not in self.series:
2025 2025 index = self.fullseriesend() + i
2026 2026 self.fullseries[index:index] = [patchname]
2027 2027 self.parseseries()
2028 2028 self.seriesdirty = True
2029 2029 self.ui.warn(_("adding %s to series file\n") % patchname)
2030 2030 self.added.append(patchname)
2031 2031 imported.append(patchname)
2032 2032 patchname = None
2033 2033
2034 2034 self.removeundo(repo)
2035 2035 return imported
2036 2036
2037 2037 def fixkeepchangesopts(ui, opts):
2038 2038 if (not ui.configbool('mq', 'keepchanges') or opts.get('force')
2039 2039 or opts.get('exact')):
2040 2040 return opts
2041 2041 opts = dict(opts)
2042 2042 opts['keep_changes'] = True
2043 2043 return opts
2044 2044
2045 2045 @command("qdelete|qremove|qrm",
2046 2046 [('k', 'keep', None, _('keep patch file')),
2047 2047 ('r', 'rev', [],
2048 2048 _('stop managing a revision (DEPRECATED)'), _('REV'))],
2049 2049 _('hg qdelete [-k] [PATCH]...'))
2050 2050 def delete(ui, repo, *patches, **opts):
2051 2051 """remove patches from queue
2052 2052
2053 2053 The patches must not be applied, and at least one patch is required. Exact
2054 2054 patch identifiers must be given. With -k/--keep, the patch files are
2055 2055 preserved in the patch directory.
2056 2056
2057 2057 To stop managing a patch and move it into permanent history,
2058 2058 use the :hg:`qfinish` command."""
2059 2059 q = repo.mq
2060 2060 q.delete(repo, patches, opts)
2061 2061 q.savedirty()
2062 2062 return 0
2063 2063
2064 2064 @command("qapplied",
2065 2065 [('1', 'last', None, _('show only the preceding applied patch'))
2066 2066 ] + seriesopts,
2067 2067 _('hg qapplied [-1] [-s] [PATCH]'))
2068 2068 def applied(ui, repo, patch=None, **opts):
2069 2069 """print the patches already applied
2070 2070
2071 2071 Returns 0 on success."""
2072 2072
2073 2073 q = repo.mq
2074 2074
2075 2075 if patch:
2076 2076 if patch not in q.series:
2077 2077 raise util.Abort(_("patch %s is not in series file") % patch)
2078 2078 end = q.series.index(patch) + 1
2079 2079 else:
2080 2080 end = q.seriesend(True)
2081 2081
2082 2082 if opts.get('last') and not end:
2083 2083 ui.write(_("no patches applied\n"))
2084 2084 return 1
2085 2085 elif opts.get('last') and end == 1:
2086 2086 ui.write(_("only one patch applied\n"))
2087 2087 return 1
2088 2088 elif opts.get('last'):
2089 2089 start = end - 2
2090 2090 end = 1
2091 2091 else:
2092 2092 start = 0
2093 2093
2094 2094 q.qseries(repo, length=end, start=start, status='A',
2095 2095 summary=opts.get('summary'))
2096 2096
2097 2097
2098 2098 @command("qunapplied",
2099 2099 [('1', 'first', None, _('show only the first patch'))] + seriesopts,
2100 2100 _('hg qunapplied [-1] [-s] [PATCH]'))
2101 2101 def unapplied(ui, repo, patch=None, **opts):
2102 2102 """print the patches not yet applied
2103 2103
2104 2104 Returns 0 on success."""
2105 2105
2106 2106 q = repo.mq
2107 2107 if patch:
2108 2108 if patch not in q.series:
2109 2109 raise util.Abort(_("patch %s is not in series file") % patch)
2110 2110 start = q.series.index(patch) + 1
2111 2111 else:
2112 2112 start = q.seriesend(True)
2113 2113
2114 2114 if start == len(q.series) and opts.get('first'):
2115 2115 ui.write(_("all patches applied\n"))
2116 2116 return 1
2117 2117
2118 2118 length = opts.get('first') and 1 or None
2119 2119 q.qseries(repo, start=start, length=length, status='U',
2120 2120 summary=opts.get('summary'))
2121 2121
2122 2122 @command("qimport",
2123 2123 [('e', 'existing', None, _('import file in patch directory')),
2124 2124 ('n', 'name', '',
2125 2125 _('name of patch file'), _('NAME')),
2126 2126 ('f', 'force', None, _('overwrite existing files')),
2127 2127 ('r', 'rev', [],
2128 2128 _('place existing revisions under mq control'), _('REV')),
2129 2129 ('g', 'git', None, _('use git extended diff format')),
2130 2130 ('P', 'push', None, _('qpush after importing'))],
2131 2131 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'))
2132 2132 def qimport(ui, repo, *filename, **opts):
2133 2133 """import a patch or existing changeset
2134 2134
2135 2135 The patch is inserted into the series after the last applied
2136 2136 patch. If no patches have been applied, qimport prepends the patch
2137 2137 to the series.
2138 2138
2139 2139 The patch will have the same name as its source file unless you
2140 2140 give it a new one with -n/--name.
2141 2141
2142 2142 You can register an existing patch inside the patch directory with
2143 2143 the -e/--existing flag.
2144 2144
2145 2145 With -f/--force, an existing patch of the same name will be
2146 2146 overwritten.
2147 2147
2148 2148 An existing changeset may be placed under mq control with -r/--rev
2149 2149 (e.g. qimport --rev tip -n patch will place tip under mq control).
2150 2150 With -g/--git, patches imported with --rev will use the git diff
2151 2151 format. See the diffs help topic for information on why this is
2152 2152 important for preserving rename/copy information and permission
2153 2153 changes. Use :hg:`qfinish` to remove changesets from mq control.
2154 2154
2155 2155 To import a patch from standard input, pass - as the patch file.
2156 2156 When importing from standard input, a patch name must be specified
2157 2157 using the --name flag.
2158 2158
2159 2159 To import an existing patch while renaming it::
2160 2160
2161 2161 hg qimport -e existing-patch -n new-name
2162 2162
2163 2163 Returns 0 if import succeeded.
2164 2164 """
2165 2165 lock = repo.lock() # cause this may move phase
2166 2166 try:
2167 2167 q = repo.mq
2168 2168 try:
2169 2169 imported = q.qimport(
2170 2170 repo, filename, patchname=opts.get('name'),
2171 2171 existing=opts.get('existing'), force=opts.get('force'),
2172 2172 rev=opts.get('rev'), git=opts.get('git'))
2173 2173 finally:
2174 2174 q.savedirty()
2175 2175 finally:
2176 2176 lock.release()
2177 2177
2178 2178 if imported and opts.get('push') and not opts.get('rev'):
2179 2179 return q.push(repo, imported[-1])
2180 2180 return 0
2181 2181
2182 2182 def qinit(ui, repo, create):
2183 2183 """initialize a new queue repository
2184 2184
2185 2185 This command also creates a series file for ordering patches, and
2186 2186 an mq-specific .hgignore file in the queue repository, to exclude
2187 2187 the status and guards files (these contain mostly transient state).
2188 2188
2189 2189 Returns 0 if initialization succeeded."""
2190 2190 q = repo.mq
2191 2191 r = q.init(repo, create)
2192 2192 q.savedirty()
2193 2193 if r:
2194 2194 if not os.path.exists(r.wjoin('.hgignore')):
2195 2195 fp = r.wopener('.hgignore', 'w')
2196 2196 fp.write('^\\.hg\n')
2197 2197 fp.write('^\\.mq\n')
2198 2198 fp.write('syntax: glob\n')
2199 2199 fp.write('status\n')
2200 2200 fp.write('guards\n')
2201 2201 fp.close()
2202 2202 if not os.path.exists(r.wjoin('series')):
2203 2203 r.wopener('series', 'w').close()
2204 2204 r[None].add(['.hgignore', 'series'])
2205 2205 commands.add(ui, r)
2206 2206 return 0
2207 2207
2208 2208 @command("^qinit",
2209 2209 [('c', 'create-repo', None, _('create queue repository'))],
2210 2210 _('hg qinit [-c]'))
2211 2211 def init(ui, repo, **opts):
2212 2212 """init a new queue repository (DEPRECATED)
2213 2213
2214 2214 The queue repository is unversioned by default. If
2215 2215 -c/--create-repo is specified, qinit will create a separate nested
2216 2216 repository for patches (qinit -c may also be run later to convert
2217 2217 an unversioned patch repository into a versioned one). You can use
2218 2218 qcommit to commit changes to this queue repository.
2219 2219
2220 2220 This command is deprecated. Without -c, it's implied by other relevant
2221 2221 commands. With -c, use :hg:`init --mq` instead."""
2222 2222 return qinit(ui, repo, create=opts.get('create_repo'))
2223 2223
2224 2224 @command("qclone",
2225 2225 [('', 'pull', None, _('use pull protocol to copy metadata')),
2226 2226 ('U', 'noupdate', None,
2227 2227 _('do not update the new working directories')),
2228 2228 ('', 'uncompressed', None,
2229 2229 _('use uncompressed transfer (fast over LAN)')),
2230 2230 ('p', 'patches', '',
2231 2231 _('location of source patch repository'), _('REPO')),
2232 2232 ] + commands.remoteopts,
2233 2233 _('hg qclone [OPTION]... SOURCE [DEST]'))
2234 2234 def clone(ui, source, dest=None, **opts):
2235 2235 '''clone main and patch repository at same time
2236 2236
2237 2237 If source is local, destination will have no patches applied. If
2238 2238 source is remote, this command can not check if patches are
2239 2239 applied in source, so cannot guarantee that patches are not
2240 2240 applied in destination. If you clone remote repository, be sure
2241 2241 before that it has no patches applied.
2242 2242
2243 2243 Source patch repository is looked for in <src>/.hg/patches by
2244 2244 default. Use -p <url> to change.
2245 2245
2246 2246 The patch directory must be a nested Mercurial repository, as
2247 2247 would be created by :hg:`init --mq`.
2248 2248
2249 2249 Return 0 on success.
2250 2250 '''
2251 2251 def patchdir(repo):
2252 2252 """compute a patch repo url from a repo object"""
2253 2253 url = repo.url()
2254 2254 if url.endswith('/'):
2255 2255 url = url[:-1]
2256 2256 return url + '/.hg/patches'
2257 2257
2258 2258 # main repo (destination and sources)
2259 2259 if dest is None:
2260 2260 dest = hg.defaultdest(source)
2261 2261 sr = hg.peer(ui, opts, ui.expandpath(source))
2262 2262
2263 2263 # patches repo (source only)
2264 2264 if opts.get('patches'):
2265 2265 patchespath = ui.expandpath(opts.get('patches'))
2266 2266 else:
2267 2267 patchespath = patchdir(sr)
2268 2268 try:
2269 2269 hg.peer(ui, opts, patchespath)
2270 2270 except error.RepoError:
2271 2271 raise util.Abort(_('versioned patch repository not found'
2272 2272 ' (see init --mq)'))
2273 2273 qbase, destrev = None, None
2274 2274 if sr.local():
2275 2275 repo = sr.local()
2276 2276 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2277 2277 qbase = repo.mq.applied[0].node
2278 2278 if not hg.islocal(dest):
2279 2279 heads = set(repo.heads())
2280 2280 destrev = list(heads.difference(repo.heads(qbase)))
2281 2281 destrev.append(repo.changelog.parents(qbase)[0])
2282 2282 elif sr.capable('lookup'):
2283 2283 try:
2284 2284 qbase = sr.lookup('qbase')
2285 2285 except error.RepoError:
2286 2286 pass
2287 2287
2288 2288 ui.note(_('cloning main repository\n'))
2289 2289 sr, dr = hg.clone(ui, opts, sr.url(), dest,
2290 2290 pull=opts.get('pull'),
2291 2291 rev=destrev,
2292 2292 update=False,
2293 2293 stream=opts.get('uncompressed'))
2294 2294
2295 2295 ui.note(_('cloning patch repository\n'))
2296 2296 hg.clone(ui, opts, opts.get('patches') or patchdir(sr), patchdir(dr),
2297 2297 pull=opts.get('pull'), update=not opts.get('noupdate'),
2298 2298 stream=opts.get('uncompressed'))
2299 2299
2300 2300 if dr.local():
2301 2301 repo = dr.local()
2302 2302 if qbase:
2303 2303 ui.note(_('stripping applied patches from destination '
2304 2304 'repository\n'))
2305 2305 repo.mq.strip(repo, [qbase], update=False, backup=None)
2306 2306 if not opts.get('noupdate'):
2307 2307 ui.note(_('updating destination repository\n'))
2308 2308 hg.update(repo, repo.changelog.tip())
2309 2309
2310 2310 @command("qcommit|qci",
2311 2311 commands.table["^commit|ci"][1],
2312 2312 _('hg qcommit [OPTION]... [FILE]...'))
2313 2313 def commit(ui, repo, *pats, **opts):
2314 2314 """commit changes in the queue repository (DEPRECATED)
2315 2315
2316 2316 This command is deprecated; use :hg:`commit --mq` instead."""
2317 2317 q = repo.mq
2318 2318 r = q.qrepo()
2319 2319 if not r:
2320 2320 raise util.Abort('no queue repository')
2321 2321 commands.commit(r.ui, r, *pats, **opts)
2322 2322
2323 2323 @command("qseries",
2324 2324 [('m', 'missing', None, _('print patches not in series')),
2325 2325 ] + seriesopts,
2326 2326 _('hg qseries [-ms]'))
2327 2327 def series(ui, repo, **opts):
2328 2328 """print the entire series file
2329 2329
2330 2330 Returns 0 on success."""
2331 2331 repo.mq.qseries(repo, missing=opts.get('missing'),
2332 2332 summary=opts.get('summary'))
2333 2333 return 0
2334 2334
2335 2335 @command("qtop", seriesopts, _('hg qtop [-s]'))
2336 2336 def top(ui, repo, **opts):
2337 2337 """print the name of the current patch
2338 2338
2339 2339 Returns 0 on success."""
2340 2340 q = repo.mq
2341 2341 t = q.applied and q.seriesend(True) or 0
2342 2342 if t:
2343 2343 q.qseries(repo, start=t - 1, length=1, status='A',
2344 2344 summary=opts.get('summary'))
2345 2345 else:
2346 2346 ui.write(_("no patches applied\n"))
2347 2347 return 1
2348 2348
2349 2349 @command("qnext", seriesopts, _('hg qnext [-s]'))
2350 2350 def next(ui, repo, **opts):
2351 2351 """print the name of the next pushable patch
2352 2352
2353 2353 Returns 0 on success."""
2354 2354 q = repo.mq
2355 2355 end = q.seriesend()
2356 2356 if end == len(q.series):
2357 2357 ui.write(_("all patches applied\n"))
2358 2358 return 1
2359 2359 q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
2360 2360
2361 2361 @command("qprev", seriesopts, _('hg qprev [-s]'))
2362 2362 def prev(ui, repo, **opts):
2363 2363 """print the name of the preceding applied patch
2364 2364
2365 2365 Returns 0 on success."""
2366 2366 q = repo.mq
2367 2367 l = len(q.applied)
2368 2368 if l == 1:
2369 2369 ui.write(_("only one patch applied\n"))
2370 2370 return 1
2371 2371 if not l:
2372 2372 ui.write(_("no patches applied\n"))
2373 2373 return 1
2374 2374 idx = q.series.index(q.applied[-2].name)
2375 2375 q.qseries(repo, start=idx, length=1, status='A',
2376 2376 summary=opts.get('summary'))
2377 2377
2378 2378 def setupheaderopts(ui, opts):
2379 2379 if not opts.get('user') and opts.get('currentuser'):
2380 2380 opts['user'] = ui.username()
2381 2381 if not opts.get('date') and opts.get('currentdate'):
2382 2382 opts['date'] = "%d %d" % util.makedate()
2383 2383
2384 2384 @command("^qnew",
2385 2385 [('e', 'edit', None, _('edit commit message')),
2386 2386 ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')),
2387 2387 ('g', 'git', None, _('use git extended diff format')),
2388 2388 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2389 2389 ('u', 'user', '',
2390 2390 _('add "From: <USER>" to patch'), _('USER')),
2391 2391 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2392 2392 ('d', 'date', '',
2393 2393 _('add "Date: <DATE>" to patch'), _('DATE'))
2394 2394 ] + commands.walkopts + commands.commitopts,
2395 2395 _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'))
2396 2396 def new(ui, repo, patch, *args, **opts):
2397 2397 """create a new patch
2398 2398
2399 2399 qnew creates a new patch on top of the currently-applied patch (if
2400 2400 any). The patch will be initialized with any outstanding changes
2401 2401 in the working directory. You may also use -I/--include,
2402 2402 -X/--exclude, and/or a list of files after the patch name to add
2403 2403 only changes to matching files to the new patch, leaving the rest
2404 2404 as uncommitted modifications.
2405 2405
2406 2406 -u/--user and -d/--date can be used to set the (given) user and
2407 2407 date, respectively. -U/--currentuser and -D/--currentdate set user
2408 2408 to current user and date to current date.
2409 2409
2410 2410 -e/--edit, -m/--message or -l/--logfile set the patch header as
2411 2411 well as the commit message. If none is specified, the header is
2412 2412 empty and the commit message is '[mq]: PATCH'.
2413 2413
2414 2414 Use the -g/--git option to keep the patch in the git extended diff
2415 2415 format. Read the diffs help topic for more information on why this
2416 2416 is important for preserving permission changes and copy/rename
2417 2417 information.
2418 2418
2419 2419 Returns 0 on successful creation of a new patch.
2420 2420 """
2421 2421 msg = cmdutil.logmessage(ui, opts)
2422 2422 def getmsg():
2423 2423 return ui.edit(msg, opts.get('user') or ui.username())
2424 2424 q = repo.mq
2425 2425 opts['msg'] = msg
2426 2426 if opts.get('edit'):
2427 2427 opts['msg'] = getmsg
2428 2428 else:
2429 2429 opts['msg'] = msg
2430 2430 setupheaderopts(ui, opts)
2431 2431 q.new(repo, patch, *args, **opts)
2432 2432 q.savedirty()
2433 2433 return 0
2434 2434
2435 2435 @command("^qrefresh",
2436 2436 [('e', 'edit', None, _('edit commit message')),
2437 2437 ('g', 'git', None, _('use git extended diff format')),
2438 2438 ('s', 'short', None,
2439 2439 _('refresh only files already in the patch and specified files')),
2440 2440 ('U', 'currentuser', None,
2441 2441 _('add/update author field in patch with current user')),
2442 2442 ('u', 'user', '',
2443 2443 _('add/update author field in patch with given user'), _('USER')),
2444 2444 ('D', 'currentdate', None,
2445 2445 _('add/update date field in patch with current date')),
2446 2446 ('d', 'date', '',
2447 2447 _('add/update date field in patch with given date'), _('DATE'))
2448 2448 ] + commands.walkopts + commands.commitopts,
2449 2449 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'))
2450 2450 def refresh(ui, repo, *pats, **opts):
2451 2451 """update the current patch
2452 2452
2453 2453 If any file patterns are provided, the refreshed patch will
2454 2454 contain only the modifications that match those patterns; the
2455 2455 remaining modifications will remain in the working directory.
2456 2456
2457 2457 If -s/--short is specified, files currently included in the patch
2458 2458 will be refreshed just like matched files and remain in the patch.
2459 2459
2460 2460 If -e/--edit is specified, Mercurial will start your configured editor for
2461 2461 you to enter a message. In case qrefresh fails, you will find a backup of
2462 2462 your message in ``.hg/last-message.txt``.
2463 2463
2464 2464 hg add/remove/copy/rename work as usual, though you might want to
2465 2465 use git-style patches (-g/--git or [diff] git=1) to track copies
2466 2466 and renames. See the diffs help topic for more information on the
2467 2467 git diff format.
2468 2468
2469 2469 Returns 0 on success.
2470 2470 """
2471 2471 q = repo.mq
2472 2472 message = cmdutil.logmessage(ui, opts)
2473 2473 if opts.get('edit'):
2474 2474 if not q.applied:
2475 2475 ui.write(_("no patches applied\n"))
2476 2476 return 1
2477 2477 if message:
2478 2478 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
2479 2479 patch = q.applied[-1].name
2480 2480 ph = patchheader(q.join(patch), q.plainmode)
2481 2481 message = ui.edit('\n'.join(ph.message), ph.user or ui.username())
2482 2482 # We don't want to lose the patch message if qrefresh fails (issue2062)
2483 2483 repo.savecommitmessage(message)
2484 2484 setupheaderopts(ui, opts)
2485 2485 wlock = repo.wlock()
2486 2486 try:
2487 2487 ret = q.refresh(repo, pats, msg=message, **opts)
2488 2488 q.savedirty()
2489 2489 return ret
2490 2490 finally:
2491 2491 wlock.release()
2492 2492
2493 2493 @command("^qdiff",
2494 2494 commands.diffopts + commands.diffopts2 + commands.walkopts,
2495 2495 _('hg qdiff [OPTION]... [FILE]...'))
2496 2496 def diff(ui, repo, *pats, **opts):
2497 2497 """diff of the current patch and subsequent modifications
2498 2498
2499 2499 Shows a diff which includes the current patch as well as any
2500 2500 changes which have been made in the working directory since the
2501 2501 last refresh (thus showing what the current patch would become
2502 2502 after a qrefresh).
2503 2503
2504 2504 Use :hg:`diff` if you only want to see the changes made since the
2505 2505 last qrefresh, or :hg:`export qtip` if you want to see changes
2506 2506 made by the current patch without including changes made since the
2507 2507 qrefresh.
2508 2508
2509 2509 Returns 0 on success.
2510 2510 """
2511 2511 repo.mq.diff(repo, pats, opts)
2512 2512 return 0
2513 2513
2514 2514 @command('qfold',
2515 2515 [('e', 'edit', None, _('edit patch header')),
2516 2516 ('k', 'keep', None, _('keep folded patch files')),
2517 2517 ] + commands.commitopts,
2518 2518 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'))
2519 2519 def fold(ui, repo, *files, **opts):
2520 2520 """fold the named patches into the current patch
2521 2521
2522 2522 Patches must not yet be applied. Each patch will be successively
2523 2523 applied to the current patch in the order given. If all the
2524 2524 patches apply successfully, the current patch will be refreshed
2525 2525 with the new cumulative patch, and the folded patches will be
2526 2526 deleted. With -k/--keep, the folded patch files will not be
2527 2527 removed afterwards.
2528 2528
2529 2529 The header for each folded patch will be concatenated with the
2530 2530 current patch header, separated by a line of ``* * *``.
2531 2531
2532 2532 Returns 0 on success."""
2533 2533 q = repo.mq
2534 2534 if not files:
2535 2535 raise util.Abort(_('qfold requires at least one patch name'))
2536 2536 if not q.checktoppatch(repo)[0]:
2537 2537 raise util.Abort(_('no patches applied'))
2538 2538 q.checklocalchanges(repo)
2539 2539
2540 2540 message = cmdutil.logmessage(ui, opts)
2541 2541 if opts.get('edit'):
2542 2542 if message:
2543 2543 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
2544 2544
2545 2545 parent = q.lookup('qtip')
2546 2546 patches = []
2547 2547 messages = []
2548 2548 for f in files:
2549 2549 p = q.lookup(f)
2550 2550 if p in patches or p == parent:
2551 2551 ui.warn(_('skipping already folded patch %s\n') % p)
2552 2552 if q.isapplied(p):
2553 2553 raise util.Abort(_('qfold cannot fold already applied patch %s')
2554 2554 % p)
2555 2555 patches.append(p)
2556 2556
2557 2557 for p in patches:
2558 2558 if not message:
2559 2559 ph = patchheader(q.join(p), q.plainmode)
2560 2560 if ph.message:
2561 2561 messages.append(ph.message)
2562 2562 pf = q.join(p)
2563 2563 (patchsuccess, files, fuzz) = q.patch(repo, pf)
2564 2564 if not patchsuccess:
2565 2565 raise util.Abort(_('error folding patch %s') % p)
2566 2566
2567 2567 if not message:
2568 2568 ph = patchheader(q.join(parent), q.plainmode)
2569 2569 message, user = ph.message, ph.user
2570 2570 for msg in messages:
2571 2571 message.append('* * *')
2572 2572 message.extend(msg)
2573 2573 message = '\n'.join(message)
2574 2574
2575 2575 if opts.get('edit'):
2576 2576 message = ui.edit(message, user or ui.username())
2577 2577
2578 2578 diffopts = q.patchopts(q.diffopts(), *patches)
2579 2579 wlock = repo.wlock()
2580 2580 try:
2581 2581 q.refresh(repo, msg=message, git=diffopts.git)
2582 2582 q.delete(repo, patches, opts)
2583 2583 q.savedirty()
2584 2584 finally:
2585 2585 wlock.release()
2586 2586
2587 2587 @command("qgoto",
2588 2588 [('', 'keep-changes', None,
2589 2589 _('tolerate non-conflicting local changes')),
2590 2590 ('f', 'force', None, _('overwrite any local changes')),
2591 2591 ('', 'no-backup', None, _('do not save backup copies of files'))],
2592 2592 _('hg qgoto [OPTION]... PATCH'))
2593 2593 def goto(ui, repo, patch, **opts):
2594 2594 '''push or pop patches until named patch is at top of stack
2595 2595
2596 2596 Returns 0 on success.'''
2597 2597 opts = fixkeepchangesopts(ui, opts)
2598 2598 q = repo.mq
2599 2599 patch = q.lookup(patch)
2600 2600 nobackup = opts.get('no_backup')
2601 2601 keepchanges = opts.get('keep_changes')
2602 2602 if q.isapplied(patch):
2603 2603 ret = q.pop(repo, patch, force=opts.get('force'), nobackup=nobackup,
2604 2604 keepchanges=keepchanges)
2605 2605 else:
2606 2606 ret = q.push(repo, patch, force=opts.get('force'), nobackup=nobackup,
2607 2607 keepchanges=keepchanges)
2608 2608 q.savedirty()
2609 2609 return ret
2610 2610
2611 2611 @command("qguard",
2612 2612 [('l', 'list', None, _('list all patches and guards')),
2613 2613 ('n', 'none', None, _('drop all guards'))],
2614 2614 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'))
2615 2615 def guard(ui, repo, *args, **opts):
2616 2616 '''set or print guards for a patch
2617 2617
2618 2618 Guards control whether a patch can be pushed. A patch with no
2619 2619 guards is always pushed. A patch with a positive guard ("+foo") is
2620 2620 pushed only if the :hg:`qselect` command has activated it. A patch with
2621 2621 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
2622 2622 has activated it.
2623 2623
2624 2624 With no arguments, print the currently active guards.
2625 2625 With arguments, set guards for the named patch.
2626 2626
2627 2627 .. note::
2628 2628 Specifying negative guards now requires '--'.
2629 2629
2630 2630 To set guards on another patch::
2631 2631
2632 2632 hg qguard other.patch -- +2.6.17 -stable
2633 2633
2634 2634 Returns 0 on success.
2635 2635 '''
2636 2636 def status(idx):
2637 2637 guards = q.seriesguards[idx] or ['unguarded']
2638 2638 if q.series[idx] in applied:
2639 2639 state = 'applied'
2640 2640 elif q.pushable(idx)[0]:
2641 2641 state = 'unapplied'
2642 2642 else:
2643 2643 state = 'guarded'
2644 2644 label = 'qguard.patch qguard.%s qseries.%s' % (state, state)
2645 2645 ui.write('%s: ' % ui.label(q.series[idx], label))
2646 2646
2647 2647 for i, guard in enumerate(guards):
2648 2648 if guard.startswith('+'):
2649 2649 ui.write(guard, label='qguard.positive')
2650 2650 elif guard.startswith('-'):
2651 2651 ui.write(guard, label='qguard.negative')
2652 2652 else:
2653 2653 ui.write(guard, label='qguard.unguarded')
2654 2654 if i != len(guards) - 1:
2655 2655 ui.write(' ')
2656 2656 ui.write('\n')
2657 2657 q = repo.mq
2658 2658 applied = set(p.name for p in q.applied)
2659 2659 patch = None
2660 2660 args = list(args)
2661 2661 if opts.get('list'):
2662 2662 if args or opts.get('none'):
2663 2663 raise util.Abort(_('cannot mix -l/--list with options or '
2664 2664 'arguments'))
2665 2665 for i in xrange(len(q.series)):
2666 2666 status(i)
2667 2667 return
2668 2668 if not args or args[0][0:1] in '-+':
2669 2669 if not q.applied:
2670 2670 raise util.Abort(_('no patches applied'))
2671 2671 patch = q.applied[-1].name
2672 2672 if patch is None and args[0][0:1] not in '-+':
2673 2673 patch = args.pop(0)
2674 2674 if patch is None:
2675 2675 raise util.Abort(_('no patch to work with'))
2676 2676 if args or opts.get('none'):
2677 2677 idx = q.findseries(patch)
2678 2678 if idx is None:
2679 2679 raise util.Abort(_('no patch named %s') % patch)
2680 2680 q.setguards(idx, args)
2681 2681 q.savedirty()
2682 2682 else:
2683 2683 status(q.series.index(q.lookup(patch)))
2684 2684
2685 2685 @command("qheader", [], _('hg qheader [PATCH]'))
2686 2686 def header(ui, repo, patch=None):
2687 2687 """print the header of the topmost or specified patch
2688 2688
2689 2689 Returns 0 on success."""
2690 2690 q = repo.mq
2691 2691
2692 2692 if patch:
2693 2693 patch = q.lookup(patch)
2694 2694 else:
2695 2695 if not q.applied:
2696 2696 ui.write(_('no patches applied\n'))
2697 2697 return 1
2698 2698 patch = q.lookup('qtip')
2699 2699 ph = patchheader(q.join(patch), q.plainmode)
2700 2700
2701 2701 ui.write('\n'.join(ph.message) + '\n')
2702 2702
2703 2703 def lastsavename(path):
2704 2704 (directory, base) = os.path.split(path)
2705 2705 names = os.listdir(directory)
2706 2706 namere = re.compile("%s.([0-9]+)" % base)
2707 2707 maxindex = None
2708 2708 maxname = None
2709 2709 for f in names:
2710 2710 m = namere.match(f)
2711 2711 if m:
2712 2712 index = int(m.group(1))
2713 2713 if maxindex is None or index > maxindex:
2714 2714 maxindex = index
2715 2715 maxname = f
2716 2716 if maxname:
2717 2717 return (os.path.join(directory, maxname), maxindex)
2718 2718 return (None, None)
2719 2719
2720 2720 def savename(path):
2721 2721 (last, index) = lastsavename(path)
2722 2722 if last is None:
2723 2723 index = 0
2724 2724 newpath = path + ".%d" % (index + 1)
2725 2725 return newpath
2726 2726
2727 2727 @command("^qpush",
2728 2728 [('', 'keep-changes', None,
2729 2729 _('tolerate non-conflicting local changes')),
2730 2730 ('f', 'force', None, _('apply on top of local changes')),
2731 2731 ('e', 'exact', None,
2732 2732 _('apply the target patch to its recorded parent')),
2733 2733 ('l', 'list', None, _('list patch name in commit text')),
2734 2734 ('a', 'all', None, _('apply all patches')),
2735 2735 ('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
2736 2736 ('n', 'name', '',
2737 2737 _('merge queue name (DEPRECATED)'), _('NAME')),
2738 2738 ('', 'move', None,
2739 2739 _('reorder patch series and apply only the patch')),
2740 2740 ('', 'no-backup', None, _('do not save backup copies of files'))],
2741 2741 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
2742 2742 def push(ui, repo, patch=None, **opts):
2743 2743 """push the next patch onto the stack
2744 2744
2745 2745 By default, abort if the working directory contains uncommitted
2746 2746 changes. With --keep-changes, abort only if the uncommitted files
2747 2747 overlap with patched files. With -f/--force, backup and patch over
2748 2748 uncommitted changes.
2749 2749
2750 2750 Return 0 on success.
2751 2751 """
2752 2752 q = repo.mq
2753 2753 mergeq = None
2754 2754
2755 2755 opts = fixkeepchangesopts(ui, opts)
2756 2756 if opts.get('merge'):
2757 2757 if opts.get('name'):
2758 2758 newpath = repo.join(opts.get('name'))
2759 2759 else:
2760 2760 newpath, i = lastsavename(q.path)
2761 2761 if not newpath:
2762 2762 ui.warn(_("no saved queues found, please use -n\n"))
2763 2763 return 1
2764 2764 mergeq = queue(ui, repo.path, newpath)
2765 2765 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2766 2766 ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
2767 2767 mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
2768 2768 exact=opts.get('exact'), nobackup=opts.get('no_backup'),
2769 2769 keepchanges=opts.get('keep_changes'))
2770 2770 return ret
2771 2771
2772 2772 @command("^qpop",
2773 2773 [('a', 'all', None, _('pop all patches')),
2774 2774 ('n', 'name', '',
2775 2775 _('queue name to pop (DEPRECATED)'), _('NAME')),
2776 2776 ('', 'keep-changes', None,
2777 2777 _('tolerate non-conflicting local changes')),
2778 2778 ('f', 'force', None, _('forget any local changes to patched files')),
2779 2779 ('', 'no-backup', None, _('do not save backup copies of files'))],
2780 2780 _('hg qpop [-a] [-f] [PATCH | INDEX]'))
2781 2781 def pop(ui, repo, patch=None, **opts):
2782 2782 """pop the current patch off the stack
2783 2783
2784 2784 Without argument, pops off the top of the patch stack. If given a
2785 2785 patch name, keeps popping off patches until the named patch is at
2786 2786 the top of the stack.
2787 2787
2788 2788 By default, abort if the working directory contains uncommitted
2789 2789 changes. With --keep-changes, abort only if the uncommitted files
2790 2790 overlap with patched files. With -f/--force, backup and discard
2791 2791 changes made to such files.
2792 2792
2793 2793 Return 0 on success.
2794 2794 """
2795 2795 opts = fixkeepchangesopts(ui, opts)
2796 2796 localupdate = True
2797 2797 if opts.get('name'):
2798 2798 q = queue(ui, repo.path, repo.join(opts.get('name')))
2799 2799 ui.warn(_('using patch queue: %s\n') % q.path)
2800 2800 localupdate = False
2801 2801 else:
2802 2802 q = repo.mq
2803 2803 ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
2804 2804 all=opts.get('all'), nobackup=opts.get('no_backup'),
2805 2805 keepchanges=opts.get('keep_changes'))
2806 2806 q.savedirty()
2807 2807 return ret
2808 2808
2809 2809 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'))
2810 2810 def rename(ui, repo, patch, name=None, **opts):
2811 2811 """rename a patch
2812 2812
2813 2813 With one argument, renames the current patch to PATCH1.
2814 2814 With two arguments, renames PATCH1 to PATCH2.
2815 2815
2816 2816 Returns 0 on success."""
2817 2817 q = repo.mq
2818 2818 if not name:
2819 2819 name = patch
2820 2820 patch = None
2821 2821
2822 2822 if patch:
2823 2823 patch = q.lookup(patch)
2824 2824 else:
2825 2825 if not q.applied:
2826 2826 ui.write(_('no patches applied\n'))
2827 2827 return
2828 2828 patch = q.lookup('qtip')
2829 2829 absdest = q.join(name)
2830 2830 if os.path.isdir(absdest):
2831 2831 name = normname(os.path.join(name, os.path.basename(patch)))
2832 2832 absdest = q.join(name)
2833 2833 q.checkpatchname(name)
2834 2834
2835 2835 ui.note(_('renaming %s to %s\n') % (patch, name))
2836 2836 i = q.findseries(patch)
2837 2837 guards = q.guard_re.findall(q.fullseries[i])
2838 2838 q.fullseries[i] = name + ''.join([' #' + g for g in guards])
2839 2839 q.parseseries()
2840 2840 q.seriesdirty = True
2841 2841
2842 2842 info = q.isapplied(patch)
2843 2843 if info:
2844 2844 q.applied[info[0]] = statusentry(info[1], name)
2845 2845 q.applieddirty = True
2846 2846
2847 2847 destdir = os.path.dirname(absdest)
2848 2848 if not os.path.isdir(destdir):
2849 2849 os.makedirs(destdir)
2850 2850 util.rename(q.join(patch), absdest)
2851 2851 r = q.qrepo()
2852 2852 if r and patch in r.dirstate:
2853 2853 wctx = r[None]
2854 2854 wlock = r.wlock()
2855 2855 try:
2856 2856 if r.dirstate[patch] == 'a':
2857 2857 r.dirstate.drop(patch)
2858 2858 r.dirstate.add(name)
2859 2859 else:
2860 2860 wctx.copy(patch, name)
2861 2861 wctx.forget([patch])
2862 2862 finally:
2863 2863 wlock.release()
2864 2864
2865 2865 q.savedirty()
2866 2866
2867 2867 @command("qrestore",
2868 2868 [('d', 'delete', None, _('delete save entry')),
2869 2869 ('u', 'update', None, _('update queue working directory'))],
2870 2870 _('hg qrestore [-d] [-u] REV'))
2871 2871 def restore(ui, repo, rev, **opts):
2872 2872 """restore the queue state saved by a revision (DEPRECATED)
2873 2873
2874 2874 This command is deprecated, use :hg:`rebase` instead."""
2875 2875 rev = repo.lookup(rev)
2876 2876 q = repo.mq
2877 2877 q.restore(repo, rev, delete=opts.get('delete'),
2878 2878 qupdate=opts.get('update'))
2879 2879 q.savedirty()
2880 2880 return 0
2881 2881
2882 2882 @command("qsave",
2883 2883 [('c', 'copy', None, _('copy patch directory')),
2884 2884 ('n', 'name', '',
2885 2885 _('copy directory name'), _('NAME')),
2886 2886 ('e', 'empty', None, _('clear queue status file')),
2887 2887 ('f', 'force', None, _('force copy'))] + commands.commitopts,
2888 2888 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'))
2889 2889 def save(ui, repo, **opts):
2890 2890 """save current queue state (DEPRECATED)
2891 2891
2892 2892 This command is deprecated, use :hg:`rebase` instead."""
2893 2893 q = repo.mq
2894 2894 message = cmdutil.logmessage(ui, opts)
2895 2895 ret = q.save(repo, msg=message)
2896 2896 if ret:
2897 2897 return ret
2898 2898 q.savedirty() # save to .hg/patches before copying
2899 2899 if opts.get('copy'):
2900 2900 path = q.path
2901 2901 if opts.get('name'):
2902 2902 newpath = os.path.join(q.basepath, opts.get('name'))
2903 2903 if os.path.exists(newpath):
2904 2904 if not os.path.isdir(newpath):
2905 2905 raise util.Abort(_('destination %s exists and is not '
2906 2906 'a directory') % newpath)
2907 2907 if not opts.get('force'):
2908 2908 raise util.Abort(_('destination %s exists, '
2909 2909 'use -f to force') % newpath)
2910 2910 else:
2911 2911 newpath = savename(path)
2912 2912 ui.warn(_("copy %s to %s\n") % (path, newpath))
2913 2913 util.copyfiles(path, newpath)
2914 2914 if opts.get('empty'):
2915 2915 del q.applied[:]
2916 2916 q.applieddirty = True
2917 2917 q.savedirty()
2918 2918 return 0
2919 2919
2920 2920 @command("strip",
2921 2921 [
2922 2922 ('r', 'rev', [], _('strip specified revision (optional, '
2923 2923 'can specify revisions without this '
2924 2924 'option)'), _('REV')),
2925 2925 ('f', 'force', None, _('force removal of changesets, discard '
2926 2926 'uncommitted changes (no backup)')),
2927 2927 ('b', 'backup', None, _('bundle only changesets with local revision'
2928 2928 ' number greater than REV which are not'
2929 2929 ' descendants of REV (DEPRECATED)')),
2930 2930 ('', 'no-backup', None, _('no backups')),
2931 2931 ('', 'nobackup', None, _('no backups (DEPRECATED)')),
2932 2932 ('n', '', None, _('ignored (DEPRECATED)')),
2933 2933 ('k', 'keep', None, _("do not modify working copy during strip")),
2934 2934 ('B', 'bookmark', '', _("remove revs only reachable from given"
2935 2935 " bookmark"))],
2936 2936 _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
2937 2937 def strip(ui, repo, *revs, **opts):
2938 2938 """strip changesets and all their descendants from the repository
2939 2939
2940 2940 The strip command removes the specified changesets and all their
2941 2941 descendants. If the working directory has uncommitted changes, the
2942 2942 operation is aborted unless the --force flag is supplied, in which
2943 2943 case changes will be discarded.
2944 2944
2945 2945 If a parent of the working directory is stripped, then the working
2946 2946 directory will automatically be updated to the most recent
2947 2947 available ancestor of the stripped parent after the operation
2948 2948 completes.
2949 2949
2950 2950 Any stripped changesets are stored in ``.hg/strip-backup`` as a
2951 2951 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
2952 2952 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
2953 2953 where BUNDLE is the bundle file created by the strip. Note that
2954 2954 the local revision numbers will in general be different after the
2955 2955 restore.
2956 2956
2957 2957 Use the --no-backup option to discard the backup bundle once the
2958 2958 operation completes.
2959 2959
2960 2960 Strip is not a history-rewriting operation and can be used on
2961 2961 changesets in the public phase. But if the stripped changesets have
2962 2962 been pushed to a remote repository you will likely pull them again.
2963 2963
2964 2964 Return 0 on success.
2965 2965 """
2966 2966 backup = 'all'
2967 2967 if opts.get('backup'):
2968 2968 backup = 'strip'
2969 2969 elif opts.get('no_backup') or opts.get('nobackup'):
2970 2970 backup = 'none'
2971 2971
2972 2972 cl = repo.changelog
2973 2973 revs = list(revs) + opts.get('rev')
2974 2974 revs = set(scmutil.revrange(repo, revs))
2975 2975
2976 2976 if opts.get('bookmark'):
2977 2977 mark = opts.get('bookmark')
2978 2978 marks = repo._bookmarks
2979 2979 if mark not in marks:
2980 2980 raise util.Abort(_("bookmark '%s' not found") % mark)
2981 2981
2982 2982 # If the requested bookmark is not the only one pointing to a
2983 2983 # a revision we have to only delete the bookmark and not strip
2984 2984 # anything. revsets cannot detect that case.
2985 2985 uniquebm = True
2986 2986 for m, n in marks.iteritems():
2987 2987 if m != mark and n == repo[mark].node():
2988 2988 uniquebm = False
2989 2989 break
2990 2990 if uniquebm:
2991 2991 rsrevs = repo.revs("ancestors(bookmark(%s)) - "
2992 2992 "ancestors(head() and not bookmark(%s)) - "
2993 2993 "ancestors(bookmark() and not bookmark(%s))",
2994 2994 mark, mark, mark)
2995 2995 revs.update(set(rsrevs))
2996 2996 if not revs:
2997 2997 del marks[mark]
2998 2998 marks.write()
2999 2999 ui.write(_("bookmark '%s' deleted\n") % mark)
3000 3000
3001 3001 if not revs:
3002 3002 raise util.Abort(_('empty revision set'))
3003 3003
3004 3004 descendants = set(cl.descendants(revs))
3005 3005 strippedrevs = revs.union(descendants)
3006 3006 roots = revs.difference(descendants)
3007 3007
3008 3008 update = False
3009 3009 # if one of the wdir parent is stripped we'll need
3010 3010 # to update away to an earlier revision
3011 3011 for p in repo.dirstate.parents():
3012 3012 if p != nullid and cl.rev(p) in strippedrevs:
3013 3013 update = True
3014 3014 break
3015 3015
3016 3016 rootnodes = set(cl.node(r) for r in roots)
3017 3017
3018 3018 q = repo.mq
3019 3019 if q.applied:
3020 3020 # refresh queue state if we're about to strip
3021 3021 # applied patches
3022 3022 if cl.rev(repo.lookup('qtip')) in strippedrevs:
3023 3023 q.applieddirty = True
3024 3024 start = 0
3025 3025 end = len(q.applied)
3026 3026 for i, statusentry in enumerate(q.applied):
3027 3027 if statusentry.node in rootnodes:
3028 3028 # if one of the stripped roots is an applied
3029 3029 # patch, only part of the queue is stripped
3030 3030 start = i
3031 3031 break
3032 3032 del q.applied[start:end]
3033 3033 q.savedirty()
3034 3034
3035 3035 revs = sorted(rootnodes)
3036 3036 if update and opts.get('keep'):
3037 3037 wlock = repo.wlock()
3038 3038 try:
3039 3039 urev = repo.mq.qparents(repo, revs[0])
3040 repo.dirstate.rebuild(urev, repo[urev].manifest())
3040 uctx = repo[urev]
3041
3042 # only reset the dirstate for files that would actually change
3043 # between the working context and uctx
3044 descendantrevs = repo.revs("%s::." % uctx.rev())
3045 changedfiles = []
3046 for rev in descendantrevs:
3047 # blindy reset the files, regardless of what actually changed
3048 changedfiles.extend(repo[rev].files())
3049
3050 # reset files that only changed in the dirstate too
3051 dirstate = repo.dirstate
3052 dirchanges = [f for f in dirstate if dirstate[f] != 'n']
3053 changedfiles.extend(dirchanges)
3054
3055 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
3041 3056 repo.dirstate.write()
3042 3057 update = False
3043 3058 finally:
3044 3059 wlock.release()
3045 3060
3046 3061 if opts.get('bookmark'):
3047 3062 del marks[mark]
3048 3063 marks.write()
3049 3064 ui.write(_("bookmark '%s' deleted\n") % mark)
3050 3065
3051 3066 repo.mq.strip(repo, revs, backup=backup, update=update,
3052 3067 force=opts.get('force'))
3053 3068
3054 3069 return 0
3055 3070
3056 3071 @command("qselect",
3057 3072 [('n', 'none', None, _('disable all guards')),
3058 3073 ('s', 'series', None, _('list all guards in series file')),
3059 3074 ('', 'pop', None, _('pop to before first guarded applied patch')),
3060 3075 ('', 'reapply', None, _('pop, then reapply patches'))],
3061 3076 _('hg qselect [OPTION]... [GUARD]...'))
3062 3077 def select(ui, repo, *args, **opts):
3063 3078 '''set or print guarded patches to push
3064 3079
3065 3080 Use the :hg:`qguard` command to set or print guards on patch, then use
3066 3081 qselect to tell mq which guards to use. A patch will be pushed if
3067 3082 it has no guards or any positive guards match the currently
3068 3083 selected guard, but will not be pushed if any negative guards
3069 3084 match the current guard. For example::
3070 3085
3071 3086 qguard foo.patch -- -stable (negative guard)
3072 3087 qguard bar.patch +stable (positive guard)
3073 3088 qselect stable
3074 3089
3075 3090 This activates the "stable" guard. mq will skip foo.patch (because
3076 3091 it has a negative match) but push bar.patch (because it has a
3077 3092 positive match).
3078 3093
3079 3094 With no arguments, prints the currently active guards.
3080 3095 With one argument, sets the active guard.
3081 3096
3082 3097 Use -n/--none to deactivate guards (no other arguments needed).
3083 3098 When no guards are active, patches with positive guards are
3084 3099 skipped and patches with negative guards are pushed.
3085 3100
3086 3101 qselect can change the guards on applied patches. It does not pop
3087 3102 guarded patches by default. Use --pop to pop back to the last
3088 3103 applied patch that is not guarded. Use --reapply (which implies
3089 3104 --pop) to push back to the current patch afterwards, but skip
3090 3105 guarded patches.
3091 3106
3092 3107 Use -s/--series to print a list of all guards in the series file
3093 3108 (no other arguments needed). Use -v for more information.
3094 3109
3095 3110 Returns 0 on success.'''
3096 3111
3097 3112 q = repo.mq
3098 3113 guards = q.active()
3099 3114 if args or opts.get('none'):
3100 3115 old_unapplied = q.unapplied(repo)
3101 3116 old_guarded = [i for i in xrange(len(q.applied)) if
3102 3117 not q.pushable(i)[0]]
3103 3118 q.setactive(args)
3104 3119 q.savedirty()
3105 3120 if not args:
3106 3121 ui.status(_('guards deactivated\n'))
3107 3122 if not opts.get('pop') and not opts.get('reapply'):
3108 3123 unapplied = q.unapplied(repo)
3109 3124 guarded = [i for i in xrange(len(q.applied))
3110 3125 if not q.pushable(i)[0]]
3111 3126 if len(unapplied) != len(old_unapplied):
3112 3127 ui.status(_('number of unguarded, unapplied patches has '
3113 3128 'changed from %d to %d\n') %
3114 3129 (len(old_unapplied), len(unapplied)))
3115 3130 if len(guarded) != len(old_guarded):
3116 3131 ui.status(_('number of guarded, applied patches has changed '
3117 3132 'from %d to %d\n') %
3118 3133 (len(old_guarded), len(guarded)))
3119 3134 elif opts.get('series'):
3120 3135 guards = {}
3121 3136 noguards = 0
3122 3137 for gs in q.seriesguards:
3123 3138 if not gs:
3124 3139 noguards += 1
3125 3140 for g in gs:
3126 3141 guards.setdefault(g, 0)
3127 3142 guards[g] += 1
3128 3143 if ui.verbose:
3129 3144 guards['NONE'] = noguards
3130 3145 guards = guards.items()
3131 3146 guards.sort(key=lambda x: x[0][1:])
3132 3147 if guards:
3133 3148 ui.note(_('guards in series file:\n'))
3134 3149 for guard, count in guards:
3135 3150 ui.note('%2d ' % count)
3136 3151 ui.write(guard, '\n')
3137 3152 else:
3138 3153 ui.note(_('no guards in series file\n'))
3139 3154 else:
3140 3155 if guards:
3141 3156 ui.note(_('active guards:\n'))
3142 3157 for g in guards:
3143 3158 ui.write(g, '\n')
3144 3159 else:
3145 3160 ui.write(_('no active guards\n'))
3146 3161 reapply = opts.get('reapply') and q.applied and q.appliedname(-1)
3147 3162 popped = False
3148 3163 if opts.get('pop') or opts.get('reapply'):
3149 3164 for i in xrange(len(q.applied)):
3150 3165 pushable, reason = q.pushable(i)
3151 3166 if not pushable:
3152 3167 ui.status(_('popping guarded patches\n'))
3153 3168 popped = True
3154 3169 if i == 0:
3155 3170 q.pop(repo, all=True)
3156 3171 else:
3157 3172 q.pop(repo, str(i - 1))
3158 3173 break
3159 3174 if popped:
3160 3175 try:
3161 3176 if reapply:
3162 3177 ui.status(_('reapplying unguarded patches\n'))
3163 3178 q.push(repo, reapply)
3164 3179 finally:
3165 3180 q.savedirty()
3166 3181
3167 3182 @command("qfinish",
3168 3183 [('a', 'applied', None, _('finish all applied changesets'))],
3169 3184 _('hg qfinish [-a] [REV]...'))
3170 3185 def finish(ui, repo, *revrange, **opts):
3171 3186 """move applied patches into repository history
3172 3187
3173 3188 Finishes the specified revisions (corresponding to applied
3174 3189 patches) by moving them out of mq control into regular repository
3175 3190 history.
3176 3191
3177 3192 Accepts a revision range or the -a/--applied option. If --applied
3178 3193 is specified, all applied mq revisions are removed from mq
3179 3194 control. Otherwise, the given revisions must be at the base of the
3180 3195 stack of applied patches.
3181 3196
3182 3197 This can be especially useful if your changes have been applied to
3183 3198 an upstream repository, or if you are about to push your changes
3184 3199 to upstream.
3185 3200
3186 3201 Returns 0 on success.
3187 3202 """
3188 3203 if not opts.get('applied') and not revrange:
3189 3204 raise util.Abort(_('no revisions specified'))
3190 3205 elif opts.get('applied'):
3191 3206 revrange = ('qbase::qtip',) + revrange
3192 3207
3193 3208 q = repo.mq
3194 3209 if not q.applied:
3195 3210 ui.status(_('no patches applied\n'))
3196 3211 return 0
3197 3212
3198 3213 revs = scmutil.revrange(repo, revrange)
3199 3214 if repo['.'].rev() in revs and repo[None].files():
3200 3215 ui.warn(_('warning: uncommitted changes in the working directory\n'))
3201 3216 # queue.finish may changes phases but leave the responsibility to lock the
3202 3217 # repo to the caller to avoid deadlock with wlock. This command code is
3203 3218 # responsibility for this locking.
3204 3219 lock = repo.lock()
3205 3220 try:
3206 3221 q.finish(repo, revs)
3207 3222 q.savedirty()
3208 3223 finally:
3209 3224 lock.release()
3210 3225 return 0
3211 3226
3212 3227 @command("qqueue",
3213 3228 [('l', 'list', False, _('list all available queues')),
3214 3229 ('', 'active', False, _('print name of active queue')),
3215 3230 ('c', 'create', False, _('create new queue')),
3216 3231 ('', 'rename', False, _('rename active queue')),
3217 3232 ('', 'delete', False, _('delete reference to queue')),
3218 3233 ('', 'purge', False, _('delete queue, and remove patch dir')),
3219 3234 ],
3220 3235 _('[OPTION] [QUEUE]'))
3221 3236 def qqueue(ui, repo, name=None, **opts):
3222 3237 '''manage multiple patch queues
3223 3238
3224 3239 Supports switching between different patch queues, as well as creating
3225 3240 new patch queues and deleting existing ones.
3226 3241
3227 3242 Omitting a queue name or specifying -l/--list will show you the registered
3228 3243 queues - by default the "normal" patches queue is registered. The currently
3229 3244 active queue will be marked with "(active)". Specifying --active will print
3230 3245 only the name of the active queue.
3231 3246
3232 3247 To create a new queue, use -c/--create. The queue is automatically made
3233 3248 active, except in the case where there are applied patches from the
3234 3249 currently active queue in the repository. Then the queue will only be
3235 3250 created and switching will fail.
3236 3251
3237 3252 To delete an existing queue, use --delete. You cannot delete the currently
3238 3253 active queue.
3239 3254
3240 3255 Returns 0 on success.
3241 3256 '''
3242 3257 q = repo.mq
3243 3258 _defaultqueue = 'patches'
3244 3259 _allqueues = 'patches.queues'
3245 3260 _activequeue = 'patches.queue'
3246 3261
3247 3262 def _getcurrent():
3248 3263 cur = os.path.basename(q.path)
3249 3264 if cur.startswith('patches-'):
3250 3265 cur = cur[8:]
3251 3266 return cur
3252 3267
3253 3268 def _noqueues():
3254 3269 try:
3255 3270 fh = repo.opener(_allqueues, 'r')
3256 3271 fh.close()
3257 3272 except IOError:
3258 3273 return True
3259 3274
3260 3275 return False
3261 3276
3262 3277 def _getqueues():
3263 3278 current = _getcurrent()
3264 3279
3265 3280 try:
3266 3281 fh = repo.opener(_allqueues, 'r')
3267 3282 queues = [queue.strip() for queue in fh if queue.strip()]
3268 3283 fh.close()
3269 3284 if current not in queues:
3270 3285 queues.append(current)
3271 3286 except IOError:
3272 3287 queues = [_defaultqueue]
3273 3288
3274 3289 return sorted(queues)
3275 3290
3276 3291 def _setactive(name):
3277 3292 if q.applied:
3278 3293 raise util.Abort(_('new queue created, but cannot make active '
3279 3294 'as patches are applied'))
3280 3295 _setactivenocheck(name)
3281 3296
3282 3297 def _setactivenocheck(name):
3283 3298 fh = repo.opener(_activequeue, 'w')
3284 3299 if name != 'patches':
3285 3300 fh.write(name)
3286 3301 fh.close()
3287 3302
3288 3303 def _addqueue(name):
3289 3304 fh = repo.opener(_allqueues, 'a')
3290 3305 fh.write('%s\n' % (name,))
3291 3306 fh.close()
3292 3307
3293 3308 def _queuedir(name):
3294 3309 if name == 'patches':
3295 3310 return repo.join('patches')
3296 3311 else:
3297 3312 return repo.join('patches-' + name)
3298 3313
3299 3314 def _validname(name):
3300 3315 for n in name:
3301 3316 if n in ':\\/.':
3302 3317 return False
3303 3318 return True
3304 3319
3305 3320 def _delete(name):
3306 3321 if name not in existing:
3307 3322 raise util.Abort(_('cannot delete queue that does not exist'))
3308 3323
3309 3324 current = _getcurrent()
3310 3325
3311 3326 if name == current:
3312 3327 raise util.Abort(_('cannot delete currently active queue'))
3313 3328
3314 3329 fh = repo.opener('patches.queues.new', 'w')
3315 3330 for queue in existing:
3316 3331 if queue == name:
3317 3332 continue
3318 3333 fh.write('%s\n' % (queue,))
3319 3334 fh.close()
3320 3335 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3321 3336
3322 3337 if not name or opts.get('list') or opts.get('active'):
3323 3338 current = _getcurrent()
3324 3339 if opts.get('active'):
3325 3340 ui.write('%s\n' % (current,))
3326 3341 return
3327 3342 for queue in _getqueues():
3328 3343 ui.write('%s' % (queue,))
3329 3344 if queue == current and not ui.quiet:
3330 3345 ui.write(_(' (active)\n'))
3331 3346 else:
3332 3347 ui.write('\n')
3333 3348 return
3334 3349
3335 3350 if not _validname(name):
3336 3351 raise util.Abort(
3337 3352 _('invalid queue name, may not contain the characters ":\\/."'))
3338 3353
3339 3354 existing = _getqueues()
3340 3355
3341 3356 if opts.get('create'):
3342 3357 if name in existing:
3343 3358 raise util.Abort(_('queue "%s" already exists') % name)
3344 3359 if _noqueues():
3345 3360 _addqueue(_defaultqueue)
3346 3361 _addqueue(name)
3347 3362 _setactive(name)
3348 3363 elif opts.get('rename'):
3349 3364 current = _getcurrent()
3350 3365 if name == current:
3351 3366 raise util.Abort(_('can\'t rename "%s" to its current name') % name)
3352 3367 if name in existing:
3353 3368 raise util.Abort(_('queue "%s" already exists') % name)
3354 3369
3355 3370 olddir = _queuedir(current)
3356 3371 newdir = _queuedir(name)
3357 3372
3358 3373 if os.path.exists(newdir):
3359 3374 raise util.Abort(_('non-queue directory "%s" already exists') %
3360 3375 newdir)
3361 3376
3362 3377 fh = repo.opener('patches.queues.new', 'w')
3363 3378 for queue in existing:
3364 3379 if queue == current:
3365 3380 fh.write('%s\n' % (name,))
3366 3381 if os.path.exists(olddir):
3367 3382 util.rename(olddir, newdir)
3368 3383 else:
3369 3384 fh.write('%s\n' % (queue,))
3370 3385 fh.close()
3371 3386 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3372 3387 _setactivenocheck(name)
3373 3388 elif opts.get('delete'):
3374 3389 _delete(name)
3375 3390 elif opts.get('purge'):
3376 3391 if name in existing:
3377 3392 _delete(name)
3378 3393 qdir = _queuedir(name)
3379 3394 if os.path.exists(qdir):
3380 3395 shutil.rmtree(qdir)
3381 3396 else:
3382 3397 if name not in existing:
3383 3398 raise util.Abort(_('use --create to create a new queue'))
3384 3399 _setactive(name)
3385 3400
3386 3401 def mqphasedefaults(repo, roots):
3387 3402 """callback used to set mq changeset as secret when no phase data exists"""
3388 3403 if repo.mq.applied:
3389 3404 if repo.ui.configbool('mq', 'secret', False):
3390 3405 mqphase = phases.secret
3391 3406 else:
3392 3407 mqphase = phases.draft
3393 3408 qbase = repo[repo.mq.applied[0].node]
3394 3409 roots[mqphase].add(qbase.node())
3395 3410 return roots
3396 3411
3397 3412 def reposetup(ui, repo):
3398 3413 class mqrepo(repo.__class__):
3399 3414 @util.propertycache
3400 3415 def mq(self):
3401 3416 return queue(self.ui, self.path)
3402 3417
3403 3418 def abortifwdirpatched(self, errmsg, force=False):
3404 3419 if self.mq.applied and not force:
3405 3420 parents = self.dirstate.parents()
3406 3421 patches = [s.node for s in self.mq.applied]
3407 3422 if parents[0] in patches or parents[1] in patches:
3408 3423 raise util.Abort(errmsg)
3409 3424
3410 3425 def commit(self, text="", user=None, date=None, match=None,
3411 3426 force=False, editor=False, extra={}):
3412 3427 self.abortifwdirpatched(
3413 3428 _('cannot commit over an applied mq patch'),
3414 3429 force)
3415 3430
3416 3431 return super(mqrepo, self).commit(text, user, date, match, force,
3417 3432 editor, extra)
3418 3433
3419 3434 def checkpush(self, force, revs):
3420 3435 if self.mq.applied and not force:
3421 3436 outapplied = [e.node for e in self.mq.applied]
3422 3437 if revs:
3423 3438 # Assume applied patches have no non-patch descendants and
3424 3439 # are not on remote already. Filtering any changeset not
3425 3440 # pushed.
3426 3441 heads = set(revs)
3427 3442 for node in reversed(outapplied):
3428 3443 if node in heads:
3429 3444 break
3430 3445 else:
3431 3446 outapplied.pop()
3432 3447 # looking for pushed and shared changeset
3433 3448 for node in outapplied:
3434 3449 if self[node].phase() < phases.secret:
3435 3450 raise util.Abort(_('source has mq patches applied'))
3436 3451 # no non-secret patches pushed
3437 3452 super(mqrepo, self).checkpush(force, revs)
3438 3453
3439 3454 def _findtags(self):
3440 3455 '''augment tags from base class with patch tags'''
3441 3456 result = super(mqrepo, self)._findtags()
3442 3457
3443 3458 q = self.mq
3444 3459 if not q.applied:
3445 3460 return result
3446 3461
3447 3462 mqtags = [(patch.node, patch.name) for patch in q.applied]
3448 3463
3449 3464 try:
3450 3465 # for now ignore filtering business
3451 3466 self.unfiltered().changelog.rev(mqtags[-1][0])
3452 3467 except error.LookupError:
3453 3468 self.ui.warn(_('mq status file refers to unknown node %s\n')
3454 3469 % short(mqtags[-1][0]))
3455 3470 return result
3456 3471
3457 3472 # do not add fake tags for filtered revisions
3458 3473 included = self.changelog.hasnode
3459 3474 mqtags = [mqt for mqt in mqtags if included(mqt[0])]
3460 3475 if not mqtags:
3461 3476 return result
3462 3477
3463 3478 mqtags.append((mqtags[-1][0], 'qtip'))
3464 3479 mqtags.append((mqtags[0][0], 'qbase'))
3465 3480 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
3466 3481 tags = result[0]
3467 3482 for patch in mqtags:
3468 3483 if patch[1] in tags:
3469 3484 self.ui.warn(_('tag %s overrides mq patch of the same '
3470 3485 'name\n') % patch[1])
3471 3486 else:
3472 3487 tags[patch[1]] = patch[0]
3473 3488
3474 3489 return result
3475 3490
3476 3491 if repo.local():
3477 3492 repo.__class__ = mqrepo
3478 3493
3479 3494 repo._phasedefaults.append(mqphasedefaults)
3480 3495
3481 3496 def mqimport(orig, ui, repo, *args, **kwargs):
3482 3497 if (util.safehasattr(repo, 'abortifwdirpatched')
3483 3498 and not kwargs.get('no_commit', False)):
3484 3499 repo.abortifwdirpatched(_('cannot import over an applied patch'),
3485 3500 kwargs.get('force'))
3486 3501 return orig(ui, repo, *args, **kwargs)
3487 3502
3488 3503 def mqinit(orig, ui, *args, **kwargs):
3489 3504 mq = kwargs.pop('mq', None)
3490 3505
3491 3506 if not mq:
3492 3507 return orig(ui, *args, **kwargs)
3493 3508
3494 3509 if args:
3495 3510 repopath = args[0]
3496 3511 if not hg.islocal(repopath):
3497 3512 raise util.Abort(_('only a local queue repository '
3498 3513 'may be initialized'))
3499 3514 else:
3500 3515 repopath = cmdutil.findrepo(os.getcwd())
3501 3516 if not repopath:
3502 3517 raise util.Abort(_('there is no Mercurial repository here '
3503 3518 '(.hg not found)'))
3504 3519 repo = hg.repository(ui, repopath)
3505 3520 return qinit(ui, repo, True)
3506 3521
3507 3522 def mqcommand(orig, ui, repo, *args, **kwargs):
3508 3523 """Add --mq option to operate on patch repository instead of main"""
3509 3524
3510 3525 # some commands do not like getting unknown options
3511 3526 mq = kwargs.pop('mq', None)
3512 3527
3513 3528 if not mq:
3514 3529 return orig(ui, repo, *args, **kwargs)
3515 3530
3516 3531 q = repo.mq
3517 3532 r = q.qrepo()
3518 3533 if not r:
3519 3534 raise util.Abort(_('no queue repository'))
3520 3535 return orig(r.ui, r, *args, **kwargs)
3521 3536
3522 3537 def summary(orig, ui, repo, *args, **kwargs):
3523 3538 r = orig(ui, repo, *args, **kwargs)
3524 3539 q = repo.mq
3525 3540 m = []
3526 3541 a, u = len(q.applied), len(q.unapplied(repo))
3527 3542 if a:
3528 3543 m.append(ui.label(_("%d applied"), 'qseries.applied') % a)
3529 3544 if u:
3530 3545 m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u)
3531 3546 if m:
3532 3547 # i18n: column positioning for "hg summary"
3533 3548 ui.write(_("mq: %s\n") % ', '.join(m))
3534 3549 else:
3535 3550 # i18n: column positioning for "hg summary"
3536 3551 ui.note(_("mq: (empty queue)\n"))
3537 3552 return r
3538 3553
3539 3554 def revsetmq(repo, subset, x):
3540 3555 """``mq()``
3541 3556 Changesets managed by MQ.
3542 3557 """
3543 3558 revset.getargs(x, 0, 0, _("mq takes no arguments"))
3544 3559 applied = set([repo[r.node].rev() for r in repo.mq.applied])
3545 3560 return [r for r in subset if r in applied]
3546 3561
3547 3562 # tell hggettext to extract docstrings from these functions:
3548 3563 i18nfunctions = [revsetmq]
3549 3564
3550 3565 def extsetup(ui):
3551 3566 # Ensure mq wrappers are called first, regardless of extension load order by
3552 3567 # NOT wrapping in uisetup() and instead deferring to init stage two here.
3553 3568 mqopt = [('', 'mq', None, _("operate on patch repository"))]
3554 3569
3555 3570 extensions.wrapcommand(commands.table, 'import', mqimport)
3556 3571 extensions.wrapcommand(commands.table, 'summary', summary)
3557 3572
3558 3573 entry = extensions.wrapcommand(commands.table, 'init', mqinit)
3559 3574 entry[1].extend(mqopt)
3560 3575
3561 3576 nowrap = set(commands.norepo.split(" "))
3562 3577
3563 3578 def dotable(cmdtable):
3564 3579 for cmd in cmdtable.keys():
3565 3580 cmd = cmdutil.parsealiases(cmd)[0]
3566 3581 if cmd in nowrap:
3567 3582 continue
3568 3583 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
3569 3584 entry[1].extend(mqopt)
3570 3585
3571 3586 dotable(commands.table)
3572 3587
3573 3588 for extname, extmodule in extensions.extensions():
3574 3589 if extmodule.__file__ != __file__:
3575 3590 dotable(getattr(extmodule, 'cmdtable', {}))
3576 3591
3577 3592 revset.symbols['mq'] = revsetmq
3578 3593
3579 3594 colortable = {'qguard.negative': 'red',
3580 3595 'qguard.positive': 'yellow',
3581 3596 'qguard.unguarded': 'green',
3582 3597 'qseries.applied': 'blue bold underline',
3583 3598 'qseries.guarded': 'black bold',
3584 3599 'qseries.missing': 'red bold',
3585 3600 'qseries.unapplied': 'black bold'}
3586 3601
3587 3602 commands.inferrepo += " qnew qrefresh qdiff qcommit"
@@ -1,820 +1,825
1 1 # dirstate.py - working directory tracking for 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 import errno
8 8
9 9 from node import nullid
10 10 from i18n import _
11 11 import scmutil, util, ignore, osutil, parsers, encoding
12 12 import os, stat, errno, gc
13 13
14 14 propertycache = util.propertycache
15 15 filecache = scmutil.filecache
16 16 _rangemask = 0x7fffffff
17 17
18 18 class repocache(filecache):
19 19 """filecache for files in .hg/"""
20 20 def join(self, obj, fname):
21 21 return obj._opener.join(fname)
22 22
23 23 class rootcache(filecache):
24 24 """filecache for files in the repository root"""
25 25 def join(self, obj, fname):
26 26 return obj._join(fname)
27 27
28 28 def _finddirs(path):
29 29 pos = path.rfind('/')
30 30 while pos != -1:
31 31 yield path[:pos]
32 32 pos = path.rfind('/', 0, pos)
33 33
34 34 def _incdirs(dirs, path):
35 35 for base in _finddirs(path):
36 36 if base in dirs:
37 37 dirs[base] += 1
38 38 return
39 39 dirs[base] = 1
40 40
41 41 def _decdirs(dirs, path):
42 42 for base in _finddirs(path):
43 43 if dirs[base] > 1:
44 44 dirs[base] -= 1
45 45 return
46 46 del dirs[base]
47 47
48 48 class dirstate(object):
49 49
50 50 def __init__(self, opener, ui, root, validate):
51 51 '''Create a new dirstate object.
52 52
53 53 opener is an open()-like callable that can be used to open the
54 54 dirstate file; root is the root of the directory tracked by
55 55 the dirstate.
56 56 '''
57 57 self._opener = opener
58 58 self._validate = validate
59 59 self._root = root
60 60 self._rootdir = os.path.join(root, '')
61 61 self._dirty = False
62 62 self._dirtypl = False
63 63 self._lastnormaltime = 0
64 64 self._ui = ui
65 65 self._filecache = {}
66 66
67 67 @propertycache
68 68 def _map(self):
69 69 '''Return the dirstate contents as a map from filename to
70 70 (state, mode, size, time).'''
71 71 self._read()
72 72 return self._map
73 73
74 74 @propertycache
75 75 def _copymap(self):
76 76 self._read()
77 77 return self._copymap
78 78
79 79 @propertycache
80 80 def _foldmap(self):
81 81 f = {}
82 82 for name in self._map:
83 83 f[util.normcase(name)] = name
84 84 for name in self._dirs:
85 85 f[util.normcase(name)] = name
86 86 f['.'] = '.' # prevents useless util.fspath() invocation
87 87 return f
88 88
89 89 @repocache('branch')
90 90 def _branch(self):
91 91 try:
92 92 return self._opener.read("branch").strip() or "default"
93 93 except IOError, inst:
94 94 if inst.errno != errno.ENOENT:
95 95 raise
96 96 return "default"
97 97
98 98 @propertycache
99 99 def _pl(self):
100 100 try:
101 101 fp = self._opener("dirstate")
102 102 st = fp.read(40)
103 103 fp.close()
104 104 l = len(st)
105 105 if l == 40:
106 106 return st[:20], st[20:40]
107 107 elif l > 0 and l < 40:
108 108 raise util.Abort(_('working directory state appears damaged!'))
109 109 except IOError, err:
110 110 if err.errno != errno.ENOENT:
111 111 raise
112 112 return [nullid, nullid]
113 113
114 114 @propertycache
115 115 def _dirs(self):
116 116 dirs = {}
117 117 for f, s in self._map.iteritems():
118 118 if s[0] != 'r':
119 119 _incdirs(dirs, f)
120 120 return dirs
121 121
122 122 def dirs(self):
123 123 return self._dirs
124 124
125 125 @rootcache('.hgignore')
126 126 def _ignore(self):
127 127 files = [self._join('.hgignore')]
128 128 for name, path in self._ui.configitems("ui"):
129 129 if name == 'ignore' or name.startswith('ignore.'):
130 130 files.append(util.expandpath(path))
131 131 return ignore.ignore(self._root, files, self._ui.warn)
132 132
133 133 @propertycache
134 134 def _slash(self):
135 135 return self._ui.configbool('ui', 'slash') and os.sep != '/'
136 136
137 137 @propertycache
138 138 def _checklink(self):
139 139 return util.checklink(self._root)
140 140
141 141 @propertycache
142 142 def _checkexec(self):
143 143 return util.checkexec(self._root)
144 144
145 145 @propertycache
146 146 def _checkcase(self):
147 147 return not util.checkcase(self._join('.hg'))
148 148
149 149 def _join(self, f):
150 150 # much faster than os.path.join()
151 151 # it's safe because f is always a relative path
152 152 return self._rootdir + f
153 153
154 154 def flagfunc(self, buildfallback):
155 155 if self._checklink and self._checkexec:
156 156 def f(x):
157 157 p = self._join(x)
158 158 if os.path.islink(p):
159 159 return 'l'
160 160 if util.isexec(p):
161 161 return 'x'
162 162 return ''
163 163 return f
164 164
165 165 fallback = buildfallback()
166 166 if self._checklink:
167 167 def f(x):
168 168 if os.path.islink(self._join(x)):
169 169 return 'l'
170 170 if 'x' in fallback(x):
171 171 return 'x'
172 172 return ''
173 173 return f
174 174 if self._checkexec:
175 175 def f(x):
176 176 if 'l' in fallback(x):
177 177 return 'l'
178 178 if util.isexec(self._join(x)):
179 179 return 'x'
180 180 return ''
181 181 return f
182 182 else:
183 183 return fallback
184 184
185 185 def getcwd(self):
186 186 cwd = os.getcwd()
187 187 if cwd == self._root:
188 188 return ''
189 189 # self._root ends with a path separator if self._root is '/' or 'C:\'
190 190 rootsep = self._root
191 191 if not util.endswithsep(rootsep):
192 192 rootsep += os.sep
193 193 if cwd.startswith(rootsep):
194 194 return cwd[len(rootsep):]
195 195 else:
196 196 # we're outside the repo. return an absolute path.
197 197 return cwd
198 198
199 199 def pathto(self, f, cwd=None):
200 200 if cwd is None:
201 201 cwd = self.getcwd()
202 202 path = util.pathto(self._root, cwd, f)
203 203 if self._slash:
204 204 return util.normpath(path)
205 205 return path
206 206
207 207 def __getitem__(self, key):
208 208 '''Return the current state of key (a filename) in the dirstate.
209 209
210 210 States are:
211 211 n normal
212 212 m needs merging
213 213 r marked for removal
214 214 a marked for addition
215 215 ? not tracked
216 216 '''
217 217 return self._map.get(key, ("?",))[0]
218 218
219 219 def __contains__(self, key):
220 220 return key in self._map
221 221
222 222 def __iter__(self):
223 223 for x in sorted(self._map):
224 224 yield x
225 225
226 226 def parents(self):
227 227 return [self._validate(p) for p in self._pl]
228 228
229 229 def p1(self):
230 230 return self._validate(self._pl[0])
231 231
232 232 def p2(self):
233 233 return self._validate(self._pl[1])
234 234
235 235 def branch(self):
236 236 return encoding.tolocal(self._branch)
237 237
238 238 def setparents(self, p1, p2=nullid):
239 239 """Set dirstate parents to p1 and p2.
240 240
241 241 When moving from two parents to one, 'm' merged entries a
242 242 adjusted to normal and previous copy records discarded and
243 243 returned by the call.
244 244
245 245 See localrepo.setparents()
246 246 """
247 247 self._dirty = self._dirtypl = True
248 248 oldp2 = self._pl[1]
249 249 self._pl = p1, p2
250 250 copies = {}
251 251 if oldp2 != nullid and p2 == nullid:
252 252 # Discard 'm' markers when moving away from a merge state
253 253 for f, s in self._map.iteritems():
254 254 if s[0] == 'm':
255 255 if f in self._copymap:
256 256 copies[f] = self._copymap[f]
257 257 self.normallookup(f)
258 258 return copies
259 259
260 260 def setbranch(self, branch):
261 261 self._branch = encoding.fromlocal(branch)
262 262 f = self._opener('branch', 'w', atomictemp=True)
263 263 try:
264 264 f.write(self._branch + '\n')
265 265 f.close()
266 266
267 267 # make sure filecache has the correct stat info for _branch after
268 268 # replacing the underlying file
269 269 ce = self._filecache['_branch']
270 270 if ce:
271 271 ce.refresh()
272 272 except: # re-raises
273 273 f.discard()
274 274 raise
275 275
276 276 def _read(self):
277 277 self._map = {}
278 278 self._copymap = {}
279 279 try:
280 280 st = self._opener.read("dirstate")
281 281 except IOError, err:
282 282 if err.errno != errno.ENOENT:
283 283 raise
284 284 return
285 285 if not st:
286 286 return
287 287
288 288 # Python's garbage collector triggers a GC each time a certain number
289 289 # of container objects (the number being defined by
290 290 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
291 291 # for each file in the dirstate. The C version then immediately marks
292 292 # them as not to be tracked by the collector. However, this has no
293 293 # effect on when GCs are triggered, only on what objects the GC looks
294 294 # into. This means that O(number of files) GCs are unavoidable.
295 295 # Depending on when in the process's lifetime the dirstate is parsed,
296 296 # this can get very expensive. As a workaround, disable GC while
297 297 # parsing the dirstate.
298 298 gcenabled = gc.isenabled()
299 299 gc.disable()
300 300 try:
301 301 p = parsers.parse_dirstate(self._map, self._copymap, st)
302 302 finally:
303 303 if gcenabled:
304 304 gc.enable()
305 305 if not self._dirtypl:
306 306 self._pl = p
307 307
308 308 def invalidate(self):
309 309 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
310 310 "_ignore"):
311 311 if a in self.__dict__:
312 312 delattr(self, a)
313 313 self._lastnormaltime = 0
314 314 self._dirty = False
315 315
316 316 def copy(self, source, dest):
317 317 """Mark dest as a copy of source. Unmark dest if source is None."""
318 318 if source == dest:
319 319 return
320 320 self._dirty = True
321 321 if source is not None:
322 322 self._copymap[dest] = source
323 323 elif dest in self._copymap:
324 324 del self._copymap[dest]
325 325
326 326 def copied(self, file):
327 327 return self._copymap.get(file, None)
328 328
329 329 def copies(self):
330 330 return self._copymap
331 331
332 332 def _droppath(self, f):
333 333 if self[f] not in "?r" and "_dirs" in self.__dict__:
334 334 _decdirs(self._dirs, f)
335 335
336 336 def _addpath(self, f, state, mode, size, mtime):
337 337 oldstate = self[f]
338 338 if state == 'a' or oldstate == 'r':
339 339 scmutil.checkfilename(f)
340 340 if f in self._dirs:
341 341 raise util.Abort(_('directory %r already in dirstate') % f)
342 342 # shadows
343 343 for d in _finddirs(f):
344 344 if d in self._dirs:
345 345 break
346 346 if d in self._map and self[d] != 'r':
347 347 raise util.Abort(
348 348 _('file %r in dirstate clashes with %r') % (d, f))
349 349 if oldstate in "?r" and "_dirs" in self.__dict__:
350 350 _incdirs(self._dirs, f)
351 351 self._dirty = True
352 352 self._map[f] = (state, mode, size, mtime)
353 353
354 354 def normal(self, f):
355 355 '''Mark a file normal and clean.'''
356 356 s = os.lstat(self._join(f))
357 357 mtime = int(s.st_mtime)
358 358 self._addpath(f, 'n', s.st_mode,
359 359 s.st_size & _rangemask, mtime & _rangemask)
360 360 if f in self._copymap:
361 361 del self._copymap[f]
362 362 if mtime > self._lastnormaltime:
363 363 # Remember the most recent modification timeslot for status(),
364 364 # to make sure we won't miss future size-preserving file content
365 365 # modifications that happen within the same timeslot.
366 366 self._lastnormaltime = mtime
367 367
368 368 def normallookup(self, f):
369 369 '''Mark a file normal, but possibly dirty.'''
370 370 if self._pl[1] != nullid and f in self._map:
371 371 # if there is a merge going on and the file was either
372 372 # in state 'm' (-1) or coming from other parent (-2) before
373 373 # being removed, restore that state.
374 374 entry = self._map[f]
375 375 if entry[0] == 'r' and entry[2] in (-1, -2):
376 376 source = self._copymap.get(f)
377 377 if entry[2] == -1:
378 378 self.merge(f)
379 379 elif entry[2] == -2:
380 380 self.otherparent(f)
381 381 if source:
382 382 self.copy(source, f)
383 383 return
384 384 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
385 385 return
386 386 self._addpath(f, 'n', 0, -1, -1)
387 387 if f in self._copymap:
388 388 del self._copymap[f]
389 389
390 390 def otherparent(self, f):
391 391 '''Mark as coming from the other parent, always dirty.'''
392 392 if self._pl[1] == nullid:
393 393 raise util.Abort(_("setting %r to other parent "
394 394 "only allowed in merges") % f)
395 395 self._addpath(f, 'n', 0, -2, -1)
396 396 if f in self._copymap:
397 397 del self._copymap[f]
398 398
399 399 def add(self, f):
400 400 '''Mark a file added.'''
401 401 self._addpath(f, 'a', 0, -1, -1)
402 402 if f in self._copymap:
403 403 del self._copymap[f]
404 404
405 405 def remove(self, f):
406 406 '''Mark a file removed.'''
407 407 self._dirty = True
408 408 self._droppath(f)
409 409 size = 0
410 410 if self._pl[1] != nullid and f in self._map:
411 411 # backup the previous state
412 412 entry = self._map[f]
413 413 if entry[0] == 'm': # merge
414 414 size = -1
415 415 elif entry[0] == 'n' and entry[2] == -2: # other parent
416 416 size = -2
417 417 self._map[f] = ('r', 0, size, 0)
418 418 if size == 0 and f in self._copymap:
419 419 del self._copymap[f]
420 420
421 421 def merge(self, f):
422 422 '''Mark a file merged.'''
423 423 if self._pl[1] == nullid:
424 424 return self.normallookup(f)
425 425 s = os.lstat(self._join(f))
426 426 self._addpath(f, 'm', s.st_mode,
427 427 s.st_size & _rangemask, int(s.st_mtime) & _rangemask)
428 428 if f in self._copymap:
429 429 del self._copymap[f]
430 430
431 431 def drop(self, f):
432 432 '''Drop a file from the dirstate'''
433 433 if f in self._map:
434 434 self._dirty = True
435 435 self._droppath(f)
436 436 del self._map[f]
437 437
438 438 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
439 439 normed = util.normcase(path)
440 440 folded = self._foldmap.get(normed, None)
441 441 if folded is None:
442 442 if isknown:
443 443 folded = path
444 444 else:
445 445 if exists is None:
446 446 exists = os.path.lexists(os.path.join(self._root, path))
447 447 if not exists:
448 448 # Maybe a path component exists
449 449 if not ignoremissing and '/' in path:
450 450 d, f = path.rsplit('/', 1)
451 451 d = self._normalize(d, isknown, ignoremissing, None)
452 452 folded = d + "/" + f
453 453 else:
454 454 # No path components, preserve original case
455 455 folded = path
456 456 else:
457 457 # recursively normalize leading directory components
458 458 # against dirstate
459 459 if '/' in normed:
460 460 d, f = normed.rsplit('/', 1)
461 461 d = self._normalize(d, isknown, ignoremissing, True)
462 462 r = self._root + "/" + d
463 463 folded = d + "/" + util.fspath(f, r)
464 464 else:
465 465 folded = util.fspath(normed, self._root)
466 466 self._foldmap[normed] = folded
467 467
468 468 return folded
469 469
470 470 def normalize(self, path, isknown=False, ignoremissing=False):
471 471 '''
472 472 normalize the case of a pathname when on a casefolding filesystem
473 473
474 474 isknown specifies whether the filename came from walking the
475 475 disk, to avoid extra filesystem access.
476 476
477 477 If ignoremissing is True, missing path are returned
478 478 unchanged. Otherwise, we try harder to normalize possibly
479 479 existing path components.
480 480
481 481 The normalized case is determined based on the following precedence:
482 482
483 483 - version of name already stored in the dirstate
484 484 - version of name stored on disk
485 485 - version provided via command arguments
486 486 '''
487 487
488 488 if self._checkcase:
489 489 return self._normalize(path, isknown, ignoremissing)
490 490 return path
491 491
492 492 def clear(self):
493 493 self._map = {}
494 494 if "_dirs" in self.__dict__:
495 495 delattr(self, "_dirs")
496 496 self._copymap = {}
497 497 self._pl = [nullid, nullid]
498 498 self._lastnormaltime = 0
499 499 self._dirty = True
500 500
501 def rebuild(self, parent, files):
501 def rebuild(self, parent, allfiles, changedfiles=None):
502 changedfiles = changedfiles or allfiles
503 oldmap = self._map
502 504 self.clear()
503 for f in files:
504 if 'x' in files.flags(f):
505 self._map[f] = ('n', 0777, -1, 0)
505 for f in allfiles:
506 if f not in changedfiles:
507 self._map[f] = oldmap[f]
506 508 else:
507 self._map[f] = ('n', 0666, -1, 0)
509 if 'x' in allfiles.flags(f):
510 self._map[f] = ('n', 0777, -1, 0)
511 else:
512 self._map[f] = ('n', 0666, -1, 0)
508 513 self._pl = (parent, nullid)
509 514 self._dirty = True
510 515
511 516 def write(self):
512 517 if not self._dirty:
513 518 return
514 519 st = self._opener("dirstate", "w", atomictemp=True)
515 520
516 521 def finish(s):
517 522 st.write(s)
518 523 st.close()
519 524 self._lastnormaltime = 0
520 525 self._dirty = self._dirtypl = False
521 526
522 527 # use the modification time of the newly created temporary file as the
523 528 # filesystem's notion of 'now'
524 529 now = util.fstat(st).st_mtime
525 530 finish(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
526 531
527 532 def _dirignore(self, f):
528 533 if f == '.':
529 534 return False
530 535 if self._ignore(f):
531 536 return True
532 537 for p in _finddirs(f):
533 538 if self._ignore(p):
534 539 return True
535 540 return False
536 541
537 542 def walk(self, match, subrepos, unknown, ignored):
538 543 '''
539 544 Walk recursively through the directory tree, finding all files
540 545 matched by match.
541 546
542 547 Return a dict mapping filename to stat-like object (either
543 548 mercurial.osutil.stat instance or return value of os.stat()).
544 549 '''
545 550
546 551 def fwarn(f, msg):
547 552 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
548 553 return False
549 554
550 555 def badtype(mode):
551 556 kind = _('unknown')
552 557 if stat.S_ISCHR(mode):
553 558 kind = _('character device')
554 559 elif stat.S_ISBLK(mode):
555 560 kind = _('block device')
556 561 elif stat.S_ISFIFO(mode):
557 562 kind = _('fifo')
558 563 elif stat.S_ISSOCK(mode):
559 564 kind = _('socket')
560 565 elif stat.S_ISDIR(mode):
561 566 kind = _('directory')
562 567 return _('unsupported file type (type is %s)') % kind
563 568
564 569 ignore = self._ignore
565 570 dirignore = self._dirignore
566 571 if ignored:
567 572 ignore = util.never
568 573 dirignore = util.never
569 574 elif not unknown:
570 575 # if unknown and ignored are False, skip step 2
571 576 ignore = util.always
572 577 dirignore = util.always
573 578
574 579 matchfn = match.matchfn
575 580 badfn = match.bad
576 581 dmap = self._map
577 582 normpath = util.normpath
578 583 listdir = osutil.listdir
579 584 lstat = os.lstat
580 585 getkind = stat.S_IFMT
581 586 dirkind = stat.S_IFDIR
582 587 regkind = stat.S_IFREG
583 588 lnkkind = stat.S_IFLNK
584 589 join = self._join
585 590 work = []
586 591 wadd = work.append
587 592
588 593 exact = skipstep3 = False
589 594 if matchfn == match.exact: # match.exact
590 595 exact = True
591 596 dirignore = util.always # skip step 2
592 597 elif match.files() and not match.anypats(): # match.match, no patterns
593 598 skipstep3 = True
594 599
595 600 if not exact and self._checkcase:
596 601 normalize = self._normalize
597 602 skipstep3 = False
598 603 else:
599 604 normalize = None
600 605
601 606 files = sorted(match.files())
602 607 subrepos.sort()
603 608 i, j = 0, 0
604 609 while i < len(files) and j < len(subrepos):
605 610 subpath = subrepos[j] + "/"
606 611 if files[i] < subpath:
607 612 i += 1
608 613 continue
609 614 while i < len(files) and files[i].startswith(subpath):
610 615 del files[i]
611 616 j += 1
612 617
613 618 if not files or '.' in files:
614 619 files = ['']
615 620 results = dict.fromkeys(subrepos)
616 621 results['.hg'] = None
617 622
618 623 # step 1: find all explicit files
619 624 for ff in files:
620 625 if normalize:
621 626 nf = normalize(normpath(ff), False, True)
622 627 else:
623 628 nf = normpath(ff)
624 629 if nf in results:
625 630 continue
626 631
627 632 try:
628 633 st = lstat(join(nf))
629 634 kind = getkind(st.st_mode)
630 635 if kind == dirkind:
631 636 skipstep3 = False
632 637 if nf in dmap:
633 638 #file deleted on disk but still in dirstate
634 639 results[nf] = None
635 640 match.dir(nf)
636 641 if not dirignore(nf):
637 642 wadd(nf)
638 643 elif kind == regkind or kind == lnkkind:
639 644 results[nf] = st
640 645 else:
641 646 badfn(ff, badtype(kind))
642 647 if nf in dmap:
643 648 results[nf] = None
644 649 except OSError, inst:
645 650 if nf in dmap: # does it exactly match a file?
646 651 results[nf] = None
647 652 else: # does it match a directory?
648 653 prefix = nf + "/"
649 654 for fn in dmap:
650 655 if fn.startswith(prefix):
651 656 match.dir(nf)
652 657 skipstep3 = False
653 658 break
654 659 else:
655 660 badfn(ff, inst.strerror)
656 661
657 662 # step 2: visit subdirectories
658 663 while work:
659 664 nd = work.pop()
660 665 skip = None
661 666 if nd == '.':
662 667 nd = ''
663 668 else:
664 669 skip = '.hg'
665 670 try:
666 671 entries = listdir(join(nd), stat=True, skip=skip)
667 672 except OSError, inst:
668 673 if inst.errno in (errno.EACCES, errno.ENOENT):
669 674 fwarn(nd, inst.strerror)
670 675 continue
671 676 raise
672 677 for f, kind, st in entries:
673 678 if normalize:
674 679 nf = normalize(nd and (nd + "/" + f) or f, True, True)
675 680 else:
676 681 nf = nd and (nd + "/" + f) or f
677 682 if nf not in results:
678 683 if kind == dirkind:
679 684 if not ignore(nf):
680 685 match.dir(nf)
681 686 wadd(nf)
682 687 if nf in dmap and matchfn(nf):
683 688 results[nf] = None
684 689 elif kind == regkind or kind == lnkkind:
685 690 if nf in dmap:
686 691 if matchfn(nf):
687 692 results[nf] = st
688 693 elif matchfn(nf) and not ignore(nf):
689 694 results[nf] = st
690 695 elif nf in dmap and matchfn(nf):
691 696 results[nf] = None
692 697
693 698 # step 3: report unseen items in the dmap hash
694 699 if not skipstep3 and not exact:
695 700 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
696 701 if unknown:
697 702 # unknown == True means we walked the full directory tree above.
698 703 # So if a file is not seen it was either a) not matching matchfn
699 704 # b) ignored, c) missing, or d) under a symlink directory.
700 705 audit_path = scmutil.pathauditor(self._root)
701 706
702 707 for nf in iter(visit):
703 708 # Report ignored items in the dmap as long as they are not
704 709 # under a symlink directory.
705 710 if ignore(nf) and audit_path.check(nf):
706 711 try:
707 712 results[nf] = lstat(join(nf))
708 713 except OSError:
709 714 # file doesn't exist
710 715 results[nf] = None
711 716 else:
712 717 # It's either missing or under a symlink directory
713 718 results[nf] = None
714 719 else:
715 720 # We may not have walked the full directory tree above,
716 721 # so stat everything we missed.
717 722 nf = iter(visit).next
718 723 for st in util.statfiles([join(i) for i in visit]):
719 724 results[nf()] = st
720 725 for s in subrepos:
721 726 del results[s]
722 727 del results['.hg']
723 728 return results
724 729
725 730 def status(self, match, subrepos, ignored, clean, unknown):
726 731 '''Determine the status of the working copy relative to the
727 732 dirstate and return a tuple of lists (unsure, modified, added,
728 733 removed, deleted, unknown, ignored, clean), where:
729 734
730 735 unsure:
731 736 files that might have been modified since the dirstate was
732 737 written, but need to be read to be sure (size is the same
733 738 but mtime differs)
734 739 modified:
735 740 files that have definitely been modified since the dirstate
736 741 was written (different size or mode)
737 742 added:
738 743 files that have been explicitly added with hg add
739 744 removed:
740 745 files that have been explicitly removed with hg remove
741 746 deleted:
742 747 files that have been deleted through other means ("missing")
743 748 unknown:
744 749 files not in the dirstate that are not ignored
745 750 ignored:
746 751 files not in the dirstate that are ignored
747 752 (by _dirignore())
748 753 clean:
749 754 files that have definitely not been modified since the
750 755 dirstate was written
751 756 '''
752 757 listignored, listclean, listunknown = ignored, clean, unknown
753 758 lookup, modified, added, unknown, ignored = [], [], [], [], []
754 759 removed, deleted, clean = [], [], []
755 760
756 761 dmap = self._map
757 762 ladd = lookup.append # aka "unsure"
758 763 madd = modified.append
759 764 aadd = added.append
760 765 uadd = unknown.append
761 766 iadd = ignored.append
762 767 radd = removed.append
763 768 dadd = deleted.append
764 769 cadd = clean.append
765 770 mexact = match.exact
766 771 dirignore = self._dirignore
767 772 checkexec = self._checkexec
768 773 checklink = self._checklink
769 774 copymap = self._copymap
770 775 lastnormaltime = self._lastnormaltime
771 776
772 777 lnkkind = stat.S_IFLNK
773 778
774 779 for fn, st in self.walk(match, subrepos, listunknown,
775 780 listignored).iteritems():
776 781 if fn not in dmap:
777 782 if (listignored or mexact(fn)) and dirignore(fn):
778 783 if listignored:
779 784 iadd(fn)
780 785 elif listunknown:
781 786 uadd(fn)
782 787 continue
783 788
784 789 state, mode, size, time = dmap[fn]
785 790
786 791 if not st and state in "nma":
787 792 dadd(fn)
788 793 elif state == 'n':
789 794 # The "mode & lnkkind != lnkkind or self._checklink"
790 795 # lines are an expansion of "islink => checklink"
791 796 # where islink means "is this a link?" and checklink
792 797 # means "can we check links?".
793 798 mtime = int(st.st_mtime)
794 799 if (size >= 0 and
795 800 ((size != st.st_size and size != st.st_size & _rangemask)
796 801 or ((mode ^ st.st_mode) & 0100 and checkexec))
797 802 and (mode & lnkkind != lnkkind or checklink)
798 803 or size == -2 # other parent
799 804 or fn in copymap):
800 805 madd(fn)
801 806 elif ((time != mtime and time != mtime & _rangemask)
802 807 and (mode & lnkkind != lnkkind or checklink)):
803 808 ladd(fn)
804 809 elif mtime == lastnormaltime:
805 810 # fn may have been changed in the same timeslot without
806 811 # changing its size. This can happen if we quickly do
807 812 # multiple commits in a single transaction.
808 813 # Force lookup, so we don't miss such a racy file change.
809 814 ladd(fn)
810 815 elif listclean:
811 816 cadd(fn)
812 817 elif state == 'm':
813 818 madd(fn)
814 819 elif state == 'a':
815 820 aadd(fn)
816 821 elif state == 'r':
817 822 radd(fn)
818 823
819 824 return (lookup, modified, added, removed, deleted, unknown, ignored,
820 825 clean)
@@ -1,468 +1,487
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "mq=" >> $HGRCPATH
3 3 $ echo "graphlog=" >> $HGRCPATH
4 4
5 5 $ restore() {
6 6 > hg unbundle -q .hg/strip-backup/*
7 7 > rm .hg/strip-backup/*
8 8 > }
9 9 $ teststrip() {
10 10 > hg up -C $1
11 11 > echo % before update $1, strip $2
12 12 > hg parents
13 13 > hg --traceback strip $2
14 14 > echo % after update $1, strip $2
15 15 > hg parents
16 16 > restore
17 17 > }
18 18
19 19 $ hg init test
20 20 $ cd test
21 21
22 22 $ echo foo > bar
23 23 $ hg ci -Ama
24 24 adding bar
25 25
26 26 $ echo more >> bar
27 27 $ hg ci -Amb
28 28
29 29 $ echo blah >> bar
30 30 $ hg ci -Amc
31 31
32 32 $ hg up 1
33 33 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 34 $ echo blah >> bar
35 35 $ hg ci -Amd
36 36 created new head
37 37
38 38 $ echo final >> bar
39 39 $ hg ci -Ame
40 40
41 41 $ hg log
42 42 changeset: 4:443431ffac4f
43 43 tag: tip
44 44 user: test
45 45 date: Thu Jan 01 00:00:00 1970 +0000
46 46 summary: e
47 47
48 48 changeset: 3:65bd5f99a4a3
49 49 parent: 1:ef3a871183d7
50 50 user: test
51 51 date: Thu Jan 01 00:00:00 1970 +0000
52 52 summary: d
53 53
54 54 changeset: 2:264128213d29
55 55 user: test
56 56 date: Thu Jan 01 00:00:00 1970 +0000
57 57 summary: c
58 58
59 59 changeset: 1:ef3a871183d7
60 60 user: test
61 61 date: Thu Jan 01 00:00:00 1970 +0000
62 62 summary: b
63 63
64 64 changeset: 0:9ab35a2d17cb
65 65 user: test
66 66 date: Thu Jan 01 00:00:00 1970 +0000
67 67 summary: a
68 68
69 69
70 70 $ teststrip 4 4
71 71 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
72 72 % before update 4, strip 4
73 73 changeset: 4:443431ffac4f
74 74 tag: tip
75 75 user: test
76 76 date: Thu Jan 01 00:00:00 1970 +0000
77 77 summary: e
78 78
79 79 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
80 80 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
81 81 % after update 4, strip 4
82 82 changeset: 3:65bd5f99a4a3
83 83 tag: tip
84 84 parent: 1:ef3a871183d7
85 85 user: test
86 86 date: Thu Jan 01 00:00:00 1970 +0000
87 87 summary: d
88 88
89 89 $ teststrip 4 3
90 90 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
91 91 % before update 4, strip 3
92 92 changeset: 4:443431ffac4f
93 93 tag: tip
94 94 user: test
95 95 date: Thu Jan 01 00:00:00 1970 +0000
96 96 summary: e
97 97
98 98 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
99 99 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
100 100 % after update 4, strip 3
101 101 changeset: 1:ef3a871183d7
102 102 user: test
103 103 date: Thu Jan 01 00:00:00 1970 +0000
104 104 summary: b
105 105
106 106 $ teststrip 1 4
107 107 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
108 108 % before update 1, strip 4
109 109 changeset: 1:ef3a871183d7
110 110 user: test
111 111 date: Thu Jan 01 00:00:00 1970 +0000
112 112 summary: b
113 113
114 114 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
115 115 % after update 1, strip 4
116 116 changeset: 1:ef3a871183d7
117 117 user: test
118 118 date: Thu Jan 01 00:00:00 1970 +0000
119 119 summary: b
120 120
121 121 $ teststrip 4 2
122 122 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
123 123 % before update 4, strip 2
124 124 changeset: 4:443431ffac4f
125 125 tag: tip
126 126 user: test
127 127 date: Thu Jan 01 00:00:00 1970 +0000
128 128 summary: e
129 129
130 130 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
131 131 % after update 4, strip 2
132 132 changeset: 3:443431ffac4f
133 133 tag: tip
134 134 user: test
135 135 date: Thu Jan 01 00:00:00 1970 +0000
136 136 summary: e
137 137
138 138 $ teststrip 4 1
139 139 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
140 140 % before update 4, strip 1
141 141 changeset: 4:264128213d29
142 142 tag: tip
143 143 parent: 1:ef3a871183d7
144 144 user: test
145 145 date: Thu Jan 01 00:00:00 1970 +0000
146 146 summary: c
147 147
148 148 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
149 149 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
150 150 % after update 4, strip 1
151 151 changeset: 0:9ab35a2d17cb
152 152 tag: tip
153 153 user: test
154 154 date: Thu Jan 01 00:00:00 1970 +0000
155 155 summary: a
156 156
157 157 $ teststrip null 4
158 158 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
159 159 % before update null, strip 4
160 160 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
161 161 % after update null, strip 4
162 162
163 163 $ hg log
164 164 changeset: 4:264128213d29
165 165 tag: tip
166 166 parent: 1:ef3a871183d7
167 167 user: test
168 168 date: Thu Jan 01 00:00:00 1970 +0000
169 169 summary: c
170 170
171 171 changeset: 3:443431ffac4f
172 172 user: test
173 173 date: Thu Jan 01 00:00:00 1970 +0000
174 174 summary: e
175 175
176 176 changeset: 2:65bd5f99a4a3
177 177 user: test
178 178 date: Thu Jan 01 00:00:00 1970 +0000
179 179 summary: d
180 180
181 181 changeset: 1:ef3a871183d7
182 182 user: test
183 183 date: Thu Jan 01 00:00:00 1970 +0000
184 184 summary: b
185 185
186 186 changeset: 0:9ab35a2d17cb
187 187 user: test
188 188 date: Thu Jan 01 00:00:00 1970 +0000
189 189 summary: a
190 190
191 191
192 192 $ hg up -C 2
193 193 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
194 194 $ hg merge 4
195 195 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
196 196 (branch merge, don't forget to commit)
197 197
198 198 before strip of merge parent
199 199
200 200 $ hg parents
201 201 changeset: 2:65bd5f99a4a3
202 202 user: test
203 203 date: Thu Jan 01 00:00:00 1970 +0000
204 204 summary: d
205 205
206 206 changeset: 4:264128213d29
207 207 tag: tip
208 208 parent: 1:ef3a871183d7
209 209 user: test
210 210 date: Thu Jan 01 00:00:00 1970 +0000
211 211 summary: c
212 212
213 213 $ hg strip 4
214 214 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
215 215 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
216 216
217 217 after strip of merge parent
218 218
219 219 $ hg parents
220 220 changeset: 1:ef3a871183d7
221 221 user: test
222 222 date: Thu Jan 01 00:00:00 1970 +0000
223 223 summary: b
224 224
225 225 $ restore
226 226
227 227 $ hg up
228 228 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
229 229 $ hg glog
230 230 @ changeset: 4:264128213d29
231 231 | tag: tip
232 232 | parent: 1:ef3a871183d7
233 233 | user: test
234 234 | date: Thu Jan 01 00:00:00 1970 +0000
235 235 | summary: c
236 236 |
237 237 | o changeset: 3:443431ffac4f
238 238 | | user: test
239 239 | | date: Thu Jan 01 00:00:00 1970 +0000
240 240 | | summary: e
241 241 | |
242 242 | o changeset: 2:65bd5f99a4a3
243 243 |/ user: test
244 244 | date: Thu Jan 01 00:00:00 1970 +0000
245 245 | summary: d
246 246 |
247 247 o changeset: 1:ef3a871183d7
248 248 | user: test
249 249 | date: Thu Jan 01 00:00:00 1970 +0000
250 250 | summary: b
251 251 |
252 252 o changeset: 0:9ab35a2d17cb
253 253 user: test
254 254 date: Thu Jan 01 00:00:00 1970 +0000
255 255 summary: a
256 256
257 257
258 258 2 is parent of 3, only one strip should happen
259 259
260 260 $ hg strip "roots(2)" 3
261 261 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
262 262 $ hg glog
263 263 @ changeset: 2:264128213d29
264 264 | tag: tip
265 265 | user: test
266 266 | date: Thu Jan 01 00:00:00 1970 +0000
267 267 | summary: c
268 268 |
269 269 o changeset: 1:ef3a871183d7
270 270 | user: test
271 271 | date: Thu Jan 01 00:00:00 1970 +0000
272 272 | summary: b
273 273 |
274 274 o changeset: 0:9ab35a2d17cb
275 275 user: test
276 276 date: Thu Jan 01 00:00:00 1970 +0000
277 277 summary: a
278 278
279 279 $ restore
280 280 $ hg glog
281 281 o changeset: 4:443431ffac4f
282 282 | tag: tip
283 283 | user: test
284 284 | date: Thu Jan 01 00:00:00 1970 +0000
285 285 | summary: e
286 286 |
287 287 o changeset: 3:65bd5f99a4a3
288 288 | parent: 1:ef3a871183d7
289 289 | user: test
290 290 | date: Thu Jan 01 00:00:00 1970 +0000
291 291 | summary: d
292 292 |
293 293 | @ changeset: 2:264128213d29
294 294 |/ user: test
295 295 | date: Thu Jan 01 00:00:00 1970 +0000
296 296 | summary: c
297 297 |
298 298 o changeset: 1:ef3a871183d7
299 299 | user: test
300 300 | date: Thu Jan 01 00:00:00 1970 +0000
301 301 | summary: b
302 302 |
303 303 o changeset: 0:9ab35a2d17cb
304 304 user: test
305 305 date: Thu Jan 01 00:00:00 1970 +0000
306 306 summary: a
307 307
308 308
309 309 2 different branches: 2 strips
310 310
311 311 $ hg strip 2 4
312 312 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
313 313 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
314 314 $ hg glog
315 315 o changeset: 2:65bd5f99a4a3
316 316 | tag: tip
317 317 | user: test
318 318 | date: Thu Jan 01 00:00:00 1970 +0000
319 319 | summary: d
320 320 |
321 321 @ changeset: 1:ef3a871183d7
322 322 | user: test
323 323 | date: Thu Jan 01 00:00:00 1970 +0000
324 324 | summary: b
325 325 |
326 326 o changeset: 0:9ab35a2d17cb
327 327 user: test
328 328 date: Thu Jan 01 00:00:00 1970 +0000
329 329 summary: a
330 330
331 331 $ restore
332 332
333 333 2 different branches and a common ancestor: 1 strip
334 334
335 335 $ hg strip 1 "2|4"
336 336 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
337 337 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
338 338 $ restore
339 339
340 340 stripping an empty revset
341 341
342 342 $ hg strip "1 and not 1"
343 343 abort: empty revision set
344 344 [255]
345 345
346 346 remove branchy history for qimport tests
347 347
348 348 $ hg strip 3
349 349 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
350 350
351 351
352 352 strip of applied mq should cleanup status file
353 353
354 354 $ hg up -C 3
355 355 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
356 356 $ echo fooagain >> bar
357 357 $ hg ci -mf
358 358 $ hg qimport -r tip:2
359 359
360 360 applied patches before strip
361 361
362 362 $ hg qapplied
363 363 2.diff
364 364 3.diff
365 365 4.diff
366 366
367 367 stripping revision in queue
368 368
369 369 $ hg strip 3
370 370 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
371 371 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
372 372
373 373 applied patches after stripping rev in queue
374 374
375 375 $ hg qapplied
376 376 2.diff
377 377
378 378 stripping ancestor of queue
379 379
380 380 $ hg strip 1
381 381 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
382 382 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
383 383
384 384 applied patches after stripping ancestor of queue
385 385
386 386 $ hg qapplied
387 387
388 388 Verify strip protects against stripping wc parent when there are uncommited mods
389 389
390 390 $ echo b > b
391 391 $ hg add b
392 392 $ hg ci -m 'b'
393 393 $ hg log --graph
394 394 @ changeset: 1:7519abd79d14
395 395 | tag: tip
396 396 | user: test
397 397 | date: Thu Jan 01 00:00:00 1970 +0000
398 398 | summary: b
399 399 |
400 400 o changeset: 0:9ab35a2d17cb
401 401 user: test
402 402 date: Thu Jan 01 00:00:00 1970 +0000
403 403 summary: a
404 404
405 405
406 406 $ echo c > b
407 407 $ echo c > bar
408 408 $ hg strip tip
409 409 abort: local changes found
410 410 [255]
411 411 $ hg strip tip --keep
412 412 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
413 413 $ hg log --graph
414 414 @ changeset: 0:9ab35a2d17cb
415 415 tag: tip
416 416 user: test
417 417 date: Thu Jan 01 00:00:00 1970 +0000
418 418 summary: a
419 419
420 420 $ hg status
421 421 M bar
422 422 ? b
423
424 Strip adds, removes, modifies with --keep
425
426 $ touch b
427 $ hg add b
428 $ hg commit -mb
429 $ touch c
430 $ hg add c
431 $ hg rm bar
432 $ hg commit -mc
433 $ echo b > b
434 $ echo d > d
435 $ hg strip --keep tip
436 saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
437 $ hg status
438 M b
439 ! bar
440 ? c
441 ? d
423 442 $ cd ..
424 443
425 444 stripping many nodes on a complex graph (issue3299)
426 445
427 446 $ hg init issue3299
428 447 $ cd issue3299
429 448 $ hg debugbuilddag '@a.:a@b.:b.:x<a@a.:a<b@b.:b<a@a.:a'
430 449 $ hg strip 'not ancestors(x)'
431 450 saved backup bundle to $TESTTMP/issue3299/.hg/strip-backup/*-backup.hg (glob)
432 451
433 452 test hg strip -B bookmark
434 453
435 454 $ cd ..
436 455 $ hg init bookmarks
437 456 $ cd bookmarks
438 457 $ hg debugbuilddag '..<2.*1/2:m<2+3:c<m+3:a<2.:b'
439 458 $ hg bookmark -r 'a' 'todelete'
440 459 $ hg bookmark -r 'b' 'B'
441 460 $ hg bookmark -r 'b' 'nostrip'
442 461 $ hg bookmark -r 'c' 'delete'
443 462 $ hg up -C todelete
444 463 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
445 464 $ hg strip -B nostrip
446 465 bookmark 'nostrip' deleted
447 466 abort: empty revision set
448 467 [255]
449 468 $ hg strip -B todelete
450 469 bookmark 'todelete' deleted
451 470 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
452 471 saved backup bundle to $TESTTMP/bookmarks/.hg/strip-backup/*-backup.hg (glob)
453 472 $ hg id -ir dcbb326fdec2
454 473 abort: unknown revision 'dcbb326fdec2'!
455 474 [255]
456 475 $ hg id -ir d62d843c9a01
457 476 d62d843c9a01
458 477 $ hg bookmarks
459 478 B 9:ff43616e5d0f
460 479 delete 6:2702dd0c91e7
461 480 $ hg strip -B delete
462 481 bookmark 'delete' deleted
463 482 saved backup bundle to $TESTTMP/bookmarks/.hg/strip-backup/*-backup.hg (glob)
464 483 $ hg id -ir 6:2702dd0c91e7
465 484 abort: unknown revision '2702dd0c91e7'!
466 485 [255]
467 486
468 487 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now