##// END OF EJS Templates
mq: write the dirstate before stripping...
marmoute -
r50975:2a46555c default
parent child Browse files
Show More
@@ -1,4302 +1,4303 b''
1 1 # mq.py - patch queues for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''manage a stack of patches
9 9
10 10 This extension lets you work with a stack of patches in a Mercurial
11 11 repository. It manages two stacks of patches - all known patches, and
12 12 applied patches (subset of known patches).
13 13
14 14 Known patches are represented as patch files in the .hg/patches
15 15 directory. Applied patches are both patch files and changesets.
16 16
17 17 Common tasks (use :hg:`help COMMAND` for more details)::
18 18
19 19 create new patch qnew
20 20 import existing patch qimport
21 21
22 22 print patch series qseries
23 23 print applied patches qapplied
24 24
25 25 add known patch to applied stack qpush
26 26 remove patch from applied stack qpop
27 27 refresh contents of top applied patch qrefresh
28 28
29 29 By default, mq will automatically use git patches when required to
30 30 avoid losing file mode changes, copy records, binary files or empty
31 31 files creations or deletions. This behavior can be configured with::
32 32
33 33 [mq]
34 34 git = auto/keep/yes/no
35 35
36 36 If set to 'keep', mq will obey the [diff] section configuration while
37 37 preserving existing git patches upon qrefresh. If set to 'yes' or
38 38 'no', mq will override the [diff] section and always generate git or
39 39 regular patches, possibly losing data in the second case.
40 40
41 41 It may be desirable for mq changesets to be kept in the secret phase (see
42 42 :hg:`help phases`), which can be enabled with the following setting::
43 43
44 44 [mq]
45 45 secret = True
46 46
47 47 You will by default be managing a patch queue named "patches". You can
48 48 create other, independent patch queues with the :hg:`qqueue` command.
49 49
50 50 If the working directory contains uncommitted files, qpush, qpop and
51 51 qgoto abort immediately. If -f/--force is used, the changes are
52 52 discarded. Setting::
53 53
54 54 [mq]
55 55 keepchanges = True
56 56
57 57 make them behave as if --keep-changes were passed, and non-conflicting
58 58 local changes will be tolerated and preserved. If incompatible options
59 59 such as -f/--force or --exact are passed, this setting is ignored.
60 60
61 61 This extension used to provide a strip command. This command now lives
62 62 in the strip extension.
63 63 '''
64 64
65 65
66 66 import os
67 67 import re
68 68 import shutil
69 69 import sys
70 70 from mercurial.i18n import _
71 71 from mercurial.node import (
72 72 bin,
73 73 hex,
74 74 nullrev,
75 75 short,
76 76 )
77 77 from mercurial.pycompat import (
78 78 delattr,
79 79 getattr,
80 80 open,
81 81 )
82 82 from mercurial import (
83 83 cmdutil,
84 84 commands,
85 85 encoding,
86 86 error,
87 87 extensions,
88 88 hg,
89 89 localrepo,
90 90 lock as lockmod,
91 91 logcmdutil,
92 92 patch as patchmod,
93 93 phases,
94 94 pycompat,
95 95 registrar,
96 96 revsetlang,
97 97 scmutil,
98 98 smartset,
99 99 strip,
100 100 subrepoutil,
101 101 util,
102 102 vfs as vfsmod,
103 103 )
104 104 from mercurial.utils import (
105 105 dateutil,
106 106 stringutil,
107 107 urlutil,
108 108 )
109 109
110 110 release = lockmod.release
111 111 seriesopts = [(b's', b'summary', None, _(b'print first line of patch header'))]
112 112
113 113 cmdtable = {}
114 114 command = registrar.command(cmdtable)
115 115 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
116 116 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
117 117 # be specifying the version(s) of Mercurial they are tested with, or
118 118 # leave the attribute unspecified.
119 119 testedwith = b'ships-with-hg-core'
120 120
121 121 configtable = {}
122 122 configitem = registrar.configitem(configtable)
123 123
124 124 configitem(
125 125 b'mq',
126 126 b'git',
127 127 default=b'auto',
128 128 )
129 129 configitem(
130 130 b'mq',
131 131 b'keepchanges',
132 132 default=False,
133 133 )
134 134 configitem(
135 135 b'mq',
136 136 b'plain',
137 137 default=False,
138 138 )
139 139 configitem(
140 140 b'mq',
141 141 b'secret',
142 142 default=False,
143 143 )
144 144
145 145 # force load strip extension formerly included in mq and import some utility
146 146 try:
147 147 extensions.find(b'strip')
148 148 except KeyError:
149 149 # note: load is lazy so we could avoid the try-except,
150 150 # but I (marmoute) prefer this explicit code.
151 151 class dummyui:
152 152 def debug(self, msg):
153 153 pass
154 154
155 155 def log(self, event, msgfmt, *msgargs, **opts):
156 156 pass
157 157
158 158 extensions.load(dummyui(), b'strip', b'')
159 159
160 160 strip = strip.strip
161 161
162 162
163 163 def checksubstate(repo, baserev=None):
164 164 """return list of subrepos at a different revision than substate.
165 165 Abort if any subrepos have uncommitted changes."""
166 166 inclsubs = []
167 167 wctx = repo[None]
168 168 if baserev:
169 169 bctx = repo[baserev]
170 170 else:
171 171 bctx = wctx.p1()
172 172 for s in sorted(wctx.substate):
173 173 wctx.sub(s).bailifchanged(True)
174 174 if s not in bctx.substate or bctx.sub(s).dirty():
175 175 inclsubs.append(s)
176 176 return inclsubs
177 177
178 178
179 179 # Patch names looks like unix-file names.
180 180 # They must be joinable with queue directory and result in the patch path.
181 181 normname = util.normpath
182 182
183 183
184 184 class statusentry:
185 185 def __init__(self, node, name):
186 186 self.node, self.name = node, name
187 187
188 188 def __bytes__(self):
189 189 return hex(self.node) + b':' + self.name
190 190
191 191 __str__ = encoding.strmethod(__bytes__)
192 192 __repr__ = encoding.strmethod(__bytes__)
193 193
194 194
195 195 # The order of the headers in 'hg export' HG patches:
196 196 HGHEADERS = [
197 197 # '# HG changeset patch',
198 198 b'# User ',
199 199 b'# Date ',
200 200 b'# ',
201 201 b'# Branch ',
202 202 b'# Node ID ',
203 203 b'# Parent ', # can occur twice for merges - but that is not relevant for mq
204 204 ]
205 205 # The order of headers in plain 'mail style' patches:
206 206 PLAINHEADERS = {
207 207 b'from': 0,
208 208 b'date': 1,
209 209 b'subject': 2,
210 210 }
211 211
212 212
213 213 def inserthgheader(lines, header, value):
214 214 """Assuming lines contains a HG patch header, add a header line with value.
215 215 >>> try: inserthgheader([], b'# Date ', b'z')
216 216 ... except ValueError as inst: print("oops")
217 217 oops
218 218 >>> inserthgheader([b'# HG changeset patch'], b'# Date ', b'z')
219 219 ['# HG changeset patch', '# Date z']
220 220 >>> inserthgheader([b'# HG changeset patch', b''], b'# Date ', b'z')
221 221 ['# HG changeset patch', '# Date z', '']
222 222 >>> inserthgheader([b'# HG changeset patch', b'# User y'], b'# Date ', b'z')
223 223 ['# HG changeset patch', '# User y', '# Date z']
224 224 >>> inserthgheader([b'# HG changeset patch', b'# Date x', b'# User y'],
225 225 ... b'# User ', b'z')
226 226 ['# HG changeset patch', '# Date x', '# User z']
227 227 >>> inserthgheader([b'# HG changeset patch', b'# Date y'], b'# Date ', b'z')
228 228 ['# HG changeset patch', '# Date z']
229 229 >>> inserthgheader([b'# HG changeset patch', b'', b'# Date y'],
230 230 ... b'# Date ', b'z')
231 231 ['# HG changeset patch', '# Date z', '', '# Date y']
232 232 >>> inserthgheader([b'# HG changeset patch', b'# Parent y'],
233 233 ... b'# Date ', b'z')
234 234 ['# HG changeset patch', '# Date z', '# Parent y']
235 235 """
236 236 start = lines.index(b'# HG changeset patch') + 1
237 237 newindex = HGHEADERS.index(header)
238 238 bestpos = len(lines)
239 239 for i in range(start, len(lines)):
240 240 line = lines[i]
241 241 if not line.startswith(b'# '):
242 242 bestpos = min(bestpos, i)
243 243 break
244 244 for lineindex, h in enumerate(HGHEADERS):
245 245 if line.startswith(h):
246 246 if lineindex == newindex:
247 247 lines[i] = header + value
248 248 return lines
249 249 if lineindex > newindex:
250 250 bestpos = min(bestpos, i)
251 251 break # next line
252 252 lines.insert(bestpos, header + value)
253 253 return lines
254 254
255 255
256 256 def insertplainheader(lines, header, value):
257 257 """For lines containing a plain patch header, add a header line with value.
258 258 >>> insertplainheader([], b'Date', b'z')
259 259 ['Date: z']
260 260 >>> insertplainheader([b''], b'Date', b'z')
261 261 ['Date: z', '']
262 262 >>> insertplainheader([b'x'], b'Date', b'z')
263 263 ['Date: z', '', 'x']
264 264 >>> insertplainheader([b'From: y', b'x'], b'Date', b'z')
265 265 ['From: y', 'Date: z', '', 'x']
266 266 >>> insertplainheader([b' date : x', b' from : y', b''], b'From', b'z')
267 267 [' date : x', 'From: z', '']
268 268 >>> insertplainheader([b'', b'Date: y'], b'Date', b'z')
269 269 ['Date: z', '', 'Date: y']
270 270 >>> insertplainheader([b'foo: bar', b'DATE: z', b'x'], b'From', b'y')
271 271 ['From: y', 'foo: bar', 'DATE: z', '', 'x']
272 272 """
273 273 newprio = PLAINHEADERS[header.lower()]
274 274 bestpos = len(lines)
275 275 for i, line in enumerate(lines):
276 276 if b':' in line:
277 277 lheader = line.split(b':', 1)[0].strip().lower()
278 278 lprio = PLAINHEADERS.get(lheader, newprio + 1)
279 279 if lprio == newprio:
280 280 lines[i] = b'%s: %s' % (header, value)
281 281 return lines
282 282 if lprio > newprio and i < bestpos:
283 283 bestpos = i
284 284 else:
285 285 if line:
286 286 lines.insert(i, b'')
287 287 if i < bestpos:
288 288 bestpos = i
289 289 break
290 290 lines.insert(bestpos, b'%s: %s' % (header, value))
291 291 return lines
292 292
293 293
294 294 class patchheader:
295 295 def __init__(self, pf, plainmode=False):
296 296 def eatdiff(lines):
297 297 while lines:
298 298 l = lines[-1]
299 299 if (
300 300 l.startswith(b"diff -")
301 301 or l.startswith(b"Index:")
302 302 or l.startswith(b"===========")
303 303 ):
304 304 del lines[-1]
305 305 else:
306 306 break
307 307
308 308 def eatempty(lines):
309 309 while lines:
310 310 if not lines[-1].strip():
311 311 del lines[-1]
312 312 else:
313 313 break
314 314
315 315 message = []
316 316 comments = []
317 317 user = None
318 318 date = None
319 319 parent = None
320 320 format = None
321 321 subject = None
322 322 branch = None
323 323 nodeid = None
324 324 diffstart = 0
325 325
326 326 for line in open(pf, b'rb'):
327 327 line = line.rstrip()
328 328 if line.startswith(b'diff --git') or (
329 329 diffstart and line.startswith(b'+++ ')
330 330 ):
331 331 diffstart = 2
332 332 break
333 333 diffstart = 0 # reset
334 334 if line.startswith(b"--- "):
335 335 diffstart = 1
336 336 continue
337 337 elif format == b"hgpatch":
338 338 # parse values when importing the result of an hg export
339 339 if line.startswith(b"# User "):
340 340 user = line[7:]
341 341 elif line.startswith(b"# Date "):
342 342 date = line[7:]
343 343 elif line.startswith(b"# Parent "):
344 344 parent = line[9:].lstrip() # handle double trailing space
345 345 elif line.startswith(b"# Branch "):
346 346 branch = line[9:]
347 347 elif line.startswith(b"# Node ID "):
348 348 nodeid = line[10:]
349 349 elif not line.startswith(b"# ") and line:
350 350 message.append(line)
351 351 format = None
352 352 elif line == b'# HG changeset patch':
353 353 message = []
354 354 format = b"hgpatch"
355 355 elif format != b"tagdone" and (
356 356 line.startswith(b"Subject: ") or line.startswith(b"subject: ")
357 357 ):
358 358 subject = line[9:]
359 359 format = b"tag"
360 360 elif format != b"tagdone" and (
361 361 line.startswith(b"From: ") or line.startswith(b"from: ")
362 362 ):
363 363 user = line[6:]
364 364 format = b"tag"
365 365 elif format != b"tagdone" and (
366 366 line.startswith(b"Date: ") or line.startswith(b"date: ")
367 367 ):
368 368 date = line[6:]
369 369 format = b"tag"
370 370 elif format == b"tag" and line == b"":
371 371 # when looking for tags (subject: from: etc) they
372 372 # end once you find a blank line in the source
373 373 format = b"tagdone"
374 374 elif message or line:
375 375 message.append(line)
376 376 comments.append(line)
377 377
378 378 eatdiff(message)
379 379 eatdiff(comments)
380 380 # Remember the exact starting line of the patch diffs before consuming
381 381 # empty lines, for external use by TortoiseHg and others
382 382 self.diffstartline = len(comments)
383 383 eatempty(message)
384 384 eatempty(comments)
385 385
386 386 # make sure message isn't empty
387 387 if format and format.startswith(b"tag") and subject:
388 388 message.insert(0, subject)
389 389
390 390 self.message = message
391 391 self.comments = comments
392 392 self.user = user
393 393 self.date = date
394 394 self.parent = parent
395 395 # nodeid and branch are for external use by TortoiseHg and others
396 396 self.nodeid = nodeid
397 397 self.branch = branch
398 398 self.haspatch = diffstart > 1
399 399 self.plainmode = (
400 400 plainmode
401 401 or b'# HG changeset patch' not in self.comments
402 402 and any(
403 403 c.startswith(b'Date: ') or c.startswith(b'From: ')
404 404 for c in self.comments
405 405 )
406 406 )
407 407
408 408 def setuser(self, user):
409 409 try:
410 410 inserthgheader(self.comments, b'# User ', user)
411 411 except ValueError:
412 412 if self.plainmode:
413 413 insertplainheader(self.comments, b'From', user)
414 414 else:
415 415 tmp = [b'# HG changeset patch', b'# User ' + user]
416 416 self.comments = tmp + self.comments
417 417 self.user = user
418 418
419 419 def setdate(self, date):
420 420 try:
421 421 inserthgheader(self.comments, b'# Date ', date)
422 422 except ValueError:
423 423 if self.plainmode:
424 424 insertplainheader(self.comments, b'Date', date)
425 425 else:
426 426 tmp = [b'# HG changeset patch', b'# Date ' + date]
427 427 self.comments = tmp + self.comments
428 428 self.date = date
429 429
430 430 def setparent(self, parent):
431 431 try:
432 432 inserthgheader(self.comments, b'# Parent ', parent)
433 433 except ValueError:
434 434 if not self.plainmode:
435 435 tmp = [b'# HG changeset patch', b'# Parent ' + parent]
436 436 self.comments = tmp + self.comments
437 437 self.parent = parent
438 438
439 439 def setmessage(self, message):
440 440 if self.comments:
441 441 self._delmsg()
442 442 self.message = [message]
443 443 if message:
444 444 if self.plainmode and self.comments and self.comments[-1]:
445 445 self.comments.append(b'')
446 446 self.comments.append(message)
447 447
448 448 def __bytes__(self):
449 449 s = b'\n'.join(self.comments).rstrip()
450 450 if not s:
451 451 return b''
452 452 return s + b'\n\n'
453 453
454 454 __str__ = encoding.strmethod(__bytes__)
455 455
456 456 def _delmsg(self):
457 457 """Remove existing message, keeping the rest of the comments fields.
458 458 If comments contains 'subject: ', message will prepend
459 459 the field and a blank line."""
460 460 if self.message:
461 461 subj = b'subject: ' + self.message[0].lower()
462 462 for i in range(len(self.comments)):
463 463 if subj == self.comments[i].lower():
464 464 del self.comments[i]
465 465 self.message = self.message[2:]
466 466 break
467 467 ci = 0
468 468 for mi in self.message:
469 469 while mi != self.comments[ci]:
470 470 ci += 1
471 471 del self.comments[ci]
472 472
473 473
474 474 def newcommit(repo, phase, *args, **kwargs):
475 475 """helper dedicated to ensure a commit respect mq.secret setting
476 476
477 477 It should be used instead of repo.commit inside the mq source for operation
478 478 creating new changeset.
479 479 """
480 480 repo = repo.unfiltered()
481 481 if phase is None:
482 482 if repo.ui.configbool(b'mq', b'secret'):
483 483 phase = phases.secret
484 484 overrides = {(b'ui', b'allowemptycommit'): True}
485 485 if phase is not None:
486 486 overrides[(b'phases', b'new-commit')] = phase
487 487 with repo.ui.configoverride(overrides, b'mq'):
488 488 repo.ui.setconfig(b'ui', b'allowemptycommit', True)
489 489 return repo.commit(*args, **kwargs)
490 490
491 491
492 492 class AbortNoCleanup(error.Abort):
493 493 pass
494 494
495 495
496 496 class queue:
497 497 def __init__(self, ui, baseui, path, patchdir=None):
498 498 self.basepath = path
499 499 try:
500 500 with open(os.path.join(path, b'patches.queue'), 'rb') as fh:
501 501 cur = fh.read().rstrip()
502 502
503 503 if not cur:
504 504 curpath = os.path.join(path, b'patches')
505 505 else:
506 506 curpath = os.path.join(path, b'patches-' + cur)
507 507 except IOError:
508 508 curpath = os.path.join(path, b'patches')
509 509 self.path = patchdir or curpath
510 510 self.opener = vfsmod.vfs(self.path)
511 511 self.ui = ui
512 512 self.baseui = baseui
513 513 self.applieddirty = False
514 514 self.seriesdirty = False
515 515 self.added = []
516 516 self.seriespath = b"series"
517 517 self.statuspath = b"status"
518 518 self.guardspath = b"guards"
519 519 self.activeguards = None
520 520 self.guardsdirty = False
521 521 # Handle mq.git as a bool with extended values
522 522 gitmode = ui.config(b'mq', b'git').lower()
523 523 boolmode = stringutil.parsebool(gitmode)
524 524 if boolmode is not None:
525 525 if boolmode:
526 526 gitmode = b'yes'
527 527 else:
528 528 gitmode = b'no'
529 529 self.gitmode = gitmode
530 530 # deprecated config: mq.plain
531 531 self.plainmode = ui.configbool(b'mq', b'plain')
532 532 self.checkapplied = True
533 533
534 534 @util.propertycache
535 535 def applied(self):
536 536 def parselines(lines):
537 537 for l in lines:
538 538 entry = l.split(b':', 1)
539 539 if len(entry) > 1:
540 540 n, name = entry
541 541 yield statusentry(bin(n), name)
542 542 elif l.strip():
543 543 self.ui.warn(
544 544 _(b'malformated mq status line: %s\n')
545 545 % stringutil.pprint(entry)
546 546 )
547 547 # else we ignore empty lines
548 548
549 549 try:
550 550 lines = self.opener.read(self.statuspath).splitlines()
551 551 return list(parselines(lines))
552 552 except FileNotFoundError:
553 553 return []
554 554
555 555 @util.propertycache
556 556 def fullseries(self):
557 557 try:
558 558 return self.opener.read(self.seriespath).splitlines()
559 559 except FileNotFoundError:
560 560 return []
561 561
562 562 @util.propertycache
563 563 def series(self):
564 564 self.parseseries()
565 565 return self.series
566 566
567 567 @util.propertycache
568 568 def seriesguards(self):
569 569 self.parseseries()
570 570 return self.seriesguards
571 571
572 572 def invalidate(self):
573 573 for a in 'applied fullseries series seriesguards'.split():
574 574 if a in self.__dict__:
575 575 delattr(self, a)
576 576 self.applieddirty = False
577 577 self.seriesdirty = False
578 578 self.guardsdirty = False
579 579 self.activeguards = None
580 580
581 581 def diffopts(self, opts=None, patchfn=None, plain=False):
582 582 """Return diff options tweaked for this mq use, possibly upgrading to
583 583 git format, and possibly plain and without lossy options."""
584 584 diffopts = patchmod.difffeatureopts(
585 585 self.ui,
586 586 opts,
587 587 git=True,
588 588 whitespace=not plain,
589 589 formatchanging=not plain,
590 590 )
591 591 if self.gitmode == b'auto':
592 592 diffopts.upgrade = True
593 593 elif self.gitmode == b'keep':
594 594 pass
595 595 elif self.gitmode in (b'yes', b'no'):
596 596 diffopts.git = self.gitmode == b'yes'
597 597 else:
598 598 raise error.Abort(
599 599 _(b'mq.git option can be auto/keep/yes/no got %s')
600 600 % self.gitmode
601 601 )
602 602 if patchfn:
603 603 diffopts = self.patchopts(diffopts, patchfn)
604 604 return diffopts
605 605
606 606 def patchopts(self, diffopts, *patches):
607 607 """Return a copy of input diff options with git set to true if
608 608 referenced patch is a git patch and should be preserved as such.
609 609 """
610 610 diffopts = diffopts.copy()
611 611 if not diffopts.git and self.gitmode == b'keep':
612 612 for patchfn in patches:
613 613 patchf = self.opener(patchfn, b'r')
614 614 # if the patch was a git patch, refresh it as a git patch
615 615 diffopts.git = any(
616 616 line.startswith(b'diff --git') for line in patchf
617 617 )
618 618 patchf.close()
619 619 return diffopts
620 620
621 621 def join(self, *p):
622 622 return os.path.join(self.path, *p)
623 623
624 624 def findseries(self, patch):
625 625 def matchpatch(l):
626 626 l = l.split(b'#', 1)[0]
627 627 return l.strip() == patch
628 628
629 629 for index, l in enumerate(self.fullseries):
630 630 if matchpatch(l):
631 631 return index
632 632 return None
633 633
634 634 guard_re = re.compile(br'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
635 635
636 636 def parseseries(self):
637 637 self.series = []
638 638 self.seriesguards = []
639 639 for l in self.fullseries:
640 640 h = l.find(b'#')
641 641 if h == -1:
642 642 patch = l
643 643 comment = b''
644 644 elif h == 0:
645 645 continue
646 646 else:
647 647 patch = l[:h]
648 648 comment = l[h:]
649 649 patch = patch.strip()
650 650 if patch:
651 651 if patch in self.series:
652 652 raise error.Abort(
653 653 _(b'%s appears more than once in %s')
654 654 % (patch, self.join(self.seriespath))
655 655 )
656 656 self.series.append(patch)
657 657 self.seriesguards.append(self.guard_re.findall(comment))
658 658
659 659 def checkguard(self, guard):
660 660 if not guard:
661 661 return _(b'guard cannot be an empty string')
662 662 bad_chars = b'# \t\r\n\f'
663 663 first = guard[0]
664 664 if first in b'-+':
665 665 return _(b'guard %r starts with invalid character: %r') % (
666 666 guard,
667 667 first,
668 668 )
669 669 for c in bad_chars:
670 670 if c in guard:
671 671 return _(b'invalid character in guard %r: %r') % (guard, c)
672 672
673 673 def setactive(self, guards):
674 674 for guard in guards:
675 675 bad = self.checkguard(guard)
676 676 if bad:
677 677 raise error.Abort(bad)
678 678 guards = sorted(set(guards))
679 679 self.ui.debug(b'active guards: %s\n' % b' '.join(guards))
680 680 self.activeguards = guards
681 681 self.guardsdirty = True
682 682
683 683 def active(self):
684 684 if self.activeguards is None:
685 685 self.activeguards = []
686 686 try:
687 687 guards = self.opener.read(self.guardspath).split()
688 688 except FileNotFoundError:
689 689 guards = []
690 690 for i, guard in enumerate(guards):
691 691 bad = self.checkguard(guard)
692 692 if bad:
693 693 self.ui.warn(
694 694 b'%s:%d: %s\n'
695 695 % (self.join(self.guardspath), i + 1, bad)
696 696 )
697 697 else:
698 698 self.activeguards.append(guard)
699 699 return self.activeguards
700 700
701 701 def setguards(self, idx, guards):
702 702 for g in guards:
703 703 if len(g) < 2:
704 704 raise error.Abort(_(b'guard %r too short') % g)
705 705 if g[0] not in b'-+':
706 706 raise error.Abort(_(b'guard %r starts with invalid char') % g)
707 707 bad = self.checkguard(g[1:])
708 708 if bad:
709 709 raise error.Abort(bad)
710 710 drop = self.guard_re.sub(b'', self.fullseries[idx])
711 711 self.fullseries[idx] = drop + b''.join([b' #' + g for g in guards])
712 712 self.parseseries()
713 713 self.seriesdirty = True
714 714
715 715 def pushable(self, idx):
716 716 if isinstance(idx, bytes):
717 717 idx = self.series.index(idx)
718 718 patchguards = self.seriesguards[idx]
719 719 if not patchguards:
720 720 return True, None
721 721 guards = self.active()
722 722 exactneg = [
723 723 g for g in patchguards if g.startswith(b'-') and g[1:] in guards
724 724 ]
725 725 if exactneg:
726 726 return False, stringutil.pprint(exactneg[0])
727 727 pos = [g for g in patchguards if g.startswith(b'+')]
728 728 exactpos = [g for g in pos if g[1:] in guards]
729 729 if pos:
730 730 if exactpos:
731 731 return True, stringutil.pprint(exactpos[0])
732 732 return False, b' '.join([stringutil.pprint(p) for p in pos])
733 733 return True, b''
734 734
735 735 def explainpushable(self, idx, all_patches=False):
736 736 if all_patches:
737 737 write = self.ui.write
738 738 else:
739 739 write = self.ui.warn
740 740
741 741 if all_patches or self.ui.verbose:
742 742 if isinstance(idx, bytes):
743 743 idx = self.series.index(idx)
744 744 pushable, why = self.pushable(idx)
745 745 if all_patches and pushable:
746 746 if why is None:
747 747 write(
748 748 _(b'allowing %s - no guards in effect\n')
749 749 % self.series[idx]
750 750 )
751 751 else:
752 752 if not why:
753 753 write(
754 754 _(b'allowing %s - no matching negative guards\n')
755 755 % self.series[idx]
756 756 )
757 757 else:
758 758 write(
759 759 _(b'allowing %s - guarded by %s\n')
760 760 % (self.series[idx], why)
761 761 )
762 762 if not pushable:
763 763 if why:
764 764 write(
765 765 _(b'skipping %s - guarded by %s\n')
766 766 % (self.series[idx], why)
767 767 )
768 768 else:
769 769 write(
770 770 _(b'skipping %s - no matching guards\n')
771 771 % self.series[idx]
772 772 )
773 773
774 774 def savedirty(self):
775 775 def writelist(items, path):
776 776 fp = self.opener(path, b'wb')
777 777 for i in items:
778 778 fp.write(b"%s\n" % i)
779 779 fp.close()
780 780
781 781 if self.applieddirty:
782 782 writelist(map(bytes, self.applied), self.statuspath)
783 783 self.applieddirty = False
784 784 if self.seriesdirty:
785 785 writelist(self.fullseries, self.seriespath)
786 786 self.seriesdirty = False
787 787 if self.guardsdirty:
788 788 writelist(self.activeguards, self.guardspath)
789 789 self.guardsdirty = False
790 790 if self.added:
791 791 qrepo = self.qrepo()
792 792 if qrepo:
793 793 with qrepo.wlock(), qrepo.dirstate.changing_files(qrepo):
794 794 qrepo[None].add(
795 795 f for f in self.added if f not in qrepo[None]
796 796 )
797 797 self.added = []
798 798
799 799 def removeundo(self, repo):
800 800 undo = repo.sjoin(b'undo')
801 801 if not os.path.exists(undo):
802 802 return
803 803 try:
804 804 os.unlink(undo)
805 805 except OSError as inst:
806 806 self.ui.warn(
807 807 _(b'error removing undo: %s\n') % stringutil.forcebytestr(inst)
808 808 )
809 809
810 810 def backup(self, repo, files, copy=False):
811 811 # backup local changes in --force case
812 812 for f in sorted(files):
813 813 absf = repo.wjoin(f)
814 814 if os.path.lexists(absf):
815 815 absorig = scmutil.backuppath(self.ui, repo, f)
816 816 self.ui.note(
817 817 _(b'saving current version of %s as %s\n')
818 818 % (f, os.path.relpath(absorig))
819 819 )
820 820
821 821 if copy:
822 822 util.copyfile(absf, absorig)
823 823 else:
824 824 util.rename(absf, absorig)
825 825
826 826 def printdiff(
827 827 self,
828 828 repo,
829 829 diffopts,
830 830 node1,
831 831 node2=None,
832 832 files=None,
833 833 fp=None,
834 834 changes=None,
835 835 opts=None,
836 836 ):
837 837 if opts is None:
838 838 opts = {}
839 839 stat = opts.get(b'stat')
840 840 m = scmutil.match(repo[node1], files, opts)
841 841 logcmdutil.diffordiffstat(
842 842 self.ui,
843 843 repo,
844 844 diffopts,
845 845 repo[node1],
846 846 repo[node2],
847 847 m,
848 848 changes,
849 849 stat,
850 850 fp,
851 851 )
852 852
853 853 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
854 854 # first try just applying the patch
855 855 (err, n) = self.apply(
856 856 repo, [patch], update_status=False, strict=True, merge=rev
857 857 )
858 858
859 859 if err == 0:
860 860 return (err, n)
861 861
862 862 if n is None:
863 863 raise error.Abort(_(b"apply failed for patch %s") % patch)
864 864
865 865 self.ui.warn(_(b"patch didn't work out, merging %s\n") % patch)
866 866
867 867 # apply failed, strip away that rev and merge.
868 868 hg.clean(repo, head)
869 869 strip(self.ui, repo, [n], update=False, backup=False)
870 870
871 871 ctx = repo[rev]
872 872 ret = hg.merge(ctx, remind=False)
873 873 if ret:
874 874 raise error.Abort(_(b"update returned %d") % ret)
875 875 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
876 876 if n is None:
877 877 raise error.Abort(_(b"repo commit failed"))
878 878 try:
879 879 ph = patchheader(mergeq.join(patch), self.plainmode)
880 880 except Exception:
881 881 raise error.Abort(_(b"unable to read %s") % patch)
882 882
883 883 diffopts = self.patchopts(diffopts, patch)
884 884 patchf = self.opener(patch, b"w")
885 885 comments = bytes(ph)
886 886 if comments:
887 887 patchf.write(comments)
888 888 self.printdiff(repo, diffopts, head, n, fp=patchf)
889 889 patchf.close()
890 890 self.removeundo(repo)
891 891 return (0, n)
892 892
893 893 def qparents(self, repo, rev=None):
894 894 """return the mq handled parent or p1
895 895
896 896 In some case where mq get himself in being the parent of a merge the
897 897 appropriate parent may be p2.
898 898 (eg: an in progress merge started with mq disabled)
899 899
900 900 If no parent are managed by mq, p1 is returned.
901 901 """
902 902 if rev is None:
903 903 (p1, p2) = repo.dirstate.parents()
904 904 if p2 == repo.nullid:
905 905 return p1
906 906 if not self.applied:
907 907 return None
908 908 return self.applied[-1].node
909 909 p1, p2 = repo.changelog.parents(rev)
910 910 if p2 != repo.nullid and p2 in [x.node for x in self.applied]:
911 911 return p2
912 912 return p1
913 913
914 914 def mergepatch(self, repo, mergeq, series, diffopts):
915 915 if not self.applied:
916 916 # each of the patches merged in will have two parents. This
917 917 # can confuse the qrefresh, qdiff, and strip code because it
918 918 # needs to know which parent is actually in the patch queue.
919 919 # so, we insert a merge marker with only one parent. This way
920 920 # the first patch in the queue is never a merge patch
921 921 #
922 922 pname = b".hg.patches.merge.marker"
923 923 n = newcommit(repo, None, b'[mq]: merge marker', force=True)
924 924 self.removeundo(repo)
925 925 self.applied.append(statusentry(n, pname))
926 926 self.applieddirty = True
927 927
928 928 head = self.qparents(repo)
929 929
930 930 for patch in series:
931 931 patch = mergeq.lookup(patch, strict=True)
932 932 if not patch:
933 933 self.ui.warn(_(b"patch %s does not exist\n") % patch)
934 934 return (1, None)
935 935 pushable, reason = self.pushable(patch)
936 936 if not pushable:
937 937 self.explainpushable(patch, all_patches=True)
938 938 continue
939 939 info = mergeq.isapplied(patch)
940 940 if not info:
941 941 self.ui.warn(_(b"patch %s is not applied\n") % patch)
942 942 return (1, None)
943 943 rev = info[1]
944 944 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
945 945 if head:
946 946 self.applied.append(statusentry(head, patch))
947 947 self.applieddirty = True
948 948 if err:
949 949 return (err, head)
950 950 self.savedirty()
951 951 return (0, head)
952 952
953 953 def patch(self, repo, patchfile):
954 954 """Apply patchfile to the working directory.
955 955 patchfile: name of patch file"""
956 956 files = set()
957 957 try:
958 958 fuzz = patchmod.patch(
959 959 self.ui, repo, patchfile, strip=1, files=files, eolmode=None
960 960 )
961 961 return (True, list(files), fuzz)
962 962 except Exception as inst:
963 963 self.ui.note(stringutil.forcebytestr(inst) + b'\n')
964 964 if not self.ui.verbose:
965 965 self.ui.warn(_(b"patch failed, unable to continue (try -v)\n"))
966 966 self.ui.traceback()
967 967 return (False, list(files), False)
968 968
969 969 def apply(
970 970 self,
971 971 repo,
972 972 series,
973 973 list=False,
974 974 update_status=True,
975 975 strict=False,
976 976 patchdir=None,
977 977 merge=None,
978 978 all_files=None,
979 979 tobackup=None,
980 980 keepchanges=False,
981 981 ):
982 982 wlock = lock = tr = None
983 983 try:
984 984 wlock = repo.wlock()
985 985 lock = repo.lock()
986 986 tr = repo.transaction(b"qpush")
987 987 try:
988 988 ret = self._apply(
989 989 repo,
990 990 series,
991 991 list,
992 992 update_status,
993 993 strict,
994 994 patchdir,
995 995 merge,
996 996 all_files=all_files,
997 997 tobackup=tobackup,
998 998 keepchanges=keepchanges,
999 999 )
1000 1000 tr.close()
1001 1001 self.savedirty()
1002 1002 return ret
1003 1003 except AbortNoCleanup:
1004 1004 tr.close()
1005 1005 self.savedirty()
1006 1006 raise
1007 1007 except: # re-raises
1008 1008 try:
1009 1009 tr.abort()
1010 1010 finally:
1011 1011 self.invalidate()
1012 1012 raise
1013 1013 finally:
1014 1014 release(tr, lock, wlock)
1015 1015 self.removeundo(repo)
1016 1016
1017 1017 def _apply(
1018 1018 self,
1019 1019 repo,
1020 1020 series,
1021 1021 list=False,
1022 1022 update_status=True,
1023 1023 strict=False,
1024 1024 patchdir=None,
1025 1025 merge=None,
1026 1026 all_files=None,
1027 1027 tobackup=None,
1028 1028 keepchanges=False,
1029 1029 ):
1030 1030 """returns (error, hash)
1031 1031
1032 1032 error = 1 for unable to read, 2 for patch failed, 3 for patch
1033 1033 fuzz. tobackup is None or a set of files to backup before they
1034 1034 are modified by a patch.
1035 1035 """
1036 1036 # TODO unify with commands.py
1037 1037 if not patchdir:
1038 1038 patchdir = self.path
1039 1039 err = 0
1040 1040 n = None
1041 1041 for patchname in series:
1042 1042 pushable, reason = self.pushable(patchname)
1043 1043 if not pushable:
1044 1044 self.explainpushable(patchname, all_patches=True)
1045 1045 continue
1046 1046 self.ui.status(_(b"applying %s\n") % patchname)
1047 1047 pf = os.path.join(patchdir, patchname)
1048 1048
1049 1049 try:
1050 1050 ph = patchheader(self.join(patchname), self.plainmode)
1051 1051 except IOError:
1052 1052 self.ui.warn(_(b"unable to read %s\n") % patchname)
1053 1053 err = 1
1054 1054 break
1055 1055
1056 1056 message = ph.message
1057 1057 if not message:
1058 1058 # The commit message should not be translated
1059 1059 message = b"imported patch %s\n" % patchname
1060 1060 else:
1061 1061 if list:
1062 1062 # The commit message should not be translated
1063 1063 message.append(b"\nimported patch %s" % patchname)
1064 1064 message = b'\n'.join(message)
1065 1065
1066 1066 if ph.haspatch:
1067 1067 if tobackup:
1068 1068 touched = patchmod.changedfiles(self.ui, repo, pf)
1069 1069 touched = set(touched) & tobackup
1070 1070 if touched and keepchanges:
1071 1071 raise AbortNoCleanup(
1072 1072 _(b"conflicting local changes found"),
1073 1073 hint=_(b"did you forget to qrefresh?"),
1074 1074 )
1075 1075 self.backup(repo, touched, copy=True)
1076 1076 tobackup = tobackup - touched
1077 1077 (patcherr, files, fuzz) = self.patch(repo, pf)
1078 1078 if all_files is not None:
1079 1079 all_files.update(files)
1080 1080 patcherr = not patcherr
1081 1081 else:
1082 1082 self.ui.warn(_(b"patch %s is empty\n") % patchname)
1083 1083 patcherr, files, fuzz = 0, [], 0
1084 1084
1085 1085 if merge and files:
1086 1086 # Mark as removed/merged and update dirstate parent info
1087 1087 with repo.dirstate.changing_parents(repo):
1088 1088 for f in files:
1089 1089 repo.dirstate.update_file_p1(f, p1_tracked=True)
1090 1090 p1 = repo.dirstate.p1()
1091 1091 repo.setparents(p1, merge)
1092 1092
1093 1093 if all_files and b'.hgsubstate' in all_files:
1094 1094 wctx = repo[None]
1095 1095 pctx = repo[b'.']
1096 1096 overwrite = False
1097 1097 mergedsubstate = subrepoutil.submerge(
1098 1098 repo, pctx, wctx, wctx, overwrite
1099 1099 )
1100 1100 files += mergedsubstate.keys()
1101 1101
1102 1102 match = scmutil.matchfiles(repo, files or [])
1103 1103 oldtip = repo.changelog.tip()
1104 1104 n = newcommit(
1105 1105 repo, None, message, ph.user, ph.date, match=match, force=True
1106 1106 )
1107 1107 if repo.changelog.tip() == oldtip:
1108 1108 raise error.Abort(
1109 1109 _(b"qpush exactly duplicates child changeset")
1110 1110 )
1111 1111 if n is None:
1112 1112 raise error.Abort(_(b"repository commit failed"))
1113 1113
1114 1114 if update_status:
1115 1115 self.applied.append(statusentry(n, patchname))
1116 1116
1117 1117 if patcherr:
1118 1118 self.ui.warn(
1119 1119 _(b"patch failed, rejects left in working directory\n")
1120 1120 )
1121 1121 err = 2
1122 1122 break
1123 1123
1124 1124 if fuzz and strict:
1125 1125 self.ui.warn(_(b"fuzz found when applying patch, stopping\n"))
1126 1126 err = 3
1127 1127 break
1128 1128 return (err, n)
1129 1129
1130 1130 def _cleanup(self, patches, numrevs, keep=False):
1131 1131 if not keep:
1132 1132 r = self.qrepo()
1133 1133 if r:
1134 1134 with r.wlock(), r.dirstate.changing_files(r):
1135 1135 r[None].forget(patches)
1136 1136 for p in patches:
1137 1137 try:
1138 1138 os.unlink(self.join(p))
1139 1139 except FileNotFoundError:
1140 1140 pass
1141 1141
1142 1142 qfinished = []
1143 1143 if numrevs:
1144 1144 qfinished = self.applied[:numrevs]
1145 1145 del self.applied[:numrevs]
1146 1146 self.applieddirty = True
1147 1147
1148 1148 unknown = []
1149 1149
1150 1150 sortedseries = []
1151 1151 for p in patches:
1152 1152 idx = self.findseries(p)
1153 1153 if idx is None:
1154 1154 sortedseries.append((-1, p))
1155 1155 else:
1156 1156 sortedseries.append((idx, p))
1157 1157
1158 1158 sortedseries.sort(reverse=True)
1159 1159 for i, p in sortedseries:
1160 1160 if i != -1:
1161 1161 del self.fullseries[i]
1162 1162 else:
1163 1163 unknown.append(p)
1164 1164
1165 1165 if unknown:
1166 1166 if numrevs:
1167 1167 rev = {entry.name: entry.node for entry in qfinished}
1168 1168 for p in unknown:
1169 1169 msg = _(b'revision %s refers to unknown patches: %s\n')
1170 1170 self.ui.warn(msg % (short(rev[p]), p))
1171 1171 else:
1172 1172 msg = _(b'unknown patches: %s\n')
1173 1173 raise error.Abort(b''.join(msg % p for p in unknown))
1174 1174
1175 1175 self.parseseries()
1176 1176 self.seriesdirty = True
1177 1177 return [entry.node for entry in qfinished]
1178 1178
1179 1179 def _revpatches(self, repo, revs):
1180 1180 firstrev = repo[self.applied[0].node].rev()
1181 1181 patches = []
1182 1182 for i, rev in enumerate(revs):
1183 1183 if rev < firstrev:
1184 1184 raise error.Abort(_(b'revision %d is not managed') % rev)
1185 1185
1186 1186 ctx = repo[rev]
1187 1187 base = self.applied[i].node
1188 1188 if ctx.node() != base:
1189 1189 msg = _(b'cannot delete revision %d above applied patches')
1190 1190 raise error.Abort(msg % rev)
1191 1191
1192 1192 patch = self.applied[i].name
1193 1193 for fmt in (b'[mq]: %s', b'imported patch %s'):
1194 1194 if ctx.description() == fmt % patch:
1195 1195 msg = _(b'patch %s finalized without changeset message\n')
1196 1196 repo.ui.status(msg % patch)
1197 1197 break
1198 1198
1199 1199 patches.append(patch)
1200 1200 return patches
1201 1201
1202 1202 def finish(self, repo, revs):
1203 1203 # Manually trigger phase computation to ensure phasedefaults is
1204 1204 # executed before we remove the patches.
1205 1205 repo._phasecache
1206 1206 patches = self._revpatches(repo, sorted(revs))
1207 1207 qfinished = self._cleanup(patches, len(patches))
1208 1208 if qfinished and repo.ui.configbool(b'mq', b'secret'):
1209 1209 # only use this logic when the secret option is added
1210 1210 oldqbase = repo[qfinished[0]]
1211 1211 tphase = phases.newcommitphase(repo.ui)
1212 1212 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
1213 1213 with repo.transaction(b'qfinish') as tr:
1214 1214 phases.advanceboundary(repo, tr, tphase, qfinished)
1215 1215
1216 1216 def delete(self, repo, patches, opts):
1217 1217 if not patches and not opts.get(b'rev'):
1218 1218 raise error.Abort(
1219 1219 _(b'qdelete requires at least one revision or patch name')
1220 1220 )
1221 1221
1222 1222 realpatches = []
1223 1223 for patch in patches:
1224 1224 patch = self.lookup(patch, strict=True)
1225 1225 info = self.isapplied(patch)
1226 1226 if info:
1227 1227 raise error.Abort(_(b"cannot delete applied patch %s") % patch)
1228 1228 if patch not in self.series:
1229 1229 raise error.Abort(_(b"patch %s not in series file") % patch)
1230 1230 if patch not in realpatches:
1231 1231 realpatches.append(patch)
1232 1232
1233 1233 numrevs = 0
1234 1234 if opts.get(b'rev'):
1235 1235 if not self.applied:
1236 1236 raise error.Abort(_(b'no patches applied'))
1237 1237 revs = logcmdutil.revrange(repo, opts.get(b'rev'))
1238 1238 revs.sort()
1239 1239 revpatches = self._revpatches(repo, revs)
1240 1240 realpatches += revpatches
1241 1241 numrevs = len(revpatches)
1242 1242
1243 1243 self._cleanup(realpatches, numrevs, opts.get(b'keep'))
1244 1244
1245 1245 def checktoppatch(self, repo):
1246 1246 '''check that working directory is at qtip'''
1247 1247 if self.applied:
1248 1248 top = self.applied[-1].node
1249 1249 patch = self.applied[-1].name
1250 1250 if repo.dirstate.p1() != top:
1251 1251 raise error.Abort(_(b"working directory revision is not qtip"))
1252 1252 return top, patch
1253 1253 return None, None
1254 1254
1255 1255 def putsubstate2changes(self, substatestate, changes):
1256 1256 if isinstance(changes, list):
1257 1257 mar = changes[:3]
1258 1258 else:
1259 1259 mar = (changes.modified, changes.added, changes.removed)
1260 1260 if any((b'.hgsubstate' in files for files in mar)):
1261 1261 return # already listed up
1262 1262 # not yet listed up
1263 1263 if substatestate.added or not substatestate.any_tracked:
1264 1264 mar[1].append(b'.hgsubstate')
1265 1265 elif substatestate.removed:
1266 1266 mar[2].append(b'.hgsubstate')
1267 1267 else: # modified
1268 1268 mar[0].append(b'.hgsubstate')
1269 1269
1270 1270 def checklocalchanges(self, repo, force=False, refresh=True):
1271 1271 excsuffix = b''
1272 1272 if refresh:
1273 1273 excsuffix = b', qrefresh first'
1274 1274 # plain versions for i18n tool to detect them
1275 1275 _(b"local changes found, qrefresh first")
1276 1276 _(b"local changed subrepos found, qrefresh first")
1277 1277
1278 1278 s = repo.status()
1279 1279 if not force:
1280 1280 cmdutil.checkunfinished(repo)
1281 1281 if s.modified or s.added or s.removed or s.deleted:
1282 1282 _(b"local changes found") # i18n tool detection
1283 1283 raise error.Abort(_(b"local changes found" + excsuffix))
1284 1284 if checksubstate(repo):
1285 1285 _(b"local changed subrepos found") # i18n tool detection
1286 1286 raise error.Abort(
1287 1287 _(b"local changed subrepos found" + excsuffix)
1288 1288 )
1289 1289 else:
1290 1290 cmdutil.checkunfinished(repo, skipmerge=True)
1291 1291 return s
1292 1292
1293 1293 _reserved = (b'series', b'status', b'guards', b'.', b'..')
1294 1294
1295 1295 def checkreservedname(self, name):
1296 1296 if name in self._reserved:
1297 1297 raise error.Abort(
1298 1298 _(b'"%s" cannot be used as the name of a patch') % name
1299 1299 )
1300 1300 if name != name.strip():
1301 1301 # whitespace is stripped by parseseries()
1302 1302 raise error.Abort(
1303 1303 _(b'patch name cannot begin or end with whitespace')
1304 1304 )
1305 1305 for prefix in (b'.hg', b'.mq'):
1306 1306 if name.startswith(prefix):
1307 1307 raise error.Abort(
1308 1308 _(b'patch name cannot begin with "%s"') % prefix
1309 1309 )
1310 1310 for c in (b'#', b':', b'\r', b'\n'):
1311 1311 if c in name:
1312 1312 raise error.Abort(
1313 1313 _(b'%r cannot be used in the name of a patch')
1314 1314 % pycompat.bytestr(c)
1315 1315 )
1316 1316
1317 1317 def checkpatchname(self, name, force=False):
1318 1318 self.checkreservedname(name)
1319 1319 if not force and os.path.exists(self.join(name)):
1320 1320 if os.path.isdir(self.join(name)):
1321 1321 raise error.Abort(
1322 1322 _(b'"%s" already exists as a directory') % name
1323 1323 )
1324 1324 else:
1325 1325 raise error.Abort(_(b'patch "%s" already exists') % name)
1326 1326
1327 1327 def makepatchname(self, title, fallbackname):
1328 1328 """Return a suitable filename for title, adding a suffix to make
1329 1329 it unique in the existing list"""
1330 1330 namebase = re.sub(br'[\s\W_]+', b'_', title.lower()).strip(b'_')
1331 1331 namebase = namebase[:75] # avoid too long name (issue5117)
1332 1332 if namebase:
1333 1333 try:
1334 1334 self.checkreservedname(namebase)
1335 1335 except error.Abort:
1336 1336 namebase = fallbackname
1337 1337 else:
1338 1338 namebase = fallbackname
1339 1339 name = namebase
1340 1340 i = 0
1341 1341 while True:
1342 1342 if name not in self.fullseries:
1343 1343 try:
1344 1344 self.checkpatchname(name)
1345 1345 break
1346 1346 except error.Abort:
1347 1347 pass
1348 1348 i += 1
1349 1349 name = b'%s__%d' % (namebase, i)
1350 1350 return name
1351 1351
1352 1352 def checkkeepchanges(self, keepchanges, force):
1353 1353 if force and keepchanges:
1354 1354 raise error.Abort(_(b'cannot use both --force and --keep-changes'))
1355 1355
1356 1356 def new(self, repo, patchfn, *pats, **opts):
1357 1357 """options:
1358 1358 msg: a string or a no-argument function returning a string
1359 1359 """
1360 1360 opts = pycompat.byteskwargs(opts)
1361 1361 msg = opts.get(b'msg')
1362 1362 edit = opts.get(b'edit')
1363 1363 editform = opts.get(b'editform', b'mq.qnew')
1364 1364 user = opts.get(b'user')
1365 1365 date = opts.get(b'date')
1366 1366 if date:
1367 1367 date = dateutil.parsedate(date)
1368 1368 diffopts = self.diffopts({b'git': opts.get(b'git')}, plain=True)
1369 1369 if opts.get(b'checkname', True):
1370 1370 self.checkpatchname(patchfn)
1371 1371 inclsubs = checksubstate(repo)
1372 1372 if inclsubs:
1373 1373 substatestate = repo.dirstate.get_entry(b'.hgsubstate')
1374 1374 if opts.get(b'include') or opts.get(b'exclude') or pats:
1375 1375 # detect missing files in pats
1376 1376 def badfn(f, msg):
1377 1377 if f != b'.hgsubstate': # .hgsubstate is auto-created
1378 1378 raise error.Abort(b'%s: %s' % (f, msg))
1379 1379
1380 1380 match = scmutil.match(repo[None], pats, opts, badfn=badfn)
1381 1381 changes = repo.status(match=match)
1382 1382 else:
1383 1383 changes = self.checklocalchanges(repo, force=True)
1384 1384 commitfiles = list(inclsubs)
1385 1385 commitfiles.extend(changes.modified)
1386 1386 commitfiles.extend(changes.added)
1387 1387 commitfiles.extend(changes.removed)
1388 1388 match = scmutil.matchfiles(repo, commitfiles)
1389 1389 if len(repo[None].parents()) > 1:
1390 1390 raise error.Abort(_(b'cannot manage merge changesets'))
1391 1391 self.checktoppatch(repo)
1392 1392 insert = self.fullseriesend()
1393 1393 with repo.wlock():
1394 1394 try:
1395 1395 # if patch file write fails, abort early
1396 1396 p = self.opener(patchfn, b"w")
1397 1397 except IOError as e:
1398 1398 raise error.Abort(
1399 1399 _(b'cannot write patch "%s": %s')
1400 1400 % (patchfn, encoding.strtolocal(e.strerror))
1401 1401 )
1402 1402 try:
1403 1403 defaultmsg = b"[mq]: %s" % patchfn
1404 1404 editor = cmdutil.getcommiteditor(editform=editform)
1405 1405 if edit:
1406 1406
1407 1407 def finishdesc(desc):
1408 1408 if desc.rstrip():
1409 1409 return desc
1410 1410 else:
1411 1411 return defaultmsg
1412 1412
1413 1413 # i18n: this message is shown in editor with "HG: " prefix
1414 1414 extramsg = _(b'Leave message empty to use default message.')
1415 1415 editor = cmdutil.getcommiteditor(
1416 1416 finishdesc=finishdesc,
1417 1417 extramsg=extramsg,
1418 1418 editform=editform,
1419 1419 )
1420 1420 commitmsg = msg
1421 1421 else:
1422 1422 commitmsg = msg or defaultmsg
1423 1423
1424 1424 n = newcommit(
1425 1425 repo,
1426 1426 None,
1427 1427 commitmsg,
1428 1428 user,
1429 1429 date,
1430 1430 match=match,
1431 1431 force=True,
1432 1432 editor=editor,
1433 1433 )
1434 1434 if n is None:
1435 1435 raise error.Abort(_(b"repo commit failed"))
1436 1436 try:
1437 1437 self.fullseries[insert:insert] = [patchfn]
1438 1438 self.applied.append(statusentry(n, patchfn))
1439 1439 self.parseseries()
1440 1440 self.seriesdirty = True
1441 1441 self.applieddirty = True
1442 1442 nctx = repo[n]
1443 1443 ph = patchheader(self.join(patchfn), self.plainmode)
1444 1444 if user:
1445 1445 ph.setuser(user)
1446 1446 if date:
1447 1447 ph.setdate(b'%d %d' % date)
1448 1448 ph.setparent(hex(nctx.p1().node()))
1449 1449 msg = nctx.description().strip()
1450 1450 if msg == defaultmsg.strip():
1451 1451 msg = b''
1452 1452 ph.setmessage(msg)
1453 1453 p.write(bytes(ph))
1454 1454 if commitfiles:
1455 1455 parent = self.qparents(repo, n)
1456 1456 if inclsubs:
1457 1457 self.putsubstate2changes(substatestate, changes)
1458 1458 chunks = patchmod.diff(
1459 1459 repo,
1460 1460 node1=parent,
1461 1461 node2=n,
1462 1462 changes=changes,
1463 1463 opts=diffopts,
1464 1464 )
1465 1465 for chunk in chunks:
1466 1466 p.write(chunk)
1467 1467 p.close()
1468 1468 r = self.qrepo()
1469 1469 if r:
1470 1470 with r.wlock(), r.dirstate.changing_files(r):
1471 1471 r[None].add([patchfn])
1472 1472 except: # re-raises
1473 1473 repo.rollback()
1474 1474 raise
1475 1475 except Exception:
1476 1476 patchpath = self.join(patchfn)
1477 1477 try:
1478 1478 os.unlink(patchpath)
1479 1479 except OSError:
1480 1480 self.ui.warn(_(b'error unlinking %s\n') % patchpath)
1481 1481 raise
1482 1482 self.removeundo(repo)
1483 1483
1484 1484 def isapplied(self, patch):
1485 1485 """returns (index, rev, patch)"""
1486 1486 for i, a in enumerate(self.applied):
1487 1487 if a.name == patch:
1488 1488 return (i, a.node, a.name)
1489 1489 return None
1490 1490
1491 1491 # if the exact patch name does not exist, we try a few
1492 1492 # variations. If strict is passed, we try only #1
1493 1493 #
1494 1494 # 1) a number (as string) to indicate an offset in the series file
1495 1495 # 2) a unique substring of the patch name was given
1496 1496 # 3) patchname[-+]num to indicate an offset in the series file
1497 1497 def lookup(self, patch, strict=False):
1498 1498 def partialname(s):
1499 1499 if s in self.series:
1500 1500 return s
1501 1501 matches = [x for x in self.series if s in x]
1502 1502 if len(matches) > 1:
1503 1503 self.ui.warn(_(b'patch name "%s" is ambiguous:\n') % s)
1504 1504 for m in matches:
1505 1505 self.ui.warn(b' %s\n' % m)
1506 1506 return None
1507 1507 if matches:
1508 1508 return matches[0]
1509 1509 if self.series and self.applied:
1510 1510 if s == b'qtip':
1511 1511 return self.series[self.seriesend(True) - 1]
1512 1512 if s == b'qbase':
1513 1513 return self.series[0]
1514 1514 return None
1515 1515
1516 1516 if patch in self.series:
1517 1517 return patch
1518 1518
1519 1519 if not os.path.isfile(self.join(patch)):
1520 1520 try:
1521 1521 sno = int(patch)
1522 1522 except (ValueError, OverflowError):
1523 1523 pass
1524 1524 else:
1525 1525 if -len(self.series) <= sno < len(self.series):
1526 1526 return self.series[sno]
1527 1527
1528 1528 if not strict:
1529 1529 res = partialname(patch)
1530 1530 if res:
1531 1531 return res
1532 1532 minus = patch.rfind(b'-')
1533 1533 if minus >= 0:
1534 1534 res = partialname(patch[:minus])
1535 1535 if res:
1536 1536 i = self.series.index(res)
1537 1537 try:
1538 1538 off = int(patch[minus + 1 :] or 1)
1539 1539 except (ValueError, OverflowError):
1540 1540 pass
1541 1541 else:
1542 1542 if i - off >= 0:
1543 1543 return self.series[i - off]
1544 1544 plus = patch.rfind(b'+')
1545 1545 if plus >= 0:
1546 1546 res = partialname(patch[:plus])
1547 1547 if res:
1548 1548 i = self.series.index(res)
1549 1549 try:
1550 1550 off = int(patch[plus + 1 :] or 1)
1551 1551 except (ValueError, OverflowError):
1552 1552 pass
1553 1553 else:
1554 1554 if i + off < len(self.series):
1555 1555 return self.series[i + off]
1556 1556 raise error.Abort(_(b"patch %s not in series") % patch)
1557 1557
1558 1558 def push(
1559 1559 self,
1560 1560 repo,
1561 1561 patch=None,
1562 1562 force=False,
1563 1563 list=False,
1564 1564 mergeq=None,
1565 1565 all=False,
1566 1566 move=False,
1567 1567 exact=False,
1568 1568 nobackup=False,
1569 1569 keepchanges=False,
1570 1570 ):
1571 1571 self.checkkeepchanges(keepchanges, force)
1572 1572 diffopts = self.diffopts()
1573 1573 with repo.wlock():
1574 1574 heads = []
1575 1575 for hs in repo.branchmap().iterheads():
1576 1576 heads.extend(hs)
1577 1577 if not heads:
1578 1578 heads = [repo.nullid]
1579 1579 if repo.dirstate.p1() not in heads and not exact:
1580 1580 self.ui.status(_(b"(working directory not at a head)\n"))
1581 1581
1582 1582 if not self.series:
1583 1583 self.ui.warn(_(b'no patches in series\n'))
1584 1584 return 0
1585 1585
1586 1586 # Suppose our series file is: A B C and the current 'top'
1587 1587 # patch is B. qpush C should be performed (moving forward)
1588 1588 # qpush B is a NOP (no change) qpush A is an error (can't
1589 1589 # go backwards with qpush)
1590 1590 if patch:
1591 1591 patch = self.lookup(patch)
1592 1592 info = self.isapplied(patch)
1593 1593 if info and info[0] >= len(self.applied) - 1:
1594 1594 self.ui.warn(
1595 1595 _(b'qpush: %s is already at the top\n') % patch
1596 1596 )
1597 1597 return 0
1598 1598
1599 1599 pushable, reason = self.pushable(patch)
1600 1600 if pushable:
1601 1601 if self.series.index(patch) < self.seriesend():
1602 1602 raise error.Abort(
1603 1603 _(b"cannot push to a previous patch: %s") % patch
1604 1604 )
1605 1605 else:
1606 1606 if reason:
1607 1607 reason = _(b'guarded by %s') % reason
1608 1608 else:
1609 1609 reason = _(b'no matching guards')
1610 1610 self.ui.warn(
1611 1611 _(b"cannot push '%s' - %s\n") % (patch, reason)
1612 1612 )
1613 1613 return 1
1614 1614 elif all:
1615 1615 patch = self.series[-1]
1616 1616 if self.isapplied(patch):
1617 1617 self.ui.warn(_(b'all patches are currently applied\n'))
1618 1618 return 0
1619 1619
1620 1620 # Following the above example, starting at 'top' of B:
1621 1621 # qpush should be performed (pushes C), but a subsequent
1622 1622 # qpush without an argument is an error (nothing to
1623 1623 # apply). This allows a loop of "...while hg qpush..." to
1624 1624 # work as it detects an error when done
1625 1625 start = self.seriesend()
1626 1626 if start == len(self.series):
1627 1627 self.ui.warn(_(b'patch series already fully applied\n'))
1628 1628 return 1
1629 1629 if not force and not keepchanges:
1630 1630 self.checklocalchanges(repo, refresh=self.applied)
1631 1631
1632 1632 if exact:
1633 1633 if keepchanges:
1634 1634 raise error.Abort(
1635 1635 _(b"cannot use --exact and --keep-changes together")
1636 1636 )
1637 1637 if move:
1638 1638 raise error.Abort(
1639 1639 _(b'cannot use --exact and --move together')
1640 1640 )
1641 1641 if self.applied:
1642 1642 raise error.Abort(
1643 1643 _(b'cannot push --exact with applied patches')
1644 1644 )
1645 1645 root = self.series[start]
1646 1646 target = patchheader(self.join(root), self.plainmode).parent
1647 1647 if not target:
1648 1648 raise error.Abort(
1649 1649 _(b"%s does not have a parent recorded") % root
1650 1650 )
1651 1651 if not repo[target] == repo[b'.']:
1652 1652 hg.update(repo, target)
1653 1653
1654 1654 if move:
1655 1655 if not patch:
1656 1656 raise error.Abort(_(b"please specify the patch to move"))
1657 1657 for fullstart, rpn in enumerate(self.fullseries):
1658 1658 # strip markers for patch guards
1659 1659 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1660 1660 break
1661 1661 for i, rpn in enumerate(self.fullseries[fullstart:]):
1662 1662 # strip markers for patch guards
1663 1663 if self.guard_re.split(rpn, 1)[0] == patch:
1664 1664 break
1665 1665 index = fullstart + i
1666 1666 assert index < len(self.fullseries)
1667 1667 fullpatch = self.fullseries[index]
1668 1668 del self.fullseries[index]
1669 1669 self.fullseries.insert(fullstart, fullpatch)
1670 1670 self.parseseries()
1671 1671 self.seriesdirty = True
1672 1672
1673 1673 self.applieddirty = True
1674 1674 if start > 0:
1675 1675 self.checktoppatch(repo)
1676 1676 if not patch:
1677 1677 patch = self.series[start]
1678 1678 end = start + 1
1679 1679 else:
1680 1680 end = self.series.index(patch, start) + 1
1681 1681
1682 1682 tobackup = set()
1683 1683 if (not nobackup and force) or keepchanges:
1684 1684 status = self.checklocalchanges(repo, force=True)
1685 1685 if keepchanges:
1686 1686 tobackup.update(
1687 1687 status.modified
1688 1688 + status.added
1689 1689 + status.removed
1690 1690 + status.deleted
1691 1691 )
1692 1692 else:
1693 1693 tobackup.update(status.modified + status.added)
1694 1694
1695 1695 s = self.series[start:end]
1696 1696 all_files = set()
1697 1697 try:
1698 1698 if mergeq:
1699 1699 ret = self.mergepatch(repo, mergeq, s, diffopts)
1700 1700 else:
1701 1701 ret = self.apply(
1702 1702 repo,
1703 1703 s,
1704 1704 list,
1705 1705 all_files=all_files,
1706 1706 tobackup=tobackup,
1707 1707 keepchanges=keepchanges,
1708 1708 )
1709 1709 except AbortNoCleanup:
1710 1710 raise
1711 1711 except: # re-raises
1712 1712 self.ui.warn(_(b'cleaning up working directory...\n'))
1713 1713 cmdutil.revert(
1714 1714 self.ui,
1715 1715 repo,
1716 1716 repo[b'.'],
1717 1717 no_backup=True,
1718 1718 )
1719 1719 # only remove unknown files that we know we touched or
1720 1720 # created while patching
1721 1721 for f in all_files:
1722 1722 if f not in repo.dirstate:
1723 1723 repo.wvfs.unlinkpath(f, ignoremissing=True)
1724 1724 self.ui.warn(_(b'done\n'))
1725 1725 raise
1726 1726
1727 1727 if not self.applied:
1728 1728 return ret[0]
1729 1729 top = self.applied[-1].name
1730 1730 if ret[0] and ret[0] > 1:
1731 1731 msg = _(b"errors during apply, please fix and qrefresh %s\n")
1732 1732 self.ui.write(msg % top)
1733 1733 else:
1734 1734 self.ui.write(_(b"now at: %s\n") % top)
1735 1735 return ret[0]
1736 1736
1737 1737 def pop(
1738 1738 self,
1739 1739 repo,
1740 1740 patch=None,
1741 1741 force=False,
1742 1742 update=True,
1743 1743 all=False,
1744 1744 nobackup=False,
1745 1745 keepchanges=False,
1746 1746 ):
1747 1747 self.checkkeepchanges(keepchanges, force)
1748 1748 with repo.wlock():
1749 1749 if patch:
1750 1750 # index, rev, patch
1751 1751 info = self.isapplied(patch)
1752 1752 if not info:
1753 1753 patch = self.lookup(patch)
1754 1754 info = self.isapplied(patch)
1755 1755 if not info:
1756 1756 raise error.Abort(_(b"patch %s is not applied") % patch)
1757 1757
1758 1758 if not self.applied:
1759 1759 # Allow qpop -a to work repeatedly,
1760 1760 # but not qpop without an argument
1761 1761 self.ui.warn(_(b"no patches applied\n"))
1762 1762 return not all
1763 1763
1764 1764 if all:
1765 1765 start = 0
1766 1766 elif patch:
1767 1767 start = info[0] + 1
1768 1768 else:
1769 1769 start = len(self.applied) - 1
1770 1770
1771 1771 if start >= len(self.applied):
1772 1772 self.ui.warn(_(b"qpop: %s is already at the top\n") % patch)
1773 1773 return
1774 1774
1775 1775 if not update:
1776 1776 parents = repo.dirstate.parents()
1777 1777 rr = [x.node for x in self.applied]
1778 1778 for p in parents:
1779 1779 if p in rr:
1780 1780 self.ui.warn(_(b"qpop: forcing dirstate update\n"))
1781 1781 update = True
1782 1782 else:
1783 1783 parents = [p.node() for p in repo[None].parents()]
1784 1784 update = any(
1785 1785 entry.node in parents for entry in self.applied[start:]
1786 1786 )
1787 1787
1788 1788 tobackup = set()
1789 1789 if update:
1790 1790 s = self.checklocalchanges(repo, force=force or keepchanges)
1791 1791 if force:
1792 1792 if not nobackup:
1793 1793 tobackup.update(s.modified + s.added)
1794 1794 elif keepchanges:
1795 1795 tobackup.update(
1796 1796 s.modified + s.added + s.removed + s.deleted
1797 1797 )
1798 1798
1799 1799 self.applieddirty = True
1800 1800 end = len(self.applied)
1801 1801 rev = self.applied[start].node
1802 1802
1803 1803 try:
1804 1804 heads = repo.changelog.heads(rev)
1805 1805 except error.LookupError:
1806 1806 node = short(rev)
1807 1807 raise error.Abort(_(b'trying to pop unknown node %s') % node)
1808 1808
1809 1809 if heads != [self.applied[-1].node]:
1810 1810 raise error.Abort(
1811 1811 _(
1812 1812 b"popping would remove a revision not "
1813 1813 b"managed by this patch queue"
1814 1814 )
1815 1815 )
1816 1816 if not repo[self.applied[-1].node].mutable():
1817 1817 raise error.Abort(
1818 1818 _(b"popping would remove a public revision"),
1819 1819 hint=_(b"see 'hg help phases' for details"),
1820 1820 )
1821 1821
1822 1822 # we know there are no local changes, so we can make a simplified
1823 1823 # form of hg.update.
1824 1824 if update:
1825 1825 qp = self.qparents(repo, rev)
1826 1826 ctx = repo[qp]
1827 1827 st = repo.status(qp, b'.')
1828 1828 m, a, r, d = st.modified, st.added, st.removed, st.deleted
1829 1829 if d:
1830 1830 raise error.Abort(_(b"deletions found between repo revs"))
1831 1831
1832 1832 tobackup = set(a + m + r) & tobackup
1833 1833 if keepchanges and tobackup:
1834 1834 raise error.Abort(_(b"local changes found, qrefresh first"))
1835 1835 self.backup(repo, tobackup)
1836 1836 with repo.dirstate.changing_parents(repo):
1837 1837 for f in a:
1838 1838 repo.wvfs.unlinkpath(f, ignoremissing=True)
1839 1839 repo.dirstate.update_file(
1840 1840 f, p1_tracked=False, wc_tracked=False
1841 1841 )
1842 1842 for f in m + r:
1843 1843 fctx = ctx[f]
1844 1844 repo.wwrite(f, fctx.data(), fctx.flags())
1845 1845 repo.dirstate.update_file(
1846 1846 f, p1_tracked=True, wc_tracked=True
1847 1847 )
1848 1848 repo.setparents(qp, repo.nullid)
1849 1849 for patch in reversed(self.applied[start:end]):
1850 1850 self.ui.status(_(b"popping %s\n") % patch.name)
1851 1851 del self.applied[start:end]
1852 1852 strip(self.ui, repo, [rev], update=False, backup=False)
1853 1853 for s, state in repo[b'.'].substate.items():
1854 1854 repo[b'.'].sub(s).get(state)
1855 1855 if self.applied:
1856 1856 self.ui.write(_(b"now at: %s\n") % self.applied[-1].name)
1857 1857 else:
1858 1858 self.ui.write(_(b"patch queue now empty\n"))
1859 1859
1860 1860 def diff(self, repo, pats, opts):
1861 1861 top, patch = self.checktoppatch(repo)
1862 1862 if not top:
1863 1863 self.ui.write(_(b"no patches applied\n"))
1864 1864 return
1865 1865 qp = self.qparents(repo, top)
1866 1866 if opts.get(b'reverse'):
1867 1867 node1, node2 = None, qp
1868 1868 else:
1869 1869 node1, node2 = qp, None
1870 1870 diffopts = self.diffopts(opts, patch)
1871 1871 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1872 1872
1873 1873 def refresh(self, repo, pats=None, **opts):
1874 1874 opts = pycompat.byteskwargs(opts)
1875 1875 if not self.applied:
1876 1876 self.ui.write(_(b"no patches applied\n"))
1877 1877 return 1
1878 1878 msg = opts.get(b'msg', b'').rstrip()
1879 1879 edit = opts.get(b'edit')
1880 1880 editform = opts.get(b'editform', b'mq.qrefresh')
1881 1881 newuser = opts.get(b'user')
1882 1882 newdate = opts.get(b'date')
1883 1883 if newdate:
1884 1884 newdate = b'%d %d' % dateutil.parsedate(newdate)
1885 1885 wlock = repo.wlock()
1886 1886
1887 1887 try:
1888 1888 self.checktoppatch(repo)
1889 1889 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1890 1890 if repo.changelog.heads(top) != [top]:
1891 1891 raise error.Abort(
1892 1892 _(b"cannot qrefresh a revision with children")
1893 1893 )
1894 1894 if not repo[top].mutable():
1895 1895 raise error.Abort(
1896 1896 _(b"cannot qrefresh public revision"),
1897 1897 hint=_(b"see 'hg help phases' for details"),
1898 1898 )
1899 1899
1900 1900 cparents = repo.changelog.parents(top)
1901 1901 patchparent = self.qparents(repo, top)
1902 1902
1903 1903 inclsubs = checksubstate(repo, patchparent)
1904 1904 if inclsubs:
1905 1905 substatestate = repo.dirstate.get_entry(b'.hgsubstate')
1906 1906
1907 1907 ph = patchheader(self.join(patchfn), self.plainmode)
1908 1908 diffopts = self.diffopts(
1909 1909 {b'git': opts.get(b'git')}, patchfn, plain=True
1910 1910 )
1911 1911 if newuser:
1912 1912 ph.setuser(newuser)
1913 1913 if newdate:
1914 1914 ph.setdate(newdate)
1915 1915 ph.setparent(hex(patchparent))
1916 1916
1917 1917 # only commit new patch when write is complete
1918 1918 patchf = self.opener(patchfn, b'w', atomictemp=True)
1919 1919
1920 1920 # update the dirstate in place, strip off the qtip commit
1921 1921 # and then commit.
1922 1922 #
1923 1923 # this should really read:
1924 1924 # st = repo.status(top, patchparent)
1925 1925 # but we do it backwards to take advantage of manifest/changelog
1926 1926 # caching against the next repo.status call
1927 1927 st = repo.status(patchparent, top)
1928 1928 mm, aa, dd = st.modified, st.added, st.removed
1929 1929 ctx = repo[top]
1930 1930 aaa = aa[:]
1931 1931 match1 = scmutil.match(repo[None], pats, opts)
1932 1932 # in short mode, we only diff the files included in the
1933 1933 # patch already plus specified files
1934 1934 if opts.get(b'short'):
1935 1935 # if amending a patch, we start with existing
1936 1936 # files plus specified files - unfiltered
1937 1937 match = scmutil.matchfiles(repo, mm + aa + dd + match1.files())
1938 1938 # filter with include/exclude options
1939 1939 match1 = scmutil.match(repo[None], opts=opts)
1940 1940 else:
1941 1941 match = scmutil.matchall(repo)
1942 1942 stb = repo.status(match=match)
1943 1943 m, a, r, d = stb.modified, stb.added, stb.removed, stb.deleted
1944 1944 mm = set(mm)
1945 1945 aa = set(aa)
1946 1946 dd = set(dd)
1947 1947
1948 1948 # we might end up with files that were added between
1949 1949 # qtip and the dirstate parent, but then changed in the
1950 1950 # local dirstate. in this case, we want them to only
1951 1951 # show up in the added section
1952 1952 for x in m:
1953 1953 if x not in aa:
1954 1954 mm.add(x)
1955 1955 # we might end up with files added by the local dirstate that
1956 1956 # were deleted by the patch. In this case, they should only
1957 1957 # show up in the changed section.
1958 1958 for x in a:
1959 1959 if x in dd:
1960 1960 dd.remove(x)
1961 1961 mm.add(x)
1962 1962 else:
1963 1963 aa.add(x)
1964 1964 # make sure any files deleted in the local dirstate
1965 1965 # are not in the add or change column of the patch
1966 1966 forget = []
1967 1967 for x in d + r:
1968 1968 if x in aa:
1969 1969 aa.remove(x)
1970 1970 forget.append(x)
1971 1971 continue
1972 1972 else:
1973 1973 mm.discard(x)
1974 1974 dd.add(x)
1975 1975
1976 1976 m = list(mm)
1977 1977 r = list(dd)
1978 1978 a = list(aa)
1979 1979
1980 1980 # create 'match' that includes the files to be recommitted.
1981 1981 # apply match1 via repo.status to ensure correct case handling.
1982 1982 st = repo.status(patchparent, match=match1)
1983 1983 cm, ca, cr, cd = st.modified, st.added, st.removed, st.deleted
1984 1984 allmatches = set(cm + ca + cr + cd)
1985 1985 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1986 1986
1987 1987 files = set(inclsubs)
1988 1988 for x in refreshchanges:
1989 1989 files.update(x)
1990 1990 match = scmutil.matchfiles(repo, files)
1991 1991
1992 1992 bmlist = repo[top].bookmarks()
1993 1993
1994 1994 with repo.dirstate.changing_parents(repo):
1995 1995 if diffopts.git or diffopts.upgrade:
1996 1996 copies = {}
1997 1997 for dst in a:
1998 1998 src = repo.dirstate.copied(dst)
1999 1999 # during qfold, the source file for copies may
2000 2000 # be removed. Treat this as a simple add.
2001 2001 if src is not None and src in repo.dirstate:
2002 2002 copies.setdefault(src, []).append(dst)
2003 2003 repo.dirstate.update_file(
2004 2004 dst, p1_tracked=False, wc_tracked=True
2005 2005 )
2006 2006 # remember the copies between patchparent and qtip
2007 2007 for dst in aaa:
2008 2008 src = ctx[dst].copysource()
2009 2009 if src:
2010 2010 copies.setdefault(src, []).extend(
2011 2011 copies.get(dst, [])
2012 2012 )
2013 2013 if dst in a:
2014 2014 copies[src].append(dst)
2015 2015 # we can't copy a file created by the patch itself
2016 2016 if dst in copies:
2017 2017 del copies[dst]
2018 2018 for src, dsts in copies.items():
2019 2019 for dst in dsts:
2020 2020 repo.dirstate.copy(src, dst)
2021 2021 else:
2022 2022 for dst in a:
2023 2023 repo.dirstate.update_file(
2024 2024 dst, p1_tracked=False, wc_tracked=True
2025 2025 )
2026 2026 # Drop useless copy information
2027 2027 for f in list(repo.dirstate.copies()):
2028 2028 repo.dirstate.copy(None, f)
2029 2029 for f in r:
2030 2030 repo.dirstate.update_file_p1(f, p1_tracked=True)
2031 2031 # if the patch excludes a modified file, mark that
2032 2032 # file with mtime=0 so status can see it.
2033 2033 mm = []
2034 2034 for i in range(len(m) - 1, -1, -1):
2035 2035 if not match1(m[i]):
2036 2036 mm.append(m[i])
2037 2037 del m[i]
2038 2038 for f in m:
2039 2039 repo.dirstate.update_file_p1(f, p1_tracked=True)
2040 2040 for f in mm:
2041 2041 repo.dirstate.update_file_p1(f, p1_tracked=True)
2042 2042 for f in forget:
2043 2043 repo.dirstate.update_file_p1(f, p1_tracked=False)
2044 2044
2045 2045 user = ph.user or ctx.user()
2046 2046
2047 2047 oldphase = repo[top].phase()
2048 2048
2049 2049 # assumes strip can roll itself back if interrupted
2050 2050 repo.setparents(*cparents)
2051 repo.dirstate.write(repo.currenttransaction())
2051 2052 self.applied.pop()
2052 2053 self.applieddirty = True
2053 2054 strip(self.ui, repo, [top], update=False, backup=False)
2054 2055
2055 2056 try:
2056 2057 # might be nice to attempt to roll back strip after this
2057 2058
2058 2059 defaultmsg = b"[mq]: %s" % patchfn
2059 2060 editor = cmdutil.getcommiteditor(editform=editform)
2060 2061 if edit:
2061 2062
2062 2063 def finishdesc(desc):
2063 2064 if desc.rstrip():
2064 2065 ph.setmessage(desc)
2065 2066 return desc
2066 2067 return defaultmsg
2067 2068
2068 2069 # i18n: this message is shown in editor with "HG: " prefix
2069 2070 extramsg = _(b'Leave message empty to use default message.')
2070 2071 editor = cmdutil.getcommiteditor(
2071 2072 finishdesc=finishdesc,
2072 2073 extramsg=extramsg,
2073 2074 editform=editform,
2074 2075 )
2075 2076 message = msg or b"\n".join(ph.message)
2076 2077 elif not msg:
2077 2078 if not ph.message:
2078 2079 message = defaultmsg
2079 2080 else:
2080 2081 message = b"\n".join(ph.message)
2081 2082 else:
2082 2083 message = msg
2083 2084 ph.setmessage(msg)
2084 2085
2085 2086 # Ensure we create a new changeset in the same phase than
2086 2087 # the old one.
2087 2088 lock = tr = None
2088 2089 try:
2089 2090 lock = repo.lock()
2090 2091 tr = repo.transaction(b'mq')
2091 2092 n = newcommit(
2092 2093 repo,
2093 2094 oldphase,
2094 2095 message,
2095 2096 user,
2096 2097 ph.date,
2097 2098 match=match,
2098 2099 force=True,
2099 2100 editor=editor,
2100 2101 )
2101 2102 # only write patch after a successful commit
2102 2103 c = [list(x) for x in refreshchanges]
2103 2104 if inclsubs:
2104 2105 self.putsubstate2changes(substatestate, c)
2105 2106 chunks = patchmod.diff(
2106 2107 repo, patchparent, changes=c, opts=diffopts
2107 2108 )
2108 2109 comments = bytes(ph)
2109 2110 if comments:
2110 2111 patchf.write(comments)
2111 2112 for chunk in chunks:
2112 2113 patchf.write(chunk)
2113 2114 patchf.close()
2114 2115
2115 2116 marks = repo._bookmarks
2116 2117 marks.applychanges(repo, tr, [(bm, n) for bm in bmlist])
2117 2118 tr.close()
2118 2119
2119 2120 self.applied.append(statusentry(n, patchfn))
2120 2121 finally:
2121 2122 lockmod.release(tr, lock)
2122 2123 except: # re-raises
2123 2124 ctx = repo[cparents[0]]
2124 2125 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
2125 2126 repo.dirstate.write(repo.currenttransaction())
2126 2127 self.savedirty()
2127 2128 self.ui.warn(
2128 2129 _(
2129 2130 b'qrefresh interrupted while patch was popped! '
2130 2131 b'(revert --all, qpush to recover)\n'
2131 2132 )
2132 2133 )
2133 2134 raise
2134 2135 finally:
2135 2136 wlock.release()
2136 2137 self.removeundo(repo)
2137 2138
2138 2139 def init(self, repo, create=False):
2139 2140 if not create and os.path.isdir(self.path):
2140 2141 raise error.Abort(_(b"patch queue directory already exists"))
2141 2142 try:
2142 2143 os.mkdir(self.path)
2143 2144 except FileExistsError:
2144 2145 if not create:
2145 2146 raise
2146 2147 if create:
2147 2148 return self.qrepo(create=True)
2148 2149
2149 2150 def unapplied(self, repo, patch=None):
2150 2151 if patch and patch not in self.series:
2151 2152 raise error.Abort(_(b"patch %s is not in series file") % patch)
2152 2153 if not patch:
2153 2154 start = self.seriesend()
2154 2155 else:
2155 2156 start = self.series.index(patch) + 1
2156 2157 unapplied = []
2157 2158 for i in range(start, len(self.series)):
2158 2159 pushable, reason = self.pushable(i)
2159 2160 if pushable:
2160 2161 unapplied.append((i, self.series[i]))
2161 2162 self.explainpushable(i)
2162 2163 return unapplied
2163 2164
2164 2165 def qseries(
2165 2166 self,
2166 2167 repo,
2167 2168 missing=None,
2168 2169 start=0,
2169 2170 length=None,
2170 2171 status=None,
2171 2172 summary=False,
2172 2173 ):
2173 2174 def displayname(pfx, patchname, state):
2174 2175 if pfx:
2175 2176 self.ui.write(pfx)
2176 2177 if summary:
2177 2178 ph = patchheader(self.join(patchname), self.plainmode)
2178 2179 if ph.message:
2179 2180 msg = ph.message[0]
2180 2181 else:
2181 2182 msg = b''
2182 2183
2183 2184 if self.ui.formatted():
2184 2185 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
2185 2186 if width > 0:
2186 2187 msg = stringutil.ellipsis(msg, width)
2187 2188 else:
2188 2189 msg = b''
2189 2190 self.ui.write(patchname, label=b'qseries.' + state)
2190 2191 self.ui.write(b': ')
2191 2192 self.ui.write(msg, label=b'qseries.message.' + state)
2192 2193 else:
2193 2194 self.ui.write(patchname, label=b'qseries.' + state)
2194 2195 self.ui.write(b'\n')
2195 2196
2196 2197 applied = {p.name for p in self.applied}
2197 2198 if length is None:
2198 2199 length = len(self.series) - start
2199 2200 if not missing:
2200 2201 if self.ui.verbose:
2201 2202 idxwidth = len(b"%d" % (start + length - 1))
2202 2203 for i in range(start, start + length):
2203 2204 patch = self.series[i]
2204 2205 if patch in applied:
2205 2206 char, state = b'A', b'applied'
2206 2207 elif self.pushable(i)[0]:
2207 2208 char, state = b'U', b'unapplied'
2208 2209 else:
2209 2210 char, state = b'G', b'guarded'
2210 2211 pfx = b''
2211 2212 if self.ui.verbose:
2212 2213 pfx = b'%*d %s ' % (idxwidth, i, char)
2213 2214 elif status and status != char:
2214 2215 continue
2215 2216 displayname(pfx, patch, state)
2216 2217 else:
2217 2218 msng_list = []
2218 2219 for root, dirs, files in os.walk(self.path):
2219 2220 d = root[len(self.path) + 1 :]
2220 2221 for f in files:
2221 2222 fl = os.path.join(d, f)
2222 2223 if (
2223 2224 fl not in self.series
2224 2225 and fl
2225 2226 not in (
2226 2227 self.statuspath,
2227 2228 self.seriespath,
2228 2229 self.guardspath,
2229 2230 )
2230 2231 and not fl.startswith(b'.')
2231 2232 ):
2232 2233 msng_list.append(fl)
2233 2234 for x in sorted(msng_list):
2234 2235 pfx = self.ui.verbose and b'D ' or b''
2235 2236 displayname(pfx, x, b'missing')
2236 2237
2237 2238 def issaveline(self, l):
2238 2239 if l.name == b'.hg.patches.save.line':
2239 2240 return True
2240 2241
2241 2242 def qrepo(self, create=False):
2242 2243 ui = self.baseui.copy()
2243 2244 # copy back attributes set by ui.pager()
2244 2245 if self.ui.pageractive and not ui.pageractive:
2245 2246 ui.pageractive = self.ui.pageractive
2246 2247 # internal config: ui.formatted
2247 2248 ui.setconfig(
2248 2249 b'ui',
2249 2250 b'formatted',
2250 2251 self.ui.config(b'ui', b'formatted'),
2251 2252 b'mqpager',
2252 2253 )
2253 2254 ui.setconfig(
2254 2255 b'ui',
2255 2256 b'interactive',
2256 2257 self.ui.config(b'ui', b'interactive'),
2257 2258 b'mqpager',
2258 2259 )
2259 2260 if create or os.path.isdir(self.join(b".hg")):
2260 2261 return hg.repository(ui, path=self.path, create=create)
2261 2262
2262 2263 def restore(self, repo, rev, delete=None, qupdate=None):
2263 2264 desc = repo[rev].description().strip()
2264 2265 lines = desc.splitlines()
2265 2266 datastart = None
2266 2267 series = []
2267 2268 applied = []
2268 2269 qpp = None
2269 2270 for i, line in enumerate(lines):
2270 2271 if line == b'Patch Data:':
2271 2272 datastart = i + 1
2272 2273 elif line.startswith(b'Dirstate:'):
2273 2274 l = line.rstrip()
2274 2275 l = l[10:].split(b' ')
2275 2276 qpp = [bin(x) for x in l]
2276 2277 elif datastart is not None:
2277 2278 l = line.rstrip()
2278 2279 n, name = l.split(b':', 1)
2279 2280 if n:
2280 2281 applied.append(statusentry(bin(n), name))
2281 2282 else:
2282 2283 series.append(l)
2283 2284 if datastart is None:
2284 2285 self.ui.warn(_(b"no saved patch data found\n"))
2285 2286 return 1
2286 2287 self.ui.warn(_(b"restoring status: %s\n") % lines[0])
2287 2288 self.fullseries = series
2288 2289 self.applied = applied
2289 2290 self.parseseries()
2290 2291 self.seriesdirty = True
2291 2292 self.applieddirty = True
2292 2293 heads = repo.changelog.heads()
2293 2294 if delete:
2294 2295 if rev not in heads:
2295 2296 self.ui.warn(_(b"save entry has children, leaving it alone\n"))
2296 2297 else:
2297 2298 self.ui.warn(_(b"removing save entry %s\n") % short(rev))
2298 2299 pp = repo.dirstate.parents()
2299 2300 if rev in pp:
2300 2301 update = True
2301 2302 else:
2302 2303 update = False
2303 2304 strip(self.ui, repo, [rev], update=update, backup=False)
2304 2305 if qpp:
2305 2306 self.ui.warn(
2306 2307 _(b"saved queue repository parents: %s %s\n")
2307 2308 % (short(qpp[0]), short(qpp[1]))
2308 2309 )
2309 2310 if qupdate:
2310 2311 self.ui.status(_(b"updating queue directory\n"))
2311 2312 r = self.qrepo()
2312 2313 if not r:
2313 2314 self.ui.warn(_(b"unable to load queue repository\n"))
2314 2315 return 1
2315 2316 hg.clean(r, qpp[0])
2316 2317
2317 2318 def save(self, repo, msg=None):
2318 2319 if not self.applied:
2319 2320 self.ui.warn(_(b"save: no patches applied, exiting\n"))
2320 2321 return 1
2321 2322 if self.issaveline(self.applied[-1]):
2322 2323 self.ui.warn(_(b"status is already saved\n"))
2323 2324 return 1
2324 2325
2325 2326 if not msg:
2326 2327 msg = _(b"hg patches saved state")
2327 2328 else:
2328 2329 msg = b"hg patches: " + msg.rstrip(b'\r\n')
2329 2330 r = self.qrepo()
2330 2331 if r:
2331 2332 pp = r.dirstate.parents()
2332 2333 msg += b"\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
2333 2334 msg += b"\n\nPatch Data:\n"
2334 2335 msg += b''.join(b'%s\n' % x for x in self.applied)
2335 2336 msg += b''.join(b':%s\n' % x for x in self.fullseries)
2336 2337 n = repo.commit(msg, force=True)
2337 2338 if not n:
2338 2339 self.ui.warn(_(b"repo commit failed\n"))
2339 2340 return 1
2340 2341 self.applied.append(statusentry(n, b'.hg.patches.save.line'))
2341 2342 self.applieddirty = True
2342 2343 self.removeundo(repo)
2343 2344
2344 2345 def fullseriesend(self):
2345 2346 if self.applied:
2346 2347 p = self.applied[-1].name
2347 2348 end = self.findseries(p)
2348 2349 if end is None:
2349 2350 return len(self.fullseries)
2350 2351 return end + 1
2351 2352 return 0
2352 2353
2353 2354 def seriesend(self, all_patches=False):
2354 2355 """If all_patches is False, return the index of the next pushable patch
2355 2356 in the series, or the series length. If all_patches is True, return the
2356 2357 index of the first patch past the last applied one.
2357 2358 """
2358 2359 end = 0
2359 2360
2360 2361 def nextpatch(start):
2361 2362 if all_patches or start >= len(self.series):
2362 2363 return start
2363 2364 for i in range(start, len(self.series)):
2364 2365 p, reason = self.pushable(i)
2365 2366 if p:
2366 2367 return i
2367 2368 self.explainpushable(i)
2368 2369 return len(self.series)
2369 2370
2370 2371 if self.applied:
2371 2372 p = self.applied[-1].name
2372 2373 try:
2373 2374 end = self.series.index(p)
2374 2375 except ValueError:
2375 2376 return 0
2376 2377 return nextpatch(end + 1)
2377 2378 return nextpatch(end)
2378 2379
2379 2380 def appliedname(self, index):
2380 2381 pname = self.applied[index].name
2381 2382 if not self.ui.verbose:
2382 2383 p = pname
2383 2384 else:
2384 2385 p = (b"%d" % self.series.index(pname)) + b" " + pname
2385 2386 return p
2386 2387
2387 2388 def qimport(
2388 2389 self,
2389 2390 repo,
2390 2391 files,
2391 2392 patchname=None,
2392 2393 rev=None,
2393 2394 existing=None,
2394 2395 force=None,
2395 2396 git=False,
2396 2397 ):
2397 2398 def checkseries(patchname):
2398 2399 if patchname in self.series:
2399 2400 raise error.Abort(
2400 2401 _(b'patch %s is already in the series file') % patchname
2401 2402 )
2402 2403
2403 2404 if rev:
2404 2405 if files:
2405 2406 raise error.Abort(
2406 2407 _(b'option "-r" not valid when importing files')
2407 2408 )
2408 2409 rev = logcmdutil.revrange(repo, rev)
2409 2410 rev.sort(reverse=True)
2410 2411 elif not files:
2411 2412 raise error.Abort(_(b'no files or revisions specified'))
2412 2413 if (len(files) > 1 or len(rev) > 1) and patchname:
2413 2414 raise error.Abort(
2414 2415 _(b'option "-n" not valid when importing multiple patches')
2415 2416 )
2416 2417 imported = []
2417 2418 if rev:
2418 2419 # If mq patches are applied, we can only import revisions
2419 2420 # that form a linear path to qbase.
2420 2421 # Otherwise, they should form a linear path to a head.
2421 2422 heads = repo.changelog.heads(repo.changelog.node(rev.first()))
2422 2423 if len(heads) > 1:
2423 2424 raise error.Abort(
2424 2425 _(b'revision %d is the root of more than one branch')
2425 2426 % rev.last()
2426 2427 )
2427 2428 if self.applied:
2428 2429 base = repo.changelog.node(rev.first())
2429 2430 if base in [n.node for n in self.applied]:
2430 2431 raise error.Abort(
2431 2432 _(b'revision %d is already managed') % rev.first()
2432 2433 )
2433 2434 if heads != [self.applied[-1].node]:
2434 2435 raise error.Abort(
2435 2436 _(b'revision %d is not the parent of the queue')
2436 2437 % rev.first()
2437 2438 )
2438 2439 base = repo.changelog.rev(self.applied[0].node)
2439 2440 lastparent = repo.changelog.parentrevs(base)[0]
2440 2441 else:
2441 2442 if heads != [repo.changelog.node(rev.first())]:
2442 2443 raise error.Abort(
2443 2444 _(b'revision %d has unmanaged children') % rev.first()
2444 2445 )
2445 2446 lastparent = None
2446 2447
2447 2448 diffopts = self.diffopts({b'git': git})
2448 2449 with repo.transaction(b'qimport') as tr:
2449 2450 for r in rev:
2450 2451 if not repo[r].mutable():
2451 2452 raise error.Abort(
2452 2453 _(b'revision %d is not mutable') % r,
2453 2454 hint=_(b"see 'hg help phases' " b'for details'),
2454 2455 )
2455 2456 p1, p2 = repo.changelog.parentrevs(r)
2456 2457 n = repo.changelog.node(r)
2457 2458 if p2 != nullrev:
2458 2459 raise error.Abort(
2459 2460 _(b'cannot import merge revision %d') % r
2460 2461 )
2461 2462 if lastparent and lastparent != r:
2462 2463 raise error.Abort(
2463 2464 _(b'revision %d is not the parent of %d')
2464 2465 % (r, lastparent)
2465 2466 )
2466 2467 lastparent = p1
2467 2468
2468 2469 if not patchname:
2469 2470 patchname = self.makepatchname(
2470 2471 repo[r].description().split(b'\n', 1)[0],
2471 2472 b'%d.diff' % r,
2472 2473 )
2473 2474 checkseries(patchname)
2474 2475 self.checkpatchname(patchname, force)
2475 2476 self.fullseries.insert(0, patchname)
2476 2477
2477 2478 with self.opener(patchname, b"w") as fp:
2478 2479 cmdutil.exportfile(repo, [n], fp, opts=diffopts)
2479 2480
2480 2481 se = statusentry(n, patchname)
2481 2482 self.applied.insert(0, se)
2482 2483
2483 2484 self.added.append(patchname)
2484 2485 imported.append(patchname)
2485 2486 patchname = None
2486 2487 if rev and repo.ui.configbool(b'mq', b'secret'):
2487 2488 # if we added anything with --rev, move the secret root
2488 2489 phases.retractboundary(repo, tr, phases.secret, [n])
2489 2490 self.parseseries()
2490 2491 self.applieddirty = True
2491 2492 self.seriesdirty = True
2492 2493
2493 2494 for i, filename in enumerate(files):
2494 2495 if existing:
2495 2496 if filename == b'-':
2496 2497 raise error.Abort(
2497 2498 _(b'-e is incompatible with import from -')
2498 2499 )
2499 2500 filename = normname(filename)
2500 2501 self.checkreservedname(filename)
2501 2502 if urlutil.url(filename).islocal():
2502 2503 originpath = self.join(filename)
2503 2504 if not os.path.isfile(originpath):
2504 2505 raise error.Abort(
2505 2506 _(b"patch %s does not exist") % filename
2506 2507 )
2507 2508
2508 2509 if patchname:
2509 2510 self.checkpatchname(patchname, force)
2510 2511
2511 2512 self.ui.write(
2512 2513 _(b'renaming %s to %s\n') % (filename, patchname)
2513 2514 )
2514 2515 util.rename(originpath, self.join(patchname))
2515 2516 else:
2516 2517 patchname = filename
2517 2518
2518 2519 else:
2519 2520 if filename == b'-' and not patchname:
2520 2521 raise error.Abort(
2521 2522 _(b'need --name to import a patch from -')
2522 2523 )
2523 2524 elif not patchname:
2524 2525 patchname = normname(
2525 2526 os.path.basename(filename.rstrip(b'/'))
2526 2527 )
2527 2528 self.checkpatchname(patchname, force)
2528 2529 try:
2529 2530 if filename == b'-':
2530 2531 text = self.ui.fin.read()
2531 2532 else:
2532 2533 fp = hg.openpath(self.ui, filename)
2533 2534 text = fp.read()
2534 2535 fp.close()
2535 2536 except (OSError, IOError):
2536 2537 raise error.Abort(_(b"unable to read file %s") % filename)
2537 2538 patchf = self.opener(patchname, b"w")
2538 2539 patchf.write(text)
2539 2540 patchf.close()
2540 2541 if not force:
2541 2542 checkseries(patchname)
2542 2543 if patchname not in self.series:
2543 2544 index = self.fullseriesend() + i
2544 2545 self.fullseries[index:index] = [patchname]
2545 2546 self.parseseries()
2546 2547 self.seriesdirty = True
2547 2548 self.ui.warn(_(b"adding %s to series file\n") % patchname)
2548 2549 self.added.append(patchname)
2549 2550 imported.append(patchname)
2550 2551 patchname = None
2551 2552
2552 2553 self.removeundo(repo)
2553 2554 return imported
2554 2555
2555 2556
2556 2557 def fixkeepchangesopts(ui, opts):
2557 2558 if (
2558 2559 not ui.configbool(b'mq', b'keepchanges')
2559 2560 or opts.get(b'force')
2560 2561 or opts.get(b'exact')
2561 2562 ):
2562 2563 return opts
2563 2564 opts = dict(opts)
2564 2565 opts[b'keep_changes'] = True
2565 2566 return opts
2566 2567
2567 2568
2568 2569 @command(
2569 2570 b"qdelete|qremove|qrm",
2570 2571 [
2571 2572 (b'k', b'keep', None, _(b'keep patch file')),
2572 2573 (
2573 2574 b'r',
2574 2575 b'rev',
2575 2576 [],
2576 2577 _(b'stop managing a revision (DEPRECATED)'),
2577 2578 _(b'REV'),
2578 2579 ),
2579 2580 ],
2580 2581 _(b'hg qdelete [-k] [PATCH]...'),
2581 2582 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2582 2583 )
2583 2584 def delete(ui, repo, *patches, **opts):
2584 2585 """remove patches from queue
2585 2586
2586 2587 The patches must not be applied, and at least one patch is required. Exact
2587 2588 patch identifiers must be given. With -k/--keep, the patch files are
2588 2589 preserved in the patch directory.
2589 2590
2590 2591 To stop managing a patch and move it into permanent history,
2591 2592 use the :hg:`qfinish` command."""
2592 2593 q = repo.mq
2593 2594 q.delete(repo, patches, pycompat.byteskwargs(opts))
2594 2595 q.savedirty()
2595 2596 return 0
2596 2597
2597 2598
2598 2599 @command(
2599 2600 b"qapplied",
2600 2601 [(b'1', b'last', None, _(b'show only the preceding applied patch'))]
2601 2602 + seriesopts,
2602 2603 _(b'hg qapplied [-1] [-s] [PATCH]'),
2603 2604 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2604 2605 )
2605 2606 def applied(ui, repo, patch=None, **opts):
2606 2607 """print the patches already applied
2607 2608
2608 2609 Returns 0 on success."""
2609 2610
2610 2611 q = repo.mq
2611 2612 opts = pycompat.byteskwargs(opts)
2612 2613
2613 2614 if patch:
2614 2615 if patch not in q.series:
2615 2616 raise error.Abort(_(b"patch %s is not in series file") % patch)
2616 2617 end = q.series.index(patch) + 1
2617 2618 else:
2618 2619 end = q.seriesend(True)
2619 2620
2620 2621 if opts.get(b'last') and not end:
2621 2622 ui.write(_(b"no patches applied\n"))
2622 2623 return 1
2623 2624 elif opts.get(b'last') and end == 1:
2624 2625 ui.write(_(b"only one patch applied\n"))
2625 2626 return 1
2626 2627 elif opts.get(b'last'):
2627 2628 start = end - 2
2628 2629 end = 1
2629 2630 else:
2630 2631 start = 0
2631 2632
2632 2633 q.qseries(
2633 2634 repo, length=end, start=start, status=b'A', summary=opts.get(b'summary')
2634 2635 )
2635 2636
2636 2637
2637 2638 @command(
2638 2639 b"qunapplied",
2639 2640 [(b'1', b'first', None, _(b'show only the first patch'))] + seriesopts,
2640 2641 _(b'hg qunapplied [-1] [-s] [PATCH]'),
2641 2642 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2642 2643 )
2643 2644 def unapplied(ui, repo, patch=None, **opts):
2644 2645 """print the patches not yet applied
2645 2646
2646 2647 Returns 0 on success."""
2647 2648
2648 2649 q = repo.mq
2649 2650 opts = pycompat.byteskwargs(opts)
2650 2651 if patch:
2651 2652 if patch not in q.series:
2652 2653 raise error.Abort(_(b"patch %s is not in series file") % patch)
2653 2654 start = q.series.index(patch) + 1
2654 2655 else:
2655 2656 start = q.seriesend(True)
2656 2657
2657 2658 if start == len(q.series) and opts.get(b'first'):
2658 2659 ui.write(_(b"all patches applied\n"))
2659 2660 return 1
2660 2661
2661 2662 if opts.get(b'first'):
2662 2663 length = 1
2663 2664 else:
2664 2665 length = None
2665 2666 q.qseries(
2666 2667 repo,
2667 2668 start=start,
2668 2669 length=length,
2669 2670 status=b'U',
2670 2671 summary=opts.get(b'summary'),
2671 2672 )
2672 2673
2673 2674
2674 2675 @command(
2675 2676 b"qimport",
2676 2677 [
2677 2678 (b'e', b'existing', None, _(b'import file in patch directory')),
2678 2679 (b'n', b'name', b'', _(b'name of patch file'), _(b'NAME')),
2679 2680 (b'f', b'force', None, _(b'overwrite existing files')),
2680 2681 (
2681 2682 b'r',
2682 2683 b'rev',
2683 2684 [],
2684 2685 _(b'place existing revisions under mq control'),
2685 2686 _(b'REV'),
2686 2687 ),
2687 2688 (b'g', b'git', None, _(b'use git extended diff format')),
2688 2689 (b'P', b'push', None, _(b'qpush after importing')),
2689 2690 ],
2690 2691 _(b'hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'),
2691 2692 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2692 2693 )
2693 2694 def qimport(ui, repo, *filename, **opts):
2694 2695 """import a patch or existing changeset
2695 2696
2696 2697 The patch is inserted into the series after the last applied
2697 2698 patch. If no patches have been applied, qimport prepends the patch
2698 2699 to the series.
2699 2700
2700 2701 The patch will have the same name as its source file unless you
2701 2702 give it a new one with -n/--name.
2702 2703
2703 2704 You can register an existing patch inside the patch directory with
2704 2705 the -e/--existing flag.
2705 2706
2706 2707 With -f/--force, an existing patch of the same name will be
2707 2708 overwritten.
2708 2709
2709 2710 An existing changeset may be placed under mq control with -r/--rev
2710 2711 (e.g. qimport --rev . -n patch will place the current revision
2711 2712 under mq control). With -g/--git, patches imported with --rev will
2712 2713 use the git diff format. See the diffs help topic for information
2713 2714 on why this is important for preserving rename/copy information
2714 2715 and permission changes. Use :hg:`qfinish` to remove changesets
2715 2716 from mq control.
2716 2717
2717 2718 To import a patch from standard input, pass - as the patch file.
2718 2719 When importing from standard input, a patch name must be specified
2719 2720 using the --name flag.
2720 2721
2721 2722 To import an existing patch while renaming it::
2722 2723
2723 2724 hg qimport -e existing-patch -n new-name
2724 2725
2725 2726 Returns 0 if import succeeded.
2726 2727 """
2727 2728 opts = pycompat.byteskwargs(opts)
2728 2729 with repo.lock(): # cause this may move phase
2729 2730 q = repo.mq
2730 2731 try:
2731 2732 imported = q.qimport(
2732 2733 repo,
2733 2734 filename,
2734 2735 patchname=opts.get(b'name'),
2735 2736 existing=opts.get(b'existing'),
2736 2737 force=opts.get(b'force'),
2737 2738 rev=opts.get(b'rev'),
2738 2739 git=opts.get(b'git'),
2739 2740 )
2740 2741 finally:
2741 2742 q.savedirty()
2742 2743
2743 2744 if imported and opts.get(b'push') and not opts.get(b'rev'):
2744 2745 return q.push(repo, imported[-1])
2745 2746 return 0
2746 2747
2747 2748
2748 2749 def qinit(ui, repo, create):
2749 2750 """initialize a new queue repository
2750 2751
2751 2752 This command also creates a series file for ordering patches, and
2752 2753 an mq-specific .hgignore file in the queue repository, to exclude
2753 2754 the status and guards files (these contain mostly transient state).
2754 2755
2755 2756 Returns 0 if initialization succeeded."""
2756 2757 q = repo.mq
2757 2758 r = q.init(repo, create)
2758 2759 q.savedirty()
2759 2760 if r:
2760 2761 with r.wlock(), r.dirstate.changing_files(r):
2761 2762 if not os.path.exists(r.wjoin(b'.hgignore')):
2762 2763 fp = r.wvfs(b'.hgignore', b'w')
2763 2764 fp.write(b'^\\.hg\n')
2764 2765 fp.write(b'^\\.mq\n')
2765 2766 fp.write(b'syntax: glob\n')
2766 2767 fp.write(b'status\n')
2767 2768 fp.write(b'guards\n')
2768 2769 fp.close()
2769 2770 if not os.path.exists(r.wjoin(b'series')):
2770 2771 r.wvfs(b'series', b'w').close()
2771 2772 r[None].add([b'.hgignore', b'series'])
2772 2773 commands.add(ui, r)
2773 2774 return 0
2774 2775
2775 2776
2776 2777 @command(
2777 2778 b"qinit",
2778 2779 [(b'c', b'create-repo', None, _(b'create queue repository'))],
2779 2780 _(b'hg qinit [-c]'),
2780 2781 helpcategory=command.CATEGORY_REPO_CREATION,
2781 2782 helpbasic=True,
2782 2783 )
2783 2784 def init(ui, repo, **opts):
2784 2785 """init a new queue repository (DEPRECATED)
2785 2786
2786 2787 The queue repository is unversioned by default. If
2787 2788 -c/--create-repo is specified, qinit will create a separate nested
2788 2789 repository for patches (qinit -c may also be run later to convert
2789 2790 an unversioned patch repository into a versioned one). You can use
2790 2791 qcommit to commit changes to this queue repository.
2791 2792
2792 2793 This command is deprecated. Without -c, it's implied by other relevant
2793 2794 commands. With -c, use :hg:`init --mq` instead."""
2794 2795 return qinit(ui, repo, create=opts.get('create_repo'))
2795 2796
2796 2797
2797 2798 @command(
2798 2799 b"qclone",
2799 2800 [
2800 2801 (b'', b'pull', None, _(b'use pull protocol to copy metadata')),
2801 2802 (
2802 2803 b'U',
2803 2804 b'noupdate',
2804 2805 None,
2805 2806 _(b'do not update the new working directories'),
2806 2807 ),
2807 2808 (
2808 2809 b'',
2809 2810 b'uncompressed',
2810 2811 None,
2811 2812 _(b'use uncompressed transfer (fast over LAN)'),
2812 2813 ),
2813 2814 (
2814 2815 b'p',
2815 2816 b'patches',
2816 2817 b'',
2817 2818 _(b'location of source patch repository'),
2818 2819 _(b'REPO'),
2819 2820 ),
2820 2821 ]
2821 2822 + cmdutil.remoteopts,
2822 2823 _(b'hg qclone [OPTION]... SOURCE [DEST]'),
2823 2824 helpcategory=command.CATEGORY_REPO_CREATION,
2824 2825 norepo=True,
2825 2826 )
2826 2827 def clone(ui, source, dest=None, **opts):
2827 2828 """clone main and patch repository at same time
2828 2829
2829 2830 If source is local, destination will have no patches applied. If
2830 2831 source is remote, this command can not check if patches are
2831 2832 applied in source, so cannot guarantee that patches are not
2832 2833 applied in destination. If you clone remote repository, be sure
2833 2834 before that it has no patches applied.
2834 2835
2835 2836 Source patch repository is looked for in <src>/.hg/patches by
2836 2837 default. Use -p <url> to change.
2837 2838
2838 2839 The patch directory must be a nested Mercurial repository, as
2839 2840 would be created by :hg:`init --mq`.
2840 2841
2841 2842 Return 0 on success.
2842 2843 """
2843 2844 opts = pycompat.byteskwargs(opts)
2844 2845
2845 2846 def patchdir(repo):
2846 2847 """compute a patch repo url from a repo object"""
2847 2848 url = repo.url()
2848 2849 if url.endswith(b'/'):
2849 2850 url = url[:-1]
2850 2851 return url + b'/.hg/patches'
2851 2852
2852 2853 # main repo (destination and sources)
2853 2854 if dest is None:
2854 2855 dest = hg.defaultdest(source)
2855 2856 source_path = urlutil.get_clone_path_obj(ui, source)
2856 2857 sr = hg.peer(ui, opts, source_path)
2857 2858
2858 2859 # patches repo (source only)
2859 2860 if opts.get(b'patches'):
2860 2861 patches_path = urlutil.get_clone_path_obj(ui, opts.get(b'patches'))
2861 2862 else:
2862 2863 # XXX path: we should turn this into a path object
2863 2864 patches_path = patchdir(sr)
2864 2865 try:
2865 2866 hg.peer(ui, opts, patches_path)
2866 2867 except error.RepoError:
2867 2868 raise error.Abort(
2868 2869 _(b'versioned patch repository not found (see init --mq)')
2869 2870 )
2870 2871 qbase, destrev = None, None
2871 2872 if sr.local():
2872 2873 repo = sr.local()
2873 2874 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2874 2875 qbase = repo.mq.applied[0].node
2875 2876 if not hg.islocal(dest):
2876 2877 heads = set(repo.heads())
2877 2878 destrev = list(heads.difference(repo.heads(qbase)))
2878 2879 destrev.append(repo.changelog.parents(qbase)[0])
2879 2880 elif sr.capable(b'lookup'):
2880 2881 try:
2881 2882 qbase = sr.lookup(b'qbase')
2882 2883 except error.RepoError:
2883 2884 pass
2884 2885
2885 2886 ui.note(_(b'cloning main repository\n'))
2886 2887 sr, dr = hg.clone(
2887 2888 ui,
2888 2889 opts,
2889 2890 sr.url(),
2890 2891 dest,
2891 2892 pull=opts.get(b'pull'),
2892 2893 revs=destrev,
2893 2894 update=False,
2894 2895 stream=opts.get(b'uncompressed'),
2895 2896 )
2896 2897
2897 2898 ui.note(_(b'cloning patch repository\n'))
2898 2899 hg.clone(
2899 2900 ui,
2900 2901 opts,
2901 2902 opts.get(b'patches') or patchdir(sr),
2902 2903 patchdir(dr),
2903 2904 pull=opts.get(b'pull'),
2904 2905 update=not opts.get(b'noupdate'),
2905 2906 stream=opts.get(b'uncompressed'),
2906 2907 )
2907 2908
2908 2909 if dr.local():
2909 2910 repo = dr.local()
2910 2911 if qbase:
2911 2912 ui.note(
2912 2913 _(
2913 2914 b'stripping applied patches from destination '
2914 2915 b'repository\n'
2915 2916 )
2916 2917 )
2917 2918 strip(ui, repo, [qbase], update=False, backup=None)
2918 2919 if not opts.get(b'noupdate'):
2919 2920 ui.note(_(b'updating destination repository\n'))
2920 2921 hg.update(repo, repo.changelog.tip())
2921 2922
2922 2923
2923 2924 @command(
2924 2925 b"qcommit|qci",
2925 2926 commands.table[b"commit|ci"][1],
2926 2927 _(b'hg qcommit [OPTION]... [FILE]...'),
2927 2928 helpcategory=command.CATEGORY_COMMITTING,
2928 2929 inferrepo=True,
2929 2930 )
2930 2931 def commit(ui, repo, *pats, **opts):
2931 2932 """commit changes in the queue repository (DEPRECATED)
2932 2933
2933 2934 This command is deprecated; use :hg:`commit --mq` instead."""
2934 2935 q = repo.mq
2935 2936 r = q.qrepo()
2936 2937 if not r:
2937 2938 raise error.Abort(b'no queue repository')
2938 2939 commands.commit(r.ui, r, *pats, **opts)
2939 2940
2940 2941
2941 2942 @command(
2942 2943 b"qseries",
2943 2944 [
2944 2945 (b'm', b'missing', None, _(b'print patches not in series')),
2945 2946 ]
2946 2947 + seriesopts,
2947 2948 _(b'hg qseries [-ms]'),
2948 2949 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2949 2950 )
2950 2951 def series(ui, repo, **opts):
2951 2952 """print the entire series file
2952 2953
2953 2954 Returns 0 on success."""
2954 2955 repo.mq.qseries(
2955 2956 repo, missing=opts.get('missing'), summary=opts.get('summary')
2956 2957 )
2957 2958 return 0
2958 2959
2959 2960
2960 2961 @command(
2961 2962 b"qtop",
2962 2963 seriesopts,
2963 2964 _(b'hg qtop [-s]'),
2964 2965 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2965 2966 )
2966 2967 def top(ui, repo, **opts):
2967 2968 """print the name of the current patch
2968 2969
2969 2970 Returns 0 on success."""
2970 2971 q = repo.mq
2971 2972 if q.applied:
2972 2973 t = q.seriesend(True)
2973 2974 else:
2974 2975 t = 0
2975 2976
2976 2977 if t:
2977 2978 q.qseries(
2978 2979 repo,
2979 2980 start=t - 1,
2980 2981 length=1,
2981 2982 status=b'A',
2982 2983 summary=opts.get('summary'),
2983 2984 )
2984 2985 else:
2985 2986 ui.write(_(b"no patches applied\n"))
2986 2987 return 1
2987 2988
2988 2989
2989 2990 @command(
2990 2991 b"qnext",
2991 2992 seriesopts,
2992 2993 _(b'hg qnext [-s]'),
2993 2994 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2994 2995 )
2995 2996 def next(ui, repo, **opts):
2996 2997 """print the name of the next pushable patch
2997 2998
2998 2999 Returns 0 on success."""
2999 3000 q = repo.mq
3000 3001 end = q.seriesend()
3001 3002 if end == len(q.series):
3002 3003 ui.write(_(b"all patches applied\n"))
3003 3004 return 1
3004 3005 q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
3005 3006
3006 3007
3007 3008 @command(
3008 3009 b"qprev",
3009 3010 seriesopts,
3010 3011 _(b'hg qprev [-s]'),
3011 3012 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3012 3013 )
3013 3014 def prev(ui, repo, **opts):
3014 3015 """print the name of the preceding applied patch
3015 3016
3016 3017 Returns 0 on success."""
3017 3018 q = repo.mq
3018 3019 l = len(q.applied)
3019 3020 if l == 1:
3020 3021 ui.write(_(b"only one patch applied\n"))
3021 3022 return 1
3022 3023 if not l:
3023 3024 ui.write(_(b"no patches applied\n"))
3024 3025 return 1
3025 3026 idx = q.series.index(q.applied[-2].name)
3026 3027 q.qseries(
3027 3028 repo, start=idx, length=1, status=b'A', summary=opts.get('summary')
3028 3029 )
3029 3030
3030 3031
3031 3032 def setupheaderopts(ui, opts):
3032 3033 if not opts.get(b'user') and opts.get(b'currentuser'):
3033 3034 opts[b'user'] = ui.username()
3034 3035 if not opts.get(b'date') and opts.get(b'currentdate'):
3035 3036 opts[b'date'] = b"%d %d" % dateutil.makedate()
3036 3037
3037 3038
3038 3039 @command(
3039 3040 b"qnew",
3040 3041 [
3041 3042 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3042 3043 (b'f', b'force', None, _(b'import uncommitted changes (DEPRECATED)')),
3043 3044 (b'g', b'git', None, _(b'use git extended diff format')),
3044 3045 (b'U', b'currentuser', None, _(b'add "From: <current user>" to patch')),
3045 3046 (b'u', b'user', b'', _(b'add "From: <USER>" to patch'), _(b'USER')),
3046 3047 (b'D', b'currentdate', None, _(b'add "Date: <current date>" to patch')),
3047 3048 (b'd', b'date', b'', _(b'add "Date: <DATE>" to patch'), _(b'DATE')),
3048 3049 ]
3049 3050 + cmdutil.walkopts
3050 3051 + cmdutil.commitopts,
3051 3052 _(b'hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'),
3052 3053 helpcategory=command.CATEGORY_COMMITTING,
3053 3054 helpbasic=True,
3054 3055 inferrepo=True,
3055 3056 )
3056 3057 def new(ui, repo, patch, *args, **opts):
3057 3058 """create a new patch
3058 3059
3059 3060 qnew creates a new patch on top of the currently-applied patch (if
3060 3061 any). The patch will be initialized with any outstanding changes
3061 3062 in the working directory. You may also use -I/--include,
3062 3063 -X/--exclude, and/or a list of files after the patch name to add
3063 3064 only changes to matching files to the new patch, leaving the rest
3064 3065 as uncommitted modifications.
3065 3066
3066 3067 -u/--user and -d/--date can be used to set the (given) user and
3067 3068 date, respectively. -U/--currentuser and -D/--currentdate set user
3068 3069 to current user and date to current date.
3069 3070
3070 3071 -e/--edit, -m/--message or -l/--logfile set the patch header as
3071 3072 well as the commit message. If none is specified, the header is
3072 3073 empty and the commit message is '[mq]: PATCH'.
3073 3074
3074 3075 Use the -g/--git option to keep the patch in the git extended diff
3075 3076 format. Read the diffs help topic for more information on why this
3076 3077 is important for preserving permission changes and copy/rename
3077 3078 information.
3078 3079
3079 3080 Returns 0 on successful creation of a new patch.
3080 3081 """
3081 3082 opts = pycompat.byteskwargs(opts)
3082 3083 msg = cmdutil.logmessage(ui, opts)
3083 3084 q = repo.mq
3084 3085 opts[b'msg'] = msg
3085 3086 setupheaderopts(ui, opts)
3086 3087 q.new(repo, patch, *args, **pycompat.strkwargs(opts))
3087 3088 q.savedirty()
3088 3089 return 0
3089 3090
3090 3091
3091 3092 @command(
3092 3093 b"qrefresh",
3093 3094 [
3094 3095 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3095 3096 (b'g', b'git', None, _(b'use git extended diff format')),
3096 3097 (
3097 3098 b's',
3098 3099 b'short',
3099 3100 None,
3100 3101 _(b'refresh only files already in the patch and specified files'),
3101 3102 ),
3102 3103 (
3103 3104 b'U',
3104 3105 b'currentuser',
3105 3106 None,
3106 3107 _(b'add/update author field in patch with current user'),
3107 3108 ),
3108 3109 (
3109 3110 b'u',
3110 3111 b'user',
3111 3112 b'',
3112 3113 _(b'add/update author field in patch with given user'),
3113 3114 _(b'USER'),
3114 3115 ),
3115 3116 (
3116 3117 b'D',
3117 3118 b'currentdate',
3118 3119 None,
3119 3120 _(b'add/update date field in patch with current date'),
3120 3121 ),
3121 3122 (
3122 3123 b'd',
3123 3124 b'date',
3124 3125 b'',
3125 3126 _(b'add/update date field in patch with given date'),
3126 3127 _(b'DATE'),
3127 3128 ),
3128 3129 ]
3129 3130 + cmdutil.walkopts
3130 3131 + cmdutil.commitopts,
3131 3132 _(b'hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'),
3132 3133 helpcategory=command.CATEGORY_COMMITTING,
3133 3134 helpbasic=True,
3134 3135 inferrepo=True,
3135 3136 )
3136 3137 def refresh(ui, repo, *pats, **opts):
3137 3138 """update the current patch
3138 3139
3139 3140 If any file patterns are provided, the refreshed patch will
3140 3141 contain only the modifications that match those patterns; the
3141 3142 remaining modifications will remain in the working directory.
3142 3143
3143 3144 If -s/--short is specified, files currently included in the patch
3144 3145 will be refreshed just like matched files and remain in the patch.
3145 3146
3146 3147 If -e/--edit is specified, Mercurial will start your configured editor for
3147 3148 you to enter a message. In case qrefresh fails, you will find a backup of
3148 3149 your message in ``.hg/last-message.txt``.
3149 3150
3150 3151 hg add/remove/copy/rename work as usual, though you might want to
3151 3152 use git-style patches (-g/--git or [diff] git=1) to track copies
3152 3153 and renames. See the diffs help topic for more information on the
3153 3154 git diff format.
3154 3155
3155 3156 Returns 0 on success.
3156 3157 """
3157 3158 opts = pycompat.byteskwargs(opts)
3158 3159 q = repo.mq
3159 3160 message = cmdutil.logmessage(ui, opts)
3160 3161 setupheaderopts(ui, opts)
3161 3162 with repo.wlock():
3162 3163 ret = q.refresh(repo, pats, msg=message, **pycompat.strkwargs(opts))
3163 3164 q.savedirty()
3164 3165 return ret
3165 3166
3166 3167
3167 3168 @command(
3168 3169 b"qdiff",
3169 3170 cmdutil.diffopts + cmdutil.diffopts2 + cmdutil.walkopts,
3170 3171 _(b'hg qdiff [OPTION]... [FILE]...'),
3171 3172 helpcategory=command.CATEGORY_FILE_CONTENTS,
3172 3173 helpbasic=True,
3173 3174 inferrepo=True,
3174 3175 )
3175 3176 def diff(ui, repo, *pats, **opts):
3176 3177 """diff of the current patch and subsequent modifications
3177 3178
3178 3179 Shows a diff which includes the current patch as well as any
3179 3180 changes which have been made in the working directory since the
3180 3181 last refresh (thus showing what the current patch would become
3181 3182 after a qrefresh).
3182 3183
3183 3184 Use :hg:`diff` if you only want to see the changes made since the
3184 3185 last qrefresh, or :hg:`export qtip` if you want to see changes
3185 3186 made by the current patch without including changes made since the
3186 3187 qrefresh.
3187 3188
3188 3189 Returns 0 on success.
3189 3190 """
3190 3191 ui.pager(b'qdiff')
3191 3192 repo.mq.diff(repo, pats, pycompat.byteskwargs(opts))
3192 3193 return 0
3193 3194
3194 3195
3195 3196 @command(
3196 3197 b'qfold',
3197 3198 [
3198 3199 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3199 3200 (b'k', b'keep', None, _(b'keep folded patch files')),
3200 3201 ]
3201 3202 + cmdutil.commitopts,
3202 3203 _(b'hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'),
3203 3204 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
3204 3205 )
3205 3206 def fold(ui, repo, *files, **opts):
3206 3207 """fold the named patches into the current patch
3207 3208
3208 3209 Patches must not yet be applied. Each patch will be successively
3209 3210 applied to the current patch in the order given. If all the
3210 3211 patches apply successfully, the current patch will be refreshed
3211 3212 with the new cumulative patch, and the folded patches will be
3212 3213 deleted. With -k/--keep, the folded patch files will not be
3213 3214 removed afterwards.
3214 3215
3215 3216 The header for each folded patch will be concatenated with the
3216 3217 current patch header, separated by a line of ``* * *``.
3217 3218
3218 3219 Returns 0 on success."""
3219 3220 opts = pycompat.byteskwargs(opts)
3220 3221 q = repo.mq
3221 3222 if not files:
3222 3223 raise error.Abort(_(b'qfold requires at least one patch name'))
3223 3224 if not q.checktoppatch(repo)[0]:
3224 3225 raise error.Abort(_(b'no patches applied'))
3225 3226
3226 3227 with repo.wlock():
3227 3228 q.checklocalchanges(repo)
3228 3229
3229 3230 message = cmdutil.logmessage(ui, opts)
3230 3231
3231 3232 parent = q.lookup(b'qtip')
3232 3233 patches = []
3233 3234 messages = []
3234 3235 for f in files:
3235 3236 p = q.lookup(f)
3236 3237 if p in patches or p == parent:
3237 3238 ui.warn(_(b'skipping already folded patch %s\n') % p)
3238 3239 if q.isapplied(p):
3239 3240 raise error.Abort(
3240 3241 _(b'qfold cannot fold already applied patch %s') % p
3241 3242 )
3242 3243 patches.append(p)
3243 3244
3244 3245 for p in patches:
3245 3246 if not message:
3246 3247 ph = patchheader(q.join(p), q.plainmode)
3247 3248 if ph.message:
3248 3249 messages.append(ph.message)
3249 3250 pf = q.join(p)
3250 3251 (patchsuccess, files, fuzz) = q.patch(repo, pf)
3251 3252 if not patchsuccess:
3252 3253 raise error.Abort(_(b'error folding patch %s') % p)
3253 3254
3254 3255 if not message:
3255 3256 ph = patchheader(q.join(parent), q.plainmode)
3256 3257 message = ph.message
3257 3258 for msg in messages:
3258 3259 if msg:
3259 3260 if message:
3260 3261 message.append(b'* * *')
3261 3262 message.extend(msg)
3262 3263 message = b'\n'.join(message)
3263 3264
3264 3265 diffopts = q.patchopts(q.diffopts(), *patches)
3265 3266 q.refresh(
3266 3267 repo,
3267 3268 msg=message,
3268 3269 git=diffopts.git,
3269 3270 edit=opts.get(b'edit'),
3270 3271 editform=b'mq.qfold',
3271 3272 )
3272 3273 q.delete(repo, patches, opts)
3273 3274 q.savedirty()
3274 3275
3275 3276
3276 3277 @command(
3277 3278 b"qgoto",
3278 3279 [
3279 3280 (
3280 3281 b'',
3281 3282 b'keep-changes',
3282 3283 None,
3283 3284 _(b'tolerate non-conflicting local changes'),
3284 3285 ),
3285 3286 (b'f', b'force', None, _(b'overwrite any local changes')),
3286 3287 (b'', b'no-backup', None, _(b'do not save backup copies of files')),
3287 3288 ],
3288 3289 _(b'hg qgoto [OPTION]... PATCH'),
3289 3290 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3290 3291 )
3291 3292 def goto(ui, repo, patch, **opts):
3292 3293 """push or pop patches until named patch is at top of stack
3293 3294
3294 3295 Returns 0 on success."""
3295 3296 opts = pycompat.byteskwargs(opts)
3296 3297 opts = fixkeepchangesopts(ui, opts)
3297 3298 q = repo.mq
3298 3299 patch = q.lookup(patch)
3299 3300 nobackup = opts.get(b'no_backup')
3300 3301 keepchanges = opts.get(b'keep_changes')
3301 3302 if q.isapplied(patch):
3302 3303 ret = q.pop(
3303 3304 repo,
3304 3305 patch,
3305 3306 force=opts.get(b'force'),
3306 3307 nobackup=nobackup,
3307 3308 keepchanges=keepchanges,
3308 3309 )
3309 3310 else:
3310 3311 ret = q.push(
3311 3312 repo,
3312 3313 patch,
3313 3314 force=opts.get(b'force'),
3314 3315 nobackup=nobackup,
3315 3316 keepchanges=keepchanges,
3316 3317 )
3317 3318 q.savedirty()
3318 3319 return ret
3319 3320
3320 3321
3321 3322 @command(
3322 3323 b"qguard",
3323 3324 [
3324 3325 (b'l', b'list', None, _(b'list all patches and guards')),
3325 3326 (b'n', b'none', None, _(b'drop all guards')),
3326 3327 ],
3327 3328 _(b'hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'),
3328 3329 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3329 3330 )
3330 3331 def guard(ui, repo, *args, **opts):
3331 3332 """set or print guards for a patch
3332 3333
3333 3334 Guards control whether a patch can be pushed. A patch with no
3334 3335 guards is always pushed. A patch with a positive guard ("+foo") is
3335 3336 pushed only if the :hg:`qselect` command has activated it. A patch with
3336 3337 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
3337 3338 has activated it.
3338 3339
3339 3340 With no arguments, print the currently active guards.
3340 3341 With arguments, set guards for the named patch.
3341 3342
3342 3343 .. note::
3343 3344
3344 3345 Specifying negative guards now requires '--'.
3345 3346
3346 3347 To set guards on another patch::
3347 3348
3348 3349 hg qguard other.patch -- +2.6.17 -stable
3349 3350
3350 3351 Returns 0 on success.
3351 3352 """
3352 3353
3353 3354 def status(idx):
3354 3355 guards = q.seriesguards[idx] or [b'unguarded']
3355 3356 if q.series[idx] in applied:
3356 3357 state = b'applied'
3357 3358 elif q.pushable(idx)[0]:
3358 3359 state = b'unapplied'
3359 3360 else:
3360 3361 state = b'guarded'
3361 3362 label = b'qguard.patch qguard.%s qseries.%s' % (state, state)
3362 3363 ui.write(b'%s: ' % ui.label(q.series[idx], label))
3363 3364
3364 3365 for i, guard in enumerate(guards):
3365 3366 if guard.startswith(b'+'):
3366 3367 ui.write(guard, label=b'qguard.positive')
3367 3368 elif guard.startswith(b'-'):
3368 3369 ui.write(guard, label=b'qguard.negative')
3369 3370 else:
3370 3371 ui.write(guard, label=b'qguard.unguarded')
3371 3372 if i != len(guards) - 1:
3372 3373 ui.write(b' ')
3373 3374 ui.write(b'\n')
3374 3375
3375 3376 q = repo.mq
3376 3377 applied = {p.name for p in q.applied}
3377 3378 patch = None
3378 3379 args = list(args)
3379 3380 if opts.get('list'):
3380 3381 if args or opts.get('none'):
3381 3382 raise error.Abort(
3382 3383 _(b'cannot mix -l/--list with options or arguments')
3383 3384 )
3384 3385 for i in range(len(q.series)):
3385 3386 status(i)
3386 3387 return
3387 3388 if not args or args[0][0:1] in b'-+':
3388 3389 if not q.applied:
3389 3390 raise error.Abort(_(b'no patches applied'))
3390 3391 patch = q.applied[-1].name
3391 3392 if patch is None and args[0][0:1] not in b'-+':
3392 3393 patch = args.pop(0)
3393 3394 if patch is None:
3394 3395 raise error.Abort(_(b'no patch to work with'))
3395 3396 if args or opts.get('none'):
3396 3397 idx = q.findseries(patch)
3397 3398 if idx is None:
3398 3399 raise error.Abort(_(b'no patch named %s') % patch)
3399 3400 q.setguards(idx, args)
3400 3401 q.savedirty()
3401 3402 else:
3402 3403 status(q.series.index(q.lookup(patch)))
3403 3404
3404 3405
3405 3406 @command(
3406 3407 b"qheader",
3407 3408 [],
3408 3409 _(b'hg qheader [PATCH]'),
3409 3410 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3410 3411 )
3411 3412 def header(ui, repo, patch=None):
3412 3413 """print the header of the topmost or specified patch
3413 3414
3414 3415 Returns 0 on success."""
3415 3416 q = repo.mq
3416 3417
3417 3418 if patch:
3418 3419 patch = q.lookup(patch)
3419 3420 else:
3420 3421 if not q.applied:
3421 3422 ui.write(_(b'no patches applied\n'))
3422 3423 return 1
3423 3424 patch = q.lookup(b'qtip')
3424 3425 ph = patchheader(q.join(patch), q.plainmode)
3425 3426
3426 3427 ui.write(b'\n'.join(ph.message) + b'\n')
3427 3428
3428 3429
3429 3430 def lastsavename(path):
3430 3431 (directory, base) = os.path.split(path)
3431 3432 names = os.listdir(directory)
3432 3433 namere = re.compile(b"%s.([0-9]+)" % base)
3433 3434 maxindex = None
3434 3435 maxname = None
3435 3436 for f in names:
3436 3437 m = namere.match(f)
3437 3438 if m:
3438 3439 index = int(m.group(1))
3439 3440 if maxindex is None or index > maxindex:
3440 3441 maxindex = index
3441 3442 maxname = f
3442 3443 if maxname:
3443 3444 return (os.path.join(directory, maxname), maxindex)
3444 3445 return (None, None)
3445 3446
3446 3447
3447 3448 def savename(path):
3448 3449 (last, index) = lastsavename(path)
3449 3450 if last is None:
3450 3451 index = 0
3451 3452 newpath = path + b".%d" % (index + 1)
3452 3453 return newpath
3453 3454
3454 3455
3455 3456 @command(
3456 3457 b"qpush",
3457 3458 [
3458 3459 (
3459 3460 b'',
3460 3461 b'keep-changes',
3461 3462 None,
3462 3463 _(b'tolerate non-conflicting local changes'),
3463 3464 ),
3464 3465 (b'f', b'force', None, _(b'apply on top of local changes')),
3465 3466 (
3466 3467 b'e',
3467 3468 b'exact',
3468 3469 None,
3469 3470 _(b'apply the target patch to its recorded parent'),
3470 3471 ),
3471 3472 (b'l', b'list', None, _(b'list patch name in commit text')),
3472 3473 (b'a', b'all', None, _(b'apply all patches')),
3473 3474 (b'm', b'merge', None, _(b'merge from another queue (DEPRECATED)')),
3474 3475 (b'n', b'name', b'', _(b'merge queue name (DEPRECATED)'), _(b'NAME')),
3475 3476 (
3476 3477 b'',
3477 3478 b'move',
3478 3479 None,
3479 3480 _(b'reorder patch series and apply only the patch'),
3480 3481 ),
3481 3482 (b'', b'no-backup', None, _(b'do not save backup copies of files')),
3482 3483 ],
3483 3484 _(b'hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'),
3484 3485 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3485 3486 helpbasic=True,
3486 3487 )
3487 3488 def push(ui, repo, patch=None, **opts):
3488 3489 """push the next patch onto the stack
3489 3490
3490 3491 By default, abort if the working directory contains uncommitted
3491 3492 changes. With --keep-changes, abort only if the uncommitted files
3492 3493 overlap with patched files. With -f/--force, backup and patch over
3493 3494 uncommitted changes.
3494 3495
3495 3496 Return 0 on success.
3496 3497 """
3497 3498 q = repo.mq
3498 3499 mergeq = None
3499 3500
3500 3501 opts = pycompat.byteskwargs(opts)
3501 3502 opts = fixkeepchangesopts(ui, opts)
3502 3503 if opts.get(b'merge'):
3503 3504 if opts.get(b'name'):
3504 3505 newpath = repo.vfs.join(opts.get(b'name'))
3505 3506 else:
3506 3507 newpath, i = lastsavename(q.path)
3507 3508 if not newpath:
3508 3509 ui.warn(_(b"no saved queues found, please use -n\n"))
3509 3510 return 1
3510 3511 mergeq = queue(ui, repo.baseui, repo.path, newpath)
3511 3512 ui.warn(_(b"merging with queue at: %s\n") % mergeq.path)
3512 3513 ret = q.push(
3513 3514 repo,
3514 3515 patch,
3515 3516 force=opts.get(b'force'),
3516 3517 list=opts.get(b'list'),
3517 3518 mergeq=mergeq,
3518 3519 all=opts.get(b'all'),
3519 3520 move=opts.get(b'move'),
3520 3521 exact=opts.get(b'exact'),
3521 3522 nobackup=opts.get(b'no_backup'),
3522 3523 keepchanges=opts.get(b'keep_changes'),
3523 3524 )
3524 3525 return ret
3525 3526
3526 3527
3527 3528 @command(
3528 3529 b"qpop",
3529 3530 [
3530 3531 (b'a', b'all', None, _(b'pop all patches')),
3531 3532 (b'n', b'name', b'', _(b'queue name to pop (DEPRECATED)'), _(b'NAME')),
3532 3533 (
3533 3534 b'',
3534 3535 b'keep-changes',
3535 3536 None,
3536 3537 _(b'tolerate non-conflicting local changes'),
3537 3538 ),
3538 3539 (b'f', b'force', None, _(b'forget any local changes to patched files')),
3539 3540 (b'', b'no-backup', None, _(b'do not save backup copies of files')),
3540 3541 ],
3541 3542 _(b'hg qpop [-a] [-f] [PATCH | INDEX]'),
3542 3543 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3543 3544 helpbasic=True,
3544 3545 )
3545 3546 def pop(ui, repo, patch=None, **opts):
3546 3547 """pop the current patch off the stack
3547 3548
3548 3549 Without argument, pops off the top of the patch stack. If given a
3549 3550 patch name, keeps popping off patches until the named patch is at
3550 3551 the top of the stack.
3551 3552
3552 3553 By default, abort if the working directory contains uncommitted
3553 3554 changes. With --keep-changes, abort only if the uncommitted files
3554 3555 overlap with patched files. With -f/--force, backup and discard
3555 3556 changes made to such files.
3556 3557
3557 3558 Return 0 on success.
3558 3559 """
3559 3560 opts = pycompat.byteskwargs(opts)
3560 3561 opts = fixkeepchangesopts(ui, opts)
3561 3562 localupdate = True
3562 3563 if opts.get(b'name'):
3563 3564 q = queue(ui, repo.baseui, repo.path, repo.vfs.join(opts.get(b'name')))
3564 3565 ui.warn(_(b'using patch queue: %s\n') % q.path)
3565 3566 localupdate = False
3566 3567 else:
3567 3568 q = repo.mq
3568 3569 ret = q.pop(
3569 3570 repo,
3570 3571 patch,
3571 3572 force=opts.get(b'force'),
3572 3573 update=localupdate,
3573 3574 all=opts.get(b'all'),
3574 3575 nobackup=opts.get(b'no_backup'),
3575 3576 keepchanges=opts.get(b'keep_changes'),
3576 3577 )
3577 3578 q.savedirty()
3578 3579 return ret
3579 3580
3580 3581
3581 3582 @command(
3582 3583 b"qrename|qmv",
3583 3584 [],
3584 3585 _(b'hg qrename PATCH1 [PATCH2]'),
3585 3586 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3586 3587 )
3587 3588 def rename(ui, repo, patch, name=None, **opts):
3588 3589 """rename a patch
3589 3590
3590 3591 With one argument, renames the current patch to PATCH1.
3591 3592 With two arguments, renames PATCH1 to PATCH2.
3592 3593
3593 3594 Returns 0 on success."""
3594 3595 q = repo.mq
3595 3596 if not name:
3596 3597 name = patch
3597 3598 patch = None
3598 3599
3599 3600 if patch:
3600 3601 patch = q.lookup(patch)
3601 3602 else:
3602 3603 if not q.applied:
3603 3604 ui.write(_(b'no patches applied\n'))
3604 3605 return
3605 3606 patch = q.lookup(b'qtip')
3606 3607 absdest = q.join(name)
3607 3608 if os.path.isdir(absdest):
3608 3609 name = normname(os.path.join(name, os.path.basename(patch)))
3609 3610 absdest = q.join(name)
3610 3611 q.checkpatchname(name)
3611 3612
3612 3613 ui.note(_(b'renaming %s to %s\n') % (patch, name))
3613 3614 i = q.findseries(patch)
3614 3615 guards = q.guard_re.findall(q.fullseries[i])
3615 3616 q.fullseries[i] = name + b''.join([b' #' + g for g in guards])
3616 3617 q.parseseries()
3617 3618 q.seriesdirty = True
3618 3619
3619 3620 info = q.isapplied(patch)
3620 3621 if info:
3621 3622 q.applied[info[0]] = statusentry(info[1], name)
3622 3623 q.applieddirty = True
3623 3624
3624 3625 destdir = os.path.dirname(absdest)
3625 3626 if not os.path.isdir(destdir):
3626 3627 os.makedirs(destdir)
3627 3628 util.rename(q.join(patch), absdest)
3628 3629 r = q.qrepo()
3629 3630 if r and patch in r.dirstate:
3630 3631 with r.wlock(), r.dirstate.changing_files(r):
3631 3632 wctx = r[None]
3632 3633 if r.dirstate.get_entry(patch).added:
3633 3634 r.dirstate.set_untracked(patch)
3634 3635 r.dirstate.set_tracked(name)
3635 3636 else:
3636 3637 wctx.copy(patch, name)
3637 3638 wctx.forget([patch])
3638 3639
3639 3640 q.savedirty()
3640 3641
3641 3642
3642 3643 @command(
3643 3644 b"qrestore",
3644 3645 [
3645 3646 (b'd', b'delete', None, _(b'delete save entry')),
3646 3647 (b'u', b'update', None, _(b'update queue working directory')),
3647 3648 ],
3648 3649 _(b'hg qrestore [-d] [-u] REV'),
3649 3650 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3650 3651 )
3651 3652 def restore(ui, repo, rev, **opts):
3652 3653 """restore the queue state saved by a revision (DEPRECATED)
3653 3654
3654 3655 This command is deprecated, use :hg:`rebase` instead."""
3655 3656 rev = repo.lookup(rev)
3656 3657 q = repo.mq
3657 3658 q.restore(repo, rev, delete=opts.get('delete'), qupdate=opts.get('update'))
3658 3659 q.savedirty()
3659 3660 return 0
3660 3661
3661 3662
3662 3663 @command(
3663 3664 b"qsave",
3664 3665 [
3665 3666 (b'c', b'copy', None, _(b'copy patch directory')),
3666 3667 (b'n', b'name', b'', _(b'copy directory name'), _(b'NAME')),
3667 3668 (b'e', b'empty', None, _(b'clear queue status file')),
3668 3669 (b'f', b'force', None, _(b'force copy')),
3669 3670 ]
3670 3671 + cmdutil.commitopts,
3671 3672 _(b'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'),
3672 3673 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3673 3674 )
3674 3675 def save(ui, repo, **opts):
3675 3676 """save current queue state (DEPRECATED)
3676 3677
3677 3678 This command is deprecated, use :hg:`rebase` instead."""
3678 3679 q = repo.mq
3679 3680 opts = pycompat.byteskwargs(opts)
3680 3681 message = cmdutil.logmessage(ui, opts)
3681 3682 ret = q.save(repo, msg=message)
3682 3683 if ret:
3683 3684 return ret
3684 3685 q.savedirty() # save to .hg/patches before copying
3685 3686 if opts.get(b'copy'):
3686 3687 path = q.path
3687 3688 if opts.get(b'name'):
3688 3689 newpath = os.path.join(q.basepath, opts.get(b'name'))
3689 3690 if os.path.exists(newpath):
3690 3691 if not os.path.isdir(newpath):
3691 3692 raise error.Abort(
3692 3693 _(b'destination %s exists and is not a directory')
3693 3694 % newpath
3694 3695 )
3695 3696 if not opts.get(b'force'):
3696 3697 raise error.Abort(
3697 3698 _(b'destination %s exists, use -f to force') % newpath
3698 3699 )
3699 3700 else:
3700 3701 newpath = savename(path)
3701 3702 ui.warn(_(b"copy %s to %s\n") % (path, newpath))
3702 3703 util.copyfiles(path, newpath)
3703 3704 if opts.get(b'empty'):
3704 3705 del q.applied[:]
3705 3706 q.applieddirty = True
3706 3707 q.savedirty()
3707 3708 return 0
3708 3709
3709 3710
3710 3711 @command(
3711 3712 b"qselect",
3712 3713 [
3713 3714 (b'n', b'none', None, _(b'disable all guards')),
3714 3715 (b's', b'series', None, _(b'list all guards in series file')),
3715 3716 (b'', b'pop', None, _(b'pop to before first guarded applied patch')),
3716 3717 (b'', b'reapply', None, _(b'pop, then reapply patches')),
3717 3718 ],
3718 3719 _(b'hg qselect [OPTION]... [GUARD]...'),
3719 3720 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3720 3721 )
3721 3722 def select(ui, repo, *args, **opts):
3722 3723 """set or print guarded patches to push
3723 3724
3724 3725 Use the :hg:`qguard` command to set or print guards on patch, then use
3725 3726 qselect to tell mq which guards to use. A patch will be pushed if
3726 3727 it has no guards or any positive guards match the currently
3727 3728 selected guard, but will not be pushed if any negative guards
3728 3729 match the current guard. For example::
3729 3730
3730 3731 qguard foo.patch -- -stable (negative guard)
3731 3732 qguard bar.patch +stable (positive guard)
3732 3733 qselect stable
3733 3734
3734 3735 This activates the "stable" guard. mq will skip foo.patch (because
3735 3736 it has a negative match) but push bar.patch (because it has a
3736 3737 positive match).
3737 3738
3738 3739 With no arguments, prints the currently active guards.
3739 3740 With one argument, sets the active guard.
3740 3741
3741 3742 Use -n/--none to deactivate guards (no other arguments needed).
3742 3743 When no guards are active, patches with positive guards are
3743 3744 skipped and patches with negative guards are pushed.
3744 3745
3745 3746 qselect can change the guards on applied patches. It does not pop
3746 3747 guarded patches by default. Use --pop to pop back to the last
3747 3748 applied patch that is not guarded. Use --reapply (which implies
3748 3749 --pop) to push back to the current patch afterwards, but skip
3749 3750 guarded patches.
3750 3751
3751 3752 Use -s/--series to print a list of all guards in the series file
3752 3753 (no other arguments needed). Use -v for more information.
3753 3754
3754 3755 Returns 0 on success."""
3755 3756
3756 3757 q = repo.mq
3757 3758 opts = pycompat.byteskwargs(opts)
3758 3759 guards = q.active()
3759 3760 pushable = lambda i: q.pushable(q.applied[i].name)[0]
3760 3761 if args or opts.get(b'none'):
3761 3762 old_unapplied = q.unapplied(repo)
3762 3763 old_guarded = [i for i in range(len(q.applied)) if not pushable(i)]
3763 3764 q.setactive(args)
3764 3765 q.savedirty()
3765 3766 if not args:
3766 3767 ui.status(_(b'guards deactivated\n'))
3767 3768 if not opts.get(b'pop') and not opts.get(b'reapply'):
3768 3769 unapplied = q.unapplied(repo)
3769 3770 guarded = [i for i in range(len(q.applied)) if not pushable(i)]
3770 3771 if len(unapplied) != len(old_unapplied):
3771 3772 ui.status(
3772 3773 _(
3773 3774 b'number of unguarded, unapplied patches has '
3774 3775 b'changed from %d to %d\n'
3775 3776 )
3776 3777 % (len(old_unapplied), len(unapplied))
3777 3778 )
3778 3779 if len(guarded) != len(old_guarded):
3779 3780 ui.status(
3780 3781 _(
3781 3782 b'number of guarded, applied patches has changed '
3782 3783 b'from %d to %d\n'
3783 3784 )
3784 3785 % (len(old_guarded), len(guarded))
3785 3786 )
3786 3787 elif opts.get(b'series'):
3787 3788 guards = {}
3788 3789 noguards = 0
3789 3790 for gs in q.seriesguards:
3790 3791 if not gs:
3791 3792 noguards += 1
3792 3793 for g in gs:
3793 3794 guards.setdefault(g, 0)
3794 3795 guards[g] += 1
3795 3796 if ui.verbose:
3796 3797 guards[b'NONE'] = noguards
3797 3798 guards = list(guards.items())
3798 3799 guards.sort(key=lambda x: x[0][1:])
3799 3800 if guards:
3800 3801 ui.note(_(b'guards in series file:\n'))
3801 3802 for guard, count in guards:
3802 3803 ui.note(b'%2d ' % count)
3803 3804 ui.write(guard, b'\n')
3804 3805 else:
3805 3806 ui.note(_(b'no guards in series file\n'))
3806 3807 else:
3807 3808 if guards:
3808 3809 ui.note(_(b'active guards:\n'))
3809 3810 for g in guards:
3810 3811 ui.write(g, b'\n')
3811 3812 else:
3812 3813 ui.write(_(b'no active guards\n'))
3813 3814 reapply = opts.get(b'reapply') and q.applied and q.applied[-1].name
3814 3815 popped = False
3815 3816 if opts.get(b'pop') or opts.get(b'reapply'):
3816 3817 for i in range(len(q.applied)):
3817 3818 if not pushable(i):
3818 3819 ui.status(_(b'popping guarded patches\n'))
3819 3820 popped = True
3820 3821 if i == 0:
3821 3822 q.pop(repo, all=True)
3822 3823 else:
3823 3824 q.pop(repo, q.applied[i - 1].name)
3824 3825 break
3825 3826 if popped:
3826 3827 try:
3827 3828 if reapply:
3828 3829 ui.status(_(b'reapplying unguarded patches\n'))
3829 3830 q.push(repo, reapply)
3830 3831 finally:
3831 3832 q.savedirty()
3832 3833
3833 3834
3834 3835 @command(
3835 3836 b"qfinish",
3836 3837 [(b'a', b'applied', None, _(b'finish all applied changesets'))],
3837 3838 _(b'hg qfinish [-a] [REV]...'),
3838 3839 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3839 3840 )
3840 3841 def finish(ui, repo, *revrange, **opts):
3841 3842 """move applied patches into repository history
3842 3843
3843 3844 Finishes the specified revisions (corresponding to applied
3844 3845 patches) by moving them out of mq control into regular repository
3845 3846 history.
3846 3847
3847 3848 Accepts a revision range or the -a/--applied option. If --applied
3848 3849 is specified, all applied mq revisions are removed from mq
3849 3850 control. Otherwise, the given revisions must be at the base of the
3850 3851 stack of applied patches.
3851 3852
3852 3853 This can be especially useful if your changes have been applied to
3853 3854 an upstream repository, or if you are about to push your changes
3854 3855 to upstream.
3855 3856
3856 3857 Returns 0 on success.
3857 3858 """
3858 3859 if not opts.get('applied') and not revrange:
3859 3860 raise error.Abort(_(b'no revisions specified'))
3860 3861 elif opts.get('applied'):
3861 3862 revrange = (b'qbase::qtip',) + revrange
3862 3863
3863 3864 q = repo.mq
3864 3865 if not q.applied:
3865 3866 ui.status(_(b'no patches applied\n'))
3866 3867 return 0
3867 3868
3868 3869 revs = logcmdutil.revrange(repo, revrange)
3869 3870 if repo[b'.'].rev() in revs and repo[None].files():
3870 3871 ui.warn(_(b'warning: uncommitted changes in the working directory\n'))
3871 3872 # queue.finish may changes phases but leave the responsibility to lock the
3872 3873 # repo to the caller to avoid deadlock with wlock. This command code is
3873 3874 # responsibility for this locking.
3874 3875 with repo.lock():
3875 3876 q.finish(repo, revs)
3876 3877 q.savedirty()
3877 3878 return 0
3878 3879
3879 3880
3880 3881 @command(
3881 3882 b"qqueue",
3882 3883 [
3883 3884 (b'l', b'list', False, _(b'list all available queues')),
3884 3885 (b'', b'active', False, _(b'print name of active queue')),
3885 3886 (b'c', b'create', False, _(b'create new queue')),
3886 3887 (b'', b'rename', False, _(b'rename active queue')),
3887 3888 (b'', b'delete', False, _(b'delete reference to queue')),
3888 3889 (b'', b'purge', False, _(b'delete queue, and remove patch dir')),
3889 3890 ],
3890 3891 _(b'[OPTION] [QUEUE]'),
3891 3892 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3892 3893 )
3893 3894 def qqueue(ui, repo, name=None, **opts):
3894 3895 """manage multiple patch queues
3895 3896
3896 3897 Supports switching between different patch queues, as well as creating
3897 3898 new patch queues and deleting existing ones.
3898 3899
3899 3900 Omitting a queue name or specifying -l/--list will show you the registered
3900 3901 queues - by default the "normal" patches queue is registered. The currently
3901 3902 active queue will be marked with "(active)". Specifying --active will print
3902 3903 only the name of the active queue.
3903 3904
3904 3905 To create a new queue, use -c/--create. The queue is automatically made
3905 3906 active, except in the case where there are applied patches from the
3906 3907 currently active queue in the repository. Then the queue will only be
3907 3908 created and switching will fail.
3908 3909
3909 3910 To delete an existing queue, use --delete. You cannot delete the currently
3910 3911 active queue.
3911 3912
3912 3913 Returns 0 on success.
3913 3914 """
3914 3915 q = repo.mq
3915 3916 _defaultqueue = b'patches'
3916 3917 _allqueues = b'patches.queues'
3917 3918 _activequeue = b'patches.queue'
3918 3919
3919 3920 def _getcurrent():
3920 3921 cur = os.path.basename(q.path)
3921 3922 if cur.startswith(b'patches-'):
3922 3923 cur = cur[8:]
3923 3924 return cur
3924 3925
3925 3926 def _noqueues():
3926 3927 try:
3927 3928 fh = repo.vfs(_allqueues, b'r')
3928 3929 fh.close()
3929 3930 except IOError:
3930 3931 return True
3931 3932
3932 3933 return False
3933 3934
3934 3935 def _getqueues():
3935 3936 current = _getcurrent()
3936 3937
3937 3938 try:
3938 3939 fh = repo.vfs(_allqueues, b'r')
3939 3940 queues = [queue.strip() for queue in fh if queue.strip()]
3940 3941 fh.close()
3941 3942 if current not in queues:
3942 3943 queues.append(current)
3943 3944 except IOError:
3944 3945 queues = [_defaultqueue]
3945 3946
3946 3947 return sorted(queues)
3947 3948
3948 3949 def _setactive(name):
3949 3950 if q.applied:
3950 3951 raise error.Abort(
3951 3952 _(
3952 3953 b'new queue created, but cannot make active '
3953 3954 b'as patches are applied'
3954 3955 )
3955 3956 )
3956 3957 _setactivenocheck(name)
3957 3958
3958 3959 def _setactivenocheck(name):
3959 3960 fh = repo.vfs(_activequeue, b'w')
3960 3961 if name != b'patches':
3961 3962 fh.write(name)
3962 3963 fh.close()
3963 3964
3964 3965 def _addqueue(name):
3965 3966 fh = repo.vfs(_allqueues, b'a')
3966 3967 fh.write(b'%s\n' % (name,))
3967 3968 fh.close()
3968 3969
3969 3970 def _queuedir(name):
3970 3971 if name == b'patches':
3971 3972 return repo.vfs.join(b'patches')
3972 3973 else:
3973 3974 return repo.vfs.join(b'patches-' + name)
3974 3975
3975 3976 def _validname(name):
3976 3977 for n in name:
3977 3978 if n in b':\\/.':
3978 3979 return False
3979 3980 return True
3980 3981
3981 3982 def _delete(name):
3982 3983 if name not in existing:
3983 3984 raise error.Abort(_(b'cannot delete queue that does not exist'))
3984 3985
3985 3986 current = _getcurrent()
3986 3987
3987 3988 if name == current:
3988 3989 raise error.Abort(_(b'cannot delete currently active queue'))
3989 3990
3990 3991 fh = repo.vfs(b'patches.queues.new', b'w')
3991 3992 for queue in existing:
3992 3993 if queue == name:
3993 3994 continue
3994 3995 fh.write(b'%s\n' % (queue,))
3995 3996 fh.close()
3996 3997 repo.vfs.rename(b'patches.queues.new', _allqueues)
3997 3998
3998 3999 opts = pycompat.byteskwargs(opts)
3999 4000 if not name or opts.get(b'list') or opts.get(b'active'):
4000 4001 current = _getcurrent()
4001 4002 if opts.get(b'active'):
4002 4003 ui.write(b'%s\n' % (current,))
4003 4004 return
4004 4005 for queue in _getqueues():
4005 4006 ui.write(b'%s' % (queue,))
4006 4007 if queue == current and not ui.quiet:
4007 4008 ui.write(_(b' (active)\n'))
4008 4009 else:
4009 4010 ui.write(b'\n')
4010 4011 return
4011 4012
4012 4013 if not _validname(name):
4013 4014 raise error.Abort(
4014 4015 _(b'invalid queue name, may not contain the characters ":\\/."')
4015 4016 )
4016 4017
4017 4018 with repo.wlock():
4018 4019 existing = _getqueues()
4019 4020
4020 4021 if opts.get(b'create'):
4021 4022 if name in existing:
4022 4023 raise error.Abort(_(b'queue "%s" already exists') % name)
4023 4024 if _noqueues():
4024 4025 _addqueue(_defaultqueue)
4025 4026 _addqueue(name)
4026 4027 _setactive(name)
4027 4028 elif opts.get(b'rename'):
4028 4029 current = _getcurrent()
4029 4030 if name == current:
4030 4031 raise error.Abort(
4031 4032 _(b'can\'t rename "%s" to its current name') % name
4032 4033 )
4033 4034 if name in existing:
4034 4035 raise error.Abort(_(b'queue "%s" already exists') % name)
4035 4036
4036 4037 olddir = _queuedir(current)
4037 4038 newdir = _queuedir(name)
4038 4039
4039 4040 if os.path.exists(newdir):
4040 4041 raise error.Abort(
4041 4042 _(b'non-queue directory "%s" already exists') % newdir
4042 4043 )
4043 4044
4044 4045 fh = repo.vfs(b'patches.queues.new', b'w')
4045 4046 for queue in existing:
4046 4047 if queue == current:
4047 4048 fh.write(b'%s\n' % (name,))
4048 4049 if os.path.exists(olddir):
4049 4050 util.rename(olddir, newdir)
4050 4051 else:
4051 4052 fh.write(b'%s\n' % (queue,))
4052 4053 fh.close()
4053 4054 repo.vfs.rename(b'patches.queues.new', _allqueues)
4054 4055 _setactivenocheck(name)
4055 4056 elif opts.get(b'delete'):
4056 4057 _delete(name)
4057 4058 elif opts.get(b'purge'):
4058 4059 if name in existing:
4059 4060 _delete(name)
4060 4061 qdir = _queuedir(name)
4061 4062 if os.path.exists(qdir):
4062 4063 shutil.rmtree(qdir)
4063 4064 else:
4064 4065 if name not in existing:
4065 4066 raise error.Abort(_(b'use --create to create a new queue'))
4066 4067 _setactive(name)
4067 4068
4068 4069
4069 4070 def mqphasedefaults(repo, roots):
4070 4071 """callback used to set mq changeset as secret when no phase data exists"""
4071 4072 if repo.mq.applied:
4072 4073 if repo.ui.configbool(b'mq', b'secret'):
4073 4074 mqphase = phases.secret
4074 4075 else:
4075 4076 mqphase = phases.draft
4076 4077 qbase = repo[repo.mq.applied[0].node]
4077 4078 roots[mqphase].add(qbase.node())
4078 4079 return roots
4079 4080
4080 4081
4081 4082 def reposetup(ui, repo):
4082 4083 class mqrepo(repo.__class__):
4083 4084 @localrepo.unfilteredpropertycache
4084 4085 def mq(self):
4085 4086 return queue(self.ui, self.baseui, self.path)
4086 4087
4087 4088 def invalidateall(self):
4088 4089 super(mqrepo, self).invalidateall()
4089 4090 if localrepo.hasunfilteredcache(self, 'mq'):
4090 4091 # recreate mq in case queue path was changed
4091 4092 delattr(self.unfiltered(), 'mq')
4092 4093
4093 4094 def abortifwdirpatched(self, errmsg, force=False):
4094 4095 if self.mq.applied and self.mq.checkapplied and not force:
4095 4096 parents = self.dirstate.parents()
4096 4097 patches = [s.node for s in self.mq.applied]
4097 4098 if any(p in patches for p in parents):
4098 4099 raise error.Abort(errmsg)
4099 4100
4100 4101 def commit(
4101 4102 self,
4102 4103 text=b"",
4103 4104 user=None,
4104 4105 date=None,
4105 4106 match=None,
4106 4107 force=False,
4107 4108 editor=False,
4108 4109 extra=None,
4109 4110 ):
4110 4111 if extra is None:
4111 4112 extra = {}
4112 4113 self.abortifwdirpatched(
4113 4114 _(b'cannot commit over an applied mq patch'), force
4114 4115 )
4115 4116
4116 4117 return super(mqrepo, self).commit(
4117 4118 text, user, date, match, force, editor, extra
4118 4119 )
4119 4120
4120 4121 def checkpush(self, pushop):
4121 4122 if self.mq.applied and self.mq.checkapplied and not pushop.force:
4122 4123 outapplied = [e.node for e in self.mq.applied]
4123 4124 if pushop.revs:
4124 4125 # Assume applied patches have no non-patch descendants and
4125 4126 # are not on remote already. Filtering any changeset not
4126 4127 # pushed.
4127 4128 heads = set(pushop.revs)
4128 4129 for node in reversed(outapplied):
4129 4130 if node in heads:
4130 4131 break
4131 4132 else:
4132 4133 outapplied.pop()
4133 4134 # looking for pushed and shared changeset
4134 4135 for node in outapplied:
4135 4136 if self[node].phase() < phases.secret:
4136 4137 raise error.Abort(_(b'source has mq patches applied'))
4137 4138 # no non-secret patches pushed
4138 4139 super(mqrepo, self).checkpush(pushop)
4139 4140
4140 4141 def _findtags(self):
4141 4142 '''augment tags from base class with patch tags'''
4142 4143 result = super(mqrepo, self)._findtags()
4143 4144
4144 4145 q = self.mq
4145 4146 if not q.applied:
4146 4147 return result
4147 4148
4148 4149 mqtags = [(patch.node, patch.name) for patch in q.applied]
4149 4150
4150 4151 try:
4151 4152 # for now ignore filtering business
4152 4153 self.unfiltered().changelog.rev(mqtags[-1][0])
4153 4154 except error.LookupError:
4154 4155 self.ui.warn(
4155 4156 _(b'mq status file refers to unknown node %s\n')
4156 4157 % short(mqtags[-1][0])
4157 4158 )
4158 4159 return result
4159 4160
4160 4161 # do not add fake tags for filtered revisions
4161 4162 included = self.changelog.hasnode
4162 4163 mqtags = [mqt for mqt in mqtags if included(mqt[0])]
4163 4164 if not mqtags:
4164 4165 return result
4165 4166
4166 4167 mqtags.append((mqtags[-1][0], b'qtip'))
4167 4168 mqtags.append((mqtags[0][0], b'qbase'))
4168 4169 mqtags.append((self.changelog.parents(mqtags[0][0])[0], b'qparent'))
4169 4170 tags = result[0]
4170 4171 for patch in mqtags:
4171 4172 if patch[1] in tags:
4172 4173 self.ui.warn(
4173 4174 _(b'tag %s overrides mq patch of the same name\n')
4174 4175 % patch[1]
4175 4176 )
4176 4177 else:
4177 4178 tags[patch[1]] = patch[0]
4178 4179
4179 4180 return result
4180 4181
4181 4182 if repo.local():
4182 4183 repo.__class__ = mqrepo
4183 4184
4184 4185 repo._phasedefaults.append(mqphasedefaults)
4185 4186
4186 4187
4187 4188 def mqimport(orig, ui, repo, *args, **kwargs):
4188 4189 if util.safehasattr(repo, b'abortifwdirpatched') and not kwargs.get(
4189 4190 'no_commit', False
4190 4191 ):
4191 4192 repo.abortifwdirpatched(
4192 4193 _(b'cannot import over an applied patch'), kwargs.get('force')
4193 4194 )
4194 4195 return orig(ui, repo, *args, **kwargs)
4195 4196
4196 4197
4197 4198 def mqinit(orig, ui, *args, **kwargs):
4198 4199 mq = kwargs.pop('mq', None)
4199 4200
4200 4201 if not mq:
4201 4202 return orig(ui, *args, **kwargs)
4202 4203
4203 4204 if args:
4204 4205 repopath = args[0]
4205 4206 if not hg.islocal(repopath):
4206 4207 raise error.Abort(
4207 4208 _(b'only a local queue repository may be initialized')
4208 4209 )
4209 4210 else:
4210 4211 repopath = cmdutil.findrepo(encoding.getcwd())
4211 4212 if not repopath:
4212 4213 raise error.Abort(
4213 4214 _(b'there is no Mercurial repository here (.hg not found)')
4214 4215 )
4215 4216 repo = hg.repository(ui, repopath)
4216 4217 return qinit(ui, repo, True)
4217 4218
4218 4219
4219 4220 def mqcommand(orig, ui, repo, *args, **kwargs):
4220 4221 """Add --mq option to operate on patch repository instead of main"""
4221 4222
4222 4223 # some commands do not like getting unknown options
4223 4224 mq = kwargs.pop('mq', None)
4224 4225
4225 4226 if not mq:
4226 4227 return orig(ui, repo, *args, **kwargs)
4227 4228
4228 4229 q = repo.mq
4229 4230 r = q.qrepo()
4230 4231 if not r:
4231 4232 raise error.Abort(_(b'no queue repository'))
4232 4233 return orig(r.ui, r, *args, **kwargs)
4233 4234
4234 4235
4235 4236 def summaryhook(ui, repo):
4236 4237 q = repo.mq
4237 4238 m = []
4238 4239 a, u = len(q.applied), len(q.unapplied(repo))
4239 4240 if a:
4240 4241 m.append(ui.label(_(b"%d applied"), b'qseries.applied') % a)
4241 4242 if u:
4242 4243 m.append(ui.label(_(b"%d unapplied"), b'qseries.unapplied') % u)
4243 4244 if m:
4244 4245 # i18n: column positioning for "hg summary"
4245 4246 ui.write(_(b"mq: %s\n") % b', '.join(m))
4246 4247 else:
4247 4248 # i18n: column positioning for "hg summary"
4248 4249 ui.note(_(b"mq: (empty queue)\n"))
4249 4250
4250 4251
4251 4252 revsetpredicate = registrar.revsetpredicate()
4252 4253
4253 4254
4254 4255 @revsetpredicate(b'mq()')
4255 4256 def revsetmq(repo, subset, x):
4256 4257 """Changesets managed by MQ."""
4257 4258 revsetlang.getargs(x, 0, 0, _(b"mq takes no arguments"))
4258 4259 applied = {repo[r.node].rev() for r in repo.mq.applied}
4259 4260 return smartset.baseset([r for r in subset if r in applied])
4260 4261
4261 4262
4262 4263 # tell hggettext to extract docstrings from these functions:
4263 4264 i18nfunctions = [revsetmq]
4264 4265
4265 4266
4266 4267 def extsetup(ui):
4267 4268 # Ensure mq wrappers are called first, regardless of extension load order by
4268 4269 # NOT wrapping in uisetup() and instead deferring to init stage two here.
4269 4270 mqopt = [(b'', b'mq', None, _(b"operate on patch repository"))]
4270 4271
4271 4272 extensions.wrapcommand(commands.table, b'import', mqimport)
4272 4273 cmdutil.summaryhooks.add(b'mq', summaryhook)
4273 4274
4274 4275 entry = extensions.wrapcommand(commands.table, b'init', mqinit)
4275 4276 entry[1].extend(mqopt)
4276 4277
4277 4278 def dotable(cmdtable):
4278 4279 for cmd, entry in cmdtable.items():
4279 4280 cmd = cmdutil.parsealiases(cmd)[0]
4280 4281 func = entry[0]
4281 4282 if func.norepo:
4282 4283 continue
4283 4284 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
4284 4285 entry[1].extend(mqopt)
4285 4286
4286 4287 dotable(commands.table)
4287 4288
4288 4289 thismodule = sys.modules["hgext.mq"]
4289 4290 for extname, extmodule in extensions.extensions():
4290 4291 if extmodule != thismodule:
4291 4292 dotable(getattr(extmodule, 'cmdtable', {}))
4292 4293
4293 4294
4294 4295 colortable = {
4295 4296 b'qguard.negative': b'red',
4296 4297 b'qguard.positive': b'yellow',
4297 4298 b'qguard.unguarded': b'green',
4298 4299 b'qseries.applied': b'blue bold underline',
4299 4300 b'qseries.guarded': b'black bold',
4300 4301 b'qseries.missing': b'red bold',
4301 4302 b'qseries.unapplied': b'black bold',
4302 4303 }
General Comments 0
You need to be logged in to leave comments. Login now