##// END OF EJS Templates
patch: add lexists() to backends, use it in selectfile()...
Patrick Mezard -
r14351:d54f9bbc default
parent child Browse files
Show More
@@ -1,3307 +1,3307 b''
1 1 # mq.py - patch queues for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''manage a stack of patches
9 9
10 10 This extension lets you work with a stack of patches in a Mercurial
11 11 repository. It manages two stacks of patches - all known patches, and
12 12 applied patches (subset of known patches).
13 13
14 14 Known patches are represented as patch files in the .hg/patches
15 15 directory. Applied patches are both patch files and changesets.
16 16
17 17 Common tasks (use :hg:`help command` for more details)::
18 18
19 19 create new patch qnew
20 20 import existing patch qimport
21 21
22 22 print patch series qseries
23 23 print applied patches qapplied
24 24
25 25 add known patch to applied stack qpush
26 26 remove patch from applied stack qpop
27 27 refresh contents of top applied patch qrefresh
28 28
29 29 By default, mq will automatically use git patches when required to
30 30 avoid losing file mode changes, copy records, binary files or empty
31 31 files creations or deletions. This behaviour can be configured with::
32 32
33 33 [mq]
34 34 git = auto/keep/yes/no
35 35
36 36 If set to 'keep', mq will obey the [diff] section configuration while
37 37 preserving existing git patches upon qrefresh. If set to 'yes' or
38 38 'no', mq will override the [diff] section and always generate git or
39 39 regular patches, possibly losing data in the second case.
40 40
41 41 You will by default be managing a patch queue named "patches". You can
42 42 create other, independent patch queues with the :hg:`qqueue` command.
43 43 '''
44 44
45 45 from mercurial.i18n import _
46 46 from mercurial.node import bin, hex, short, nullid, nullrev
47 47 from mercurial.lock import release
48 48 from mercurial import commands, cmdutil, hg, scmutil, util, revset
49 49 from mercurial import repair, extensions, url, error
50 50 from mercurial import patch as patchmod
51 51 import os, sys, re, errno, shutil
52 52
53 53 commands.norepo += " qclone"
54 54
55 55 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
56 56
57 57 cmdtable = {}
58 58 command = cmdutil.command(cmdtable)
59 59
60 60 # Patch names looks like unix-file names.
61 61 # They must be joinable with queue directory and result in the patch path.
62 62 normname = util.normpath
63 63
64 64 class statusentry(object):
65 65 def __init__(self, node, name):
66 66 self.node, self.name = node, name
67 67 def __repr__(self):
68 68 return hex(self.node) + ':' + self.name
69 69
70 70 class patchheader(object):
71 71 def __init__(self, pf, plainmode=False):
72 72 def eatdiff(lines):
73 73 while lines:
74 74 l = lines[-1]
75 75 if (l.startswith("diff -") or
76 76 l.startswith("Index:") or
77 77 l.startswith("===========")):
78 78 del lines[-1]
79 79 else:
80 80 break
81 81 def eatempty(lines):
82 82 while lines:
83 83 if not lines[-1].strip():
84 84 del lines[-1]
85 85 else:
86 86 break
87 87
88 88 message = []
89 89 comments = []
90 90 user = None
91 91 date = None
92 92 parent = None
93 93 format = None
94 94 subject = None
95 95 branch = None
96 96 nodeid = None
97 97 diffstart = 0
98 98
99 99 for line in file(pf):
100 100 line = line.rstrip()
101 101 if (line.startswith('diff --git')
102 102 or (diffstart and line.startswith('+++ '))):
103 103 diffstart = 2
104 104 break
105 105 diffstart = 0 # reset
106 106 if line.startswith("--- "):
107 107 diffstart = 1
108 108 continue
109 109 elif format == "hgpatch":
110 110 # parse values when importing the result of an hg export
111 111 if line.startswith("# User "):
112 112 user = line[7:]
113 113 elif line.startswith("# Date "):
114 114 date = line[7:]
115 115 elif line.startswith("# Parent "):
116 116 parent = line[9:].lstrip()
117 117 elif line.startswith("# Branch "):
118 118 branch = line[9:]
119 119 elif line.startswith("# Node ID "):
120 120 nodeid = line[10:]
121 121 elif not line.startswith("# ") and line:
122 122 message.append(line)
123 123 format = None
124 124 elif line == '# HG changeset patch':
125 125 message = []
126 126 format = "hgpatch"
127 127 elif (format != "tagdone" and (line.startswith("Subject: ") or
128 128 line.startswith("subject: "))):
129 129 subject = line[9:]
130 130 format = "tag"
131 131 elif (format != "tagdone" and (line.startswith("From: ") or
132 132 line.startswith("from: "))):
133 133 user = line[6:]
134 134 format = "tag"
135 135 elif (format != "tagdone" and (line.startswith("Date: ") or
136 136 line.startswith("date: "))):
137 137 date = line[6:]
138 138 format = "tag"
139 139 elif format == "tag" and line == "":
140 140 # when looking for tags (subject: from: etc) they
141 141 # end once you find a blank line in the source
142 142 format = "tagdone"
143 143 elif message or line:
144 144 message.append(line)
145 145 comments.append(line)
146 146
147 147 eatdiff(message)
148 148 eatdiff(comments)
149 149 # Remember the exact starting line of the patch diffs before consuming
150 150 # empty lines, for external use by TortoiseHg and others
151 151 self.diffstartline = len(comments)
152 152 eatempty(message)
153 153 eatempty(comments)
154 154
155 155 # make sure message isn't empty
156 156 if format and format.startswith("tag") and subject:
157 157 message.insert(0, "")
158 158 message.insert(0, subject)
159 159
160 160 self.message = message
161 161 self.comments = comments
162 162 self.user = user
163 163 self.date = date
164 164 self.parent = parent
165 165 # nodeid and branch are for external use by TortoiseHg and others
166 166 self.nodeid = nodeid
167 167 self.branch = branch
168 168 self.haspatch = diffstart > 1
169 169 self.plainmode = plainmode
170 170
171 171 def setuser(self, user):
172 172 if not self.updateheader(['From: ', '# User '], user):
173 173 try:
174 174 patchheaderat = self.comments.index('# HG changeset patch')
175 175 self.comments.insert(patchheaderat + 1, '# User ' + user)
176 176 except ValueError:
177 177 if self.plainmode or self._hasheader(['Date: ']):
178 178 self.comments = ['From: ' + user] + self.comments
179 179 else:
180 180 tmp = ['# HG changeset patch', '# User ' + user, '']
181 181 self.comments = tmp + self.comments
182 182 self.user = user
183 183
184 184 def setdate(self, date):
185 185 if not self.updateheader(['Date: ', '# Date '], date):
186 186 try:
187 187 patchheaderat = self.comments.index('# HG changeset patch')
188 188 self.comments.insert(patchheaderat + 1, '# Date ' + date)
189 189 except ValueError:
190 190 if self.plainmode or self._hasheader(['From: ']):
191 191 self.comments = ['Date: ' + date] + self.comments
192 192 else:
193 193 tmp = ['# HG changeset patch', '# Date ' + date, '']
194 194 self.comments = tmp + self.comments
195 195 self.date = date
196 196
197 197 def setparent(self, parent):
198 198 if not self.updateheader(['# Parent '], parent):
199 199 try:
200 200 patchheaderat = self.comments.index('# HG changeset patch')
201 201 self.comments.insert(patchheaderat + 1, '# Parent ' + parent)
202 202 except ValueError:
203 203 pass
204 204 self.parent = parent
205 205
206 206 def setmessage(self, message):
207 207 if self.comments:
208 208 self._delmsg()
209 209 self.message = [message]
210 210 self.comments += self.message
211 211
212 212 def updateheader(self, prefixes, new):
213 213 '''Update all references to a field in the patch header.
214 214 Return whether the field is present.'''
215 215 res = False
216 216 for prefix in prefixes:
217 217 for i in xrange(len(self.comments)):
218 218 if self.comments[i].startswith(prefix):
219 219 self.comments[i] = prefix + new
220 220 res = True
221 221 break
222 222 return res
223 223
224 224 def _hasheader(self, prefixes):
225 225 '''Check if a header starts with any of the given prefixes.'''
226 226 for prefix in prefixes:
227 227 for comment in self.comments:
228 228 if comment.startswith(prefix):
229 229 return True
230 230 return False
231 231
232 232 def __str__(self):
233 233 if not self.comments:
234 234 return ''
235 235 return '\n'.join(self.comments) + '\n\n'
236 236
237 237 def _delmsg(self):
238 238 '''Remove existing message, keeping the rest of the comments fields.
239 239 If comments contains 'subject: ', message will prepend
240 240 the field and a blank line.'''
241 241 if self.message:
242 242 subj = 'subject: ' + self.message[0].lower()
243 243 for i in xrange(len(self.comments)):
244 244 if subj == self.comments[i].lower():
245 245 del self.comments[i]
246 246 self.message = self.message[2:]
247 247 break
248 248 ci = 0
249 249 for mi in self.message:
250 250 while mi != self.comments[ci]:
251 251 ci += 1
252 252 del self.comments[ci]
253 253
254 254 class queue(object):
255 255 def __init__(self, ui, path, patchdir=None):
256 256 self.basepath = path
257 257 try:
258 258 fh = open(os.path.join(path, 'patches.queue'))
259 259 cur = fh.read().rstrip()
260 260 fh.close()
261 261 if not cur:
262 262 curpath = os.path.join(path, 'patches')
263 263 else:
264 264 curpath = os.path.join(path, 'patches-' + cur)
265 265 except IOError:
266 266 curpath = os.path.join(path, 'patches')
267 267 self.path = patchdir or curpath
268 268 self.opener = scmutil.opener(self.path)
269 269 self.ui = ui
270 270 self.applied_dirty = 0
271 271 self.series_dirty = 0
272 272 self.added = []
273 273 self.series_path = "series"
274 274 self.status_path = "status"
275 275 self.guards_path = "guards"
276 276 self.active_guards = None
277 277 self.guards_dirty = False
278 278 # Handle mq.git as a bool with extended values
279 279 try:
280 280 gitmode = ui.configbool('mq', 'git', None)
281 281 if gitmode is None:
282 282 raise error.ConfigError()
283 283 self.gitmode = gitmode and 'yes' or 'no'
284 284 except error.ConfigError:
285 285 self.gitmode = ui.config('mq', 'git', 'auto').lower()
286 286 self.plainmode = ui.configbool('mq', 'plain', False)
287 287
288 288 @util.propertycache
289 289 def applied(self):
290 290 if os.path.exists(self.join(self.status_path)):
291 291 def parselines(lines):
292 292 for l in lines:
293 293 entry = l.split(':', 1)
294 294 if len(entry) > 1:
295 295 n, name = entry
296 296 yield statusentry(bin(n), name)
297 297 elif l.strip():
298 298 self.ui.warn(_('malformated mq status line: %s\n') % entry)
299 299 # else we ignore empty lines
300 300 lines = self.opener.read(self.status_path).splitlines()
301 301 return list(parselines(lines))
302 302 return []
303 303
304 304 @util.propertycache
305 305 def full_series(self):
306 306 if os.path.exists(self.join(self.series_path)):
307 307 return self.opener.read(self.series_path).splitlines()
308 308 return []
309 309
310 310 @util.propertycache
311 311 def series(self):
312 312 self.parse_series()
313 313 return self.series
314 314
315 315 @util.propertycache
316 316 def series_guards(self):
317 317 self.parse_series()
318 318 return self.series_guards
319 319
320 320 def invalidate(self):
321 321 for a in 'applied full_series series series_guards'.split():
322 322 if a in self.__dict__:
323 323 delattr(self, a)
324 324 self.applied_dirty = 0
325 325 self.series_dirty = 0
326 326 self.guards_dirty = False
327 327 self.active_guards = None
328 328
329 329 def diffopts(self, opts={}, patchfn=None):
330 330 diffopts = patchmod.diffopts(self.ui, opts)
331 331 if self.gitmode == 'auto':
332 332 diffopts.upgrade = True
333 333 elif self.gitmode == 'keep':
334 334 pass
335 335 elif self.gitmode in ('yes', 'no'):
336 336 diffopts.git = self.gitmode == 'yes'
337 337 else:
338 338 raise util.Abort(_('mq.git option can be auto/keep/yes/no'
339 339 ' got %s') % self.gitmode)
340 340 if patchfn:
341 341 diffopts = self.patchopts(diffopts, patchfn)
342 342 return diffopts
343 343
344 344 def patchopts(self, diffopts, *patches):
345 345 """Return a copy of input diff options with git set to true if
346 346 referenced patch is a git patch and should be preserved as such.
347 347 """
348 348 diffopts = diffopts.copy()
349 349 if not diffopts.git and self.gitmode == 'keep':
350 350 for patchfn in patches:
351 351 patchf = self.opener(patchfn, 'r')
352 352 # if the patch was a git patch, refresh it as a git patch
353 353 for line in patchf:
354 354 if line.startswith('diff --git'):
355 355 diffopts.git = True
356 356 break
357 357 patchf.close()
358 358 return diffopts
359 359
360 360 def join(self, *p):
361 361 return os.path.join(self.path, *p)
362 362
363 363 def find_series(self, patch):
364 364 def matchpatch(l):
365 365 l = l.split('#', 1)[0]
366 366 return l.strip() == patch
367 367 for index, l in enumerate(self.full_series):
368 368 if matchpatch(l):
369 369 return index
370 370 return None
371 371
372 372 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
373 373
374 374 def parse_series(self):
375 375 self.series = []
376 376 self.series_guards = []
377 377 for l in self.full_series:
378 378 h = l.find('#')
379 379 if h == -1:
380 380 patch = l
381 381 comment = ''
382 382 elif h == 0:
383 383 continue
384 384 else:
385 385 patch = l[:h]
386 386 comment = l[h:]
387 387 patch = patch.strip()
388 388 if patch:
389 389 if patch in self.series:
390 390 raise util.Abort(_('%s appears more than once in %s') %
391 391 (patch, self.join(self.series_path)))
392 392 self.series.append(patch)
393 393 self.series_guards.append(self.guard_re.findall(comment))
394 394
395 395 def check_guard(self, guard):
396 396 if not guard:
397 397 return _('guard cannot be an empty string')
398 398 bad_chars = '# \t\r\n\f'
399 399 first = guard[0]
400 400 if first in '-+':
401 401 return (_('guard %r starts with invalid character: %r') %
402 402 (guard, first))
403 403 for c in bad_chars:
404 404 if c in guard:
405 405 return _('invalid character in guard %r: %r') % (guard, c)
406 406
407 407 def set_active(self, guards):
408 408 for guard in guards:
409 409 bad = self.check_guard(guard)
410 410 if bad:
411 411 raise util.Abort(bad)
412 412 guards = sorted(set(guards))
413 413 self.ui.debug('active guards: %s\n' % ' '.join(guards))
414 414 self.active_guards = guards
415 415 self.guards_dirty = True
416 416
417 417 def active(self):
418 418 if self.active_guards is None:
419 419 self.active_guards = []
420 420 try:
421 421 guards = self.opener.read(self.guards_path).split()
422 422 except IOError, err:
423 423 if err.errno != errno.ENOENT:
424 424 raise
425 425 guards = []
426 426 for i, guard in enumerate(guards):
427 427 bad = self.check_guard(guard)
428 428 if bad:
429 429 self.ui.warn('%s:%d: %s\n' %
430 430 (self.join(self.guards_path), i + 1, bad))
431 431 else:
432 432 self.active_guards.append(guard)
433 433 return self.active_guards
434 434
435 435 def set_guards(self, idx, guards):
436 436 for g in guards:
437 437 if len(g) < 2:
438 438 raise util.Abort(_('guard %r too short') % g)
439 439 if g[0] not in '-+':
440 440 raise util.Abort(_('guard %r starts with invalid char') % g)
441 441 bad = self.check_guard(g[1:])
442 442 if bad:
443 443 raise util.Abort(bad)
444 444 drop = self.guard_re.sub('', self.full_series[idx])
445 445 self.full_series[idx] = drop + ''.join([' #' + g for g in guards])
446 446 self.parse_series()
447 447 self.series_dirty = True
448 448
449 449 def pushable(self, idx):
450 450 if isinstance(idx, str):
451 451 idx = self.series.index(idx)
452 452 patchguards = self.series_guards[idx]
453 453 if not patchguards:
454 454 return True, None
455 455 guards = self.active()
456 456 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
457 457 if exactneg:
458 458 return False, exactneg[0]
459 459 pos = [g for g in patchguards if g[0] == '+']
460 460 exactpos = [g for g in pos if g[1:] in guards]
461 461 if pos:
462 462 if exactpos:
463 463 return True, exactpos[0]
464 464 return False, pos
465 465 return True, ''
466 466
467 467 def explain_pushable(self, idx, all_patches=False):
468 468 write = all_patches and self.ui.write or self.ui.warn
469 469 if all_patches or self.ui.verbose:
470 470 if isinstance(idx, str):
471 471 idx = self.series.index(idx)
472 472 pushable, why = self.pushable(idx)
473 473 if all_patches and pushable:
474 474 if why is None:
475 475 write(_('allowing %s - no guards in effect\n') %
476 476 self.series[idx])
477 477 else:
478 478 if not why:
479 479 write(_('allowing %s - no matching negative guards\n') %
480 480 self.series[idx])
481 481 else:
482 482 write(_('allowing %s - guarded by %r\n') %
483 483 (self.series[idx], why))
484 484 if not pushable:
485 485 if why:
486 486 write(_('skipping %s - guarded by %r\n') %
487 487 (self.series[idx], why))
488 488 else:
489 489 write(_('skipping %s - no matching guards\n') %
490 490 self.series[idx])
491 491
492 492 def save_dirty(self):
493 493 def write_list(items, path):
494 494 fp = self.opener(path, 'w')
495 495 for i in items:
496 496 fp.write("%s\n" % i)
497 497 fp.close()
498 498 if self.applied_dirty:
499 499 write_list(map(str, self.applied), self.status_path)
500 500 if self.series_dirty:
501 501 write_list(self.full_series, self.series_path)
502 502 if self.guards_dirty:
503 503 write_list(self.active_guards, self.guards_path)
504 504 if self.added:
505 505 qrepo = self.qrepo()
506 506 if qrepo:
507 507 qrepo[None].add(f for f in self.added if f not in qrepo[None])
508 508 self.added = []
509 509
510 510 def removeundo(self, repo):
511 511 undo = repo.sjoin('undo')
512 512 if not os.path.exists(undo):
513 513 return
514 514 try:
515 515 os.unlink(undo)
516 516 except OSError, inst:
517 517 self.ui.warn(_('error removing undo: %s\n') % str(inst))
518 518
519 519 def printdiff(self, repo, diffopts, node1, node2=None, files=None,
520 520 fp=None, changes=None, opts={}):
521 521 stat = opts.get('stat')
522 522 m = scmutil.match(repo, files, opts)
523 523 cmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m,
524 524 changes, stat, fp)
525 525
526 526 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
527 527 # first try just applying the patch
528 528 (err, n) = self.apply(repo, [patch], update_status=False,
529 529 strict=True, merge=rev)
530 530
531 531 if err == 0:
532 532 return (err, n)
533 533
534 534 if n is None:
535 535 raise util.Abort(_("apply failed for patch %s") % patch)
536 536
537 537 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
538 538
539 539 # apply failed, strip away that rev and merge.
540 540 hg.clean(repo, head)
541 541 self.strip(repo, [n], update=False, backup='strip')
542 542
543 543 ctx = repo[rev]
544 544 ret = hg.merge(repo, rev)
545 545 if ret:
546 546 raise util.Abort(_("update returned %d") % ret)
547 547 n = repo.commit(ctx.description(), ctx.user(), force=True)
548 548 if n is None:
549 549 raise util.Abort(_("repo commit failed"))
550 550 try:
551 551 ph = patchheader(mergeq.join(patch), self.plainmode)
552 552 except:
553 553 raise util.Abort(_("unable to read %s") % patch)
554 554
555 555 diffopts = self.patchopts(diffopts, patch)
556 556 patchf = self.opener(patch, "w")
557 557 comments = str(ph)
558 558 if comments:
559 559 patchf.write(comments)
560 560 self.printdiff(repo, diffopts, head, n, fp=patchf)
561 561 patchf.close()
562 562 self.removeundo(repo)
563 563 return (0, n)
564 564
565 565 def qparents(self, repo, rev=None):
566 566 if rev is None:
567 567 (p1, p2) = repo.dirstate.parents()
568 568 if p2 == nullid:
569 569 return p1
570 570 if not self.applied:
571 571 return None
572 572 return self.applied[-1].node
573 573 p1, p2 = repo.changelog.parents(rev)
574 574 if p2 != nullid and p2 in [x.node for x in self.applied]:
575 575 return p2
576 576 return p1
577 577
578 578 def mergepatch(self, repo, mergeq, series, diffopts):
579 579 if not self.applied:
580 580 # each of the patches merged in will have two parents. This
581 581 # can confuse the qrefresh, qdiff, and strip code because it
582 582 # needs to know which parent is actually in the patch queue.
583 583 # so, we insert a merge marker with only one parent. This way
584 584 # the first patch in the queue is never a merge patch
585 585 #
586 586 pname = ".hg.patches.merge.marker"
587 587 n = repo.commit('[mq]: merge marker', force=True)
588 588 self.removeundo(repo)
589 589 self.applied.append(statusentry(n, pname))
590 590 self.applied_dirty = 1
591 591
592 592 head = self.qparents(repo)
593 593
594 594 for patch in series:
595 595 patch = mergeq.lookup(patch, strict=True)
596 596 if not patch:
597 597 self.ui.warn(_("patch %s does not exist\n") % patch)
598 598 return (1, None)
599 599 pushable, reason = self.pushable(patch)
600 600 if not pushable:
601 601 self.explain_pushable(patch, all_patches=True)
602 602 continue
603 603 info = mergeq.isapplied(patch)
604 604 if not info:
605 605 self.ui.warn(_("patch %s is not applied\n") % patch)
606 606 return (1, None)
607 607 rev = info[1]
608 608 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
609 609 if head:
610 610 self.applied.append(statusentry(head, patch))
611 611 self.applied_dirty = 1
612 612 if err:
613 613 return (err, head)
614 614 self.save_dirty()
615 615 return (0, head)
616 616
617 617 def patch(self, repo, patchfile):
618 618 '''Apply patchfile to the working directory.
619 619 patchfile: name of patch file'''
620 620 files = {}
621 621 try:
622 622 fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
623 623 cwd=repo.root, files=files, eolmode=None)
624 624 return (True, list(files), fuzz)
625 625 except Exception, inst:
626 626 self.ui.note(str(inst) + '\n')
627 627 if not self.ui.verbose:
628 628 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
629 629 return (False, list(files), False)
630 630
631 631 def apply(self, repo, series, list=False, update_status=True,
632 632 strict=False, patchdir=None, merge=None, all_files=None):
633 633 wlock = lock = tr = None
634 634 try:
635 635 wlock = repo.wlock()
636 636 lock = repo.lock()
637 637 tr = repo.transaction("qpush")
638 638 try:
639 639 ret = self._apply(repo, series, list, update_status,
640 640 strict, patchdir, merge, all_files=all_files)
641 641 tr.close()
642 642 self.save_dirty()
643 643 return ret
644 644 except:
645 645 try:
646 646 tr.abort()
647 647 finally:
648 648 repo.invalidate()
649 649 repo.dirstate.invalidate()
650 650 raise
651 651 finally:
652 652 release(tr, lock, wlock)
653 653 self.removeundo(repo)
654 654
655 655 def _apply(self, repo, series, list=False, update_status=True,
656 656 strict=False, patchdir=None, merge=None, all_files=None):
657 657 '''returns (error, hash)
658 658 error = 1 for unable to read, 2 for patch failed, 3 for patch fuzz'''
659 659 # TODO unify with commands.py
660 660 if not patchdir:
661 661 patchdir = self.path
662 662 err = 0
663 663 n = None
664 664 for patchname in series:
665 665 pushable, reason = self.pushable(patchname)
666 666 if not pushable:
667 667 self.explain_pushable(patchname, all_patches=True)
668 668 continue
669 669 self.ui.status(_("applying %s\n") % patchname)
670 670 pf = os.path.join(patchdir, patchname)
671 671
672 672 try:
673 673 ph = patchheader(self.join(patchname), self.plainmode)
674 674 except IOError:
675 675 self.ui.warn(_("unable to read %s\n") % patchname)
676 676 err = 1
677 677 break
678 678
679 679 message = ph.message
680 680 if not message:
681 681 # The commit message should not be translated
682 682 message = "imported patch %s\n" % patchname
683 683 else:
684 684 if list:
685 685 # The commit message should not be translated
686 686 message.append("\nimported patch %s" % patchname)
687 687 message = '\n'.join(message)
688 688
689 689 if ph.haspatch:
690 690 (patcherr, files, fuzz) = self.patch(repo, pf)
691 691 if all_files is not None:
692 692 all_files.update(files)
693 693 patcherr = not patcherr
694 694 else:
695 695 self.ui.warn(_("patch %s is empty\n") % patchname)
696 696 patcherr, files, fuzz = 0, [], 0
697 697
698 698 if merge and files:
699 699 # Mark as removed/merged and update dirstate parent info
700 700 removed = []
701 701 merged = []
702 702 for f in files:
703 703 if os.path.lexists(repo.wjoin(f)):
704 704 merged.append(f)
705 705 else:
706 706 removed.append(f)
707 707 for f in removed:
708 708 repo.dirstate.remove(f)
709 709 for f in merged:
710 710 repo.dirstate.merge(f)
711 711 p1, p2 = repo.dirstate.parents()
712 712 repo.dirstate.setparents(p1, merge)
713 713
714 714 match = scmutil.matchfiles(repo, files or [])
715 715 n = repo.commit(message, ph.user, ph.date, match=match, force=True)
716 716
717 717 if n is None:
718 718 raise util.Abort(_("repository commit failed"))
719 719
720 720 if update_status:
721 721 self.applied.append(statusentry(n, patchname))
722 722
723 723 if patcherr:
724 724 self.ui.warn(_("patch failed, rejects left in working dir\n"))
725 725 err = 2
726 726 break
727 727
728 728 if fuzz and strict:
729 729 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
730 730 err = 3
731 731 break
732 732 return (err, n)
733 733
734 734 def _cleanup(self, patches, numrevs, keep=False):
735 735 if not keep:
736 736 r = self.qrepo()
737 737 if r:
738 738 r[None].remove(patches, True)
739 739 else:
740 740 for p in patches:
741 741 os.unlink(self.join(p))
742 742
743 743 if numrevs:
744 744 qfinished = self.applied[:numrevs]
745 745 del self.applied[:numrevs]
746 746 self.applied_dirty = 1
747 747
748 748 unknown = []
749 749
750 750 for (i, p) in sorted([(self.find_series(p), p) for p in patches],
751 751 reverse=True):
752 752 if i is not None:
753 753 del self.full_series[i]
754 754 else:
755 755 unknown.append(p)
756 756
757 757 if unknown:
758 758 if numrevs:
759 759 rev = dict((entry.name, entry.node) for entry in qfinished)
760 760 for p in unknown:
761 761 msg = _('revision %s refers to unknown patches: %s\n')
762 762 self.ui.warn(msg % (short(rev[p]), p))
763 763 else:
764 764 msg = _('unknown patches: %s\n')
765 765 raise util.Abort(''.join(msg % p for p in unknown))
766 766
767 767 self.parse_series()
768 768 self.series_dirty = 1
769 769
770 770 def _revpatches(self, repo, revs):
771 771 firstrev = repo[self.applied[0].node].rev()
772 772 patches = []
773 773 for i, rev in enumerate(revs):
774 774
775 775 if rev < firstrev:
776 776 raise util.Abort(_('revision %d is not managed') % rev)
777 777
778 778 ctx = repo[rev]
779 779 base = self.applied[i].node
780 780 if ctx.node() != base:
781 781 msg = _('cannot delete revision %d above applied patches')
782 782 raise util.Abort(msg % rev)
783 783
784 784 patch = self.applied[i].name
785 785 for fmt in ('[mq]: %s', 'imported patch %s'):
786 786 if ctx.description() == fmt % patch:
787 787 msg = _('patch %s finalized without changeset message\n')
788 788 repo.ui.status(msg % patch)
789 789 break
790 790
791 791 patches.append(patch)
792 792 return patches
793 793
794 794 def finish(self, repo, revs):
795 795 patches = self._revpatches(repo, sorted(revs))
796 796 self._cleanup(patches, len(patches))
797 797
798 798 def delete(self, repo, patches, opts):
799 799 if not patches and not opts.get('rev'):
800 800 raise util.Abort(_('qdelete requires at least one revision or '
801 801 'patch name'))
802 802
803 803 realpatches = []
804 804 for patch in patches:
805 805 patch = self.lookup(patch, strict=True)
806 806 info = self.isapplied(patch)
807 807 if info:
808 808 raise util.Abort(_("cannot delete applied patch %s") % patch)
809 809 if patch not in self.series:
810 810 raise util.Abort(_("patch %s not in series file") % patch)
811 811 if patch not in realpatches:
812 812 realpatches.append(patch)
813 813
814 814 numrevs = 0
815 815 if opts.get('rev'):
816 816 if not self.applied:
817 817 raise util.Abort(_('no patches applied'))
818 818 revs = scmutil.revrange(repo, opts.get('rev'))
819 819 if len(revs) > 1 and revs[0] > revs[1]:
820 820 revs.reverse()
821 821 revpatches = self._revpatches(repo, revs)
822 822 realpatches += revpatches
823 823 numrevs = len(revpatches)
824 824
825 825 self._cleanup(realpatches, numrevs, opts.get('keep'))
826 826
827 827 def check_toppatch(self, repo):
828 828 if self.applied:
829 829 top = self.applied[-1].node
830 830 patch = self.applied[-1].name
831 831 pp = repo.dirstate.parents()
832 832 if top not in pp:
833 833 raise util.Abort(_("working directory revision is not qtip"))
834 834 return top, patch
835 835 return None, None
836 836
837 837 def check_substate(self, repo):
838 838 '''return list of subrepos at a different revision than substate.
839 839 Abort if any subrepos have uncommitted changes.'''
840 840 inclsubs = []
841 841 wctx = repo[None]
842 842 for s in wctx.substate:
843 843 if wctx.sub(s).dirty(True):
844 844 raise util.Abort(
845 845 _("uncommitted changes in subrepository %s") % s)
846 846 elif wctx.sub(s).dirty():
847 847 inclsubs.append(s)
848 848 return inclsubs
849 849
850 850 def localchangesfound(self, refresh=True):
851 851 if refresh:
852 852 raise util.Abort(_("local changes found, refresh first"))
853 853 else:
854 854 raise util.Abort(_("local changes found"))
855 855
856 856 def check_localchanges(self, repo, force=False, refresh=True):
857 857 m, a, r, d = repo.status()[:4]
858 858 if (m or a or r or d) and not force:
859 859 self.localchangesfound(refresh)
860 860 return m, a, r, d
861 861
862 862 _reserved = ('series', 'status', 'guards', '.', '..')
863 863 def check_reserved_name(self, name):
864 864 if name in self._reserved:
865 865 raise util.Abort(_('"%s" cannot be used as the name of a patch')
866 866 % name)
867 867 for prefix in ('.hg', '.mq'):
868 868 if name.startswith(prefix):
869 869 raise util.Abort(_('patch name cannot begin with "%s"')
870 870 % prefix)
871 871 for c in ('#', ':'):
872 872 if c in name:
873 873 raise util.Abort(_('"%s" cannot be used in the name of a patch')
874 874 % c)
875 875
876 876
877 877 def new(self, repo, patchfn, *pats, **opts):
878 878 """options:
879 879 msg: a string or a no-argument function returning a string
880 880 """
881 881 msg = opts.get('msg')
882 882 user = opts.get('user')
883 883 date = opts.get('date')
884 884 if date:
885 885 date = util.parsedate(date)
886 886 diffopts = self.diffopts({'git': opts.get('git')})
887 887 self.check_reserved_name(patchfn)
888 888 if os.path.exists(self.join(patchfn)):
889 889 if os.path.isdir(self.join(patchfn)):
890 890 raise util.Abort(_('"%s" already exists as a directory')
891 891 % patchfn)
892 892 else:
893 893 raise util.Abort(_('patch "%s" already exists') % patchfn)
894 894
895 895 inclsubs = self.check_substate(repo)
896 896 if inclsubs:
897 897 inclsubs.append('.hgsubstate')
898 898 if opts.get('include') or opts.get('exclude') or pats:
899 899 if inclsubs:
900 900 pats = list(pats or []) + inclsubs
901 901 match = scmutil.match(repo, pats, opts)
902 902 # detect missing files in pats
903 903 def badfn(f, msg):
904 904 if f != '.hgsubstate': # .hgsubstate is auto-created
905 905 raise util.Abort('%s: %s' % (f, msg))
906 906 match.bad = badfn
907 907 m, a, r, d = repo.status(match=match)[:4]
908 908 else:
909 909 m, a, r, d = self.check_localchanges(repo, force=True)
910 910 match = scmutil.matchfiles(repo, m + a + r + inclsubs)
911 911 if len(repo[None].parents()) > 1:
912 912 raise util.Abort(_('cannot manage merge changesets'))
913 913 commitfiles = m + a + r
914 914 self.check_toppatch(repo)
915 915 insert = self.full_series_end()
916 916 wlock = repo.wlock()
917 917 try:
918 918 try:
919 919 # if patch file write fails, abort early
920 920 p = self.opener(patchfn, "w")
921 921 except IOError, e:
922 922 raise util.Abort(_('cannot write patch "%s": %s')
923 923 % (patchfn, e.strerror))
924 924 try:
925 925 if self.plainmode:
926 926 if user:
927 927 p.write("From: " + user + "\n")
928 928 if not date:
929 929 p.write("\n")
930 930 if date:
931 931 p.write("Date: %d %d\n\n" % date)
932 932 else:
933 933 p.write("# HG changeset patch\n")
934 934 p.write("# Parent "
935 935 + hex(repo[None].p1().node()) + "\n")
936 936 if user:
937 937 p.write("# User " + user + "\n")
938 938 if date:
939 939 p.write("# Date %s %s\n\n" % date)
940 940 if hasattr(msg, '__call__'):
941 941 msg = msg()
942 942 commitmsg = msg and msg or ("[mq]: %s" % patchfn)
943 943 n = repo.commit(commitmsg, user, date, match=match, force=True)
944 944 if n is None:
945 945 raise util.Abort(_("repo commit failed"))
946 946 try:
947 947 self.full_series[insert:insert] = [patchfn]
948 948 self.applied.append(statusentry(n, patchfn))
949 949 self.parse_series()
950 950 self.series_dirty = 1
951 951 self.applied_dirty = 1
952 952 if msg:
953 953 msg = msg + "\n\n"
954 954 p.write(msg)
955 955 if commitfiles:
956 956 parent = self.qparents(repo, n)
957 957 chunks = patchmod.diff(repo, node1=parent, node2=n,
958 958 match=match, opts=diffopts)
959 959 for chunk in chunks:
960 960 p.write(chunk)
961 961 p.close()
962 962 wlock.release()
963 963 wlock = None
964 964 r = self.qrepo()
965 965 if r:
966 966 r[None].add([patchfn])
967 967 except:
968 968 repo.rollback()
969 969 raise
970 970 except Exception:
971 971 patchpath = self.join(patchfn)
972 972 try:
973 973 os.unlink(patchpath)
974 974 except:
975 975 self.ui.warn(_('error unlinking %s\n') % patchpath)
976 976 raise
977 977 self.removeundo(repo)
978 978 finally:
979 979 release(wlock)
980 980
981 981 def strip(self, repo, revs, update=True, backup="all", force=None):
982 982 wlock = lock = None
983 983 try:
984 984 wlock = repo.wlock()
985 985 lock = repo.lock()
986 986
987 987 if update:
988 988 self.check_localchanges(repo, force=force, refresh=False)
989 989 urev = self.qparents(repo, revs[0])
990 990 hg.clean(repo, urev)
991 991 repo.dirstate.write()
992 992
993 993 self.removeundo(repo)
994 994 for rev in revs:
995 995 repair.strip(self.ui, repo, rev, backup)
996 996 # strip may have unbundled a set of backed up revisions after
997 997 # the actual strip
998 998 self.removeundo(repo)
999 999 finally:
1000 1000 release(lock, wlock)
1001 1001
1002 1002 def isapplied(self, patch):
1003 1003 """returns (index, rev, patch)"""
1004 1004 for i, a in enumerate(self.applied):
1005 1005 if a.name == patch:
1006 1006 return (i, a.node, a.name)
1007 1007 return None
1008 1008
1009 1009 # if the exact patch name does not exist, we try a few
1010 1010 # variations. If strict is passed, we try only #1
1011 1011 #
1012 1012 # 1) a number to indicate an offset in the series file
1013 1013 # 2) a unique substring of the patch name was given
1014 1014 # 3) patchname[-+]num to indicate an offset in the series file
1015 1015 def lookup(self, patch, strict=False):
1016 1016 patch = patch and str(patch)
1017 1017
1018 1018 def partial_name(s):
1019 1019 if s in self.series:
1020 1020 return s
1021 1021 matches = [x for x in self.series if s in x]
1022 1022 if len(matches) > 1:
1023 1023 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
1024 1024 for m in matches:
1025 1025 self.ui.warn(' %s\n' % m)
1026 1026 return None
1027 1027 if matches:
1028 1028 return matches[0]
1029 1029 if self.series and self.applied:
1030 1030 if s == 'qtip':
1031 1031 return self.series[self.series_end(True)-1]
1032 1032 if s == 'qbase':
1033 1033 return self.series[0]
1034 1034 return None
1035 1035
1036 1036 if patch is None:
1037 1037 return None
1038 1038 if patch in self.series:
1039 1039 return patch
1040 1040
1041 1041 if not os.path.isfile(self.join(patch)):
1042 1042 try:
1043 1043 sno = int(patch)
1044 1044 except (ValueError, OverflowError):
1045 1045 pass
1046 1046 else:
1047 1047 if -len(self.series) <= sno < len(self.series):
1048 1048 return self.series[sno]
1049 1049
1050 1050 if not strict:
1051 1051 res = partial_name(patch)
1052 1052 if res:
1053 1053 return res
1054 1054 minus = patch.rfind('-')
1055 1055 if minus >= 0:
1056 1056 res = partial_name(patch[:minus])
1057 1057 if res:
1058 1058 i = self.series.index(res)
1059 1059 try:
1060 1060 off = int(patch[minus + 1:] or 1)
1061 1061 except (ValueError, OverflowError):
1062 1062 pass
1063 1063 else:
1064 1064 if i - off >= 0:
1065 1065 return self.series[i - off]
1066 1066 plus = patch.rfind('+')
1067 1067 if plus >= 0:
1068 1068 res = partial_name(patch[:plus])
1069 1069 if res:
1070 1070 i = self.series.index(res)
1071 1071 try:
1072 1072 off = int(patch[plus + 1:] or 1)
1073 1073 except (ValueError, OverflowError):
1074 1074 pass
1075 1075 else:
1076 1076 if i + off < len(self.series):
1077 1077 return self.series[i + off]
1078 1078 raise util.Abort(_("patch %s not in series") % patch)
1079 1079
1080 1080 def push(self, repo, patch=None, force=False, list=False,
1081 1081 mergeq=None, all=False, move=False, exact=False):
1082 1082 diffopts = self.diffopts()
1083 1083 wlock = repo.wlock()
1084 1084 try:
1085 1085 heads = []
1086 1086 for b, ls in repo.branchmap().iteritems():
1087 1087 heads += ls
1088 1088 if not heads:
1089 1089 heads = [nullid]
1090 1090 if repo.dirstate.p1() not in heads and not exact:
1091 1091 self.ui.status(_("(working directory not at a head)\n"))
1092 1092
1093 1093 if not self.series:
1094 1094 self.ui.warn(_('no patches in series\n'))
1095 1095 return 0
1096 1096
1097 1097 patch = self.lookup(patch)
1098 1098 # Suppose our series file is: A B C and the current 'top'
1099 1099 # patch is B. qpush C should be performed (moving forward)
1100 1100 # qpush B is a NOP (no change) qpush A is an error (can't
1101 1101 # go backwards with qpush)
1102 1102 if patch:
1103 1103 info = self.isapplied(patch)
1104 1104 if info and info[0] >= len(self.applied) - 1:
1105 1105 self.ui.warn(
1106 1106 _('qpush: %s is already at the top\n') % patch)
1107 1107 return 0
1108 1108
1109 1109 pushable, reason = self.pushable(patch)
1110 1110 if pushable:
1111 1111 if self.series.index(patch) < self.series_end():
1112 1112 raise util.Abort(
1113 1113 _("cannot push to a previous patch: %s") % patch)
1114 1114 else:
1115 1115 if reason:
1116 1116 reason = _('guarded by %r') % reason
1117 1117 else:
1118 1118 reason = _('no matching guards')
1119 1119 self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason))
1120 1120 return 1
1121 1121 elif all:
1122 1122 patch = self.series[-1]
1123 1123 if self.isapplied(patch):
1124 1124 self.ui.warn(_('all patches are currently applied\n'))
1125 1125 return 0
1126 1126
1127 1127 # Following the above example, starting at 'top' of B:
1128 1128 # qpush should be performed (pushes C), but a subsequent
1129 1129 # qpush without an argument is an error (nothing to
1130 1130 # apply). This allows a loop of "...while hg qpush..." to
1131 1131 # work as it detects an error when done
1132 1132 start = self.series_end()
1133 1133 if start == len(self.series):
1134 1134 self.ui.warn(_('patch series already fully applied\n'))
1135 1135 return 1
1136 1136
1137 1137 if exact:
1138 1138 if move:
1139 1139 raise util.Abort(_("cannot use --exact and --move together"))
1140 1140 if self.applied:
1141 1141 raise util.Abort(_("cannot push --exact with applied patches"))
1142 1142 root = self.series[start]
1143 1143 target = patchheader(self.join(root), self.plainmode).parent
1144 1144 if not target:
1145 1145 raise util.Abort(_("%s does not have a parent recorded" % root))
1146 1146 if not repo[target] == repo['.']:
1147 1147 hg.update(repo, target)
1148 1148
1149 1149 if move:
1150 1150 if not patch:
1151 1151 raise util.Abort(_("please specify the patch to move"))
1152 1152 for i, rpn in enumerate(self.full_series[start:]):
1153 1153 # strip markers for patch guards
1154 1154 if self.guard_re.split(rpn, 1)[0] == patch:
1155 1155 break
1156 1156 index = start + i
1157 1157 assert index < len(self.full_series)
1158 1158 fullpatch = self.full_series[index]
1159 1159 del self.full_series[index]
1160 1160 self.full_series.insert(start, fullpatch)
1161 1161 self.parse_series()
1162 1162 self.series_dirty = 1
1163 1163
1164 1164 self.applied_dirty = 1
1165 1165 if start > 0:
1166 1166 self.check_toppatch(repo)
1167 1167 if not patch:
1168 1168 patch = self.series[start]
1169 1169 end = start + 1
1170 1170 else:
1171 1171 end = self.series.index(patch, start) + 1
1172 1172
1173 1173 s = self.series[start:end]
1174 1174
1175 1175 if not force:
1176 1176 mm, aa, rr, dd = repo.status()[:4]
1177 1177 wcfiles = set(mm + aa + rr + dd)
1178 1178 if wcfiles:
1179 1179 for patchname in s:
1180 1180 pf = os.path.join(self.path, patchname)
1181 patchfiles = patchmod.changedfiles(pf, strip=1)
1181 patchfiles = patchmod.changedfiles(self.ui, repo, pf)
1182 1182 if wcfiles.intersection(patchfiles):
1183 1183 self.localchangesfound(self.applied)
1184 1184 elif mergeq:
1185 1185 self.check_localchanges(refresh=self.applied)
1186 1186
1187 1187 all_files = set()
1188 1188 try:
1189 1189 if mergeq:
1190 1190 ret = self.mergepatch(repo, mergeq, s, diffopts)
1191 1191 else:
1192 1192 ret = self.apply(repo, s, list, all_files=all_files)
1193 1193 except:
1194 1194 self.ui.warn(_('cleaning up working directory...'))
1195 1195 node = repo.dirstate.p1()
1196 1196 hg.revert(repo, node, None)
1197 1197 # only remove unknown files that we know we touched or
1198 1198 # created while patching
1199 1199 for f in all_files:
1200 1200 if f not in repo.dirstate:
1201 1201 try:
1202 1202 util.unlinkpath(repo.wjoin(f))
1203 1203 except OSError, inst:
1204 1204 if inst.errno != errno.ENOENT:
1205 1205 raise
1206 1206 self.ui.warn(_('done\n'))
1207 1207 raise
1208 1208
1209 1209 if not self.applied:
1210 1210 return ret[0]
1211 1211 top = self.applied[-1].name
1212 1212 if ret[0] and ret[0] > 1:
1213 1213 msg = _("errors during apply, please fix and refresh %s\n")
1214 1214 self.ui.write(msg % top)
1215 1215 else:
1216 1216 self.ui.write(_("now at: %s\n") % top)
1217 1217 return ret[0]
1218 1218
1219 1219 finally:
1220 1220 wlock.release()
1221 1221
1222 1222 def pop(self, repo, patch=None, force=False, update=True, all=False):
1223 1223 wlock = repo.wlock()
1224 1224 try:
1225 1225 if patch:
1226 1226 # index, rev, patch
1227 1227 info = self.isapplied(patch)
1228 1228 if not info:
1229 1229 patch = self.lookup(patch)
1230 1230 info = self.isapplied(patch)
1231 1231 if not info:
1232 1232 raise util.Abort(_("patch %s is not applied") % patch)
1233 1233
1234 1234 if not self.applied:
1235 1235 # Allow qpop -a to work repeatedly,
1236 1236 # but not qpop without an argument
1237 1237 self.ui.warn(_("no patches applied\n"))
1238 1238 return not all
1239 1239
1240 1240 if all:
1241 1241 start = 0
1242 1242 elif patch:
1243 1243 start = info[0] + 1
1244 1244 else:
1245 1245 start = len(self.applied) - 1
1246 1246
1247 1247 if start >= len(self.applied):
1248 1248 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
1249 1249 return
1250 1250
1251 1251 if not update:
1252 1252 parents = repo.dirstate.parents()
1253 1253 rr = [x.node for x in self.applied]
1254 1254 for p in parents:
1255 1255 if p in rr:
1256 1256 self.ui.warn(_("qpop: forcing dirstate update\n"))
1257 1257 update = True
1258 1258 else:
1259 1259 parents = [p.node() for p in repo[None].parents()]
1260 1260 needupdate = False
1261 1261 for entry in self.applied[start:]:
1262 1262 if entry.node in parents:
1263 1263 needupdate = True
1264 1264 break
1265 1265 update = needupdate
1266 1266
1267 1267 self.applied_dirty = 1
1268 1268 end = len(self.applied)
1269 1269 rev = self.applied[start].node
1270 1270 if update:
1271 1271 top = self.check_toppatch(repo)[0]
1272 1272
1273 1273 try:
1274 1274 heads = repo.changelog.heads(rev)
1275 1275 except error.LookupError:
1276 1276 node = short(rev)
1277 1277 raise util.Abort(_('trying to pop unknown node %s') % node)
1278 1278
1279 1279 if heads != [self.applied[-1].node]:
1280 1280 raise util.Abort(_("popping would remove a revision not "
1281 1281 "managed by this patch queue"))
1282 1282
1283 1283 # we know there are no local changes, so we can make a simplified
1284 1284 # form of hg.update.
1285 1285 if update:
1286 1286 qp = self.qparents(repo, rev)
1287 1287 ctx = repo[qp]
1288 1288 m, a, r, d = repo.status(qp, top)[:4]
1289 1289 parentfiles = set(m + a + r + d)
1290 1290 if not force and parentfiles:
1291 1291 mm, aa, rr, dd = repo.status()[:4]
1292 1292 wcfiles = set(mm + aa + rr + dd)
1293 1293 if wcfiles.intersection(parentfiles):
1294 1294 self.localchangesfound()
1295 1295 if d:
1296 1296 raise util.Abort(_("deletions found between repo revs"))
1297 1297 for f in a:
1298 1298 try:
1299 1299 util.unlinkpath(repo.wjoin(f))
1300 1300 except OSError, e:
1301 1301 if e.errno != errno.ENOENT:
1302 1302 raise
1303 1303 repo.dirstate.forget(f)
1304 1304 for f in m + r:
1305 1305 fctx = ctx[f]
1306 1306 repo.wwrite(f, fctx.data(), fctx.flags())
1307 1307 repo.dirstate.normal(f)
1308 1308 repo.dirstate.setparents(qp, nullid)
1309 1309 for patch in reversed(self.applied[start:end]):
1310 1310 self.ui.status(_("popping %s\n") % patch.name)
1311 1311 del self.applied[start:end]
1312 1312 self.strip(repo, [rev], update=False, backup='strip')
1313 1313 if self.applied:
1314 1314 self.ui.write(_("now at: %s\n") % self.applied[-1].name)
1315 1315 else:
1316 1316 self.ui.write(_("patch queue now empty\n"))
1317 1317 finally:
1318 1318 wlock.release()
1319 1319
1320 1320 def diff(self, repo, pats, opts):
1321 1321 top, patch = self.check_toppatch(repo)
1322 1322 if not top:
1323 1323 self.ui.write(_("no patches applied\n"))
1324 1324 return
1325 1325 qp = self.qparents(repo, top)
1326 1326 if opts.get('reverse'):
1327 1327 node1, node2 = None, qp
1328 1328 else:
1329 1329 node1, node2 = qp, None
1330 1330 diffopts = self.diffopts(opts, patch)
1331 1331 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1332 1332
1333 1333 def refresh(self, repo, pats=None, **opts):
1334 1334 if not self.applied:
1335 1335 self.ui.write(_("no patches applied\n"))
1336 1336 return 1
1337 1337 msg = opts.get('msg', '').rstrip()
1338 1338 newuser = opts.get('user')
1339 1339 newdate = opts.get('date')
1340 1340 if newdate:
1341 1341 newdate = '%d %d' % util.parsedate(newdate)
1342 1342 wlock = repo.wlock()
1343 1343
1344 1344 try:
1345 1345 self.check_toppatch(repo)
1346 1346 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1347 1347 if repo.changelog.heads(top) != [top]:
1348 1348 raise util.Abort(_("cannot refresh a revision with children"))
1349 1349
1350 1350 inclsubs = self.check_substate(repo)
1351 1351
1352 1352 cparents = repo.changelog.parents(top)
1353 1353 patchparent = self.qparents(repo, top)
1354 1354 ph = patchheader(self.join(patchfn), self.plainmode)
1355 1355 diffopts = self.diffopts({'git': opts.get('git')}, patchfn)
1356 1356 if msg:
1357 1357 ph.setmessage(msg)
1358 1358 if newuser:
1359 1359 ph.setuser(newuser)
1360 1360 if newdate:
1361 1361 ph.setdate(newdate)
1362 1362 ph.setparent(hex(patchparent))
1363 1363
1364 1364 # only commit new patch when write is complete
1365 1365 patchf = self.opener(patchfn, 'w', atomictemp=True)
1366 1366
1367 1367 comments = str(ph)
1368 1368 if comments:
1369 1369 patchf.write(comments)
1370 1370
1371 1371 # update the dirstate in place, strip off the qtip commit
1372 1372 # and then commit.
1373 1373 #
1374 1374 # this should really read:
1375 1375 # mm, dd, aa = repo.status(top, patchparent)[:3]
1376 1376 # but we do it backwards to take advantage of manifest/chlog
1377 1377 # caching against the next repo.status call
1378 1378 mm, aa, dd = repo.status(patchparent, top)[:3]
1379 1379 changes = repo.changelog.read(top)
1380 1380 man = repo.manifest.read(changes[0])
1381 1381 aaa = aa[:]
1382 1382 matchfn = scmutil.match(repo, pats, opts)
1383 1383 # in short mode, we only diff the files included in the
1384 1384 # patch already plus specified files
1385 1385 if opts.get('short'):
1386 1386 # if amending a patch, we start with existing
1387 1387 # files plus specified files - unfiltered
1388 1388 match = scmutil.matchfiles(repo, mm + aa + dd + matchfn.files())
1389 1389 # filter with inc/exl options
1390 1390 matchfn = scmutil.match(repo, opts=opts)
1391 1391 else:
1392 1392 match = scmutil.matchall(repo)
1393 1393 m, a, r, d = repo.status(match=match)[:4]
1394 1394 mm = set(mm)
1395 1395 aa = set(aa)
1396 1396 dd = set(dd)
1397 1397
1398 1398 # we might end up with files that were added between
1399 1399 # qtip and the dirstate parent, but then changed in the
1400 1400 # local dirstate. in this case, we want them to only
1401 1401 # show up in the added section
1402 1402 for x in m:
1403 1403 if x not in aa:
1404 1404 mm.add(x)
1405 1405 # we might end up with files added by the local dirstate that
1406 1406 # were deleted by the patch. In this case, they should only
1407 1407 # show up in the changed section.
1408 1408 for x in a:
1409 1409 if x in dd:
1410 1410 dd.remove(x)
1411 1411 mm.add(x)
1412 1412 else:
1413 1413 aa.add(x)
1414 1414 # make sure any files deleted in the local dirstate
1415 1415 # are not in the add or change column of the patch
1416 1416 forget = []
1417 1417 for x in d + r:
1418 1418 if x in aa:
1419 1419 aa.remove(x)
1420 1420 forget.append(x)
1421 1421 continue
1422 1422 else:
1423 1423 mm.discard(x)
1424 1424 dd.add(x)
1425 1425
1426 1426 m = list(mm)
1427 1427 r = list(dd)
1428 1428 a = list(aa)
1429 1429 c = [filter(matchfn, l) for l in (m, a, r)]
1430 1430 match = scmutil.matchfiles(repo, set(c[0] + c[1] + c[2] + inclsubs))
1431 1431 chunks = patchmod.diff(repo, patchparent, match=match,
1432 1432 changes=c, opts=diffopts)
1433 1433 for chunk in chunks:
1434 1434 patchf.write(chunk)
1435 1435
1436 1436 try:
1437 1437 if diffopts.git or diffopts.upgrade:
1438 1438 copies = {}
1439 1439 for dst in a:
1440 1440 src = repo.dirstate.copied(dst)
1441 1441 # during qfold, the source file for copies may
1442 1442 # be removed. Treat this as a simple add.
1443 1443 if src is not None and src in repo.dirstate:
1444 1444 copies.setdefault(src, []).append(dst)
1445 1445 repo.dirstate.add(dst)
1446 1446 # remember the copies between patchparent and qtip
1447 1447 for dst in aaa:
1448 1448 f = repo.file(dst)
1449 1449 src = f.renamed(man[dst])
1450 1450 if src:
1451 1451 copies.setdefault(src[0], []).extend(
1452 1452 copies.get(dst, []))
1453 1453 if dst in a:
1454 1454 copies[src[0]].append(dst)
1455 1455 # we can't copy a file created by the patch itself
1456 1456 if dst in copies:
1457 1457 del copies[dst]
1458 1458 for src, dsts in copies.iteritems():
1459 1459 for dst in dsts:
1460 1460 repo.dirstate.copy(src, dst)
1461 1461 else:
1462 1462 for dst in a:
1463 1463 repo.dirstate.add(dst)
1464 1464 # Drop useless copy information
1465 1465 for f in list(repo.dirstate.copies()):
1466 1466 repo.dirstate.copy(None, f)
1467 1467 for f in r:
1468 1468 repo.dirstate.remove(f)
1469 1469 # if the patch excludes a modified file, mark that
1470 1470 # file with mtime=0 so status can see it.
1471 1471 mm = []
1472 1472 for i in xrange(len(m)-1, -1, -1):
1473 1473 if not matchfn(m[i]):
1474 1474 mm.append(m[i])
1475 1475 del m[i]
1476 1476 for f in m:
1477 1477 repo.dirstate.normal(f)
1478 1478 for f in mm:
1479 1479 repo.dirstate.normallookup(f)
1480 1480 for f in forget:
1481 1481 repo.dirstate.forget(f)
1482 1482
1483 1483 if not msg:
1484 1484 if not ph.message:
1485 1485 message = "[mq]: %s\n" % patchfn
1486 1486 else:
1487 1487 message = "\n".join(ph.message)
1488 1488 else:
1489 1489 message = msg
1490 1490
1491 1491 user = ph.user or changes[1]
1492 1492
1493 1493 # assumes strip can roll itself back if interrupted
1494 1494 repo.dirstate.setparents(*cparents)
1495 1495 self.applied.pop()
1496 1496 self.applied_dirty = 1
1497 1497 self.strip(repo, [top], update=False,
1498 1498 backup='strip')
1499 1499 except:
1500 1500 repo.dirstate.invalidate()
1501 1501 raise
1502 1502
1503 1503 try:
1504 1504 # might be nice to attempt to roll back strip after this
1505 1505 n = repo.commit(message, user, ph.date, match=match,
1506 1506 force=True)
1507 1507 # only write patch after a successful commit
1508 1508 patchf.rename()
1509 1509 self.applied.append(statusentry(n, patchfn))
1510 1510 except:
1511 1511 ctx = repo[cparents[0]]
1512 1512 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
1513 1513 self.save_dirty()
1514 1514 self.ui.warn(_('refresh interrupted while patch was popped! '
1515 1515 '(revert --all, qpush to recover)\n'))
1516 1516 raise
1517 1517 finally:
1518 1518 wlock.release()
1519 1519 self.removeundo(repo)
1520 1520
1521 1521 def init(self, repo, create=False):
1522 1522 if not create and os.path.isdir(self.path):
1523 1523 raise util.Abort(_("patch queue directory already exists"))
1524 1524 try:
1525 1525 os.mkdir(self.path)
1526 1526 except OSError, inst:
1527 1527 if inst.errno != errno.EEXIST or not create:
1528 1528 raise
1529 1529 if create:
1530 1530 return self.qrepo(create=True)
1531 1531
1532 1532 def unapplied(self, repo, patch=None):
1533 1533 if patch and patch not in self.series:
1534 1534 raise util.Abort(_("patch %s is not in series file") % patch)
1535 1535 if not patch:
1536 1536 start = self.series_end()
1537 1537 else:
1538 1538 start = self.series.index(patch) + 1
1539 1539 unapplied = []
1540 1540 for i in xrange(start, len(self.series)):
1541 1541 pushable, reason = self.pushable(i)
1542 1542 if pushable:
1543 1543 unapplied.append((i, self.series[i]))
1544 1544 self.explain_pushable(i)
1545 1545 return unapplied
1546 1546
1547 1547 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1548 1548 summary=False):
1549 1549 def displayname(pfx, patchname, state):
1550 1550 if pfx:
1551 1551 self.ui.write(pfx)
1552 1552 if summary:
1553 1553 ph = patchheader(self.join(patchname), self.plainmode)
1554 1554 msg = ph.message and ph.message[0] or ''
1555 1555 if self.ui.formatted():
1556 1556 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
1557 1557 if width > 0:
1558 1558 msg = util.ellipsis(msg, width)
1559 1559 else:
1560 1560 msg = ''
1561 1561 self.ui.write(patchname, label='qseries.' + state)
1562 1562 self.ui.write(': ')
1563 1563 self.ui.write(msg, label='qseries.message.' + state)
1564 1564 else:
1565 1565 self.ui.write(patchname, label='qseries.' + state)
1566 1566 self.ui.write('\n')
1567 1567
1568 1568 applied = set([p.name for p in self.applied])
1569 1569 if length is None:
1570 1570 length = len(self.series) - start
1571 1571 if not missing:
1572 1572 if self.ui.verbose:
1573 1573 idxwidth = len(str(start + length - 1))
1574 1574 for i in xrange(start, start + length):
1575 1575 patch = self.series[i]
1576 1576 if patch in applied:
1577 1577 char, state = 'A', 'applied'
1578 1578 elif self.pushable(i)[0]:
1579 1579 char, state = 'U', 'unapplied'
1580 1580 else:
1581 1581 char, state = 'G', 'guarded'
1582 1582 pfx = ''
1583 1583 if self.ui.verbose:
1584 1584 pfx = '%*d %s ' % (idxwidth, i, char)
1585 1585 elif status and status != char:
1586 1586 continue
1587 1587 displayname(pfx, patch, state)
1588 1588 else:
1589 1589 msng_list = []
1590 1590 for root, dirs, files in os.walk(self.path):
1591 1591 d = root[len(self.path) + 1:]
1592 1592 for f in files:
1593 1593 fl = os.path.join(d, f)
1594 1594 if (fl not in self.series and
1595 1595 fl not in (self.status_path, self.series_path,
1596 1596 self.guards_path)
1597 1597 and not fl.startswith('.')):
1598 1598 msng_list.append(fl)
1599 1599 for x in sorted(msng_list):
1600 1600 pfx = self.ui.verbose and ('D ') or ''
1601 1601 displayname(pfx, x, 'missing')
1602 1602
1603 1603 def issaveline(self, l):
1604 1604 if l.name == '.hg.patches.save.line':
1605 1605 return True
1606 1606
1607 1607 def qrepo(self, create=False):
1608 1608 ui = self.ui.copy()
1609 1609 ui.setconfig('paths', 'default', '', overlay=False)
1610 1610 ui.setconfig('paths', 'default-push', '', overlay=False)
1611 1611 if create or os.path.isdir(self.join(".hg")):
1612 1612 return hg.repository(ui, path=self.path, create=create)
1613 1613
1614 1614 def restore(self, repo, rev, delete=None, qupdate=None):
1615 1615 desc = repo[rev].description().strip()
1616 1616 lines = desc.splitlines()
1617 1617 i = 0
1618 1618 datastart = None
1619 1619 series = []
1620 1620 applied = []
1621 1621 qpp = None
1622 1622 for i, line in enumerate(lines):
1623 1623 if line == 'Patch Data:':
1624 1624 datastart = i + 1
1625 1625 elif line.startswith('Dirstate:'):
1626 1626 l = line.rstrip()
1627 1627 l = l[10:].split(' ')
1628 1628 qpp = [bin(x) for x in l]
1629 1629 elif datastart is not None:
1630 1630 l = line.rstrip()
1631 1631 n, name = l.split(':', 1)
1632 1632 if n:
1633 1633 applied.append(statusentry(bin(n), name))
1634 1634 else:
1635 1635 series.append(l)
1636 1636 if datastart is None:
1637 1637 self.ui.warn(_("No saved patch data found\n"))
1638 1638 return 1
1639 1639 self.ui.warn(_("restoring status: %s\n") % lines[0])
1640 1640 self.full_series = series
1641 1641 self.applied = applied
1642 1642 self.parse_series()
1643 1643 self.series_dirty = 1
1644 1644 self.applied_dirty = 1
1645 1645 heads = repo.changelog.heads()
1646 1646 if delete:
1647 1647 if rev not in heads:
1648 1648 self.ui.warn(_("save entry has children, leaving it alone\n"))
1649 1649 else:
1650 1650 self.ui.warn(_("removing save entry %s\n") % short(rev))
1651 1651 pp = repo.dirstate.parents()
1652 1652 if rev in pp:
1653 1653 update = True
1654 1654 else:
1655 1655 update = False
1656 1656 self.strip(repo, [rev], update=update, backup='strip')
1657 1657 if qpp:
1658 1658 self.ui.warn(_("saved queue repository parents: %s %s\n") %
1659 1659 (short(qpp[0]), short(qpp[1])))
1660 1660 if qupdate:
1661 1661 self.ui.status(_("updating queue directory\n"))
1662 1662 r = self.qrepo()
1663 1663 if not r:
1664 1664 self.ui.warn(_("Unable to load queue repository\n"))
1665 1665 return 1
1666 1666 hg.clean(r, qpp[0])
1667 1667
1668 1668 def save(self, repo, msg=None):
1669 1669 if not self.applied:
1670 1670 self.ui.warn(_("save: no patches applied, exiting\n"))
1671 1671 return 1
1672 1672 if self.issaveline(self.applied[-1]):
1673 1673 self.ui.warn(_("status is already saved\n"))
1674 1674 return 1
1675 1675
1676 1676 if not msg:
1677 1677 msg = _("hg patches saved state")
1678 1678 else:
1679 1679 msg = "hg patches: " + msg.rstrip('\r\n')
1680 1680 r = self.qrepo()
1681 1681 if r:
1682 1682 pp = r.dirstate.parents()
1683 1683 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
1684 1684 msg += "\n\nPatch Data:\n"
1685 1685 msg += ''.join('%s\n' % x for x in self.applied)
1686 1686 msg += ''.join(':%s\n' % x for x in self.full_series)
1687 1687 n = repo.commit(msg, force=True)
1688 1688 if not n:
1689 1689 self.ui.warn(_("repo commit failed\n"))
1690 1690 return 1
1691 1691 self.applied.append(statusentry(n, '.hg.patches.save.line'))
1692 1692 self.applied_dirty = 1
1693 1693 self.removeundo(repo)
1694 1694
1695 1695 def full_series_end(self):
1696 1696 if self.applied:
1697 1697 p = self.applied[-1].name
1698 1698 end = self.find_series(p)
1699 1699 if end is None:
1700 1700 return len(self.full_series)
1701 1701 return end + 1
1702 1702 return 0
1703 1703
1704 1704 def series_end(self, all_patches=False):
1705 1705 """If all_patches is False, return the index of the next pushable patch
1706 1706 in the series, or the series length. If all_patches is True, return the
1707 1707 index of the first patch past the last applied one.
1708 1708 """
1709 1709 end = 0
1710 1710 def next(start):
1711 1711 if all_patches or start >= len(self.series):
1712 1712 return start
1713 1713 for i in xrange(start, len(self.series)):
1714 1714 p, reason = self.pushable(i)
1715 1715 if p:
1716 1716 break
1717 1717 self.explain_pushable(i)
1718 1718 return i
1719 1719 if self.applied:
1720 1720 p = self.applied[-1].name
1721 1721 try:
1722 1722 end = self.series.index(p)
1723 1723 except ValueError:
1724 1724 return 0
1725 1725 return next(end + 1)
1726 1726 return next(end)
1727 1727
1728 1728 def appliedname(self, index):
1729 1729 pname = self.applied[index].name
1730 1730 if not self.ui.verbose:
1731 1731 p = pname
1732 1732 else:
1733 1733 p = str(self.series.index(pname)) + " " + pname
1734 1734 return p
1735 1735
1736 1736 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
1737 1737 force=None, git=False):
1738 1738 def checkseries(patchname):
1739 1739 if patchname in self.series:
1740 1740 raise util.Abort(_('patch %s is already in the series file')
1741 1741 % patchname)
1742 1742 def checkfile(patchname):
1743 1743 if not force and os.path.exists(self.join(patchname)):
1744 1744 raise util.Abort(_('patch "%s" already exists')
1745 1745 % patchname)
1746 1746
1747 1747 if rev:
1748 1748 if files:
1749 1749 raise util.Abort(_('option "-r" not valid when importing '
1750 1750 'files'))
1751 1751 rev = scmutil.revrange(repo, rev)
1752 1752 rev.sort(reverse=True)
1753 1753 if (len(files) > 1 or len(rev) > 1) and patchname:
1754 1754 raise util.Abort(_('option "-n" not valid when importing multiple '
1755 1755 'patches'))
1756 1756 if rev:
1757 1757 # If mq patches are applied, we can only import revisions
1758 1758 # that form a linear path to qbase.
1759 1759 # Otherwise, they should form a linear path to a head.
1760 1760 heads = repo.changelog.heads(repo.changelog.node(rev[-1]))
1761 1761 if len(heads) > 1:
1762 1762 raise util.Abort(_('revision %d is the root of more than one '
1763 1763 'branch') % rev[-1])
1764 1764 if self.applied:
1765 1765 base = repo.changelog.node(rev[0])
1766 1766 if base in [n.node for n in self.applied]:
1767 1767 raise util.Abort(_('revision %d is already managed')
1768 1768 % rev[0])
1769 1769 if heads != [self.applied[-1].node]:
1770 1770 raise util.Abort(_('revision %d is not the parent of '
1771 1771 'the queue') % rev[0])
1772 1772 base = repo.changelog.rev(self.applied[0].node)
1773 1773 lastparent = repo.changelog.parentrevs(base)[0]
1774 1774 else:
1775 1775 if heads != [repo.changelog.node(rev[0])]:
1776 1776 raise util.Abort(_('revision %d has unmanaged children')
1777 1777 % rev[0])
1778 1778 lastparent = None
1779 1779
1780 1780 diffopts = self.diffopts({'git': git})
1781 1781 for r in rev:
1782 1782 p1, p2 = repo.changelog.parentrevs(r)
1783 1783 n = repo.changelog.node(r)
1784 1784 if p2 != nullrev:
1785 1785 raise util.Abort(_('cannot import merge revision %d') % r)
1786 1786 if lastparent and lastparent != r:
1787 1787 raise util.Abort(_('revision %d is not the parent of %d')
1788 1788 % (r, lastparent))
1789 1789 lastparent = p1
1790 1790
1791 1791 if not patchname:
1792 1792 patchname = normname('%d.diff' % r)
1793 1793 self.check_reserved_name(patchname)
1794 1794 checkseries(patchname)
1795 1795 checkfile(patchname)
1796 1796 self.full_series.insert(0, patchname)
1797 1797
1798 1798 patchf = self.opener(patchname, "w")
1799 1799 cmdutil.export(repo, [n], fp=patchf, opts=diffopts)
1800 1800 patchf.close()
1801 1801
1802 1802 se = statusentry(n, patchname)
1803 1803 self.applied.insert(0, se)
1804 1804
1805 1805 self.added.append(patchname)
1806 1806 patchname = None
1807 1807 self.parse_series()
1808 1808 self.applied_dirty = 1
1809 1809 self.series_dirty = True
1810 1810
1811 1811 for i, filename in enumerate(files):
1812 1812 if existing:
1813 1813 if filename == '-':
1814 1814 raise util.Abort(_('-e is incompatible with import from -'))
1815 1815 filename = normname(filename)
1816 1816 self.check_reserved_name(filename)
1817 1817 originpath = self.join(filename)
1818 1818 if not os.path.isfile(originpath):
1819 1819 raise util.Abort(_("patch %s does not exist") % filename)
1820 1820
1821 1821 if patchname:
1822 1822 self.check_reserved_name(patchname)
1823 1823 checkfile(patchname)
1824 1824
1825 1825 self.ui.write(_('renaming %s to %s\n')
1826 1826 % (filename, patchname))
1827 1827 util.rename(originpath, self.join(patchname))
1828 1828 else:
1829 1829 patchname = filename
1830 1830
1831 1831 else:
1832 1832 try:
1833 1833 if filename == '-':
1834 1834 if not patchname:
1835 1835 raise util.Abort(
1836 1836 _('need --name to import a patch from -'))
1837 1837 text = sys.stdin.read()
1838 1838 else:
1839 1839 fp = url.open(self.ui, filename)
1840 1840 text = fp.read()
1841 1841 fp.close()
1842 1842 except (OSError, IOError):
1843 1843 raise util.Abort(_("unable to read file %s") % filename)
1844 1844 if not patchname:
1845 1845 patchname = normname(os.path.basename(filename))
1846 1846 self.check_reserved_name(patchname)
1847 1847 checkfile(patchname)
1848 1848 patchf = self.opener(patchname, "w")
1849 1849 patchf.write(text)
1850 1850 patchf.close()
1851 1851 if not force:
1852 1852 checkseries(patchname)
1853 1853 if patchname not in self.series:
1854 1854 index = self.full_series_end() + i
1855 1855 self.full_series[index:index] = [patchname]
1856 1856 self.parse_series()
1857 1857 self.series_dirty = True
1858 1858 self.ui.warn(_("adding %s to series file\n") % patchname)
1859 1859 self.added.append(patchname)
1860 1860 patchname = None
1861 1861
1862 1862 self.removeundo(repo)
1863 1863
1864 1864 @command("qdelete|qremove|qrm",
1865 1865 [('k', 'keep', None, _('keep patch file')),
1866 1866 ('r', 'rev', [],
1867 1867 _('stop managing a revision (DEPRECATED)'), _('REV'))],
1868 1868 _('hg qdelete [-k] [PATCH]...'))
1869 1869 def delete(ui, repo, *patches, **opts):
1870 1870 """remove patches from queue
1871 1871
1872 1872 The patches must not be applied, and at least one patch is required. With
1873 1873 -k/--keep, the patch files are preserved in the patch directory.
1874 1874
1875 1875 To stop managing a patch and move it into permanent history,
1876 1876 use the :hg:`qfinish` command."""
1877 1877 q = repo.mq
1878 1878 q.delete(repo, patches, opts)
1879 1879 q.save_dirty()
1880 1880 return 0
1881 1881
1882 1882 @command("qapplied",
1883 1883 [('1', 'last', None, _('show only the last patch'))
1884 1884 ] + seriesopts,
1885 1885 _('hg qapplied [-1] [-s] [PATCH]'))
1886 1886 def applied(ui, repo, patch=None, **opts):
1887 1887 """print the patches already applied
1888 1888
1889 1889 Returns 0 on success."""
1890 1890
1891 1891 q = repo.mq
1892 1892
1893 1893 if patch:
1894 1894 if patch not in q.series:
1895 1895 raise util.Abort(_("patch %s is not in series file") % patch)
1896 1896 end = q.series.index(patch) + 1
1897 1897 else:
1898 1898 end = q.series_end(True)
1899 1899
1900 1900 if opts.get('last') and not end:
1901 1901 ui.write(_("no patches applied\n"))
1902 1902 return 1
1903 1903 elif opts.get('last') and end == 1:
1904 1904 ui.write(_("only one patch applied\n"))
1905 1905 return 1
1906 1906 elif opts.get('last'):
1907 1907 start = end - 2
1908 1908 end = 1
1909 1909 else:
1910 1910 start = 0
1911 1911
1912 1912 q.qseries(repo, length=end, start=start, status='A',
1913 1913 summary=opts.get('summary'))
1914 1914
1915 1915
1916 1916 @command("qunapplied",
1917 1917 [('1', 'first', None, _('show only the first patch'))] + seriesopts,
1918 1918 _('hg qunapplied [-1] [-s] [PATCH]'))
1919 1919 def unapplied(ui, repo, patch=None, **opts):
1920 1920 """print the patches not yet applied
1921 1921
1922 1922 Returns 0 on success."""
1923 1923
1924 1924 q = repo.mq
1925 1925 if patch:
1926 1926 if patch not in q.series:
1927 1927 raise util.Abort(_("patch %s is not in series file") % patch)
1928 1928 start = q.series.index(patch) + 1
1929 1929 else:
1930 1930 start = q.series_end(True)
1931 1931
1932 1932 if start == len(q.series) and opts.get('first'):
1933 1933 ui.write(_("all patches applied\n"))
1934 1934 return 1
1935 1935
1936 1936 length = opts.get('first') and 1 or None
1937 1937 q.qseries(repo, start=start, length=length, status='U',
1938 1938 summary=opts.get('summary'))
1939 1939
1940 1940 @command("qimport",
1941 1941 [('e', 'existing', None, _('import file in patch directory')),
1942 1942 ('n', 'name', '',
1943 1943 _('name of patch file'), _('NAME')),
1944 1944 ('f', 'force', None, _('overwrite existing files')),
1945 1945 ('r', 'rev', [],
1946 1946 _('place existing revisions under mq control'), _('REV')),
1947 1947 ('g', 'git', None, _('use git extended diff format')),
1948 1948 ('P', 'push', None, _('qpush after importing'))],
1949 1949 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... FILE...'))
1950 1950 def qimport(ui, repo, *filename, **opts):
1951 1951 """import a patch
1952 1952
1953 1953 The patch is inserted into the series after the last applied
1954 1954 patch. If no patches have been applied, qimport prepends the patch
1955 1955 to the series.
1956 1956
1957 1957 The patch will have the same name as its source file unless you
1958 1958 give it a new one with -n/--name.
1959 1959
1960 1960 You can register an existing patch inside the patch directory with
1961 1961 the -e/--existing flag.
1962 1962
1963 1963 With -f/--force, an existing patch of the same name will be
1964 1964 overwritten.
1965 1965
1966 1966 An existing changeset may be placed under mq control with -r/--rev
1967 1967 (e.g. qimport --rev tip -n patch will place tip under mq control).
1968 1968 With -g/--git, patches imported with --rev will use the git diff
1969 1969 format. See the diffs help topic for information on why this is
1970 1970 important for preserving rename/copy information and permission
1971 1971 changes. Use :hg:`qfinish` to remove changesets from mq control.
1972 1972
1973 1973 To import a patch from standard input, pass - as the patch file.
1974 1974 When importing from standard input, a patch name must be specified
1975 1975 using the --name flag.
1976 1976
1977 1977 To import an existing patch while renaming it::
1978 1978
1979 1979 hg qimport -e existing-patch -n new-name
1980 1980
1981 1981 Returns 0 if import succeeded.
1982 1982 """
1983 1983 q = repo.mq
1984 1984 try:
1985 1985 q.qimport(repo, filename, patchname=opts.get('name'),
1986 1986 existing=opts.get('existing'), force=opts.get('force'),
1987 1987 rev=opts.get('rev'), git=opts.get('git'))
1988 1988 finally:
1989 1989 q.save_dirty()
1990 1990
1991 1991 if opts.get('push') and not opts.get('rev'):
1992 1992 return q.push(repo, None)
1993 1993 return 0
1994 1994
1995 1995 def qinit(ui, repo, create):
1996 1996 """initialize a new queue repository
1997 1997
1998 1998 This command also creates a series file for ordering patches, and
1999 1999 an mq-specific .hgignore file in the queue repository, to exclude
2000 2000 the status and guards files (these contain mostly transient state).
2001 2001
2002 2002 Returns 0 if initialization succeeded."""
2003 2003 q = repo.mq
2004 2004 r = q.init(repo, create)
2005 2005 q.save_dirty()
2006 2006 if r:
2007 2007 if not os.path.exists(r.wjoin('.hgignore')):
2008 2008 fp = r.wopener('.hgignore', 'w')
2009 2009 fp.write('^\\.hg\n')
2010 2010 fp.write('^\\.mq\n')
2011 2011 fp.write('syntax: glob\n')
2012 2012 fp.write('status\n')
2013 2013 fp.write('guards\n')
2014 2014 fp.close()
2015 2015 if not os.path.exists(r.wjoin('series')):
2016 2016 r.wopener('series', 'w').close()
2017 2017 r[None].add(['.hgignore', 'series'])
2018 2018 commands.add(ui, r)
2019 2019 return 0
2020 2020
2021 2021 @command("^qinit",
2022 2022 [('c', 'create-repo', None, _('create queue repository'))],
2023 2023 _('hg qinit [-c]'))
2024 2024 def init(ui, repo, **opts):
2025 2025 """init a new queue repository (DEPRECATED)
2026 2026
2027 2027 The queue repository is unversioned by default. If
2028 2028 -c/--create-repo is specified, qinit will create a separate nested
2029 2029 repository for patches (qinit -c may also be run later to convert
2030 2030 an unversioned patch repository into a versioned one). You can use
2031 2031 qcommit to commit changes to this queue repository.
2032 2032
2033 2033 This command is deprecated. Without -c, it's implied by other relevant
2034 2034 commands. With -c, use :hg:`init --mq` instead."""
2035 2035 return qinit(ui, repo, create=opts.get('create_repo'))
2036 2036
2037 2037 @command("qclone",
2038 2038 [('', 'pull', None, _('use pull protocol to copy metadata')),
2039 2039 ('U', 'noupdate', None, _('do not update the new working directories')),
2040 2040 ('', 'uncompressed', None,
2041 2041 _('use uncompressed transfer (fast over LAN)')),
2042 2042 ('p', 'patches', '',
2043 2043 _('location of source patch repository'), _('REPO')),
2044 2044 ] + commands.remoteopts,
2045 2045 _('hg qclone [OPTION]... SOURCE [DEST]'))
2046 2046 def clone(ui, source, dest=None, **opts):
2047 2047 '''clone main and patch repository at same time
2048 2048
2049 2049 If source is local, destination will have no patches applied. If
2050 2050 source is remote, this command can not check if patches are
2051 2051 applied in source, so cannot guarantee that patches are not
2052 2052 applied in destination. If you clone remote repository, be sure
2053 2053 before that it has no patches applied.
2054 2054
2055 2055 Source patch repository is looked for in <src>/.hg/patches by
2056 2056 default. Use -p <url> to change.
2057 2057
2058 2058 The patch directory must be a nested Mercurial repository, as
2059 2059 would be created by :hg:`init --mq`.
2060 2060
2061 2061 Return 0 on success.
2062 2062 '''
2063 2063 def patchdir(repo):
2064 2064 url = repo.url()
2065 2065 if url.endswith('/'):
2066 2066 url = url[:-1]
2067 2067 return url + '/.hg/patches'
2068 2068 if dest is None:
2069 2069 dest = hg.defaultdest(source)
2070 2070 sr = hg.repository(hg.remoteui(ui, opts), ui.expandpath(source))
2071 2071 if opts.get('patches'):
2072 2072 patchespath = ui.expandpath(opts.get('patches'))
2073 2073 else:
2074 2074 patchespath = patchdir(sr)
2075 2075 try:
2076 2076 hg.repository(ui, patchespath)
2077 2077 except error.RepoError:
2078 2078 raise util.Abort(_('versioned patch repository not found'
2079 2079 ' (see init --mq)'))
2080 2080 qbase, destrev = None, None
2081 2081 if sr.local():
2082 2082 if sr.mq.applied:
2083 2083 qbase = sr.mq.applied[0].node
2084 2084 if not hg.islocal(dest):
2085 2085 heads = set(sr.heads())
2086 2086 destrev = list(heads.difference(sr.heads(qbase)))
2087 2087 destrev.append(sr.changelog.parents(qbase)[0])
2088 2088 elif sr.capable('lookup'):
2089 2089 try:
2090 2090 qbase = sr.lookup('qbase')
2091 2091 except error.RepoError:
2092 2092 pass
2093 2093 ui.note(_('cloning main repository\n'))
2094 2094 sr, dr = hg.clone(ui, sr.url(), dest,
2095 2095 pull=opts.get('pull'),
2096 2096 rev=destrev,
2097 2097 update=False,
2098 2098 stream=opts.get('uncompressed'))
2099 2099 ui.note(_('cloning patch repository\n'))
2100 2100 hg.clone(ui, opts.get('patches') or patchdir(sr), patchdir(dr),
2101 2101 pull=opts.get('pull'), update=not opts.get('noupdate'),
2102 2102 stream=opts.get('uncompressed'))
2103 2103 if dr.local():
2104 2104 if qbase:
2105 2105 ui.note(_('stripping applied patches from destination '
2106 2106 'repository\n'))
2107 2107 dr.mq.strip(dr, [qbase], update=False, backup=None)
2108 2108 if not opts.get('noupdate'):
2109 2109 ui.note(_('updating destination repository\n'))
2110 2110 hg.update(dr, dr.changelog.tip())
2111 2111
2112 2112 @command("qcommit|qci",
2113 2113 commands.table["^commit|ci"][1],
2114 2114 _('hg qcommit [OPTION]... [FILE]...'))
2115 2115 def commit(ui, repo, *pats, **opts):
2116 2116 """commit changes in the queue repository (DEPRECATED)
2117 2117
2118 2118 This command is deprecated; use :hg:`commit --mq` instead."""
2119 2119 q = repo.mq
2120 2120 r = q.qrepo()
2121 2121 if not r:
2122 2122 raise util.Abort('no queue repository')
2123 2123 commands.commit(r.ui, r, *pats, **opts)
2124 2124
2125 2125 @command("qseries",
2126 2126 [('m', 'missing', None, _('print patches not in series')),
2127 2127 ] + seriesopts,
2128 2128 _('hg qseries [-ms]'))
2129 2129 def series(ui, repo, **opts):
2130 2130 """print the entire series file
2131 2131
2132 2132 Returns 0 on success."""
2133 2133 repo.mq.qseries(repo, missing=opts.get('missing'), summary=opts.get('summary'))
2134 2134 return 0
2135 2135
2136 2136 @command("qtop", [] + seriesopts, _('hg qtop [-s]'))
2137 2137 def top(ui, repo, **opts):
2138 2138 """print the name of the current patch
2139 2139
2140 2140 Returns 0 on success."""
2141 2141 q = repo.mq
2142 2142 t = q.applied and q.series_end(True) or 0
2143 2143 if t:
2144 2144 q.qseries(repo, start=t - 1, length=1, status='A',
2145 2145 summary=opts.get('summary'))
2146 2146 else:
2147 2147 ui.write(_("no patches applied\n"))
2148 2148 return 1
2149 2149
2150 2150 @command("qnext", [] + seriesopts, _('hg qnext [-s]'))
2151 2151 def next(ui, repo, **opts):
2152 2152 """print the name of the next patch
2153 2153
2154 2154 Returns 0 on success."""
2155 2155 q = repo.mq
2156 2156 end = q.series_end()
2157 2157 if end == len(q.series):
2158 2158 ui.write(_("all patches applied\n"))
2159 2159 return 1
2160 2160 q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
2161 2161
2162 2162 @command("qprev", [] + seriesopts, _('hg qprev [-s]'))
2163 2163 def prev(ui, repo, **opts):
2164 2164 """print the name of the previous patch
2165 2165
2166 2166 Returns 0 on success."""
2167 2167 q = repo.mq
2168 2168 l = len(q.applied)
2169 2169 if l == 1:
2170 2170 ui.write(_("only one patch applied\n"))
2171 2171 return 1
2172 2172 if not l:
2173 2173 ui.write(_("no patches applied\n"))
2174 2174 return 1
2175 2175 q.qseries(repo, start=l - 2, length=1, status='A',
2176 2176 summary=opts.get('summary'))
2177 2177
2178 2178 def setupheaderopts(ui, opts):
2179 2179 if not opts.get('user') and opts.get('currentuser'):
2180 2180 opts['user'] = ui.username()
2181 2181 if not opts.get('date') and opts.get('currentdate'):
2182 2182 opts['date'] = "%d %d" % util.makedate()
2183 2183
2184 2184 @command("^qnew",
2185 2185 [('e', 'edit', None, _('edit commit message')),
2186 2186 ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')),
2187 2187 ('g', 'git', None, _('use git extended diff format')),
2188 2188 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2189 2189 ('u', 'user', '',
2190 2190 _('add "From: <USER>" to patch'), _('USER')),
2191 2191 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2192 2192 ('d', 'date', '',
2193 2193 _('add "Date: <DATE>" to patch'), _('DATE'))
2194 2194 ] + commands.walkopts + commands.commitopts,
2195 2195 _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'))
2196 2196 def new(ui, repo, patch, *args, **opts):
2197 2197 """create a new patch
2198 2198
2199 2199 qnew creates a new patch on top of the currently-applied patch (if
2200 2200 any). The patch will be initialized with any outstanding changes
2201 2201 in the working directory. You may also use -I/--include,
2202 2202 -X/--exclude, and/or a list of files after the patch name to add
2203 2203 only changes to matching files to the new patch, leaving the rest
2204 2204 as uncommitted modifications.
2205 2205
2206 2206 -u/--user and -d/--date can be used to set the (given) user and
2207 2207 date, respectively. -U/--currentuser and -D/--currentdate set user
2208 2208 to current user and date to current date.
2209 2209
2210 2210 -e/--edit, -m/--message or -l/--logfile set the patch header as
2211 2211 well as the commit message. If none is specified, the header is
2212 2212 empty and the commit message is '[mq]: PATCH'.
2213 2213
2214 2214 Use the -g/--git option to keep the patch in the git extended diff
2215 2215 format. Read the diffs help topic for more information on why this
2216 2216 is important for preserving permission changes and copy/rename
2217 2217 information.
2218 2218
2219 2219 Returns 0 on successful creation of a new patch.
2220 2220 """
2221 2221 msg = cmdutil.logmessage(opts)
2222 2222 def getmsg():
2223 2223 return ui.edit(msg, opts.get('user') or ui.username())
2224 2224 q = repo.mq
2225 2225 opts['msg'] = msg
2226 2226 if opts.get('edit'):
2227 2227 opts['msg'] = getmsg
2228 2228 else:
2229 2229 opts['msg'] = msg
2230 2230 setupheaderopts(ui, opts)
2231 2231 q.new(repo, patch, *args, **opts)
2232 2232 q.save_dirty()
2233 2233 return 0
2234 2234
2235 2235 @command("^qrefresh",
2236 2236 [('e', 'edit', None, _('edit commit message')),
2237 2237 ('g', 'git', None, _('use git extended diff format')),
2238 2238 ('s', 'short', None,
2239 2239 _('refresh only files already in the patch and specified files')),
2240 2240 ('U', 'currentuser', None,
2241 2241 _('add/update author field in patch with current user')),
2242 2242 ('u', 'user', '',
2243 2243 _('add/update author field in patch with given user'), _('USER')),
2244 2244 ('D', 'currentdate', None,
2245 2245 _('add/update date field in patch with current date')),
2246 2246 ('d', 'date', '',
2247 2247 _('add/update date field in patch with given date'), _('DATE'))
2248 2248 ] + commands.walkopts + commands.commitopts,
2249 2249 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'))
2250 2250 def refresh(ui, repo, *pats, **opts):
2251 2251 """update the current patch
2252 2252
2253 2253 If any file patterns are provided, the refreshed patch will
2254 2254 contain only the modifications that match those patterns; the
2255 2255 remaining modifications will remain in the working directory.
2256 2256
2257 2257 If -s/--short is specified, files currently included in the patch
2258 2258 will be refreshed just like matched files and remain in the patch.
2259 2259
2260 2260 If -e/--edit is specified, Mercurial will start your configured editor for
2261 2261 you to enter a message. In case qrefresh fails, you will find a backup of
2262 2262 your message in ``.hg/last-message.txt``.
2263 2263
2264 2264 hg add/remove/copy/rename work as usual, though you might want to
2265 2265 use git-style patches (-g/--git or [diff] git=1) to track copies
2266 2266 and renames. See the diffs help topic for more information on the
2267 2267 git diff format.
2268 2268
2269 2269 Returns 0 on success.
2270 2270 """
2271 2271 q = repo.mq
2272 2272 message = cmdutil.logmessage(opts)
2273 2273 if opts.get('edit'):
2274 2274 if not q.applied:
2275 2275 ui.write(_("no patches applied\n"))
2276 2276 return 1
2277 2277 if message:
2278 2278 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
2279 2279 patch = q.applied[-1].name
2280 2280 ph = patchheader(q.join(patch), q.plainmode)
2281 2281 message = ui.edit('\n'.join(ph.message), ph.user or ui.username())
2282 2282 # We don't want to lose the patch message if qrefresh fails (issue2062)
2283 2283 msgfile = repo.opener('last-message.txt', 'wb')
2284 2284 msgfile.write(message)
2285 2285 msgfile.close()
2286 2286 setupheaderopts(ui, opts)
2287 2287 ret = q.refresh(repo, pats, msg=message, **opts)
2288 2288 q.save_dirty()
2289 2289 return ret
2290 2290
2291 2291 @command("^qdiff",
2292 2292 commands.diffopts + commands.diffopts2 + commands.walkopts,
2293 2293 _('hg qdiff [OPTION]... [FILE]...'))
2294 2294 def diff(ui, repo, *pats, **opts):
2295 2295 """diff of the current patch and subsequent modifications
2296 2296
2297 2297 Shows a diff which includes the current patch as well as any
2298 2298 changes which have been made in the working directory since the
2299 2299 last refresh (thus showing what the current patch would become
2300 2300 after a qrefresh).
2301 2301
2302 2302 Use :hg:`diff` if you only want to see the changes made since the
2303 2303 last qrefresh, or :hg:`export qtip` if you want to see changes
2304 2304 made by the current patch without including changes made since the
2305 2305 qrefresh.
2306 2306
2307 2307 Returns 0 on success.
2308 2308 """
2309 2309 repo.mq.diff(repo, pats, opts)
2310 2310 return 0
2311 2311
2312 2312 @command('qfold',
2313 2313 [('e', 'edit', None, _('edit patch header')),
2314 2314 ('k', 'keep', None, _('keep folded patch files')),
2315 2315 ] + commands.commitopts,
2316 2316 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'))
2317 2317 def fold(ui, repo, *files, **opts):
2318 2318 """fold the named patches into the current patch
2319 2319
2320 2320 Patches must not yet be applied. Each patch will be successively
2321 2321 applied to the current patch in the order given. If all the
2322 2322 patches apply successfully, the current patch will be refreshed
2323 2323 with the new cumulative patch, and the folded patches will be
2324 2324 deleted. With -k/--keep, the folded patch files will not be
2325 2325 removed afterwards.
2326 2326
2327 2327 The header for each folded patch will be concatenated with the
2328 2328 current patch header, separated by a line of ``* * *``.
2329 2329
2330 2330 Returns 0 on success."""
2331 2331
2332 2332 q = repo.mq
2333 2333
2334 2334 if not files:
2335 2335 raise util.Abort(_('qfold requires at least one patch name'))
2336 2336 if not q.check_toppatch(repo)[0]:
2337 2337 raise util.Abort(_('no patches applied'))
2338 2338 q.check_localchanges(repo)
2339 2339
2340 2340 message = cmdutil.logmessage(opts)
2341 2341 if opts.get('edit'):
2342 2342 if message:
2343 2343 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
2344 2344
2345 2345 parent = q.lookup('qtip')
2346 2346 patches = []
2347 2347 messages = []
2348 2348 for f in files:
2349 2349 p = q.lookup(f)
2350 2350 if p in patches or p == parent:
2351 2351 ui.warn(_('Skipping already folded patch %s\n') % p)
2352 2352 if q.isapplied(p):
2353 2353 raise util.Abort(_('qfold cannot fold already applied patch %s') % p)
2354 2354 patches.append(p)
2355 2355
2356 2356 for p in patches:
2357 2357 if not message:
2358 2358 ph = patchheader(q.join(p), q.plainmode)
2359 2359 if ph.message:
2360 2360 messages.append(ph.message)
2361 2361 pf = q.join(p)
2362 2362 (patchsuccess, files, fuzz) = q.patch(repo, pf)
2363 2363 if not patchsuccess:
2364 2364 raise util.Abort(_('error folding patch %s') % p)
2365 2365
2366 2366 if not message:
2367 2367 ph = patchheader(q.join(parent), q.plainmode)
2368 2368 message, user = ph.message, ph.user
2369 2369 for msg in messages:
2370 2370 message.append('* * *')
2371 2371 message.extend(msg)
2372 2372 message = '\n'.join(message)
2373 2373
2374 2374 if opts.get('edit'):
2375 2375 message = ui.edit(message, user or ui.username())
2376 2376
2377 2377 diffopts = q.patchopts(q.diffopts(), *patches)
2378 2378 q.refresh(repo, msg=message, git=diffopts.git)
2379 2379 q.delete(repo, patches, opts)
2380 2380 q.save_dirty()
2381 2381
2382 2382 @command("qgoto",
2383 2383 [('f', 'force', None, _('overwrite any local changes'))],
2384 2384 _('hg qgoto [OPTION]... PATCH'))
2385 2385 def goto(ui, repo, patch, **opts):
2386 2386 '''push or pop patches until named patch is at top of stack
2387 2387
2388 2388 Returns 0 on success.'''
2389 2389 q = repo.mq
2390 2390 patch = q.lookup(patch)
2391 2391 if q.isapplied(patch):
2392 2392 ret = q.pop(repo, patch, force=opts.get('force'))
2393 2393 else:
2394 2394 ret = q.push(repo, patch, force=opts.get('force'))
2395 2395 q.save_dirty()
2396 2396 return ret
2397 2397
2398 2398 @command("qguard",
2399 2399 [('l', 'list', None, _('list all patches and guards')),
2400 2400 ('n', 'none', None, _('drop all guards'))],
2401 2401 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'))
2402 2402 def guard(ui, repo, *args, **opts):
2403 2403 '''set or print guards for a patch
2404 2404
2405 2405 Guards control whether a patch can be pushed. A patch with no
2406 2406 guards is always pushed. A patch with a positive guard ("+foo") is
2407 2407 pushed only if the :hg:`qselect` command has activated it. A patch with
2408 2408 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
2409 2409 has activated it.
2410 2410
2411 2411 With no arguments, print the currently active guards.
2412 2412 With arguments, set guards for the named patch.
2413 2413
2414 2414 .. note::
2415 2415 Specifying negative guards now requires '--'.
2416 2416
2417 2417 To set guards on another patch::
2418 2418
2419 2419 hg qguard other.patch -- +2.6.17 -stable
2420 2420
2421 2421 Returns 0 on success.
2422 2422 '''
2423 2423 def status(idx):
2424 2424 guards = q.series_guards[idx] or ['unguarded']
2425 2425 if q.series[idx] in applied:
2426 2426 state = 'applied'
2427 2427 elif q.pushable(idx)[0]:
2428 2428 state = 'unapplied'
2429 2429 else:
2430 2430 state = 'guarded'
2431 2431 label = 'qguard.patch qguard.%s qseries.%s' % (state, state)
2432 2432 ui.write('%s: ' % ui.label(q.series[idx], label))
2433 2433
2434 2434 for i, guard in enumerate(guards):
2435 2435 if guard.startswith('+'):
2436 2436 ui.write(guard, label='qguard.positive')
2437 2437 elif guard.startswith('-'):
2438 2438 ui.write(guard, label='qguard.negative')
2439 2439 else:
2440 2440 ui.write(guard, label='qguard.unguarded')
2441 2441 if i != len(guards) - 1:
2442 2442 ui.write(' ')
2443 2443 ui.write('\n')
2444 2444 q = repo.mq
2445 2445 applied = set(p.name for p in q.applied)
2446 2446 patch = None
2447 2447 args = list(args)
2448 2448 if opts.get('list'):
2449 2449 if args or opts.get('none'):
2450 2450 raise util.Abort(_('cannot mix -l/--list with options or arguments'))
2451 2451 for i in xrange(len(q.series)):
2452 2452 status(i)
2453 2453 return
2454 2454 if not args or args[0][0:1] in '-+':
2455 2455 if not q.applied:
2456 2456 raise util.Abort(_('no patches applied'))
2457 2457 patch = q.applied[-1].name
2458 2458 if patch is None and args[0][0:1] not in '-+':
2459 2459 patch = args.pop(0)
2460 2460 if patch is None:
2461 2461 raise util.Abort(_('no patch to work with'))
2462 2462 if args or opts.get('none'):
2463 2463 idx = q.find_series(patch)
2464 2464 if idx is None:
2465 2465 raise util.Abort(_('no patch named %s') % patch)
2466 2466 q.set_guards(idx, args)
2467 2467 q.save_dirty()
2468 2468 else:
2469 2469 status(q.series.index(q.lookup(patch)))
2470 2470
2471 2471 @command("qheader", [], _('hg qheader [PATCH]'))
2472 2472 def header(ui, repo, patch=None):
2473 2473 """print the header of the topmost or specified patch
2474 2474
2475 2475 Returns 0 on success."""
2476 2476 q = repo.mq
2477 2477
2478 2478 if patch:
2479 2479 patch = q.lookup(patch)
2480 2480 else:
2481 2481 if not q.applied:
2482 2482 ui.write(_('no patches applied\n'))
2483 2483 return 1
2484 2484 patch = q.lookup('qtip')
2485 2485 ph = patchheader(q.join(patch), q.plainmode)
2486 2486
2487 2487 ui.write('\n'.join(ph.message) + '\n')
2488 2488
2489 2489 def lastsavename(path):
2490 2490 (directory, base) = os.path.split(path)
2491 2491 names = os.listdir(directory)
2492 2492 namere = re.compile("%s.([0-9]+)" % base)
2493 2493 maxindex = None
2494 2494 maxname = None
2495 2495 for f in names:
2496 2496 m = namere.match(f)
2497 2497 if m:
2498 2498 index = int(m.group(1))
2499 2499 if maxindex is None or index > maxindex:
2500 2500 maxindex = index
2501 2501 maxname = f
2502 2502 if maxname:
2503 2503 return (os.path.join(directory, maxname), maxindex)
2504 2504 return (None, None)
2505 2505
2506 2506 def savename(path):
2507 2507 (last, index) = lastsavename(path)
2508 2508 if last is None:
2509 2509 index = 0
2510 2510 newpath = path + ".%d" % (index + 1)
2511 2511 return newpath
2512 2512
2513 2513 @command("^qpush",
2514 2514 [('f', 'force', None, _('apply on top of local changes')),
2515 2515 ('e', 'exact', None, _('apply the target patch to its recorded parent')),
2516 2516 ('l', 'list', None, _('list patch name in commit text')),
2517 2517 ('a', 'all', None, _('apply all patches')),
2518 2518 ('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
2519 2519 ('n', 'name', '',
2520 2520 _('merge queue name (DEPRECATED)'), _('NAME')),
2521 2521 ('', 'move', None, _('reorder patch series and apply only the patch'))],
2522 2522 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
2523 2523 def push(ui, repo, patch=None, **opts):
2524 2524 """push the next patch onto the stack
2525 2525
2526 2526 When -f/--force is applied, all local changes in patched files
2527 2527 will be lost.
2528 2528
2529 2529 Return 0 on success.
2530 2530 """
2531 2531 q = repo.mq
2532 2532 mergeq = None
2533 2533
2534 2534 if opts.get('merge'):
2535 2535 if opts.get('name'):
2536 2536 newpath = repo.join(opts.get('name'))
2537 2537 else:
2538 2538 newpath, i = lastsavename(q.path)
2539 2539 if not newpath:
2540 2540 ui.warn(_("no saved queues found, please use -n\n"))
2541 2541 return 1
2542 2542 mergeq = queue(ui, repo.join(""), newpath)
2543 2543 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2544 2544 ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
2545 2545 mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
2546 2546 exact=opts.get('exact'))
2547 2547 return ret
2548 2548
2549 2549 @command("^qpop",
2550 2550 [('a', 'all', None, _('pop all patches')),
2551 2551 ('n', 'name', '',
2552 2552 _('queue name to pop (DEPRECATED)'), _('NAME')),
2553 2553 ('f', 'force', None, _('forget any local changes to patched files'))],
2554 2554 _('hg qpop [-a] [-f] [PATCH | INDEX]'))
2555 2555 def pop(ui, repo, patch=None, **opts):
2556 2556 """pop the current patch off the stack
2557 2557
2558 2558 By default, pops off the top of the patch stack. If given a patch
2559 2559 name, keeps popping off patches until the named patch is at the
2560 2560 top of the stack.
2561 2561
2562 2562 Return 0 on success.
2563 2563 """
2564 2564 localupdate = True
2565 2565 if opts.get('name'):
2566 2566 q = queue(ui, repo.join(""), repo.join(opts.get('name')))
2567 2567 ui.warn(_('using patch queue: %s\n') % q.path)
2568 2568 localupdate = False
2569 2569 else:
2570 2570 q = repo.mq
2571 2571 ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
2572 2572 all=opts.get('all'))
2573 2573 q.save_dirty()
2574 2574 return ret
2575 2575
2576 2576 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'))
2577 2577 def rename(ui, repo, patch, name=None, **opts):
2578 2578 """rename a patch
2579 2579
2580 2580 With one argument, renames the current patch to PATCH1.
2581 2581 With two arguments, renames PATCH1 to PATCH2.
2582 2582
2583 2583 Returns 0 on success."""
2584 2584
2585 2585 q = repo.mq
2586 2586
2587 2587 if not name:
2588 2588 name = patch
2589 2589 patch = None
2590 2590
2591 2591 if patch:
2592 2592 patch = q.lookup(patch)
2593 2593 else:
2594 2594 if not q.applied:
2595 2595 ui.write(_('no patches applied\n'))
2596 2596 return
2597 2597 patch = q.lookup('qtip')
2598 2598 absdest = q.join(name)
2599 2599 if os.path.isdir(absdest):
2600 2600 name = normname(os.path.join(name, os.path.basename(patch)))
2601 2601 absdest = q.join(name)
2602 2602 if os.path.exists(absdest):
2603 2603 raise util.Abort(_('%s already exists') % absdest)
2604 2604
2605 2605 if name in q.series:
2606 2606 raise util.Abort(
2607 2607 _('A patch named %s already exists in the series file') % name)
2608 2608
2609 2609 ui.note(_('renaming %s to %s\n') % (patch, name))
2610 2610 i = q.find_series(patch)
2611 2611 guards = q.guard_re.findall(q.full_series[i])
2612 2612 q.full_series[i] = name + ''.join([' #' + g for g in guards])
2613 2613 q.parse_series()
2614 2614 q.series_dirty = 1
2615 2615
2616 2616 info = q.isapplied(patch)
2617 2617 if info:
2618 2618 q.applied[info[0]] = statusentry(info[1], name)
2619 2619 q.applied_dirty = 1
2620 2620
2621 2621 destdir = os.path.dirname(absdest)
2622 2622 if not os.path.isdir(destdir):
2623 2623 os.makedirs(destdir)
2624 2624 util.rename(q.join(patch), absdest)
2625 2625 r = q.qrepo()
2626 2626 if r and patch in r.dirstate:
2627 2627 wctx = r[None]
2628 2628 wlock = r.wlock()
2629 2629 try:
2630 2630 if r.dirstate[patch] == 'a':
2631 2631 r.dirstate.forget(patch)
2632 2632 r.dirstate.add(name)
2633 2633 else:
2634 2634 if r.dirstate[name] == 'r':
2635 2635 wctx.undelete([name])
2636 2636 wctx.copy(patch, name)
2637 2637 wctx.remove([patch], False)
2638 2638 finally:
2639 2639 wlock.release()
2640 2640
2641 2641 q.save_dirty()
2642 2642
2643 2643 @command("qrestore",
2644 2644 [('d', 'delete', None, _('delete save entry')),
2645 2645 ('u', 'update', None, _('update queue working directory'))],
2646 2646 _('hg qrestore [-d] [-u] REV'))
2647 2647 def restore(ui, repo, rev, **opts):
2648 2648 """restore the queue state saved by a revision (DEPRECATED)
2649 2649
2650 2650 This command is deprecated, use :hg:`rebase` instead."""
2651 2651 rev = repo.lookup(rev)
2652 2652 q = repo.mq
2653 2653 q.restore(repo, rev, delete=opts.get('delete'),
2654 2654 qupdate=opts.get('update'))
2655 2655 q.save_dirty()
2656 2656 return 0
2657 2657
2658 2658 @command("qsave",
2659 2659 [('c', 'copy', None, _('copy patch directory')),
2660 2660 ('n', 'name', '',
2661 2661 _('copy directory name'), _('NAME')),
2662 2662 ('e', 'empty', None, _('clear queue status file')),
2663 2663 ('f', 'force', None, _('force copy'))] + commands.commitopts,
2664 2664 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'))
2665 2665 def save(ui, repo, **opts):
2666 2666 """save current queue state (DEPRECATED)
2667 2667
2668 2668 This command is deprecated, use :hg:`rebase` instead."""
2669 2669 q = repo.mq
2670 2670 message = cmdutil.logmessage(opts)
2671 2671 ret = q.save(repo, msg=message)
2672 2672 if ret:
2673 2673 return ret
2674 2674 q.save_dirty()
2675 2675 if opts.get('copy'):
2676 2676 path = q.path
2677 2677 if opts.get('name'):
2678 2678 newpath = os.path.join(q.basepath, opts.get('name'))
2679 2679 if os.path.exists(newpath):
2680 2680 if not os.path.isdir(newpath):
2681 2681 raise util.Abort(_('destination %s exists and is not '
2682 2682 'a directory') % newpath)
2683 2683 if not opts.get('force'):
2684 2684 raise util.Abort(_('destination %s exists, '
2685 2685 'use -f to force') % newpath)
2686 2686 else:
2687 2687 newpath = savename(path)
2688 2688 ui.warn(_("copy %s to %s\n") % (path, newpath))
2689 2689 util.copyfiles(path, newpath)
2690 2690 if opts.get('empty'):
2691 2691 try:
2692 2692 os.unlink(q.join(q.status_path))
2693 2693 except:
2694 2694 pass
2695 2695 return 0
2696 2696
2697 2697 @command("strip",
2698 2698 [('f', 'force', None, _('force removal of changesets, discard '
2699 2699 'uncommitted changes (no backup)')),
2700 2700 ('b', 'backup', None, _('bundle only changesets with local revision'
2701 2701 ' number greater than REV which are not'
2702 2702 ' descendants of REV (DEPRECATED)')),
2703 2703 ('n', 'no-backup', None, _('no backups')),
2704 2704 ('', 'nobackup', None, _('no backups (DEPRECATED)')),
2705 2705 ('k', 'keep', None, _("do not modify working copy during strip"))],
2706 2706 _('hg strip [-k] [-f] [-n] REV...'))
2707 2707 def strip(ui, repo, *revs, **opts):
2708 2708 """strip changesets and all their descendants from the repository
2709 2709
2710 2710 The strip command removes the specified changesets and all their
2711 2711 descendants. If the working directory has uncommitted changes, the
2712 2712 operation is aborted unless the --force flag is supplied, in which
2713 2713 case changes will be discarded.
2714 2714
2715 2715 If a parent of the working directory is stripped, then the working
2716 2716 directory will automatically be updated to the most recent
2717 2717 available ancestor of the stripped parent after the operation
2718 2718 completes.
2719 2719
2720 2720 Any stripped changesets are stored in ``.hg/strip-backup`` as a
2721 2721 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
2722 2722 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
2723 2723 where BUNDLE is the bundle file created by the strip. Note that
2724 2724 the local revision numbers will in general be different after the
2725 2725 restore.
2726 2726
2727 2727 Use the --no-backup option to discard the backup bundle once the
2728 2728 operation completes.
2729 2729
2730 2730 Return 0 on success.
2731 2731 """
2732 2732 backup = 'all'
2733 2733 if opts.get('backup'):
2734 2734 backup = 'strip'
2735 2735 elif opts.get('no_backup') or opts.get('nobackup'):
2736 2736 backup = 'none'
2737 2737
2738 2738 cl = repo.changelog
2739 2739 revs = set(scmutil.revrange(repo, revs))
2740 2740 if not revs:
2741 2741 raise util.Abort(_('empty revision set'))
2742 2742
2743 2743 descendants = set(cl.descendants(*revs))
2744 2744 strippedrevs = revs.union(descendants)
2745 2745 roots = revs.difference(descendants)
2746 2746
2747 2747 update = False
2748 2748 # if one of the wdir parent is stripped we'll need
2749 2749 # to update away to an earlier revision
2750 2750 for p in repo.dirstate.parents():
2751 2751 if p != nullid and cl.rev(p) in strippedrevs:
2752 2752 update = True
2753 2753 break
2754 2754
2755 2755 rootnodes = set(cl.node(r) for r in roots)
2756 2756
2757 2757 q = repo.mq
2758 2758 if q.applied:
2759 2759 # refresh queue state if we're about to strip
2760 2760 # applied patches
2761 2761 if cl.rev(repo.lookup('qtip')) in strippedrevs:
2762 2762 q.applied_dirty = True
2763 2763 start = 0
2764 2764 end = len(q.applied)
2765 2765 for i, statusentry in enumerate(q.applied):
2766 2766 if statusentry.node in rootnodes:
2767 2767 # if one of the stripped roots is an applied
2768 2768 # patch, only part of the queue is stripped
2769 2769 start = i
2770 2770 break
2771 2771 del q.applied[start:end]
2772 2772 q.save_dirty()
2773 2773
2774 2774 revs = list(rootnodes)
2775 2775 if update and opts.get('keep'):
2776 2776 wlock = repo.wlock()
2777 2777 try:
2778 2778 urev = repo.mq.qparents(repo, revs[0])
2779 2779 repo.dirstate.rebuild(urev, repo[urev].manifest())
2780 2780 repo.dirstate.write()
2781 2781 update = False
2782 2782 finally:
2783 2783 wlock.release()
2784 2784
2785 2785 repo.mq.strip(repo, revs, backup=backup, update=update,
2786 2786 force=opts.get('force'))
2787 2787 return 0
2788 2788
2789 2789 @command("qselect",
2790 2790 [('n', 'none', None, _('disable all guards')),
2791 2791 ('s', 'series', None, _('list all guards in series file')),
2792 2792 ('', 'pop', None, _('pop to before first guarded applied patch')),
2793 2793 ('', 'reapply', None, _('pop, then reapply patches'))],
2794 2794 _('hg qselect [OPTION]... [GUARD]...'))
2795 2795 def select(ui, repo, *args, **opts):
2796 2796 '''set or print guarded patches to push
2797 2797
2798 2798 Use the :hg:`qguard` command to set or print guards on patch, then use
2799 2799 qselect to tell mq which guards to use. A patch will be pushed if
2800 2800 it has no guards or any positive guards match the currently
2801 2801 selected guard, but will not be pushed if any negative guards
2802 2802 match the current guard. For example::
2803 2803
2804 2804 qguard foo.patch -- -stable (negative guard)
2805 2805 qguard bar.patch +stable (positive guard)
2806 2806 qselect stable
2807 2807
2808 2808 This activates the "stable" guard. mq will skip foo.patch (because
2809 2809 it has a negative match) but push bar.patch (because it has a
2810 2810 positive match).
2811 2811
2812 2812 With no arguments, prints the currently active guards.
2813 2813 With one argument, sets the active guard.
2814 2814
2815 2815 Use -n/--none to deactivate guards (no other arguments needed).
2816 2816 When no guards are active, patches with positive guards are
2817 2817 skipped and patches with negative guards are pushed.
2818 2818
2819 2819 qselect can change the guards on applied patches. It does not pop
2820 2820 guarded patches by default. Use --pop to pop back to the last
2821 2821 applied patch that is not guarded. Use --reapply (which implies
2822 2822 --pop) to push back to the current patch afterwards, but skip
2823 2823 guarded patches.
2824 2824
2825 2825 Use -s/--series to print a list of all guards in the series file
2826 2826 (no other arguments needed). Use -v for more information.
2827 2827
2828 2828 Returns 0 on success.'''
2829 2829
2830 2830 q = repo.mq
2831 2831 guards = q.active()
2832 2832 if args or opts.get('none'):
2833 2833 old_unapplied = q.unapplied(repo)
2834 2834 old_guarded = [i for i in xrange(len(q.applied)) if
2835 2835 not q.pushable(i)[0]]
2836 2836 q.set_active(args)
2837 2837 q.save_dirty()
2838 2838 if not args:
2839 2839 ui.status(_('guards deactivated\n'))
2840 2840 if not opts.get('pop') and not opts.get('reapply'):
2841 2841 unapplied = q.unapplied(repo)
2842 2842 guarded = [i for i in xrange(len(q.applied))
2843 2843 if not q.pushable(i)[0]]
2844 2844 if len(unapplied) != len(old_unapplied):
2845 2845 ui.status(_('number of unguarded, unapplied patches has '
2846 2846 'changed from %d to %d\n') %
2847 2847 (len(old_unapplied), len(unapplied)))
2848 2848 if len(guarded) != len(old_guarded):
2849 2849 ui.status(_('number of guarded, applied patches has changed '
2850 2850 'from %d to %d\n') %
2851 2851 (len(old_guarded), len(guarded)))
2852 2852 elif opts.get('series'):
2853 2853 guards = {}
2854 2854 noguards = 0
2855 2855 for gs in q.series_guards:
2856 2856 if not gs:
2857 2857 noguards += 1
2858 2858 for g in gs:
2859 2859 guards.setdefault(g, 0)
2860 2860 guards[g] += 1
2861 2861 if ui.verbose:
2862 2862 guards['NONE'] = noguards
2863 2863 guards = guards.items()
2864 2864 guards.sort(key=lambda x: x[0][1:])
2865 2865 if guards:
2866 2866 ui.note(_('guards in series file:\n'))
2867 2867 for guard, count in guards:
2868 2868 ui.note('%2d ' % count)
2869 2869 ui.write(guard, '\n')
2870 2870 else:
2871 2871 ui.note(_('no guards in series file\n'))
2872 2872 else:
2873 2873 if guards:
2874 2874 ui.note(_('active guards:\n'))
2875 2875 for g in guards:
2876 2876 ui.write(g, '\n')
2877 2877 else:
2878 2878 ui.write(_('no active guards\n'))
2879 2879 reapply = opts.get('reapply') and q.applied and q.appliedname(-1)
2880 2880 popped = False
2881 2881 if opts.get('pop') or opts.get('reapply'):
2882 2882 for i in xrange(len(q.applied)):
2883 2883 pushable, reason = q.pushable(i)
2884 2884 if not pushable:
2885 2885 ui.status(_('popping guarded patches\n'))
2886 2886 popped = True
2887 2887 if i == 0:
2888 2888 q.pop(repo, all=True)
2889 2889 else:
2890 2890 q.pop(repo, i - 1)
2891 2891 break
2892 2892 if popped:
2893 2893 try:
2894 2894 if reapply:
2895 2895 ui.status(_('reapplying unguarded patches\n'))
2896 2896 q.push(repo, reapply)
2897 2897 finally:
2898 2898 q.save_dirty()
2899 2899
2900 2900 @command("qfinish",
2901 2901 [('a', 'applied', None, _('finish all applied changesets'))],
2902 2902 _('hg qfinish [-a] [REV]...'))
2903 2903 def finish(ui, repo, *revrange, **opts):
2904 2904 """move applied patches into repository history
2905 2905
2906 2906 Finishes the specified revisions (corresponding to applied
2907 2907 patches) by moving them out of mq control into regular repository
2908 2908 history.
2909 2909
2910 2910 Accepts a revision range or the -a/--applied option. If --applied
2911 2911 is specified, all applied mq revisions are removed from mq
2912 2912 control. Otherwise, the given revisions must be at the base of the
2913 2913 stack of applied patches.
2914 2914
2915 2915 This can be especially useful if your changes have been applied to
2916 2916 an upstream repository, or if you are about to push your changes
2917 2917 to upstream.
2918 2918
2919 2919 Returns 0 on success.
2920 2920 """
2921 2921 if not opts.get('applied') and not revrange:
2922 2922 raise util.Abort(_('no revisions specified'))
2923 2923 elif opts.get('applied'):
2924 2924 revrange = ('qbase::qtip',) + revrange
2925 2925
2926 2926 q = repo.mq
2927 2927 if not q.applied:
2928 2928 ui.status(_('no patches applied\n'))
2929 2929 return 0
2930 2930
2931 2931 revs = scmutil.revrange(repo, revrange)
2932 2932 q.finish(repo, revs)
2933 2933 q.save_dirty()
2934 2934 return 0
2935 2935
2936 2936 @command("qqueue",
2937 2937 [('l', 'list', False, _('list all available queues')),
2938 2938 ('c', 'create', False, _('create new queue')),
2939 2939 ('', 'rename', False, _('rename active queue')),
2940 2940 ('', 'delete', False, _('delete reference to queue')),
2941 2941 ('', 'purge', False, _('delete queue, and remove patch dir')),
2942 2942 ],
2943 2943 _('[OPTION] [QUEUE]'))
2944 2944 def qqueue(ui, repo, name=None, **opts):
2945 2945 '''manage multiple patch queues
2946 2946
2947 2947 Supports switching between different patch queues, as well as creating
2948 2948 new patch queues and deleting existing ones.
2949 2949
2950 2950 Omitting a queue name or specifying -l/--list will show you the registered
2951 2951 queues - by default the "normal" patches queue is registered. The currently
2952 2952 active queue will be marked with "(active)".
2953 2953
2954 2954 To create a new queue, use -c/--create. The queue is automatically made
2955 2955 active, except in the case where there are applied patches from the
2956 2956 currently active queue in the repository. Then the queue will only be
2957 2957 created and switching will fail.
2958 2958
2959 2959 To delete an existing queue, use --delete. You cannot delete the currently
2960 2960 active queue.
2961 2961
2962 2962 Returns 0 on success.
2963 2963 '''
2964 2964
2965 2965 q = repo.mq
2966 2966
2967 2967 _defaultqueue = 'patches'
2968 2968 _allqueues = 'patches.queues'
2969 2969 _activequeue = 'patches.queue'
2970 2970
2971 2971 def _getcurrent():
2972 2972 cur = os.path.basename(q.path)
2973 2973 if cur.startswith('patches-'):
2974 2974 cur = cur[8:]
2975 2975 return cur
2976 2976
2977 2977 def _noqueues():
2978 2978 try:
2979 2979 fh = repo.opener(_allqueues, 'r')
2980 2980 fh.close()
2981 2981 except IOError:
2982 2982 return True
2983 2983
2984 2984 return False
2985 2985
2986 2986 def _getqueues():
2987 2987 current = _getcurrent()
2988 2988
2989 2989 try:
2990 2990 fh = repo.opener(_allqueues, 'r')
2991 2991 queues = [queue.strip() for queue in fh if queue.strip()]
2992 2992 fh.close()
2993 2993 if current not in queues:
2994 2994 queues.append(current)
2995 2995 except IOError:
2996 2996 queues = [_defaultqueue]
2997 2997
2998 2998 return sorted(queues)
2999 2999
3000 3000 def _setactive(name):
3001 3001 if q.applied:
3002 3002 raise util.Abort(_('patches applied - cannot set new queue active'))
3003 3003 _setactivenocheck(name)
3004 3004
3005 3005 def _setactivenocheck(name):
3006 3006 fh = repo.opener(_activequeue, 'w')
3007 3007 if name != 'patches':
3008 3008 fh.write(name)
3009 3009 fh.close()
3010 3010
3011 3011 def _addqueue(name):
3012 3012 fh = repo.opener(_allqueues, 'a')
3013 3013 fh.write('%s\n' % (name,))
3014 3014 fh.close()
3015 3015
3016 3016 def _queuedir(name):
3017 3017 if name == 'patches':
3018 3018 return repo.join('patches')
3019 3019 else:
3020 3020 return repo.join('patches-' + name)
3021 3021
3022 3022 def _validname(name):
3023 3023 for n in name:
3024 3024 if n in ':\\/.':
3025 3025 return False
3026 3026 return True
3027 3027
3028 3028 def _delete(name):
3029 3029 if name not in existing:
3030 3030 raise util.Abort(_('cannot delete queue that does not exist'))
3031 3031
3032 3032 current = _getcurrent()
3033 3033
3034 3034 if name == current:
3035 3035 raise util.Abort(_('cannot delete currently active queue'))
3036 3036
3037 3037 fh = repo.opener('patches.queues.new', 'w')
3038 3038 for queue in existing:
3039 3039 if queue == name:
3040 3040 continue
3041 3041 fh.write('%s\n' % (queue,))
3042 3042 fh.close()
3043 3043 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3044 3044
3045 3045 if not name or opts.get('list'):
3046 3046 current = _getcurrent()
3047 3047 for queue in _getqueues():
3048 3048 ui.write('%s' % (queue,))
3049 3049 if queue == current and not ui.quiet:
3050 3050 ui.write(_(' (active)\n'))
3051 3051 else:
3052 3052 ui.write('\n')
3053 3053 return
3054 3054
3055 3055 if not _validname(name):
3056 3056 raise util.Abort(
3057 3057 _('invalid queue name, may not contain the characters ":\\/."'))
3058 3058
3059 3059 existing = _getqueues()
3060 3060
3061 3061 if opts.get('create'):
3062 3062 if name in existing:
3063 3063 raise util.Abort(_('queue "%s" already exists') % name)
3064 3064 if _noqueues():
3065 3065 _addqueue(_defaultqueue)
3066 3066 _addqueue(name)
3067 3067 _setactive(name)
3068 3068 elif opts.get('rename'):
3069 3069 current = _getcurrent()
3070 3070 if name == current:
3071 3071 raise util.Abort(_('can\'t rename "%s" to its current name') % name)
3072 3072 if name in existing:
3073 3073 raise util.Abort(_('queue "%s" already exists') % name)
3074 3074
3075 3075 olddir = _queuedir(current)
3076 3076 newdir = _queuedir(name)
3077 3077
3078 3078 if os.path.exists(newdir):
3079 3079 raise util.Abort(_('non-queue directory "%s" already exists') %
3080 3080 newdir)
3081 3081
3082 3082 fh = repo.opener('patches.queues.new', 'w')
3083 3083 for queue in existing:
3084 3084 if queue == current:
3085 3085 fh.write('%s\n' % (name,))
3086 3086 if os.path.exists(olddir):
3087 3087 util.rename(olddir, newdir)
3088 3088 else:
3089 3089 fh.write('%s\n' % (queue,))
3090 3090 fh.close()
3091 3091 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3092 3092 _setactivenocheck(name)
3093 3093 elif opts.get('delete'):
3094 3094 _delete(name)
3095 3095 elif opts.get('purge'):
3096 3096 if name in existing:
3097 3097 _delete(name)
3098 3098 qdir = _queuedir(name)
3099 3099 if os.path.exists(qdir):
3100 3100 shutil.rmtree(qdir)
3101 3101 else:
3102 3102 if name not in existing:
3103 3103 raise util.Abort(_('use --create to create a new queue'))
3104 3104 _setactive(name)
3105 3105
3106 3106 def reposetup(ui, repo):
3107 3107 class mqrepo(repo.__class__):
3108 3108 @util.propertycache
3109 3109 def mq(self):
3110 3110 return queue(self.ui, self.join(""))
3111 3111
3112 3112 def abort_if_wdir_patched(self, errmsg, force=False):
3113 3113 if self.mq.applied and not force:
3114 3114 parents = self.dirstate.parents()
3115 3115 patches = [s.node for s in self.mq.applied]
3116 3116 if parents[0] in patches or parents[1] in patches:
3117 3117 raise util.Abort(errmsg)
3118 3118
3119 3119 def commit(self, text="", user=None, date=None, match=None,
3120 3120 force=False, editor=False, extra={}):
3121 3121 self.abort_if_wdir_patched(
3122 3122 _('cannot commit over an applied mq patch'),
3123 3123 force)
3124 3124
3125 3125 return super(mqrepo, self).commit(text, user, date, match, force,
3126 3126 editor, extra)
3127 3127
3128 3128 def checkpush(self, force, revs):
3129 3129 if self.mq.applied and not force:
3130 3130 haspatches = True
3131 3131 if revs:
3132 3132 # Assume applied patches have no non-patch descendants
3133 3133 # and are not on remote already. If they appear in the
3134 3134 # set of resolved 'revs', bail out.
3135 3135 applied = set(e.node for e in self.mq.applied)
3136 3136 haspatches = bool([n for n in revs if n in applied])
3137 3137 if haspatches:
3138 3138 raise util.Abort(_('source has mq patches applied'))
3139 3139 super(mqrepo, self).checkpush(force, revs)
3140 3140
3141 3141 def _findtags(self):
3142 3142 '''augment tags from base class with patch tags'''
3143 3143 result = super(mqrepo, self)._findtags()
3144 3144
3145 3145 q = self.mq
3146 3146 if not q.applied:
3147 3147 return result
3148 3148
3149 3149 mqtags = [(patch.node, patch.name) for patch in q.applied]
3150 3150
3151 3151 try:
3152 3152 self.changelog.rev(mqtags[-1][0])
3153 3153 except error.RepoLookupError:
3154 3154 self.ui.warn(_('mq status file refers to unknown node %s\n')
3155 3155 % short(mqtags[-1][0]))
3156 3156 return result
3157 3157
3158 3158 mqtags.append((mqtags[-1][0], 'qtip'))
3159 3159 mqtags.append((mqtags[0][0], 'qbase'))
3160 3160 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
3161 3161 tags = result[0]
3162 3162 for patch in mqtags:
3163 3163 if patch[1] in tags:
3164 3164 self.ui.warn(_('Tag %s overrides mq patch of the same name\n')
3165 3165 % patch[1])
3166 3166 else:
3167 3167 tags[patch[1]] = patch[0]
3168 3168
3169 3169 return result
3170 3170
3171 3171 def _branchtags(self, partial, lrev):
3172 3172 q = self.mq
3173 3173 if not q.applied:
3174 3174 return super(mqrepo, self)._branchtags(partial, lrev)
3175 3175
3176 3176 cl = self.changelog
3177 3177 qbasenode = q.applied[0].node
3178 3178 try:
3179 3179 qbase = cl.rev(qbasenode)
3180 3180 except error.LookupError:
3181 3181 self.ui.warn(_('mq status file refers to unknown node %s\n')
3182 3182 % short(qbasenode))
3183 3183 return super(mqrepo, self)._branchtags(partial, lrev)
3184 3184
3185 3185 start = lrev + 1
3186 3186 if start < qbase:
3187 3187 # update the cache (excluding the patches) and save it
3188 3188 ctxgen = (self[r] for r in xrange(lrev + 1, qbase))
3189 3189 self._updatebranchcache(partial, ctxgen)
3190 3190 self._writebranchcache(partial, cl.node(qbase - 1), qbase - 1)
3191 3191 start = qbase
3192 3192 # if start = qbase, the cache is as updated as it should be.
3193 3193 # if start > qbase, the cache includes (part of) the patches.
3194 3194 # we might as well use it, but we won't save it.
3195 3195
3196 3196 # update the cache up to the tip
3197 3197 ctxgen = (self[r] for r in xrange(start, len(cl)))
3198 3198 self._updatebranchcache(partial, ctxgen)
3199 3199
3200 3200 return partial
3201 3201
3202 3202 if repo.local():
3203 3203 repo.__class__ = mqrepo
3204 3204
3205 3205 def mqimport(orig, ui, repo, *args, **kwargs):
3206 3206 if (hasattr(repo, 'abort_if_wdir_patched')
3207 3207 and not kwargs.get('no_commit', False)):
3208 3208 repo.abort_if_wdir_patched(_('cannot import over an applied patch'),
3209 3209 kwargs.get('force'))
3210 3210 return orig(ui, repo, *args, **kwargs)
3211 3211
3212 3212 def mqinit(orig, ui, *args, **kwargs):
3213 3213 mq = kwargs.pop('mq', None)
3214 3214
3215 3215 if not mq:
3216 3216 return orig(ui, *args, **kwargs)
3217 3217
3218 3218 if args:
3219 3219 repopath = args[0]
3220 3220 if not hg.islocal(repopath):
3221 3221 raise util.Abort(_('only a local queue repository '
3222 3222 'may be initialized'))
3223 3223 else:
3224 3224 repopath = cmdutil.findrepo(os.getcwd())
3225 3225 if not repopath:
3226 3226 raise util.Abort(_('there is no Mercurial repository here '
3227 3227 '(.hg not found)'))
3228 3228 repo = hg.repository(ui, repopath)
3229 3229 return qinit(ui, repo, True)
3230 3230
3231 3231 def mqcommand(orig, ui, repo, *args, **kwargs):
3232 3232 """Add --mq option to operate on patch repository instead of main"""
3233 3233
3234 3234 # some commands do not like getting unknown options
3235 3235 mq = kwargs.pop('mq', None)
3236 3236
3237 3237 if not mq:
3238 3238 return orig(ui, repo, *args, **kwargs)
3239 3239
3240 3240 q = repo.mq
3241 3241 r = q.qrepo()
3242 3242 if not r:
3243 3243 raise util.Abort(_('no queue repository'))
3244 3244 return orig(r.ui, r, *args, **kwargs)
3245 3245
3246 3246 def summary(orig, ui, repo, *args, **kwargs):
3247 3247 r = orig(ui, repo, *args, **kwargs)
3248 3248 q = repo.mq
3249 3249 m = []
3250 3250 a, u = len(q.applied), len(q.unapplied(repo))
3251 3251 if a:
3252 3252 m.append(ui.label(_("%d applied"), 'qseries.applied') % a)
3253 3253 if u:
3254 3254 m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u)
3255 3255 if m:
3256 3256 ui.write("mq: %s\n" % ', '.join(m))
3257 3257 else:
3258 3258 ui.note(_("mq: (empty queue)\n"))
3259 3259 return r
3260 3260
3261 3261 def revsetmq(repo, subset, x):
3262 3262 """``mq()``
3263 3263 Changesets managed by MQ.
3264 3264 """
3265 3265 revset.getargs(x, 0, 0, _("mq takes no arguments"))
3266 3266 applied = set([repo[r.node].rev() for r in repo.mq.applied])
3267 3267 return [r for r in subset if r in applied]
3268 3268
3269 3269 def extsetup(ui):
3270 3270 revset.symbols['mq'] = revsetmq
3271 3271
3272 3272 # tell hggettext to extract docstrings from these functions:
3273 3273 i18nfunctions = [revsetmq]
3274 3274
3275 3275 def uisetup(ui):
3276 3276 mqopt = [('', 'mq', None, _("operate on patch repository"))]
3277 3277
3278 3278 extensions.wrapcommand(commands.table, 'import', mqimport)
3279 3279 extensions.wrapcommand(commands.table, 'summary', summary)
3280 3280
3281 3281 entry = extensions.wrapcommand(commands.table, 'init', mqinit)
3282 3282 entry[1].extend(mqopt)
3283 3283
3284 3284 nowrap = set(commands.norepo.split(" ") + ['qrecord'])
3285 3285
3286 3286 def dotable(cmdtable):
3287 3287 for cmd in cmdtable.keys():
3288 3288 cmd = cmdutil.parsealiases(cmd)[0]
3289 3289 if cmd in nowrap:
3290 3290 continue
3291 3291 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
3292 3292 entry[1].extend(mqopt)
3293 3293
3294 3294 dotable(commands.table)
3295 3295
3296 3296 for extname, extmodule in extensions.extensions():
3297 3297 if extmodule.__file__ != __file__:
3298 3298 dotable(getattr(extmodule, 'cmdtable', {}))
3299 3299
3300 3300
3301 3301 colortable = {'qguard.negative': 'red',
3302 3302 'qguard.positive': 'yellow',
3303 3303 'qguard.unguarded': 'green',
3304 3304 'qseries.applied': 'blue bold underline',
3305 3305 'qseries.guarded': 'black bold',
3306 3306 'qseries.missing': 'red bold',
3307 3307 'qseries.unapplied': 'black bold'}
@@ -1,1745 +1,1752 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import cStringIO, email.Parser, os, errno, re
10 10 import tempfile, zlib
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
15 15
16 16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 17
18 18 class PatchError(Exception):
19 19 pass
20 20
21 21
22 22 # public functions
23 23
24 24 def split(stream):
25 25 '''return an iterator of individual patches from a stream'''
26 26 def isheader(line, inheader):
27 27 if inheader and line[0] in (' ', '\t'):
28 28 # continuation
29 29 return True
30 30 if line[0] in (' ', '-', '+'):
31 31 # diff line - don't check for header pattern in there
32 32 return False
33 33 l = line.split(': ', 1)
34 34 return len(l) == 2 and ' ' not in l[0]
35 35
36 36 def chunk(lines):
37 37 return cStringIO.StringIO(''.join(lines))
38 38
39 39 def hgsplit(stream, cur):
40 40 inheader = True
41 41
42 42 for line in stream:
43 43 if not line.strip():
44 44 inheader = False
45 45 if not inheader and line.startswith('# HG changeset patch'):
46 46 yield chunk(cur)
47 47 cur = []
48 48 inheader = True
49 49
50 50 cur.append(line)
51 51
52 52 if cur:
53 53 yield chunk(cur)
54 54
55 55 def mboxsplit(stream, cur):
56 56 for line in stream:
57 57 if line.startswith('From '):
58 58 for c in split(chunk(cur[1:])):
59 59 yield c
60 60 cur = []
61 61
62 62 cur.append(line)
63 63
64 64 if cur:
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67
68 68 def mimesplit(stream, cur):
69 69 def msgfp(m):
70 70 fp = cStringIO.StringIO()
71 71 g = email.Generator.Generator(fp, mangle_from_=False)
72 72 g.flatten(m)
73 73 fp.seek(0)
74 74 return fp
75 75
76 76 for line in stream:
77 77 cur.append(line)
78 78 c = chunk(cur)
79 79
80 80 m = email.Parser.Parser().parse(c)
81 81 if not m.is_multipart():
82 82 yield msgfp(m)
83 83 else:
84 84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
85 85 for part in m.walk():
86 86 ct = part.get_content_type()
87 87 if ct not in ok_types:
88 88 continue
89 89 yield msgfp(part)
90 90
91 91 def headersplit(stream, cur):
92 92 inheader = False
93 93
94 94 for line in stream:
95 95 if not inheader and isheader(line, inheader):
96 96 yield chunk(cur)
97 97 cur = []
98 98 inheader = True
99 99 if inheader and not isheader(line, inheader):
100 100 inheader = False
101 101
102 102 cur.append(line)
103 103
104 104 if cur:
105 105 yield chunk(cur)
106 106
107 107 def remainder(cur):
108 108 yield chunk(cur)
109 109
110 110 class fiter(object):
111 111 def __init__(self, fp):
112 112 self.fp = fp
113 113
114 114 def __iter__(self):
115 115 return self
116 116
117 117 def next(self):
118 118 l = self.fp.readline()
119 119 if not l:
120 120 raise StopIteration
121 121 return l
122 122
123 123 inheader = False
124 124 cur = []
125 125
126 126 mimeheaders = ['content-type']
127 127
128 128 if not hasattr(stream, 'next'):
129 129 # http responses, for example, have readline but not next
130 130 stream = fiter(stream)
131 131
132 132 for line in stream:
133 133 cur.append(line)
134 134 if line.startswith('# HG changeset patch'):
135 135 return hgsplit(stream, cur)
136 136 elif line.startswith('From '):
137 137 return mboxsplit(stream, cur)
138 138 elif isheader(line, inheader):
139 139 inheader = True
140 140 if line.split(':', 1)[0].lower() in mimeheaders:
141 141 # let email parser handle this
142 142 return mimesplit(stream, cur)
143 143 elif line.startswith('--- ') and inheader:
144 144 # No evil headers seen by diff start, split by hand
145 145 return headersplit(stream, cur)
146 146 # Not enough info, keep reading
147 147
148 148 # if we are here, we have a very plain patch
149 149 return remainder(cur)
150 150
151 151 def extract(ui, fileobj):
152 152 '''extract patch from data read from fileobj.
153 153
154 154 patch can be a normal patch or contained in an email message.
155 155
156 156 return tuple (filename, message, user, date, branch, node, p1, p2).
157 157 Any item in the returned tuple can be None. If filename is None,
158 158 fileobj did not contain a patch. Caller must unlink filename when done.'''
159 159
160 160 # attempt to detect the start of a patch
161 161 # (this heuristic is borrowed from quilt)
162 162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
163 163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
164 164 r'---[ \t].*?^\+\+\+[ \t]|'
165 165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
166 166
167 167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
168 168 tmpfp = os.fdopen(fd, 'w')
169 169 try:
170 170 msg = email.Parser.Parser().parse(fileobj)
171 171
172 172 subject = msg['Subject']
173 173 user = msg['From']
174 174 if not subject and not user:
175 175 # Not an email, restore parsed headers if any
176 176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
177 177
178 178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
179 179 # should try to parse msg['Date']
180 180 date = None
181 181 nodeid = None
182 182 branch = None
183 183 parents = []
184 184
185 185 if subject:
186 186 if subject.startswith('[PATCH'):
187 187 pend = subject.find(']')
188 188 if pend >= 0:
189 189 subject = subject[pend + 1:].lstrip()
190 190 subject = subject.replace('\n\t', ' ')
191 191 ui.debug('Subject: %s\n' % subject)
192 192 if user:
193 193 ui.debug('From: %s\n' % user)
194 194 diffs_seen = 0
195 195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
196 196 message = ''
197 197 for part in msg.walk():
198 198 content_type = part.get_content_type()
199 199 ui.debug('Content-Type: %s\n' % content_type)
200 200 if content_type not in ok_types:
201 201 continue
202 202 payload = part.get_payload(decode=True)
203 203 m = diffre.search(payload)
204 204 if m:
205 205 hgpatch = False
206 206 hgpatchheader = False
207 207 ignoretext = False
208 208
209 209 ui.debug('found patch at byte %d\n' % m.start(0))
210 210 diffs_seen += 1
211 211 cfp = cStringIO.StringIO()
212 212 for line in payload[:m.start(0)].splitlines():
213 213 if line.startswith('# HG changeset patch') and not hgpatch:
214 214 ui.debug('patch generated by hg export\n')
215 215 hgpatch = True
216 216 hgpatchheader = True
217 217 # drop earlier commit message content
218 218 cfp.seek(0)
219 219 cfp.truncate()
220 220 subject = None
221 221 elif hgpatchheader:
222 222 if line.startswith('# User '):
223 223 user = line[7:]
224 224 ui.debug('From: %s\n' % user)
225 225 elif line.startswith("# Date "):
226 226 date = line[7:]
227 227 elif line.startswith("# Branch "):
228 228 branch = line[9:]
229 229 elif line.startswith("# Node ID "):
230 230 nodeid = line[10:]
231 231 elif line.startswith("# Parent "):
232 232 parents.append(line[10:])
233 233 elif not line.startswith("# "):
234 234 hgpatchheader = False
235 235 elif line == '---' and gitsendmail:
236 236 ignoretext = True
237 237 if not hgpatchheader and not ignoretext:
238 238 cfp.write(line)
239 239 cfp.write('\n')
240 240 message = cfp.getvalue()
241 241 if tmpfp:
242 242 tmpfp.write(payload)
243 243 if not payload.endswith('\n'):
244 244 tmpfp.write('\n')
245 245 elif not diffs_seen and message and content_type == 'text/plain':
246 246 message += '\n' + payload
247 247 except:
248 248 tmpfp.close()
249 249 os.unlink(tmpname)
250 250 raise
251 251
252 252 if subject and not message.startswith(subject):
253 253 message = '%s\n%s' % (subject, message)
254 254 tmpfp.close()
255 255 if not diffs_seen:
256 256 os.unlink(tmpname)
257 257 return None, message, user, date, branch, None, None, None
258 258 p1 = parents and parents.pop(0) or None
259 259 p2 = parents and parents.pop(0) or None
260 260 return tmpname, message, user, date, branch, nodeid, p1, p2
261 261
262 262 class patchmeta(object):
263 263 """Patched file metadata
264 264
265 265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
266 266 or COPY. 'path' is patched file path. 'oldpath' is set to the
267 267 origin file when 'op' is either COPY or RENAME, None otherwise. If
268 268 file mode is changed, 'mode' is a tuple (islink, isexec) where
269 269 'islink' is True if the file is a symlink and 'isexec' is True if
270 270 the file is executable. Otherwise, 'mode' is None.
271 271 """
272 272 def __init__(self, path):
273 273 self.path = path
274 274 self.oldpath = None
275 275 self.mode = None
276 276 self.op = 'MODIFY'
277 277 self.binary = False
278 278
279 279 def setmode(self, mode):
280 280 islink = mode & 020000
281 281 isexec = mode & 0100
282 282 self.mode = (islink, isexec)
283 283
284 284 def __repr__(self):
285 285 return "<patchmeta %s %r>" % (self.op, self.path)
286 286
287 287 def readgitpatch(lr):
288 288 """extract git-style metadata about patches from <patchname>"""
289 289
290 290 # Filter patch for git information
291 291 gp = None
292 292 gitpatches = []
293 293 for line in lr:
294 294 line = line.rstrip(' \r\n')
295 295 if line.startswith('diff --git'):
296 296 m = gitre.match(line)
297 297 if m:
298 298 if gp:
299 299 gitpatches.append(gp)
300 300 dst = m.group(2)
301 301 gp = patchmeta(dst)
302 302 elif gp:
303 303 if line.startswith('--- '):
304 304 gitpatches.append(gp)
305 305 gp = None
306 306 continue
307 307 if line.startswith('rename from '):
308 308 gp.op = 'RENAME'
309 309 gp.oldpath = line[12:]
310 310 elif line.startswith('rename to '):
311 311 gp.path = line[10:]
312 312 elif line.startswith('copy from '):
313 313 gp.op = 'COPY'
314 314 gp.oldpath = line[10:]
315 315 elif line.startswith('copy to '):
316 316 gp.path = line[8:]
317 317 elif line.startswith('deleted file'):
318 318 gp.op = 'DELETE'
319 319 elif line.startswith('new file mode '):
320 320 gp.op = 'ADD'
321 321 gp.setmode(int(line[-6:], 8))
322 322 elif line.startswith('new mode '):
323 323 gp.setmode(int(line[-6:], 8))
324 324 elif line.startswith('GIT binary patch'):
325 325 gp.binary = True
326 326 if gp:
327 327 gitpatches.append(gp)
328 328
329 329 return gitpatches
330 330
331 331 class linereader(object):
332 332 # simple class to allow pushing lines back into the input stream
333 333 def __init__(self, fp, textmode=False):
334 334 self.fp = fp
335 335 self.buf = []
336 336 self.textmode = textmode
337 337 self.eol = None
338 338
339 339 def push(self, line):
340 340 if line is not None:
341 341 self.buf.append(line)
342 342
343 343 def readline(self):
344 344 if self.buf:
345 345 l = self.buf[0]
346 346 del self.buf[0]
347 347 return l
348 348 l = self.fp.readline()
349 349 if not self.eol:
350 350 if l.endswith('\r\n'):
351 351 self.eol = '\r\n'
352 352 elif l.endswith('\n'):
353 353 self.eol = '\n'
354 354 if self.textmode and l.endswith('\r\n'):
355 355 l = l[:-2] + '\n'
356 356 return l
357 357
358 358 def __iter__(self):
359 359 while 1:
360 360 l = self.readline()
361 361 if not l:
362 362 break
363 363 yield l
364 364
365 365 class abstractbackend(object):
366 366 def __init__(self, ui):
367 367 self.ui = ui
368 368
369 369 def readlines(self, fname):
370 370 """Return target file lines, or its content as a single line
371 371 for symlinks.
372 372 """
373 373 raise NotImplementedError
374 374
375 375 def writelines(self, fname, lines):
376 376 """Write lines to target file."""
377 377 raise NotImplementedError
378 378
379 379 def unlink(self, fname):
380 380 """Unlink target file."""
381 381 raise NotImplementedError
382 382
383 383 def writerej(self, fname, failed, total, lines):
384 384 """Write rejected lines for fname. total is the number of hunks
385 385 which failed to apply and total the total number of hunks for this
386 386 files.
387 387 """
388 388 pass
389 389
390 390 def copy(self, src, dst):
391 391 """Copy src file into dst file. Create intermediate directories if
392 392 necessary. Files are specified relatively to the patching base
393 393 directory.
394 394 """
395 395 raise NotImplementedError
396 396
397 def exists(self, fname):
398 raise NotImplementedError
399
397 400 class fsbackend(abstractbackend):
398 401 def __init__(self, ui, basedir):
399 402 super(fsbackend, self).__init__(ui)
400 403 self.opener = scmutil.opener(basedir)
401 404
402 405 def readlines(self, fname):
403 406 if os.path.islink(fname):
404 407 return [os.readlink(fname)]
405 408 fp = self.opener(fname, 'r')
406 409 try:
407 410 return list(fp)
408 411 finally:
409 412 fp.close()
410 413
411 414 def writelines(self, fname, lines):
412 415 # Ensure supplied data ends in fname, being a regular file or
413 416 # a symlink. _updatedir will -too magically- take care
414 417 # of setting it to the proper type afterwards.
415 418 st_mode = None
416 419 islink = os.path.islink(fname)
417 420 if islink:
418 421 fp = cStringIO.StringIO()
419 422 else:
420 423 try:
421 424 st_mode = os.lstat(fname).st_mode & 0777
422 425 except OSError, e:
423 426 if e.errno != errno.ENOENT:
424 427 raise
425 428 fp = self.opener(fname, 'w')
426 429 try:
427 430 fp.writelines(lines)
428 431 if islink:
429 432 self.opener.symlink(fp.getvalue(), fname)
430 433 if st_mode is not None:
431 434 os.chmod(fname, st_mode)
432 435 finally:
433 436 fp.close()
434 437
435 438 def unlink(self, fname):
436 439 os.unlink(fname)
437 440
438 441 def writerej(self, fname, failed, total, lines):
439 442 fname = fname + ".rej"
440 443 self.ui.warn(
441 444 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
442 445 (failed, total, fname))
443 446 fp = self.opener(fname, 'w')
444 447 fp.writelines(lines)
445 448 fp.close()
446 449
447 450 def copy(self, src, dst):
448 451 basedir = self.opener.base
449 452 abssrc, absdst = [scmutil.canonpath(basedir, basedir, x)
450 453 for x in [src, dst]]
451 454 if os.path.lexists(absdst):
452 455 raise util.Abort(_("cannot create %s: destination already exists")
453 456 % dst)
454 457 dstdir = os.path.dirname(absdst)
455 458 if dstdir and not os.path.isdir(dstdir):
456 459 try:
457 460 os.makedirs(dstdir)
458 461 except IOError:
459 462 raise util.Abort(
460 463 _("cannot create %s: unable to create destination directory")
461 464 % dst)
462 465 util.copyfile(abssrc, absdst)
463 466
467 def exists(self, fname):
468 return os.path.lexists(fname)
469
464 470 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
465 471 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
466 472 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
467 473 eolmodes = ['strict', 'crlf', 'lf', 'auto']
468 474
469 475 class patchfile(object):
470 476 def __init__(self, ui, fname, backend, missing=False, eolmode='strict'):
471 477 self.fname = fname
472 478 self.eolmode = eolmode
473 479 self.eol = None
474 480 self.backend = backend
475 481 self.ui = ui
476 482 self.lines = []
477 483 self.exists = False
478 484 self.missing = missing
479 485 if not missing:
480 486 try:
481 487 self.lines = self.backend.readlines(fname)
482 488 if self.lines:
483 489 # Normalize line endings
484 490 if self.lines[0].endswith('\r\n'):
485 491 self.eol = '\r\n'
486 492 elif self.lines[0].endswith('\n'):
487 493 self.eol = '\n'
488 494 if eolmode != 'strict':
489 495 nlines = []
490 496 for l in self.lines:
491 497 if l.endswith('\r\n'):
492 498 l = l[:-2] + '\n'
493 499 nlines.append(l)
494 500 self.lines = nlines
495 501 self.exists = True
496 502 except IOError:
497 503 pass
498 504 else:
499 505 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
500 506
501 507 self.hash = {}
502 508 self.dirty = 0
503 509 self.offset = 0
504 510 self.skew = 0
505 511 self.rej = []
506 512 self.fileprinted = False
507 513 self.printfile(False)
508 514 self.hunks = 0
509 515
510 516 def writelines(self, fname, lines):
511 517 if self.eolmode == 'auto':
512 518 eol = self.eol
513 519 elif self.eolmode == 'crlf':
514 520 eol = '\r\n'
515 521 else:
516 522 eol = '\n'
517 523
518 524 if self.eolmode != 'strict' and eol and eol != '\n':
519 525 rawlines = []
520 526 for l in lines:
521 527 if l and l[-1] == '\n':
522 528 l = l[:-1] + eol
523 529 rawlines.append(l)
524 530 lines = rawlines
525 531
526 532 self.backend.writelines(fname, lines)
527 533
528 534 def printfile(self, warn):
529 535 if self.fileprinted:
530 536 return
531 537 if warn or self.ui.verbose:
532 538 self.fileprinted = True
533 539 s = _("patching file %s\n") % self.fname
534 540 if warn:
535 541 self.ui.warn(s)
536 542 else:
537 543 self.ui.note(s)
538 544
539 545
540 546 def findlines(self, l, linenum):
541 547 # looks through the hash and finds candidate lines. The
542 548 # result is a list of line numbers sorted based on distance
543 549 # from linenum
544 550
545 551 cand = self.hash.get(l, [])
546 552 if len(cand) > 1:
547 553 # resort our list of potentials forward then back.
548 554 cand.sort(key=lambda x: abs(x - linenum))
549 555 return cand
550 556
551 557 def write_rej(self):
552 558 # our rejects are a little different from patch(1). This always
553 559 # creates rejects in the same form as the original patch. A file
554 560 # header is inserted so that you can run the reject through patch again
555 561 # without having to type the filename.
556 562 if not self.rej:
557 563 return
558 564 base = os.path.basename(self.fname)
559 565 lines = ["--- %s\n+++ %s\n" % (base, base)]
560 566 for x in self.rej:
561 567 for l in x.hunk:
562 568 lines.append(l)
563 569 if l[-1] != '\n':
564 570 lines.append("\n\ No newline at end of file\n")
565 571 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
566 572
567 573 def apply(self, h):
568 574 if not h.complete():
569 575 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
570 576 (h.number, h.desc, len(h.a), h.lena, len(h.b),
571 577 h.lenb))
572 578
573 579 self.hunks += 1
574 580
575 581 if self.missing:
576 582 self.rej.append(h)
577 583 return -1
578 584
579 585 if self.exists and h.createfile():
580 586 self.ui.warn(_("file %s already exists\n") % self.fname)
581 587 self.rej.append(h)
582 588 return -1
583 589
584 590 if isinstance(h, binhunk):
585 591 if h.rmfile():
586 592 self.backend.unlink(self.fname)
587 593 else:
588 594 self.lines[:] = h.new()
589 595 self.offset += len(h.new())
590 596 self.dirty = True
591 597 return 0
592 598
593 599 horig = h
594 600 if (self.eolmode in ('crlf', 'lf')
595 601 or self.eolmode == 'auto' and self.eol):
596 602 # If new eols are going to be normalized, then normalize
597 603 # hunk data before patching. Otherwise, preserve input
598 604 # line-endings.
599 605 h = h.getnormalized()
600 606
601 607 # fast case first, no offsets, no fuzz
602 608 old = h.old()
603 609 # patch starts counting at 1 unless we are adding the file
604 610 if h.starta == 0:
605 611 start = 0
606 612 else:
607 613 start = h.starta + self.offset - 1
608 614 orig_start = start
609 615 # if there's skew we want to emit the "(offset %d lines)" even
610 616 # when the hunk cleanly applies at start + skew, so skip the
611 617 # fast case code
612 618 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
613 619 if h.rmfile():
614 620 self.backend.unlink(self.fname)
615 621 else:
616 622 self.lines[start : start + h.lena] = h.new()
617 623 self.offset += h.lenb - h.lena
618 624 self.dirty = True
619 625 return 0
620 626
621 627 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
622 628 self.hash = {}
623 629 for x, s in enumerate(self.lines):
624 630 self.hash.setdefault(s, []).append(x)
625 631 if h.hunk[-1][0] != ' ':
626 632 # if the hunk tried to put something at the bottom of the file
627 633 # override the start line and use eof here
628 634 search_start = len(self.lines)
629 635 else:
630 636 search_start = orig_start + self.skew
631 637
632 638 for fuzzlen in xrange(3):
633 639 for toponly in [True, False]:
634 640 old = h.old(fuzzlen, toponly)
635 641
636 642 cand = self.findlines(old[0][1:], search_start)
637 643 for l in cand:
638 644 if diffhelpers.testhunk(old, self.lines, l) == 0:
639 645 newlines = h.new(fuzzlen, toponly)
640 646 self.lines[l : l + len(old)] = newlines
641 647 self.offset += len(newlines) - len(old)
642 648 self.skew = l - orig_start
643 649 self.dirty = True
644 650 offset = l - orig_start - fuzzlen
645 651 if fuzzlen:
646 652 msg = _("Hunk #%d succeeded at %d "
647 653 "with fuzz %d "
648 654 "(offset %d lines).\n")
649 655 self.printfile(True)
650 656 self.ui.warn(msg %
651 657 (h.number, l + 1, fuzzlen, offset))
652 658 else:
653 659 msg = _("Hunk #%d succeeded at %d "
654 660 "(offset %d lines).\n")
655 661 self.ui.note(msg % (h.number, l + 1, offset))
656 662 return fuzzlen
657 663 self.printfile(True)
658 664 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
659 665 self.rej.append(horig)
660 666 return -1
661 667
662 668 def close(self):
663 669 if self.dirty:
664 670 self.writelines(self.fname, self.lines)
665 671 self.write_rej()
666 672 return len(self.rej)
667 673
668 674 class hunk(object):
669 675 def __init__(self, desc, num, lr, context, create=False, remove=False):
670 676 self.number = num
671 677 self.desc = desc
672 678 self.hunk = [desc]
673 679 self.a = []
674 680 self.b = []
675 681 self.starta = self.lena = None
676 682 self.startb = self.lenb = None
677 683 if lr is not None:
678 684 if context:
679 685 self.read_context_hunk(lr)
680 686 else:
681 687 self.read_unified_hunk(lr)
682 688 self.create = create
683 689 self.remove = remove and not create
684 690
685 691 def getnormalized(self):
686 692 """Return a copy with line endings normalized to LF."""
687 693
688 694 def normalize(lines):
689 695 nlines = []
690 696 for line in lines:
691 697 if line.endswith('\r\n'):
692 698 line = line[:-2] + '\n'
693 699 nlines.append(line)
694 700 return nlines
695 701
696 702 # Dummy object, it is rebuilt manually
697 703 nh = hunk(self.desc, self.number, None, None, False, False)
698 704 nh.number = self.number
699 705 nh.desc = self.desc
700 706 nh.hunk = self.hunk
701 707 nh.a = normalize(self.a)
702 708 nh.b = normalize(self.b)
703 709 nh.starta = self.starta
704 710 nh.startb = self.startb
705 711 nh.lena = self.lena
706 712 nh.lenb = self.lenb
707 713 nh.create = self.create
708 714 nh.remove = self.remove
709 715 return nh
710 716
711 717 def read_unified_hunk(self, lr):
712 718 m = unidesc.match(self.desc)
713 719 if not m:
714 720 raise PatchError(_("bad hunk #%d") % self.number)
715 721 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
716 722 if self.lena is None:
717 723 self.lena = 1
718 724 else:
719 725 self.lena = int(self.lena)
720 726 if self.lenb is None:
721 727 self.lenb = 1
722 728 else:
723 729 self.lenb = int(self.lenb)
724 730 self.starta = int(self.starta)
725 731 self.startb = int(self.startb)
726 732 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
727 733 # if we hit eof before finishing out the hunk, the last line will
728 734 # be zero length. Lets try to fix it up.
729 735 while len(self.hunk[-1]) == 0:
730 736 del self.hunk[-1]
731 737 del self.a[-1]
732 738 del self.b[-1]
733 739 self.lena -= 1
734 740 self.lenb -= 1
735 741 self._fixnewline(lr)
736 742
737 743 def read_context_hunk(self, lr):
738 744 self.desc = lr.readline()
739 745 m = contextdesc.match(self.desc)
740 746 if not m:
741 747 raise PatchError(_("bad hunk #%d") % self.number)
742 748 foo, self.starta, foo2, aend, foo3 = m.groups()
743 749 self.starta = int(self.starta)
744 750 if aend is None:
745 751 aend = self.starta
746 752 self.lena = int(aend) - self.starta
747 753 if self.starta:
748 754 self.lena += 1
749 755 for x in xrange(self.lena):
750 756 l = lr.readline()
751 757 if l.startswith('---'):
752 758 # lines addition, old block is empty
753 759 lr.push(l)
754 760 break
755 761 s = l[2:]
756 762 if l.startswith('- ') or l.startswith('! '):
757 763 u = '-' + s
758 764 elif l.startswith(' '):
759 765 u = ' ' + s
760 766 else:
761 767 raise PatchError(_("bad hunk #%d old text line %d") %
762 768 (self.number, x))
763 769 self.a.append(u)
764 770 self.hunk.append(u)
765 771
766 772 l = lr.readline()
767 773 if l.startswith('\ '):
768 774 s = self.a[-1][:-1]
769 775 self.a[-1] = s
770 776 self.hunk[-1] = s
771 777 l = lr.readline()
772 778 m = contextdesc.match(l)
773 779 if not m:
774 780 raise PatchError(_("bad hunk #%d") % self.number)
775 781 foo, self.startb, foo2, bend, foo3 = m.groups()
776 782 self.startb = int(self.startb)
777 783 if bend is None:
778 784 bend = self.startb
779 785 self.lenb = int(bend) - self.startb
780 786 if self.startb:
781 787 self.lenb += 1
782 788 hunki = 1
783 789 for x in xrange(self.lenb):
784 790 l = lr.readline()
785 791 if l.startswith('\ '):
786 792 # XXX: the only way to hit this is with an invalid line range.
787 793 # The no-eol marker is not counted in the line range, but I
788 794 # guess there are diff(1) out there which behave differently.
789 795 s = self.b[-1][:-1]
790 796 self.b[-1] = s
791 797 self.hunk[hunki - 1] = s
792 798 continue
793 799 if not l:
794 800 # line deletions, new block is empty and we hit EOF
795 801 lr.push(l)
796 802 break
797 803 s = l[2:]
798 804 if l.startswith('+ ') or l.startswith('! '):
799 805 u = '+' + s
800 806 elif l.startswith(' '):
801 807 u = ' ' + s
802 808 elif len(self.b) == 0:
803 809 # line deletions, new block is empty
804 810 lr.push(l)
805 811 break
806 812 else:
807 813 raise PatchError(_("bad hunk #%d old text line %d") %
808 814 (self.number, x))
809 815 self.b.append(s)
810 816 while True:
811 817 if hunki >= len(self.hunk):
812 818 h = ""
813 819 else:
814 820 h = self.hunk[hunki]
815 821 hunki += 1
816 822 if h == u:
817 823 break
818 824 elif h.startswith('-'):
819 825 continue
820 826 else:
821 827 self.hunk.insert(hunki - 1, u)
822 828 break
823 829
824 830 if not self.a:
825 831 # this happens when lines were only added to the hunk
826 832 for x in self.hunk:
827 833 if x.startswith('-') or x.startswith(' '):
828 834 self.a.append(x)
829 835 if not self.b:
830 836 # this happens when lines were only deleted from the hunk
831 837 for x in self.hunk:
832 838 if x.startswith('+') or x.startswith(' '):
833 839 self.b.append(x[1:])
834 840 # @@ -start,len +start,len @@
835 841 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
836 842 self.startb, self.lenb)
837 843 self.hunk[0] = self.desc
838 844 self._fixnewline(lr)
839 845
840 846 def _fixnewline(self, lr):
841 847 l = lr.readline()
842 848 if l.startswith('\ '):
843 849 diffhelpers.fix_newline(self.hunk, self.a, self.b)
844 850 else:
845 851 lr.push(l)
846 852
847 853 def complete(self):
848 854 return len(self.a) == self.lena and len(self.b) == self.lenb
849 855
850 856 def createfile(self):
851 857 return self.starta == 0 and self.lena == 0 and self.create
852 858
853 859 def rmfile(self):
854 860 return self.startb == 0 and self.lenb == 0 and self.remove
855 861
856 862 def fuzzit(self, l, fuzz, toponly):
857 863 # this removes context lines from the top and bottom of list 'l'. It
858 864 # checks the hunk to make sure only context lines are removed, and then
859 865 # returns a new shortened list of lines.
860 866 fuzz = min(fuzz, len(l)-1)
861 867 if fuzz:
862 868 top = 0
863 869 bot = 0
864 870 hlen = len(self.hunk)
865 871 for x in xrange(hlen - 1):
866 872 # the hunk starts with the @@ line, so use x+1
867 873 if self.hunk[x + 1][0] == ' ':
868 874 top += 1
869 875 else:
870 876 break
871 877 if not toponly:
872 878 for x in xrange(hlen - 1):
873 879 if self.hunk[hlen - bot - 1][0] == ' ':
874 880 bot += 1
875 881 else:
876 882 break
877 883
878 884 # top and bot now count context in the hunk
879 885 # adjust them if either one is short
880 886 context = max(top, bot, 3)
881 887 if bot < context:
882 888 bot = max(0, fuzz - (context - bot))
883 889 else:
884 890 bot = min(fuzz, bot)
885 891 if top < context:
886 892 top = max(0, fuzz - (context - top))
887 893 else:
888 894 top = min(fuzz, top)
889 895
890 896 return l[top:len(l)-bot]
891 897 return l
892 898
893 899 def old(self, fuzz=0, toponly=False):
894 900 return self.fuzzit(self.a, fuzz, toponly)
895 901
896 902 def new(self, fuzz=0, toponly=False):
897 903 return self.fuzzit(self.b, fuzz, toponly)
898 904
899 905 class binhunk:
900 906 'A binary patch file. Only understands literals so far.'
901 907 def __init__(self, gitpatch):
902 908 self.gitpatch = gitpatch
903 909 self.text = None
904 910 self.hunk = ['GIT binary patch\n']
905 911
906 912 def createfile(self):
907 913 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
908 914
909 915 def rmfile(self):
910 916 return self.gitpatch.op == 'DELETE'
911 917
912 918 def complete(self):
913 919 return self.text is not None
914 920
915 921 def new(self):
916 922 return [self.text]
917 923
918 924 def extract(self, lr):
919 925 line = lr.readline()
920 926 self.hunk.append(line)
921 927 while line and not line.startswith('literal '):
922 928 line = lr.readline()
923 929 self.hunk.append(line)
924 930 if not line:
925 931 raise PatchError(_('could not extract binary patch'))
926 932 size = int(line[8:].rstrip())
927 933 dec = []
928 934 line = lr.readline()
929 935 self.hunk.append(line)
930 936 while len(line) > 1:
931 937 l = line[0]
932 938 if l <= 'Z' and l >= 'A':
933 939 l = ord(l) - ord('A') + 1
934 940 else:
935 941 l = ord(l) - ord('a') + 27
936 942 dec.append(base85.b85decode(line[1:-1])[:l])
937 943 line = lr.readline()
938 944 self.hunk.append(line)
939 945 text = zlib.decompress(''.join(dec))
940 946 if len(text) != size:
941 947 raise PatchError(_('binary patch is %d bytes, not %d') %
942 948 len(text), size)
943 949 self.text = text
944 950
945 951 def parsefilename(str):
946 952 # --- filename \t|space stuff
947 953 s = str[4:].rstrip('\r\n')
948 954 i = s.find('\t')
949 955 if i < 0:
950 956 i = s.find(' ')
951 957 if i < 0:
952 958 return s
953 959 return s[:i]
954 960
955 961 def pathstrip(path, strip):
956 962 pathlen = len(path)
957 963 i = 0
958 964 if strip == 0:
959 965 return '', path.rstrip()
960 966 count = strip
961 967 while count > 0:
962 968 i = path.find('/', i)
963 969 if i == -1:
964 970 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
965 971 (count, strip, path))
966 972 i += 1
967 973 # consume '//' in the path
968 974 while i < pathlen - 1 and path[i] == '/':
969 975 i += 1
970 976 count -= 1
971 977 return path[:i].lstrip(), path[i:].rstrip()
972 978
973 def selectfile(afile_orig, bfile_orig, hunk, strip):
979 def selectfile(backend, afile_orig, bfile_orig, hunk, strip):
974 980 nulla = afile_orig == "/dev/null"
975 981 nullb = bfile_orig == "/dev/null"
976 982 abase, afile = pathstrip(afile_orig, strip)
977 gooda = not nulla and os.path.lexists(afile)
983 gooda = not nulla and backend.exists(afile)
978 984 bbase, bfile = pathstrip(bfile_orig, strip)
979 985 if afile == bfile:
980 986 goodb = gooda
981 987 else:
982 goodb = not nullb and os.path.lexists(bfile)
988 goodb = not nullb and backend.exists(bfile)
983 989 createfunc = hunk.createfile
984 990 missing = not goodb and not gooda and not createfunc()
985 991
986 992 # some diff programs apparently produce patches where the afile is
987 993 # not /dev/null, but afile starts with bfile
988 994 abasedir = afile[:afile.rfind('/') + 1]
989 995 bbasedir = bfile[:bfile.rfind('/') + 1]
990 996 if missing and abasedir == bbasedir and afile.startswith(bfile):
991 997 # this isn't very pretty
992 998 hunk.create = True
993 999 if createfunc():
994 1000 missing = False
995 1001 else:
996 1002 hunk.create = False
997 1003
998 1004 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
999 1005 # diff is between a file and its backup. In this case, the original
1000 1006 # file should be patched (see original mpatch code).
1001 1007 isbackup = (abase == bbase and bfile.startswith(afile))
1002 1008 fname = None
1003 1009 if not missing:
1004 1010 if gooda and goodb:
1005 1011 fname = isbackup and afile or bfile
1006 1012 elif gooda:
1007 1013 fname = afile
1008 1014
1009 1015 if not fname:
1010 1016 if not nullb:
1011 1017 fname = isbackup and afile or bfile
1012 1018 elif not nulla:
1013 1019 fname = afile
1014 1020 else:
1015 1021 raise PatchError(_("undefined source and destination files"))
1016 1022
1017 1023 return fname, missing
1018 1024
1019 1025 def scangitpatch(lr, firstline):
1020 1026 """
1021 1027 Git patches can emit:
1022 1028 - rename a to b
1023 1029 - change b
1024 1030 - copy a to c
1025 1031 - change c
1026 1032
1027 1033 We cannot apply this sequence as-is, the renamed 'a' could not be
1028 1034 found for it would have been renamed already. And we cannot copy
1029 1035 from 'b' instead because 'b' would have been changed already. So
1030 1036 we scan the git patch for copy and rename commands so we can
1031 1037 perform the copies ahead of time.
1032 1038 """
1033 1039 pos = 0
1034 1040 try:
1035 1041 pos = lr.fp.tell()
1036 1042 fp = lr.fp
1037 1043 except IOError:
1038 1044 fp = cStringIO.StringIO(lr.fp.read())
1039 1045 gitlr = linereader(fp, lr.textmode)
1040 1046 gitlr.push(firstline)
1041 1047 gitpatches = readgitpatch(gitlr)
1042 1048 fp.seek(pos)
1043 1049 return gitpatches
1044 1050
1045 1051 def iterhunks(fp):
1046 1052 """Read a patch and yield the following events:
1047 1053 - ("file", afile, bfile, firsthunk): select a new target file.
1048 1054 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1049 1055 "file" event.
1050 1056 - ("git", gitchanges): current diff is in git format, gitchanges
1051 1057 maps filenames to gitpatch records. Unique event.
1052 1058 """
1053 1059 changed = {}
1054 1060 afile = ""
1055 1061 bfile = ""
1056 1062 state = None
1057 1063 hunknum = 0
1058 1064 emitfile = newfile = False
1059 1065 git = False
1060 1066
1061 1067 # our states
1062 1068 BFILE = 1
1063 1069 context = None
1064 1070 lr = linereader(fp)
1065 1071
1066 1072 while True:
1067 1073 x = lr.readline()
1068 1074 if not x:
1069 1075 break
1070 1076 if (state == BFILE and ((not context and x[0] == '@') or
1071 1077 ((context is not False) and x.startswith('***************')))):
1072 1078 if context is None and x.startswith('***************'):
1073 1079 context = True
1074 1080 gpatch = changed.get(bfile)
1075 1081 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1076 1082 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1077 1083 h = hunk(x, hunknum + 1, lr, context, create, remove)
1078 1084 hunknum += 1
1079 1085 if emitfile:
1080 1086 emitfile = False
1081 1087 yield 'file', (afile, bfile, h)
1082 1088 yield 'hunk', h
1083 1089 elif state == BFILE and x.startswith('GIT binary patch'):
1084 1090 h = binhunk(changed[bfile])
1085 1091 hunknum += 1
1086 1092 if emitfile:
1087 1093 emitfile = False
1088 1094 yield 'file', ('a/' + afile, 'b/' + bfile, h)
1089 1095 h.extract(lr)
1090 1096 yield 'hunk', h
1091 1097 elif x.startswith('diff --git'):
1092 1098 # check for git diff, scanning the whole patch file if needed
1093 1099 m = gitre.match(x)
1094 1100 if m:
1095 1101 afile, bfile = m.group(1, 2)
1096 1102 if not git:
1097 1103 git = True
1098 1104 gitpatches = scangitpatch(lr, x)
1099 1105 yield 'git', gitpatches
1100 1106 for gp in gitpatches:
1101 1107 changed[gp.path] = gp
1102 1108 # else error?
1103 1109 # copy/rename + modify should modify target, not source
1104 1110 gp = changed.get(bfile)
1105 1111 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1106 1112 or gp.mode):
1107 1113 afile = bfile
1108 1114 newfile = True
1109 1115 elif x.startswith('---'):
1110 1116 # check for a unified diff
1111 1117 l2 = lr.readline()
1112 1118 if not l2.startswith('+++'):
1113 1119 lr.push(l2)
1114 1120 continue
1115 1121 newfile = True
1116 1122 context = False
1117 1123 afile = parsefilename(x)
1118 1124 bfile = parsefilename(l2)
1119 1125 elif x.startswith('***'):
1120 1126 # check for a context diff
1121 1127 l2 = lr.readline()
1122 1128 if not l2.startswith('---'):
1123 1129 lr.push(l2)
1124 1130 continue
1125 1131 l3 = lr.readline()
1126 1132 lr.push(l3)
1127 1133 if not l3.startswith("***************"):
1128 1134 lr.push(l2)
1129 1135 continue
1130 1136 newfile = True
1131 1137 context = True
1132 1138 afile = parsefilename(x)
1133 1139 bfile = parsefilename(l2)
1134 1140
1135 1141 if newfile:
1136 1142 newfile = False
1137 1143 emitfile = True
1138 1144 state = BFILE
1139 1145 hunknum = 0
1140 1146
1141 1147 def applydiff(ui, fp, changed, strip=1, eolmode='strict'):
1142 1148 """Reads a patch from fp and tries to apply it.
1143 1149
1144 1150 The dict 'changed' is filled in with all of the filenames changed
1145 1151 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1146 1152 found and 1 if there was any fuzz.
1147 1153
1148 1154 If 'eolmode' is 'strict', the patch content and patched file are
1149 1155 read in binary mode. Otherwise, line endings are ignored when
1150 1156 patching then normalized according to 'eolmode'.
1151 1157
1152 1158 Callers probably want to call '_updatedir' after this to
1153 1159 apply certain categories of changes not done by this function.
1154 1160 """
1155 1161 return _applydiff(ui, fp, patchfile, changed, strip=strip,
1156 1162 eolmode=eolmode)
1157 1163
1158 1164 def _applydiff(ui, fp, patcher, changed, strip=1, eolmode='strict'):
1159 1165 rejects = 0
1160 1166 err = 0
1161 1167 current_file = None
1162 1168 cwd = os.getcwd()
1163 1169 backend = fsbackend(ui, os.getcwd())
1164 1170
1165 1171 for state, values in iterhunks(fp):
1166 1172 if state == 'hunk':
1167 1173 if not current_file:
1168 1174 continue
1169 1175 ret = current_file.apply(values)
1170 1176 if ret >= 0:
1171 1177 changed.setdefault(current_file.fname, None)
1172 1178 if ret > 0:
1173 1179 err = 1
1174 1180 elif state == 'file':
1175 1181 if current_file:
1176 1182 rejects += current_file.close()
1177 1183 afile, bfile, first_hunk = values
1178 1184 try:
1179 current_file, missing = selectfile(afile, bfile,
1185 current_file, missing = selectfile(backend, afile, bfile,
1180 1186 first_hunk, strip)
1181 1187 current_file = patcher(ui, current_file, backend,
1182 1188 missing=missing, eolmode=eolmode)
1183 1189 except PatchError, inst:
1184 1190 ui.warn(str(inst) + '\n')
1185 1191 current_file = None
1186 1192 rejects += 1
1187 1193 continue
1188 1194 elif state == 'git':
1189 1195 for gp in values:
1190 1196 gp.path = pathstrip(gp.path, strip - 1)[1]
1191 1197 if gp.oldpath:
1192 1198 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1193 1199 # Binary patches really overwrite target files, copying them
1194 1200 # will just make it fails with "target file exists"
1195 1201 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1196 1202 backend.copy(gp.oldpath, gp.path)
1197 1203 changed[gp.path] = gp
1198 1204 else:
1199 1205 raise util.Abort(_('unsupported parser state: %s') % state)
1200 1206
1201 1207 if current_file:
1202 1208 rejects += current_file.close()
1203 1209
1204 1210 if rejects:
1205 1211 return -1
1206 1212 return err
1207 1213
1208 1214 def _updatedir(ui, repo, patches, similarity=0):
1209 1215 '''Update dirstate after patch application according to metadata'''
1210 1216 if not patches:
1211 1217 return []
1212 1218 copies = []
1213 1219 removes = set()
1214 1220 cfiles = patches.keys()
1215 1221 cwd = repo.getcwd()
1216 1222 if cwd:
1217 1223 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1218 1224 for f in patches:
1219 1225 gp = patches[f]
1220 1226 if not gp:
1221 1227 continue
1222 1228 if gp.op == 'RENAME':
1223 1229 copies.append((gp.oldpath, gp.path))
1224 1230 removes.add(gp.oldpath)
1225 1231 elif gp.op == 'COPY':
1226 1232 copies.append((gp.oldpath, gp.path))
1227 1233 elif gp.op == 'DELETE':
1228 1234 removes.add(gp.path)
1229 1235
1230 1236 wctx = repo[None]
1231 1237 for src, dst in copies:
1232 1238 scmutil.dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
1233 1239 if (not similarity) and removes:
1234 1240 wctx.remove(sorted(removes), True)
1235 1241
1236 1242 for f in patches:
1237 1243 gp = patches[f]
1238 1244 if gp and gp.mode:
1239 1245 islink, isexec = gp.mode
1240 1246 dst = repo.wjoin(gp.path)
1241 1247 # patch won't create empty files
1242 1248 if gp.op == 'ADD' and not os.path.lexists(dst):
1243 1249 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1244 1250 repo.wwrite(gp.path, '', flags)
1245 1251 util.setflags(dst, islink, isexec)
1246 1252 scmutil.addremove(repo, cfiles, similarity=similarity)
1247 1253 files = patches.keys()
1248 1254 files.extend([r for r in removes if r not in files])
1249 1255 return sorted(files)
1250 1256
1251 1257 def _externalpatch(patcher, patchname, ui, strip, cwd, files):
1252 1258 """use <patcher> to apply <patchname> to the working directory.
1253 1259 returns whether patch was applied with fuzz factor."""
1254 1260
1255 1261 fuzz = False
1256 1262 args = []
1257 1263 if cwd:
1258 1264 args.append('-d %s' % util.shellquote(cwd))
1259 1265 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1260 1266 util.shellquote(patchname)))
1261 1267
1262 1268 for line in fp:
1263 1269 line = line.rstrip()
1264 1270 ui.note(line + '\n')
1265 1271 if line.startswith('patching file '):
1266 1272 pf = util.parsepatchoutput(line)
1267 1273 printed_file = False
1268 1274 files.setdefault(pf, None)
1269 1275 elif line.find('with fuzz') >= 0:
1270 1276 fuzz = True
1271 1277 if not printed_file:
1272 1278 ui.warn(pf + '\n')
1273 1279 printed_file = True
1274 1280 ui.warn(line + '\n')
1275 1281 elif line.find('saving rejects to file') >= 0:
1276 1282 ui.warn(line + '\n')
1277 1283 elif line.find('FAILED') >= 0:
1278 1284 if not printed_file:
1279 1285 ui.warn(pf + '\n')
1280 1286 printed_file = True
1281 1287 ui.warn(line + '\n')
1282 1288 code = fp.close()
1283 1289 if code:
1284 1290 raise PatchError(_("patch command failed: %s") %
1285 1291 util.explainexit(code)[0])
1286 1292 return fuzz
1287 1293
1288 1294 def internalpatch(ui, repo, patchobj, strip, cwd, files=None, eolmode='strict',
1289 1295 similarity=0):
1290 1296 """use builtin patch to apply <patchobj> to the working directory.
1291 1297 returns whether patch was applied with fuzz factor."""
1292 1298
1293 1299 if files is None:
1294 1300 files = {}
1295 1301 if eolmode is None:
1296 1302 eolmode = ui.config('patch', 'eol', 'strict')
1297 1303 if eolmode.lower() not in eolmodes:
1298 1304 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1299 1305 eolmode = eolmode.lower()
1300 1306
1301 1307 try:
1302 1308 fp = open(patchobj, 'rb')
1303 1309 except TypeError:
1304 1310 fp = patchobj
1305 1311 if cwd:
1306 1312 curdir = os.getcwd()
1307 1313 os.chdir(cwd)
1308 1314 try:
1309 1315 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1310 1316 finally:
1311 1317 if cwd:
1312 1318 os.chdir(curdir)
1313 1319 if fp != patchobj:
1314 1320 fp.close()
1315 1321 touched = _updatedir(ui, repo, files, similarity)
1316 1322 files.update(dict.fromkeys(touched))
1317 1323 if ret < 0:
1318 1324 raise PatchError(_('patch failed to apply'))
1319 1325 return ret > 0
1320 1326
1321 1327 def patch(ui, repo, patchname, strip=1, cwd=None, files=None, eolmode='strict',
1322 1328 similarity=0):
1323 1329 """Apply <patchname> to the working directory.
1324 1330
1325 1331 'eolmode' specifies how end of lines should be handled. It can be:
1326 1332 - 'strict': inputs are read in binary mode, EOLs are preserved
1327 1333 - 'crlf': EOLs are ignored when patching and reset to CRLF
1328 1334 - 'lf': EOLs are ignored when patching and reset to LF
1329 1335 - None: get it from user settings, default to 'strict'
1330 1336 'eolmode' is ignored when using an external patcher program.
1331 1337
1332 1338 Returns whether patch was applied with fuzz factor.
1333 1339 """
1334 1340 patcher = ui.config('ui', 'patch')
1335 1341 if files is None:
1336 1342 files = {}
1337 1343 try:
1338 1344 if patcher:
1339 1345 try:
1340 1346 return _externalpatch(patcher, patchname, ui, strip, cwd,
1341 1347 files)
1342 1348 finally:
1343 1349 touched = _updatedir(ui, repo, files, similarity)
1344 1350 files.update(dict.fromkeys(touched))
1345 1351 return internalpatch(ui, repo, patchname, strip, cwd, files, eolmode,
1346 1352 similarity)
1347 1353 except PatchError, err:
1348 1354 raise util.Abort(str(err))
1349 1355
1350 def changedfiles(patchpath, strip=1):
1356 def changedfiles(ui, repo, patchpath, strip=1):
1357 backend = fsbackend(ui, repo.root)
1351 1358 fp = open(patchpath, 'rb')
1352 1359 try:
1353 1360 changed = set()
1354 1361 for state, values in iterhunks(fp):
1355 1362 if state == 'hunk':
1356 1363 continue
1357 1364 elif state == 'file':
1358 1365 afile, bfile, first_hunk = values
1359 current_file, missing = selectfile(afile, bfile,
1366 current_file, missing = selectfile(backend, afile, bfile,
1360 1367 first_hunk, strip)
1361 1368 changed.add(current_file)
1362 1369 elif state == 'git':
1363 1370 for gp in values:
1364 1371 gp.path = pathstrip(gp.path, strip - 1)[1]
1365 1372 changed.add(gp.path)
1366 1373 if gp.oldpath:
1367 1374 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1368 1375 if gp.op == 'RENAME':
1369 1376 changed.add(gp.oldpath)
1370 1377 else:
1371 1378 raise util.Abort(_('unsupported parser state: %s') % state)
1372 1379 return changed
1373 1380 finally:
1374 1381 fp.close()
1375 1382
1376 1383 def b85diff(to, tn):
1377 1384 '''print base85-encoded binary diff'''
1378 1385 def gitindex(text):
1379 1386 if not text:
1380 1387 return hex(nullid)
1381 1388 l = len(text)
1382 1389 s = util.sha1('blob %d\0' % l)
1383 1390 s.update(text)
1384 1391 return s.hexdigest()
1385 1392
1386 1393 def fmtline(line):
1387 1394 l = len(line)
1388 1395 if l <= 26:
1389 1396 l = chr(ord('A') + l - 1)
1390 1397 else:
1391 1398 l = chr(l - 26 + ord('a') - 1)
1392 1399 return '%c%s\n' % (l, base85.b85encode(line, True))
1393 1400
1394 1401 def chunk(text, csize=52):
1395 1402 l = len(text)
1396 1403 i = 0
1397 1404 while i < l:
1398 1405 yield text[i:i + csize]
1399 1406 i += csize
1400 1407
1401 1408 tohash = gitindex(to)
1402 1409 tnhash = gitindex(tn)
1403 1410 if tohash == tnhash:
1404 1411 return ""
1405 1412
1406 1413 # TODO: deltas
1407 1414 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1408 1415 (tohash, tnhash, len(tn))]
1409 1416 for l in chunk(zlib.compress(tn)):
1410 1417 ret.append(fmtline(l))
1411 1418 ret.append('\n')
1412 1419 return ''.join(ret)
1413 1420
1414 1421 class GitDiffRequired(Exception):
1415 1422 pass
1416 1423
1417 1424 def diffopts(ui, opts=None, untrusted=False):
1418 1425 def get(key, name=None, getter=ui.configbool):
1419 1426 return ((opts and opts.get(key)) or
1420 1427 getter('diff', name or key, None, untrusted=untrusted))
1421 1428 return mdiff.diffopts(
1422 1429 text=opts and opts.get('text'),
1423 1430 git=get('git'),
1424 1431 nodates=get('nodates'),
1425 1432 showfunc=get('show_function', 'showfunc'),
1426 1433 ignorews=get('ignore_all_space', 'ignorews'),
1427 1434 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1428 1435 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1429 1436 context=get('unified', getter=ui.config))
1430 1437
1431 1438 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1432 1439 losedatafn=None, prefix=''):
1433 1440 '''yields diff of changes to files between two nodes, or node and
1434 1441 working directory.
1435 1442
1436 1443 if node1 is None, use first dirstate parent instead.
1437 1444 if node2 is None, compare node1 with working directory.
1438 1445
1439 1446 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1440 1447 every time some change cannot be represented with the current
1441 1448 patch format. Return False to upgrade to git patch format, True to
1442 1449 accept the loss or raise an exception to abort the diff. It is
1443 1450 called with the name of current file being diffed as 'fn'. If set
1444 1451 to None, patches will always be upgraded to git format when
1445 1452 necessary.
1446 1453
1447 1454 prefix is a filename prefix that is prepended to all filenames on
1448 1455 display (used for subrepos).
1449 1456 '''
1450 1457
1451 1458 if opts is None:
1452 1459 opts = mdiff.defaultopts
1453 1460
1454 1461 if not node1 and not node2:
1455 1462 node1 = repo.dirstate.p1()
1456 1463
1457 1464 def lrugetfilectx():
1458 1465 cache = {}
1459 1466 order = []
1460 1467 def getfilectx(f, ctx):
1461 1468 fctx = ctx.filectx(f, filelog=cache.get(f))
1462 1469 if f not in cache:
1463 1470 if len(cache) > 20:
1464 1471 del cache[order.pop(0)]
1465 1472 cache[f] = fctx.filelog()
1466 1473 else:
1467 1474 order.remove(f)
1468 1475 order.append(f)
1469 1476 return fctx
1470 1477 return getfilectx
1471 1478 getfilectx = lrugetfilectx()
1472 1479
1473 1480 ctx1 = repo[node1]
1474 1481 ctx2 = repo[node2]
1475 1482
1476 1483 if not changes:
1477 1484 changes = repo.status(ctx1, ctx2, match=match)
1478 1485 modified, added, removed = changes[:3]
1479 1486
1480 1487 if not modified and not added and not removed:
1481 1488 return []
1482 1489
1483 1490 revs = None
1484 1491 if not repo.ui.quiet:
1485 1492 hexfunc = repo.ui.debugflag and hex or short
1486 1493 revs = [hexfunc(node) for node in [node1, node2] if node]
1487 1494
1488 1495 copy = {}
1489 1496 if opts.git or opts.upgrade:
1490 1497 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1491 1498
1492 1499 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1493 1500 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1494 1501 if opts.upgrade and not opts.git:
1495 1502 try:
1496 1503 def losedata(fn):
1497 1504 if not losedatafn or not losedatafn(fn=fn):
1498 1505 raise GitDiffRequired()
1499 1506 # Buffer the whole output until we are sure it can be generated
1500 1507 return list(difffn(opts.copy(git=False), losedata))
1501 1508 except GitDiffRequired:
1502 1509 return difffn(opts.copy(git=True), None)
1503 1510 else:
1504 1511 return difffn(opts, None)
1505 1512
1506 1513 def difflabel(func, *args, **kw):
1507 1514 '''yields 2-tuples of (output, label) based on the output of func()'''
1508 1515 prefixes = [('diff', 'diff.diffline'),
1509 1516 ('copy', 'diff.extended'),
1510 1517 ('rename', 'diff.extended'),
1511 1518 ('old', 'diff.extended'),
1512 1519 ('new', 'diff.extended'),
1513 1520 ('deleted', 'diff.extended'),
1514 1521 ('---', 'diff.file_a'),
1515 1522 ('+++', 'diff.file_b'),
1516 1523 ('@@', 'diff.hunk'),
1517 1524 ('-', 'diff.deleted'),
1518 1525 ('+', 'diff.inserted')]
1519 1526
1520 1527 for chunk in func(*args, **kw):
1521 1528 lines = chunk.split('\n')
1522 1529 for i, line in enumerate(lines):
1523 1530 if i != 0:
1524 1531 yield ('\n', '')
1525 1532 stripline = line
1526 1533 if line and line[0] in '+-':
1527 1534 # highlight trailing whitespace, but only in changed lines
1528 1535 stripline = line.rstrip()
1529 1536 for prefix, label in prefixes:
1530 1537 if stripline.startswith(prefix):
1531 1538 yield (stripline, label)
1532 1539 break
1533 1540 else:
1534 1541 yield (line, '')
1535 1542 if line != stripline:
1536 1543 yield (line[len(stripline):], 'diff.trailingwhitespace')
1537 1544
1538 1545 def diffui(*args, **kw):
1539 1546 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1540 1547 return difflabel(diff, *args, **kw)
1541 1548
1542 1549
1543 1550 def _addmodehdr(header, omode, nmode):
1544 1551 if omode != nmode:
1545 1552 header.append('old mode %s\n' % omode)
1546 1553 header.append('new mode %s\n' % nmode)
1547 1554
1548 1555 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1549 1556 copy, getfilectx, opts, losedatafn, prefix):
1550 1557
1551 1558 def join(f):
1552 1559 return os.path.join(prefix, f)
1553 1560
1554 1561 date1 = util.datestr(ctx1.date())
1555 1562 man1 = ctx1.manifest()
1556 1563
1557 1564 gone = set()
1558 1565 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1559 1566
1560 1567 copyto = dict([(v, k) for k, v in copy.items()])
1561 1568
1562 1569 if opts.git:
1563 1570 revs = None
1564 1571
1565 1572 for f in sorted(modified + added + removed):
1566 1573 to = None
1567 1574 tn = None
1568 1575 dodiff = True
1569 1576 header = []
1570 1577 if f in man1:
1571 1578 to = getfilectx(f, ctx1).data()
1572 1579 if f not in removed:
1573 1580 tn = getfilectx(f, ctx2).data()
1574 1581 a, b = f, f
1575 1582 if opts.git or losedatafn:
1576 1583 if f in added:
1577 1584 mode = gitmode[ctx2.flags(f)]
1578 1585 if f in copy or f in copyto:
1579 1586 if opts.git:
1580 1587 if f in copy:
1581 1588 a = copy[f]
1582 1589 else:
1583 1590 a = copyto[f]
1584 1591 omode = gitmode[man1.flags(a)]
1585 1592 _addmodehdr(header, omode, mode)
1586 1593 if a in removed and a not in gone:
1587 1594 op = 'rename'
1588 1595 gone.add(a)
1589 1596 else:
1590 1597 op = 'copy'
1591 1598 header.append('%s from %s\n' % (op, join(a)))
1592 1599 header.append('%s to %s\n' % (op, join(f)))
1593 1600 to = getfilectx(a, ctx1).data()
1594 1601 else:
1595 1602 losedatafn(f)
1596 1603 else:
1597 1604 if opts.git:
1598 1605 header.append('new file mode %s\n' % mode)
1599 1606 elif ctx2.flags(f):
1600 1607 losedatafn(f)
1601 1608 # In theory, if tn was copied or renamed we should check
1602 1609 # if the source is binary too but the copy record already
1603 1610 # forces git mode.
1604 1611 if util.binary(tn):
1605 1612 if opts.git:
1606 1613 dodiff = 'binary'
1607 1614 else:
1608 1615 losedatafn(f)
1609 1616 if not opts.git and not tn:
1610 1617 # regular diffs cannot represent new empty file
1611 1618 losedatafn(f)
1612 1619 elif f in removed:
1613 1620 if opts.git:
1614 1621 # have we already reported a copy above?
1615 1622 if ((f in copy and copy[f] in added
1616 1623 and copyto[copy[f]] == f) or
1617 1624 (f in copyto and copyto[f] in added
1618 1625 and copy[copyto[f]] == f)):
1619 1626 dodiff = False
1620 1627 else:
1621 1628 header.append('deleted file mode %s\n' %
1622 1629 gitmode[man1.flags(f)])
1623 1630 elif not to or util.binary(to):
1624 1631 # regular diffs cannot represent empty file deletion
1625 1632 losedatafn(f)
1626 1633 else:
1627 1634 oflag = man1.flags(f)
1628 1635 nflag = ctx2.flags(f)
1629 1636 binary = util.binary(to) or util.binary(tn)
1630 1637 if opts.git:
1631 1638 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1632 1639 if binary:
1633 1640 dodiff = 'binary'
1634 1641 elif binary or nflag != oflag:
1635 1642 losedatafn(f)
1636 1643 if opts.git:
1637 1644 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1638 1645
1639 1646 if dodiff:
1640 1647 if dodiff == 'binary':
1641 1648 text = b85diff(to, tn)
1642 1649 else:
1643 1650 text = mdiff.unidiff(to, date1,
1644 1651 # ctx2 date may be dynamic
1645 1652 tn, util.datestr(ctx2.date()),
1646 1653 join(a), join(b), revs, opts=opts)
1647 1654 if header and (text or len(header) > 1):
1648 1655 yield ''.join(header)
1649 1656 if text:
1650 1657 yield text
1651 1658
1652 1659 def diffstatdata(lines):
1653 1660 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1654 1661
1655 1662 filename, adds, removes = None, 0, 0
1656 1663 for line in lines:
1657 1664 if line.startswith('diff'):
1658 1665 if filename:
1659 1666 isbinary = adds == 0 and removes == 0
1660 1667 yield (filename, adds, removes, isbinary)
1661 1668 # set numbers to 0 anyway when starting new file
1662 1669 adds, removes = 0, 0
1663 1670 if line.startswith('diff --git'):
1664 1671 filename = gitre.search(line).group(1)
1665 1672 elif line.startswith('diff -r'):
1666 1673 # format: "diff -r ... -r ... filename"
1667 1674 filename = diffre.search(line).group(1)
1668 1675 elif line.startswith('+') and not line.startswith('+++'):
1669 1676 adds += 1
1670 1677 elif line.startswith('-') and not line.startswith('---'):
1671 1678 removes += 1
1672 1679 if filename:
1673 1680 isbinary = adds == 0 and removes == 0
1674 1681 yield (filename, adds, removes, isbinary)
1675 1682
1676 1683 def diffstat(lines, width=80, git=False):
1677 1684 output = []
1678 1685 stats = list(diffstatdata(lines))
1679 1686
1680 1687 maxtotal, maxname = 0, 0
1681 1688 totaladds, totalremoves = 0, 0
1682 1689 hasbinary = False
1683 1690
1684 1691 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1685 1692 for filename, adds, removes, isbinary in stats]
1686 1693
1687 1694 for filename, adds, removes, isbinary, namewidth in sized:
1688 1695 totaladds += adds
1689 1696 totalremoves += removes
1690 1697 maxname = max(maxname, namewidth)
1691 1698 maxtotal = max(maxtotal, adds + removes)
1692 1699 if isbinary:
1693 1700 hasbinary = True
1694 1701
1695 1702 countwidth = len(str(maxtotal))
1696 1703 if hasbinary and countwidth < 3:
1697 1704 countwidth = 3
1698 1705 graphwidth = width - countwidth - maxname - 6
1699 1706 if graphwidth < 10:
1700 1707 graphwidth = 10
1701 1708
1702 1709 def scale(i):
1703 1710 if maxtotal <= graphwidth:
1704 1711 return i
1705 1712 # If diffstat runs out of room it doesn't print anything,
1706 1713 # which isn't very useful, so always print at least one + or -
1707 1714 # if there were at least some changes.
1708 1715 return max(i * graphwidth // maxtotal, int(bool(i)))
1709 1716
1710 1717 for filename, adds, removes, isbinary, namewidth in sized:
1711 1718 if git and isbinary:
1712 1719 count = 'Bin'
1713 1720 else:
1714 1721 count = adds + removes
1715 1722 pluses = '+' * scale(adds)
1716 1723 minuses = '-' * scale(removes)
1717 1724 output.append(' %s%s | %*s %s%s\n' %
1718 1725 (filename, ' ' * (maxname - namewidth),
1719 1726 countwidth, count,
1720 1727 pluses, minuses))
1721 1728
1722 1729 if stats:
1723 1730 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1724 1731 % (len(stats), totaladds, totalremoves))
1725 1732
1726 1733 return ''.join(output)
1727 1734
1728 1735 def diffstatui(*args, **kw):
1729 1736 '''like diffstat(), but yields 2-tuples of (output, label) for
1730 1737 ui.write()
1731 1738 '''
1732 1739
1733 1740 for line in diffstat(*args, **kw).splitlines():
1734 1741 if line and line[-1] in '+-':
1735 1742 name, graph = line.rsplit(' ', 1)
1736 1743 yield (name + ' ', '')
1737 1744 m = re.search(r'\++', graph)
1738 1745 if m:
1739 1746 yield (m.group(0), 'diffstat.inserted')
1740 1747 m = re.search(r'-+', graph)
1741 1748 if m:
1742 1749 yield (m.group(0), 'diffstat.deleted')
1743 1750 else:
1744 1751 yield (line, '')
1745 1752 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now