##// END OF EJS Templates
path: pass `path` to `peer` in mq...
marmoute -
r50638:f22364e4 default
parent child Browse files
Show More
@@ -1,4302 +1,4303
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 dirstateguard,
86 86 encoding,
87 87 error,
88 88 extensions,
89 89 hg,
90 90 localrepo,
91 91 lock as lockmod,
92 92 logcmdutil,
93 93 patch as patchmod,
94 94 phases,
95 95 pycompat,
96 96 registrar,
97 97 revsetlang,
98 98 scmutil,
99 99 smartset,
100 100 strip,
101 101 subrepoutil,
102 102 util,
103 103 vfs as vfsmod,
104 104 )
105 105 from mercurial.utils import (
106 106 dateutil,
107 107 stringutil,
108 108 urlutil,
109 109 )
110 110
111 111 release = lockmod.release
112 112 seriesopts = [(b's', b'summary', None, _(b'print first line of patch header'))]
113 113
114 114 cmdtable = {}
115 115 command = registrar.command(cmdtable)
116 116 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
117 117 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
118 118 # be specifying the version(s) of Mercurial they are tested with, or
119 119 # leave the attribute unspecified.
120 120 testedwith = b'ships-with-hg-core'
121 121
122 122 configtable = {}
123 123 configitem = registrar.configitem(configtable)
124 124
125 125 configitem(
126 126 b'mq',
127 127 b'git',
128 128 default=b'auto',
129 129 )
130 130 configitem(
131 131 b'mq',
132 132 b'keepchanges',
133 133 default=False,
134 134 )
135 135 configitem(
136 136 b'mq',
137 137 b'plain',
138 138 default=False,
139 139 )
140 140 configitem(
141 141 b'mq',
142 142 b'secret',
143 143 default=False,
144 144 )
145 145
146 146 # force load strip extension formerly included in mq and import some utility
147 147 try:
148 148 extensions.find(b'strip')
149 149 except KeyError:
150 150 # note: load is lazy so we could avoid the try-except,
151 151 # but I (marmoute) prefer this explicit code.
152 152 class dummyui:
153 153 def debug(self, msg):
154 154 pass
155 155
156 156 def log(self, event, msgfmt, *msgargs, **opts):
157 157 pass
158 158
159 159 extensions.load(dummyui(), b'strip', b'')
160 160
161 161 strip = strip.strip
162 162
163 163
164 164 def checksubstate(repo, baserev=None):
165 165 """return list of subrepos at a different revision than substate.
166 166 Abort if any subrepos have uncommitted changes."""
167 167 inclsubs = []
168 168 wctx = repo[None]
169 169 if baserev:
170 170 bctx = repo[baserev]
171 171 else:
172 172 bctx = wctx.p1()
173 173 for s in sorted(wctx.substate):
174 174 wctx.sub(s).bailifchanged(True)
175 175 if s not in bctx.substate or bctx.sub(s).dirty():
176 176 inclsubs.append(s)
177 177 return inclsubs
178 178
179 179
180 180 # Patch names looks like unix-file names.
181 181 # They must be joinable with queue directory and result in the patch path.
182 182 normname = util.normpath
183 183
184 184
185 185 class statusentry:
186 186 def __init__(self, node, name):
187 187 self.node, self.name = node, name
188 188
189 189 def __bytes__(self):
190 190 return hex(self.node) + b':' + self.name
191 191
192 192 __str__ = encoding.strmethod(__bytes__)
193 193 __repr__ = encoding.strmethod(__bytes__)
194 194
195 195
196 196 # The order of the headers in 'hg export' HG patches:
197 197 HGHEADERS = [
198 198 # '# HG changeset patch',
199 199 b'# User ',
200 200 b'# Date ',
201 201 b'# ',
202 202 b'# Branch ',
203 203 b'# Node ID ',
204 204 b'# Parent ', # can occur twice for merges - but that is not relevant for mq
205 205 ]
206 206 # The order of headers in plain 'mail style' patches:
207 207 PLAINHEADERS = {
208 208 b'from': 0,
209 209 b'date': 1,
210 210 b'subject': 2,
211 211 }
212 212
213 213
214 214 def inserthgheader(lines, header, value):
215 215 """Assuming lines contains a HG patch header, add a header line with value.
216 216 >>> try: inserthgheader([], b'# Date ', b'z')
217 217 ... except ValueError as inst: print("oops")
218 218 oops
219 219 >>> inserthgheader([b'# HG changeset patch'], b'# Date ', b'z')
220 220 ['# HG changeset patch', '# Date z']
221 221 >>> inserthgheader([b'# HG changeset patch', b''], b'# Date ', b'z')
222 222 ['# HG changeset patch', '# Date z', '']
223 223 >>> inserthgheader([b'# HG changeset patch', b'# User y'], b'# Date ', b'z')
224 224 ['# HG changeset patch', '# User y', '# Date z']
225 225 >>> inserthgheader([b'# HG changeset patch', b'# Date x', b'# User y'],
226 226 ... b'# User ', b'z')
227 227 ['# HG changeset patch', '# Date x', '# User z']
228 228 >>> inserthgheader([b'# HG changeset patch', b'# Date y'], b'# Date ', b'z')
229 229 ['# HG changeset patch', '# Date z']
230 230 >>> inserthgheader([b'# HG changeset patch', b'', b'# Date y'],
231 231 ... b'# Date ', b'z')
232 232 ['# HG changeset patch', '# Date z', '', '# Date y']
233 233 >>> inserthgheader([b'# HG changeset patch', b'# Parent y'],
234 234 ... b'# Date ', b'z')
235 235 ['# HG changeset patch', '# Date z', '# Parent y']
236 236 """
237 237 start = lines.index(b'# HG changeset patch') + 1
238 238 newindex = HGHEADERS.index(header)
239 239 bestpos = len(lines)
240 240 for i in range(start, len(lines)):
241 241 line = lines[i]
242 242 if not line.startswith(b'# '):
243 243 bestpos = min(bestpos, i)
244 244 break
245 245 for lineindex, h in enumerate(HGHEADERS):
246 246 if line.startswith(h):
247 247 if lineindex == newindex:
248 248 lines[i] = header + value
249 249 return lines
250 250 if lineindex > newindex:
251 251 bestpos = min(bestpos, i)
252 252 break # next line
253 253 lines.insert(bestpos, header + value)
254 254 return lines
255 255
256 256
257 257 def insertplainheader(lines, header, value):
258 258 """For lines containing a plain patch header, add a header line with value.
259 259 >>> insertplainheader([], b'Date', b'z')
260 260 ['Date: z']
261 261 >>> insertplainheader([b''], b'Date', b'z')
262 262 ['Date: z', '']
263 263 >>> insertplainheader([b'x'], b'Date', b'z')
264 264 ['Date: z', '', 'x']
265 265 >>> insertplainheader([b'From: y', b'x'], b'Date', b'z')
266 266 ['From: y', 'Date: z', '', 'x']
267 267 >>> insertplainheader([b' date : x', b' from : y', b''], b'From', b'z')
268 268 [' date : x', 'From: z', '']
269 269 >>> insertplainheader([b'', b'Date: y'], b'Date', b'z')
270 270 ['Date: z', '', 'Date: y']
271 271 >>> insertplainheader([b'foo: bar', b'DATE: z', b'x'], b'From', b'y')
272 272 ['From: y', 'foo: bar', 'DATE: z', '', 'x']
273 273 """
274 274 newprio = PLAINHEADERS[header.lower()]
275 275 bestpos = len(lines)
276 276 for i, line in enumerate(lines):
277 277 if b':' in line:
278 278 lheader = line.split(b':', 1)[0].strip().lower()
279 279 lprio = PLAINHEADERS.get(lheader, newprio + 1)
280 280 if lprio == newprio:
281 281 lines[i] = b'%s: %s' % (header, value)
282 282 return lines
283 283 if lprio > newprio and i < bestpos:
284 284 bestpos = i
285 285 else:
286 286 if line:
287 287 lines.insert(i, b'')
288 288 if i < bestpos:
289 289 bestpos = i
290 290 break
291 291 lines.insert(bestpos, b'%s: %s' % (header, value))
292 292 return lines
293 293
294 294
295 295 class patchheader:
296 296 def __init__(self, pf, plainmode=False):
297 297 def eatdiff(lines):
298 298 while lines:
299 299 l = lines[-1]
300 300 if (
301 301 l.startswith(b"diff -")
302 302 or l.startswith(b"Index:")
303 303 or l.startswith(b"===========")
304 304 ):
305 305 del lines[-1]
306 306 else:
307 307 break
308 308
309 309 def eatempty(lines):
310 310 while lines:
311 311 if not lines[-1].strip():
312 312 del lines[-1]
313 313 else:
314 314 break
315 315
316 316 message = []
317 317 comments = []
318 318 user = None
319 319 date = None
320 320 parent = None
321 321 format = None
322 322 subject = None
323 323 branch = None
324 324 nodeid = None
325 325 diffstart = 0
326 326
327 327 for line in open(pf, b'rb'):
328 328 line = line.rstrip()
329 329 if line.startswith(b'diff --git') or (
330 330 diffstart and line.startswith(b'+++ ')
331 331 ):
332 332 diffstart = 2
333 333 break
334 334 diffstart = 0 # reset
335 335 if line.startswith(b"--- "):
336 336 diffstart = 1
337 337 continue
338 338 elif format == b"hgpatch":
339 339 # parse values when importing the result of an hg export
340 340 if line.startswith(b"# User "):
341 341 user = line[7:]
342 342 elif line.startswith(b"# Date "):
343 343 date = line[7:]
344 344 elif line.startswith(b"# Parent "):
345 345 parent = line[9:].lstrip() # handle double trailing space
346 346 elif line.startswith(b"# Branch "):
347 347 branch = line[9:]
348 348 elif line.startswith(b"# Node ID "):
349 349 nodeid = line[10:]
350 350 elif not line.startswith(b"# ") and line:
351 351 message.append(line)
352 352 format = None
353 353 elif line == b'# HG changeset patch':
354 354 message = []
355 355 format = b"hgpatch"
356 356 elif format != b"tagdone" and (
357 357 line.startswith(b"Subject: ") or line.startswith(b"subject: ")
358 358 ):
359 359 subject = line[9:]
360 360 format = b"tag"
361 361 elif format != b"tagdone" and (
362 362 line.startswith(b"From: ") or line.startswith(b"from: ")
363 363 ):
364 364 user = line[6:]
365 365 format = b"tag"
366 366 elif format != b"tagdone" and (
367 367 line.startswith(b"Date: ") or line.startswith(b"date: ")
368 368 ):
369 369 date = line[6:]
370 370 format = b"tag"
371 371 elif format == b"tag" and line == b"":
372 372 # when looking for tags (subject: from: etc) they
373 373 # end once you find a blank line in the source
374 374 format = b"tagdone"
375 375 elif message or line:
376 376 message.append(line)
377 377 comments.append(line)
378 378
379 379 eatdiff(message)
380 380 eatdiff(comments)
381 381 # Remember the exact starting line of the patch diffs before consuming
382 382 # empty lines, for external use by TortoiseHg and others
383 383 self.diffstartline = len(comments)
384 384 eatempty(message)
385 385 eatempty(comments)
386 386
387 387 # make sure message isn't empty
388 388 if format and format.startswith(b"tag") and subject:
389 389 message.insert(0, subject)
390 390
391 391 self.message = message
392 392 self.comments = comments
393 393 self.user = user
394 394 self.date = date
395 395 self.parent = parent
396 396 # nodeid and branch are for external use by TortoiseHg and others
397 397 self.nodeid = nodeid
398 398 self.branch = branch
399 399 self.haspatch = diffstart > 1
400 400 self.plainmode = (
401 401 plainmode
402 402 or b'# HG changeset patch' not in self.comments
403 403 and any(
404 404 c.startswith(b'Date: ') or c.startswith(b'From: ')
405 405 for c in self.comments
406 406 )
407 407 )
408 408
409 409 def setuser(self, user):
410 410 try:
411 411 inserthgheader(self.comments, b'# User ', user)
412 412 except ValueError:
413 413 if self.plainmode:
414 414 insertplainheader(self.comments, b'From', user)
415 415 else:
416 416 tmp = [b'# HG changeset patch', b'# User ' + user]
417 417 self.comments = tmp + self.comments
418 418 self.user = user
419 419
420 420 def setdate(self, date):
421 421 try:
422 422 inserthgheader(self.comments, b'# Date ', date)
423 423 except ValueError:
424 424 if self.plainmode:
425 425 insertplainheader(self.comments, b'Date', date)
426 426 else:
427 427 tmp = [b'# HG changeset patch', b'# Date ' + date]
428 428 self.comments = tmp + self.comments
429 429 self.date = date
430 430
431 431 def setparent(self, parent):
432 432 try:
433 433 inserthgheader(self.comments, b'# Parent ', parent)
434 434 except ValueError:
435 435 if not self.plainmode:
436 436 tmp = [b'# HG changeset patch', b'# Parent ' + parent]
437 437 self.comments = tmp + self.comments
438 438 self.parent = parent
439 439
440 440 def setmessage(self, message):
441 441 if self.comments:
442 442 self._delmsg()
443 443 self.message = [message]
444 444 if message:
445 445 if self.plainmode and self.comments and self.comments[-1]:
446 446 self.comments.append(b'')
447 447 self.comments.append(message)
448 448
449 449 def __bytes__(self):
450 450 s = b'\n'.join(self.comments).rstrip()
451 451 if not s:
452 452 return b''
453 453 return s + b'\n\n'
454 454
455 455 __str__ = encoding.strmethod(__bytes__)
456 456
457 457 def _delmsg(self):
458 458 """Remove existing message, keeping the rest of the comments fields.
459 459 If comments contains 'subject: ', message will prepend
460 460 the field and a blank line."""
461 461 if self.message:
462 462 subj = b'subject: ' + self.message[0].lower()
463 463 for i in range(len(self.comments)):
464 464 if subj == self.comments[i].lower():
465 465 del self.comments[i]
466 466 self.message = self.message[2:]
467 467 break
468 468 ci = 0
469 469 for mi in self.message:
470 470 while mi != self.comments[ci]:
471 471 ci += 1
472 472 del self.comments[ci]
473 473
474 474
475 475 def newcommit(repo, phase, *args, **kwargs):
476 476 """helper dedicated to ensure a commit respect mq.secret setting
477 477
478 478 It should be used instead of repo.commit inside the mq source for operation
479 479 creating new changeset.
480 480 """
481 481 repo = repo.unfiltered()
482 482 if phase is None:
483 483 if repo.ui.configbool(b'mq', b'secret'):
484 484 phase = phases.secret
485 485 overrides = {(b'ui', b'allowemptycommit'): True}
486 486 if phase is not None:
487 487 overrides[(b'phases', b'new-commit')] = phase
488 488 with repo.ui.configoverride(overrides, b'mq'):
489 489 repo.ui.setconfig(b'ui', b'allowemptycommit', True)
490 490 return repo.commit(*args, **kwargs)
491 491
492 492
493 493 class AbortNoCleanup(error.Abort):
494 494 pass
495 495
496 496
497 497 class queue:
498 498 def __init__(self, ui, baseui, path, patchdir=None):
499 499 self.basepath = path
500 500 try:
501 501 with open(os.path.join(path, b'patches.queue'), 'rb') as fh:
502 502 cur = fh.read().rstrip()
503 503
504 504 if not cur:
505 505 curpath = os.path.join(path, b'patches')
506 506 else:
507 507 curpath = os.path.join(path, b'patches-' + cur)
508 508 except IOError:
509 509 curpath = os.path.join(path, b'patches')
510 510 self.path = patchdir or curpath
511 511 self.opener = vfsmod.vfs(self.path)
512 512 self.ui = ui
513 513 self.baseui = baseui
514 514 self.applieddirty = False
515 515 self.seriesdirty = False
516 516 self.added = []
517 517 self.seriespath = b"series"
518 518 self.statuspath = b"status"
519 519 self.guardspath = b"guards"
520 520 self.activeguards = None
521 521 self.guardsdirty = False
522 522 # Handle mq.git as a bool with extended values
523 523 gitmode = ui.config(b'mq', b'git').lower()
524 524 boolmode = stringutil.parsebool(gitmode)
525 525 if boolmode is not None:
526 526 if boolmode:
527 527 gitmode = b'yes'
528 528 else:
529 529 gitmode = b'no'
530 530 self.gitmode = gitmode
531 531 # deprecated config: mq.plain
532 532 self.plainmode = ui.configbool(b'mq', b'plain')
533 533 self.checkapplied = True
534 534
535 535 @util.propertycache
536 536 def applied(self):
537 537 def parselines(lines):
538 538 for l in lines:
539 539 entry = l.split(b':', 1)
540 540 if len(entry) > 1:
541 541 n, name = entry
542 542 yield statusentry(bin(n), name)
543 543 elif l.strip():
544 544 self.ui.warn(
545 545 _(b'malformated mq status line: %s\n')
546 546 % stringutil.pprint(entry)
547 547 )
548 548 # else we ignore empty lines
549 549
550 550 try:
551 551 lines = self.opener.read(self.statuspath).splitlines()
552 552 return list(parselines(lines))
553 553 except FileNotFoundError:
554 554 return []
555 555
556 556 @util.propertycache
557 557 def fullseries(self):
558 558 try:
559 559 return self.opener.read(self.seriespath).splitlines()
560 560 except FileNotFoundError:
561 561 return []
562 562
563 563 @util.propertycache
564 564 def series(self):
565 565 self.parseseries()
566 566 return self.series
567 567
568 568 @util.propertycache
569 569 def seriesguards(self):
570 570 self.parseseries()
571 571 return self.seriesguards
572 572
573 573 def invalidate(self):
574 574 for a in 'applied fullseries series seriesguards'.split():
575 575 if a in self.__dict__:
576 576 delattr(self, a)
577 577 self.applieddirty = False
578 578 self.seriesdirty = False
579 579 self.guardsdirty = False
580 580 self.activeguards = None
581 581
582 582 def diffopts(self, opts=None, patchfn=None, plain=False):
583 583 """Return diff options tweaked for this mq use, possibly upgrading to
584 584 git format, and possibly plain and without lossy options."""
585 585 diffopts = patchmod.difffeatureopts(
586 586 self.ui,
587 587 opts,
588 588 git=True,
589 589 whitespace=not plain,
590 590 formatchanging=not plain,
591 591 )
592 592 if self.gitmode == b'auto':
593 593 diffopts.upgrade = True
594 594 elif self.gitmode == b'keep':
595 595 pass
596 596 elif self.gitmode in (b'yes', b'no'):
597 597 diffopts.git = self.gitmode == b'yes'
598 598 else:
599 599 raise error.Abort(
600 600 _(b'mq.git option can be auto/keep/yes/no got %s')
601 601 % self.gitmode
602 602 )
603 603 if patchfn:
604 604 diffopts = self.patchopts(diffopts, patchfn)
605 605 return diffopts
606 606
607 607 def patchopts(self, diffopts, *patches):
608 608 """Return a copy of input diff options with git set to true if
609 609 referenced patch is a git patch and should be preserved as such.
610 610 """
611 611 diffopts = diffopts.copy()
612 612 if not diffopts.git and self.gitmode == b'keep':
613 613 for patchfn in patches:
614 614 patchf = self.opener(patchfn, b'r')
615 615 # if the patch was a git patch, refresh it as a git patch
616 616 diffopts.git = any(
617 617 line.startswith(b'diff --git') for line in patchf
618 618 )
619 619 patchf.close()
620 620 return diffopts
621 621
622 622 def join(self, *p):
623 623 return os.path.join(self.path, *p)
624 624
625 625 def findseries(self, patch):
626 626 def matchpatch(l):
627 627 l = l.split(b'#', 1)[0]
628 628 return l.strip() == patch
629 629
630 630 for index, l in enumerate(self.fullseries):
631 631 if matchpatch(l):
632 632 return index
633 633 return None
634 634
635 635 guard_re = re.compile(br'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
636 636
637 637 def parseseries(self):
638 638 self.series = []
639 639 self.seriesguards = []
640 640 for l in self.fullseries:
641 641 h = l.find(b'#')
642 642 if h == -1:
643 643 patch = l
644 644 comment = b''
645 645 elif h == 0:
646 646 continue
647 647 else:
648 648 patch = l[:h]
649 649 comment = l[h:]
650 650 patch = patch.strip()
651 651 if patch:
652 652 if patch in self.series:
653 653 raise error.Abort(
654 654 _(b'%s appears more than once in %s')
655 655 % (patch, self.join(self.seriespath))
656 656 )
657 657 self.series.append(patch)
658 658 self.seriesguards.append(self.guard_re.findall(comment))
659 659
660 660 def checkguard(self, guard):
661 661 if not guard:
662 662 return _(b'guard cannot be an empty string')
663 663 bad_chars = b'# \t\r\n\f'
664 664 first = guard[0]
665 665 if first in b'-+':
666 666 return _(b'guard %r starts with invalid character: %r') % (
667 667 guard,
668 668 first,
669 669 )
670 670 for c in bad_chars:
671 671 if c in guard:
672 672 return _(b'invalid character in guard %r: %r') % (guard, c)
673 673
674 674 def setactive(self, guards):
675 675 for guard in guards:
676 676 bad = self.checkguard(guard)
677 677 if bad:
678 678 raise error.Abort(bad)
679 679 guards = sorted(set(guards))
680 680 self.ui.debug(b'active guards: %s\n' % b' '.join(guards))
681 681 self.activeguards = guards
682 682 self.guardsdirty = True
683 683
684 684 def active(self):
685 685 if self.activeguards is None:
686 686 self.activeguards = []
687 687 try:
688 688 guards = self.opener.read(self.guardspath).split()
689 689 except FileNotFoundError:
690 690 guards = []
691 691 for i, guard in enumerate(guards):
692 692 bad = self.checkguard(guard)
693 693 if bad:
694 694 self.ui.warn(
695 695 b'%s:%d: %s\n'
696 696 % (self.join(self.guardspath), i + 1, bad)
697 697 )
698 698 else:
699 699 self.activeguards.append(guard)
700 700 return self.activeguards
701 701
702 702 def setguards(self, idx, guards):
703 703 for g in guards:
704 704 if len(g) < 2:
705 705 raise error.Abort(_(b'guard %r too short') % g)
706 706 if g[0] not in b'-+':
707 707 raise error.Abort(_(b'guard %r starts with invalid char') % g)
708 708 bad = self.checkguard(g[1:])
709 709 if bad:
710 710 raise error.Abort(bad)
711 711 drop = self.guard_re.sub(b'', self.fullseries[idx])
712 712 self.fullseries[idx] = drop + b''.join([b' #' + g for g in guards])
713 713 self.parseseries()
714 714 self.seriesdirty = True
715 715
716 716 def pushable(self, idx):
717 717 if isinstance(idx, bytes):
718 718 idx = self.series.index(idx)
719 719 patchguards = self.seriesguards[idx]
720 720 if not patchguards:
721 721 return True, None
722 722 guards = self.active()
723 723 exactneg = [
724 724 g for g in patchguards if g.startswith(b'-') and g[1:] in guards
725 725 ]
726 726 if exactneg:
727 727 return False, stringutil.pprint(exactneg[0])
728 728 pos = [g for g in patchguards if g.startswith(b'+')]
729 729 exactpos = [g for g in pos if g[1:] in guards]
730 730 if pos:
731 731 if exactpos:
732 732 return True, stringutil.pprint(exactpos[0])
733 733 return False, b' '.join([stringutil.pprint(p) for p in pos])
734 734 return True, b''
735 735
736 736 def explainpushable(self, idx, all_patches=False):
737 737 if all_patches:
738 738 write = self.ui.write
739 739 else:
740 740 write = self.ui.warn
741 741
742 742 if all_patches or self.ui.verbose:
743 743 if isinstance(idx, bytes):
744 744 idx = self.series.index(idx)
745 745 pushable, why = self.pushable(idx)
746 746 if all_patches and pushable:
747 747 if why is None:
748 748 write(
749 749 _(b'allowing %s - no guards in effect\n')
750 750 % self.series[idx]
751 751 )
752 752 else:
753 753 if not why:
754 754 write(
755 755 _(b'allowing %s - no matching negative guards\n')
756 756 % self.series[idx]
757 757 )
758 758 else:
759 759 write(
760 760 _(b'allowing %s - guarded by %s\n')
761 761 % (self.series[idx], why)
762 762 )
763 763 if not pushable:
764 764 if why:
765 765 write(
766 766 _(b'skipping %s - guarded by %s\n')
767 767 % (self.series[idx], why)
768 768 )
769 769 else:
770 770 write(
771 771 _(b'skipping %s - no matching guards\n')
772 772 % self.series[idx]
773 773 )
774 774
775 775 def savedirty(self):
776 776 def writelist(items, path):
777 777 fp = self.opener(path, b'wb')
778 778 for i in items:
779 779 fp.write(b"%s\n" % i)
780 780 fp.close()
781 781
782 782 if self.applieddirty:
783 783 writelist(map(bytes, self.applied), self.statuspath)
784 784 self.applieddirty = False
785 785 if self.seriesdirty:
786 786 writelist(self.fullseries, self.seriespath)
787 787 self.seriesdirty = False
788 788 if self.guardsdirty:
789 789 writelist(self.activeguards, self.guardspath)
790 790 self.guardsdirty = False
791 791 if self.added:
792 792 qrepo = self.qrepo()
793 793 if qrepo:
794 794 qrepo[None].add(f for f in self.added if f not in qrepo[None])
795 795 self.added = []
796 796
797 797 def removeundo(self, repo):
798 798 undo = repo.sjoin(b'undo')
799 799 if not os.path.exists(undo):
800 800 return
801 801 try:
802 802 os.unlink(undo)
803 803 except OSError as inst:
804 804 self.ui.warn(
805 805 _(b'error removing undo: %s\n') % stringutil.forcebytestr(inst)
806 806 )
807 807
808 808 def backup(self, repo, files, copy=False):
809 809 # backup local changes in --force case
810 810 for f in sorted(files):
811 811 absf = repo.wjoin(f)
812 812 if os.path.lexists(absf):
813 813 absorig = scmutil.backuppath(self.ui, repo, f)
814 814 self.ui.note(
815 815 _(b'saving current version of %s as %s\n')
816 816 % (f, os.path.relpath(absorig))
817 817 )
818 818
819 819 if copy:
820 820 util.copyfile(absf, absorig)
821 821 else:
822 822 util.rename(absf, absorig)
823 823
824 824 def printdiff(
825 825 self,
826 826 repo,
827 827 diffopts,
828 828 node1,
829 829 node2=None,
830 830 files=None,
831 831 fp=None,
832 832 changes=None,
833 833 opts=None,
834 834 ):
835 835 if opts is None:
836 836 opts = {}
837 837 stat = opts.get(b'stat')
838 838 m = scmutil.match(repo[node1], files, opts)
839 839 logcmdutil.diffordiffstat(
840 840 self.ui,
841 841 repo,
842 842 diffopts,
843 843 repo[node1],
844 844 repo[node2],
845 845 m,
846 846 changes,
847 847 stat,
848 848 fp,
849 849 )
850 850
851 851 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
852 852 # first try just applying the patch
853 853 (err, n) = self.apply(
854 854 repo, [patch], update_status=False, strict=True, merge=rev
855 855 )
856 856
857 857 if err == 0:
858 858 return (err, n)
859 859
860 860 if n is None:
861 861 raise error.Abort(_(b"apply failed for patch %s") % patch)
862 862
863 863 self.ui.warn(_(b"patch didn't work out, merging %s\n") % patch)
864 864
865 865 # apply failed, strip away that rev and merge.
866 866 hg.clean(repo, head)
867 867 strip(self.ui, repo, [n], update=False, backup=False)
868 868
869 869 ctx = repo[rev]
870 870 ret = hg.merge(ctx, remind=False)
871 871 if ret:
872 872 raise error.Abort(_(b"update returned %d") % ret)
873 873 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
874 874 if n is None:
875 875 raise error.Abort(_(b"repo commit failed"))
876 876 try:
877 877 ph = patchheader(mergeq.join(patch), self.plainmode)
878 878 except Exception:
879 879 raise error.Abort(_(b"unable to read %s") % patch)
880 880
881 881 diffopts = self.patchopts(diffopts, patch)
882 882 patchf = self.opener(patch, b"w")
883 883 comments = bytes(ph)
884 884 if comments:
885 885 patchf.write(comments)
886 886 self.printdiff(repo, diffopts, head, n, fp=patchf)
887 887 patchf.close()
888 888 self.removeundo(repo)
889 889 return (0, n)
890 890
891 891 def qparents(self, repo, rev=None):
892 892 """return the mq handled parent or p1
893 893
894 894 In some case where mq get himself in being the parent of a merge the
895 895 appropriate parent may be p2.
896 896 (eg: an in progress merge started with mq disabled)
897 897
898 898 If no parent are managed by mq, p1 is returned.
899 899 """
900 900 if rev is None:
901 901 (p1, p2) = repo.dirstate.parents()
902 902 if p2 == repo.nullid:
903 903 return p1
904 904 if not self.applied:
905 905 return None
906 906 return self.applied[-1].node
907 907 p1, p2 = repo.changelog.parents(rev)
908 908 if p2 != repo.nullid and p2 in [x.node for x in self.applied]:
909 909 return p2
910 910 return p1
911 911
912 912 def mergepatch(self, repo, mergeq, series, diffopts):
913 913 if not self.applied:
914 914 # each of the patches merged in will have two parents. This
915 915 # can confuse the qrefresh, qdiff, and strip code because it
916 916 # needs to know which parent is actually in the patch queue.
917 917 # so, we insert a merge marker with only one parent. This way
918 918 # the first patch in the queue is never a merge patch
919 919 #
920 920 pname = b".hg.patches.merge.marker"
921 921 n = newcommit(repo, None, b'[mq]: merge marker', force=True)
922 922 self.removeundo(repo)
923 923 self.applied.append(statusentry(n, pname))
924 924 self.applieddirty = True
925 925
926 926 head = self.qparents(repo)
927 927
928 928 for patch in series:
929 929 patch = mergeq.lookup(patch, strict=True)
930 930 if not patch:
931 931 self.ui.warn(_(b"patch %s does not exist\n") % patch)
932 932 return (1, None)
933 933 pushable, reason = self.pushable(patch)
934 934 if not pushable:
935 935 self.explainpushable(patch, all_patches=True)
936 936 continue
937 937 info = mergeq.isapplied(patch)
938 938 if not info:
939 939 self.ui.warn(_(b"patch %s is not applied\n") % patch)
940 940 return (1, None)
941 941 rev = info[1]
942 942 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
943 943 if head:
944 944 self.applied.append(statusentry(head, patch))
945 945 self.applieddirty = True
946 946 if err:
947 947 return (err, head)
948 948 self.savedirty()
949 949 return (0, head)
950 950
951 951 def patch(self, repo, patchfile):
952 952 """Apply patchfile to the working directory.
953 953 patchfile: name of patch file"""
954 954 files = set()
955 955 try:
956 956 fuzz = patchmod.patch(
957 957 self.ui, repo, patchfile, strip=1, files=files, eolmode=None
958 958 )
959 959 return (True, list(files), fuzz)
960 960 except Exception as inst:
961 961 self.ui.note(stringutil.forcebytestr(inst) + b'\n')
962 962 if not self.ui.verbose:
963 963 self.ui.warn(_(b"patch failed, unable to continue (try -v)\n"))
964 964 self.ui.traceback()
965 965 return (False, list(files), False)
966 966
967 967 def apply(
968 968 self,
969 969 repo,
970 970 series,
971 971 list=False,
972 972 update_status=True,
973 973 strict=False,
974 974 patchdir=None,
975 975 merge=None,
976 976 all_files=None,
977 977 tobackup=None,
978 978 keepchanges=False,
979 979 ):
980 980 wlock = lock = tr = None
981 981 try:
982 982 wlock = repo.wlock()
983 983 lock = repo.lock()
984 984 tr = repo.transaction(b"qpush")
985 985 try:
986 986 ret = self._apply(
987 987 repo,
988 988 series,
989 989 list,
990 990 update_status,
991 991 strict,
992 992 patchdir,
993 993 merge,
994 994 all_files=all_files,
995 995 tobackup=tobackup,
996 996 keepchanges=keepchanges,
997 997 )
998 998 tr.close()
999 999 self.savedirty()
1000 1000 return ret
1001 1001 except AbortNoCleanup:
1002 1002 tr.close()
1003 1003 self.savedirty()
1004 1004 raise
1005 1005 except: # re-raises
1006 1006 try:
1007 1007 tr.abort()
1008 1008 finally:
1009 1009 self.invalidate()
1010 1010 raise
1011 1011 finally:
1012 1012 release(tr, lock, wlock)
1013 1013 self.removeundo(repo)
1014 1014
1015 1015 def _apply(
1016 1016 self,
1017 1017 repo,
1018 1018 series,
1019 1019 list=False,
1020 1020 update_status=True,
1021 1021 strict=False,
1022 1022 patchdir=None,
1023 1023 merge=None,
1024 1024 all_files=None,
1025 1025 tobackup=None,
1026 1026 keepchanges=False,
1027 1027 ):
1028 1028 """returns (error, hash)
1029 1029
1030 1030 error = 1 for unable to read, 2 for patch failed, 3 for patch
1031 1031 fuzz. tobackup is None or a set of files to backup before they
1032 1032 are modified by a patch.
1033 1033 """
1034 1034 # TODO unify with commands.py
1035 1035 if not patchdir:
1036 1036 patchdir = self.path
1037 1037 err = 0
1038 1038 n = None
1039 1039 for patchname in series:
1040 1040 pushable, reason = self.pushable(patchname)
1041 1041 if not pushable:
1042 1042 self.explainpushable(patchname, all_patches=True)
1043 1043 continue
1044 1044 self.ui.status(_(b"applying %s\n") % patchname)
1045 1045 pf = os.path.join(patchdir, patchname)
1046 1046
1047 1047 try:
1048 1048 ph = patchheader(self.join(patchname), self.plainmode)
1049 1049 except IOError:
1050 1050 self.ui.warn(_(b"unable to read %s\n") % patchname)
1051 1051 err = 1
1052 1052 break
1053 1053
1054 1054 message = ph.message
1055 1055 if not message:
1056 1056 # The commit message should not be translated
1057 1057 message = b"imported patch %s\n" % patchname
1058 1058 else:
1059 1059 if list:
1060 1060 # The commit message should not be translated
1061 1061 message.append(b"\nimported patch %s" % patchname)
1062 1062 message = b'\n'.join(message)
1063 1063
1064 1064 if ph.haspatch:
1065 1065 if tobackup:
1066 1066 touched = patchmod.changedfiles(self.ui, repo, pf)
1067 1067 touched = set(touched) & tobackup
1068 1068 if touched and keepchanges:
1069 1069 raise AbortNoCleanup(
1070 1070 _(b"conflicting local changes found"),
1071 1071 hint=_(b"did you forget to qrefresh?"),
1072 1072 )
1073 1073 self.backup(repo, touched, copy=True)
1074 1074 tobackup = tobackup - touched
1075 1075 (patcherr, files, fuzz) = self.patch(repo, pf)
1076 1076 if all_files is not None:
1077 1077 all_files.update(files)
1078 1078 patcherr = not patcherr
1079 1079 else:
1080 1080 self.ui.warn(_(b"patch %s is empty\n") % patchname)
1081 1081 patcherr, files, fuzz = 0, [], 0
1082 1082
1083 1083 if merge and files:
1084 1084 # Mark as removed/merged and update dirstate parent info
1085 1085 with repo.dirstate.parentchange():
1086 1086 for f in files:
1087 1087 repo.dirstate.update_file_p1(f, p1_tracked=True)
1088 1088 p1 = repo.dirstate.p1()
1089 1089 repo.setparents(p1, merge)
1090 1090
1091 1091 if all_files and b'.hgsubstate' in all_files:
1092 1092 wctx = repo[None]
1093 1093 pctx = repo[b'.']
1094 1094 overwrite = False
1095 1095 mergedsubstate = subrepoutil.submerge(
1096 1096 repo, pctx, wctx, wctx, overwrite
1097 1097 )
1098 1098 files += mergedsubstate.keys()
1099 1099
1100 1100 match = scmutil.matchfiles(repo, files or [])
1101 1101 oldtip = repo.changelog.tip()
1102 1102 n = newcommit(
1103 1103 repo, None, message, ph.user, ph.date, match=match, force=True
1104 1104 )
1105 1105 if repo.changelog.tip() == oldtip:
1106 1106 raise error.Abort(
1107 1107 _(b"qpush exactly duplicates child changeset")
1108 1108 )
1109 1109 if n is None:
1110 1110 raise error.Abort(_(b"repository commit failed"))
1111 1111
1112 1112 if update_status:
1113 1113 self.applied.append(statusentry(n, patchname))
1114 1114
1115 1115 if patcherr:
1116 1116 self.ui.warn(
1117 1117 _(b"patch failed, rejects left in working directory\n")
1118 1118 )
1119 1119 err = 2
1120 1120 break
1121 1121
1122 1122 if fuzz and strict:
1123 1123 self.ui.warn(_(b"fuzz found when applying patch, stopping\n"))
1124 1124 err = 3
1125 1125 break
1126 1126 return (err, n)
1127 1127
1128 1128 def _cleanup(self, patches, numrevs, keep=False):
1129 1129 if not keep:
1130 1130 r = self.qrepo()
1131 1131 if r:
1132 1132 r[None].forget(patches)
1133 1133 for p in patches:
1134 1134 try:
1135 1135 os.unlink(self.join(p))
1136 1136 except FileNotFoundError:
1137 1137 pass
1138 1138
1139 1139 qfinished = []
1140 1140 if numrevs:
1141 1141 qfinished = self.applied[:numrevs]
1142 1142 del self.applied[:numrevs]
1143 1143 self.applieddirty = True
1144 1144
1145 1145 unknown = []
1146 1146
1147 1147 sortedseries = []
1148 1148 for p in patches:
1149 1149 idx = self.findseries(p)
1150 1150 if idx is None:
1151 1151 sortedseries.append((-1, p))
1152 1152 else:
1153 1153 sortedseries.append((idx, p))
1154 1154
1155 1155 sortedseries.sort(reverse=True)
1156 1156 for (i, p) in sortedseries:
1157 1157 if i != -1:
1158 1158 del self.fullseries[i]
1159 1159 else:
1160 1160 unknown.append(p)
1161 1161
1162 1162 if unknown:
1163 1163 if numrevs:
1164 1164 rev = {entry.name: entry.node for entry in qfinished}
1165 1165 for p in unknown:
1166 1166 msg = _(b'revision %s refers to unknown patches: %s\n')
1167 1167 self.ui.warn(msg % (short(rev[p]), p))
1168 1168 else:
1169 1169 msg = _(b'unknown patches: %s\n')
1170 1170 raise error.Abort(b''.join(msg % p for p in unknown))
1171 1171
1172 1172 self.parseseries()
1173 1173 self.seriesdirty = True
1174 1174 return [entry.node for entry in qfinished]
1175 1175
1176 1176 def _revpatches(self, repo, revs):
1177 1177 firstrev = repo[self.applied[0].node].rev()
1178 1178 patches = []
1179 1179 for i, rev in enumerate(revs):
1180 1180
1181 1181 if rev < firstrev:
1182 1182 raise error.Abort(_(b'revision %d is not managed') % rev)
1183 1183
1184 1184 ctx = repo[rev]
1185 1185 base = self.applied[i].node
1186 1186 if ctx.node() != base:
1187 1187 msg = _(b'cannot delete revision %d above applied patches')
1188 1188 raise error.Abort(msg % rev)
1189 1189
1190 1190 patch = self.applied[i].name
1191 1191 for fmt in (b'[mq]: %s', b'imported patch %s'):
1192 1192 if ctx.description() == fmt % patch:
1193 1193 msg = _(b'patch %s finalized without changeset message\n')
1194 1194 repo.ui.status(msg % patch)
1195 1195 break
1196 1196
1197 1197 patches.append(patch)
1198 1198 return patches
1199 1199
1200 1200 def finish(self, repo, revs):
1201 1201 # Manually trigger phase computation to ensure phasedefaults is
1202 1202 # executed before we remove the patches.
1203 1203 repo._phasecache
1204 1204 patches = self._revpatches(repo, sorted(revs))
1205 1205 qfinished = self._cleanup(patches, len(patches))
1206 1206 if qfinished and repo.ui.configbool(b'mq', b'secret'):
1207 1207 # only use this logic when the secret option is added
1208 1208 oldqbase = repo[qfinished[0]]
1209 1209 tphase = phases.newcommitphase(repo.ui)
1210 1210 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
1211 1211 with repo.transaction(b'qfinish') as tr:
1212 1212 phases.advanceboundary(repo, tr, tphase, qfinished)
1213 1213
1214 1214 def delete(self, repo, patches, opts):
1215 1215 if not patches and not opts.get(b'rev'):
1216 1216 raise error.Abort(
1217 1217 _(b'qdelete requires at least one revision or patch name')
1218 1218 )
1219 1219
1220 1220 realpatches = []
1221 1221 for patch in patches:
1222 1222 patch = self.lookup(patch, strict=True)
1223 1223 info = self.isapplied(patch)
1224 1224 if info:
1225 1225 raise error.Abort(_(b"cannot delete applied patch %s") % patch)
1226 1226 if patch not in self.series:
1227 1227 raise error.Abort(_(b"patch %s not in series file") % patch)
1228 1228 if patch not in realpatches:
1229 1229 realpatches.append(patch)
1230 1230
1231 1231 numrevs = 0
1232 1232 if opts.get(b'rev'):
1233 1233 if not self.applied:
1234 1234 raise error.Abort(_(b'no patches applied'))
1235 1235 revs = logcmdutil.revrange(repo, opts.get(b'rev'))
1236 1236 revs.sort()
1237 1237 revpatches = self._revpatches(repo, revs)
1238 1238 realpatches += revpatches
1239 1239 numrevs = len(revpatches)
1240 1240
1241 1241 self._cleanup(realpatches, numrevs, opts.get(b'keep'))
1242 1242
1243 1243 def checktoppatch(self, repo):
1244 1244 '''check that working directory is at qtip'''
1245 1245 if self.applied:
1246 1246 top = self.applied[-1].node
1247 1247 patch = self.applied[-1].name
1248 1248 if repo.dirstate.p1() != top:
1249 1249 raise error.Abort(_(b"working directory revision is not qtip"))
1250 1250 return top, patch
1251 1251 return None, None
1252 1252
1253 1253 def putsubstate2changes(self, substatestate, changes):
1254 1254 if isinstance(changes, list):
1255 1255 mar = changes[:3]
1256 1256 else:
1257 1257 mar = (changes.modified, changes.added, changes.removed)
1258 1258 if any((b'.hgsubstate' in files for files in mar)):
1259 1259 return # already listed up
1260 1260 # not yet listed up
1261 1261 if substatestate.added or not substatestate.any_tracked:
1262 1262 mar[1].append(b'.hgsubstate')
1263 1263 elif substatestate.removed:
1264 1264 mar[2].append(b'.hgsubstate')
1265 1265 else: # modified
1266 1266 mar[0].append(b'.hgsubstate')
1267 1267
1268 1268 def checklocalchanges(self, repo, force=False, refresh=True):
1269 1269 excsuffix = b''
1270 1270 if refresh:
1271 1271 excsuffix = b', qrefresh first'
1272 1272 # plain versions for i18n tool to detect them
1273 1273 _(b"local changes found, qrefresh first")
1274 1274 _(b"local changed subrepos found, qrefresh first")
1275 1275
1276 1276 s = repo.status()
1277 1277 if not force:
1278 1278 cmdutil.checkunfinished(repo)
1279 1279 if s.modified or s.added or s.removed or s.deleted:
1280 1280 _(b"local changes found") # i18n tool detection
1281 1281 raise error.Abort(_(b"local changes found" + excsuffix))
1282 1282 if checksubstate(repo):
1283 1283 _(b"local changed subrepos found") # i18n tool detection
1284 1284 raise error.Abort(
1285 1285 _(b"local changed subrepos found" + excsuffix)
1286 1286 )
1287 1287 else:
1288 1288 cmdutil.checkunfinished(repo, skipmerge=True)
1289 1289 return s
1290 1290
1291 1291 _reserved = (b'series', b'status', b'guards', b'.', b'..')
1292 1292
1293 1293 def checkreservedname(self, name):
1294 1294 if name in self._reserved:
1295 1295 raise error.Abort(
1296 1296 _(b'"%s" cannot be used as the name of a patch') % name
1297 1297 )
1298 1298 if name != name.strip():
1299 1299 # whitespace is stripped by parseseries()
1300 1300 raise error.Abort(
1301 1301 _(b'patch name cannot begin or end with whitespace')
1302 1302 )
1303 1303 for prefix in (b'.hg', b'.mq'):
1304 1304 if name.startswith(prefix):
1305 1305 raise error.Abort(
1306 1306 _(b'patch name cannot begin with "%s"') % prefix
1307 1307 )
1308 1308 for c in (b'#', b':', b'\r', b'\n'):
1309 1309 if c in name:
1310 1310 raise error.Abort(
1311 1311 _(b'%r cannot be used in the name of a patch')
1312 1312 % pycompat.bytestr(c)
1313 1313 )
1314 1314
1315 1315 def checkpatchname(self, name, force=False):
1316 1316 self.checkreservedname(name)
1317 1317 if not force and os.path.exists(self.join(name)):
1318 1318 if os.path.isdir(self.join(name)):
1319 1319 raise error.Abort(
1320 1320 _(b'"%s" already exists as a directory') % name
1321 1321 )
1322 1322 else:
1323 1323 raise error.Abort(_(b'patch "%s" already exists') % name)
1324 1324
1325 1325 def makepatchname(self, title, fallbackname):
1326 1326 """Return a suitable filename for title, adding a suffix to make
1327 1327 it unique in the existing list"""
1328 1328 namebase = re.sub(br'[\s\W_]+', b'_', title.lower()).strip(b'_')
1329 1329 namebase = namebase[:75] # avoid too long name (issue5117)
1330 1330 if namebase:
1331 1331 try:
1332 1332 self.checkreservedname(namebase)
1333 1333 except error.Abort:
1334 1334 namebase = fallbackname
1335 1335 else:
1336 1336 namebase = fallbackname
1337 1337 name = namebase
1338 1338 i = 0
1339 1339 while True:
1340 1340 if name not in self.fullseries:
1341 1341 try:
1342 1342 self.checkpatchname(name)
1343 1343 break
1344 1344 except error.Abort:
1345 1345 pass
1346 1346 i += 1
1347 1347 name = b'%s__%d' % (namebase, i)
1348 1348 return name
1349 1349
1350 1350 def checkkeepchanges(self, keepchanges, force):
1351 1351 if force and keepchanges:
1352 1352 raise error.Abort(_(b'cannot use both --force and --keep-changes'))
1353 1353
1354 1354 def new(self, repo, patchfn, *pats, **opts):
1355 1355 """options:
1356 1356 msg: a string or a no-argument function returning a string
1357 1357 """
1358 1358 opts = pycompat.byteskwargs(opts)
1359 1359 msg = opts.get(b'msg')
1360 1360 edit = opts.get(b'edit')
1361 1361 editform = opts.get(b'editform', b'mq.qnew')
1362 1362 user = opts.get(b'user')
1363 1363 date = opts.get(b'date')
1364 1364 if date:
1365 1365 date = dateutil.parsedate(date)
1366 1366 diffopts = self.diffopts({b'git': opts.get(b'git')}, plain=True)
1367 1367 if opts.get(b'checkname', True):
1368 1368 self.checkpatchname(patchfn)
1369 1369 inclsubs = checksubstate(repo)
1370 1370 if inclsubs:
1371 1371 substatestate = repo.dirstate.get_entry(b'.hgsubstate')
1372 1372 if opts.get(b'include') or opts.get(b'exclude') or pats:
1373 1373 # detect missing files in pats
1374 1374 def badfn(f, msg):
1375 1375 if f != b'.hgsubstate': # .hgsubstate is auto-created
1376 1376 raise error.Abort(b'%s: %s' % (f, msg))
1377 1377
1378 1378 match = scmutil.match(repo[None], pats, opts, badfn=badfn)
1379 1379 changes = repo.status(match=match)
1380 1380 else:
1381 1381 changes = self.checklocalchanges(repo, force=True)
1382 1382 commitfiles = list(inclsubs)
1383 1383 commitfiles.extend(changes.modified)
1384 1384 commitfiles.extend(changes.added)
1385 1385 commitfiles.extend(changes.removed)
1386 1386 match = scmutil.matchfiles(repo, commitfiles)
1387 1387 if len(repo[None].parents()) > 1:
1388 1388 raise error.Abort(_(b'cannot manage merge changesets'))
1389 1389 self.checktoppatch(repo)
1390 1390 insert = self.fullseriesend()
1391 1391 with repo.wlock():
1392 1392 try:
1393 1393 # if patch file write fails, abort early
1394 1394 p = self.opener(patchfn, b"w")
1395 1395 except IOError as e:
1396 1396 raise error.Abort(
1397 1397 _(b'cannot write patch "%s": %s')
1398 1398 % (patchfn, encoding.strtolocal(e.strerror))
1399 1399 )
1400 1400 try:
1401 1401 defaultmsg = b"[mq]: %s" % patchfn
1402 1402 editor = cmdutil.getcommiteditor(editform=editform)
1403 1403 if edit:
1404 1404
1405 1405 def finishdesc(desc):
1406 1406 if desc.rstrip():
1407 1407 return desc
1408 1408 else:
1409 1409 return defaultmsg
1410 1410
1411 1411 # i18n: this message is shown in editor with "HG: " prefix
1412 1412 extramsg = _(b'Leave message empty to use default message.')
1413 1413 editor = cmdutil.getcommiteditor(
1414 1414 finishdesc=finishdesc,
1415 1415 extramsg=extramsg,
1416 1416 editform=editform,
1417 1417 )
1418 1418 commitmsg = msg
1419 1419 else:
1420 1420 commitmsg = msg or defaultmsg
1421 1421
1422 1422 n = newcommit(
1423 1423 repo,
1424 1424 None,
1425 1425 commitmsg,
1426 1426 user,
1427 1427 date,
1428 1428 match=match,
1429 1429 force=True,
1430 1430 editor=editor,
1431 1431 )
1432 1432 if n is None:
1433 1433 raise error.Abort(_(b"repo commit failed"))
1434 1434 try:
1435 1435 self.fullseries[insert:insert] = [patchfn]
1436 1436 self.applied.append(statusentry(n, patchfn))
1437 1437 self.parseseries()
1438 1438 self.seriesdirty = True
1439 1439 self.applieddirty = True
1440 1440 nctx = repo[n]
1441 1441 ph = patchheader(self.join(patchfn), self.plainmode)
1442 1442 if user:
1443 1443 ph.setuser(user)
1444 1444 if date:
1445 1445 ph.setdate(b'%d %d' % date)
1446 1446 ph.setparent(hex(nctx.p1().node()))
1447 1447 msg = nctx.description().strip()
1448 1448 if msg == defaultmsg.strip():
1449 1449 msg = b''
1450 1450 ph.setmessage(msg)
1451 1451 p.write(bytes(ph))
1452 1452 if commitfiles:
1453 1453 parent = self.qparents(repo, n)
1454 1454 if inclsubs:
1455 1455 self.putsubstate2changes(substatestate, changes)
1456 1456 chunks = patchmod.diff(
1457 1457 repo,
1458 1458 node1=parent,
1459 1459 node2=n,
1460 1460 changes=changes,
1461 1461 opts=diffopts,
1462 1462 )
1463 1463 for chunk in chunks:
1464 1464 p.write(chunk)
1465 1465 p.close()
1466 1466 r = self.qrepo()
1467 1467 if r:
1468 1468 r[None].add([patchfn])
1469 1469 except: # re-raises
1470 1470 repo.rollback()
1471 1471 raise
1472 1472 except Exception:
1473 1473 patchpath = self.join(patchfn)
1474 1474 try:
1475 1475 os.unlink(patchpath)
1476 1476 except OSError:
1477 1477 self.ui.warn(_(b'error unlinking %s\n') % patchpath)
1478 1478 raise
1479 1479 self.removeundo(repo)
1480 1480
1481 1481 def isapplied(self, patch):
1482 1482 """returns (index, rev, patch)"""
1483 1483 for i, a in enumerate(self.applied):
1484 1484 if a.name == patch:
1485 1485 return (i, a.node, a.name)
1486 1486 return None
1487 1487
1488 1488 # if the exact patch name does not exist, we try a few
1489 1489 # variations. If strict is passed, we try only #1
1490 1490 #
1491 1491 # 1) a number (as string) to indicate an offset in the series file
1492 1492 # 2) a unique substring of the patch name was given
1493 1493 # 3) patchname[-+]num to indicate an offset in the series file
1494 1494 def lookup(self, patch, strict=False):
1495 1495 def partialname(s):
1496 1496 if s in self.series:
1497 1497 return s
1498 1498 matches = [x for x in self.series if s in x]
1499 1499 if len(matches) > 1:
1500 1500 self.ui.warn(_(b'patch name "%s" is ambiguous:\n') % s)
1501 1501 for m in matches:
1502 1502 self.ui.warn(b' %s\n' % m)
1503 1503 return None
1504 1504 if matches:
1505 1505 return matches[0]
1506 1506 if self.series and self.applied:
1507 1507 if s == b'qtip':
1508 1508 return self.series[self.seriesend(True) - 1]
1509 1509 if s == b'qbase':
1510 1510 return self.series[0]
1511 1511 return None
1512 1512
1513 1513 if patch in self.series:
1514 1514 return patch
1515 1515
1516 1516 if not os.path.isfile(self.join(patch)):
1517 1517 try:
1518 1518 sno = int(patch)
1519 1519 except (ValueError, OverflowError):
1520 1520 pass
1521 1521 else:
1522 1522 if -len(self.series) <= sno < len(self.series):
1523 1523 return self.series[sno]
1524 1524
1525 1525 if not strict:
1526 1526 res = partialname(patch)
1527 1527 if res:
1528 1528 return res
1529 1529 minus = patch.rfind(b'-')
1530 1530 if minus >= 0:
1531 1531 res = partialname(patch[:minus])
1532 1532 if res:
1533 1533 i = self.series.index(res)
1534 1534 try:
1535 1535 off = int(patch[minus + 1 :] or 1)
1536 1536 except (ValueError, OverflowError):
1537 1537 pass
1538 1538 else:
1539 1539 if i - off >= 0:
1540 1540 return self.series[i - off]
1541 1541 plus = patch.rfind(b'+')
1542 1542 if plus >= 0:
1543 1543 res = partialname(patch[:plus])
1544 1544 if res:
1545 1545 i = self.series.index(res)
1546 1546 try:
1547 1547 off = int(patch[plus + 1 :] or 1)
1548 1548 except (ValueError, OverflowError):
1549 1549 pass
1550 1550 else:
1551 1551 if i + off < len(self.series):
1552 1552 return self.series[i + off]
1553 1553 raise error.Abort(_(b"patch %s not in series") % patch)
1554 1554
1555 1555 def push(
1556 1556 self,
1557 1557 repo,
1558 1558 patch=None,
1559 1559 force=False,
1560 1560 list=False,
1561 1561 mergeq=None,
1562 1562 all=False,
1563 1563 move=False,
1564 1564 exact=False,
1565 1565 nobackup=False,
1566 1566 keepchanges=False,
1567 1567 ):
1568 1568 self.checkkeepchanges(keepchanges, force)
1569 1569 diffopts = self.diffopts()
1570 1570 with repo.wlock():
1571 1571 heads = []
1572 1572 for hs in repo.branchmap().iterheads():
1573 1573 heads.extend(hs)
1574 1574 if not heads:
1575 1575 heads = [repo.nullid]
1576 1576 if repo.dirstate.p1() not in heads and not exact:
1577 1577 self.ui.status(_(b"(working directory not at a head)\n"))
1578 1578
1579 1579 if not self.series:
1580 1580 self.ui.warn(_(b'no patches in series\n'))
1581 1581 return 0
1582 1582
1583 1583 # Suppose our series file is: A B C and the current 'top'
1584 1584 # patch is B. qpush C should be performed (moving forward)
1585 1585 # qpush B is a NOP (no change) qpush A is an error (can't
1586 1586 # go backwards with qpush)
1587 1587 if patch:
1588 1588 patch = self.lookup(patch)
1589 1589 info = self.isapplied(patch)
1590 1590 if info and info[0] >= len(self.applied) - 1:
1591 1591 self.ui.warn(
1592 1592 _(b'qpush: %s is already at the top\n') % patch
1593 1593 )
1594 1594 return 0
1595 1595
1596 1596 pushable, reason = self.pushable(patch)
1597 1597 if pushable:
1598 1598 if self.series.index(patch) < self.seriesend():
1599 1599 raise error.Abort(
1600 1600 _(b"cannot push to a previous patch: %s") % patch
1601 1601 )
1602 1602 else:
1603 1603 if reason:
1604 1604 reason = _(b'guarded by %s') % reason
1605 1605 else:
1606 1606 reason = _(b'no matching guards')
1607 1607 self.ui.warn(
1608 1608 _(b"cannot push '%s' - %s\n") % (patch, reason)
1609 1609 )
1610 1610 return 1
1611 1611 elif all:
1612 1612 patch = self.series[-1]
1613 1613 if self.isapplied(patch):
1614 1614 self.ui.warn(_(b'all patches are currently applied\n'))
1615 1615 return 0
1616 1616
1617 1617 # Following the above example, starting at 'top' of B:
1618 1618 # qpush should be performed (pushes C), but a subsequent
1619 1619 # qpush without an argument is an error (nothing to
1620 1620 # apply). This allows a loop of "...while hg qpush..." to
1621 1621 # work as it detects an error when done
1622 1622 start = self.seriesend()
1623 1623 if start == len(self.series):
1624 1624 self.ui.warn(_(b'patch series already fully applied\n'))
1625 1625 return 1
1626 1626 if not force and not keepchanges:
1627 1627 self.checklocalchanges(repo, refresh=self.applied)
1628 1628
1629 1629 if exact:
1630 1630 if keepchanges:
1631 1631 raise error.Abort(
1632 1632 _(b"cannot use --exact and --keep-changes together")
1633 1633 )
1634 1634 if move:
1635 1635 raise error.Abort(
1636 1636 _(b'cannot use --exact and --move together')
1637 1637 )
1638 1638 if self.applied:
1639 1639 raise error.Abort(
1640 1640 _(b'cannot push --exact with applied patches')
1641 1641 )
1642 1642 root = self.series[start]
1643 1643 target = patchheader(self.join(root), self.plainmode).parent
1644 1644 if not target:
1645 1645 raise error.Abort(
1646 1646 _(b"%s does not have a parent recorded") % root
1647 1647 )
1648 1648 if not repo[target] == repo[b'.']:
1649 1649 hg.update(repo, target)
1650 1650
1651 1651 if move:
1652 1652 if not patch:
1653 1653 raise error.Abort(_(b"please specify the patch to move"))
1654 1654 for fullstart, rpn in enumerate(self.fullseries):
1655 1655 # strip markers for patch guards
1656 1656 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1657 1657 break
1658 1658 for i, rpn in enumerate(self.fullseries[fullstart:]):
1659 1659 # strip markers for patch guards
1660 1660 if self.guard_re.split(rpn, 1)[0] == patch:
1661 1661 break
1662 1662 index = fullstart + i
1663 1663 assert index < len(self.fullseries)
1664 1664 fullpatch = self.fullseries[index]
1665 1665 del self.fullseries[index]
1666 1666 self.fullseries.insert(fullstart, fullpatch)
1667 1667 self.parseseries()
1668 1668 self.seriesdirty = True
1669 1669
1670 1670 self.applieddirty = True
1671 1671 if start > 0:
1672 1672 self.checktoppatch(repo)
1673 1673 if not patch:
1674 1674 patch = self.series[start]
1675 1675 end = start + 1
1676 1676 else:
1677 1677 end = self.series.index(patch, start) + 1
1678 1678
1679 1679 tobackup = set()
1680 1680 if (not nobackup and force) or keepchanges:
1681 1681 status = self.checklocalchanges(repo, force=True)
1682 1682 if keepchanges:
1683 1683 tobackup.update(
1684 1684 status.modified
1685 1685 + status.added
1686 1686 + status.removed
1687 1687 + status.deleted
1688 1688 )
1689 1689 else:
1690 1690 tobackup.update(status.modified + status.added)
1691 1691
1692 1692 s = self.series[start:end]
1693 1693 all_files = set()
1694 1694 try:
1695 1695 if mergeq:
1696 1696 ret = self.mergepatch(repo, mergeq, s, diffopts)
1697 1697 else:
1698 1698 ret = self.apply(
1699 1699 repo,
1700 1700 s,
1701 1701 list,
1702 1702 all_files=all_files,
1703 1703 tobackup=tobackup,
1704 1704 keepchanges=keepchanges,
1705 1705 )
1706 1706 except AbortNoCleanup:
1707 1707 raise
1708 1708 except: # re-raises
1709 1709 self.ui.warn(_(b'cleaning up working directory...\n'))
1710 1710 cmdutil.revert(
1711 1711 self.ui,
1712 1712 repo,
1713 1713 repo[b'.'],
1714 1714 no_backup=True,
1715 1715 )
1716 1716 # only remove unknown files that we know we touched or
1717 1717 # created while patching
1718 1718 for f in all_files:
1719 1719 if f not in repo.dirstate:
1720 1720 repo.wvfs.unlinkpath(f, ignoremissing=True)
1721 1721 self.ui.warn(_(b'done\n'))
1722 1722 raise
1723 1723
1724 1724 if not self.applied:
1725 1725 return ret[0]
1726 1726 top = self.applied[-1].name
1727 1727 if ret[0] and ret[0] > 1:
1728 1728 msg = _(b"errors during apply, please fix and qrefresh %s\n")
1729 1729 self.ui.write(msg % top)
1730 1730 else:
1731 1731 self.ui.write(_(b"now at: %s\n") % top)
1732 1732 return ret[0]
1733 1733
1734 1734 def pop(
1735 1735 self,
1736 1736 repo,
1737 1737 patch=None,
1738 1738 force=False,
1739 1739 update=True,
1740 1740 all=False,
1741 1741 nobackup=False,
1742 1742 keepchanges=False,
1743 1743 ):
1744 1744 self.checkkeepchanges(keepchanges, force)
1745 1745 with repo.wlock():
1746 1746 if patch:
1747 1747 # index, rev, patch
1748 1748 info = self.isapplied(patch)
1749 1749 if not info:
1750 1750 patch = self.lookup(patch)
1751 1751 info = self.isapplied(patch)
1752 1752 if not info:
1753 1753 raise error.Abort(_(b"patch %s is not applied") % patch)
1754 1754
1755 1755 if not self.applied:
1756 1756 # Allow qpop -a to work repeatedly,
1757 1757 # but not qpop without an argument
1758 1758 self.ui.warn(_(b"no patches applied\n"))
1759 1759 return not all
1760 1760
1761 1761 if all:
1762 1762 start = 0
1763 1763 elif patch:
1764 1764 start = info[0] + 1
1765 1765 else:
1766 1766 start = len(self.applied) - 1
1767 1767
1768 1768 if start >= len(self.applied):
1769 1769 self.ui.warn(_(b"qpop: %s is already at the top\n") % patch)
1770 1770 return
1771 1771
1772 1772 if not update:
1773 1773 parents = repo.dirstate.parents()
1774 1774 rr = [x.node for x in self.applied]
1775 1775 for p in parents:
1776 1776 if p in rr:
1777 1777 self.ui.warn(_(b"qpop: forcing dirstate update\n"))
1778 1778 update = True
1779 1779 else:
1780 1780 parents = [p.node() for p in repo[None].parents()]
1781 1781 update = any(
1782 1782 entry.node in parents for entry in self.applied[start:]
1783 1783 )
1784 1784
1785 1785 tobackup = set()
1786 1786 if update:
1787 1787 s = self.checklocalchanges(repo, force=force or keepchanges)
1788 1788 if force:
1789 1789 if not nobackup:
1790 1790 tobackup.update(s.modified + s.added)
1791 1791 elif keepchanges:
1792 1792 tobackup.update(
1793 1793 s.modified + s.added + s.removed + s.deleted
1794 1794 )
1795 1795
1796 1796 self.applieddirty = True
1797 1797 end = len(self.applied)
1798 1798 rev = self.applied[start].node
1799 1799
1800 1800 try:
1801 1801 heads = repo.changelog.heads(rev)
1802 1802 except error.LookupError:
1803 1803 node = short(rev)
1804 1804 raise error.Abort(_(b'trying to pop unknown node %s') % node)
1805 1805
1806 1806 if heads != [self.applied[-1].node]:
1807 1807 raise error.Abort(
1808 1808 _(
1809 1809 b"popping would remove a revision not "
1810 1810 b"managed by this patch queue"
1811 1811 )
1812 1812 )
1813 1813 if not repo[self.applied[-1].node].mutable():
1814 1814 raise error.Abort(
1815 1815 _(b"popping would remove a public revision"),
1816 1816 hint=_(b"see 'hg help phases' for details"),
1817 1817 )
1818 1818
1819 1819 # we know there are no local changes, so we can make a simplified
1820 1820 # form of hg.update.
1821 1821 if update:
1822 1822 qp = self.qparents(repo, rev)
1823 1823 ctx = repo[qp]
1824 1824 st = repo.status(qp, b'.')
1825 1825 m, a, r, d = st.modified, st.added, st.removed, st.deleted
1826 1826 if d:
1827 1827 raise error.Abort(_(b"deletions found between repo revs"))
1828 1828
1829 1829 tobackup = set(a + m + r) & tobackup
1830 1830 if keepchanges and tobackup:
1831 1831 raise error.Abort(_(b"local changes found, qrefresh first"))
1832 1832 self.backup(repo, tobackup)
1833 1833 with repo.dirstate.parentchange():
1834 1834 for f in a:
1835 1835 repo.wvfs.unlinkpath(f, ignoremissing=True)
1836 1836 repo.dirstate.update_file(
1837 1837 f, p1_tracked=False, wc_tracked=False
1838 1838 )
1839 1839 for f in m + r:
1840 1840 fctx = ctx[f]
1841 1841 repo.wwrite(f, fctx.data(), fctx.flags())
1842 1842 repo.dirstate.update_file(
1843 1843 f, p1_tracked=True, wc_tracked=True
1844 1844 )
1845 1845 repo.setparents(qp, repo.nullid)
1846 1846 for patch in reversed(self.applied[start:end]):
1847 1847 self.ui.status(_(b"popping %s\n") % patch.name)
1848 1848 del self.applied[start:end]
1849 1849 strip(self.ui, repo, [rev], update=False, backup=False)
1850 1850 for s, state in repo[b'.'].substate.items():
1851 1851 repo[b'.'].sub(s).get(state)
1852 1852 if self.applied:
1853 1853 self.ui.write(_(b"now at: %s\n") % self.applied[-1].name)
1854 1854 else:
1855 1855 self.ui.write(_(b"patch queue now empty\n"))
1856 1856
1857 1857 def diff(self, repo, pats, opts):
1858 1858 top, patch = self.checktoppatch(repo)
1859 1859 if not top:
1860 1860 self.ui.write(_(b"no patches applied\n"))
1861 1861 return
1862 1862 qp = self.qparents(repo, top)
1863 1863 if opts.get(b'reverse'):
1864 1864 node1, node2 = None, qp
1865 1865 else:
1866 1866 node1, node2 = qp, None
1867 1867 diffopts = self.diffopts(opts, patch)
1868 1868 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1869 1869
1870 1870 def refresh(self, repo, pats=None, **opts):
1871 1871 opts = pycompat.byteskwargs(opts)
1872 1872 if not self.applied:
1873 1873 self.ui.write(_(b"no patches applied\n"))
1874 1874 return 1
1875 1875 msg = opts.get(b'msg', b'').rstrip()
1876 1876 edit = opts.get(b'edit')
1877 1877 editform = opts.get(b'editform', b'mq.qrefresh')
1878 1878 newuser = opts.get(b'user')
1879 1879 newdate = opts.get(b'date')
1880 1880 if newdate:
1881 1881 newdate = b'%d %d' % dateutil.parsedate(newdate)
1882 1882 wlock = repo.wlock()
1883 1883
1884 1884 try:
1885 1885 self.checktoppatch(repo)
1886 1886 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1887 1887 if repo.changelog.heads(top) != [top]:
1888 1888 raise error.Abort(
1889 1889 _(b"cannot qrefresh a revision with children")
1890 1890 )
1891 1891 if not repo[top].mutable():
1892 1892 raise error.Abort(
1893 1893 _(b"cannot qrefresh public revision"),
1894 1894 hint=_(b"see 'hg help phases' for details"),
1895 1895 )
1896 1896
1897 1897 cparents = repo.changelog.parents(top)
1898 1898 patchparent = self.qparents(repo, top)
1899 1899
1900 1900 inclsubs = checksubstate(repo, patchparent)
1901 1901 if inclsubs:
1902 1902 substatestate = repo.dirstate.get_entry(b'.hgsubstate')
1903 1903
1904 1904 ph = patchheader(self.join(patchfn), self.plainmode)
1905 1905 diffopts = self.diffopts(
1906 1906 {b'git': opts.get(b'git')}, patchfn, plain=True
1907 1907 )
1908 1908 if newuser:
1909 1909 ph.setuser(newuser)
1910 1910 if newdate:
1911 1911 ph.setdate(newdate)
1912 1912 ph.setparent(hex(patchparent))
1913 1913
1914 1914 # only commit new patch when write is complete
1915 1915 patchf = self.opener(patchfn, b'w', atomictemp=True)
1916 1916
1917 1917 # update the dirstate in place, strip off the qtip commit
1918 1918 # and then commit.
1919 1919 #
1920 1920 # this should really read:
1921 1921 # st = repo.status(top, patchparent)
1922 1922 # but we do it backwards to take advantage of manifest/changelog
1923 1923 # caching against the next repo.status call
1924 1924 st = repo.status(patchparent, top)
1925 1925 mm, aa, dd = st.modified, st.added, st.removed
1926 1926 ctx = repo[top]
1927 1927 aaa = aa[:]
1928 1928 match1 = scmutil.match(repo[None], pats, opts)
1929 1929 # in short mode, we only diff the files included in the
1930 1930 # patch already plus specified files
1931 1931 if opts.get(b'short'):
1932 1932 # if amending a patch, we start with existing
1933 1933 # files plus specified files - unfiltered
1934 1934 match = scmutil.matchfiles(repo, mm + aa + dd + match1.files())
1935 1935 # filter with include/exclude options
1936 1936 match1 = scmutil.match(repo[None], opts=opts)
1937 1937 else:
1938 1938 match = scmutil.matchall(repo)
1939 1939 stb = repo.status(match=match)
1940 1940 m, a, r, d = stb.modified, stb.added, stb.removed, stb.deleted
1941 1941 mm = set(mm)
1942 1942 aa = set(aa)
1943 1943 dd = set(dd)
1944 1944
1945 1945 # we might end up with files that were added between
1946 1946 # qtip and the dirstate parent, but then changed in the
1947 1947 # local dirstate. in this case, we want them to only
1948 1948 # show up in the added section
1949 1949 for x in m:
1950 1950 if x not in aa:
1951 1951 mm.add(x)
1952 1952 # we might end up with files added by the local dirstate that
1953 1953 # were deleted by the patch. In this case, they should only
1954 1954 # show up in the changed section.
1955 1955 for x in a:
1956 1956 if x in dd:
1957 1957 dd.remove(x)
1958 1958 mm.add(x)
1959 1959 else:
1960 1960 aa.add(x)
1961 1961 # make sure any files deleted in the local dirstate
1962 1962 # are not in the add or change column of the patch
1963 1963 forget = []
1964 1964 for x in d + r:
1965 1965 if x in aa:
1966 1966 aa.remove(x)
1967 1967 forget.append(x)
1968 1968 continue
1969 1969 else:
1970 1970 mm.discard(x)
1971 1971 dd.add(x)
1972 1972
1973 1973 m = list(mm)
1974 1974 r = list(dd)
1975 1975 a = list(aa)
1976 1976
1977 1977 # create 'match' that includes the files to be recommitted.
1978 1978 # apply match1 via repo.status to ensure correct case handling.
1979 1979 st = repo.status(patchparent, match=match1)
1980 1980 cm, ca, cr, cd = st.modified, st.added, st.removed, st.deleted
1981 1981 allmatches = set(cm + ca + cr + cd)
1982 1982 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1983 1983
1984 1984 files = set(inclsubs)
1985 1985 for x in refreshchanges:
1986 1986 files.update(x)
1987 1987 match = scmutil.matchfiles(repo, files)
1988 1988
1989 1989 bmlist = repo[top].bookmarks()
1990 1990
1991 1991 with repo.dirstate.parentchange():
1992 1992 # XXX do we actually need the dirstateguard
1993 1993 dsguard = None
1994 1994 try:
1995 1995 dsguard = dirstateguard.dirstateguard(repo, b'mq.refresh')
1996 1996 if diffopts.git or diffopts.upgrade:
1997 1997 copies = {}
1998 1998 for dst in a:
1999 1999 src = repo.dirstate.copied(dst)
2000 2000 # during qfold, the source file for copies may
2001 2001 # be removed. Treat this as a simple add.
2002 2002 if src is not None and src in repo.dirstate:
2003 2003 copies.setdefault(src, []).append(dst)
2004 2004 repo.dirstate.update_file(
2005 2005 dst, p1_tracked=False, wc_tracked=True
2006 2006 )
2007 2007 # remember the copies between patchparent and qtip
2008 2008 for dst in aaa:
2009 2009 src = ctx[dst].copysource()
2010 2010 if src:
2011 2011 copies.setdefault(src, []).extend(
2012 2012 copies.get(dst, [])
2013 2013 )
2014 2014 if dst in a:
2015 2015 copies[src].append(dst)
2016 2016 # we can't copy a file created by the patch itself
2017 2017 if dst in copies:
2018 2018 del copies[dst]
2019 2019 for src, dsts in copies.items():
2020 2020 for dst in dsts:
2021 2021 repo.dirstate.copy(src, dst)
2022 2022 else:
2023 2023 for dst in a:
2024 2024 repo.dirstate.update_file(
2025 2025 dst, p1_tracked=False, wc_tracked=True
2026 2026 )
2027 2027 # Drop useless copy information
2028 2028 for f in list(repo.dirstate.copies()):
2029 2029 repo.dirstate.copy(None, f)
2030 2030 for f in r:
2031 2031 repo.dirstate.update_file_p1(f, p1_tracked=True)
2032 2032 # if the patch excludes a modified file, mark that
2033 2033 # file with mtime=0 so status can see it.
2034 2034 mm = []
2035 2035 for i in range(len(m) - 1, -1, -1):
2036 2036 if not match1(m[i]):
2037 2037 mm.append(m[i])
2038 2038 del m[i]
2039 2039 for f in m:
2040 2040 repo.dirstate.update_file_p1(f, p1_tracked=True)
2041 2041 for f in mm:
2042 2042 repo.dirstate.update_file_p1(f, p1_tracked=True)
2043 2043 for f in forget:
2044 2044 repo.dirstate.update_file_p1(f, p1_tracked=False)
2045 2045
2046 2046 user = ph.user or ctx.user()
2047 2047
2048 2048 oldphase = repo[top].phase()
2049 2049
2050 2050 # assumes strip can roll itself back if interrupted
2051 2051 repo.setparents(*cparents)
2052 2052 self.applied.pop()
2053 2053 self.applieddirty = True
2054 2054 strip(self.ui, repo, [top], update=False, backup=False)
2055 2055 dsguard.close()
2056 2056 finally:
2057 2057 release(dsguard)
2058 2058
2059 2059 try:
2060 2060 # might be nice to attempt to roll back strip after this
2061 2061
2062 2062 defaultmsg = b"[mq]: %s" % patchfn
2063 2063 editor = cmdutil.getcommiteditor(editform=editform)
2064 2064 if edit:
2065 2065
2066 2066 def finishdesc(desc):
2067 2067 if desc.rstrip():
2068 2068 ph.setmessage(desc)
2069 2069 return desc
2070 2070 return defaultmsg
2071 2071
2072 2072 # i18n: this message is shown in editor with "HG: " prefix
2073 2073 extramsg = _(b'Leave message empty to use default message.')
2074 2074 editor = cmdutil.getcommiteditor(
2075 2075 finishdesc=finishdesc,
2076 2076 extramsg=extramsg,
2077 2077 editform=editform,
2078 2078 )
2079 2079 message = msg or b"\n".join(ph.message)
2080 2080 elif not msg:
2081 2081 if not ph.message:
2082 2082 message = defaultmsg
2083 2083 else:
2084 2084 message = b"\n".join(ph.message)
2085 2085 else:
2086 2086 message = msg
2087 2087 ph.setmessage(msg)
2088 2088
2089 2089 # Ensure we create a new changeset in the same phase than
2090 2090 # the old one.
2091 2091 lock = tr = None
2092 2092 try:
2093 2093 lock = repo.lock()
2094 2094 tr = repo.transaction(b'mq')
2095 2095 n = newcommit(
2096 2096 repo,
2097 2097 oldphase,
2098 2098 message,
2099 2099 user,
2100 2100 ph.date,
2101 2101 match=match,
2102 2102 force=True,
2103 2103 editor=editor,
2104 2104 )
2105 2105 # only write patch after a successful commit
2106 2106 c = [list(x) for x in refreshchanges]
2107 2107 if inclsubs:
2108 2108 self.putsubstate2changes(substatestate, c)
2109 2109 chunks = patchmod.diff(
2110 2110 repo, patchparent, changes=c, opts=diffopts
2111 2111 )
2112 2112 comments = bytes(ph)
2113 2113 if comments:
2114 2114 patchf.write(comments)
2115 2115 for chunk in chunks:
2116 2116 patchf.write(chunk)
2117 2117 patchf.close()
2118 2118
2119 2119 marks = repo._bookmarks
2120 2120 marks.applychanges(repo, tr, [(bm, n) for bm in bmlist])
2121 2121 tr.close()
2122 2122
2123 2123 self.applied.append(statusentry(n, patchfn))
2124 2124 finally:
2125 2125 lockmod.release(tr, lock)
2126 2126 except: # re-raises
2127 2127 ctx = repo[cparents[0]]
2128 2128 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
2129 2129 self.savedirty()
2130 2130 self.ui.warn(
2131 2131 _(
2132 2132 b'qrefresh interrupted while patch was popped! '
2133 2133 b'(revert --all, qpush to recover)\n'
2134 2134 )
2135 2135 )
2136 2136 raise
2137 2137 finally:
2138 2138 wlock.release()
2139 2139 self.removeundo(repo)
2140 2140
2141 2141 def init(self, repo, create=False):
2142 2142 if not create and os.path.isdir(self.path):
2143 2143 raise error.Abort(_(b"patch queue directory already exists"))
2144 2144 try:
2145 2145 os.mkdir(self.path)
2146 2146 except FileExistsError:
2147 2147 if not create:
2148 2148 raise
2149 2149 if create:
2150 2150 return self.qrepo(create=True)
2151 2151
2152 2152 def unapplied(self, repo, patch=None):
2153 2153 if patch and patch not in self.series:
2154 2154 raise error.Abort(_(b"patch %s is not in series file") % patch)
2155 2155 if not patch:
2156 2156 start = self.seriesend()
2157 2157 else:
2158 2158 start = self.series.index(patch) + 1
2159 2159 unapplied = []
2160 2160 for i in range(start, len(self.series)):
2161 2161 pushable, reason = self.pushable(i)
2162 2162 if pushable:
2163 2163 unapplied.append((i, self.series[i]))
2164 2164 self.explainpushable(i)
2165 2165 return unapplied
2166 2166
2167 2167 def qseries(
2168 2168 self,
2169 2169 repo,
2170 2170 missing=None,
2171 2171 start=0,
2172 2172 length=None,
2173 2173 status=None,
2174 2174 summary=False,
2175 2175 ):
2176 2176 def displayname(pfx, patchname, state):
2177 2177 if pfx:
2178 2178 self.ui.write(pfx)
2179 2179 if summary:
2180 2180 ph = patchheader(self.join(patchname), self.plainmode)
2181 2181 if ph.message:
2182 2182 msg = ph.message[0]
2183 2183 else:
2184 2184 msg = b''
2185 2185
2186 2186 if self.ui.formatted():
2187 2187 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
2188 2188 if width > 0:
2189 2189 msg = stringutil.ellipsis(msg, width)
2190 2190 else:
2191 2191 msg = b''
2192 2192 self.ui.write(patchname, label=b'qseries.' + state)
2193 2193 self.ui.write(b': ')
2194 2194 self.ui.write(msg, label=b'qseries.message.' + state)
2195 2195 else:
2196 2196 self.ui.write(patchname, label=b'qseries.' + state)
2197 2197 self.ui.write(b'\n')
2198 2198
2199 2199 applied = {p.name for p in self.applied}
2200 2200 if length is None:
2201 2201 length = len(self.series) - start
2202 2202 if not missing:
2203 2203 if self.ui.verbose:
2204 2204 idxwidth = len(b"%d" % (start + length - 1))
2205 2205 for i in range(start, start + length):
2206 2206 patch = self.series[i]
2207 2207 if patch in applied:
2208 2208 char, state = b'A', b'applied'
2209 2209 elif self.pushable(i)[0]:
2210 2210 char, state = b'U', b'unapplied'
2211 2211 else:
2212 2212 char, state = b'G', b'guarded'
2213 2213 pfx = b''
2214 2214 if self.ui.verbose:
2215 2215 pfx = b'%*d %s ' % (idxwidth, i, char)
2216 2216 elif status and status != char:
2217 2217 continue
2218 2218 displayname(pfx, patch, state)
2219 2219 else:
2220 2220 msng_list = []
2221 2221 for root, dirs, files in os.walk(self.path):
2222 2222 d = root[len(self.path) + 1 :]
2223 2223 for f in files:
2224 2224 fl = os.path.join(d, f)
2225 2225 if (
2226 2226 fl not in self.series
2227 2227 and fl
2228 2228 not in (
2229 2229 self.statuspath,
2230 2230 self.seriespath,
2231 2231 self.guardspath,
2232 2232 )
2233 2233 and not fl.startswith(b'.')
2234 2234 ):
2235 2235 msng_list.append(fl)
2236 2236 for x in sorted(msng_list):
2237 2237 pfx = self.ui.verbose and b'D ' or b''
2238 2238 displayname(pfx, x, b'missing')
2239 2239
2240 2240 def issaveline(self, l):
2241 2241 if l.name == b'.hg.patches.save.line':
2242 2242 return True
2243 2243
2244 2244 def qrepo(self, create=False):
2245 2245 ui = self.baseui.copy()
2246 2246 # copy back attributes set by ui.pager()
2247 2247 if self.ui.pageractive and not ui.pageractive:
2248 2248 ui.pageractive = self.ui.pageractive
2249 2249 # internal config: ui.formatted
2250 2250 ui.setconfig(
2251 2251 b'ui',
2252 2252 b'formatted',
2253 2253 self.ui.config(b'ui', b'formatted'),
2254 2254 b'mqpager',
2255 2255 )
2256 2256 ui.setconfig(
2257 2257 b'ui',
2258 2258 b'interactive',
2259 2259 self.ui.config(b'ui', b'interactive'),
2260 2260 b'mqpager',
2261 2261 )
2262 2262 if create or os.path.isdir(self.join(b".hg")):
2263 2263 return hg.repository(ui, path=self.path, create=create)
2264 2264
2265 2265 def restore(self, repo, rev, delete=None, qupdate=None):
2266 2266 desc = repo[rev].description().strip()
2267 2267 lines = desc.splitlines()
2268 2268 datastart = None
2269 2269 series = []
2270 2270 applied = []
2271 2271 qpp = None
2272 2272 for i, line in enumerate(lines):
2273 2273 if line == b'Patch Data:':
2274 2274 datastart = i + 1
2275 2275 elif line.startswith(b'Dirstate:'):
2276 2276 l = line.rstrip()
2277 2277 l = l[10:].split(b' ')
2278 2278 qpp = [bin(x) for x in l]
2279 2279 elif datastart is not None:
2280 2280 l = line.rstrip()
2281 2281 n, name = l.split(b':', 1)
2282 2282 if n:
2283 2283 applied.append(statusentry(bin(n), name))
2284 2284 else:
2285 2285 series.append(l)
2286 2286 if datastart is None:
2287 2287 self.ui.warn(_(b"no saved patch data found\n"))
2288 2288 return 1
2289 2289 self.ui.warn(_(b"restoring status: %s\n") % lines[0])
2290 2290 self.fullseries = series
2291 2291 self.applied = applied
2292 2292 self.parseseries()
2293 2293 self.seriesdirty = True
2294 2294 self.applieddirty = True
2295 2295 heads = repo.changelog.heads()
2296 2296 if delete:
2297 2297 if rev not in heads:
2298 2298 self.ui.warn(_(b"save entry has children, leaving it alone\n"))
2299 2299 else:
2300 2300 self.ui.warn(_(b"removing save entry %s\n") % short(rev))
2301 2301 pp = repo.dirstate.parents()
2302 2302 if rev in pp:
2303 2303 update = True
2304 2304 else:
2305 2305 update = False
2306 2306 strip(self.ui, repo, [rev], update=update, backup=False)
2307 2307 if qpp:
2308 2308 self.ui.warn(
2309 2309 _(b"saved queue repository parents: %s %s\n")
2310 2310 % (short(qpp[0]), short(qpp[1]))
2311 2311 )
2312 2312 if qupdate:
2313 2313 self.ui.status(_(b"updating queue directory\n"))
2314 2314 r = self.qrepo()
2315 2315 if not r:
2316 2316 self.ui.warn(_(b"unable to load queue repository\n"))
2317 2317 return 1
2318 2318 hg.clean(r, qpp[0])
2319 2319
2320 2320 def save(self, repo, msg=None):
2321 2321 if not self.applied:
2322 2322 self.ui.warn(_(b"save: no patches applied, exiting\n"))
2323 2323 return 1
2324 2324 if self.issaveline(self.applied[-1]):
2325 2325 self.ui.warn(_(b"status is already saved\n"))
2326 2326 return 1
2327 2327
2328 2328 if not msg:
2329 2329 msg = _(b"hg patches saved state")
2330 2330 else:
2331 2331 msg = b"hg patches: " + msg.rstrip(b'\r\n')
2332 2332 r = self.qrepo()
2333 2333 if r:
2334 2334 pp = r.dirstate.parents()
2335 2335 msg += b"\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
2336 2336 msg += b"\n\nPatch Data:\n"
2337 2337 msg += b''.join(b'%s\n' % x for x in self.applied)
2338 2338 msg += b''.join(b':%s\n' % x for x in self.fullseries)
2339 2339 n = repo.commit(msg, force=True)
2340 2340 if not n:
2341 2341 self.ui.warn(_(b"repo commit failed\n"))
2342 2342 return 1
2343 2343 self.applied.append(statusentry(n, b'.hg.patches.save.line'))
2344 2344 self.applieddirty = True
2345 2345 self.removeundo(repo)
2346 2346
2347 2347 def fullseriesend(self):
2348 2348 if self.applied:
2349 2349 p = self.applied[-1].name
2350 2350 end = self.findseries(p)
2351 2351 if end is None:
2352 2352 return len(self.fullseries)
2353 2353 return end + 1
2354 2354 return 0
2355 2355
2356 2356 def seriesend(self, all_patches=False):
2357 2357 """If all_patches is False, return the index of the next pushable patch
2358 2358 in the series, or the series length. If all_patches is True, return the
2359 2359 index of the first patch past the last applied one.
2360 2360 """
2361 2361 end = 0
2362 2362
2363 2363 def nextpatch(start):
2364 2364 if all_patches or start >= len(self.series):
2365 2365 return start
2366 2366 for i in range(start, len(self.series)):
2367 2367 p, reason = self.pushable(i)
2368 2368 if p:
2369 2369 return i
2370 2370 self.explainpushable(i)
2371 2371 return len(self.series)
2372 2372
2373 2373 if self.applied:
2374 2374 p = self.applied[-1].name
2375 2375 try:
2376 2376 end = self.series.index(p)
2377 2377 except ValueError:
2378 2378 return 0
2379 2379 return nextpatch(end + 1)
2380 2380 return nextpatch(end)
2381 2381
2382 2382 def appliedname(self, index):
2383 2383 pname = self.applied[index].name
2384 2384 if not self.ui.verbose:
2385 2385 p = pname
2386 2386 else:
2387 2387 p = (b"%d" % self.series.index(pname)) + b" " + pname
2388 2388 return p
2389 2389
2390 2390 def qimport(
2391 2391 self,
2392 2392 repo,
2393 2393 files,
2394 2394 patchname=None,
2395 2395 rev=None,
2396 2396 existing=None,
2397 2397 force=None,
2398 2398 git=False,
2399 2399 ):
2400 2400 def checkseries(patchname):
2401 2401 if patchname in self.series:
2402 2402 raise error.Abort(
2403 2403 _(b'patch %s is already in the series file') % patchname
2404 2404 )
2405 2405
2406 2406 if rev:
2407 2407 if files:
2408 2408 raise error.Abort(
2409 2409 _(b'option "-r" not valid when importing files')
2410 2410 )
2411 2411 rev = logcmdutil.revrange(repo, rev)
2412 2412 rev.sort(reverse=True)
2413 2413 elif not files:
2414 2414 raise error.Abort(_(b'no files or revisions specified'))
2415 2415 if (len(files) > 1 or len(rev) > 1) and patchname:
2416 2416 raise error.Abort(
2417 2417 _(b'option "-n" not valid when importing multiple patches')
2418 2418 )
2419 2419 imported = []
2420 2420 if rev:
2421 2421 # If mq patches are applied, we can only import revisions
2422 2422 # that form a linear path to qbase.
2423 2423 # Otherwise, they should form a linear path to a head.
2424 2424 heads = repo.changelog.heads(repo.changelog.node(rev.first()))
2425 2425 if len(heads) > 1:
2426 2426 raise error.Abort(
2427 2427 _(b'revision %d is the root of more than one branch')
2428 2428 % rev.last()
2429 2429 )
2430 2430 if self.applied:
2431 2431 base = repo.changelog.node(rev.first())
2432 2432 if base in [n.node for n in self.applied]:
2433 2433 raise error.Abort(
2434 2434 _(b'revision %d is already managed') % rev.first()
2435 2435 )
2436 2436 if heads != [self.applied[-1].node]:
2437 2437 raise error.Abort(
2438 2438 _(b'revision %d is not the parent of the queue')
2439 2439 % rev.first()
2440 2440 )
2441 2441 base = repo.changelog.rev(self.applied[0].node)
2442 2442 lastparent = repo.changelog.parentrevs(base)[0]
2443 2443 else:
2444 2444 if heads != [repo.changelog.node(rev.first())]:
2445 2445 raise error.Abort(
2446 2446 _(b'revision %d has unmanaged children') % rev.first()
2447 2447 )
2448 2448 lastparent = None
2449 2449
2450 2450 diffopts = self.diffopts({b'git': git})
2451 2451 with repo.transaction(b'qimport') as tr:
2452 2452 for r in rev:
2453 2453 if not repo[r].mutable():
2454 2454 raise error.Abort(
2455 2455 _(b'revision %d is not mutable') % r,
2456 2456 hint=_(b"see 'hg help phases' " b'for details'),
2457 2457 )
2458 2458 p1, p2 = repo.changelog.parentrevs(r)
2459 2459 n = repo.changelog.node(r)
2460 2460 if p2 != nullrev:
2461 2461 raise error.Abort(
2462 2462 _(b'cannot import merge revision %d') % r
2463 2463 )
2464 2464 if lastparent and lastparent != r:
2465 2465 raise error.Abort(
2466 2466 _(b'revision %d is not the parent of %d')
2467 2467 % (r, lastparent)
2468 2468 )
2469 2469 lastparent = p1
2470 2470
2471 2471 if not patchname:
2472 2472 patchname = self.makepatchname(
2473 2473 repo[r].description().split(b'\n', 1)[0],
2474 2474 b'%d.diff' % r,
2475 2475 )
2476 2476 checkseries(patchname)
2477 2477 self.checkpatchname(patchname, force)
2478 2478 self.fullseries.insert(0, patchname)
2479 2479
2480 2480 with self.opener(patchname, b"w") as fp:
2481 2481 cmdutil.exportfile(repo, [n], fp, opts=diffopts)
2482 2482
2483 2483 se = statusentry(n, patchname)
2484 2484 self.applied.insert(0, se)
2485 2485
2486 2486 self.added.append(patchname)
2487 2487 imported.append(patchname)
2488 2488 patchname = None
2489 2489 if rev and repo.ui.configbool(b'mq', b'secret'):
2490 2490 # if we added anything with --rev, move the secret root
2491 2491 phases.retractboundary(repo, tr, phases.secret, [n])
2492 2492 self.parseseries()
2493 2493 self.applieddirty = True
2494 2494 self.seriesdirty = True
2495 2495
2496 2496 for i, filename in enumerate(files):
2497 2497 if existing:
2498 2498 if filename == b'-':
2499 2499 raise error.Abort(
2500 2500 _(b'-e is incompatible with import from -')
2501 2501 )
2502 2502 filename = normname(filename)
2503 2503 self.checkreservedname(filename)
2504 2504 if urlutil.url(filename).islocal():
2505 2505 originpath = self.join(filename)
2506 2506 if not os.path.isfile(originpath):
2507 2507 raise error.Abort(
2508 2508 _(b"patch %s does not exist") % filename
2509 2509 )
2510 2510
2511 2511 if patchname:
2512 2512 self.checkpatchname(patchname, force)
2513 2513
2514 2514 self.ui.write(
2515 2515 _(b'renaming %s to %s\n') % (filename, patchname)
2516 2516 )
2517 2517 util.rename(originpath, self.join(patchname))
2518 2518 else:
2519 2519 patchname = filename
2520 2520
2521 2521 else:
2522 2522 if filename == b'-' and not patchname:
2523 2523 raise error.Abort(
2524 2524 _(b'need --name to import a patch from -')
2525 2525 )
2526 2526 elif not patchname:
2527 2527 patchname = normname(
2528 2528 os.path.basename(filename.rstrip(b'/'))
2529 2529 )
2530 2530 self.checkpatchname(patchname, force)
2531 2531 try:
2532 2532 if filename == b'-':
2533 2533 text = self.ui.fin.read()
2534 2534 else:
2535 2535 fp = hg.openpath(self.ui, filename)
2536 2536 text = fp.read()
2537 2537 fp.close()
2538 2538 except (OSError, IOError):
2539 2539 raise error.Abort(_(b"unable to read file %s") % filename)
2540 2540 patchf = self.opener(patchname, b"w")
2541 2541 patchf.write(text)
2542 2542 patchf.close()
2543 2543 if not force:
2544 2544 checkseries(patchname)
2545 2545 if patchname not in self.series:
2546 2546 index = self.fullseriesend() + i
2547 2547 self.fullseries[index:index] = [patchname]
2548 2548 self.parseseries()
2549 2549 self.seriesdirty = True
2550 2550 self.ui.warn(_(b"adding %s to series file\n") % patchname)
2551 2551 self.added.append(patchname)
2552 2552 imported.append(patchname)
2553 2553 patchname = None
2554 2554
2555 2555 self.removeundo(repo)
2556 2556 return imported
2557 2557
2558 2558
2559 2559 def fixkeepchangesopts(ui, opts):
2560 2560 if (
2561 2561 not ui.configbool(b'mq', b'keepchanges')
2562 2562 or opts.get(b'force')
2563 2563 or opts.get(b'exact')
2564 2564 ):
2565 2565 return opts
2566 2566 opts = dict(opts)
2567 2567 opts[b'keep_changes'] = True
2568 2568 return opts
2569 2569
2570 2570
2571 2571 @command(
2572 2572 b"qdelete|qremove|qrm",
2573 2573 [
2574 2574 (b'k', b'keep', None, _(b'keep patch file')),
2575 2575 (
2576 2576 b'r',
2577 2577 b'rev',
2578 2578 [],
2579 2579 _(b'stop managing a revision (DEPRECATED)'),
2580 2580 _(b'REV'),
2581 2581 ),
2582 2582 ],
2583 2583 _(b'hg qdelete [-k] [PATCH]...'),
2584 2584 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2585 2585 )
2586 2586 def delete(ui, repo, *patches, **opts):
2587 2587 """remove patches from queue
2588 2588
2589 2589 The patches must not be applied, and at least one patch is required. Exact
2590 2590 patch identifiers must be given. With -k/--keep, the patch files are
2591 2591 preserved in the patch directory.
2592 2592
2593 2593 To stop managing a patch and move it into permanent history,
2594 2594 use the :hg:`qfinish` command."""
2595 2595 q = repo.mq
2596 2596 q.delete(repo, patches, pycompat.byteskwargs(opts))
2597 2597 q.savedirty()
2598 2598 return 0
2599 2599
2600 2600
2601 2601 @command(
2602 2602 b"qapplied",
2603 2603 [(b'1', b'last', None, _(b'show only the preceding applied patch'))]
2604 2604 + seriesopts,
2605 2605 _(b'hg qapplied [-1] [-s] [PATCH]'),
2606 2606 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2607 2607 )
2608 2608 def applied(ui, repo, patch=None, **opts):
2609 2609 """print the patches already applied
2610 2610
2611 2611 Returns 0 on success."""
2612 2612
2613 2613 q = repo.mq
2614 2614 opts = pycompat.byteskwargs(opts)
2615 2615
2616 2616 if patch:
2617 2617 if patch not in q.series:
2618 2618 raise error.Abort(_(b"patch %s is not in series file") % patch)
2619 2619 end = q.series.index(patch) + 1
2620 2620 else:
2621 2621 end = q.seriesend(True)
2622 2622
2623 2623 if opts.get(b'last') and not end:
2624 2624 ui.write(_(b"no patches applied\n"))
2625 2625 return 1
2626 2626 elif opts.get(b'last') and end == 1:
2627 2627 ui.write(_(b"only one patch applied\n"))
2628 2628 return 1
2629 2629 elif opts.get(b'last'):
2630 2630 start = end - 2
2631 2631 end = 1
2632 2632 else:
2633 2633 start = 0
2634 2634
2635 2635 q.qseries(
2636 2636 repo, length=end, start=start, status=b'A', summary=opts.get(b'summary')
2637 2637 )
2638 2638
2639 2639
2640 2640 @command(
2641 2641 b"qunapplied",
2642 2642 [(b'1', b'first', None, _(b'show only the first patch'))] + seriesopts,
2643 2643 _(b'hg qunapplied [-1] [-s] [PATCH]'),
2644 2644 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2645 2645 )
2646 2646 def unapplied(ui, repo, patch=None, **opts):
2647 2647 """print the patches not yet applied
2648 2648
2649 2649 Returns 0 on success."""
2650 2650
2651 2651 q = repo.mq
2652 2652 opts = pycompat.byteskwargs(opts)
2653 2653 if patch:
2654 2654 if patch not in q.series:
2655 2655 raise error.Abort(_(b"patch %s is not in series file") % patch)
2656 2656 start = q.series.index(patch) + 1
2657 2657 else:
2658 2658 start = q.seriesend(True)
2659 2659
2660 2660 if start == len(q.series) and opts.get(b'first'):
2661 2661 ui.write(_(b"all patches applied\n"))
2662 2662 return 1
2663 2663
2664 2664 if opts.get(b'first'):
2665 2665 length = 1
2666 2666 else:
2667 2667 length = None
2668 2668 q.qseries(
2669 2669 repo,
2670 2670 start=start,
2671 2671 length=length,
2672 2672 status=b'U',
2673 2673 summary=opts.get(b'summary'),
2674 2674 )
2675 2675
2676 2676
2677 2677 @command(
2678 2678 b"qimport",
2679 2679 [
2680 2680 (b'e', b'existing', None, _(b'import file in patch directory')),
2681 2681 (b'n', b'name', b'', _(b'name of patch file'), _(b'NAME')),
2682 2682 (b'f', b'force', None, _(b'overwrite existing files')),
2683 2683 (
2684 2684 b'r',
2685 2685 b'rev',
2686 2686 [],
2687 2687 _(b'place existing revisions under mq control'),
2688 2688 _(b'REV'),
2689 2689 ),
2690 2690 (b'g', b'git', None, _(b'use git extended diff format')),
2691 2691 (b'P', b'push', None, _(b'qpush after importing')),
2692 2692 ],
2693 2693 _(b'hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'),
2694 2694 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2695 2695 )
2696 2696 def qimport(ui, repo, *filename, **opts):
2697 2697 """import a patch or existing changeset
2698 2698
2699 2699 The patch is inserted into the series after the last applied
2700 2700 patch. If no patches have been applied, qimport prepends the patch
2701 2701 to the series.
2702 2702
2703 2703 The patch will have the same name as its source file unless you
2704 2704 give it a new one with -n/--name.
2705 2705
2706 2706 You can register an existing patch inside the patch directory with
2707 2707 the -e/--existing flag.
2708 2708
2709 2709 With -f/--force, an existing patch of the same name will be
2710 2710 overwritten.
2711 2711
2712 2712 An existing changeset may be placed under mq control with -r/--rev
2713 2713 (e.g. qimport --rev . -n patch will place the current revision
2714 2714 under mq control). With -g/--git, patches imported with --rev will
2715 2715 use the git diff format. See the diffs help topic for information
2716 2716 on why this is important for preserving rename/copy information
2717 2717 and permission changes. Use :hg:`qfinish` to remove changesets
2718 2718 from mq control.
2719 2719
2720 2720 To import a patch from standard input, pass - as the patch file.
2721 2721 When importing from standard input, a patch name must be specified
2722 2722 using the --name flag.
2723 2723
2724 2724 To import an existing patch while renaming it::
2725 2725
2726 2726 hg qimport -e existing-patch -n new-name
2727 2727
2728 2728 Returns 0 if import succeeded.
2729 2729 """
2730 2730 opts = pycompat.byteskwargs(opts)
2731 2731 with repo.lock(): # cause this may move phase
2732 2732 q = repo.mq
2733 2733 try:
2734 2734 imported = q.qimport(
2735 2735 repo,
2736 2736 filename,
2737 2737 patchname=opts.get(b'name'),
2738 2738 existing=opts.get(b'existing'),
2739 2739 force=opts.get(b'force'),
2740 2740 rev=opts.get(b'rev'),
2741 2741 git=opts.get(b'git'),
2742 2742 )
2743 2743 finally:
2744 2744 q.savedirty()
2745 2745
2746 2746 if imported and opts.get(b'push') and not opts.get(b'rev'):
2747 2747 return q.push(repo, imported[-1])
2748 2748 return 0
2749 2749
2750 2750
2751 2751 def qinit(ui, repo, create):
2752 2752 """initialize a new queue repository
2753 2753
2754 2754 This command also creates a series file for ordering patches, and
2755 2755 an mq-specific .hgignore file in the queue repository, to exclude
2756 2756 the status and guards files (these contain mostly transient state).
2757 2757
2758 2758 Returns 0 if initialization succeeded."""
2759 2759 q = repo.mq
2760 2760 r = q.init(repo, create)
2761 2761 q.savedirty()
2762 2762 if r:
2763 2763 if not os.path.exists(r.wjoin(b'.hgignore')):
2764 2764 fp = r.wvfs(b'.hgignore', b'w')
2765 2765 fp.write(b'^\\.hg\n')
2766 2766 fp.write(b'^\\.mq\n')
2767 2767 fp.write(b'syntax: glob\n')
2768 2768 fp.write(b'status\n')
2769 2769 fp.write(b'guards\n')
2770 2770 fp.close()
2771 2771 if not os.path.exists(r.wjoin(b'series')):
2772 2772 r.wvfs(b'series', b'w').close()
2773 2773 r[None].add([b'.hgignore', b'series'])
2774 2774 commands.add(ui, r)
2775 2775 return 0
2776 2776
2777 2777
2778 2778 @command(
2779 2779 b"qinit",
2780 2780 [(b'c', b'create-repo', None, _(b'create queue repository'))],
2781 2781 _(b'hg qinit [-c]'),
2782 2782 helpcategory=command.CATEGORY_REPO_CREATION,
2783 2783 helpbasic=True,
2784 2784 )
2785 2785 def init(ui, repo, **opts):
2786 2786 """init a new queue repository (DEPRECATED)
2787 2787
2788 2788 The queue repository is unversioned by default. If
2789 2789 -c/--create-repo is specified, qinit will create a separate nested
2790 2790 repository for patches (qinit -c may also be run later to convert
2791 2791 an unversioned patch repository into a versioned one). You can use
2792 2792 qcommit to commit changes to this queue repository.
2793 2793
2794 2794 This command is deprecated. Without -c, it's implied by other relevant
2795 2795 commands. With -c, use :hg:`init --mq` instead."""
2796 2796 return qinit(ui, repo, create=opts.get('create_repo'))
2797 2797
2798 2798
2799 2799 @command(
2800 2800 b"qclone",
2801 2801 [
2802 2802 (b'', b'pull', None, _(b'use pull protocol to copy metadata')),
2803 2803 (
2804 2804 b'U',
2805 2805 b'noupdate',
2806 2806 None,
2807 2807 _(b'do not update the new working directories'),
2808 2808 ),
2809 2809 (
2810 2810 b'',
2811 2811 b'uncompressed',
2812 2812 None,
2813 2813 _(b'use uncompressed transfer (fast over LAN)'),
2814 2814 ),
2815 2815 (
2816 2816 b'p',
2817 2817 b'patches',
2818 2818 b'',
2819 2819 _(b'location of source patch repository'),
2820 2820 _(b'REPO'),
2821 2821 ),
2822 2822 ]
2823 2823 + cmdutil.remoteopts,
2824 2824 _(b'hg qclone [OPTION]... SOURCE [DEST]'),
2825 2825 helpcategory=command.CATEGORY_REPO_CREATION,
2826 2826 norepo=True,
2827 2827 )
2828 2828 def clone(ui, source, dest=None, **opts):
2829 2829 """clone main and patch repository at same time
2830 2830
2831 2831 If source is local, destination will have no patches applied. If
2832 2832 source is remote, this command can not check if patches are
2833 2833 applied in source, so cannot guarantee that patches are not
2834 2834 applied in destination. If you clone remote repository, be sure
2835 2835 before that it has no patches applied.
2836 2836
2837 2837 Source patch repository is looked for in <src>/.hg/patches by
2838 2838 default. Use -p <url> to change.
2839 2839
2840 2840 The patch directory must be a nested Mercurial repository, as
2841 2841 would be created by :hg:`init --mq`.
2842 2842
2843 2843 Return 0 on success.
2844 2844 """
2845 2845 opts = pycompat.byteskwargs(opts)
2846 2846
2847 2847 def patchdir(repo):
2848 2848 """compute a patch repo url from a repo object"""
2849 2849 url = repo.url()
2850 2850 if url.endswith(b'/'):
2851 2851 url = url[:-1]
2852 2852 return url + b'/.hg/patches'
2853 2853
2854 2854 # main repo (destination and sources)
2855 2855 if dest is None:
2856 2856 dest = hg.defaultdest(source)
2857 __, source_path, __ = urlutil.get_clone_path(ui, source)
2857 source_path = urlutil.get_clone_path_obj(ui, source)
2858 2858 sr = hg.peer(ui, opts, source_path)
2859 2859
2860 2860 # patches repo (source only)
2861 2861 if opts.get(b'patches'):
2862 __, patchespath, __ = urlutil.get_clone_path(ui, opts.get(b'patches'))
2862 patches_path = urlutil.get_clone_path_obj(ui, opts.get(b'patches'))
2863 2863 else:
2864 patchespath = patchdir(sr)
2864 # XXX path: we should turn this into a path object
2865 patches_path = patchdir(sr)
2865 2866 try:
2866 hg.peer(ui, opts, patchespath)
2867 hg.peer(ui, opts, patches_path)
2867 2868 except error.RepoError:
2868 2869 raise error.Abort(
2869 2870 _(b'versioned patch repository not found (see init --mq)')
2870 2871 )
2871 2872 qbase, destrev = None, None
2872 2873 if sr.local():
2873 2874 repo = sr.local()
2874 2875 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2875 2876 qbase = repo.mq.applied[0].node
2876 2877 if not hg.islocal(dest):
2877 2878 heads = set(repo.heads())
2878 2879 destrev = list(heads.difference(repo.heads(qbase)))
2879 2880 destrev.append(repo.changelog.parents(qbase)[0])
2880 2881 elif sr.capable(b'lookup'):
2881 2882 try:
2882 2883 qbase = sr.lookup(b'qbase')
2883 2884 except error.RepoError:
2884 2885 pass
2885 2886
2886 2887 ui.note(_(b'cloning main repository\n'))
2887 2888 sr, dr = hg.clone(
2888 2889 ui,
2889 2890 opts,
2890 2891 sr.url(),
2891 2892 dest,
2892 2893 pull=opts.get(b'pull'),
2893 2894 revs=destrev,
2894 2895 update=False,
2895 2896 stream=opts.get(b'uncompressed'),
2896 2897 )
2897 2898
2898 2899 ui.note(_(b'cloning patch repository\n'))
2899 2900 hg.clone(
2900 2901 ui,
2901 2902 opts,
2902 2903 opts.get(b'patches') or patchdir(sr),
2903 2904 patchdir(dr),
2904 2905 pull=opts.get(b'pull'),
2905 2906 update=not opts.get(b'noupdate'),
2906 2907 stream=opts.get(b'uncompressed'),
2907 2908 )
2908 2909
2909 2910 if dr.local():
2910 2911 repo = dr.local()
2911 2912 if qbase:
2912 2913 ui.note(
2913 2914 _(
2914 2915 b'stripping applied patches from destination '
2915 2916 b'repository\n'
2916 2917 )
2917 2918 )
2918 2919 strip(ui, repo, [qbase], update=False, backup=None)
2919 2920 if not opts.get(b'noupdate'):
2920 2921 ui.note(_(b'updating destination repository\n'))
2921 2922 hg.update(repo, repo.changelog.tip())
2922 2923
2923 2924
2924 2925 @command(
2925 2926 b"qcommit|qci",
2926 2927 commands.table[b"commit|ci"][1],
2927 2928 _(b'hg qcommit [OPTION]... [FILE]...'),
2928 2929 helpcategory=command.CATEGORY_COMMITTING,
2929 2930 inferrepo=True,
2930 2931 )
2931 2932 def commit(ui, repo, *pats, **opts):
2932 2933 """commit changes in the queue repository (DEPRECATED)
2933 2934
2934 2935 This command is deprecated; use :hg:`commit --mq` instead."""
2935 2936 q = repo.mq
2936 2937 r = q.qrepo()
2937 2938 if not r:
2938 2939 raise error.Abort(b'no queue repository')
2939 2940 commands.commit(r.ui, r, *pats, **opts)
2940 2941
2941 2942
2942 2943 @command(
2943 2944 b"qseries",
2944 2945 [
2945 2946 (b'm', b'missing', None, _(b'print patches not in series')),
2946 2947 ]
2947 2948 + seriesopts,
2948 2949 _(b'hg qseries [-ms]'),
2949 2950 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2950 2951 )
2951 2952 def series(ui, repo, **opts):
2952 2953 """print the entire series file
2953 2954
2954 2955 Returns 0 on success."""
2955 2956 repo.mq.qseries(
2956 2957 repo, missing=opts.get('missing'), summary=opts.get('summary')
2957 2958 )
2958 2959 return 0
2959 2960
2960 2961
2961 2962 @command(
2962 2963 b"qtop",
2963 2964 seriesopts,
2964 2965 _(b'hg qtop [-s]'),
2965 2966 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2966 2967 )
2967 2968 def top(ui, repo, **opts):
2968 2969 """print the name of the current patch
2969 2970
2970 2971 Returns 0 on success."""
2971 2972 q = repo.mq
2972 2973 if q.applied:
2973 2974 t = q.seriesend(True)
2974 2975 else:
2975 2976 t = 0
2976 2977
2977 2978 if t:
2978 2979 q.qseries(
2979 2980 repo,
2980 2981 start=t - 1,
2981 2982 length=1,
2982 2983 status=b'A',
2983 2984 summary=opts.get('summary'),
2984 2985 )
2985 2986 else:
2986 2987 ui.write(_(b"no patches applied\n"))
2987 2988 return 1
2988 2989
2989 2990
2990 2991 @command(
2991 2992 b"qnext",
2992 2993 seriesopts,
2993 2994 _(b'hg qnext [-s]'),
2994 2995 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2995 2996 )
2996 2997 def next(ui, repo, **opts):
2997 2998 """print the name of the next pushable patch
2998 2999
2999 3000 Returns 0 on success."""
3000 3001 q = repo.mq
3001 3002 end = q.seriesend()
3002 3003 if end == len(q.series):
3003 3004 ui.write(_(b"all patches applied\n"))
3004 3005 return 1
3005 3006 q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
3006 3007
3007 3008
3008 3009 @command(
3009 3010 b"qprev",
3010 3011 seriesopts,
3011 3012 _(b'hg qprev [-s]'),
3012 3013 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3013 3014 )
3014 3015 def prev(ui, repo, **opts):
3015 3016 """print the name of the preceding applied patch
3016 3017
3017 3018 Returns 0 on success."""
3018 3019 q = repo.mq
3019 3020 l = len(q.applied)
3020 3021 if l == 1:
3021 3022 ui.write(_(b"only one patch applied\n"))
3022 3023 return 1
3023 3024 if not l:
3024 3025 ui.write(_(b"no patches applied\n"))
3025 3026 return 1
3026 3027 idx = q.series.index(q.applied[-2].name)
3027 3028 q.qseries(
3028 3029 repo, start=idx, length=1, status=b'A', summary=opts.get('summary')
3029 3030 )
3030 3031
3031 3032
3032 3033 def setupheaderopts(ui, opts):
3033 3034 if not opts.get(b'user') and opts.get(b'currentuser'):
3034 3035 opts[b'user'] = ui.username()
3035 3036 if not opts.get(b'date') and opts.get(b'currentdate'):
3036 3037 opts[b'date'] = b"%d %d" % dateutil.makedate()
3037 3038
3038 3039
3039 3040 @command(
3040 3041 b"qnew",
3041 3042 [
3042 3043 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3043 3044 (b'f', b'force', None, _(b'import uncommitted changes (DEPRECATED)')),
3044 3045 (b'g', b'git', None, _(b'use git extended diff format')),
3045 3046 (b'U', b'currentuser', None, _(b'add "From: <current user>" to patch')),
3046 3047 (b'u', b'user', b'', _(b'add "From: <USER>" to patch'), _(b'USER')),
3047 3048 (b'D', b'currentdate', None, _(b'add "Date: <current date>" to patch')),
3048 3049 (b'd', b'date', b'', _(b'add "Date: <DATE>" to patch'), _(b'DATE')),
3049 3050 ]
3050 3051 + cmdutil.walkopts
3051 3052 + cmdutil.commitopts,
3052 3053 _(b'hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'),
3053 3054 helpcategory=command.CATEGORY_COMMITTING,
3054 3055 helpbasic=True,
3055 3056 inferrepo=True,
3056 3057 )
3057 3058 def new(ui, repo, patch, *args, **opts):
3058 3059 """create a new patch
3059 3060
3060 3061 qnew creates a new patch on top of the currently-applied patch (if
3061 3062 any). The patch will be initialized with any outstanding changes
3062 3063 in the working directory. You may also use -I/--include,
3063 3064 -X/--exclude, and/or a list of files after the patch name to add
3064 3065 only changes to matching files to the new patch, leaving the rest
3065 3066 as uncommitted modifications.
3066 3067
3067 3068 -u/--user and -d/--date can be used to set the (given) user and
3068 3069 date, respectively. -U/--currentuser and -D/--currentdate set user
3069 3070 to current user and date to current date.
3070 3071
3071 3072 -e/--edit, -m/--message or -l/--logfile set the patch header as
3072 3073 well as the commit message. If none is specified, the header is
3073 3074 empty and the commit message is '[mq]: PATCH'.
3074 3075
3075 3076 Use the -g/--git option to keep the patch in the git extended diff
3076 3077 format. Read the diffs help topic for more information on why this
3077 3078 is important for preserving permission changes and copy/rename
3078 3079 information.
3079 3080
3080 3081 Returns 0 on successful creation of a new patch.
3081 3082 """
3082 3083 opts = pycompat.byteskwargs(opts)
3083 3084 msg = cmdutil.logmessage(ui, opts)
3084 3085 q = repo.mq
3085 3086 opts[b'msg'] = msg
3086 3087 setupheaderopts(ui, opts)
3087 3088 q.new(repo, patch, *args, **pycompat.strkwargs(opts))
3088 3089 q.savedirty()
3089 3090 return 0
3090 3091
3091 3092
3092 3093 @command(
3093 3094 b"qrefresh",
3094 3095 [
3095 3096 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3096 3097 (b'g', b'git', None, _(b'use git extended diff format')),
3097 3098 (
3098 3099 b's',
3099 3100 b'short',
3100 3101 None,
3101 3102 _(b'refresh only files already in the patch and specified files'),
3102 3103 ),
3103 3104 (
3104 3105 b'U',
3105 3106 b'currentuser',
3106 3107 None,
3107 3108 _(b'add/update author field in patch with current user'),
3108 3109 ),
3109 3110 (
3110 3111 b'u',
3111 3112 b'user',
3112 3113 b'',
3113 3114 _(b'add/update author field in patch with given user'),
3114 3115 _(b'USER'),
3115 3116 ),
3116 3117 (
3117 3118 b'D',
3118 3119 b'currentdate',
3119 3120 None,
3120 3121 _(b'add/update date field in patch with current date'),
3121 3122 ),
3122 3123 (
3123 3124 b'd',
3124 3125 b'date',
3125 3126 b'',
3126 3127 _(b'add/update date field in patch with given date'),
3127 3128 _(b'DATE'),
3128 3129 ),
3129 3130 ]
3130 3131 + cmdutil.walkopts
3131 3132 + cmdutil.commitopts,
3132 3133 _(b'hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'),
3133 3134 helpcategory=command.CATEGORY_COMMITTING,
3134 3135 helpbasic=True,
3135 3136 inferrepo=True,
3136 3137 )
3137 3138 def refresh(ui, repo, *pats, **opts):
3138 3139 """update the current patch
3139 3140
3140 3141 If any file patterns are provided, the refreshed patch will
3141 3142 contain only the modifications that match those patterns; the
3142 3143 remaining modifications will remain in the working directory.
3143 3144
3144 3145 If -s/--short is specified, files currently included in the patch
3145 3146 will be refreshed just like matched files and remain in the patch.
3146 3147
3147 3148 If -e/--edit is specified, Mercurial will start your configured editor for
3148 3149 you to enter a message. In case qrefresh fails, you will find a backup of
3149 3150 your message in ``.hg/last-message.txt``.
3150 3151
3151 3152 hg add/remove/copy/rename work as usual, though you might want to
3152 3153 use git-style patches (-g/--git or [diff] git=1) to track copies
3153 3154 and renames. See the diffs help topic for more information on the
3154 3155 git diff format.
3155 3156
3156 3157 Returns 0 on success.
3157 3158 """
3158 3159 opts = pycompat.byteskwargs(opts)
3159 3160 q = repo.mq
3160 3161 message = cmdutil.logmessage(ui, opts)
3161 3162 setupheaderopts(ui, opts)
3162 3163 with repo.wlock():
3163 3164 ret = q.refresh(repo, pats, msg=message, **pycompat.strkwargs(opts))
3164 3165 q.savedirty()
3165 3166 return ret
3166 3167
3167 3168
3168 3169 @command(
3169 3170 b"qdiff",
3170 3171 cmdutil.diffopts + cmdutil.diffopts2 + cmdutil.walkopts,
3171 3172 _(b'hg qdiff [OPTION]... [FILE]...'),
3172 3173 helpcategory=command.CATEGORY_FILE_CONTENTS,
3173 3174 helpbasic=True,
3174 3175 inferrepo=True,
3175 3176 )
3176 3177 def diff(ui, repo, *pats, **opts):
3177 3178 """diff of the current patch and subsequent modifications
3178 3179
3179 3180 Shows a diff which includes the current patch as well as any
3180 3181 changes which have been made in the working directory since the
3181 3182 last refresh (thus showing what the current patch would become
3182 3183 after a qrefresh).
3183 3184
3184 3185 Use :hg:`diff` if you only want to see the changes made since the
3185 3186 last qrefresh, or :hg:`export qtip` if you want to see changes
3186 3187 made by the current patch without including changes made since the
3187 3188 qrefresh.
3188 3189
3189 3190 Returns 0 on success.
3190 3191 """
3191 3192 ui.pager(b'qdiff')
3192 3193 repo.mq.diff(repo, pats, pycompat.byteskwargs(opts))
3193 3194 return 0
3194 3195
3195 3196
3196 3197 @command(
3197 3198 b'qfold',
3198 3199 [
3199 3200 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3200 3201 (b'k', b'keep', None, _(b'keep folded patch files')),
3201 3202 ]
3202 3203 + cmdutil.commitopts,
3203 3204 _(b'hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'),
3204 3205 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
3205 3206 )
3206 3207 def fold(ui, repo, *files, **opts):
3207 3208 """fold the named patches into the current patch
3208 3209
3209 3210 Patches must not yet be applied. Each patch will be successively
3210 3211 applied to the current patch in the order given. If all the
3211 3212 patches apply successfully, the current patch will be refreshed
3212 3213 with the new cumulative patch, and the folded patches will be
3213 3214 deleted. With -k/--keep, the folded patch files will not be
3214 3215 removed afterwards.
3215 3216
3216 3217 The header for each folded patch will be concatenated with the
3217 3218 current patch header, separated by a line of ``* * *``.
3218 3219
3219 3220 Returns 0 on success."""
3220 3221 opts = pycompat.byteskwargs(opts)
3221 3222 q = repo.mq
3222 3223 if not files:
3223 3224 raise error.Abort(_(b'qfold requires at least one patch name'))
3224 3225 if not q.checktoppatch(repo)[0]:
3225 3226 raise error.Abort(_(b'no patches applied'))
3226 3227 q.checklocalchanges(repo)
3227 3228
3228 3229 message = cmdutil.logmessage(ui, opts)
3229 3230
3230 3231 parent = q.lookup(b'qtip')
3231 3232 patches = []
3232 3233 messages = []
3233 3234 for f in files:
3234 3235 p = q.lookup(f)
3235 3236 if p in patches or p == parent:
3236 3237 ui.warn(_(b'skipping already folded patch %s\n') % p)
3237 3238 if q.isapplied(p):
3238 3239 raise error.Abort(
3239 3240 _(b'qfold cannot fold already applied patch %s') % p
3240 3241 )
3241 3242 patches.append(p)
3242 3243
3243 3244 for p in patches:
3244 3245 if not message:
3245 3246 ph = patchheader(q.join(p), q.plainmode)
3246 3247 if ph.message:
3247 3248 messages.append(ph.message)
3248 3249 pf = q.join(p)
3249 3250 (patchsuccess, files, fuzz) = q.patch(repo, pf)
3250 3251 if not patchsuccess:
3251 3252 raise error.Abort(_(b'error folding patch %s') % p)
3252 3253
3253 3254 if not message:
3254 3255 ph = patchheader(q.join(parent), q.plainmode)
3255 3256 message = ph.message
3256 3257 for msg in messages:
3257 3258 if msg:
3258 3259 if message:
3259 3260 message.append(b'* * *')
3260 3261 message.extend(msg)
3261 3262 message = b'\n'.join(message)
3262 3263
3263 3264 diffopts = q.patchopts(q.diffopts(), *patches)
3264 3265 with repo.wlock():
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 wctx = r[None]
3631 3632 with r.wlock():
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