##// END OF EJS Templates
record: edit patch of newly added files (issue4304)...
Laurent Charignon -
r24845:8133494a stable
parent child Browse files
Show More
@@ -1,3247 +1,3258 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile, cStringIO, shutil
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import context, repair, graphmod, revset, phases, obsolete, pathutil
14 14 import changelog
15 15 import bookmarks
16 16 import encoding
17 17 import crecord as crecordmod
18 18 import lock as lockmod
19 19
20 20 def parsealiases(cmd):
21 21 return cmd.lstrip("^").split("|")
22 22
23 23 def setupwrapcolorwrite(ui):
24 24 # wrap ui.write so diff output can be labeled/colorized
25 25 def wrapwrite(orig, *args, **kw):
26 26 label = kw.pop('label', '')
27 27 for chunk, l in patch.difflabel(lambda: args):
28 28 orig(chunk, label=label + l)
29 29
30 30 oldwrite = ui.write
31 31 def wrap(*args, **kwargs):
32 32 return wrapwrite(oldwrite, *args, **kwargs)
33 33 setattr(ui, 'write', wrap)
34 34 return oldwrite
35 35
36 36 def filterchunks(ui, originalhunks, usecurses, testfile):
37 37 if usecurses:
38 38 if testfile:
39 39 recordfn = crecordmod.testdecorator(testfile,
40 40 crecordmod.testchunkselector)
41 41 else:
42 42 recordfn = crecordmod.chunkselector
43 43
44 44 return crecordmod.filterpatch(ui, originalhunks, recordfn)
45 45
46 46 else:
47 47 return patch.filterpatch(ui, originalhunks)
48 48
49 49 def recordfilter(ui, originalhunks):
50 50 usecurses = ui.configbool('experimental', 'crecord', False)
51 51 testfile = ui.config('experimental', 'crecordtest', None)
52 52 oldwrite = setupwrapcolorwrite(ui)
53 53 try:
54 54 newchunks = filterchunks(ui, originalhunks, usecurses, testfile)
55 55 finally:
56 56 ui.write = oldwrite
57 57 return newchunks
58 58
59 59 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
60 60 filterfn, *pats, **opts):
61 61 import merge as mergemod
62 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
63 ishunk = lambda x: isinstance(x, hunkclasses)
62 64
63 65 if not ui.interactive():
64 66 raise util.Abort(_('running non-interactively, use %s instead') %
65 67 cmdsuggest)
66 68
67 69 # make sure username is set before going interactive
68 70 if not opts.get('user'):
69 71 ui.username() # raise exception, username not provided
70 72
71 73 def recordfunc(ui, repo, message, match, opts):
72 74 """This is generic record driver.
73 75
74 76 Its job is to interactively filter local changes, and
75 77 accordingly prepare working directory into a state in which the
76 78 job can be delegated to a non-interactive commit command such as
77 79 'commit' or 'qrefresh'.
78 80
79 81 After the actual job is done by non-interactive command, the
80 82 working directory is restored to its original state.
81 83
82 84 In the end we'll record interesting changes, and everything else
83 85 will be left in place, so the user can continue working.
84 86 """
85 87
86 88 checkunfinished(repo, commit=True)
87 89 merge = len(repo[None].parents()) > 1
88 90 if merge:
89 91 raise util.Abort(_('cannot partially commit a merge '
90 92 '(use "hg commit" instead)'))
91 93
92 94 status = repo.status(match=match)
93 95 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
94 96 diffopts.nodates = True
95 97 diffopts.git = True
96 98 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
97 99 originalchunks = patch.parsepatch(originaldiff)
98 100
99 101 # 1. filter patch, so we have intending-to apply subset of it
100 102 try:
101 103 chunks = filterfn(ui, originalchunks)
102 104 except patch.PatchError, err:
103 105 raise util.Abort(_('error parsing patch: %s') % err)
104 106
107 # We need to keep a backup of files that have been newly added and
108 # modified during the recording process because there is a previous
109 # version without the edit in the workdir
110 newlyaddedandmodifiedfiles = set()
111 for chunk in chunks:
112 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
113 originalchunks:
114 newlyaddedandmodifiedfiles.add(chunk.header.filename())
105 115 contenders = set()
106 116 for h in chunks:
107 117 try:
108 118 contenders.update(set(h.files()))
109 119 except AttributeError:
110 120 pass
111 121
112 122 changed = status.modified + status.added + status.removed
113 123 newfiles = [f for f in changed if f in contenders]
114 124 if not newfiles:
115 125 ui.status(_('no changes to record\n'))
116 126 return 0
117 127
118 128 modified = set(status.modified)
119 129
120 130 # 2. backup changed files, so we can restore them in the end
121 131
122 132 if backupall:
123 133 tobackup = changed
124 134 else:
125 tobackup = [f for f in newfiles if f in modified]
126
135 tobackup = [f for f in newfiles if f in modified or f in \
136 newlyaddedandmodifiedfiles]
127 137 backups = {}
128 138 if tobackup:
129 139 backupdir = repo.join('record-backups')
130 140 try:
131 141 os.mkdir(backupdir)
132 142 except OSError, err:
133 143 if err.errno != errno.EEXIST:
134 144 raise
135 145 try:
136 146 # backup continues
137 147 for f in tobackup:
138 148 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
139 149 dir=backupdir)
140 150 os.close(fd)
141 151 ui.debug('backup %r as %r\n' % (f, tmpname))
142 152 util.copyfile(repo.wjoin(f), tmpname)
143 153 shutil.copystat(repo.wjoin(f), tmpname)
144 154 backups[f] = tmpname
145 155
146 156 fp = cStringIO.StringIO()
147 157 for c in chunks:
148 158 fname = c.filename()
149 159 if fname in backups:
150 160 c.write(fp)
151 161 dopatch = fp.tell()
152 162 fp.seek(0)
153 163
164 [os.unlink(c) for c in newlyaddedandmodifiedfiles]
154 165 # 3a. apply filtered patch to clean repo (clean)
155 166 if backups:
156 167 # Equivalent to hg.revert
157 168 choices = lambda key: key in backups
158 169 mergemod.update(repo, repo.dirstate.p1(),
159 170 False, True, choices)
160 171
161 172 # 3b. (apply)
162 173 if dopatch:
163 174 try:
164 175 ui.debug('applying patch\n')
165 176 ui.debug(fp.getvalue())
166 177 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
167 178 except patch.PatchError, err:
168 179 raise util.Abort(str(err))
169 180 del fp
170 181
171 182 # 4. We prepared working directory according to filtered
172 183 # patch. Now is the time to delegate the job to
173 184 # commit/qrefresh or the like!
174 185
175 186 # Make all of the pathnames absolute.
176 187 newfiles = [repo.wjoin(nf) for nf in newfiles]
177 188 return commitfunc(ui, repo, *newfiles, **opts)
178 189 finally:
179 190 # 5. finally restore backed-up files
180 191 try:
181 192 for realname, tmpname in backups.iteritems():
182 193 ui.debug('restoring %r to %r\n' % (tmpname, realname))
183 194 util.copyfile(tmpname, repo.wjoin(realname))
184 195 # Our calls to copystat() here and above are a
185 196 # hack to trick any editors that have f open that
186 197 # we haven't modified them.
187 198 #
188 199 # Also note that this racy as an editor could
189 200 # notice the file's mtime before we've finished
190 201 # writing it.
191 202 shutil.copystat(tmpname, repo.wjoin(realname))
192 203 os.unlink(tmpname)
193 204 if tobackup:
194 205 os.rmdir(backupdir)
195 206 except OSError:
196 207 pass
197 208
198 209 return commit(ui, repo, recordfunc, pats, opts)
199 210
200 211 def findpossible(cmd, table, strict=False):
201 212 """
202 213 Return cmd -> (aliases, command table entry)
203 214 for each matching command.
204 215 Return debug commands (or their aliases) only if no normal command matches.
205 216 """
206 217 choice = {}
207 218 debugchoice = {}
208 219
209 220 if cmd in table:
210 221 # short-circuit exact matches, "log" alias beats "^log|history"
211 222 keys = [cmd]
212 223 else:
213 224 keys = table.keys()
214 225
215 226 allcmds = []
216 227 for e in keys:
217 228 aliases = parsealiases(e)
218 229 allcmds.extend(aliases)
219 230 found = None
220 231 if cmd in aliases:
221 232 found = cmd
222 233 elif not strict:
223 234 for a in aliases:
224 235 if a.startswith(cmd):
225 236 found = a
226 237 break
227 238 if found is not None:
228 239 if aliases[0].startswith("debug") or found.startswith("debug"):
229 240 debugchoice[found] = (aliases, table[e])
230 241 else:
231 242 choice[found] = (aliases, table[e])
232 243
233 244 if not choice and debugchoice:
234 245 choice = debugchoice
235 246
236 247 return choice, allcmds
237 248
238 249 def findcmd(cmd, table, strict=True):
239 250 """Return (aliases, command table entry) for command string."""
240 251 choice, allcmds = findpossible(cmd, table, strict)
241 252
242 253 if cmd in choice:
243 254 return choice[cmd]
244 255
245 256 if len(choice) > 1:
246 257 clist = choice.keys()
247 258 clist.sort()
248 259 raise error.AmbiguousCommand(cmd, clist)
249 260
250 261 if choice:
251 262 return choice.values()[0]
252 263
253 264 raise error.UnknownCommand(cmd, allcmds)
254 265
255 266 def findrepo(p):
256 267 while not os.path.isdir(os.path.join(p, ".hg")):
257 268 oldp, p = p, os.path.dirname(p)
258 269 if p == oldp:
259 270 return None
260 271
261 272 return p
262 273
263 274 def bailifchanged(repo, merge=True):
264 275 if merge and repo.dirstate.p2() != nullid:
265 276 raise util.Abort(_('outstanding uncommitted merge'))
266 277 modified, added, removed, deleted = repo.status()[:4]
267 278 if modified or added or removed or deleted:
268 279 raise util.Abort(_('uncommitted changes'))
269 280 ctx = repo[None]
270 281 for s in sorted(ctx.substate):
271 282 ctx.sub(s).bailifchanged()
272 283
273 284 def logmessage(ui, opts):
274 285 """ get the log message according to -m and -l option """
275 286 message = opts.get('message')
276 287 logfile = opts.get('logfile')
277 288
278 289 if message and logfile:
279 290 raise util.Abort(_('options --message and --logfile are mutually '
280 291 'exclusive'))
281 292 if not message and logfile:
282 293 try:
283 294 if logfile == '-':
284 295 message = ui.fin.read()
285 296 else:
286 297 message = '\n'.join(util.readfile(logfile).splitlines())
287 298 except IOError, inst:
288 299 raise util.Abort(_("can't read commit message '%s': %s") %
289 300 (logfile, inst.strerror))
290 301 return message
291 302
292 303 def mergeeditform(ctxorbool, baseformname):
293 304 """return appropriate editform name (referencing a committemplate)
294 305
295 306 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
296 307 merging is committed.
297 308
298 309 This returns baseformname with '.merge' appended if it is a merge,
299 310 otherwise '.normal' is appended.
300 311 """
301 312 if isinstance(ctxorbool, bool):
302 313 if ctxorbool:
303 314 return baseformname + ".merge"
304 315 elif 1 < len(ctxorbool.parents()):
305 316 return baseformname + ".merge"
306 317
307 318 return baseformname + ".normal"
308 319
309 320 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
310 321 editform='', **opts):
311 322 """get appropriate commit message editor according to '--edit' option
312 323
313 324 'finishdesc' is a function to be called with edited commit message
314 325 (= 'description' of the new changeset) just after editing, but
315 326 before checking empty-ness. It should return actual text to be
316 327 stored into history. This allows to change description before
317 328 storing.
318 329
319 330 'extramsg' is a extra message to be shown in the editor instead of
320 331 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
321 332 is automatically added.
322 333
323 334 'editform' is a dot-separated list of names, to distinguish
324 335 the purpose of commit text editing.
325 336
326 337 'getcommiteditor' returns 'commitforceeditor' regardless of
327 338 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
328 339 they are specific for usage in MQ.
329 340 """
330 341 if edit or finishdesc or extramsg:
331 342 return lambda r, c, s: commitforceeditor(r, c, s,
332 343 finishdesc=finishdesc,
333 344 extramsg=extramsg,
334 345 editform=editform)
335 346 elif editform:
336 347 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
337 348 else:
338 349 return commiteditor
339 350
340 351 def loglimit(opts):
341 352 """get the log limit according to option -l/--limit"""
342 353 limit = opts.get('limit')
343 354 if limit:
344 355 try:
345 356 limit = int(limit)
346 357 except ValueError:
347 358 raise util.Abort(_('limit must be a positive integer'))
348 359 if limit <= 0:
349 360 raise util.Abort(_('limit must be positive'))
350 361 else:
351 362 limit = None
352 363 return limit
353 364
354 365 def makefilename(repo, pat, node, desc=None,
355 366 total=None, seqno=None, revwidth=None, pathname=None):
356 367 node_expander = {
357 368 'H': lambda: hex(node),
358 369 'R': lambda: str(repo.changelog.rev(node)),
359 370 'h': lambda: short(node),
360 371 'm': lambda: re.sub('[^\w]', '_', str(desc))
361 372 }
362 373 expander = {
363 374 '%': lambda: '%',
364 375 'b': lambda: os.path.basename(repo.root),
365 376 }
366 377
367 378 try:
368 379 if node:
369 380 expander.update(node_expander)
370 381 if node:
371 382 expander['r'] = (lambda:
372 383 str(repo.changelog.rev(node)).zfill(revwidth or 0))
373 384 if total is not None:
374 385 expander['N'] = lambda: str(total)
375 386 if seqno is not None:
376 387 expander['n'] = lambda: str(seqno)
377 388 if total is not None and seqno is not None:
378 389 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
379 390 if pathname is not None:
380 391 expander['s'] = lambda: os.path.basename(pathname)
381 392 expander['d'] = lambda: os.path.dirname(pathname) or '.'
382 393 expander['p'] = lambda: pathname
383 394
384 395 newname = []
385 396 patlen = len(pat)
386 397 i = 0
387 398 while i < patlen:
388 399 c = pat[i]
389 400 if c == '%':
390 401 i += 1
391 402 c = pat[i]
392 403 c = expander[c]()
393 404 newname.append(c)
394 405 i += 1
395 406 return ''.join(newname)
396 407 except KeyError, inst:
397 408 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
398 409 inst.args[0])
399 410
400 411 def makefileobj(repo, pat, node=None, desc=None, total=None,
401 412 seqno=None, revwidth=None, mode='wb', modemap=None,
402 413 pathname=None):
403 414
404 415 writable = mode not in ('r', 'rb')
405 416
406 417 if not pat or pat == '-':
407 418 if writable:
408 419 fp = repo.ui.fout
409 420 else:
410 421 fp = repo.ui.fin
411 422 if util.safehasattr(fp, 'fileno'):
412 423 return os.fdopen(os.dup(fp.fileno()), mode)
413 424 else:
414 425 # if this fp can't be duped properly, return
415 426 # a dummy object that can be closed
416 427 class wrappedfileobj(object):
417 428 noop = lambda x: None
418 429 def __init__(self, f):
419 430 self.f = f
420 431 def __getattr__(self, attr):
421 432 if attr == 'close':
422 433 return self.noop
423 434 else:
424 435 return getattr(self.f, attr)
425 436
426 437 return wrappedfileobj(fp)
427 438 if util.safehasattr(pat, 'write') and writable:
428 439 return pat
429 440 if util.safehasattr(pat, 'read') and 'r' in mode:
430 441 return pat
431 442 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
432 443 if modemap is not None:
433 444 mode = modemap.get(fn, mode)
434 445 if mode == 'wb':
435 446 modemap[fn] = 'ab'
436 447 return open(fn, mode)
437 448
438 449 def openrevlog(repo, cmd, file_, opts):
439 450 """opens the changelog, manifest, a filelog or a given revlog"""
440 451 cl = opts['changelog']
441 452 mf = opts['manifest']
442 453 msg = None
443 454 if cl and mf:
444 455 msg = _('cannot specify --changelog and --manifest at the same time')
445 456 elif cl or mf:
446 457 if file_:
447 458 msg = _('cannot specify filename with --changelog or --manifest')
448 459 elif not repo:
449 460 msg = _('cannot specify --changelog or --manifest '
450 461 'without a repository')
451 462 if msg:
452 463 raise util.Abort(msg)
453 464
454 465 r = None
455 466 if repo:
456 467 if cl:
457 468 r = repo.unfiltered().changelog
458 469 elif mf:
459 470 r = repo.manifest
460 471 elif file_:
461 472 filelog = repo.file(file_)
462 473 if len(filelog):
463 474 r = filelog
464 475 if not r:
465 476 if not file_:
466 477 raise error.CommandError(cmd, _('invalid arguments'))
467 478 if not os.path.isfile(file_):
468 479 raise util.Abort(_("revlog '%s' not found") % file_)
469 480 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
470 481 file_[:-2] + ".i")
471 482 return r
472 483
473 484 def copy(ui, repo, pats, opts, rename=False):
474 485 # called with the repo lock held
475 486 #
476 487 # hgsep => pathname that uses "/" to separate directories
477 488 # ossep => pathname that uses os.sep to separate directories
478 489 cwd = repo.getcwd()
479 490 targets = {}
480 491 after = opts.get("after")
481 492 dryrun = opts.get("dry_run")
482 493 wctx = repo[None]
483 494
484 495 def walkpat(pat):
485 496 srcs = []
486 497 if after:
487 498 badstates = '?'
488 499 else:
489 500 badstates = '?r'
490 501 m = scmutil.match(repo[None], [pat], opts, globbed=True)
491 502 for abs in repo.walk(m):
492 503 state = repo.dirstate[abs]
493 504 rel = m.rel(abs)
494 505 exact = m.exact(abs)
495 506 if state in badstates:
496 507 if exact and state == '?':
497 508 ui.warn(_('%s: not copying - file is not managed\n') % rel)
498 509 if exact and state == 'r':
499 510 ui.warn(_('%s: not copying - file has been marked for'
500 511 ' remove\n') % rel)
501 512 continue
502 513 # abs: hgsep
503 514 # rel: ossep
504 515 srcs.append((abs, rel, exact))
505 516 return srcs
506 517
507 518 # abssrc: hgsep
508 519 # relsrc: ossep
509 520 # otarget: ossep
510 521 def copyfile(abssrc, relsrc, otarget, exact):
511 522 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
512 523 if '/' in abstarget:
513 524 # We cannot normalize abstarget itself, this would prevent
514 525 # case only renames, like a => A.
515 526 abspath, absname = abstarget.rsplit('/', 1)
516 527 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
517 528 reltarget = repo.pathto(abstarget, cwd)
518 529 target = repo.wjoin(abstarget)
519 530 src = repo.wjoin(abssrc)
520 531 state = repo.dirstate[abstarget]
521 532
522 533 scmutil.checkportable(ui, abstarget)
523 534
524 535 # check for collisions
525 536 prevsrc = targets.get(abstarget)
526 537 if prevsrc is not None:
527 538 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
528 539 (reltarget, repo.pathto(abssrc, cwd),
529 540 repo.pathto(prevsrc, cwd)))
530 541 return
531 542
532 543 # check for overwrites
533 544 exists = os.path.lexists(target)
534 545 samefile = False
535 546 if exists and abssrc != abstarget:
536 547 if (repo.dirstate.normalize(abssrc) ==
537 548 repo.dirstate.normalize(abstarget)):
538 549 if not rename:
539 550 ui.warn(_("%s: can't copy - same file\n") % reltarget)
540 551 return
541 552 exists = False
542 553 samefile = True
543 554
544 555 if not after and exists or after and state in 'mn':
545 556 if not opts['force']:
546 557 ui.warn(_('%s: not overwriting - file exists\n') %
547 558 reltarget)
548 559 return
549 560
550 561 if after:
551 562 if not exists:
552 563 if rename:
553 564 ui.warn(_('%s: not recording move - %s does not exist\n') %
554 565 (relsrc, reltarget))
555 566 else:
556 567 ui.warn(_('%s: not recording copy - %s does not exist\n') %
557 568 (relsrc, reltarget))
558 569 return
559 570 elif not dryrun:
560 571 try:
561 572 if exists:
562 573 os.unlink(target)
563 574 targetdir = os.path.dirname(target) or '.'
564 575 if not os.path.isdir(targetdir):
565 576 os.makedirs(targetdir)
566 577 if samefile:
567 578 tmp = target + "~hgrename"
568 579 os.rename(src, tmp)
569 580 os.rename(tmp, target)
570 581 else:
571 582 util.copyfile(src, target)
572 583 srcexists = True
573 584 except IOError, inst:
574 585 if inst.errno == errno.ENOENT:
575 586 ui.warn(_('%s: deleted in working directory\n') % relsrc)
576 587 srcexists = False
577 588 else:
578 589 ui.warn(_('%s: cannot copy - %s\n') %
579 590 (relsrc, inst.strerror))
580 591 return True # report a failure
581 592
582 593 if ui.verbose or not exact:
583 594 if rename:
584 595 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
585 596 else:
586 597 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
587 598
588 599 targets[abstarget] = abssrc
589 600
590 601 # fix up dirstate
591 602 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
592 603 dryrun=dryrun, cwd=cwd)
593 604 if rename and not dryrun:
594 605 if not after and srcexists and not samefile:
595 606 util.unlinkpath(repo.wjoin(abssrc))
596 607 wctx.forget([abssrc])
597 608
598 609 # pat: ossep
599 610 # dest ossep
600 611 # srcs: list of (hgsep, hgsep, ossep, bool)
601 612 # return: function that takes hgsep and returns ossep
602 613 def targetpathfn(pat, dest, srcs):
603 614 if os.path.isdir(pat):
604 615 abspfx = pathutil.canonpath(repo.root, cwd, pat)
605 616 abspfx = util.localpath(abspfx)
606 617 if destdirexists:
607 618 striplen = len(os.path.split(abspfx)[0])
608 619 else:
609 620 striplen = len(abspfx)
610 621 if striplen:
611 622 striplen += len(os.sep)
612 623 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
613 624 elif destdirexists:
614 625 res = lambda p: os.path.join(dest,
615 626 os.path.basename(util.localpath(p)))
616 627 else:
617 628 res = lambda p: dest
618 629 return res
619 630
620 631 # pat: ossep
621 632 # dest ossep
622 633 # srcs: list of (hgsep, hgsep, ossep, bool)
623 634 # return: function that takes hgsep and returns ossep
624 635 def targetpathafterfn(pat, dest, srcs):
625 636 if matchmod.patkind(pat):
626 637 # a mercurial pattern
627 638 res = lambda p: os.path.join(dest,
628 639 os.path.basename(util.localpath(p)))
629 640 else:
630 641 abspfx = pathutil.canonpath(repo.root, cwd, pat)
631 642 if len(abspfx) < len(srcs[0][0]):
632 643 # A directory. Either the target path contains the last
633 644 # component of the source path or it does not.
634 645 def evalpath(striplen):
635 646 score = 0
636 647 for s in srcs:
637 648 t = os.path.join(dest, util.localpath(s[0])[striplen:])
638 649 if os.path.lexists(t):
639 650 score += 1
640 651 return score
641 652
642 653 abspfx = util.localpath(abspfx)
643 654 striplen = len(abspfx)
644 655 if striplen:
645 656 striplen += len(os.sep)
646 657 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
647 658 score = evalpath(striplen)
648 659 striplen1 = len(os.path.split(abspfx)[0])
649 660 if striplen1:
650 661 striplen1 += len(os.sep)
651 662 if evalpath(striplen1) > score:
652 663 striplen = striplen1
653 664 res = lambda p: os.path.join(dest,
654 665 util.localpath(p)[striplen:])
655 666 else:
656 667 # a file
657 668 if destdirexists:
658 669 res = lambda p: os.path.join(dest,
659 670 os.path.basename(util.localpath(p)))
660 671 else:
661 672 res = lambda p: dest
662 673 return res
663 674
664 675 pats = scmutil.expandpats(pats)
665 676 if not pats:
666 677 raise util.Abort(_('no source or destination specified'))
667 678 if len(pats) == 1:
668 679 raise util.Abort(_('no destination specified'))
669 680 dest = pats.pop()
670 681 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
671 682 if not destdirexists:
672 683 if len(pats) > 1 or matchmod.patkind(pats[0]):
673 684 raise util.Abort(_('with multiple sources, destination must be an '
674 685 'existing directory'))
675 686 if util.endswithsep(dest):
676 687 raise util.Abort(_('destination %s is not a directory') % dest)
677 688
678 689 tfn = targetpathfn
679 690 if after:
680 691 tfn = targetpathafterfn
681 692 copylist = []
682 693 for pat in pats:
683 694 srcs = walkpat(pat)
684 695 if not srcs:
685 696 continue
686 697 copylist.append((tfn(pat, dest, srcs), srcs))
687 698 if not copylist:
688 699 raise util.Abort(_('no files to copy'))
689 700
690 701 errors = 0
691 702 for targetpath, srcs in copylist:
692 703 for abssrc, relsrc, exact in srcs:
693 704 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
694 705 errors += 1
695 706
696 707 if errors:
697 708 ui.warn(_('(consider using --after)\n'))
698 709
699 710 return errors != 0
700 711
701 712 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
702 713 runargs=None, appendpid=False):
703 714 '''Run a command as a service.'''
704 715
705 716 def writepid(pid):
706 717 if opts['pid_file']:
707 718 if appendpid:
708 719 mode = 'a'
709 720 else:
710 721 mode = 'w'
711 722 fp = open(opts['pid_file'], mode)
712 723 fp.write(str(pid) + '\n')
713 724 fp.close()
714 725
715 726 if opts['daemon'] and not opts['daemon_pipefds']:
716 727 # Signal child process startup with file removal
717 728 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
718 729 os.close(lockfd)
719 730 try:
720 731 if not runargs:
721 732 runargs = util.hgcmd() + sys.argv[1:]
722 733 runargs.append('--daemon-pipefds=%s' % lockpath)
723 734 # Don't pass --cwd to the child process, because we've already
724 735 # changed directory.
725 736 for i in xrange(1, len(runargs)):
726 737 if runargs[i].startswith('--cwd='):
727 738 del runargs[i]
728 739 break
729 740 elif runargs[i].startswith('--cwd'):
730 741 del runargs[i:i + 2]
731 742 break
732 743 def condfn():
733 744 return not os.path.exists(lockpath)
734 745 pid = util.rundetached(runargs, condfn)
735 746 if pid < 0:
736 747 raise util.Abort(_('child process failed to start'))
737 748 writepid(pid)
738 749 finally:
739 750 try:
740 751 os.unlink(lockpath)
741 752 except OSError, e:
742 753 if e.errno != errno.ENOENT:
743 754 raise
744 755 if parentfn:
745 756 return parentfn(pid)
746 757 else:
747 758 return
748 759
749 760 if initfn:
750 761 initfn()
751 762
752 763 if not opts['daemon']:
753 764 writepid(os.getpid())
754 765
755 766 if opts['daemon_pipefds']:
756 767 lockpath = opts['daemon_pipefds']
757 768 try:
758 769 os.setsid()
759 770 except AttributeError:
760 771 pass
761 772 os.unlink(lockpath)
762 773 util.hidewindow()
763 774 sys.stdout.flush()
764 775 sys.stderr.flush()
765 776
766 777 nullfd = os.open(os.devnull, os.O_RDWR)
767 778 logfilefd = nullfd
768 779 if logfile:
769 780 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
770 781 os.dup2(nullfd, 0)
771 782 os.dup2(logfilefd, 1)
772 783 os.dup2(logfilefd, 2)
773 784 if nullfd not in (0, 1, 2):
774 785 os.close(nullfd)
775 786 if logfile and logfilefd not in (0, 1, 2):
776 787 os.close(logfilefd)
777 788
778 789 if runfn:
779 790 return runfn()
780 791
781 792 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
782 793 """Utility function used by commands.import to import a single patch
783 794
784 795 This function is explicitly defined here to help the evolve extension to
785 796 wrap this part of the import logic.
786 797
787 798 The API is currently a bit ugly because it a simple code translation from
788 799 the import command. Feel free to make it better.
789 800
790 801 :hunk: a patch (as a binary string)
791 802 :parents: nodes that will be parent of the created commit
792 803 :opts: the full dict of option passed to the import command
793 804 :msgs: list to save commit message to.
794 805 (used in case we need to save it when failing)
795 806 :updatefunc: a function that update a repo to a given node
796 807 updatefunc(<repo>, <node>)
797 808 """
798 809 tmpname, message, user, date, branch, nodeid, p1, p2 = \
799 810 patch.extract(ui, hunk)
800 811
801 812 update = not opts.get('bypass')
802 813 strip = opts["strip"]
803 814 prefix = opts["prefix"]
804 815 sim = float(opts.get('similarity') or 0)
805 816 if not tmpname:
806 817 return (None, None, False)
807 818 msg = _('applied to working directory')
808 819
809 820 rejects = False
810 821
811 822 try:
812 823 cmdline_message = logmessage(ui, opts)
813 824 if cmdline_message:
814 825 # pickup the cmdline msg
815 826 message = cmdline_message
816 827 elif message:
817 828 # pickup the patch msg
818 829 message = message.strip()
819 830 else:
820 831 # launch the editor
821 832 message = None
822 833 ui.debug('message:\n%s\n' % message)
823 834
824 835 if len(parents) == 1:
825 836 parents.append(repo[nullid])
826 837 if opts.get('exact'):
827 838 if not nodeid or not p1:
828 839 raise util.Abort(_('not a Mercurial patch'))
829 840 p1 = repo[p1]
830 841 p2 = repo[p2 or nullid]
831 842 elif p2:
832 843 try:
833 844 p1 = repo[p1]
834 845 p2 = repo[p2]
835 846 # Without any options, consider p2 only if the
836 847 # patch is being applied on top of the recorded
837 848 # first parent.
838 849 if p1 != parents[0]:
839 850 p1 = parents[0]
840 851 p2 = repo[nullid]
841 852 except error.RepoError:
842 853 p1, p2 = parents
843 854 if p2.node() == nullid:
844 855 ui.warn(_("warning: import the patch as a normal revision\n"
845 856 "(use --exact to import the patch as a merge)\n"))
846 857 else:
847 858 p1, p2 = parents
848 859
849 860 n = None
850 861 if update:
851 862 repo.dirstate.beginparentchange()
852 863 if p1 != parents[0]:
853 864 updatefunc(repo, p1.node())
854 865 if p2 != parents[1]:
855 866 repo.setparents(p1.node(), p2.node())
856 867
857 868 if opts.get('exact') or opts.get('import_branch'):
858 869 repo.dirstate.setbranch(branch or 'default')
859 870
860 871 partial = opts.get('partial', False)
861 872 files = set()
862 873 try:
863 874 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
864 875 files=files, eolmode=None, similarity=sim / 100.0)
865 876 except patch.PatchError, e:
866 877 if not partial:
867 878 raise util.Abort(str(e))
868 879 if partial:
869 880 rejects = True
870 881
871 882 files = list(files)
872 883 if opts.get('no_commit'):
873 884 if message:
874 885 msgs.append(message)
875 886 else:
876 887 if opts.get('exact') or p2:
877 888 # If you got here, you either use --force and know what
878 889 # you are doing or used --exact or a merge patch while
879 890 # being updated to its first parent.
880 891 m = None
881 892 else:
882 893 m = scmutil.matchfiles(repo, files or [])
883 894 editform = mergeeditform(repo[None], 'import.normal')
884 895 if opts.get('exact'):
885 896 editor = None
886 897 else:
887 898 editor = getcommiteditor(editform=editform, **opts)
888 899 n = repo.commit(message, opts.get('user') or user,
889 900 opts.get('date') or date, match=m,
890 901 editor=editor, force=partial)
891 902 repo.dirstate.endparentchange()
892 903 else:
893 904 if opts.get('exact') or opts.get('import_branch'):
894 905 branch = branch or 'default'
895 906 else:
896 907 branch = p1.branch()
897 908 store = patch.filestore()
898 909 try:
899 910 files = set()
900 911 try:
901 912 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
902 913 files, eolmode=None)
903 914 except patch.PatchError, e:
904 915 raise util.Abort(str(e))
905 916 if opts.get('exact'):
906 917 editor = None
907 918 else:
908 919 editor = getcommiteditor(editform='import.bypass')
909 920 memctx = context.makememctx(repo, (p1.node(), p2.node()),
910 921 message,
911 922 opts.get('user') or user,
912 923 opts.get('date') or date,
913 924 branch, files, store,
914 925 editor=editor)
915 926 n = memctx.commit()
916 927 finally:
917 928 store.close()
918 929 if opts.get('exact') and opts.get('no_commit'):
919 930 # --exact with --no-commit is still useful in that it does merge
920 931 # and branch bits
921 932 ui.warn(_("warning: can't check exact import with --no-commit\n"))
922 933 elif opts.get('exact') and hex(n) != nodeid:
923 934 raise util.Abort(_('patch is damaged or loses information'))
924 935 if n:
925 936 # i18n: refers to a short changeset id
926 937 msg = _('created %s') % short(n)
927 938 return (msg, n, rejects)
928 939 finally:
929 940 os.unlink(tmpname)
930 941
931 942 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
932 943 opts=None):
933 944 '''export changesets as hg patches.'''
934 945
935 946 total = len(revs)
936 947 revwidth = max([len(str(rev)) for rev in revs])
937 948 filemode = {}
938 949
939 950 def single(rev, seqno, fp):
940 951 ctx = repo[rev]
941 952 node = ctx.node()
942 953 parents = [p.node() for p in ctx.parents() if p]
943 954 branch = ctx.branch()
944 955 if switch_parent:
945 956 parents.reverse()
946 957
947 958 if parents:
948 959 prev = parents[0]
949 960 else:
950 961 prev = nullid
951 962
952 963 shouldclose = False
953 964 if not fp and len(template) > 0:
954 965 desc_lines = ctx.description().rstrip().split('\n')
955 966 desc = desc_lines[0] #Commit always has a first line.
956 967 fp = makefileobj(repo, template, node, desc=desc, total=total,
957 968 seqno=seqno, revwidth=revwidth, mode='wb',
958 969 modemap=filemode)
959 970 if fp != template:
960 971 shouldclose = True
961 972 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
962 973 repo.ui.note("%s\n" % fp.name)
963 974
964 975 if not fp:
965 976 write = repo.ui.write
966 977 else:
967 978 def write(s, **kw):
968 979 fp.write(s)
969 980
970 981 write("# HG changeset patch\n")
971 982 write("# User %s\n" % ctx.user())
972 983 write("# Date %d %d\n" % ctx.date())
973 984 write("# %s\n" % util.datestr(ctx.date()))
974 985 if branch and branch != 'default':
975 986 write("# Branch %s\n" % branch)
976 987 write("# Node ID %s\n" % hex(node))
977 988 write("# Parent %s\n" % hex(prev))
978 989 if len(parents) > 1:
979 990 write("# Parent %s\n" % hex(parents[1]))
980 991 write(ctx.description().rstrip())
981 992 write("\n\n")
982 993
983 994 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
984 995 write(chunk, label=label)
985 996
986 997 if shouldclose:
987 998 fp.close()
988 999
989 1000 for seqno, rev in enumerate(revs):
990 1001 single(rev, seqno + 1, fp)
991 1002
992 1003 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
993 1004 changes=None, stat=False, fp=None, prefix='',
994 1005 root='', listsubrepos=False):
995 1006 '''show diff or diffstat.'''
996 1007 if fp is None:
997 1008 write = ui.write
998 1009 else:
999 1010 def write(s, **kw):
1000 1011 fp.write(s)
1001 1012
1002 1013 if root:
1003 1014 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1004 1015 else:
1005 1016 relroot = ''
1006 1017 if relroot != '':
1007 1018 # XXX relative roots currently don't work if the root is within a
1008 1019 # subrepo
1009 1020 uirelroot = match.uipath(relroot)
1010 1021 relroot += '/'
1011 1022 for matchroot in match.files():
1012 1023 if not matchroot.startswith(relroot):
1013 1024 ui.warn(_('warning: %s not inside relative root %s\n') % (
1014 1025 match.uipath(matchroot), uirelroot))
1015 1026
1016 1027 if stat:
1017 1028 diffopts = diffopts.copy(context=0)
1018 1029 width = 80
1019 1030 if not ui.plain():
1020 1031 width = ui.termwidth()
1021 1032 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1022 1033 prefix=prefix, relroot=relroot)
1023 1034 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1024 1035 width=width,
1025 1036 git=diffopts.git):
1026 1037 write(chunk, label=label)
1027 1038 else:
1028 1039 for chunk, label in patch.diffui(repo, node1, node2, match,
1029 1040 changes, diffopts, prefix=prefix,
1030 1041 relroot=relroot):
1031 1042 write(chunk, label=label)
1032 1043
1033 1044 if listsubrepos:
1034 1045 ctx1 = repo[node1]
1035 1046 ctx2 = repo[node2]
1036 1047 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1037 1048 tempnode2 = node2
1038 1049 try:
1039 1050 if node2 is not None:
1040 1051 tempnode2 = ctx2.substate[subpath][1]
1041 1052 except KeyError:
1042 1053 # A subrepo that existed in node1 was deleted between node1 and
1043 1054 # node2 (inclusive). Thus, ctx2's substate won't contain that
1044 1055 # subpath. The best we can do is to ignore it.
1045 1056 tempnode2 = None
1046 1057 submatch = matchmod.narrowmatcher(subpath, match)
1047 1058 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1048 1059 stat=stat, fp=fp, prefix=prefix)
1049 1060
1050 1061 class changeset_printer(object):
1051 1062 '''show changeset information when templating not requested.'''
1052 1063
1053 1064 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1054 1065 self.ui = ui
1055 1066 self.repo = repo
1056 1067 self.buffered = buffered
1057 1068 self.matchfn = matchfn
1058 1069 self.diffopts = diffopts
1059 1070 self.header = {}
1060 1071 self.hunk = {}
1061 1072 self.lastheader = None
1062 1073 self.footer = None
1063 1074
1064 1075 def flush(self, rev):
1065 1076 if rev in self.header:
1066 1077 h = self.header[rev]
1067 1078 if h != self.lastheader:
1068 1079 self.lastheader = h
1069 1080 self.ui.write(h)
1070 1081 del self.header[rev]
1071 1082 if rev in self.hunk:
1072 1083 self.ui.write(self.hunk[rev])
1073 1084 del self.hunk[rev]
1074 1085 return 1
1075 1086 return 0
1076 1087
1077 1088 def close(self):
1078 1089 if self.footer:
1079 1090 self.ui.write(self.footer)
1080 1091
1081 1092 def show(self, ctx, copies=None, matchfn=None, **props):
1082 1093 if self.buffered:
1083 1094 self.ui.pushbuffer()
1084 1095 self._show(ctx, copies, matchfn, props)
1085 1096 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
1086 1097 else:
1087 1098 self._show(ctx, copies, matchfn, props)
1088 1099
1089 1100 def _show(self, ctx, copies, matchfn, props):
1090 1101 '''show a single changeset or file revision'''
1091 1102 changenode = ctx.node()
1092 1103 rev = ctx.rev()
1093 1104 if self.ui.debugflag:
1094 1105 hexfunc = hex
1095 1106 else:
1096 1107 hexfunc = short
1097 1108 if rev is None:
1098 1109 pctx = ctx.p1()
1099 1110 revnode = (pctx.rev(), hexfunc(pctx.node()) + '+')
1100 1111 else:
1101 1112 revnode = (rev, hexfunc(changenode))
1102 1113
1103 1114 if self.ui.quiet:
1104 1115 self.ui.write("%d:%s\n" % revnode, label='log.node')
1105 1116 return
1106 1117
1107 1118 date = util.datestr(ctx.date())
1108 1119
1109 1120 # i18n: column positioning for "hg log"
1110 1121 self.ui.write(_("changeset: %d:%s\n") % revnode,
1111 1122 label='log.changeset changeset.%s' % ctx.phasestr())
1112 1123
1113 1124 # branches are shown first before any other names due to backwards
1114 1125 # compatibility
1115 1126 branch = ctx.branch()
1116 1127 # don't show the default branch name
1117 1128 if branch != 'default':
1118 1129 # i18n: column positioning for "hg log"
1119 1130 self.ui.write(_("branch: %s\n") % branch,
1120 1131 label='log.branch')
1121 1132
1122 1133 for name, ns in self.repo.names.iteritems():
1123 1134 # branches has special logic already handled above, so here we just
1124 1135 # skip it
1125 1136 if name == 'branches':
1126 1137 continue
1127 1138 # we will use the templatename as the color name since those two
1128 1139 # should be the same
1129 1140 for name in ns.names(self.repo, changenode):
1130 1141 self.ui.write(ns.logfmt % name,
1131 1142 label='log.%s' % ns.colorname)
1132 1143 if self.ui.debugflag:
1133 1144 # i18n: column positioning for "hg log"
1134 1145 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
1135 1146 label='log.phase')
1136 1147 for pctx in self._meaningful_parentrevs(ctx):
1137 1148 label = 'log.parent changeset.%s' % pctx.phasestr()
1138 1149 # i18n: column positioning for "hg log"
1139 1150 self.ui.write(_("parent: %d:%s\n")
1140 1151 % (pctx.rev(), hexfunc(pctx.node())),
1141 1152 label=label)
1142 1153
1143 1154 if self.ui.debugflag and rev is not None:
1144 1155 mnode = ctx.manifestnode()
1145 1156 # i18n: column positioning for "hg log"
1146 1157 self.ui.write(_("manifest: %d:%s\n") %
1147 1158 (self.repo.manifest.rev(mnode), hex(mnode)),
1148 1159 label='ui.debug log.manifest')
1149 1160 # i18n: column positioning for "hg log"
1150 1161 self.ui.write(_("user: %s\n") % ctx.user(),
1151 1162 label='log.user')
1152 1163 # i18n: column positioning for "hg log"
1153 1164 self.ui.write(_("date: %s\n") % date,
1154 1165 label='log.date')
1155 1166
1156 1167 if self.ui.debugflag:
1157 1168 files = ctx.p1().status(ctx)[:3]
1158 1169 for key, value in zip([# i18n: column positioning for "hg log"
1159 1170 _("files:"),
1160 1171 # i18n: column positioning for "hg log"
1161 1172 _("files+:"),
1162 1173 # i18n: column positioning for "hg log"
1163 1174 _("files-:")], files):
1164 1175 if value:
1165 1176 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1166 1177 label='ui.debug log.files')
1167 1178 elif ctx.files() and self.ui.verbose:
1168 1179 # i18n: column positioning for "hg log"
1169 1180 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1170 1181 label='ui.note log.files')
1171 1182 if copies and self.ui.verbose:
1172 1183 copies = ['%s (%s)' % c for c in copies]
1173 1184 # i18n: column positioning for "hg log"
1174 1185 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1175 1186 label='ui.note log.copies')
1176 1187
1177 1188 extra = ctx.extra()
1178 1189 if extra and self.ui.debugflag:
1179 1190 for key, value in sorted(extra.items()):
1180 1191 # i18n: column positioning for "hg log"
1181 1192 self.ui.write(_("extra: %s=%s\n")
1182 1193 % (key, value.encode('string_escape')),
1183 1194 label='ui.debug log.extra')
1184 1195
1185 1196 description = ctx.description().strip()
1186 1197 if description:
1187 1198 if self.ui.verbose:
1188 1199 self.ui.write(_("description:\n"),
1189 1200 label='ui.note log.description')
1190 1201 self.ui.write(description,
1191 1202 label='ui.note log.description')
1192 1203 self.ui.write("\n\n")
1193 1204 else:
1194 1205 # i18n: column positioning for "hg log"
1195 1206 self.ui.write(_("summary: %s\n") %
1196 1207 description.splitlines()[0],
1197 1208 label='log.summary')
1198 1209 self.ui.write("\n")
1199 1210
1200 1211 self.showpatch(changenode, matchfn)
1201 1212
1202 1213 def showpatch(self, node, matchfn):
1203 1214 if not matchfn:
1204 1215 matchfn = self.matchfn
1205 1216 if matchfn:
1206 1217 stat = self.diffopts.get('stat')
1207 1218 diff = self.diffopts.get('patch')
1208 1219 diffopts = patch.diffallopts(self.ui, self.diffopts)
1209 1220 prev = self.repo.changelog.parents(node)[0]
1210 1221 if stat:
1211 1222 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1212 1223 match=matchfn, stat=True)
1213 1224 if diff:
1214 1225 if stat:
1215 1226 self.ui.write("\n")
1216 1227 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1217 1228 match=matchfn, stat=False)
1218 1229 self.ui.write("\n")
1219 1230
1220 1231 def _meaningful_parentrevs(self, ctx):
1221 1232 """Return list of meaningful (or all if debug) parentrevs for rev.
1222 1233
1223 1234 For merges (two non-nullrev revisions) both parents are meaningful.
1224 1235 Otherwise the first parent revision is considered meaningful if it
1225 1236 is not the preceding revision.
1226 1237 """
1227 1238 parents = ctx.parents()
1228 1239 if len(parents) > 1:
1229 1240 return parents
1230 1241 if self.ui.debugflag:
1231 1242 return [parents[0], self.repo['null']]
1232 1243 if parents[0].rev() >= scmutil.intrev(self.repo, ctx.rev()) - 1:
1233 1244 return []
1234 1245 return parents
1235 1246
1236 1247 class jsonchangeset(changeset_printer):
1237 1248 '''format changeset information.'''
1238 1249
1239 1250 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1240 1251 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1241 1252 self.cache = {}
1242 1253 self._first = True
1243 1254
1244 1255 def close(self):
1245 1256 if not self._first:
1246 1257 self.ui.write("\n]\n")
1247 1258 else:
1248 1259 self.ui.write("[]\n")
1249 1260
1250 1261 def _show(self, ctx, copies, matchfn, props):
1251 1262 '''show a single changeset or file revision'''
1252 1263 rev = ctx.rev()
1253 1264 if rev is None:
1254 1265 jrev = jnode = 'null'
1255 1266 else:
1256 1267 jrev = str(rev)
1257 1268 jnode = '"%s"' % hex(ctx.node())
1258 1269 j = encoding.jsonescape
1259 1270
1260 1271 if self._first:
1261 1272 self.ui.write("[\n {")
1262 1273 self._first = False
1263 1274 else:
1264 1275 self.ui.write(",\n {")
1265 1276
1266 1277 if self.ui.quiet:
1267 1278 self.ui.write('\n "rev": %s' % jrev)
1268 1279 self.ui.write(',\n "node": %s' % jnode)
1269 1280 self.ui.write('\n }')
1270 1281 return
1271 1282
1272 1283 self.ui.write('\n "rev": %s' % jrev)
1273 1284 self.ui.write(',\n "node": %s' % jnode)
1274 1285 self.ui.write(',\n "branch": "%s"' % j(ctx.branch()))
1275 1286 self.ui.write(',\n "phase": "%s"' % ctx.phasestr())
1276 1287 self.ui.write(',\n "user": "%s"' % j(ctx.user()))
1277 1288 self.ui.write(',\n "date": [%d, %d]' % ctx.date())
1278 1289 self.ui.write(',\n "desc": "%s"' % j(ctx.description()))
1279 1290
1280 1291 self.ui.write(',\n "bookmarks": [%s]' %
1281 1292 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1282 1293 self.ui.write(',\n "tags": [%s]' %
1283 1294 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1284 1295 self.ui.write(',\n "parents": [%s]' %
1285 1296 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1286 1297
1287 1298 if self.ui.debugflag:
1288 1299 if rev is None:
1289 1300 jmanifestnode = 'null'
1290 1301 else:
1291 1302 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1292 1303 self.ui.write(',\n "manifest": %s' % jmanifestnode)
1293 1304
1294 1305 self.ui.write(',\n "extra": {%s}' %
1295 1306 ", ".join('"%s": "%s"' % (j(k), j(v))
1296 1307 for k, v in ctx.extra().items()))
1297 1308
1298 1309 files = ctx.p1().status(ctx)
1299 1310 self.ui.write(',\n "modified": [%s]' %
1300 1311 ", ".join('"%s"' % j(f) for f in files[0]))
1301 1312 self.ui.write(',\n "added": [%s]' %
1302 1313 ", ".join('"%s"' % j(f) for f in files[1]))
1303 1314 self.ui.write(',\n "removed": [%s]' %
1304 1315 ", ".join('"%s"' % j(f) for f in files[2]))
1305 1316
1306 1317 elif self.ui.verbose:
1307 1318 self.ui.write(',\n "files": [%s]' %
1308 1319 ", ".join('"%s"' % j(f) for f in ctx.files()))
1309 1320
1310 1321 if copies:
1311 1322 self.ui.write(',\n "copies": {%s}' %
1312 1323 ", ".join('"%s": "%s"' % (j(k), j(v))
1313 1324 for k, v in copies))
1314 1325
1315 1326 matchfn = self.matchfn
1316 1327 if matchfn:
1317 1328 stat = self.diffopts.get('stat')
1318 1329 diff = self.diffopts.get('patch')
1319 1330 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1320 1331 node, prev = ctx.node(), ctx.p1().node()
1321 1332 if stat:
1322 1333 self.ui.pushbuffer()
1323 1334 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1324 1335 match=matchfn, stat=True)
1325 1336 self.ui.write(',\n "diffstat": "%s"' % j(self.ui.popbuffer()))
1326 1337 if diff:
1327 1338 self.ui.pushbuffer()
1328 1339 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1329 1340 match=matchfn, stat=False)
1330 1341 self.ui.write(',\n "diff": "%s"' % j(self.ui.popbuffer()))
1331 1342
1332 1343 self.ui.write("\n }")
1333 1344
1334 1345 class changeset_templater(changeset_printer):
1335 1346 '''format changeset information.'''
1336 1347
1337 1348 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1338 1349 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1339 1350 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1340 1351 defaulttempl = {
1341 1352 'parent': '{rev}:{node|formatnode} ',
1342 1353 'manifest': '{rev}:{node|formatnode}',
1343 1354 'file_copy': '{name} ({source})',
1344 1355 'extra': '{key}={value|stringescape}'
1345 1356 }
1346 1357 # filecopy is preserved for compatibility reasons
1347 1358 defaulttempl['filecopy'] = defaulttempl['file_copy']
1348 1359 self.t = templater.templater(mapfile, {'formatnode': formatnode},
1349 1360 cache=defaulttempl)
1350 1361 if tmpl:
1351 1362 self.t.cache['changeset'] = tmpl
1352 1363
1353 1364 self.cache = {}
1354 1365
1355 1366 def _show(self, ctx, copies, matchfn, props):
1356 1367 '''show a single changeset or file revision'''
1357 1368
1358 1369 showlist = templatekw.showlist
1359 1370
1360 1371 # showparents() behaviour depends on ui trace level which
1361 1372 # causes unexpected behaviours at templating level and makes
1362 1373 # it harder to extract it in a standalone function. Its
1363 1374 # behaviour cannot be changed so leave it here for now.
1364 1375 def showparents(**args):
1365 1376 ctx = args['ctx']
1366 1377 parents = [[('rev', p.rev()),
1367 1378 ('node', p.hex()),
1368 1379 ('phase', p.phasestr())]
1369 1380 for p in self._meaningful_parentrevs(ctx)]
1370 1381 return showlist('parent', parents, **args)
1371 1382
1372 1383 props = props.copy()
1373 1384 props.update(templatekw.keywords)
1374 1385 props['parents'] = showparents
1375 1386 props['templ'] = self.t
1376 1387 props['ctx'] = ctx
1377 1388 props['repo'] = self.repo
1378 1389 props['revcache'] = {'copies': copies}
1379 1390 props['cache'] = self.cache
1380 1391
1381 1392 # find correct templates for current mode
1382 1393
1383 1394 tmplmodes = [
1384 1395 (True, None),
1385 1396 (self.ui.verbose, 'verbose'),
1386 1397 (self.ui.quiet, 'quiet'),
1387 1398 (self.ui.debugflag, 'debug'),
1388 1399 ]
1389 1400
1390 1401 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
1391 1402 for mode, postfix in tmplmodes:
1392 1403 for type in types:
1393 1404 cur = postfix and ('%s_%s' % (type, postfix)) or type
1394 1405 if mode and cur in self.t:
1395 1406 types[type] = cur
1396 1407
1397 1408 try:
1398 1409
1399 1410 # write header
1400 1411 if types['header']:
1401 1412 h = templater.stringify(self.t(types['header'], **props))
1402 1413 if self.buffered:
1403 1414 self.header[ctx.rev()] = h
1404 1415 else:
1405 1416 if self.lastheader != h:
1406 1417 self.lastheader = h
1407 1418 self.ui.write(h)
1408 1419
1409 1420 # write changeset metadata, then patch if requested
1410 1421 key = types['changeset']
1411 1422 self.ui.write(templater.stringify(self.t(key, **props)))
1412 1423 self.showpatch(ctx.node(), matchfn)
1413 1424
1414 1425 if types['footer']:
1415 1426 if not self.footer:
1416 1427 self.footer = templater.stringify(self.t(types['footer'],
1417 1428 **props))
1418 1429
1419 1430 except KeyError, inst:
1420 1431 msg = _("%s: no key named '%s'")
1421 1432 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1422 1433 except SyntaxError, inst:
1423 1434 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1424 1435
1425 1436 def gettemplate(ui, tmpl, style):
1426 1437 """
1427 1438 Find the template matching the given template spec or style.
1428 1439 """
1429 1440
1430 1441 # ui settings
1431 1442 if not tmpl and not style: # template are stronger than style
1432 1443 tmpl = ui.config('ui', 'logtemplate')
1433 1444 if tmpl:
1434 1445 try:
1435 1446 tmpl = templater.parsestring(tmpl)
1436 1447 except SyntaxError:
1437 1448 tmpl = templater.parsestring(tmpl, quoted=False)
1438 1449 return tmpl, None
1439 1450 else:
1440 1451 style = util.expandpath(ui.config('ui', 'style', ''))
1441 1452
1442 1453 if not tmpl and style:
1443 1454 mapfile = style
1444 1455 if not os.path.split(mapfile)[0]:
1445 1456 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1446 1457 or templater.templatepath(mapfile))
1447 1458 if mapname:
1448 1459 mapfile = mapname
1449 1460 return None, mapfile
1450 1461
1451 1462 if not tmpl:
1452 1463 return None, None
1453 1464
1454 1465 # looks like a literal template?
1455 1466 if '{' in tmpl:
1456 1467 return tmpl, None
1457 1468
1458 1469 # perhaps a stock style?
1459 1470 if not os.path.split(tmpl)[0]:
1460 1471 mapname = (templater.templatepath('map-cmdline.' + tmpl)
1461 1472 or templater.templatepath(tmpl))
1462 1473 if mapname and os.path.isfile(mapname):
1463 1474 return None, mapname
1464 1475
1465 1476 # perhaps it's a reference to [templates]
1466 1477 t = ui.config('templates', tmpl)
1467 1478 if t:
1468 1479 try:
1469 1480 tmpl = templater.parsestring(t)
1470 1481 except SyntaxError:
1471 1482 tmpl = templater.parsestring(t, quoted=False)
1472 1483 return tmpl, None
1473 1484
1474 1485 if tmpl == 'list':
1475 1486 ui.write(_("available styles: %s\n") % templater.stylelist())
1476 1487 raise util.Abort(_("specify a template"))
1477 1488
1478 1489 # perhaps it's a path to a map or a template
1479 1490 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
1480 1491 # is it a mapfile for a style?
1481 1492 if os.path.basename(tmpl).startswith("map-"):
1482 1493 return None, os.path.realpath(tmpl)
1483 1494 tmpl = open(tmpl).read()
1484 1495 return tmpl, None
1485 1496
1486 1497 # constant string?
1487 1498 return tmpl, None
1488 1499
1489 1500 def show_changeset(ui, repo, opts, buffered=False):
1490 1501 """show one changeset using template or regular display.
1491 1502
1492 1503 Display format will be the first non-empty hit of:
1493 1504 1. option 'template'
1494 1505 2. option 'style'
1495 1506 3. [ui] setting 'logtemplate'
1496 1507 4. [ui] setting 'style'
1497 1508 If all of these values are either the unset or the empty string,
1498 1509 regular display via changeset_printer() is done.
1499 1510 """
1500 1511 # options
1501 1512 matchfn = None
1502 1513 if opts.get('patch') or opts.get('stat'):
1503 1514 matchfn = scmutil.matchall(repo)
1504 1515
1505 1516 if opts.get('template') == 'json':
1506 1517 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1507 1518
1508 1519 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1509 1520
1510 1521 if not tmpl and not mapfile:
1511 1522 return changeset_printer(ui, repo, matchfn, opts, buffered)
1512 1523
1513 1524 try:
1514 1525 t = changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile,
1515 1526 buffered)
1516 1527 except SyntaxError, inst:
1517 1528 raise util.Abort(inst.args[0])
1518 1529 return t
1519 1530
1520 1531 def showmarker(ui, marker):
1521 1532 """utility function to display obsolescence marker in a readable way
1522 1533
1523 1534 To be used by debug function."""
1524 1535 ui.write(hex(marker.precnode()))
1525 1536 for repl in marker.succnodes():
1526 1537 ui.write(' ')
1527 1538 ui.write(hex(repl))
1528 1539 ui.write(' %X ' % marker.flags())
1529 1540 parents = marker.parentnodes()
1530 1541 if parents is not None:
1531 1542 ui.write('{%s} ' % ', '.join(hex(p) for p in parents))
1532 1543 ui.write('(%s) ' % util.datestr(marker.date()))
1533 1544 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1534 1545 sorted(marker.metadata().items())
1535 1546 if t[0] != 'date')))
1536 1547 ui.write('\n')
1537 1548
1538 1549 def finddate(ui, repo, date):
1539 1550 """Find the tipmost changeset that matches the given date spec"""
1540 1551
1541 1552 df = util.matchdate(date)
1542 1553 m = scmutil.matchall(repo)
1543 1554 results = {}
1544 1555
1545 1556 def prep(ctx, fns):
1546 1557 d = ctx.date()
1547 1558 if df(d[0]):
1548 1559 results[ctx.rev()] = d
1549 1560
1550 1561 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1551 1562 rev = ctx.rev()
1552 1563 if rev in results:
1553 1564 ui.status(_("found revision %s from %s\n") %
1554 1565 (rev, util.datestr(results[rev])))
1555 1566 return str(rev)
1556 1567
1557 1568 raise util.Abort(_("revision matching date not found"))
1558 1569
1559 1570 def increasingwindows(windowsize=8, sizelimit=512):
1560 1571 while True:
1561 1572 yield windowsize
1562 1573 if windowsize < sizelimit:
1563 1574 windowsize *= 2
1564 1575
1565 1576 class FileWalkError(Exception):
1566 1577 pass
1567 1578
1568 1579 def walkfilerevs(repo, match, follow, revs, fncache):
1569 1580 '''Walks the file history for the matched files.
1570 1581
1571 1582 Returns the changeset revs that are involved in the file history.
1572 1583
1573 1584 Throws FileWalkError if the file history can't be walked using
1574 1585 filelogs alone.
1575 1586 '''
1576 1587 wanted = set()
1577 1588 copies = []
1578 1589 minrev, maxrev = min(revs), max(revs)
1579 1590 def filerevgen(filelog, last):
1580 1591 """
1581 1592 Only files, no patterns. Check the history of each file.
1582 1593
1583 1594 Examines filelog entries within minrev, maxrev linkrev range
1584 1595 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1585 1596 tuples in backwards order
1586 1597 """
1587 1598 cl_count = len(repo)
1588 1599 revs = []
1589 1600 for j in xrange(0, last + 1):
1590 1601 linkrev = filelog.linkrev(j)
1591 1602 if linkrev < minrev:
1592 1603 continue
1593 1604 # only yield rev for which we have the changelog, it can
1594 1605 # happen while doing "hg log" during a pull or commit
1595 1606 if linkrev >= cl_count:
1596 1607 break
1597 1608
1598 1609 parentlinkrevs = []
1599 1610 for p in filelog.parentrevs(j):
1600 1611 if p != nullrev:
1601 1612 parentlinkrevs.append(filelog.linkrev(p))
1602 1613 n = filelog.node(j)
1603 1614 revs.append((linkrev, parentlinkrevs,
1604 1615 follow and filelog.renamed(n)))
1605 1616
1606 1617 return reversed(revs)
1607 1618 def iterfiles():
1608 1619 pctx = repo['.']
1609 1620 for filename in match.files():
1610 1621 if follow:
1611 1622 if filename not in pctx:
1612 1623 raise util.Abort(_('cannot follow file not in parent '
1613 1624 'revision: "%s"') % filename)
1614 1625 yield filename, pctx[filename].filenode()
1615 1626 else:
1616 1627 yield filename, None
1617 1628 for filename_node in copies:
1618 1629 yield filename_node
1619 1630
1620 1631 for file_, node in iterfiles():
1621 1632 filelog = repo.file(file_)
1622 1633 if not len(filelog):
1623 1634 if node is None:
1624 1635 # A zero count may be a directory or deleted file, so
1625 1636 # try to find matching entries on the slow path.
1626 1637 if follow:
1627 1638 raise util.Abort(
1628 1639 _('cannot follow nonexistent file: "%s"') % file_)
1629 1640 raise FileWalkError("Cannot walk via filelog")
1630 1641 else:
1631 1642 continue
1632 1643
1633 1644 if node is None:
1634 1645 last = len(filelog) - 1
1635 1646 else:
1636 1647 last = filelog.rev(node)
1637 1648
1638 1649 # keep track of all ancestors of the file
1639 1650 ancestors = set([filelog.linkrev(last)])
1640 1651
1641 1652 # iterate from latest to oldest revision
1642 1653 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1643 1654 if not follow:
1644 1655 if rev > maxrev:
1645 1656 continue
1646 1657 else:
1647 1658 # Note that last might not be the first interesting
1648 1659 # rev to us:
1649 1660 # if the file has been changed after maxrev, we'll
1650 1661 # have linkrev(last) > maxrev, and we still need
1651 1662 # to explore the file graph
1652 1663 if rev not in ancestors:
1653 1664 continue
1654 1665 # XXX insert 1327 fix here
1655 1666 if flparentlinkrevs:
1656 1667 ancestors.update(flparentlinkrevs)
1657 1668
1658 1669 fncache.setdefault(rev, []).append(file_)
1659 1670 wanted.add(rev)
1660 1671 if copied:
1661 1672 copies.append(copied)
1662 1673
1663 1674 return wanted
1664 1675
1665 1676 class _followfilter(object):
1666 1677 def __init__(self, repo, onlyfirst=False):
1667 1678 self.repo = repo
1668 1679 self.startrev = nullrev
1669 1680 self.roots = set()
1670 1681 self.onlyfirst = onlyfirst
1671 1682
1672 1683 def match(self, rev):
1673 1684 def realparents(rev):
1674 1685 if self.onlyfirst:
1675 1686 return self.repo.changelog.parentrevs(rev)[0:1]
1676 1687 else:
1677 1688 return filter(lambda x: x != nullrev,
1678 1689 self.repo.changelog.parentrevs(rev))
1679 1690
1680 1691 if self.startrev == nullrev:
1681 1692 self.startrev = rev
1682 1693 return True
1683 1694
1684 1695 if rev > self.startrev:
1685 1696 # forward: all descendants
1686 1697 if not self.roots:
1687 1698 self.roots.add(self.startrev)
1688 1699 for parent in realparents(rev):
1689 1700 if parent in self.roots:
1690 1701 self.roots.add(rev)
1691 1702 return True
1692 1703 else:
1693 1704 # backwards: all parents
1694 1705 if not self.roots:
1695 1706 self.roots.update(realparents(self.startrev))
1696 1707 if rev in self.roots:
1697 1708 self.roots.remove(rev)
1698 1709 self.roots.update(realparents(rev))
1699 1710 return True
1700 1711
1701 1712 return False
1702 1713
1703 1714 def walkchangerevs(repo, match, opts, prepare):
1704 1715 '''Iterate over files and the revs in which they changed.
1705 1716
1706 1717 Callers most commonly need to iterate backwards over the history
1707 1718 in which they are interested. Doing so has awful (quadratic-looking)
1708 1719 performance, so we use iterators in a "windowed" way.
1709 1720
1710 1721 We walk a window of revisions in the desired order. Within the
1711 1722 window, we first walk forwards to gather data, then in the desired
1712 1723 order (usually backwards) to display it.
1713 1724
1714 1725 This function returns an iterator yielding contexts. Before
1715 1726 yielding each context, the iterator will first call the prepare
1716 1727 function on each context in the window in forward order.'''
1717 1728
1718 1729 follow = opts.get('follow') or opts.get('follow_first')
1719 1730 revs = _logrevs(repo, opts)
1720 1731 if not revs:
1721 1732 return []
1722 1733 wanted = set()
1723 1734 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1724 1735 fncache = {}
1725 1736 change = repo.changectx
1726 1737
1727 1738 # First step is to fill wanted, the set of revisions that we want to yield.
1728 1739 # When it does not induce extra cost, we also fill fncache for revisions in
1729 1740 # wanted: a cache of filenames that were changed (ctx.files()) and that
1730 1741 # match the file filtering conditions.
1731 1742
1732 1743 if match.always():
1733 1744 # No files, no patterns. Display all revs.
1734 1745 wanted = revs
1735 1746
1736 1747 if not slowpath and match.files():
1737 1748 # We only have to read through the filelog to find wanted revisions
1738 1749
1739 1750 try:
1740 1751 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1741 1752 except FileWalkError:
1742 1753 slowpath = True
1743 1754
1744 1755 # We decided to fall back to the slowpath because at least one
1745 1756 # of the paths was not a file. Check to see if at least one of them
1746 1757 # existed in history, otherwise simply return
1747 1758 for path in match.files():
1748 1759 if path == '.' or path in repo.store:
1749 1760 break
1750 1761 else:
1751 1762 return []
1752 1763
1753 1764 if slowpath:
1754 1765 # We have to read the changelog to match filenames against
1755 1766 # changed files
1756 1767
1757 1768 if follow:
1758 1769 raise util.Abort(_('can only follow copies/renames for explicit '
1759 1770 'filenames'))
1760 1771
1761 1772 # The slow path checks files modified in every changeset.
1762 1773 # This is really slow on large repos, so compute the set lazily.
1763 1774 class lazywantedset(object):
1764 1775 def __init__(self):
1765 1776 self.set = set()
1766 1777 self.revs = set(revs)
1767 1778
1768 1779 # No need to worry about locality here because it will be accessed
1769 1780 # in the same order as the increasing window below.
1770 1781 def __contains__(self, value):
1771 1782 if value in self.set:
1772 1783 return True
1773 1784 elif not value in self.revs:
1774 1785 return False
1775 1786 else:
1776 1787 self.revs.discard(value)
1777 1788 ctx = change(value)
1778 1789 matches = filter(match, ctx.files())
1779 1790 if matches:
1780 1791 fncache[value] = matches
1781 1792 self.set.add(value)
1782 1793 return True
1783 1794 return False
1784 1795
1785 1796 def discard(self, value):
1786 1797 self.revs.discard(value)
1787 1798 self.set.discard(value)
1788 1799
1789 1800 wanted = lazywantedset()
1790 1801
1791 1802 # it might be worthwhile to do this in the iterator if the rev range
1792 1803 # is descending and the prune args are all within that range
1793 1804 for rev in opts.get('prune', ()):
1794 1805 rev = repo[rev].rev()
1795 1806 ff = _followfilter(repo)
1796 1807 stop = min(revs[0], revs[-1])
1797 1808 for x in xrange(rev, stop - 1, -1):
1798 1809 if ff.match(x):
1799 1810 wanted = wanted - [x]
1800 1811
1801 1812 # Now that wanted is correctly initialized, we can iterate over the
1802 1813 # revision range, yielding only revisions in wanted.
1803 1814 def iterate():
1804 1815 if follow and not match.files():
1805 1816 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1806 1817 def want(rev):
1807 1818 return ff.match(rev) and rev in wanted
1808 1819 else:
1809 1820 def want(rev):
1810 1821 return rev in wanted
1811 1822
1812 1823 it = iter(revs)
1813 1824 stopiteration = False
1814 1825 for windowsize in increasingwindows():
1815 1826 nrevs = []
1816 1827 for i in xrange(windowsize):
1817 1828 try:
1818 1829 rev = it.next()
1819 1830 if want(rev):
1820 1831 nrevs.append(rev)
1821 1832 except (StopIteration):
1822 1833 stopiteration = True
1823 1834 break
1824 1835 for rev in sorted(nrevs):
1825 1836 fns = fncache.get(rev)
1826 1837 ctx = change(rev)
1827 1838 if not fns:
1828 1839 def fns_generator():
1829 1840 for f in ctx.files():
1830 1841 if match(f):
1831 1842 yield f
1832 1843 fns = fns_generator()
1833 1844 prepare(ctx, fns)
1834 1845 for rev in nrevs:
1835 1846 yield change(rev)
1836 1847
1837 1848 if stopiteration:
1838 1849 break
1839 1850
1840 1851 return iterate()
1841 1852
1842 1853 def _makefollowlogfilematcher(repo, files, followfirst):
1843 1854 # When displaying a revision with --patch --follow FILE, we have
1844 1855 # to know which file of the revision must be diffed. With
1845 1856 # --follow, we want the names of the ancestors of FILE in the
1846 1857 # revision, stored in "fcache". "fcache" is populated by
1847 1858 # reproducing the graph traversal already done by --follow revset
1848 1859 # and relating linkrevs to file names (which is not "correct" but
1849 1860 # good enough).
1850 1861 fcache = {}
1851 1862 fcacheready = [False]
1852 1863 pctx = repo['.']
1853 1864
1854 1865 def populate():
1855 1866 for fn in files:
1856 1867 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1857 1868 for c in i:
1858 1869 fcache.setdefault(c.linkrev(), set()).add(c.path())
1859 1870
1860 1871 def filematcher(rev):
1861 1872 if not fcacheready[0]:
1862 1873 # Lazy initialization
1863 1874 fcacheready[0] = True
1864 1875 populate()
1865 1876 return scmutil.matchfiles(repo, fcache.get(rev, []))
1866 1877
1867 1878 return filematcher
1868 1879
1869 1880 def _makenofollowlogfilematcher(repo, pats, opts):
1870 1881 '''hook for extensions to override the filematcher for non-follow cases'''
1871 1882 return None
1872 1883
1873 1884 def _makelogrevset(repo, pats, opts, revs):
1874 1885 """Return (expr, filematcher) where expr is a revset string built
1875 1886 from log options and file patterns or None. If --stat or --patch
1876 1887 are not passed filematcher is None. Otherwise it is a callable
1877 1888 taking a revision number and returning a match objects filtering
1878 1889 the files to be detailed when displaying the revision.
1879 1890 """
1880 1891 opt2revset = {
1881 1892 'no_merges': ('not merge()', None),
1882 1893 'only_merges': ('merge()', None),
1883 1894 '_ancestors': ('ancestors(%(val)s)', None),
1884 1895 '_fancestors': ('_firstancestors(%(val)s)', None),
1885 1896 '_descendants': ('descendants(%(val)s)', None),
1886 1897 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1887 1898 '_matchfiles': ('_matchfiles(%(val)s)', None),
1888 1899 'date': ('date(%(val)r)', None),
1889 1900 'branch': ('branch(%(val)r)', ' or '),
1890 1901 '_patslog': ('filelog(%(val)r)', ' or '),
1891 1902 '_patsfollow': ('follow(%(val)r)', ' or '),
1892 1903 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1893 1904 'keyword': ('keyword(%(val)r)', ' or '),
1894 1905 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1895 1906 'user': ('user(%(val)r)', ' or '),
1896 1907 }
1897 1908
1898 1909 opts = dict(opts)
1899 1910 # follow or not follow?
1900 1911 follow = opts.get('follow') or opts.get('follow_first')
1901 1912 if opts.get('follow_first'):
1902 1913 followfirst = 1
1903 1914 else:
1904 1915 followfirst = 0
1905 1916 # --follow with FILE behaviour depends on revs...
1906 1917 it = iter(revs)
1907 1918 startrev = it.next()
1908 1919 try:
1909 1920 followdescendants = startrev < it.next()
1910 1921 except (StopIteration):
1911 1922 followdescendants = False
1912 1923
1913 1924 # branch and only_branch are really aliases and must be handled at
1914 1925 # the same time
1915 1926 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1916 1927 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1917 1928 # pats/include/exclude are passed to match.match() directly in
1918 1929 # _matchfiles() revset but walkchangerevs() builds its matcher with
1919 1930 # scmutil.match(). The difference is input pats are globbed on
1920 1931 # platforms without shell expansion (windows).
1921 1932 wctx = repo[None]
1922 1933 match, pats = scmutil.matchandpats(wctx, pats, opts)
1923 1934 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1924 1935 if not slowpath:
1925 1936 for f in match.files():
1926 1937 if follow and f not in wctx:
1927 1938 # If the file exists, it may be a directory, so let it
1928 1939 # take the slow path.
1929 1940 if os.path.exists(repo.wjoin(f)):
1930 1941 slowpath = True
1931 1942 continue
1932 1943 else:
1933 1944 raise util.Abort(_('cannot follow file not in parent '
1934 1945 'revision: "%s"') % f)
1935 1946 filelog = repo.file(f)
1936 1947 if not filelog:
1937 1948 # A zero count may be a directory or deleted file, so
1938 1949 # try to find matching entries on the slow path.
1939 1950 if follow:
1940 1951 raise util.Abort(
1941 1952 _('cannot follow nonexistent file: "%s"') % f)
1942 1953 slowpath = True
1943 1954
1944 1955 # We decided to fall back to the slowpath because at least one
1945 1956 # of the paths was not a file. Check to see if at least one of them
1946 1957 # existed in history - in that case, we'll continue down the
1947 1958 # slowpath; otherwise, we can turn off the slowpath
1948 1959 if slowpath:
1949 1960 for path in match.files():
1950 1961 if path == '.' or path in repo.store:
1951 1962 break
1952 1963 else:
1953 1964 slowpath = False
1954 1965
1955 1966 fpats = ('_patsfollow', '_patsfollowfirst')
1956 1967 fnopats = (('_ancestors', '_fancestors'),
1957 1968 ('_descendants', '_fdescendants'))
1958 1969 if slowpath:
1959 1970 # See walkchangerevs() slow path.
1960 1971 #
1961 1972 # pats/include/exclude cannot be represented as separate
1962 1973 # revset expressions as their filtering logic applies at file
1963 1974 # level. For instance "-I a -X a" matches a revision touching
1964 1975 # "a" and "b" while "file(a) and not file(b)" does
1965 1976 # not. Besides, filesets are evaluated against the working
1966 1977 # directory.
1967 1978 matchargs = ['r:', 'd:relpath']
1968 1979 for p in pats:
1969 1980 matchargs.append('p:' + p)
1970 1981 for p in opts.get('include', []):
1971 1982 matchargs.append('i:' + p)
1972 1983 for p in opts.get('exclude', []):
1973 1984 matchargs.append('x:' + p)
1974 1985 matchargs = ','.join(('%r' % p) for p in matchargs)
1975 1986 opts['_matchfiles'] = matchargs
1976 1987 if follow:
1977 1988 opts[fnopats[0][followfirst]] = '.'
1978 1989 else:
1979 1990 if follow:
1980 1991 if pats:
1981 1992 # follow() revset interprets its file argument as a
1982 1993 # manifest entry, so use match.files(), not pats.
1983 1994 opts[fpats[followfirst]] = list(match.files())
1984 1995 else:
1985 1996 op = fnopats[followdescendants][followfirst]
1986 1997 opts[op] = 'rev(%d)' % startrev
1987 1998 else:
1988 1999 opts['_patslog'] = list(pats)
1989 2000
1990 2001 filematcher = None
1991 2002 if opts.get('patch') or opts.get('stat'):
1992 2003 # When following files, track renames via a special matcher.
1993 2004 # If we're forced to take the slowpath it means we're following
1994 2005 # at least one pattern/directory, so don't bother with rename tracking.
1995 2006 if follow and not match.always() and not slowpath:
1996 2007 # _makefollowlogfilematcher expects its files argument to be
1997 2008 # relative to the repo root, so use match.files(), not pats.
1998 2009 filematcher = _makefollowlogfilematcher(repo, match.files(),
1999 2010 followfirst)
2000 2011 else:
2001 2012 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2002 2013 if filematcher is None:
2003 2014 filematcher = lambda rev: match
2004 2015
2005 2016 expr = []
2006 2017 for op, val in sorted(opts.iteritems()):
2007 2018 if not val:
2008 2019 continue
2009 2020 if op not in opt2revset:
2010 2021 continue
2011 2022 revop, andor = opt2revset[op]
2012 2023 if '%(val)' not in revop:
2013 2024 expr.append(revop)
2014 2025 else:
2015 2026 if not isinstance(val, list):
2016 2027 e = revop % {'val': val}
2017 2028 else:
2018 2029 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2019 2030 expr.append(e)
2020 2031
2021 2032 if expr:
2022 2033 expr = '(' + ' and '.join(expr) + ')'
2023 2034 else:
2024 2035 expr = None
2025 2036 return expr, filematcher
2026 2037
2027 2038 def _logrevs(repo, opts):
2028 2039 # Default --rev value depends on --follow but --follow behaviour
2029 2040 # depends on revisions resolved from --rev...
2030 2041 follow = opts.get('follow') or opts.get('follow_first')
2031 2042 if opts.get('rev'):
2032 2043 revs = scmutil.revrange(repo, opts['rev'])
2033 2044 elif follow and repo.dirstate.p1() == nullid:
2034 2045 revs = revset.baseset()
2035 2046 elif follow:
2036 2047 revs = repo.revs('reverse(:.)')
2037 2048 else:
2038 2049 revs = revset.spanset(repo)
2039 2050 revs.reverse()
2040 2051 return revs
2041 2052
2042 2053 def getgraphlogrevs(repo, pats, opts):
2043 2054 """Return (revs, expr, filematcher) where revs is an iterable of
2044 2055 revision numbers, expr is a revset string built from log options
2045 2056 and file patterns or None, and used to filter 'revs'. If --stat or
2046 2057 --patch are not passed filematcher is None. Otherwise it is a
2047 2058 callable taking a revision number and returning a match objects
2048 2059 filtering the files to be detailed when displaying the revision.
2049 2060 """
2050 2061 limit = loglimit(opts)
2051 2062 revs = _logrevs(repo, opts)
2052 2063 if not revs:
2053 2064 return revset.baseset(), None, None
2054 2065 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2055 2066 if opts.get('rev'):
2056 2067 # User-specified revs might be unsorted, but don't sort before
2057 2068 # _makelogrevset because it might depend on the order of revs
2058 2069 revs.sort(reverse=True)
2059 2070 if expr:
2060 2071 # Revset matchers often operate faster on revisions in changelog
2061 2072 # order, because most filters deal with the changelog.
2062 2073 revs.reverse()
2063 2074 matcher = revset.match(repo.ui, expr)
2064 2075 # Revset matches can reorder revisions. "A or B" typically returns
2065 2076 # returns the revision matching A then the revision matching B. Sort
2066 2077 # again to fix that.
2067 2078 revs = matcher(repo, revs)
2068 2079 revs.sort(reverse=True)
2069 2080 if limit is not None:
2070 2081 limitedrevs = []
2071 2082 for idx, rev in enumerate(revs):
2072 2083 if idx >= limit:
2073 2084 break
2074 2085 limitedrevs.append(rev)
2075 2086 revs = revset.baseset(limitedrevs)
2076 2087
2077 2088 return revs, expr, filematcher
2078 2089
2079 2090 def getlogrevs(repo, pats, opts):
2080 2091 """Return (revs, expr, filematcher) where revs is an iterable of
2081 2092 revision numbers, expr is a revset string built from log options
2082 2093 and file patterns or None, and used to filter 'revs'. If --stat or
2083 2094 --patch are not passed filematcher is None. Otherwise it is a
2084 2095 callable taking a revision number and returning a match objects
2085 2096 filtering the files to be detailed when displaying the revision.
2086 2097 """
2087 2098 limit = loglimit(opts)
2088 2099 revs = _logrevs(repo, opts)
2089 2100 if not revs:
2090 2101 return revset.baseset([]), None, None
2091 2102 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2092 2103 if expr:
2093 2104 # Revset matchers often operate faster on revisions in changelog
2094 2105 # order, because most filters deal with the changelog.
2095 2106 if not opts.get('rev'):
2096 2107 revs.reverse()
2097 2108 matcher = revset.match(repo.ui, expr)
2098 2109 # Revset matches can reorder revisions. "A or B" typically returns
2099 2110 # returns the revision matching A then the revision matching B. Sort
2100 2111 # again to fix that.
2101 2112 revs = matcher(repo, revs)
2102 2113 if not opts.get('rev'):
2103 2114 revs.sort(reverse=True)
2104 2115 if limit is not None:
2105 2116 count = 0
2106 2117 limitedrevs = []
2107 2118 it = iter(revs)
2108 2119 while count < limit:
2109 2120 try:
2110 2121 limitedrevs.append(it.next())
2111 2122 except (StopIteration):
2112 2123 break
2113 2124 count += 1
2114 2125 revs = revset.baseset(limitedrevs)
2115 2126
2116 2127 return revs, expr, filematcher
2117 2128
2118 2129 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
2119 2130 filematcher=None):
2120 2131 seen, state = [], graphmod.asciistate()
2121 2132 for rev, type, ctx, parents in dag:
2122 2133 char = 'o'
2123 2134 if ctx.node() in showparents:
2124 2135 char = '@'
2125 2136 elif ctx.obsolete():
2126 2137 char = 'x'
2127 2138 elif ctx.closesbranch():
2128 2139 char = '_'
2129 2140 copies = None
2130 2141 if getrenamed and ctx.rev():
2131 2142 copies = []
2132 2143 for fn in ctx.files():
2133 2144 rename = getrenamed(fn, ctx.rev())
2134 2145 if rename:
2135 2146 copies.append((fn, rename[0]))
2136 2147 revmatchfn = None
2137 2148 if filematcher is not None:
2138 2149 revmatchfn = filematcher(ctx.rev())
2139 2150 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2140 2151 lines = displayer.hunk.pop(rev).split('\n')
2141 2152 if not lines[-1]:
2142 2153 del lines[-1]
2143 2154 displayer.flush(rev)
2144 2155 edges = edgefn(type, char, lines, seen, rev, parents)
2145 2156 for type, char, lines, coldata in edges:
2146 2157 graphmod.ascii(ui, state, type, char, lines, coldata)
2147 2158 displayer.close()
2148 2159
2149 2160 def graphlog(ui, repo, *pats, **opts):
2150 2161 # Parameters are identical to log command ones
2151 2162 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2152 2163 revdag = graphmod.dagwalker(repo, revs)
2153 2164
2154 2165 getrenamed = None
2155 2166 if opts.get('copies'):
2156 2167 endrev = None
2157 2168 if opts.get('rev'):
2158 2169 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2159 2170 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2160 2171 displayer = show_changeset(ui, repo, opts, buffered=True)
2161 2172 showparents = [ctx.node() for ctx in repo[None].parents()]
2162 2173 displaygraph(ui, revdag, displayer, showparents,
2163 2174 graphmod.asciiedges, getrenamed, filematcher)
2164 2175
2165 2176 def checkunsupportedgraphflags(pats, opts):
2166 2177 for op in ["newest_first"]:
2167 2178 if op in opts and opts[op]:
2168 2179 raise util.Abort(_("-G/--graph option is incompatible with --%s")
2169 2180 % op.replace("_", "-"))
2170 2181
2171 2182 def graphrevs(repo, nodes, opts):
2172 2183 limit = loglimit(opts)
2173 2184 nodes.reverse()
2174 2185 if limit is not None:
2175 2186 nodes = nodes[:limit]
2176 2187 return graphmod.nodes(repo, nodes)
2177 2188
2178 2189 def add(ui, repo, match, prefix, explicitonly, **opts):
2179 2190 join = lambda f: os.path.join(prefix, f)
2180 2191 bad = []
2181 2192 oldbad = match.bad
2182 2193 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
2183 2194 names = []
2184 2195 wctx = repo[None]
2185 2196 cca = None
2186 2197 abort, warn = scmutil.checkportabilityalert(ui)
2187 2198 if abort or warn:
2188 2199 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2189 2200 for f in wctx.walk(match):
2190 2201 exact = match.exact(f)
2191 2202 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2192 2203 if cca:
2193 2204 cca(f)
2194 2205 names.append(f)
2195 2206 if ui.verbose or not exact:
2196 2207 ui.status(_('adding %s\n') % match.rel(f))
2197 2208
2198 2209 for subpath in sorted(wctx.substate):
2199 2210 sub = wctx.sub(subpath)
2200 2211 try:
2201 2212 submatch = matchmod.narrowmatcher(subpath, match)
2202 2213 if opts.get('subrepos'):
2203 2214 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2204 2215 else:
2205 2216 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2206 2217 except error.LookupError:
2207 2218 ui.status(_("skipping missing subrepository: %s\n")
2208 2219 % join(subpath))
2209 2220
2210 2221 if not opts.get('dry_run'):
2211 2222 rejected = wctx.add(names, prefix)
2212 2223 bad.extend(f for f in rejected if f in match.files())
2213 2224 return bad
2214 2225
2215 2226 def forget(ui, repo, match, prefix, explicitonly):
2216 2227 join = lambda f: os.path.join(prefix, f)
2217 2228 bad = []
2218 2229 oldbad = match.bad
2219 2230 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
2220 2231 wctx = repo[None]
2221 2232 forgot = []
2222 2233 s = repo.status(match=match, clean=True)
2223 2234 forget = sorted(s[0] + s[1] + s[3] + s[6])
2224 2235 if explicitonly:
2225 2236 forget = [f for f in forget if match.exact(f)]
2226 2237
2227 2238 for subpath in sorted(wctx.substate):
2228 2239 sub = wctx.sub(subpath)
2229 2240 try:
2230 2241 submatch = matchmod.narrowmatcher(subpath, match)
2231 2242 subbad, subforgot = sub.forget(submatch, prefix)
2232 2243 bad.extend([subpath + '/' + f for f in subbad])
2233 2244 forgot.extend([subpath + '/' + f for f in subforgot])
2234 2245 except error.LookupError:
2235 2246 ui.status(_("skipping missing subrepository: %s\n")
2236 2247 % join(subpath))
2237 2248
2238 2249 if not explicitonly:
2239 2250 for f in match.files():
2240 2251 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2241 2252 if f not in forgot:
2242 2253 if repo.wvfs.exists(f):
2243 2254 # Don't complain if the exact case match wasn't given.
2244 2255 # But don't do this until after checking 'forgot', so
2245 2256 # that subrepo files aren't normalized, and this op is
2246 2257 # purely from data cached by the status walk above.
2247 2258 if repo.dirstate.normalize(f) in repo.dirstate:
2248 2259 continue
2249 2260 ui.warn(_('not removing %s: '
2250 2261 'file is already untracked\n')
2251 2262 % match.rel(f))
2252 2263 bad.append(f)
2253 2264
2254 2265 for f in forget:
2255 2266 if ui.verbose or not match.exact(f):
2256 2267 ui.status(_('removing %s\n') % match.rel(f))
2257 2268
2258 2269 rejected = wctx.forget(forget, prefix)
2259 2270 bad.extend(f for f in rejected if f in match.files())
2260 2271 forgot.extend(f for f in forget if f not in rejected)
2261 2272 return bad, forgot
2262 2273
2263 2274 def files(ui, ctx, m, fm, fmt, subrepos):
2264 2275 rev = ctx.rev()
2265 2276 ret = 1
2266 2277 ds = ctx.repo().dirstate
2267 2278
2268 2279 for f in ctx.matches(m):
2269 2280 if rev is None and ds[f] == 'r':
2270 2281 continue
2271 2282 fm.startitem()
2272 2283 if ui.verbose:
2273 2284 fc = ctx[f]
2274 2285 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2275 2286 fm.data(abspath=f)
2276 2287 fm.write('path', fmt, m.rel(f))
2277 2288 ret = 0
2278 2289
2279 2290 if subrepos:
2280 2291 for subpath in sorted(ctx.substate):
2281 2292 sub = ctx.sub(subpath)
2282 2293 try:
2283 2294 submatch = matchmod.narrowmatcher(subpath, m)
2284 2295 if sub.printfiles(ui, submatch, fm, fmt) == 0:
2285 2296 ret = 0
2286 2297 except error.LookupError:
2287 2298 ui.status(_("skipping missing subrepository: %s\n")
2288 2299 % m.abs(subpath))
2289 2300
2290 2301 return ret
2291 2302
2292 2303 def remove(ui, repo, m, prefix, after, force, subrepos):
2293 2304 join = lambda f: os.path.join(prefix, f)
2294 2305 ret = 0
2295 2306 s = repo.status(match=m, clean=True)
2296 2307 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2297 2308
2298 2309 wctx = repo[None]
2299 2310
2300 2311 for subpath in sorted(wctx.substate):
2301 2312 def matchessubrepo(matcher, subpath):
2302 2313 if matcher.exact(subpath):
2303 2314 return True
2304 2315 for f in matcher.files():
2305 2316 if f.startswith(subpath):
2306 2317 return True
2307 2318 return False
2308 2319
2309 2320 if subrepos or matchessubrepo(m, subpath):
2310 2321 sub = wctx.sub(subpath)
2311 2322 try:
2312 2323 submatch = matchmod.narrowmatcher(subpath, m)
2313 2324 if sub.removefiles(submatch, prefix, after, force, subrepos):
2314 2325 ret = 1
2315 2326 except error.LookupError:
2316 2327 ui.status(_("skipping missing subrepository: %s\n")
2317 2328 % join(subpath))
2318 2329
2319 2330 # warn about failure to delete explicit files/dirs
2320 2331 deleteddirs = util.dirs(deleted)
2321 2332 for f in m.files():
2322 2333 def insubrepo():
2323 2334 for subpath in wctx.substate:
2324 2335 if f.startswith(subpath):
2325 2336 return True
2326 2337 return False
2327 2338
2328 2339 isdir = f in deleteddirs or f in wctx.dirs()
2329 2340 if f in repo.dirstate or isdir or f == '.' or insubrepo():
2330 2341 continue
2331 2342
2332 2343 if repo.wvfs.exists(f):
2333 2344 if repo.wvfs.isdir(f):
2334 2345 ui.warn(_('not removing %s: no tracked files\n')
2335 2346 % m.rel(f))
2336 2347 else:
2337 2348 ui.warn(_('not removing %s: file is untracked\n')
2338 2349 % m.rel(f))
2339 2350 # missing files will generate a warning elsewhere
2340 2351 ret = 1
2341 2352
2342 2353 if force:
2343 2354 list = modified + deleted + clean + added
2344 2355 elif after:
2345 2356 list = deleted
2346 2357 for f in modified + added + clean:
2347 2358 ui.warn(_('not removing %s: file still exists\n') % m.rel(f))
2348 2359 ret = 1
2349 2360 else:
2350 2361 list = deleted + clean
2351 2362 for f in modified:
2352 2363 ui.warn(_('not removing %s: file is modified (use -f'
2353 2364 ' to force removal)\n') % m.rel(f))
2354 2365 ret = 1
2355 2366 for f in added:
2356 2367 ui.warn(_('not removing %s: file has been marked for add'
2357 2368 ' (use forget to undo)\n') % m.rel(f))
2358 2369 ret = 1
2359 2370
2360 2371 for f in sorted(list):
2361 2372 if ui.verbose or not m.exact(f):
2362 2373 ui.status(_('removing %s\n') % m.rel(f))
2363 2374
2364 2375 wlock = repo.wlock()
2365 2376 try:
2366 2377 if not after:
2367 2378 for f in list:
2368 2379 if f in added:
2369 2380 continue # we never unlink added files on remove
2370 2381 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2371 2382 repo[None].forget(list)
2372 2383 finally:
2373 2384 wlock.release()
2374 2385
2375 2386 return ret
2376 2387
2377 2388 def cat(ui, repo, ctx, matcher, prefix, **opts):
2378 2389 err = 1
2379 2390
2380 2391 def write(path):
2381 2392 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2382 2393 pathname=os.path.join(prefix, path))
2383 2394 data = ctx[path].data()
2384 2395 if opts.get('decode'):
2385 2396 data = repo.wwritedata(path, data)
2386 2397 fp.write(data)
2387 2398 fp.close()
2388 2399
2389 2400 # Automation often uses hg cat on single files, so special case it
2390 2401 # for performance to avoid the cost of parsing the manifest.
2391 2402 if len(matcher.files()) == 1 and not matcher.anypats():
2392 2403 file = matcher.files()[0]
2393 2404 mf = repo.manifest
2394 2405 mfnode = ctx.manifestnode()
2395 2406 if mfnode and mf.find(mfnode, file)[0]:
2396 2407 write(file)
2397 2408 return 0
2398 2409
2399 2410 # Don't warn about "missing" files that are really in subrepos
2400 2411 bad = matcher.bad
2401 2412
2402 2413 def badfn(path, msg):
2403 2414 for subpath in ctx.substate:
2404 2415 if path.startswith(subpath):
2405 2416 return
2406 2417 bad(path, msg)
2407 2418
2408 2419 matcher.bad = badfn
2409 2420
2410 2421 for abs in ctx.walk(matcher):
2411 2422 write(abs)
2412 2423 err = 0
2413 2424
2414 2425 matcher.bad = bad
2415 2426
2416 2427 for subpath in sorted(ctx.substate):
2417 2428 sub = ctx.sub(subpath)
2418 2429 try:
2419 2430 submatch = matchmod.narrowmatcher(subpath, matcher)
2420 2431
2421 2432 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2422 2433 **opts):
2423 2434 err = 0
2424 2435 except error.RepoLookupError:
2425 2436 ui.status(_("skipping missing subrepository: %s\n")
2426 2437 % os.path.join(prefix, subpath))
2427 2438
2428 2439 return err
2429 2440
2430 2441 def commit(ui, repo, commitfunc, pats, opts):
2431 2442 '''commit the specified files or all outstanding changes'''
2432 2443 date = opts.get('date')
2433 2444 if date:
2434 2445 opts['date'] = util.parsedate(date)
2435 2446 message = logmessage(ui, opts)
2436 2447 matcher = scmutil.match(repo[None], pats, opts)
2437 2448
2438 2449 # extract addremove carefully -- this function can be called from a command
2439 2450 # that doesn't support addremove
2440 2451 if opts.get('addremove'):
2441 2452 if scmutil.addremove(repo, matcher, "", opts) != 0:
2442 2453 raise util.Abort(
2443 2454 _("failed to mark all new/missing files as added/removed"))
2444 2455
2445 2456 return commitfunc(ui, repo, message, matcher, opts)
2446 2457
2447 2458 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2448 2459 # amend will reuse the existing user if not specified, but the obsolete
2449 2460 # marker creation requires that the current user's name is specified.
2450 2461 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2451 2462 ui.username() # raise exception if username not set
2452 2463
2453 2464 ui.note(_('amending changeset %s\n') % old)
2454 2465 base = old.p1()
2455 2466
2456 2467 wlock = lock = newid = None
2457 2468 try:
2458 2469 wlock = repo.wlock()
2459 2470 lock = repo.lock()
2460 2471 tr = repo.transaction('amend')
2461 2472 try:
2462 2473 # See if we got a message from -m or -l, if not, open the editor
2463 2474 # with the message of the changeset to amend
2464 2475 message = logmessage(ui, opts)
2465 2476 # ensure logfile does not conflict with later enforcement of the
2466 2477 # message. potential logfile content has been processed by
2467 2478 # `logmessage` anyway.
2468 2479 opts.pop('logfile')
2469 2480 # First, do a regular commit to record all changes in the working
2470 2481 # directory (if there are any)
2471 2482 ui.callhooks = False
2472 2483 currentbookmark = repo._bookmarkcurrent
2473 2484 try:
2474 2485 repo._bookmarkcurrent = None
2475 2486 opts['message'] = 'temporary amend commit for %s' % old
2476 2487 node = commit(ui, repo, commitfunc, pats, opts)
2477 2488 finally:
2478 2489 repo._bookmarkcurrent = currentbookmark
2479 2490 ui.callhooks = True
2480 2491 ctx = repo[node]
2481 2492
2482 2493 # Participating changesets:
2483 2494 #
2484 2495 # node/ctx o - new (intermediate) commit that contains changes
2485 2496 # | from working dir to go into amending commit
2486 2497 # | (or a workingctx if there were no changes)
2487 2498 # |
2488 2499 # old o - changeset to amend
2489 2500 # |
2490 2501 # base o - parent of amending changeset
2491 2502
2492 2503 # Update extra dict from amended commit (e.g. to preserve graft
2493 2504 # source)
2494 2505 extra.update(old.extra())
2495 2506
2496 2507 # Also update it from the intermediate commit or from the wctx
2497 2508 extra.update(ctx.extra())
2498 2509
2499 2510 if len(old.parents()) > 1:
2500 2511 # ctx.files() isn't reliable for merges, so fall back to the
2501 2512 # slower repo.status() method
2502 2513 files = set([fn for st in repo.status(base, old)[:3]
2503 2514 for fn in st])
2504 2515 else:
2505 2516 files = set(old.files())
2506 2517
2507 2518 # Second, we use either the commit we just did, or if there were no
2508 2519 # changes the parent of the working directory as the version of the
2509 2520 # files in the final amend commit
2510 2521 if node:
2511 2522 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2512 2523
2513 2524 user = ctx.user()
2514 2525 date = ctx.date()
2515 2526 # Recompute copies (avoid recording a -> b -> a)
2516 2527 copied = copies.pathcopies(base, ctx)
2517 2528 if old.p2:
2518 2529 copied.update(copies.pathcopies(old.p2(), ctx))
2519 2530
2520 2531 # Prune files which were reverted by the updates: if old
2521 2532 # introduced file X and our intermediate commit, node,
2522 2533 # renamed that file, then those two files are the same and
2523 2534 # we can discard X from our list of files. Likewise if X
2524 2535 # was deleted, it's no longer relevant
2525 2536 files.update(ctx.files())
2526 2537
2527 2538 def samefile(f):
2528 2539 if f in ctx.manifest():
2529 2540 a = ctx.filectx(f)
2530 2541 if f in base.manifest():
2531 2542 b = base.filectx(f)
2532 2543 return (not a.cmp(b)
2533 2544 and a.flags() == b.flags())
2534 2545 else:
2535 2546 return False
2536 2547 else:
2537 2548 return f not in base.manifest()
2538 2549 files = [f for f in files if not samefile(f)]
2539 2550
2540 2551 def filectxfn(repo, ctx_, path):
2541 2552 try:
2542 2553 fctx = ctx[path]
2543 2554 flags = fctx.flags()
2544 2555 mctx = context.memfilectx(repo,
2545 2556 fctx.path(), fctx.data(),
2546 2557 islink='l' in flags,
2547 2558 isexec='x' in flags,
2548 2559 copied=copied.get(path))
2549 2560 return mctx
2550 2561 except KeyError:
2551 2562 return None
2552 2563 else:
2553 2564 ui.note(_('copying changeset %s to %s\n') % (old, base))
2554 2565
2555 2566 # Use version of files as in the old cset
2556 2567 def filectxfn(repo, ctx_, path):
2557 2568 try:
2558 2569 return old.filectx(path)
2559 2570 except KeyError:
2560 2571 return None
2561 2572
2562 2573 user = opts.get('user') or old.user()
2563 2574 date = opts.get('date') or old.date()
2564 2575 editform = mergeeditform(old, 'commit.amend')
2565 2576 editor = getcommiteditor(editform=editform, **opts)
2566 2577 if not message:
2567 2578 editor = getcommiteditor(edit=True, editform=editform)
2568 2579 message = old.description()
2569 2580
2570 2581 pureextra = extra.copy()
2571 2582 extra['amend_source'] = old.hex()
2572 2583
2573 2584 new = context.memctx(repo,
2574 2585 parents=[base.node(), old.p2().node()],
2575 2586 text=message,
2576 2587 files=files,
2577 2588 filectxfn=filectxfn,
2578 2589 user=user,
2579 2590 date=date,
2580 2591 extra=extra,
2581 2592 editor=editor)
2582 2593
2583 2594 newdesc = changelog.stripdesc(new.description())
2584 2595 if ((not node)
2585 2596 and newdesc == old.description()
2586 2597 and user == old.user()
2587 2598 and date == old.date()
2588 2599 and pureextra == old.extra()):
2589 2600 # nothing changed. continuing here would create a new node
2590 2601 # anyway because of the amend_source noise.
2591 2602 #
2592 2603 # This not what we expect from amend.
2593 2604 return old.node()
2594 2605
2595 2606 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2596 2607 try:
2597 2608 if opts.get('secret'):
2598 2609 commitphase = 'secret'
2599 2610 else:
2600 2611 commitphase = old.phase()
2601 2612 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2602 2613 newid = repo.commitctx(new)
2603 2614 finally:
2604 2615 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2605 2616 if newid != old.node():
2606 2617 # Reroute the working copy parent to the new changeset
2607 2618 repo.setparents(newid, nullid)
2608 2619
2609 2620 # Move bookmarks from old parent to amend commit
2610 2621 bms = repo.nodebookmarks(old.node())
2611 2622 if bms:
2612 2623 marks = repo._bookmarks
2613 2624 for bm in bms:
2614 2625 marks[bm] = newid
2615 2626 marks.write()
2616 2627 #commit the whole amend process
2617 2628 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2618 2629 if createmarkers and newid != old.node():
2619 2630 # mark the new changeset as successor of the rewritten one
2620 2631 new = repo[newid]
2621 2632 obs = [(old, (new,))]
2622 2633 if node:
2623 2634 obs.append((ctx, ()))
2624 2635
2625 2636 obsolete.createmarkers(repo, obs)
2626 2637 tr.close()
2627 2638 finally:
2628 2639 tr.release()
2629 2640 if not createmarkers and newid != old.node():
2630 2641 # Strip the intermediate commit (if there was one) and the amended
2631 2642 # commit
2632 2643 if node:
2633 2644 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2634 2645 ui.note(_('stripping amended changeset %s\n') % old)
2635 2646 repair.strip(ui, repo, old.node(), topic='amend-backup')
2636 2647 finally:
2637 2648 if newid is None:
2638 2649 repo.dirstate.invalidate()
2639 2650 lockmod.release(lock, wlock)
2640 2651 return newid
2641 2652
2642 2653 def commiteditor(repo, ctx, subs, editform=''):
2643 2654 if ctx.description():
2644 2655 return ctx.description()
2645 2656 return commitforceeditor(repo, ctx, subs, editform=editform)
2646 2657
2647 2658 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2648 2659 editform=''):
2649 2660 if not extramsg:
2650 2661 extramsg = _("Leave message empty to abort commit.")
2651 2662
2652 2663 forms = [e for e in editform.split('.') if e]
2653 2664 forms.insert(0, 'changeset')
2654 2665 while forms:
2655 2666 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2656 2667 if tmpl:
2657 2668 committext = buildcommittemplate(repo, ctx, subs, extramsg, tmpl)
2658 2669 break
2659 2670 forms.pop()
2660 2671 else:
2661 2672 committext = buildcommittext(repo, ctx, subs, extramsg)
2662 2673
2663 2674 # run editor in the repository root
2664 2675 olddir = os.getcwd()
2665 2676 os.chdir(repo.root)
2666 2677 text = repo.ui.edit(committext, ctx.user(), ctx.extra(), editform=editform)
2667 2678 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2668 2679 os.chdir(olddir)
2669 2680
2670 2681 if finishdesc:
2671 2682 text = finishdesc(text)
2672 2683 if not text.strip():
2673 2684 raise util.Abort(_("empty commit message"))
2674 2685
2675 2686 return text
2676 2687
2677 2688 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2678 2689 ui = repo.ui
2679 2690 tmpl, mapfile = gettemplate(ui, tmpl, None)
2680 2691
2681 2692 try:
2682 2693 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2683 2694 except SyntaxError, inst:
2684 2695 raise util.Abort(inst.args[0])
2685 2696
2686 2697 for k, v in repo.ui.configitems('committemplate'):
2687 2698 if k != 'changeset':
2688 2699 t.t.cache[k] = v
2689 2700
2690 2701 if not extramsg:
2691 2702 extramsg = '' # ensure that extramsg is string
2692 2703
2693 2704 ui.pushbuffer()
2694 2705 t.show(ctx, extramsg=extramsg)
2695 2706 return ui.popbuffer()
2696 2707
2697 2708 def buildcommittext(repo, ctx, subs, extramsg):
2698 2709 edittext = []
2699 2710 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2700 2711 if ctx.description():
2701 2712 edittext.append(ctx.description())
2702 2713 edittext.append("")
2703 2714 edittext.append("") # Empty line between message and comments.
2704 2715 edittext.append(_("HG: Enter commit message."
2705 2716 " Lines beginning with 'HG:' are removed."))
2706 2717 edittext.append("HG: %s" % extramsg)
2707 2718 edittext.append("HG: --")
2708 2719 edittext.append(_("HG: user: %s") % ctx.user())
2709 2720 if ctx.p2():
2710 2721 edittext.append(_("HG: branch merge"))
2711 2722 if ctx.branch():
2712 2723 edittext.append(_("HG: branch '%s'") % ctx.branch())
2713 2724 if bookmarks.iscurrent(repo):
2714 2725 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
2715 2726 edittext.extend([_("HG: subrepo %s") % s for s in subs])
2716 2727 edittext.extend([_("HG: added %s") % f for f in added])
2717 2728 edittext.extend([_("HG: changed %s") % f for f in modified])
2718 2729 edittext.extend([_("HG: removed %s") % f for f in removed])
2719 2730 if not added and not modified and not removed:
2720 2731 edittext.append(_("HG: no files changed"))
2721 2732 edittext.append("")
2722 2733
2723 2734 return "\n".join(edittext)
2724 2735
2725 2736 def commitstatus(repo, node, branch, bheads=None, opts={}):
2726 2737 ctx = repo[node]
2727 2738 parents = ctx.parents()
2728 2739
2729 2740 if (not opts.get('amend') and bheads and node not in bheads and not
2730 2741 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2731 2742 repo.ui.status(_('created new head\n'))
2732 2743 # The message is not printed for initial roots. For the other
2733 2744 # changesets, it is printed in the following situations:
2734 2745 #
2735 2746 # Par column: for the 2 parents with ...
2736 2747 # N: null or no parent
2737 2748 # B: parent is on another named branch
2738 2749 # C: parent is a regular non head changeset
2739 2750 # H: parent was a branch head of the current branch
2740 2751 # Msg column: whether we print "created new head" message
2741 2752 # In the following, it is assumed that there already exists some
2742 2753 # initial branch heads of the current branch, otherwise nothing is
2743 2754 # printed anyway.
2744 2755 #
2745 2756 # Par Msg Comment
2746 2757 # N N y additional topo root
2747 2758 #
2748 2759 # B N y additional branch root
2749 2760 # C N y additional topo head
2750 2761 # H N n usual case
2751 2762 #
2752 2763 # B B y weird additional branch root
2753 2764 # C B y branch merge
2754 2765 # H B n merge with named branch
2755 2766 #
2756 2767 # C C y additional head from merge
2757 2768 # C H n merge with a head
2758 2769 #
2759 2770 # H H n head merge: head count decreases
2760 2771
2761 2772 if not opts.get('close_branch'):
2762 2773 for r in parents:
2763 2774 if r.closesbranch() and r.branch() == branch:
2764 2775 repo.ui.status(_('reopening closed branch head %d\n') % r)
2765 2776
2766 2777 if repo.ui.debugflag:
2767 2778 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2768 2779 elif repo.ui.verbose:
2769 2780 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2770 2781
2771 2782 def revert(ui, repo, ctx, parents, *pats, **opts):
2772 2783 parent, p2 = parents
2773 2784 node = ctx.node()
2774 2785
2775 2786 mf = ctx.manifest()
2776 2787 if node == p2:
2777 2788 parent = p2
2778 2789 if node == parent:
2779 2790 pmf = mf
2780 2791 else:
2781 2792 pmf = None
2782 2793
2783 2794 # need all matching names in dirstate and manifest of target rev,
2784 2795 # so have to walk both. do not print errors if files exist in one
2785 2796 # but not other. in both cases, filesets should be evaluated against
2786 2797 # workingctx to get consistent result (issue4497). this means 'set:**'
2787 2798 # cannot be used to select missing files from target rev.
2788 2799
2789 2800 # `names` is a mapping for all elements in working copy and target revision
2790 2801 # The mapping is in the form:
2791 2802 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2792 2803 names = {}
2793 2804
2794 2805 wlock = repo.wlock()
2795 2806 try:
2796 2807 ## filling of the `names` mapping
2797 2808 # walk dirstate to fill `names`
2798 2809
2799 2810 interactive = opts.get('interactive', False)
2800 2811 wctx = repo[None]
2801 2812 m = scmutil.match(wctx, pats, opts)
2802 2813
2803 2814 # we'll need this later
2804 2815 targetsubs = sorted(s for s in wctx.substate if m(s))
2805 2816
2806 2817 if not m.always():
2807 2818 m.bad = lambda x, y: False
2808 2819 for abs in repo.walk(m):
2809 2820 names[abs] = m.rel(abs), m.exact(abs)
2810 2821
2811 2822 # walk target manifest to fill `names`
2812 2823
2813 2824 def badfn(path, msg):
2814 2825 if path in names:
2815 2826 return
2816 2827 if path in ctx.substate:
2817 2828 return
2818 2829 path_ = path + '/'
2819 2830 for f in names:
2820 2831 if f.startswith(path_):
2821 2832 return
2822 2833 ui.warn("%s: %s\n" % (m.rel(path), msg))
2823 2834
2824 2835 m.bad = badfn
2825 2836 for abs in ctx.walk(m):
2826 2837 if abs not in names:
2827 2838 names[abs] = m.rel(abs), m.exact(abs)
2828 2839
2829 2840 # Find status of all file in `names`.
2830 2841 m = scmutil.matchfiles(repo, names)
2831 2842
2832 2843 changes = repo.status(node1=node, match=m,
2833 2844 unknown=True, ignored=True, clean=True)
2834 2845 else:
2835 2846 changes = repo.status(node1=node, match=m)
2836 2847 for kind in changes:
2837 2848 for abs in kind:
2838 2849 names[abs] = m.rel(abs), m.exact(abs)
2839 2850
2840 2851 m = scmutil.matchfiles(repo, names)
2841 2852
2842 2853 modified = set(changes.modified)
2843 2854 added = set(changes.added)
2844 2855 removed = set(changes.removed)
2845 2856 _deleted = set(changes.deleted)
2846 2857 unknown = set(changes.unknown)
2847 2858 unknown.update(changes.ignored)
2848 2859 clean = set(changes.clean)
2849 2860 modadded = set()
2850 2861
2851 2862 # split between files known in target manifest and the others
2852 2863 smf = set(mf)
2853 2864
2854 2865 # determine the exact nature of the deleted changesets
2855 2866 deladded = _deleted - smf
2856 2867 deleted = _deleted - deladded
2857 2868
2858 2869 # We need to account for the state of the file in the dirstate,
2859 2870 # even when we revert against something else than parent. This will
2860 2871 # slightly alter the behavior of revert (doing back up or not, delete
2861 2872 # or just forget etc).
2862 2873 if parent == node:
2863 2874 dsmodified = modified
2864 2875 dsadded = added
2865 2876 dsremoved = removed
2866 2877 # store all local modifications, useful later for rename detection
2867 2878 localchanges = dsmodified | dsadded
2868 2879 modified, added, removed = set(), set(), set()
2869 2880 else:
2870 2881 changes = repo.status(node1=parent, match=m)
2871 2882 dsmodified = set(changes.modified)
2872 2883 dsadded = set(changes.added)
2873 2884 dsremoved = set(changes.removed)
2874 2885 # store all local modifications, useful later for rename detection
2875 2886 localchanges = dsmodified | dsadded
2876 2887
2877 2888 # only take into account for removes between wc and target
2878 2889 clean |= dsremoved - removed
2879 2890 dsremoved &= removed
2880 2891 # distinct between dirstate remove and other
2881 2892 removed -= dsremoved
2882 2893
2883 2894 modadded = added & dsmodified
2884 2895 added -= modadded
2885 2896
2886 2897 # tell newly modified apart.
2887 2898 dsmodified &= modified
2888 2899 dsmodified |= modified & dsadded # dirstate added may needs backup
2889 2900 modified -= dsmodified
2890 2901
2891 2902 # We need to wait for some post-processing to update this set
2892 2903 # before making the distinction. The dirstate will be used for
2893 2904 # that purpose.
2894 2905 dsadded = added
2895 2906
2896 2907 # in case of merge, files that are actually added can be reported as
2897 2908 # modified, we need to post process the result
2898 2909 if p2 != nullid:
2899 2910 if pmf is None:
2900 2911 # only need parent manifest in the merge case,
2901 2912 # so do not read by default
2902 2913 pmf = repo[parent].manifest()
2903 2914 mergeadd = dsmodified - set(pmf)
2904 2915 dsadded |= mergeadd
2905 2916 dsmodified -= mergeadd
2906 2917
2907 2918 # if f is a rename, update `names` to also revert the source
2908 2919 cwd = repo.getcwd()
2909 2920 for f in localchanges:
2910 2921 src = repo.dirstate.copied(f)
2911 2922 # XXX should we check for rename down to target node?
2912 2923 if src and src not in names and repo.dirstate[src] == 'r':
2913 2924 dsremoved.add(src)
2914 2925 names[src] = (repo.pathto(src, cwd), True)
2915 2926
2916 2927 # distinguish between file to forget and the other
2917 2928 added = set()
2918 2929 for abs in dsadded:
2919 2930 if repo.dirstate[abs] != 'a':
2920 2931 added.add(abs)
2921 2932 dsadded -= added
2922 2933
2923 2934 for abs in deladded:
2924 2935 if repo.dirstate[abs] == 'a':
2925 2936 dsadded.add(abs)
2926 2937 deladded -= dsadded
2927 2938
2928 2939 # For files marked as removed, we check if an unknown file is present at
2929 2940 # the same path. If a such file exists it may need to be backed up.
2930 2941 # Making the distinction at this stage helps have simpler backup
2931 2942 # logic.
2932 2943 removunk = set()
2933 2944 for abs in removed:
2934 2945 target = repo.wjoin(abs)
2935 2946 if os.path.lexists(target):
2936 2947 removunk.add(abs)
2937 2948 removed -= removunk
2938 2949
2939 2950 dsremovunk = set()
2940 2951 for abs in dsremoved:
2941 2952 target = repo.wjoin(abs)
2942 2953 if os.path.lexists(target):
2943 2954 dsremovunk.add(abs)
2944 2955 dsremoved -= dsremovunk
2945 2956
2946 2957 # action to be actually performed by revert
2947 2958 # (<list of file>, message>) tuple
2948 2959 actions = {'revert': ([], _('reverting %s\n')),
2949 2960 'add': ([], _('adding %s\n')),
2950 2961 'remove': ([], _('removing %s\n')),
2951 2962 'drop': ([], _('removing %s\n')),
2952 2963 'forget': ([], _('forgetting %s\n')),
2953 2964 'undelete': ([], _('undeleting %s\n')),
2954 2965 'noop': (None, _('no changes needed to %s\n')),
2955 2966 'unknown': (None, _('file not managed: %s\n')),
2956 2967 }
2957 2968
2958 2969 # "constant" that convey the backup strategy.
2959 2970 # All set to `discard` if `no-backup` is set do avoid checking
2960 2971 # no_backup lower in the code.
2961 2972 # These values are ordered for comparison purposes
2962 2973 backup = 2 # unconditionally do backup
2963 2974 check = 1 # check if the existing file differs from target
2964 2975 discard = 0 # never do backup
2965 2976 if opts.get('no_backup'):
2966 2977 backup = check = discard
2967 2978
2968 2979 backupanddel = actions['remove']
2969 2980 if not opts.get('no_backup'):
2970 2981 backupanddel = actions['drop']
2971 2982
2972 2983 disptable = (
2973 2984 # dispatch table:
2974 2985 # file state
2975 2986 # action
2976 2987 # make backup
2977 2988
2978 2989 ## Sets that results that will change file on disk
2979 2990 # Modified compared to target, no local change
2980 2991 (modified, actions['revert'], discard),
2981 2992 # Modified compared to target, but local file is deleted
2982 2993 (deleted, actions['revert'], discard),
2983 2994 # Modified compared to target, local change
2984 2995 (dsmodified, actions['revert'], backup),
2985 2996 # Added since target
2986 2997 (added, actions['remove'], discard),
2987 2998 # Added in working directory
2988 2999 (dsadded, actions['forget'], discard),
2989 3000 # Added since target, have local modification
2990 3001 (modadded, backupanddel, backup),
2991 3002 # Added since target but file is missing in working directory
2992 3003 (deladded, actions['drop'], discard),
2993 3004 # Removed since target, before working copy parent
2994 3005 (removed, actions['add'], discard),
2995 3006 # Same as `removed` but an unknown file exists at the same path
2996 3007 (removunk, actions['add'], check),
2997 3008 # Removed since targe, marked as such in working copy parent
2998 3009 (dsremoved, actions['undelete'], discard),
2999 3010 # Same as `dsremoved` but an unknown file exists at the same path
3000 3011 (dsremovunk, actions['undelete'], check),
3001 3012 ## the following sets does not result in any file changes
3002 3013 # File with no modification
3003 3014 (clean, actions['noop'], discard),
3004 3015 # Existing file, not tracked anywhere
3005 3016 (unknown, actions['unknown'], discard),
3006 3017 )
3007 3018
3008 3019 for abs, (rel, exact) in sorted(names.items()):
3009 3020 # target file to be touch on disk (relative to cwd)
3010 3021 target = repo.wjoin(abs)
3011 3022 # search the entry in the dispatch table.
3012 3023 # if the file is in any of these sets, it was touched in the working
3013 3024 # directory parent and we are sure it needs to be reverted.
3014 3025 for table, (xlist, msg), dobackup in disptable:
3015 3026 if abs not in table:
3016 3027 continue
3017 3028 if xlist is not None:
3018 3029 xlist.append(abs)
3019 3030 if dobackup and (backup <= dobackup
3020 3031 or wctx[abs].cmp(ctx[abs])):
3021 3032 bakname = "%s.orig" % rel
3022 3033 ui.note(_('saving current version of %s as %s\n') %
3023 3034 (rel, bakname))
3024 3035 if not opts.get('dry_run'):
3025 3036 if interactive:
3026 3037 util.copyfile(target, bakname)
3027 3038 else:
3028 3039 util.rename(target, bakname)
3029 3040 if ui.verbose or not exact:
3030 3041 if not isinstance(msg, basestring):
3031 3042 msg = msg(abs)
3032 3043 ui.status(msg % rel)
3033 3044 elif exact:
3034 3045 ui.warn(msg % rel)
3035 3046 break
3036 3047
3037 3048 if not opts.get('dry_run'):
3038 3049 needdata = ('revert', 'add', 'undelete')
3039 3050 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3040 3051 _performrevert(repo, parents, ctx, actions, interactive)
3041 3052
3042 3053 if targetsubs:
3043 3054 # Revert the subrepos on the revert list
3044 3055 for sub in targetsubs:
3045 3056 try:
3046 3057 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3047 3058 except KeyError:
3048 3059 raise util.Abort("subrepository '%s' does not exist in %s!"
3049 3060 % (sub, short(ctx.node())))
3050 3061 finally:
3051 3062 wlock.release()
3052 3063
3053 3064 def _revertprefetch(repo, ctx, *files):
3054 3065 """Let extension changing the storage layer prefetch content"""
3055 3066 pass
3056 3067
3057 3068 def _performrevert(repo, parents, ctx, actions, interactive=False):
3058 3069 """function that actually perform all the actions computed for revert
3059 3070
3060 3071 This is an independent function to let extension to plug in and react to
3061 3072 the imminent revert.
3062 3073
3063 3074 Make sure you have the working directory locked when calling this function.
3064 3075 """
3065 3076 parent, p2 = parents
3066 3077 node = ctx.node()
3067 3078 def checkout(f):
3068 3079 fc = ctx[f]
3069 3080 return repo.wwrite(f, fc.data(), fc.flags())
3070 3081
3071 3082 audit_path = pathutil.pathauditor(repo.root)
3072 3083 for f in actions['forget'][0]:
3073 3084 repo.dirstate.drop(f)
3074 3085 for f in actions['remove'][0]:
3075 3086 audit_path(f)
3076 3087 util.unlinkpath(repo.wjoin(f))
3077 3088 repo.dirstate.remove(f)
3078 3089 for f in actions['drop'][0]:
3079 3090 audit_path(f)
3080 3091 repo.dirstate.remove(f)
3081 3092
3082 3093 normal = None
3083 3094 if node == parent:
3084 3095 # We're reverting to our parent. If possible, we'd like status
3085 3096 # to report the file as clean. We have to use normallookup for
3086 3097 # merges to avoid losing information about merged/dirty files.
3087 3098 if p2 != nullid:
3088 3099 normal = repo.dirstate.normallookup
3089 3100 else:
3090 3101 normal = repo.dirstate.normal
3091 3102
3092 3103 if interactive:
3093 3104 # Prompt the user for changes to revert
3094 3105 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3095 3106 m = scmutil.match(ctx, torevert, {})
3096 3107 diff = patch.diff(repo, None, ctx.node(), m)
3097 3108 originalchunks = patch.parsepatch(diff)
3098 3109 try:
3099 3110 chunks = recordfilter(repo.ui, originalchunks)
3100 3111 except patch.PatchError, err:
3101 3112 raise util.Abort(_('error parsing patch: %s') % err)
3102 3113
3103 3114 # Apply changes
3104 3115 fp = cStringIO.StringIO()
3105 3116 for c in chunks:
3106 3117 c.write(fp)
3107 3118 dopatch = fp.tell()
3108 3119 fp.seek(0)
3109 3120 if dopatch:
3110 3121 try:
3111 3122 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3112 3123 except patch.PatchError, err:
3113 3124 raise util.Abort(str(err))
3114 3125 del fp
3115 3126 else:
3116 3127 for f in actions['revert'][0]:
3117 3128 wsize = checkout(f)
3118 3129 if normal:
3119 3130 normal(f)
3120 3131 elif wsize == repo.dirstate._map[f][2]:
3121 3132 # changes may be overlooked without normallookup,
3122 3133 # if size isn't changed at reverting
3123 3134 repo.dirstate.normallookup(f)
3124 3135
3125 3136 for f in actions['add'][0]:
3126 3137 checkout(f)
3127 3138 repo.dirstate.add(f)
3128 3139
3129 3140 normal = repo.dirstate.normallookup
3130 3141 if node == parent and p2 == nullid:
3131 3142 normal = repo.dirstate.normal
3132 3143 for f in actions['undelete'][0]:
3133 3144 checkout(f)
3134 3145 normal(f)
3135 3146
3136 3147 copied = copies.pathcopies(repo[parent], ctx)
3137 3148
3138 3149 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3139 3150 if f in copied:
3140 3151 repo.dirstate.copy(copied[f], f)
3141 3152
3142 3153 def command(table):
3143 3154 """Returns a function object to be used as a decorator for making commands.
3144 3155
3145 3156 This function receives a command table as its argument. The table should
3146 3157 be a dict.
3147 3158
3148 3159 The returned function can be used as a decorator for adding commands
3149 3160 to that command table. This function accepts multiple arguments to define
3150 3161 a command.
3151 3162
3152 3163 The first argument is the command name.
3153 3164
3154 3165 The options argument is an iterable of tuples defining command arguments.
3155 3166 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3156 3167
3157 3168 The synopsis argument defines a short, one line summary of how to use the
3158 3169 command. This shows up in the help output.
3159 3170
3160 3171 The norepo argument defines whether the command does not require a
3161 3172 local repository. Most commands operate against a repository, thus the
3162 3173 default is False.
3163 3174
3164 3175 The optionalrepo argument defines whether the command optionally requires
3165 3176 a local repository.
3166 3177
3167 3178 The inferrepo argument defines whether to try to find a repository from the
3168 3179 command line arguments. If True, arguments will be examined for potential
3169 3180 repository locations. See ``findrepo()``. If a repository is found, it
3170 3181 will be used.
3171 3182 """
3172 3183 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3173 3184 inferrepo=False):
3174 3185 def decorator(func):
3175 3186 if synopsis:
3176 3187 table[name] = func, list(options), synopsis
3177 3188 else:
3178 3189 table[name] = func, list(options)
3179 3190
3180 3191 if norepo:
3181 3192 # Avoid import cycle.
3182 3193 import commands
3183 3194 commands.norepo += ' %s' % ' '.join(parsealiases(name))
3184 3195
3185 3196 if optionalrepo:
3186 3197 import commands
3187 3198 commands.optionalrepo += ' %s' % ' '.join(parsealiases(name))
3188 3199
3189 3200 if inferrepo:
3190 3201 import commands
3191 3202 commands.inferrepo += ' %s' % ' '.join(parsealiases(name))
3192 3203
3193 3204 return func
3194 3205 return decorator
3195 3206
3196 3207 return cmd
3197 3208
3198 3209 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3199 3210 # commands.outgoing. "missing" is "missing" of the result of
3200 3211 # "findcommonoutgoing()"
3201 3212 outgoinghooks = util.hooks()
3202 3213
3203 3214 # a list of (ui, repo) functions called by commands.summary
3204 3215 summaryhooks = util.hooks()
3205 3216
3206 3217 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3207 3218 #
3208 3219 # functions should return tuple of booleans below, if 'changes' is None:
3209 3220 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3210 3221 #
3211 3222 # otherwise, 'changes' is a tuple of tuples below:
3212 3223 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3213 3224 # - (desturl, destbranch, destpeer, outgoing)
3214 3225 summaryremotehooks = util.hooks()
3215 3226
3216 3227 # A list of state files kept by multistep operations like graft.
3217 3228 # Since graft cannot be aborted, it is considered 'clearable' by update.
3218 3229 # note: bisect is intentionally excluded
3219 3230 # (state file, clearable, allowcommit, error, hint)
3220 3231 unfinishedstates = [
3221 3232 ('graftstate', True, False, _('graft in progress'),
3222 3233 _("use 'hg graft --continue' or 'hg update' to abort")),
3223 3234 ('updatestate', True, False, _('last update was interrupted'),
3224 3235 _("use 'hg update' to get a consistent checkout"))
3225 3236 ]
3226 3237
3227 3238 def checkunfinished(repo, commit=False):
3228 3239 '''Look for an unfinished multistep operation, like graft, and abort
3229 3240 if found. It's probably good to check this right before
3230 3241 bailifchanged().
3231 3242 '''
3232 3243 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3233 3244 if commit and allowcommit:
3234 3245 continue
3235 3246 if repo.vfs.exists(f):
3236 3247 raise util.Abort(msg, hint=hint)
3237 3248
3238 3249 def clearunfinished(repo):
3239 3250 '''Check for unfinished operations (as above), and clear the ones
3240 3251 that are clearable.
3241 3252 '''
3242 3253 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3243 3254 if not clearable and repo.vfs.exists(f):
3244 3255 raise util.Abort(msg, hint=hint)
3245 3256 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3246 3257 if clearable and repo.vfs.exists(f):
3247 3258 util.unlink(repo.join(f))
@@ -1,2464 +1,2478 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, os, errno, re, posixpath, copy
10 10 import tempfile, zlib, shutil
11 11 # On python2.4 you have to import these by name or they fail to
12 12 # load. This was not a problem on Python 2.7.
13 13 import email.Generator
14 14 import email.Parser
15 15
16 16 from i18n import _
17 17 from node import hex, short
18 18 import cStringIO
19 19 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
20 20 import pathutil
21 21
22 22 gitre = re.compile('diff --git a/(.*) b/(.*)')
23 23 tabsplitter = re.compile(r'(\t+|[^\t]+)')
24 24
25 25 class PatchError(Exception):
26 26 pass
27 27
28 28
29 29 # public functions
30 30
31 31 def split(stream):
32 32 '''return an iterator of individual patches from a stream'''
33 33 def isheader(line, inheader):
34 34 if inheader and line[0] in (' ', '\t'):
35 35 # continuation
36 36 return True
37 37 if line[0] in (' ', '-', '+'):
38 38 # diff line - don't check for header pattern in there
39 39 return False
40 40 l = line.split(': ', 1)
41 41 return len(l) == 2 and ' ' not in l[0]
42 42
43 43 def chunk(lines):
44 44 return cStringIO.StringIO(''.join(lines))
45 45
46 46 def hgsplit(stream, cur):
47 47 inheader = True
48 48
49 49 for line in stream:
50 50 if not line.strip():
51 51 inheader = False
52 52 if not inheader and line.startswith('# HG changeset patch'):
53 53 yield chunk(cur)
54 54 cur = []
55 55 inheader = True
56 56
57 57 cur.append(line)
58 58
59 59 if cur:
60 60 yield chunk(cur)
61 61
62 62 def mboxsplit(stream, cur):
63 63 for line in stream:
64 64 if line.startswith('From '):
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67 cur = []
68 68
69 69 cur.append(line)
70 70
71 71 if cur:
72 72 for c in split(chunk(cur[1:])):
73 73 yield c
74 74
75 75 def mimesplit(stream, cur):
76 76 def msgfp(m):
77 77 fp = cStringIO.StringIO()
78 78 g = email.Generator.Generator(fp, mangle_from_=False)
79 79 g.flatten(m)
80 80 fp.seek(0)
81 81 return fp
82 82
83 83 for line in stream:
84 84 cur.append(line)
85 85 c = chunk(cur)
86 86
87 87 m = email.Parser.Parser().parse(c)
88 88 if not m.is_multipart():
89 89 yield msgfp(m)
90 90 else:
91 91 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
92 92 for part in m.walk():
93 93 ct = part.get_content_type()
94 94 if ct not in ok_types:
95 95 continue
96 96 yield msgfp(part)
97 97
98 98 def headersplit(stream, cur):
99 99 inheader = False
100 100
101 101 for line in stream:
102 102 if not inheader and isheader(line, inheader):
103 103 yield chunk(cur)
104 104 cur = []
105 105 inheader = True
106 106 if inheader and not isheader(line, inheader):
107 107 inheader = False
108 108
109 109 cur.append(line)
110 110
111 111 if cur:
112 112 yield chunk(cur)
113 113
114 114 def remainder(cur):
115 115 yield chunk(cur)
116 116
117 117 class fiter(object):
118 118 def __init__(self, fp):
119 119 self.fp = fp
120 120
121 121 def __iter__(self):
122 122 return self
123 123
124 124 def next(self):
125 125 l = self.fp.readline()
126 126 if not l:
127 127 raise StopIteration
128 128 return l
129 129
130 130 inheader = False
131 131 cur = []
132 132
133 133 mimeheaders = ['content-type']
134 134
135 135 if not util.safehasattr(stream, 'next'):
136 136 # http responses, for example, have readline but not next
137 137 stream = fiter(stream)
138 138
139 139 for line in stream:
140 140 cur.append(line)
141 141 if line.startswith('# HG changeset patch'):
142 142 return hgsplit(stream, cur)
143 143 elif line.startswith('From '):
144 144 return mboxsplit(stream, cur)
145 145 elif isheader(line, inheader):
146 146 inheader = True
147 147 if line.split(':', 1)[0].lower() in mimeheaders:
148 148 # let email parser handle this
149 149 return mimesplit(stream, cur)
150 150 elif line.startswith('--- ') and inheader:
151 151 # No evil headers seen by diff start, split by hand
152 152 return headersplit(stream, cur)
153 153 # Not enough info, keep reading
154 154
155 155 # if we are here, we have a very plain patch
156 156 return remainder(cur)
157 157
158 158 def extract(ui, fileobj):
159 159 '''extract patch from data read from fileobj.
160 160
161 161 patch can be a normal patch or contained in an email message.
162 162
163 163 return tuple (filename, message, user, date, branch, node, p1, p2).
164 164 Any item in the returned tuple can be None. If filename is None,
165 165 fileobj did not contain a patch. Caller must unlink filename when done.'''
166 166
167 167 # attempt to detect the start of a patch
168 168 # (this heuristic is borrowed from quilt)
169 169 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
170 170 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
171 171 r'---[ \t].*?^\+\+\+[ \t]|'
172 172 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
173 173
174 174 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
175 175 tmpfp = os.fdopen(fd, 'w')
176 176 try:
177 177 msg = email.Parser.Parser().parse(fileobj)
178 178
179 179 subject = msg['Subject']
180 180 user = msg['From']
181 181 if not subject and not user:
182 182 # Not an email, restore parsed headers if any
183 183 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
184 184
185 185 # should try to parse msg['Date']
186 186 date = None
187 187 nodeid = None
188 188 branch = None
189 189 parents = []
190 190
191 191 if subject:
192 192 if subject.startswith('[PATCH'):
193 193 pend = subject.find(']')
194 194 if pend >= 0:
195 195 subject = subject[pend + 1:].lstrip()
196 196 subject = re.sub(r'\n[ \t]+', ' ', subject)
197 197 ui.debug('Subject: %s\n' % subject)
198 198 if user:
199 199 ui.debug('From: %s\n' % user)
200 200 diffs_seen = 0
201 201 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
202 202 message = ''
203 203 for part in msg.walk():
204 204 content_type = part.get_content_type()
205 205 ui.debug('Content-Type: %s\n' % content_type)
206 206 if content_type not in ok_types:
207 207 continue
208 208 payload = part.get_payload(decode=True)
209 209 m = diffre.search(payload)
210 210 if m:
211 211 hgpatch = False
212 212 hgpatchheader = False
213 213 ignoretext = False
214 214
215 215 ui.debug('found patch at byte %d\n' % m.start(0))
216 216 diffs_seen += 1
217 217 cfp = cStringIO.StringIO()
218 218 for line in payload[:m.start(0)].splitlines():
219 219 if line.startswith('# HG changeset patch') and not hgpatch:
220 220 ui.debug('patch generated by hg export\n')
221 221 hgpatch = True
222 222 hgpatchheader = True
223 223 # drop earlier commit message content
224 224 cfp.seek(0)
225 225 cfp.truncate()
226 226 subject = None
227 227 elif hgpatchheader:
228 228 if line.startswith('# User '):
229 229 user = line[7:]
230 230 ui.debug('From: %s\n' % user)
231 231 elif line.startswith("# Date "):
232 232 date = line[7:]
233 233 elif line.startswith("# Branch "):
234 234 branch = line[9:]
235 235 elif line.startswith("# Node ID "):
236 236 nodeid = line[10:]
237 237 elif line.startswith("# Parent "):
238 238 parents.append(line[9:].lstrip())
239 239 elif not line.startswith("# "):
240 240 hgpatchheader = False
241 241 elif line == '---':
242 242 ignoretext = True
243 243 if not hgpatchheader and not ignoretext:
244 244 cfp.write(line)
245 245 cfp.write('\n')
246 246 message = cfp.getvalue()
247 247 if tmpfp:
248 248 tmpfp.write(payload)
249 249 if not payload.endswith('\n'):
250 250 tmpfp.write('\n')
251 251 elif not diffs_seen and message and content_type == 'text/plain':
252 252 message += '\n' + payload
253 253 except: # re-raises
254 254 tmpfp.close()
255 255 os.unlink(tmpname)
256 256 raise
257 257
258 258 if subject and not message.startswith(subject):
259 259 message = '%s\n%s' % (subject, message)
260 260 tmpfp.close()
261 261 if not diffs_seen:
262 262 os.unlink(tmpname)
263 263 return None, message, user, date, branch, None, None, None
264 264
265 265 if parents:
266 266 p1 = parents.pop(0)
267 267 else:
268 268 p1 = None
269 269
270 270 if parents:
271 271 p2 = parents.pop(0)
272 272 else:
273 273 p2 = None
274 274
275 275 return tmpname, message, user, date, branch, nodeid, p1, p2
276 276
277 277 class patchmeta(object):
278 278 """Patched file metadata
279 279
280 280 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
281 281 or COPY. 'path' is patched file path. 'oldpath' is set to the
282 282 origin file when 'op' is either COPY or RENAME, None otherwise. If
283 283 file mode is changed, 'mode' is a tuple (islink, isexec) where
284 284 'islink' is True if the file is a symlink and 'isexec' is True if
285 285 the file is executable. Otherwise, 'mode' is None.
286 286 """
287 287 def __init__(self, path):
288 288 self.path = path
289 289 self.oldpath = None
290 290 self.mode = None
291 291 self.op = 'MODIFY'
292 292 self.binary = False
293 293
294 294 def setmode(self, mode):
295 295 islink = mode & 020000
296 296 isexec = mode & 0100
297 297 self.mode = (islink, isexec)
298 298
299 299 def copy(self):
300 300 other = patchmeta(self.path)
301 301 other.oldpath = self.oldpath
302 302 other.mode = self.mode
303 303 other.op = self.op
304 304 other.binary = self.binary
305 305 return other
306 306
307 307 def _ispatchinga(self, afile):
308 308 if afile == '/dev/null':
309 309 return self.op == 'ADD'
310 310 return afile == 'a/' + (self.oldpath or self.path)
311 311
312 312 def _ispatchingb(self, bfile):
313 313 if bfile == '/dev/null':
314 314 return self.op == 'DELETE'
315 315 return bfile == 'b/' + self.path
316 316
317 317 def ispatching(self, afile, bfile):
318 318 return self._ispatchinga(afile) and self._ispatchingb(bfile)
319 319
320 320 def __repr__(self):
321 321 return "<patchmeta %s %r>" % (self.op, self.path)
322 322
323 323 def readgitpatch(lr):
324 324 """extract git-style metadata about patches from <patchname>"""
325 325
326 326 # Filter patch for git information
327 327 gp = None
328 328 gitpatches = []
329 329 for line in lr:
330 330 line = line.rstrip(' \r\n')
331 331 if line.startswith('diff --git a/'):
332 332 m = gitre.match(line)
333 333 if m:
334 334 if gp:
335 335 gitpatches.append(gp)
336 336 dst = m.group(2)
337 337 gp = patchmeta(dst)
338 338 elif gp:
339 339 if line.startswith('--- '):
340 340 gitpatches.append(gp)
341 341 gp = None
342 342 continue
343 343 if line.startswith('rename from '):
344 344 gp.op = 'RENAME'
345 345 gp.oldpath = line[12:]
346 346 elif line.startswith('rename to '):
347 347 gp.path = line[10:]
348 348 elif line.startswith('copy from '):
349 349 gp.op = 'COPY'
350 350 gp.oldpath = line[10:]
351 351 elif line.startswith('copy to '):
352 352 gp.path = line[8:]
353 353 elif line.startswith('deleted file'):
354 354 gp.op = 'DELETE'
355 355 elif line.startswith('new file mode '):
356 356 gp.op = 'ADD'
357 357 gp.setmode(int(line[-6:], 8))
358 358 elif line.startswith('new mode '):
359 359 gp.setmode(int(line[-6:], 8))
360 360 elif line.startswith('GIT binary patch'):
361 361 gp.binary = True
362 362 if gp:
363 363 gitpatches.append(gp)
364 364
365 365 return gitpatches
366 366
367 367 class linereader(object):
368 368 # simple class to allow pushing lines back into the input stream
369 369 def __init__(self, fp):
370 370 self.fp = fp
371 371 self.buf = []
372 372
373 373 def push(self, line):
374 374 if line is not None:
375 375 self.buf.append(line)
376 376
377 377 def readline(self):
378 378 if self.buf:
379 379 l = self.buf[0]
380 380 del self.buf[0]
381 381 return l
382 382 return self.fp.readline()
383 383
384 384 def __iter__(self):
385 385 while True:
386 386 l = self.readline()
387 387 if not l:
388 388 break
389 389 yield l
390 390
391 391 class abstractbackend(object):
392 392 def __init__(self, ui):
393 393 self.ui = ui
394 394
395 395 def getfile(self, fname):
396 396 """Return target file data and flags as a (data, (islink,
397 397 isexec)) tuple. Data is None if file is missing/deleted.
398 398 """
399 399 raise NotImplementedError
400 400
401 401 def setfile(self, fname, data, mode, copysource):
402 402 """Write data to target file fname and set its mode. mode is a
403 403 (islink, isexec) tuple. If data is None, the file content should
404 404 be left unchanged. If the file is modified after being copied,
405 405 copysource is set to the original file name.
406 406 """
407 407 raise NotImplementedError
408 408
409 409 def unlink(self, fname):
410 410 """Unlink target file."""
411 411 raise NotImplementedError
412 412
413 413 def writerej(self, fname, failed, total, lines):
414 414 """Write rejected lines for fname. total is the number of hunks
415 415 which failed to apply and total the total number of hunks for this
416 416 files.
417 417 """
418 418 pass
419 419
420 420 def exists(self, fname):
421 421 raise NotImplementedError
422 422
423 423 class fsbackend(abstractbackend):
424 424 def __init__(self, ui, basedir):
425 425 super(fsbackend, self).__init__(ui)
426 426 self.opener = scmutil.opener(basedir)
427 427
428 428 def _join(self, f):
429 429 return os.path.join(self.opener.base, f)
430 430
431 431 def getfile(self, fname):
432 432 if self.opener.islink(fname):
433 433 return (self.opener.readlink(fname), (True, False))
434 434
435 435 isexec = False
436 436 try:
437 437 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
438 438 except OSError, e:
439 439 if e.errno != errno.ENOENT:
440 440 raise
441 441 try:
442 442 return (self.opener.read(fname), (False, isexec))
443 443 except IOError, e:
444 444 if e.errno != errno.ENOENT:
445 445 raise
446 446 return None, None
447 447
448 448 def setfile(self, fname, data, mode, copysource):
449 449 islink, isexec = mode
450 450 if data is None:
451 451 self.opener.setflags(fname, islink, isexec)
452 452 return
453 453 if islink:
454 454 self.opener.symlink(data, fname)
455 455 else:
456 456 self.opener.write(fname, data)
457 457 if isexec:
458 458 self.opener.setflags(fname, False, True)
459 459
460 460 def unlink(self, fname):
461 461 self.opener.unlinkpath(fname, ignoremissing=True)
462 462
463 463 def writerej(self, fname, failed, total, lines):
464 464 fname = fname + ".rej"
465 465 self.ui.warn(
466 466 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
467 467 (failed, total, fname))
468 468 fp = self.opener(fname, 'w')
469 469 fp.writelines(lines)
470 470 fp.close()
471 471
472 472 def exists(self, fname):
473 473 return self.opener.lexists(fname)
474 474
475 475 class workingbackend(fsbackend):
476 476 def __init__(self, ui, repo, similarity):
477 477 super(workingbackend, self).__init__(ui, repo.root)
478 478 self.repo = repo
479 479 self.similarity = similarity
480 480 self.removed = set()
481 481 self.changed = set()
482 482 self.copied = []
483 483
484 484 def _checkknown(self, fname):
485 485 if self.repo.dirstate[fname] == '?' and self.exists(fname):
486 486 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
487 487
488 488 def setfile(self, fname, data, mode, copysource):
489 489 self._checkknown(fname)
490 490 super(workingbackend, self).setfile(fname, data, mode, copysource)
491 491 if copysource is not None:
492 492 self.copied.append((copysource, fname))
493 493 self.changed.add(fname)
494 494
495 495 def unlink(self, fname):
496 496 self._checkknown(fname)
497 497 super(workingbackend, self).unlink(fname)
498 498 self.removed.add(fname)
499 499 self.changed.add(fname)
500 500
501 501 def close(self):
502 502 wctx = self.repo[None]
503 503 changed = set(self.changed)
504 504 for src, dst in self.copied:
505 505 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
506 506 if self.removed:
507 507 wctx.forget(sorted(self.removed))
508 508 for f in self.removed:
509 509 if f not in self.repo.dirstate:
510 510 # File was deleted and no longer belongs to the
511 511 # dirstate, it was probably marked added then
512 512 # deleted, and should not be considered by
513 513 # marktouched().
514 514 changed.discard(f)
515 515 if changed:
516 516 scmutil.marktouched(self.repo, changed, self.similarity)
517 517 return sorted(self.changed)
518 518
519 519 class filestore(object):
520 520 def __init__(self, maxsize=None):
521 521 self.opener = None
522 522 self.files = {}
523 523 self.created = 0
524 524 self.maxsize = maxsize
525 525 if self.maxsize is None:
526 526 self.maxsize = 4*(2**20)
527 527 self.size = 0
528 528 self.data = {}
529 529
530 530 def setfile(self, fname, data, mode, copied=None):
531 531 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
532 532 self.data[fname] = (data, mode, copied)
533 533 self.size += len(data)
534 534 else:
535 535 if self.opener is None:
536 536 root = tempfile.mkdtemp(prefix='hg-patch-')
537 537 self.opener = scmutil.opener(root)
538 538 # Avoid filename issues with these simple names
539 539 fn = str(self.created)
540 540 self.opener.write(fn, data)
541 541 self.created += 1
542 542 self.files[fname] = (fn, mode, copied)
543 543
544 544 def getfile(self, fname):
545 545 if fname in self.data:
546 546 return self.data[fname]
547 547 if not self.opener or fname not in self.files:
548 548 return None, None, None
549 549 fn, mode, copied = self.files[fname]
550 550 return self.opener.read(fn), mode, copied
551 551
552 552 def close(self):
553 553 if self.opener:
554 554 shutil.rmtree(self.opener.base)
555 555
556 556 class repobackend(abstractbackend):
557 557 def __init__(self, ui, repo, ctx, store):
558 558 super(repobackend, self).__init__(ui)
559 559 self.repo = repo
560 560 self.ctx = ctx
561 561 self.store = store
562 562 self.changed = set()
563 563 self.removed = set()
564 564 self.copied = {}
565 565
566 566 def _checkknown(self, fname):
567 567 if fname not in self.ctx:
568 568 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
569 569
570 570 def getfile(self, fname):
571 571 try:
572 572 fctx = self.ctx[fname]
573 573 except error.LookupError:
574 574 return None, None
575 575 flags = fctx.flags()
576 576 return fctx.data(), ('l' in flags, 'x' in flags)
577 577
578 578 def setfile(self, fname, data, mode, copysource):
579 579 if copysource:
580 580 self._checkknown(copysource)
581 581 if data is None:
582 582 data = self.ctx[fname].data()
583 583 self.store.setfile(fname, data, mode, copysource)
584 584 self.changed.add(fname)
585 585 if copysource:
586 586 self.copied[fname] = copysource
587 587
588 588 def unlink(self, fname):
589 589 self._checkknown(fname)
590 590 self.removed.add(fname)
591 591
592 592 def exists(self, fname):
593 593 return fname in self.ctx
594 594
595 595 def close(self):
596 596 return self.changed | self.removed
597 597
598 598 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
599 599 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
600 600 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
601 601 eolmodes = ['strict', 'crlf', 'lf', 'auto']
602 602
603 603 class patchfile(object):
604 604 def __init__(self, ui, gp, backend, store, eolmode='strict'):
605 605 self.fname = gp.path
606 606 self.eolmode = eolmode
607 607 self.eol = None
608 608 self.backend = backend
609 609 self.ui = ui
610 610 self.lines = []
611 611 self.exists = False
612 612 self.missing = True
613 613 self.mode = gp.mode
614 614 self.copysource = gp.oldpath
615 615 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
616 616 self.remove = gp.op == 'DELETE'
617 617 if self.copysource is None:
618 618 data, mode = backend.getfile(self.fname)
619 619 else:
620 620 data, mode = store.getfile(self.copysource)[:2]
621 621 if data is not None:
622 622 self.exists = self.copysource is None or backend.exists(self.fname)
623 623 self.missing = False
624 624 if data:
625 625 self.lines = mdiff.splitnewlines(data)
626 626 if self.mode is None:
627 627 self.mode = mode
628 628 if self.lines:
629 629 # Normalize line endings
630 630 if self.lines[0].endswith('\r\n'):
631 631 self.eol = '\r\n'
632 632 elif self.lines[0].endswith('\n'):
633 633 self.eol = '\n'
634 634 if eolmode != 'strict':
635 635 nlines = []
636 636 for l in self.lines:
637 637 if l.endswith('\r\n'):
638 638 l = l[:-2] + '\n'
639 639 nlines.append(l)
640 640 self.lines = nlines
641 641 else:
642 642 if self.create:
643 643 self.missing = False
644 644 if self.mode is None:
645 645 self.mode = (False, False)
646 646 if self.missing:
647 647 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
648 648
649 649 self.hash = {}
650 650 self.dirty = 0
651 651 self.offset = 0
652 652 self.skew = 0
653 653 self.rej = []
654 654 self.fileprinted = False
655 655 self.printfile(False)
656 656 self.hunks = 0
657 657
658 658 def writelines(self, fname, lines, mode):
659 659 if self.eolmode == 'auto':
660 660 eol = self.eol
661 661 elif self.eolmode == 'crlf':
662 662 eol = '\r\n'
663 663 else:
664 664 eol = '\n'
665 665
666 666 if self.eolmode != 'strict' and eol and eol != '\n':
667 667 rawlines = []
668 668 for l in lines:
669 669 if l and l[-1] == '\n':
670 670 l = l[:-1] + eol
671 671 rawlines.append(l)
672 672 lines = rawlines
673 673
674 674 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
675 675
676 676 def printfile(self, warn):
677 677 if self.fileprinted:
678 678 return
679 679 if warn or self.ui.verbose:
680 680 self.fileprinted = True
681 681 s = _("patching file %s\n") % self.fname
682 682 if warn:
683 683 self.ui.warn(s)
684 684 else:
685 685 self.ui.note(s)
686 686
687 687
688 688 def findlines(self, l, linenum):
689 689 # looks through the hash and finds candidate lines. The
690 690 # result is a list of line numbers sorted based on distance
691 691 # from linenum
692 692
693 693 cand = self.hash.get(l, [])
694 694 if len(cand) > 1:
695 695 # resort our list of potentials forward then back.
696 696 cand.sort(key=lambda x: abs(x - linenum))
697 697 return cand
698 698
699 699 def write_rej(self):
700 700 # our rejects are a little different from patch(1). This always
701 701 # creates rejects in the same form as the original patch. A file
702 702 # header is inserted so that you can run the reject through patch again
703 703 # without having to type the filename.
704 704 if not self.rej:
705 705 return
706 706 base = os.path.basename(self.fname)
707 707 lines = ["--- %s\n+++ %s\n" % (base, base)]
708 708 for x in self.rej:
709 709 for l in x.hunk:
710 710 lines.append(l)
711 711 if l[-1] != '\n':
712 712 lines.append("\n\ No newline at end of file\n")
713 713 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
714 714
715 715 def apply(self, h):
716 716 if not h.complete():
717 717 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
718 718 (h.number, h.desc, len(h.a), h.lena, len(h.b),
719 719 h.lenb))
720 720
721 721 self.hunks += 1
722 722
723 723 if self.missing:
724 724 self.rej.append(h)
725 725 return -1
726 726
727 727 if self.exists and self.create:
728 728 if self.copysource:
729 729 self.ui.warn(_("cannot create %s: destination already "
730 730 "exists\n") % self.fname)
731 731 else:
732 732 self.ui.warn(_("file %s already exists\n") % self.fname)
733 733 self.rej.append(h)
734 734 return -1
735 735
736 736 if isinstance(h, binhunk):
737 737 if self.remove:
738 738 self.backend.unlink(self.fname)
739 739 else:
740 740 l = h.new(self.lines)
741 741 self.lines[:] = l
742 742 self.offset += len(l)
743 743 self.dirty = True
744 744 return 0
745 745
746 746 horig = h
747 747 if (self.eolmode in ('crlf', 'lf')
748 748 or self.eolmode == 'auto' and self.eol):
749 749 # If new eols are going to be normalized, then normalize
750 750 # hunk data before patching. Otherwise, preserve input
751 751 # line-endings.
752 752 h = h.getnormalized()
753 753
754 754 # fast case first, no offsets, no fuzz
755 755 old, oldstart, new, newstart = h.fuzzit(0, False)
756 756 oldstart += self.offset
757 757 orig_start = oldstart
758 758 # if there's skew we want to emit the "(offset %d lines)" even
759 759 # when the hunk cleanly applies at start + skew, so skip the
760 760 # fast case code
761 761 if (self.skew == 0 and
762 762 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
763 763 if self.remove:
764 764 self.backend.unlink(self.fname)
765 765 else:
766 766 self.lines[oldstart:oldstart + len(old)] = new
767 767 self.offset += len(new) - len(old)
768 768 self.dirty = True
769 769 return 0
770 770
771 771 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
772 772 self.hash = {}
773 773 for x, s in enumerate(self.lines):
774 774 self.hash.setdefault(s, []).append(x)
775 775
776 776 for fuzzlen in xrange(3):
777 777 for toponly in [True, False]:
778 778 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
779 779 oldstart = oldstart + self.offset + self.skew
780 780 oldstart = min(oldstart, len(self.lines))
781 781 if old:
782 782 cand = self.findlines(old[0][1:], oldstart)
783 783 else:
784 784 # Only adding lines with no or fuzzed context, just
785 785 # take the skew in account
786 786 cand = [oldstart]
787 787
788 788 for l in cand:
789 789 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
790 790 self.lines[l : l + len(old)] = new
791 791 self.offset += len(new) - len(old)
792 792 self.skew = l - orig_start
793 793 self.dirty = True
794 794 offset = l - orig_start - fuzzlen
795 795 if fuzzlen:
796 796 msg = _("Hunk #%d succeeded at %d "
797 797 "with fuzz %d "
798 798 "(offset %d lines).\n")
799 799 self.printfile(True)
800 800 self.ui.warn(msg %
801 801 (h.number, l + 1, fuzzlen, offset))
802 802 else:
803 803 msg = _("Hunk #%d succeeded at %d "
804 804 "(offset %d lines).\n")
805 805 self.ui.note(msg % (h.number, l + 1, offset))
806 806 return fuzzlen
807 807 self.printfile(True)
808 808 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
809 809 self.rej.append(horig)
810 810 return -1
811 811
812 812 def close(self):
813 813 if self.dirty:
814 814 self.writelines(self.fname, self.lines, self.mode)
815 815 self.write_rej()
816 816 return len(self.rej)
817 817
818 818 class header(object):
819 819 """patch header
820 820 """
821 821 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
822 822 diff_re = re.compile('diff -r .* (.*)$')
823 allhunks_re = re.compile('(?:index|new file|deleted file) ')
823 allhunks_re = re.compile('(?:index|deleted file) ')
824 824 pretty_re = re.compile('(?:new file|deleted file) ')
825 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
825 special_re = re.compile('(?:index|deleted|copy|rename) ')
826 newfile_re = re.compile('(?:new file)')
826 827
827 828 def __init__(self, header):
828 829 self.header = header
829 830 self.hunks = []
830 831
831 832 def binary(self):
832 833 return util.any(h.startswith('index ') for h in self.header)
833 834
834 835 def pretty(self, fp):
835 836 for h in self.header:
836 837 if h.startswith('index '):
837 838 fp.write(_('this modifies a binary file (all or nothing)\n'))
838 839 break
839 840 if self.pretty_re.match(h):
840 841 fp.write(h)
841 842 if self.binary():
842 843 fp.write(_('this is a binary file\n'))
843 844 break
844 845 if h.startswith('---'):
845 846 fp.write(_('%d hunks, %d lines changed\n') %
846 847 (len(self.hunks),
847 848 sum([max(h.added, h.removed) for h in self.hunks])))
848 849 break
849 850 fp.write(h)
850 851
851 852 def write(self, fp):
852 853 fp.write(''.join(self.header))
853 854
854 855 def allhunks(self):
855 856 return util.any(self.allhunks_re.match(h) for h in self.header)
856 857
857 858 def files(self):
858 859 match = self.diffgit_re.match(self.header[0])
859 860 if match:
860 861 fromfile, tofile = match.groups()
861 862 if fromfile == tofile:
862 863 return [fromfile]
863 864 return [fromfile, tofile]
864 865 else:
865 866 return self.diff_re.match(self.header[0]).groups()
866 867
867 868 def filename(self):
868 869 return self.files()[-1]
869 870
870 871 def __repr__(self):
871 872 return '<header %s>' % (' '.join(map(repr, self.files())))
872 873
874 def isnewfile(self):
875 return util.any(self.newfile_re.match(h) for h in self.header)
876
873 877 def special(self):
874 return util.any(self.special_re.match(h) for h in self.header)
878 # Special files are shown only at the header level and not at the hunk
879 # level for example a file that has been deleted is a special file.
880 # The user cannot change the content of the operation, in the case of
881 # the deleted file he has to take the deletion or not take it, he
882 # cannot take some of it.
883 # Newly added files are special if they are empty, they are not special
884 # if they have some content as we want to be able to change it
885 nocontent = len(self.header) == 2
886 emptynewfile = self.isnewfile() and nocontent
887 return emptynewfile or \
888 util.any(self.special_re.match(h) for h in self.header)
875 889
876 890 class recordhunk(object):
877 891 """patch hunk
878 892
879 893 XXX shouldn't we merge this with the other hunk class?
880 894 """
881 895 maxcontext = 3
882 896
883 897 def __init__(self, header, fromline, toline, proc, before, hunk, after):
884 898 def trimcontext(number, lines):
885 899 delta = len(lines) - self.maxcontext
886 900 if False and delta > 0:
887 901 return number + delta, lines[:self.maxcontext]
888 902 return number, lines
889 903
890 904 self.header = header
891 905 self.fromline, self.before = trimcontext(fromline, before)
892 906 self.toline, self.after = trimcontext(toline, after)
893 907 self.proc = proc
894 908 self.hunk = hunk
895 909 self.added, self.removed = self.countchanges(self.hunk)
896 910
897 911 def __eq__(self, v):
898 912 if not isinstance(v, recordhunk):
899 913 return False
900 914
901 915 return ((v.hunk == self.hunk) and
902 916 (v.proc == self.proc) and
903 917 (self.fromline == v.fromline) and
904 918 (self.header.files() == v.header.files()))
905 919
906 920 def __hash__(self):
907 921 return hash((tuple(self.hunk),
908 922 tuple(self.header.files()),
909 923 self.fromline,
910 924 self.proc))
911 925
912 926 def countchanges(self, hunk):
913 927 """hunk -> (n+,n-)"""
914 928 add = len([h for h in hunk if h[0] == '+'])
915 929 rem = len([h for h in hunk if h[0] == '-'])
916 930 return add, rem
917 931
918 932 def write(self, fp):
919 933 delta = len(self.before) + len(self.after)
920 934 if self.after and self.after[-1] == '\\ No newline at end of file\n':
921 935 delta -= 1
922 936 fromlen = delta + self.removed
923 937 tolen = delta + self.added
924 938 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
925 939 (self.fromline, fromlen, self.toline, tolen,
926 940 self.proc and (' ' + self.proc)))
927 941 fp.write(''.join(self.before + self.hunk + self.after))
928 942
929 943 pretty = write
930 944
931 945 def filename(self):
932 946 return self.header.filename()
933 947
934 948 def __repr__(self):
935 949 return '<hunk %r@%d>' % (self.filename(), self.fromline)
936 950
937 951 def filterpatch(ui, headers):
938 952 """Interactively filter patch chunks into applied-only chunks"""
939 953
940 954 def prompt(skipfile, skipall, query, chunk):
941 955 """prompt query, and process base inputs
942 956
943 957 - y/n for the rest of file
944 958 - y/n for the rest
945 959 - ? (help)
946 960 - q (quit)
947 961
948 962 Return True/False and possibly updated skipfile and skipall.
949 963 """
950 964 newpatches = None
951 965 if skipall is not None:
952 966 return skipall, skipfile, skipall, newpatches
953 967 if skipfile is not None:
954 968 return skipfile, skipfile, skipall, newpatches
955 969 while True:
956 970 resps = _('[Ynesfdaq?]'
957 971 '$$ &Yes, record this change'
958 972 '$$ &No, skip this change'
959 973 '$$ &Edit this change manually'
960 974 '$$ &Skip remaining changes to this file'
961 975 '$$ Record remaining changes to this &file'
962 976 '$$ &Done, skip remaining changes and files'
963 977 '$$ Record &all changes to all remaining files'
964 978 '$$ &Quit, recording no changes'
965 979 '$$ &? (display help)')
966 980 r = ui.promptchoice("%s %s" % (query, resps))
967 981 ui.write("\n")
968 982 if r == 8: # ?
969 983 for c, t in ui.extractchoices(resps)[1]:
970 984 ui.write('%s - %s\n' % (c, t.lower()))
971 985 continue
972 986 elif r == 0: # yes
973 987 ret = True
974 988 elif r == 1: # no
975 989 ret = False
976 990 elif r == 2: # Edit patch
977 991 if chunk is None:
978 992 ui.write(_('cannot edit patch for whole file'))
979 993 ui.write("\n")
980 994 continue
981 995 if chunk.header.binary():
982 996 ui.write(_('cannot edit patch for binary file'))
983 997 ui.write("\n")
984 998 continue
985 999 # Patch comment based on the Git one (based on comment at end of
986 1000 # http://mercurial.selenic.com/wiki/RecordExtension)
987 1001 phelp = '---' + _("""
988 1002 To remove '-' lines, make them ' ' lines (context).
989 1003 To remove '+' lines, delete them.
990 1004 Lines starting with # will be removed from the patch.
991 1005
992 1006 If the patch applies cleanly, the edited hunk will immediately be
993 1007 added to the record list. If it does not apply cleanly, a rejects
994 1008 file will be generated: you can use that when you try again. If
995 1009 all lines of the hunk are removed, then the edit is aborted and
996 1010 the hunk is left unchanged.
997 1011 """)
998 1012 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
999 1013 suffix=".diff", text=True)
1000 1014 ncpatchfp = None
1001 1015 try:
1002 1016 # Write the initial patch
1003 1017 f = os.fdopen(patchfd, "w")
1004 1018 chunk.header.write(f)
1005 1019 chunk.write(f)
1006 1020 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1007 1021 f.close()
1008 1022 # Start the editor and wait for it to complete
1009 1023 editor = ui.geteditor()
1010 1024 ui.system("%s \"%s\"" % (editor, patchfn),
1011 1025 environ={'HGUSER': ui.username()},
1012 1026 onerr=util.Abort, errprefix=_("edit failed"))
1013 1027 # Remove comment lines
1014 1028 patchfp = open(patchfn)
1015 1029 ncpatchfp = cStringIO.StringIO()
1016 1030 for line in patchfp:
1017 1031 if not line.startswith('#'):
1018 1032 ncpatchfp.write(line)
1019 1033 patchfp.close()
1020 1034 ncpatchfp.seek(0)
1021 1035 newpatches = parsepatch(ncpatchfp)
1022 1036 finally:
1023 1037 os.unlink(patchfn)
1024 1038 del ncpatchfp
1025 1039 # Signal that the chunk shouldn't be applied as-is, but
1026 1040 # provide the new patch to be used instead.
1027 1041 ret = False
1028 1042 elif r == 3: # Skip
1029 1043 ret = skipfile = False
1030 1044 elif r == 4: # file (Record remaining)
1031 1045 ret = skipfile = True
1032 1046 elif r == 5: # done, skip remaining
1033 1047 ret = skipall = False
1034 1048 elif r == 6: # all
1035 1049 ret = skipall = True
1036 1050 elif r == 7: # quit
1037 1051 raise util.Abort(_('user quit'))
1038 1052 return ret, skipfile, skipall, newpatches
1039 1053
1040 1054 seen = set()
1041 1055 applied = {} # 'filename' -> [] of chunks
1042 1056 skipfile, skipall = None, None
1043 1057 pos, total = 1, sum(len(h.hunks) for h in headers)
1044 1058 for h in headers:
1045 1059 pos += len(h.hunks)
1046 1060 skipfile = None
1047 1061 fixoffset = 0
1048 1062 hdr = ''.join(h.header)
1049 1063 if hdr in seen:
1050 1064 continue
1051 1065 seen.add(hdr)
1052 1066 if skipall is None:
1053 1067 h.pretty(ui)
1054 1068 msg = (_('examine changes to %s?') %
1055 1069 _(' and ').join("'%s'" % f for f in h.files()))
1056 1070 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1057 1071 if not r:
1058 1072 continue
1059 1073 applied[h.filename()] = [h]
1060 1074 if h.allhunks():
1061 1075 applied[h.filename()] += h.hunks
1062 1076 continue
1063 1077 for i, chunk in enumerate(h.hunks):
1064 1078 if skipfile is None and skipall is None:
1065 1079 chunk.pretty(ui)
1066 1080 if total == 1:
1067 1081 msg = _("record this change to '%s'?") % chunk.filename()
1068 1082 else:
1069 1083 idx = pos - len(h.hunks) + i
1070 1084 msg = _("record change %d/%d to '%s'?") % (idx, total,
1071 1085 chunk.filename())
1072 1086 r, skipfile, skipall, newpatches = prompt(skipfile,
1073 1087 skipall, msg, chunk)
1074 1088 if r:
1075 1089 if fixoffset:
1076 1090 chunk = copy.copy(chunk)
1077 1091 chunk.toline += fixoffset
1078 1092 applied[chunk.filename()].append(chunk)
1079 1093 elif newpatches is not None:
1080 1094 for newpatch in newpatches:
1081 1095 for newhunk in newpatch.hunks:
1082 1096 if fixoffset:
1083 1097 newhunk.toline += fixoffset
1084 1098 applied[newhunk.filename()].append(newhunk)
1085 1099 else:
1086 1100 fixoffset += chunk.removed - chunk.added
1087 1101 return sum([h for h in applied.itervalues()
1088 1102 if h[0].special() or len(h) > 1], [])
1089 1103 class hunk(object):
1090 1104 def __init__(self, desc, num, lr, context):
1091 1105 self.number = num
1092 1106 self.desc = desc
1093 1107 self.hunk = [desc]
1094 1108 self.a = []
1095 1109 self.b = []
1096 1110 self.starta = self.lena = None
1097 1111 self.startb = self.lenb = None
1098 1112 if lr is not None:
1099 1113 if context:
1100 1114 self.read_context_hunk(lr)
1101 1115 else:
1102 1116 self.read_unified_hunk(lr)
1103 1117
1104 1118 def getnormalized(self):
1105 1119 """Return a copy with line endings normalized to LF."""
1106 1120
1107 1121 def normalize(lines):
1108 1122 nlines = []
1109 1123 for line in lines:
1110 1124 if line.endswith('\r\n'):
1111 1125 line = line[:-2] + '\n'
1112 1126 nlines.append(line)
1113 1127 return nlines
1114 1128
1115 1129 # Dummy object, it is rebuilt manually
1116 1130 nh = hunk(self.desc, self.number, None, None)
1117 1131 nh.number = self.number
1118 1132 nh.desc = self.desc
1119 1133 nh.hunk = self.hunk
1120 1134 nh.a = normalize(self.a)
1121 1135 nh.b = normalize(self.b)
1122 1136 nh.starta = self.starta
1123 1137 nh.startb = self.startb
1124 1138 nh.lena = self.lena
1125 1139 nh.lenb = self.lenb
1126 1140 return nh
1127 1141
1128 1142 def read_unified_hunk(self, lr):
1129 1143 m = unidesc.match(self.desc)
1130 1144 if not m:
1131 1145 raise PatchError(_("bad hunk #%d") % self.number)
1132 1146 self.starta, self.lena, self.startb, self.lenb = m.groups()
1133 1147 if self.lena is None:
1134 1148 self.lena = 1
1135 1149 else:
1136 1150 self.lena = int(self.lena)
1137 1151 if self.lenb is None:
1138 1152 self.lenb = 1
1139 1153 else:
1140 1154 self.lenb = int(self.lenb)
1141 1155 self.starta = int(self.starta)
1142 1156 self.startb = int(self.startb)
1143 1157 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1144 1158 self.b)
1145 1159 # if we hit eof before finishing out the hunk, the last line will
1146 1160 # be zero length. Lets try to fix it up.
1147 1161 while len(self.hunk[-1]) == 0:
1148 1162 del self.hunk[-1]
1149 1163 del self.a[-1]
1150 1164 del self.b[-1]
1151 1165 self.lena -= 1
1152 1166 self.lenb -= 1
1153 1167 self._fixnewline(lr)
1154 1168
1155 1169 def read_context_hunk(self, lr):
1156 1170 self.desc = lr.readline()
1157 1171 m = contextdesc.match(self.desc)
1158 1172 if not m:
1159 1173 raise PatchError(_("bad hunk #%d") % self.number)
1160 1174 self.starta, aend = m.groups()
1161 1175 self.starta = int(self.starta)
1162 1176 if aend is None:
1163 1177 aend = self.starta
1164 1178 self.lena = int(aend) - self.starta
1165 1179 if self.starta:
1166 1180 self.lena += 1
1167 1181 for x in xrange(self.lena):
1168 1182 l = lr.readline()
1169 1183 if l.startswith('---'):
1170 1184 # lines addition, old block is empty
1171 1185 lr.push(l)
1172 1186 break
1173 1187 s = l[2:]
1174 1188 if l.startswith('- ') or l.startswith('! '):
1175 1189 u = '-' + s
1176 1190 elif l.startswith(' '):
1177 1191 u = ' ' + s
1178 1192 else:
1179 1193 raise PatchError(_("bad hunk #%d old text line %d") %
1180 1194 (self.number, x))
1181 1195 self.a.append(u)
1182 1196 self.hunk.append(u)
1183 1197
1184 1198 l = lr.readline()
1185 1199 if l.startswith('\ '):
1186 1200 s = self.a[-1][:-1]
1187 1201 self.a[-1] = s
1188 1202 self.hunk[-1] = s
1189 1203 l = lr.readline()
1190 1204 m = contextdesc.match(l)
1191 1205 if not m:
1192 1206 raise PatchError(_("bad hunk #%d") % self.number)
1193 1207 self.startb, bend = m.groups()
1194 1208 self.startb = int(self.startb)
1195 1209 if bend is None:
1196 1210 bend = self.startb
1197 1211 self.lenb = int(bend) - self.startb
1198 1212 if self.startb:
1199 1213 self.lenb += 1
1200 1214 hunki = 1
1201 1215 for x in xrange(self.lenb):
1202 1216 l = lr.readline()
1203 1217 if l.startswith('\ '):
1204 1218 # XXX: the only way to hit this is with an invalid line range.
1205 1219 # The no-eol marker is not counted in the line range, but I
1206 1220 # guess there are diff(1) out there which behave differently.
1207 1221 s = self.b[-1][:-1]
1208 1222 self.b[-1] = s
1209 1223 self.hunk[hunki - 1] = s
1210 1224 continue
1211 1225 if not l:
1212 1226 # line deletions, new block is empty and we hit EOF
1213 1227 lr.push(l)
1214 1228 break
1215 1229 s = l[2:]
1216 1230 if l.startswith('+ ') or l.startswith('! '):
1217 1231 u = '+' + s
1218 1232 elif l.startswith(' '):
1219 1233 u = ' ' + s
1220 1234 elif len(self.b) == 0:
1221 1235 # line deletions, new block is empty
1222 1236 lr.push(l)
1223 1237 break
1224 1238 else:
1225 1239 raise PatchError(_("bad hunk #%d old text line %d") %
1226 1240 (self.number, x))
1227 1241 self.b.append(s)
1228 1242 while True:
1229 1243 if hunki >= len(self.hunk):
1230 1244 h = ""
1231 1245 else:
1232 1246 h = self.hunk[hunki]
1233 1247 hunki += 1
1234 1248 if h == u:
1235 1249 break
1236 1250 elif h.startswith('-'):
1237 1251 continue
1238 1252 else:
1239 1253 self.hunk.insert(hunki - 1, u)
1240 1254 break
1241 1255
1242 1256 if not self.a:
1243 1257 # this happens when lines were only added to the hunk
1244 1258 for x in self.hunk:
1245 1259 if x.startswith('-') or x.startswith(' '):
1246 1260 self.a.append(x)
1247 1261 if not self.b:
1248 1262 # this happens when lines were only deleted from the hunk
1249 1263 for x in self.hunk:
1250 1264 if x.startswith('+') or x.startswith(' '):
1251 1265 self.b.append(x[1:])
1252 1266 # @@ -start,len +start,len @@
1253 1267 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1254 1268 self.startb, self.lenb)
1255 1269 self.hunk[0] = self.desc
1256 1270 self._fixnewline(lr)
1257 1271
1258 1272 def _fixnewline(self, lr):
1259 1273 l = lr.readline()
1260 1274 if l.startswith('\ '):
1261 1275 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1262 1276 else:
1263 1277 lr.push(l)
1264 1278
1265 1279 def complete(self):
1266 1280 return len(self.a) == self.lena and len(self.b) == self.lenb
1267 1281
1268 1282 def _fuzzit(self, old, new, fuzz, toponly):
1269 1283 # this removes context lines from the top and bottom of list 'l'. It
1270 1284 # checks the hunk to make sure only context lines are removed, and then
1271 1285 # returns a new shortened list of lines.
1272 1286 fuzz = min(fuzz, len(old))
1273 1287 if fuzz:
1274 1288 top = 0
1275 1289 bot = 0
1276 1290 hlen = len(self.hunk)
1277 1291 for x in xrange(hlen - 1):
1278 1292 # the hunk starts with the @@ line, so use x+1
1279 1293 if self.hunk[x + 1][0] == ' ':
1280 1294 top += 1
1281 1295 else:
1282 1296 break
1283 1297 if not toponly:
1284 1298 for x in xrange(hlen - 1):
1285 1299 if self.hunk[hlen - bot - 1][0] == ' ':
1286 1300 bot += 1
1287 1301 else:
1288 1302 break
1289 1303
1290 1304 bot = min(fuzz, bot)
1291 1305 top = min(fuzz, top)
1292 1306 return old[top:len(old) - bot], new[top:len(new) - bot], top
1293 1307 return old, new, 0
1294 1308
1295 1309 def fuzzit(self, fuzz, toponly):
1296 1310 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1297 1311 oldstart = self.starta + top
1298 1312 newstart = self.startb + top
1299 1313 # zero length hunk ranges already have their start decremented
1300 1314 if self.lena and oldstart > 0:
1301 1315 oldstart -= 1
1302 1316 if self.lenb and newstart > 0:
1303 1317 newstart -= 1
1304 1318 return old, oldstart, new, newstart
1305 1319
1306 1320 class binhunk(object):
1307 1321 'A binary patch file.'
1308 1322 def __init__(self, lr, fname):
1309 1323 self.text = None
1310 1324 self.delta = False
1311 1325 self.hunk = ['GIT binary patch\n']
1312 1326 self._fname = fname
1313 1327 self._read(lr)
1314 1328
1315 1329 def complete(self):
1316 1330 return self.text is not None
1317 1331
1318 1332 def new(self, lines):
1319 1333 if self.delta:
1320 1334 return [applybindelta(self.text, ''.join(lines))]
1321 1335 return [self.text]
1322 1336
1323 1337 def _read(self, lr):
1324 1338 def getline(lr, hunk):
1325 1339 l = lr.readline()
1326 1340 hunk.append(l)
1327 1341 return l.rstrip('\r\n')
1328 1342
1329 1343 size = 0
1330 1344 while True:
1331 1345 line = getline(lr, self.hunk)
1332 1346 if not line:
1333 1347 raise PatchError(_('could not extract "%s" binary data')
1334 1348 % self._fname)
1335 1349 if line.startswith('literal '):
1336 1350 size = int(line[8:].rstrip())
1337 1351 break
1338 1352 if line.startswith('delta '):
1339 1353 size = int(line[6:].rstrip())
1340 1354 self.delta = True
1341 1355 break
1342 1356 dec = []
1343 1357 line = getline(lr, self.hunk)
1344 1358 while len(line) > 1:
1345 1359 l = line[0]
1346 1360 if l <= 'Z' and l >= 'A':
1347 1361 l = ord(l) - ord('A') + 1
1348 1362 else:
1349 1363 l = ord(l) - ord('a') + 27
1350 1364 try:
1351 1365 dec.append(base85.b85decode(line[1:])[:l])
1352 1366 except ValueError, e:
1353 1367 raise PatchError(_('could not decode "%s" binary patch: %s')
1354 1368 % (self._fname, str(e)))
1355 1369 line = getline(lr, self.hunk)
1356 1370 text = zlib.decompress(''.join(dec))
1357 1371 if len(text) != size:
1358 1372 raise PatchError(_('"%s" length is %d bytes, should be %d')
1359 1373 % (self._fname, len(text), size))
1360 1374 self.text = text
1361 1375
1362 1376 def parsefilename(str):
1363 1377 # --- filename \t|space stuff
1364 1378 s = str[4:].rstrip('\r\n')
1365 1379 i = s.find('\t')
1366 1380 if i < 0:
1367 1381 i = s.find(' ')
1368 1382 if i < 0:
1369 1383 return s
1370 1384 return s[:i]
1371 1385
1372 1386 def parsepatch(originalchunks):
1373 1387 """patch -> [] of headers -> [] of hunks """
1374 1388 class parser(object):
1375 1389 """patch parsing state machine"""
1376 1390 def __init__(self):
1377 1391 self.fromline = 0
1378 1392 self.toline = 0
1379 1393 self.proc = ''
1380 1394 self.header = None
1381 1395 self.context = []
1382 1396 self.before = []
1383 1397 self.hunk = []
1384 1398 self.headers = []
1385 1399
1386 1400 def addrange(self, limits):
1387 1401 fromstart, fromend, tostart, toend, proc = limits
1388 1402 self.fromline = int(fromstart)
1389 1403 self.toline = int(tostart)
1390 1404 self.proc = proc
1391 1405
1392 1406 def addcontext(self, context):
1393 1407 if self.hunk:
1394 1408 h = recordhunk(self.header, self.fromline, self.toline,
1395 1409 self.proc, self.before, self.hunk, context)
1396 1410 self.header.hunks.append(h)
1397 1411 self.fromline += len(self.before) + h.removed
1398 1412 self.toline += len(self.before) + h.added
1399 1413 self.before = []
1400 1414 self.hunk = []
1401 1415 self.proc = ''
1402 1416 self.context = context
1403 1417
1404 1418 def addhunk(self, hunk):
1405 1419 if self.context:
1406 1420 self.before = self.context
1407 1421 self.context = []
1408 1422 self.hunk = hunk
1409 1423
1410 1424 def newfile(self, hdr):
1411 1425 self.addcontext([])
1412 1426 h = header(hdr)
1413 1427 self.headers.append(h)
1414 1428 self.header = h
1415 1429
1416 1430 def addother(self, line):
1417 1431 pass # 'other' lines are ignored
1418 1432
1419 1433 def finished(self):
1420 1434 self.addcontext([])
1421 1435 return self.headers
1422 1436
1423 1437 transitions = {
1424 1438 'file': {'context': addcontext,
1425 1439 'file': newfile,
1426 1440 'hunk': addhunk,
1427 1441 'range': addrange},
1428 1442 'context': {'file': newfile,
1429 1443 'hunk': addhunk,
1430 1444 'range': addrange,
1431 1445 'other': addother},
1432 1446 'hunk': {'context': addcontext,
1433 1447 'file': newfile,
1434 1448 'range': addrange},
1435 1449 'range': {'context': addcontext,
1436 1450 'hunk': addhunk},
1437 1451 'other': {'other': addother},
1438 1452 }
1439 1453
1440 1454 p = parser()
1441 1455 fp = cStringIO.StringIO()
1442 1456 fp.write(''.join(originalchunks))
1443 1457 fp.seek(0)
1444 1458
1445 1459 state = 'context'
1446 1460 for newstate, data in scanpatch(fp):
1447 1461 try:
1448 1462 p.transitions[state][newstate](p, data)
1449 1463 except KeyError:
1450 1464 raise PatchError('unhandled transition: %s -> %s' %
1451 1465 (state, newstate))
1452 1466 state = newstate
1453 1467 del fp
1454 1468 return p.finished()
1455 1469
1456 1470 def pathtransform(path, strip, prefix):
1457 1471 '''turn a path from a patch into a path suitable for the repository
1458 1472
1459 1473 prefix, if not empty, is expected to be normalized with a / at the end.
1460 1474
1461 1475 Returns (stripped components, path in repository).
1462 1476
1463 1477 >>> pathtransform('a/b/c', 0, '')
1464 1478 ('', 'a/b/c')
1465 1479 >>> pathtransform(' a/b/c ', 0, '')
1466 1480 ('', ' a/b/c')
1467 1481 >>> pathtransform(' a/b/c ', 2, '')
1468 1482 ('a/b/', 'c')
1469 1483 >>> pathtransform('a/b/c', 0, 'd/e/')
1470 1484 ('', 'd/e/a/b/c')
1471 1485 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1472 1486 ('a//b/', 'd/e/c')
1473 1487 >>> pathtransform('a/b/c', 3, '')
1474 1488 Traceback (most recent call last):
1475 1489 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1476 1490 '''
1477 1491 pathlen = len(path)
1478 1492 i = 0
1479 1493 if strip == 0:
1480 1494 return '', prefix + path.rstrip()
1481 1495 count = strip
1482 1496 while count > 0:
1483 1497 i = path.find('/', i)
1484 1498 if i == -1:
1485 1499 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1486 1500 (count, strip, path))
1487 1501 i += 1
1488 1502 # consume '//' in the path
1489 1503 while i < pathlen - 1 and path[i] == '/':
1490 1504 i += 1
1491 1505 count -= 1
1492 1506 return path[:i].lstrip(), prefix + path[i:].rstrip()
1493 1507
1494 1508 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1495 1509 nulla = afile_orig == "/dev/null"
1496 1510 nullb = bfile_orig == "/dev/null"
1497 1511 create = nulla and hunk.starta == 0 and hunk.lena == 0
1498 1512 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1499 1513 abase, afile = pathtransform(afile_orig, strip, prefix)
1500 1514 gooda = not nulla and backend.exists(afile)
1501 1515 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1502 1516 if afile == bfile:
1503 1517 goodb = gooda
1504 1518 else:
1505 1519 goodb = not nullb and backend.exists(bfile)
1506 1520 missing = not goodb and not gooda and not create
1507 1521
1508 1522 # some diff programs apparently produce patches where the afile is
1509 1523 # not /dev/null, but afile starts with bfile
1510 1524 abasedir = afile[:afile.rfind('/') + 1]
1511 1525 bbasedir = bfile[:bfile.rfind('/') + 1]
1512 1526 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1513 1527 and hunk.starta == 0 and hunk.lena == 0):
1514 1528 create = True
1515 1529 missing = False
1516 1530
1517 1531 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1518 1532 # diff is between a file and its backup. In this case, the original
1519 1533 # file should be patched (see original mpatch code).
1520 1534 isbackup = (abase == bbase and bfile.startswith(afile))
1521 1535 fname = None
1522 1536 if not missing:
1523 1537 if gooda and goodb:
1524 1538 if isbackup:
1525 1539 fname = afile
1526 1540 else:
1527 1541 fname = bfile
1528 1542 elif gooda:
1529 1543 fname = afile
1530 1544
1531 1545 if not fname:
1532 1546 if not nullb:
1533 1547 if isbackup:
1534 1548 fname = afile
1535 1549 else:
1536 1550 fname = bfile
1537 1551 elif not nulla:
1538 1552 fname = afile
1539 1553 else:
1540 1554 raise PatchError(_("undefined source and destination files"))
1541 1555
1542 1556 gp = patchmeta(fname)
1543 1557 if create:
1544 1558 gp.op = 'ADD'
1545 1559 elif remove:
1546 1560 gp.op = 'DELETE'
1547 1561 return gp
1548 1562
1549 1563 def scanpatch(fp):
1550 1564 """like patch.iterhunks, but yield different events
1551 1565
1552 1566 - ('file', [header_lines + fromfile + tofile])
1553 1567 - ('context', [context_lines])
1554 1568 - ('hunk', [hunk_lines])
1555 1569 - ('range', (-start,len, +start,len, proc))
1556 1570 """
1557 1571 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1558 1572 lr = linereader(fp)
1559 1573
1560 1574 def scanwhile(first, p):
1561 1575 """scan lr while predicate holds"""
1562 1576 lines = [first]
1563 1577 while True:
1564 1578 line = lr.readline()
1565 1579 if not line:
1566 1580 break
1567 1581 if p(line):
1568 1582 lines.append(line)
1569 1583 else:
1570 1584 lr.push(line)
1571 1585 break
1572 1586 return lines
1573 1587
1574 1588 while True:
1575 1589 line = lr.readline()
1576 1590 if not line:
1577 1591 break
1578 1592 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1579 1593 def notheader(line):
1580 1594 s = line.split(None, 1)
1581 1595 return not s or s[0] not in ('---', 'diff')
1582 1596 header = scanwhile(line, notheader)
1583 1597 fromfile = lr.readline()
1584 1598 if fromfile.startswith('---'):
1585 1599 tofile = lr.readline()
1586 1600 header += [fromfile, tofile]
1587 1601 else:
1588 1602 lr.push(fromfile)
1589 1603 yield 'file', header
1590 1604 elif line[0] == ' ':
1591 1605 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1592 1606 elif line[0] in '-+':
1593 1607 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1594 1608 else:
1595 1609 m = lines_re.match(line)
1596 1610 if m:
1597 1611 yield 'range', m.groups()
1598 1612 else:
1599 1613 yield 'other', line
1600 1614
1601 1615 def scangitpatch(lr, firstline):
1602 1616 """
1603 1617 Git patches can emit:
1604 1618 - rename a to b
1605 1619 - change b
1606 1620 - copy a to c
1607 1621 - change c
1608 1622
1609 1623 We cannot apply this sequence as-is, the renamed 'a' could not be
1610 1624 found for it would have been renamed already. And we cannot copy
1611 1625 from 'b' instead because 'b' would have been changed already. So
1612 1626 we scan the git patch for copy and rename commands so we can
1613 1627 perform the copies ahead of time.
1614 1628 """
1615 1629 pos = 0
1616 1630 try:
1617 1631 pos = lr.fp.tell()
1618 1632 fp = lr.fp
1619 1633 except IOError:
1620 1634 fp = cStringIO.StringIO(lr.fp.read())
1621 1635 gitlr = linereader(fp)
1622 1636 gitlr.push(firstline)
1623 1637 gitpatches = readgitpatch(gitlr)
1624 1638 fp.seek(pos)
1625 1639 return gitpatches
1626 1640
1627 1641 def iterhunks(fp):
1628 1642 """Read a patch and yield the following events:
1629 1643 - ("file", afile, bfile, firsthunk): select a new target file.
1630 1644 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1631 1645 "file" event.
1632 1646 - ("git", gitchanges): current diff is in git format, gitchanges
1633 1647 maps filenames to gitpatch records. Unique event.
1634 1648 """
1635 1649 afile = ""
1636 1650 bfile = ""
1637 1651 state = None
1638 1652 hunknum = 0
1639 1653 emitfile = newfile = False
1640 1654 gitpatches = None
1641 1655
1642 1656 # our states
1643 1657 BFILE = 1
1644 1658 context = None
1645 1659 lr = linereader(fp)
1646 1660
1647 1661 while True:
1648 1662 x = lr.readline()
1649 1663 if not x:
1650 1664 break
1651 1665 if state == BFILE and (
1652 1666 (not context and x[0] == '@')
1653 1667 or (context is not False and x.startswith('***************'))
1654 1668 or x.startswith('GIT binary patch')):
1655 1669 gp = None
1656 1670 if (gitpatches and
1657 1671 gitpatches[-1].ispatching(afile, bfile)):
1658 1672 gp = gitpatches.pop()
1659 1673 if x.startswith('GIT binary patch'):
1660 1674 h = binhunk(lr, gp.path)
1661 1675 else:
1662 1676 if context is None and x.startswith('***************'):
1663 1677 context = True
1664 1678 h = hunk(x, hunknum + 1, lr, context)
1665 1679 hunknum += 1
1666 1680 if emitfile:
1667 1681 emitfile = False
1668 1682 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1669 1683 yield 'hunk', h
1670 1684 elif x.startswith('diff --git a/'):
1671 1685 m = gitre.match(x.rstrip(' \r\n'))
1672 1686 if not m:
1673 1687 continue
1674 1688 if gitpatches is None:
1675 1689 # scan whole input for git metadata
1676 1690 gitpatches = scangitpatch(lr, x)
1677 1691 yield 'git', [g.copy() for g in gitpatches
1678 1692 if g.op in ('COPY', 'RENAME')]
1679 1693 gitpatches.reverse()
1680 1694 afile = 'a/' + m.group(1)
1681 1695 bfile = 'b/' + m.group(2)
1682 1696 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1683 1697 gp = gitpatches.pop()
1684 1698 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1685 1699 if not gitpatches:
1686 1700 raise PatchError(_('failed to synchronize metadata for "%s"')
1687 1701 % afile[2:])
1688 1702 gp = gitpatches[-1]
1689 1703 newfile = True
1690 1704 elif x.startswith('---'):
1691 1705 # check for a unified diff
1692 1706 l2 = lr.readline()
1693 1707 if not l2.startswith('+++'):
1694 1708 lr.push(l2)
1695 1709 continue
1696 1710 newfile = True
1697 1711 context = False
1698 1712 afile = parsefilename(x)
1699 1713 bfile = parsefilename(l2)
1700 1714 elif x.startswith('***'):
1701 1715 # check for a context diff
1702 1716 l2 = lr.readline()
1703 1717 if not l2.startswith('---'):
1704 1718 lr.push(l2)
1705 1719 continue
1706 1720 l3 = lr.readline()
1707 1721 lr.push(l3)
1708 1722 if not l3.startswith("***************"):
1709 1723 lr.push(l2)
1710 1724 continue
1711 1725 newfile = True
1712 1726 context = True
1713 1727 afile = parsefilename(x)
1714 1728 bfile = parsefilename(l2)
1715 1729
1716 1730 if newfile:
1717 1731 newfile = False
1718 1732 emitfile = True
1719 1733 state = BFILE
1720 1734 hunknum = 0
1721 1735
1722 1736 while gitpatches:
1723 1737 gp = gitpatches.pop()
1724 1738 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1725 1739
1726 1740 def applybindelta(binchunk, data):
1727 1741 """Apply a binary delta hunk
1728 1742 The algorithm used is the algorithm from git's patch-delta.c
1729 1743 """
1730 1744 def deltahead(binchunk):
1731 1745 i = 0
1732 1746 for c in binchunk:
1733 1747 i += 1
1734 1748 if not (ord(c) & 0x80):
1735 1749 return i
1736 1750 return i
1737 1751 out = ""
1738 1752 s = deltahead(binchunk)
1739 1753 binchunk = binchunk[s:]
1740 1754 s = deltahead(binchunk)
1741 1755 binchunk = binchunk[s:]
1742 1756 i = 0
1743 1757 while i < len(binchunk):
1744 1758 cmd = ord(binchunk[i])
1745 1759 i += 1
1746 1760 if (cmd & 0x80):
1747 1761 offset = 0
1748 1762 size = 0
1749 1763 if (cmd & 0x01):
1750 1764 offset = ord(binchunk[i])
1751 1765 i += 1
1752 1766 if (cmd & 0x02):
1753 1767 offset |= ord(binchunk[i]) << 8
1754 1768 i += 1
1755 1769 if (cmd & 0x04):
1756 1770 offset |= ord(binchunk[i]) << 16
1757 1771 i += 1
1758 1772 if (cmd & 0x08):
1759 1773 offset |= ord(binchunk[i]) << 24
1760 1774 i += 1
1761 1775 if (cmd & 0x10):
1762 1776 size = ord(binchunk[i])
1763 1777 i += 1
1764 1778 if (cmd & 0x20):
1765 1779 size |= ord(binchunk[i]) << 8
1766 1780 i += 1
1767 1781 if (cmd & 0x40):
1768 1782 size |= ord(binchunk[i]) << 16
1769 1783 i += 1
1770 1784 if size == 0:
1771 1785 size = 0x10000
1772 1786 offset_end = offset + size
1773 1787 out += data[offset:offset_end]
1774 1788 elif cmd != 0:
1775 1789 offset_end = i + cmd
1776 1790 out += binchunk[i:offset_end]
1777 1791 i += cmd
1778 1792 else:
1779 1793 raise PatchError(_('unexpected delta opcode 0'))
1780 1794 return out
1781 1795
1782 1796 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1783 1797 """Reads a patch from fp and tries to apply it.
1784 1798
1785 1799 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1786 1800 there was any fuzz.
1787 1801
1788 1802 If 'eolmode' is 'strict', the patch content and patched file are
1789 1803 read in binary mode. Otherwise, line endings are ignored when
1790 1804 patching then normalized according to 'eolmode'.
1791 1805 """
1792 1806 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1793 1807 prefix=prefix, eolmode=eolmode)
1794 1808
1795 1809 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1796 1810 eolmode='strict'):
1797 1811
1798 1812 if prefix:
1799 1813 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1800 1814 prefix)
1801 1815 if prefix != '':
1802 1816 prefix += '/'
1803 1817 def pstrip(p):
1804 1818 return pathtransform(p, strip - 1, prefix)[1]
1805 1819
1806 1820 rejects = 0
1807 1821 err = 0
1808 1822 current_file = None
1809 1823
1810 1824 for state, values in iterhunks(fp):
1811 1825 if state == 'hunk':
1812 1826 if not current_file:
1813 1827 continue
1814 1828 ret = current_file.apply(values)
1815 1829 if ret > 0:
1816 1830 err = 1
1817 1831 elif state == 'file':
1818 1832 if current_file:
1819 1833 rejects += current_file.close()
1820 1834 current_file = None
1821 1835 afile, bfile, first_hunk, gp = values
1822 1836 if gp:
1823 1837 gp.path = pstrip(gp.path)
1824 1838 if gp.oldpath:
1825 1839 gp.oldpath = pstrip(gp.oldpath)
1826 1840 else:
1827 1841 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1828 1842 prefix)
1829 1843 if gp.op == 'RENAME':
1830 1844 backend.unlink(gp.oldpath)
1831 1845 if not first_hunk:
1832 1846 if gp.op == 'DELETE':
1833 1847 backend.unlink(gp.path)
1834 1848 continue
1835 1849 data, mode = None, None
1836 1850 if gp.op in ('RENAME', 'COPY'):
1837 1851 data, mode = store.getfile(gp.oldpath)[:2]
1838 1852 # FIXME: failing getfile has never been handled here
1839 1853 assert data is not None
1840 1854 if gp.mode:
1841 1855 mode = gp.mode
1842 1856 if gp.op == 'ADD':
1843 1857 # Added files without content have no hunk and
1844 1858 # must be created
1845 1859 data = ''
1846 1860 if data or mode:
1847 1861 if (gp.op in ('ADD', 'RENAME', 'COPY')
1848 1862 and backend.exists(gp.path)):
1849 1863 raise PatchError(_("cannot create %s: destination "
1850 1864 "already exists") % gp.path)
1851 1865 backend.setfile(gp.path, data, mode, gp.oldpath)
1852 1866 continue
1853 1867 try:
1854 1868 current_file = patcher(ui, gp, backend, store,
1855 1869 eolmode=eolmode)
1856 1870 except PatchError, inst:
1857 1871 ui.warn(str(inst) + '\n')
1858 1872 current_file = None
1859 1873 rejects += 1
1860 1874 continue
1861 1875 elif state == 'git':
1862 1876 for gp in values:
1863 1877 path = pstrip(gp.oldpath)
1864 1878 data, mode = backend.getfile(path)
1865 1879 if data is None:
1866 1880 # The error ignored here will trigger a getfile()
1867 1881 # error in a place more appropriate for error
1868 1882 # handling, and will not interrupt the patching
1869 1883 # process.
1870 1884 pass
1871 1885 else:
1872 1886 store.setfile(path, data, mode)
1873 1887 else:
1874 1888 raise util.Abort(_('unsupported parser state: %s') % state)
1875 1889
1876 1890 if current_file:
1877 1891 rejects += current_file.close()
1878 1892
1879 1893 if rejects:
1880 1894 return -1
1881 1895 return err
1882 1896
1883 1897 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1884 1898 similarity):
1885 1899 """use <patcher> to apply <patchname> to the working directory.
1886 1900 returns whether patch was applied with fuzz factor."""
1887 1901
1888 1902 fuzz = False
1889 1903 args = []
1890 1904 cwd = repo.root
1891 1905 if cwd:
1892 1906 args.append('-d %s' % util.shellquote(cwd))
1893 1907 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1894 1908 util.shellquote(patchname)))
1895 1909 try:
1896 1910 for line in fp:
1897 1911 line = line.rstrip()
1898 1912 ui.note(line + '\n')
1899 1913 if line.startswith('patching file '):
1900 1914 pf = util.parsepatchoutput(line)
1901 1915 printed_file = False
1902 1916 files.add(pf)
1903 1917 elif line.find('with fuzz') >= 0:
1904 1918 fuzz = True
1905 1919 if not printed_file:
1906 1920 ui.warn(pf + '\n')
1907 1921 printed_file = True
1908 1922 ui.warn(line + '\n')
1909 1923 elif line.find('saving rejects to file') >= 0:
1910 1924 ui.warn(line + '\n')
1911 1925 elif line.find('FAILED') >= 0:
1912 1926 if not printed_file:
1913 1927 ui.warn(pf + '\n')
1914 1928 printed_file = True
1915 1929 ui.warn(line + '\n')
1916 1930 finally:
1917 1931 if files:
1918 1932 scmutil.marktouched(repo, files, similarity)
1919 1933 code = fp.close()
1920 1934 if code:
1921 1935 raise PatchError(_("patch command failed: %s") %
1922 1936 util.explainexit(code)[0])
1923 1937 return fuzz
1924 1938
1925 1939 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
1926 1940 eolmode='strict'):
1927 1941 if files is None:
1928 1942 files = set()
1929 1943 if eolmode is None:
1930 1944 eolmode = ui.config('patch', 'eol', 'strict')
1931 1945 if eolmode.lower() not in eolmodes:
1932 1946 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1933 1947 eolmode = eolmode.lower()
1934 1948
1935 1949 store = filestore()
1936 1950 try:
1937 1951 fp = open(patchobj, 'rb')
1938 1952 except TypeError:
1939 1953 fp = patchobj
1940 1954 try:
1941 1955 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
1942 1956 eolmode=eolmode)
1943 1957 finally:
1944 1958 if fp != patchobj:
1945 1959 fp.close()
1946 1960 files.update(backend.close())
1947 1961 store.close()
1948 1962 if ret < 0:
1949 1963 raise PatchError(_('patch failed to apply'))
1950 1964 return ret > 0
1951 1965
1952 1966 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
1953 1967 eolmode='strict', similarity=0):
1954 1968 """use builtin patch to apply <patchobj> to the working directory.
1955 1969 returns whether patch was applied with fuzz factor."""
1956 1970 backend = workingbackend(ui, repo, similarity)
1957 1971 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1958 1972
1959 1973 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
1960 1974 eolmode='strict'):
1961 1975 backend = repobackend(ui, repo, ctx, store)
1962 1976 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1963 1977
1964 1978 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
1965 1979 similarity=0):
1966 1980 """Apply <patchname> to the working directory.
1967 1981
1968 1982 'eolmode' specifies how end of lines should be handled. It can be:
1969 1983 - 'strict': inputs are read in binary mode, EOLs are preserved
1970 1984 - 'crlf': EOLs are ignored when patching and reset to CRLF
1971 1985 - 'lf': EOLs are ignored when patching and reset to LF
1972 1986 - None: get it from user settings, default to 'strict'
1973 1987 'eolmode' is ignored when using an external patcher program.
1974 1988
1975 1989 Returns whether patch was applied with fuzz factor.
1976 1990 """
1977 1991 patcher = ui.config('ui', 'patch')
1978 1992 if files is None:
1979 1993 files = set()
1980 1994 if patcher:
1981 1995 return _externalpatch(ui, repo, patcher, patchname, strip,
1982 1996 files, similarity)
1983 1997 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
1984 1998 similarity)
1985 1999
1986 2000 def changedfiles(ui, repo, patchpath, strip=1):
1987 2001 backend = fsbackend(ui, repo.root)
1988 2002 fp = open(patchpath, 'rb')
1989 2003 try:
1990 2004 changed = set()
1991 2005 for state, values in iterhunks(fp):
1992 2006 if state == 'file':
1993 2007 afile, bfile, first_hunk, gp = values
1994 2008 if gp:
1995 2009 gp.path = pathtransform(gp.path, strip - 1, '')[1]
1996 2010 if gp.oldpath:
1997 2011 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
1998 2012 else:
1999 2013 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2000 2014 '')
2001 2015 changed.add(gp.path)
2002 2016 if gp.op == 'RENAME':
2003 2017 changed.add(gp.oldpath)
2004 2018 elif state not in ('hunk', 'git'):
2005 2019 raise util.Abort(_('unsupported parser state: %s') % state)
2006 2020 return changed
2007 2021 finally:
2008 2022 fp.close()
2009 2023
2010 2024 class GitDiffRequired(Exception):
2011 2025 pass
2012 2026
2013 2027 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2014 2028 '''return diffopts with all features supported and parsed'''
2015 2029 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2016 2030 git=True, whitespace=True, formatchanging=True)
2017 2031
2018 2032 diffopts = diffallopts
2019 2033
2020 2034 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2021 2035 whitespace=False, formatchanging=False):
2022 2036 '''return diffopts with only opted-in features parsed
2023 2037
2024 2038 Features:
2025 2039 - git: git-style diffs
2026 2040 - whitespace: whitespace options like ignoreblanklines and ignorews
2027 2041 - formatchanging: options that will likely break or cause correctness issues
2028 2042 with most diff parsers
2029 2043 '''
2030 2044 def get(key, name=None, getter=ui.configbool, forceplain=None):
2031 2045 if opts:
2032 2046 v = opts.get(key)
2033 2047 if v:
2034 2048 return v
2035 2049 if forceplain is not None and ui.plain():
2036 2050 return forceplain
2037 2051 return getter(section, name or key, None, untrusted=untrusted)
2038 2052
2039 2053 # core options, expected to be understood by every diff parser
2040 2054 buildopts = {
2041 2055 'nodates': get('nodates'),
2042 2056 'showfunc': get('show_function', 'showfunc'),
2043 2057 'context': get('unified', getter=ui.config),
2044 2058 }
2045 2059
2046 2060 if git:
2047 2061 buildopts['git'] = get('git')
2048 2062 if whitespace:
2049 2063 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2050 2064 buildopts['ignorewsamount'] = get('ignore_space_change',
2051 2065 'ignorewsamount')
2052 2066 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2053 2067 'ignoreblanklines')
2054 2068 if formatchanging:
2055 2069 buildopts['text'] = opts and opts.get('text')
2056 2070 buildopts['nobinary'] = get('nobinary')
2057 2071 buildopts['noprefix'] = get('noprefix', forceplain=False)
2058 2072
2059 2073 return mdiff.diffopts(**buildopts)
2060 2074
2061 2075 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2062 2076 losedatafn=None, prefix='', relroot=''):
2063 2077 '''yields diff of changes to files between two nodes, or node and
2064 2078 working directory.
2065 2079
2066 2080 if node1 is None, use first dirstate parent instead.
2067 2081 if node2 is None, compare node1 with working directory.
2068 2082
2069 2083 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2070 2084 every time some change cannot be represented with the current
2071 2085 patch format. Return False to upgrade to git patch format, True to
2072 2086 accept the loss or raise an exception to abort the diff. It is
2073 2087 called with the name of current file being diffed as 'fn'. If set
2074 2088 to None, patches will always be upgraded to git format when
2075 2089 necessary.
2076 2090
2077 2091 prefix is a filename prefix that is prepended to all filenames on
2078 2092 display (used for subrepos).
2079 2093
2080 2094 relroot, if not empty, must be normalized with a trailing /. Any match
2081 2095 patterns that fall outside it will be ignored.'''
2082 2096
2083 2097 if opts is None:
2084 2098 opts = mdiff.defaultopts
2085 2099
2086 2100 if not node1 and not node2:
2087 2101 node1 = repo.dirstate.p1()
2088 2102
2089 2103 def lrugetfilectx():
2090 2104 cache = {}
2091 2105 order = util.deque()
2092 2106 def getfilectx(f, ctx):
2093 2107 fctx = ctx.filectx(f, filelog=cache.get(f))
2094 2108 if f not in cache:
2095 2109 if len(cache) > 20:
2096 2110 del cache[order.popleft()]
2097 2111 cache[f] = fctx.filelog()
2098 2112 else:
2099 2113 order.remove(f)
2100 2114 order.append(f)
2101 2115 return fctx
2102 2116 return getfilectx
2103 2117 getfilectx = lrugetfilectx()
2104 2118
2105 2119 ctx1 = repo[node1]
2106 2120 ctx2 = repo[node2]
2107 2121
2108 2122 relfiltered = False
2109 2123 if relroot != '' and match.always():
2110 2124 # as a special case, create a new matcher with just the relroot
2111 2125 pats = [relroot]
2112 2126 match = scmutil.match(ctx2, pats, default='path')
2113 2127 relfiltered = True
2114 2128
2115 2129 if not changes:
2116 2130 changes = repo.status(ctx1, ctx2, match=match)
2117 2131 modified, added, removed = changes[:3]
2118 2132
2119 2133 if not modified and not added and not removed:
2120 2134 return []
2121 2135
2122 2136 if repo.ui.debugflag:
2123 2137 hexfunc = hex
2124 2138 else:
2125 2139 hexfunc = short
2126 2140 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2127 2141
2128 2142 copy = {}
2129 2143 if opts.git or opts.upgrade:
2130 2144 copy = copies.pathcopies(ctx1, ctx2, match=match)
2131 2145
2132 2146 if relroot is not None:
2133 2147 if not relfiltered:
2134 2148 # XXX this would ideally be done in the matcher, but that is
2135 2149 # generally meant to 'or' patterns, not 'and' them. In this case we
2136 2150 # need to 'and' all the patterns from the matcher with relroot.
2137 2151 def filterrel(l):
2138 2152 return [f for f in l if f.startswith(relroot)]
2139 2153 modified = filterrel(modified)
2140 2154 added = filterrel(added)
2141 2155 removed = filterrel(removed)
2142 2156 relfiltered = True
2143 2157 # filter out copies where either side isn't inside the relative root
2144 2158 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2145 2159 if dst.startswith(relroot)
2146 2160 and src.startswith(relroot)))
2147 2161
2148 2162 def difffn(opts, losedata):
2149 2163 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2150 2164 copy, getfilectx, opts, losedata, prefix, relroot)
2151 2165 if opts.upgrade and not opts.git:
2152 2166 try:
2153 2167 def losedata(fn):
2154 2168 if not losedatafn or not losedatafn(fn=fn):
2155 2169 raise GitDiffRequired
2156 2170 # Buffer the whole output until we are sure it can be generated
2157 2171 return list(difffn(opts.copy(git=False), losedata))
2158 2172 except GitDiffRequired:
2159 2173 return difffn(opts.copy(git=True), None)
2160 2174 else:
2161 2175 return difffn(opts, None)
2162 2176
2163 2177 def difflabel(func, *args, **kw):
2164 2178 '''yields 2-tuples of (output, label) based on the output of func()'''
2165 2179 headprefixes = [('diff', 'diff.diffline'),
2166 2180 ('copy', 'diff.extended'),
2167 2181 ('rename', 'diff.extended'),
2168 2182 ('old', 'diff.extended'),
2169 2183 ('new', 'diff.extended'),
2170 2184 ('deleted', 'diff.extended'),
2171 2185 ('---', 'diff.file_a'),
2172 2186 ('+++', 'diff.file_b')]
2173 2187 textprefixes = [('@', 'diff.hunk'),
2174 2188 ('-', 'diff.deleted'),
2175 2189 ('+', 'diff.inserted')]
2176 2190 head = False
2177 2191 for chunk in func(*args, **kw):
2178 2192 lines = chunk.split('\n')
2179 2193 for i, line in enumerate(lines):
2180 2194 if i != 0:
2181 2195 yield ('\n', '')
2182 2196 if head:
2183 2197 if line.startswith('@'):
2184 2198 head = False
2185 2199 else:
2186 2200 if line and line[0] not in ' +-@\\':
2187 2201 head = True
2188 2202 stripline = line
2189 2203 diffline = False
2190 2204 if not head and line and line[0] in '+-':
2191 2205 # highlight tabs and trailing whitespace, but only in
2192 2206 # changed lines
2193 2207 stripline = line.rstrip()
2194 2208 diffline = True
2195 2209
2196 2210 prefixes = textprefixes
2197 2211 if head:
2198 2212 prefixes = headprefixes
2199 2213 for prefix, label in prefixes:
2200 2214 if stripline.startswith(prefix):
2201 2215 if diffline:
2202 2216 for token in tabsplitter.findall(stripline):
2203 2217 if '\t' == token[0]:
2204 2218 yield (token, 'diff.tab')
2205 2219 else:
2206 2220 yield (token, label)
2207 2221 else:
2208 2222 yield (stripline, label)
2209 2223 break
2210 2224 else:
2211 2225 yield (line, '')
2212 2226 if line != stripline:
2213 2227 yield (line[len(stripline):], 'diff.trailingwhitespace')
2214 2228
2215 2229 def diffui(*args, **kw):
2216 2230 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2217 2231 return difflabel(diff, *args, **kw)
2218 2232
2219 2233 def _filepairs(ctx1, modified, added, removed, copy, opts):
2220 2234 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2221 2235 before and f2 is the the name after. For added files, f1 will be None,
2222 2236 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2223 2237 or 'rename' (the latter two only if opts.git is set).'''
2224 2238 gone = set()
2225 2239
2226 2240 copyto = dict([(v, k) for k, v in copy.items()])
2227 2241
2228 2242 addedset, removedset = set(added), set(removed)
2229 2243 # Fix up added, since merged-in additions appear as
2230 2244 # modifications during merges
2231 2245 for f in modified:
2232 2246 if f not in ctx1:
2233 2247 addedset.add(f)
2234 2248
2235 2249 for f in sorted(modified + added + removed):
2236 2250 copyop = None
2237 2251 f1, f2 = f, f
2238 2252 if f in addedset:
2239 2253 f1 = None
2240 2254 if f in copy:
2241 2255 if opts.git:
2242 2256 f1 = copy[f]
2243 2257 if f1 in removedset and f1 not in gone:
2244 2258 copyop = 'rename'
2245 2259 gone.add(f1)
2246 2260 else:
2247 2261 copyop = 'copy'
2248 2262 elif f in removedset:
2249 2263 f2 = None
2250 2264 if opts.git:
2251 2265 # have we already reported a copy above?
2252 2266 if (f in copyto and copyto[f] in addedset
2253 2267 and copy[copyto[f]] == f):
2254 2268 continue
2255 2269 yield f1, f2, copyop
2256 2270
2257 2271 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2258 2272 copy, getfilectx, opts, losedatafn, prefix, relroot):
2259 2273 '''given input data, generate a diff and yield it in blocks
2260 2274
2261 2275 If generating a diff would lose data like flags or binary data and
2262 2276 losedatafn is not None, it will be called.
2263 2277
2264 2278 relroot is removed and prefix is added to every path in the diff output.
2265 2279
2266 2280 If relroot is not empty, this function expects every path in modified,
2267 2281 added, removed and copy to start with it.'''
2268 2282
2269 2283 def gitindex(text):
2270 2284 if not text:
2271 2285 text = ""
2272 2286 l = len(text)
2273 2287 s = util.sha1('blob %d\0' % l)
2274 2288 s.update(text)
2275 2289 return s.hexdigest()
2276 2290
2277 2291 if opts.noprefix:
2278 2292 aprefix = bprefix = ''
2279 2293 else:
2280 2294 aprefix = 'a/'
2281 2295 bprefix = 'b/'
2282 2296
2283 2297 def diffline(f, revs):
2284 2298 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2285 2299 return 'diff %s %s' % (revinfo, f)
2286 2300
2287 2301 date1 = util.datestr(ctx1.date())
2288 2302 date2 = util.datestr(ctx2.date())
2289 2303
2290 2304 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2291 2305
2292 2306 if relroot != '' and (repo.ui.configbool('devel', 'all')
2293 2307 or repo.ui.configbool('devel', 'check-relroot')):
2294 2308 for f in modified + added + removed + copy.keys() + copy.values():
2295 2309 if f is not None and not f.startswith(relroot):
2296 2310 raise AssertionError(
2297 2311 "file %s doesn't start with relroot %s" % (f, relroot))
2298 2312
2299 2313 for f1, f2, copyop in _filepairs(
2300 2314 ctx1, modified, added, removed, copy, opts):
2301 2315 content1 = None
2302 2316 content2 = None
2303 2317 flag1 = None
2304 2318 flag2 = None
2305 2319 if f1:
2306 2320 content1 = getfilectx(f1, ctx1).data()
2307 2321 if opts.git or losedatafn:
2308 2322 flag1 = ctx1.flags(f1)
2309 2323 if f2:
2310 2324 content2 = getfilectx(f2, ctx2).data()
2311 2325 if opts.git or losedatafn:
2312 2326 flag2 = ctx2.flags(f2)
2313 2327 binary = False
2314 2328 if opts.git or losedatafn:
2315 2329 binary = util.binary(content1) or util.binary(content2)
2316 2330
2317 2331 if losedatafn and not opts.git:
2318 2332 if (binary or
2319 2333 # copy/rename
2320 2334 f2 in copy or
2321 2335 # empty file creation
2322 2336 (not f1 and not content2) or
2323 2337 # empty file deletion
2324 2338 (not content1 and not f2) or
2325 2339 # create with flags
2326 2340 (not f1 and flag2) or
2327 2341 # change flags
2328 2342 (f1 and f2 and flag1 != flag2)):
2329 2343 losedatafn(f2 or f1)
2330 2344
2331 2345 path1 = f1 or f2
2332 2346 path2 = f2 or f1
2333 2347 path1 = posixpath.join(prefix, path1[len(relroot):])
2334 2348 path2 = posixpath.join(prefix, path2[len(relroot):])
2335 2349 header = []
2336 2350 if opts.git:
2337 2351 header.append('diff --git %s%s %s%s' %
2338 2352 (aprefix, path1, bprefix, path2))
2339 2353 if not f1: # added
2340 2354 header.append('new file mode %s' % gitmode[flag2])
2341 2355 elif not f2: # removed
2342 2356 header.append('deleted file mode %s' % gitmode[flag1])
2343 2357 else: # modified/copied/renamed
2344 2358 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2345 2359 if mode1 != mode2:
2346 2360 header.append('old mode %s' % mode1)
2347 2361 header.append('new mode %s' % mode2)
2348 2362 if copyop is not None:
2349 2363 header.append('%s from %s' % (copyop, path1))
2350 2364 header.append('%s to %s' % (copyop, path2))
2351 2365 elif revs and not repo.ui.quiet:
2352 2366 header.append(diffline(path1, revs))
2353 2367
2354 2368 if binary and opts.git and not opts.nobinary:
2355 2369 text = mdiff.b85diff(content1, content2)
2356 2370 if text:
2357 2371 header.append('index %s..%s' %
2358 2372 (gitindex(content1), gitindex(content2)))
2359 2373 else:
2360 2374 text = mdiff.unidiff(content1, date1,
2361 2375 content2, date2,
2362 2376 path1, path2, opts=opts)
2363 2377 if header and (text or len(header) > 1):
2364 2378 yield '\n'.join(header) + '\n'
2365 2379 if text:
2366 2380 yield text
2367 2381
2368 2382 def diffstatsum(stats):
2369 2383 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2370 2384 for f, a, r, b in stats:
2371 2385 maxfile = max(maxfile, encoding.colwidth(f))
2372 2386 maxtotal = max(maxtotal, a + r)
2373 2387 addtotal += a
2374 2388 removetotal += r
2375 2389 binary = binary or b
2376 2390
2377 2391 return maxfile, maxtotal, addtotal, removetotal, binary
2378 2392
2379 2393 def diffstatdata(lines):
2380 2394 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2381 2395
2382 2396 results = []
2383 2397 filename, adds, removes, isbinary = None, 0, 0, False
2384 2398
2385 2399 def addresult():
2386 2400 if filename:
2387 2401 results.append((filename, adds, removes, isbinary))
2388 2402
2389 2403 for line in lines:
2390 2404 if line.startswith('diff'):
2391 2405 addresult()
2392 2406 # set numbers to 0 anyway when starting new file
2393 2407 adds, removes, isbinary = 0, 0, False
2394 2408 if line.startswith('diff --git a/'):
2395 2409 filename = gitre.search(line).group(2)
2396 2410 elif line.startswith('diff -r'):
2397 2411 # format: "diff -r ... -r ... filename"
2398 2412 filename = diffre.search(line).group(1)
2399 2413 elif line.startswith('+') and not line.startswith('+++ '):
2400 2414 adds += 1
2401 2415 elif line.startswith('-') and not line.startswith('--- '):
2402 2416 removes += 1
2403 2417 elif (line.startswith('GIT binary patch') or
2404 2418 line.startswith('Binary file')):
2405 2419 isbinary = True
2406 2420 addresult()
2407 2421 return results
2408 2422
2409 2423 def diffstat(lines, width=80, git=False):
2410 2424 output = []
2411 2425 stats = diffstatdata(lines)
2412 2426 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2413 2427
2414 2428 countwidth = len(str(maxtotal))
2415 2429 if hasbinary and countwidth < 3:
2416 2430 countwidth = 3
2417 2431 graphwidth = width - countwidth - maxname - 6
2418 2432 if graphwidth < 10:
2419 2433 graphwidth = 10
2420 2434
2421 2435 def scale(i):
2422 2436 if maxtotal <= graphwidth:
2423 2437 return i
2424 2438 # If diffstat runs out of room it doesn't print anything,
2425 2439 # which isn't very useful, so always print at least one + or -
2426 2440 # if there were at least some changes.
2427 2441 return max(i * graphwidth // maxtotal, int(bool(i)))
2428 2442
2429 2443 for filename, adds, removes, isbinary in stats:
2430 2444 if isbinary:
2431 2445 count = 'Bin'
2432 2446 else:
2433 2447 count = adds + removes
2434 2448 pluses = '+' * scale(adds)
2435 2449 minuses = '-' * scale(removes)
2436 2450 output.append(' %s%s | %*s %s%s\n' %
2437 2451 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2438 2452 countwidth, count, pluses, minuses))
2439 2453
2440 2454 if stats:
2441 2455 output.append(_(' %d files changed, %d insertions(+), '
2442 2456 '%d deletions(-)\n')
2443 2457 % (len(stats), totaladds, totalremoves))
2444 2458
2445 2459 return ''.join(output)
2446 2460
2447 2461 def diffstatui(*args, **kw):
2448 2462 '''like diffstat(), but yields 2-tuples of (output, label) for
2449 2463 ui.write()
2450 2464 '''
2451 2465
2452 2466 for line in diffstat(*args, **kw).splitlines():
2453 2467 if line and line[-1] in '+-':
2454 2468 name, graph = line.rsplit(' ', 1)
2455 2469 yield (name + ' ', '')
2456 2470 m = re.search(r'\++', graph)
2457 2471 if m:
2458 2472 yield (m.group(0), 'diffstat.inserted')
2459 2473 m = re.search(r'-+', graph)
2460 2474 if m:
2461 2475 yield (m.group(0), 'diffstat.deleted')
2462 2476 else:
2463 2477 yield (line, '')
2464 2478 yield ('\n', '')
@@ -1,1409 +1,1465 b''
1 1 Set up a repo
2 2
3 3 $ cat <<EOF >> $HGRCPATH
4 4 > [ui]
5 5 > interactive = true
6 6 > [extensions]
7 7 > record =
8 8 > EOF
9 9
10 10 $ hg init a
11 11 $ cd a
12 12
13 13 Select no files
14 14
15 15 $ touch empty-rw
16 16 $ hg add empty-rw
17 17
18 18 $ hg commit -i empty-rw<<EOF
19 19 > n
20 20 > EOF
21 21 diff --git a/empty-rw b/empty-rw
22 22 new file mode 100644
23 23 examine changes to 'empty-rw'? [Ynesfdaq?] n
24 24
25 25 no changes to record
26 26
27 27 $ hg tip -p
28 28 changeset: -1:000000000000
29 29 tag: tip
30 30 user:
31 31 date: Thu Jan 01 00:00:00 1970 +0000
32 32
33 33
34 34
35 35 Select files but no hunks
36 36
37 37 $ hg commit -i empty-rw<<EOF
38 38 > y
39 39 > n
40 40 > EOF
41 41 diff --git a/empty-rw b/empty-rw
42 42 new file mode 100644
43 43 examine changes to 'empty-rw'? [Ynesfdaq?] y
44 44
45 45 abort: empty commit message
46 46 [255]
47 47
48 48 $ hg tip -p
49 49 changeset: -1:000000000000
50 50 tag: tip
51 51 user:
52 52 date: Thu Jan 01 00:00:00 1970 +0000
53 53
54 54
55 55
56 56 Record empty file
57 57
58 58 $ hg commit -i -d '0 0' -m empty empty-rw<<EOF
59 59 > y
60 60 > y
61 61 > EOF
62 62 diff --git a/empty-rw b/empty-rw
63 63 new file mode 100644
64 64 examine changes to 'empty-rw'? [Ynesfdaq?] y
65 65
66 66
67 67 $ hg tip -p
68 68 changeset: 0:c0708cf4e46e
69 69 tag: tip
70 70 user: test
71 71 date: Thu Jan 01 00:00:00 1970 +0000
72 72 summary: empty
73 73
74 74
75 75
76 76 Summary shows we updated to the new cset
77 77
78 78 $ hg summary
79 79 parent: 0:c0708cf4e46e tip
80 80 empty
81 81 branch: default
82 82 commit: (clean)
83 83 update: (current)
84 84
85 85 Rename empty file
86 86
87 87 $ hg mv empty-rw empty-rename
88 88 $ hg commit -i -d '1 0' -m rename<<EOF
89 89 > y
90 90 > EOF
91 91 diff --git a/empty-rw b/empty-rename
92 92 rename from empty-rw
93 93 rename to empty-rename
94 94 examine changes to 'empty-rw' and 'empty-rename'? [Ynesfdaq?] y
95 95
96 96
97 97 $ hg tip -p
98 98 changeset: 1:d695e8dcb197
99 99 tag: tip
100 100 user: test
101 101 date: Thu Jan 01 00:00:01 1970 +0000
102 102 summary: rename
103 103
104 104
105 105
106 106 Copy empty file
107 107
108 108 $ hg cp empty-rename empty-copy
109 109 $ hg commit -i -d '2 0' -m copy<<EOF
110 110 > y
111 111 > EOF
112 112 diff --git a/empty-rename b/empty-copy
113 113 copy from empty-rename
114 114 copy to empty-copy
115 115 examine changes to 'empty-rename' and 'empty-copy'? [Ynesfdaq?] y
116 116
117 117
118 118 $ hg tip -p
119 119 changeset: 2:1d4b90bea524
120 120 tag: tip
121 121 user: test
122 122 date: Thu Jan 01 00:00:02 1970 +0000
123 123 summary: copy
124 124
125 125
126 126
127 127 Delete empty file
128 128
129 129 $ hg rm empty-copy
130 130 $ hg commit -i -d '3 0' -m delete<<EOF
131 131 > y
132 132 > EOF
133 133 diff --git a/empty-copy b/empty-copy
134 134 deleted file mode 100644
135 135 examine changes to 'empty-copy'? [Ynesfdaq?] y
136 136
137 137
138 138 $ hg tip -p
139 139 changeset: 3:b39a238f01a1
140 140 tag: tip
141 141 user: test
142 142 date: Thu Jan 01 00:00:03 1970 +0000
143 143 summary: delete
144 144
145 145
146 146
147 147 Add binary file
148 148
149 149 $ hg bundle --base -2 tip.bundle
150 150 1 changesets found
151 151 $ hg add tip.bundle
152 152 $ hg commit -i -d '4 0' -m binary<<EOF
153 153 > y
154 154 > EOF
155 155 diff --git a/tip.bundle b/tip.bundle
156 156 new file mode 100644
157 157 this is a binary file
158 158 examine changes to 'tip.bundle'? [Ynesfdaq?] y
159 159
160 160
161 161 $ hg tip -p
162 162 changeset: 4:ad816da3711e
163 163 tag: tip
164 164 user: test
165 165 date: Thu Jan 01 00:00:04 1970 +0000
166 166 summary: binary
167 167
168 168 diff -r b39a238f01a1 -r ad816da3711e tip.bundle
169 169 Binary file tip.bundle has changed
170 170
171 171
172 172 Change binary file
173 173
174 174 $ hg bundle --base -2 tip.bundle
175 175 1 changesets found
176 176 $ hg commit -i -d '5 0' -m binary-change<<EOF
177 177 > y
178 178 > EOF
179 179 diff --git a/tip.bundle b/tip.bundle
180 180 this modifies a binary file (all or nothing)
181 181 examine changes to 'tip.bundle'? [Ynesfdaq?] y
182 182
183 183
184 184 $ hg tip -p
185 185 changeset: 5:dccd6f3eb485
186 186 tag: tip
187 187 user: test
188 188 date: Thu Jan 01 00:00:05 1970 +0000
189 189 summary: binary-change
190 190
191 191 diff -r ad816da3711e -r dccd6f3eb485 tip.bundle
192 192 Binary file tip.bundle has changed
193 193
194 194
195 195 Rename and change binary file
196 196
197 197 $ hg mv tip.bundle top.bundle
198 198 $ hg bundle --base -2 top.bundle
199 199 1 changesets found
200 200 $ hg commit -i -d '6 0' -m binary-change-rename<<EOF
201 201 > y
202 202 > EOF
203 203 diff --git a/tip.bundle b/top.bundle
204 204 rename from tip.bundle
205 205 rename to top.bundle
206 206 this modifies a binary file (all or nothing)
207 207 examine changes to 'tip.bundle' and 'top.bundle'? [Ynesfdaq?] y
208 208
209 209
210 210 $ hg tip -p
211 211 changeset: 6:7fa44105f5b3
212 212 tag: tip
213 213 user: test
214 214 date: Thu Jan 01 00:00:06 1970 +0000
215 215 summary: binary-change-rename
216 216
217 217 diff -r dccd6f3eb485 -r 7fa44105f5b3 tip.bundle
218 218 Binary file tip.bundle has changed
219 219 diff -r dccd6f3eb485 -r 7fa44105f5b3 top.bundle
220 220 Binary file top.bundle has changed
221 221
222 222
223 223 Add plain file
224 224
225 225 $ for i in 1 2 3 4 5 6 7 8 9 10; do
226 226 > echo $i >> plain
227 227 > done
228 228
229 229 $ hg add plain
230 230 $ hg commit -i -d '7 0' -m plain plain<<EOF
231 231 > y
232 > y
232 233 > EOF
233 234 diff --git a/plain b/plain
234 235 new file mode 100644
235 236 examine changes to 'plain'? [Ynesfdaq?] y
236 237
238 @@ -0,0 +1,10 @@
239 +1
240 +2
241 +3
242 +4
243 +5
244 +6
245 +7
246 +8
247 +9
248 +10
249 record this change to 'plain'? [Ynesfdaq?] y
250
237 251 $ hg tip -p
238 252 changeset: 7:11fb457c1be4
239 253 tag: tip
240 254 user: test
241 255 date: Thu Jan 01 00:00:07 1970 +0000
242 256 summary: plain
243 257
244 258 diff -r 7fa44105f5b3 -r 11fb457c1be4 plain
245 259 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
246 260 +++ b/plain Thu Jan 01 00:00:07 1970 +0000
247 261 @@ -0,0 +1,10 @@
248 262 +1
249 263 +2
250 264 +3
251 265 +4
252 266 +5
253 267 +6
254 268 +7
255 269 +8
256 270 +9
257 271 +10
258 272
259 273 Modify end of plain file with username unset
260 274
261 275 $ echo 11 >> plain
262 276 $ unset HGUSER
263 277 $ hg commit -i --config ui.username= -d '8 0' -m end plain
264 278 abort: no username supplied
265 279 (use "hg config --edit" to set your username)
266 280 [255]
267 281
268 282
269 283 Modify end of plain file, also test that diffopts are accounted for
270 284
271 285 $ HGUSER="test"
272 286 $ export HGUSER
273 287 $ hg commit -i --config diff.showfunc=true -d '8 0' -m end plain <<EOF
274 288 > y
275 289 > y
276 290 > EOF
277 291 diff --git a/plain b/plain
278 292 1 hunks, 1 lines changed
279 293 examine changes to 'plain'? [Ynesfdaq?] y
280 294
281 295 @@ -8,3 +8,4 @@ 7
282 296 8
283 297 9
284 298 10
285 299 +11
286 300 record this change to 'plain'? [Ynesfdaq?] y
287 301
288 302
289 303 Modify end of plain file, no EOL
290 304
291 305 $ hg tip --template '{node}' >> plain
292 306 $ hg commit -i -d '9 0' -m noeol plain <<EOF
293 307 > y
294 308 > y
295 309 > EOF
296 310 diff --git a/plain b/plain
297 311 1 hunks, 1 lines changed
298 312 examine changes to 'plain'? [Ynesfdaq?] y
299 313
300 314 @@ -9,3 +9,4 @@
301 315 9
302 316 10
303 317 11
304 318 +7264f99c5f5ff3261504828afa4fb4d406c3af54
305 319 \ No newline at end of file
306 320 record this change to 'plain'? [Ynesfdaq?] y
307 321
308 322
309 323 Modify end of plain file, add EOL
310 324
311 325 $ echo >> plain
312 326 $ echo 1 > plain2
313 327 $ hg add plain2
314 328 $ hg commit -i -d '10 0' -m eol plain plain2 <<EOF
315 329 > y
316 330 > y
317 331 > y
332 > y
318 333 > EOF
319 334 diff --git a/plain b/plain
320 335 1 hunks, 1 lines changed
321 336 examine changes to 'plain'? [Ynesfdaq?] y
322 337
323 338 @@ -9,4 +9,4 @@
324 339 9
325 340 10
326 341 11
327 342 -7264f99c5f5ff3261504828afa4fb4d406c3af54
328 343 \ No newline at end of file
329 344 +7264f99c5f5ff3261504828afa4fb4d406c3af54
330 345 record change 1/2 to 'plain'? [Ynesfdaq?] y
331 346
332 347 diff --git a/plain2 b/plain2
333 348 new file mode 100644
334 349 examine changes to 'plain2'? [Ynesfdaq?] y
335 350
351 @@ -0,0 +1,1 @@
352 +1
353 record change 2/2 to 'plain2'? [Ynesfdaq?] y
354
336 355 Modify beginning, trim end, record both, add another file to test
337 356 changes numbering
338 357
339 358 $ rm plain
340 359 $ for i in 2 2 3 4 5 6 7 8 9 10; do
341 360 > echo $i >> plain
342 361 > done
343 362 $ echo 2 >> plain2
344 363
345 364 $ hg commit -i -d '10 0' -m begin-and-end plain plain2 <<EOF
346 365 > y
347 366 > y
348 367 > y
349 368 > y
350 369 > y
351 370 > EOF
352 371 diff --git a/plain b/plain
353 372 2 hunks, 3 lines changed
354 373 examine changes to 'plain'? [Ynesfdaq?] y
355 374
356 375 @@ -1,4 +1,4 @@
357 376 -1
358 377 +2
359 378 2
360 379 3
361 380 4
362 381 record change 1/3 to 'plain'? [Ynesfdaq?] y
363 382
364 383 @@ -8,5 +8,3 @@
365 384 8
366 385 9
367 386 10
368 387 -11
369 388 -7264f99c5f5ff3261504828afa4fb4d406c3af54
370 389 record change 2/3 to 'plain'? [Ynesfdaq?] y
371 390
372 391 diff --git a/plain2 b/plain2
373 392 1 hunks, 1 lines changed
374 393 examine changes to 'plain2'? [Ynesfdaq?] y
375 394
376 395 @@ -1,1 +1,2 @@
377 396 1
378 397 +2
379 398 record change 3/3 to 'plain2'? [Ynesfdaq?] y
380 399
381 400
382 401 $ hg tip -p
383 402 changeset: 11:21df83db12b8
384 403 tag: tip
385 404 user: test
386 405 date: Thu Jan 01 00:00:10 1970 +0000
387 406 summary: begin-and-end
388 407
389 408 diff -r ddb8b281c3ff -r 21df83db12b8 plain
390 409 --- a/plain Thu Jan 01 00:00:10 1970 +0000
391 410 +++ b/plain Thu Jan 01 00:00:10 1970 +0000
392 411 @@ -1,4 +1,4 @@
393 412 -1
394 413 +2
395 414 2
396 415 3
397 416 4
398 417 @@ -8,5 +8,3 @@
399 418 8
400 419 9
401 420 10
402 421 -11
403 422 -7264f99c5f5ff3261504828afa4fb4d406c3af54
404 423 diff -r ddb8b281c3ff -r 21df83db12b8 plain2
405 424 --- a/plain2 Thu Jan 01 00:00:10 1970 +0000
406 425 +++ b/plain2 Thu Jan 01 00:00:10 1970 +0000
407 426 @@ -1,1 +1,2 @@
408 427 1
409 428 +2
410 429
411 430
412 431 Trim beginning, modify end
413 432
414 433 $ rm plain
415 434 > for i in 4 5 6 7 8 9 10.new; do
416 435 > echo $i >> plain
417 436 > done
418 437
419 438 Record end
420 439
421 440 $ hg commit -i -d '11 0' -m end-only plain <<EOF
422 441 > y
423 442 > n
424 443 > y
425 444 > EOF
426 445 diff --git a/plain b/plain
427 446 2 hunks, 4 lines changed
428 447 examine changes to 'plain'? [Ynesfdaq?] y
429 448
430 449 @@ -1,9 +1,6 @@
431 450 -2
432 451 -2
433 452 -3
434 453 4
435 454 5
436 455 6
437 456 7
438 457 8
439 458 9
440 459 record change 1/2 to 'plain'? [Ynesfdaq?] n
441 460
442 461 @@ -4,7 +1,7 @@
443 462 4
444 463 5
445 464 6
446 465 7
447 466 8
448 467 9
449 468 -10
450 469 +10.new
451 470 record change 2/2 to 'plain'? [Ynesfdaq?] y
452 471
453 472
454 473 $ hg tip -p
455 474 changeset: 12:99337501826f
456 475 tag: tip
457 476 user: test
458 477 date: Thu Jan 01 00:00:11 1970 +0000
459 478 summary: end-only
460 479
461 480 diff -r 21df83db12b8 -r 99337501826f plain
462 481 --- a/plain Thu Jan 01 00:00:10 1970 +0000
463 482 +++ b/plain Thu Jan 01 00:00:11 1970 +0000
464 483 @@ -7,4 +7,4 @@
465 484 7
466 485 8
467 486 9
468 487 -10
469 488 +10.new
470 489
471 490
472 491 Record beginning
473 492
474 493 $ hg commit -i -d '12 0' -m begin-only plain <<EOF
475 494 > y
476 495 > y
477 496 > EOF
478 497 diff --git a/plain b/plain
479 498 1 hunks, 3 lines changed
480 499 examine changes to 'plain'? [Ynesfdaq?] y
481 500
482 501 @@ -1,6 +1,3 @@
483 502 -2
484 503 -2
485 504 -3
486 505 4
487 506 5
488 507 6
489 508 record this change to 'plain'? [Ynesfdaq?] y
490 509
491 510
492 511 $ hg tip -p
493 512 changeset: 13:bbd45465d540
494 513 tag: tip
495 514 user: test
496 515 date: Thu Jan 01 00:00:12 1970 +0000
497 516 summary: begin-only
498 517
499 518 diff -r 99337501826f -r bbd45465d540 plain
500 519 --- a/plain Thu Jan 01 00:00:11 1970 +0000
501 520 +++ b/plain Thu Jan 01 00:00:12 1970 +0000
502 521 @@ -1,6 +1,3 @@
503 522 -2
504 523 -2
505 524 -3
506 525 4
507 526 5
508 527 6
509 528
510 529
511 530 Add to beginning, trim from end
512 531
513 532 $ rm plain
514 533 $ for i in 1 2 3 4 5 6 7 8 9; do
515 534 > echo $i >> plain
516 535 > done
517 536
518 537 Record end
519 538
520 539 $ hg commit -i --traceback -d '13 0' -m end-again plain<<EOF
521 540 > y
522 541 > n
523 542 > y
524 543 > EOF
525 544 diff --git a/plain b/plain
526 545 2 hunks, 4 lines changed
527 546 examine changes to 'plain'? [Ynesfdaq?] y
528 547
529 548 @@ -1,6 +1,9 @@
530 549 +1
531 550 +2
532 551 +3
533 552 4
534 553 5
535 554 6
536 555 7
537 556 8
538 557 9
539 558 record change 1/2 to 'plain'? [Ynesfdaq?] n
540 559
541 560 @@ -1,7 +4,6 @@
542 561 4
543 562 5
544 563 6
545 564 7
546 565 8
547 566 9
548 567 -10.new
549 568 record change 2/2 to 'plain'? [Ynesfdaq?] y
550 569
551 570
552 571 Add to beginning, middle, end
553 572
554 573 $ rm plain
555 574 $ for i in 1 2 3 4 5 5.new 5.reallynew 6 7 8 9 10 11; do
556 575 > echo $i >> plain
557 576 > done
558 577
559 578 Record beginning, middle, and test that format-breaking diffopts are ignored
560 579
561 580 $ hg commit -i --config diff.noprefix=True -d '14 0' -m middle-only plain <<EOF
562 581 > y
563 582 > y
564 583 > y
565 584 > n
566 585 > EOF
567 586 diff --git a/plain b/plain
568 587 3 hunks, 7 lines changed
569 588 examine changes to 'plain'? [Ynesfdaq?] y
570 589
571 590 @@ -1,2 +1,5 @@
572 591 +1
573 592 +2
574 593 +3
575 594 4
576 595 5
577 596 record change 1/3 to 'plain'? [Ynesfdaq?] y
578 597
579 598 @@ -1,6 +4,8 @@
580 599 4
581 600 5
582 601 +5.new
583 602 +5.reallynew
584 603 6
585 604 7
586 605 8
587 606 9
588 607 record change 2/3 to 'plain'? [Ynesfdaq?] y
589 608
590 609 @@ -3,4 +8,6 @@
591 610 6
592 611 7
593 612 8
594 613 9
595 614 +10
596 615 +11
597 616 record change 3/3 to 'plain'? [Ynesfdaq?] n
598 617
599 618
600 619 $ hg tip -p
601 620 changeset: 15:f34a7937ec33
602 621 tag: tip
603 622 user: test
604 623 date: Thu Jan 01 00:00:14 1970 +0000
605 624 summary: middle-only
606 625
607 626 diff -r 82c065d0b850 -r f34a7937ec33 plain
608 627 --- a/plain Thu Jan 01 00:00:13 1970 +0000
609 628 +++ b/plain Thu Jan 01 00:00:14 1970 +0000
610 629 @@ -1,5 +1,10 @@
611 630 +1
612 631 +2
613 632 +3
614 633 4
615 634 5
616 635 +5.new
617 636 +5.reallynew
618 637 6
619 638 7
620 639 8
621 640
622 641
623 642 Record end
624 643
625 644 $ hg commit -i -d '15 0' -m end-only plain <<EOF
626 645 > y
627 646 > y
628 647 > EOF
629 648 diff --git a/plain b/plain
630 649 1 hunks, 2 lines changed
631 650 examine changes to 'plain'? [Ynesfdaq?] y
632 651
633 652 @@ -9,3 +9,5 @@
634 653 7
635 654 8
636 655 9
637 656 +10
638 657 +11
639 658 record this change to 'plain'? [Ynesfdaq?] y
640 659
641 660
642 661 $ hg tip -p
643 662 changeset: 16:f9900b71a04c
644 663 tag: tip
645 664 user: test
646 665 date: Thu Jan 01 00:00:15 1970 +0000
647 666 summary: end-only
648 667
649 668 diff -r f34a7937ec33 -r f9900b71a04c plain
650 669 --- a/plain Thu Jan 01 00:00:14 1970 +0000
651 670 +++ b/plain Thu Jan 01 00:00:15 1970 +0000
652 671 @@ -9,3 +9,5 @@
653 672 7
654 673 8
655 674 9
656 675 +10
657 676 +11
658 677
659 678
660 679 $ mkdir subdir
661 680 $ cd subdir
662 681 $ echo a > a
663 682 $ hg ci -d '16 0' -Amsubdir
664 683 adding subdir/a
665 684
666 685 $ echo a >> a
667 686 $ hg commit -i -d '16 0' -m subdir-change a <<EOF
668 687 > y
669 688 > y
670 689 > EOF
671 690 diff --git a/subdir/a b/subdir/a
672 691 1 hunks, 1 lines changed
673 692 examine changes to 'subdir/a'? [Ynesfdaq?] y
674 693
675 694 @@ -1,1 +1,2 @@
676 695 a
677 696 +a
678 697 record this change to 'subdir/a'? [Ynesfdaq?] y
679 698
680 699
681 700 $ hg tip -p
682 701 changeset: 18:61be427a9deb
683 702 tag: tip
684 703 user: test
685 704 date: Thu Jan 01 00:00:16 1970 +0000
686 705 summary: subdir-change
687 706
688 707 diff -r a7ffae4d61cb -r 61be427a9deb subdir/a
689 708 --- a/subdir/a Thu Jan 01 00:00:16 1970 +0000
690 709 +++ b/subdir/a Thu Jan 01 00:00:16 1970 +0000
691 710 @@ -1,1 +1,2 @@
692 711 a
693 712 +a
694 713
695 714
696 715 $ echo a > f1
697 716 $ echo b > f2
698 717 $ hg add f1 f2
699 718
700 719 $ hg ci -mz -d '17 0'
701 720
702 721 $ echo a >> f1
703 722 $ echo b >> f2
704 723
705 724 Help, quit
706 725
707 726 $ hg commit -i <<EOF
708 727 > ?
709 728 > q
710 729 > EOF
711 730 diff --git a/subdir/f1 b/subdir/f1
712 731 1 hunks, 1 lines changed
713 732 examine changes to 'subdir/f1'? [Ynesfdaq?] ?
714 733
715 734 y - yes, record this change
716 735 n - no, skip this change
717 736 e - edit this change manually
718 737 s - skip remaining changes to this file
719 738 f - record remaining changes to this file
720 739 d - done, skip remaining changes and files
721 740 a - record all changes to all remaining files
722 741 q - quit, recording no changes
723 742 ? - ? (display help)
724 743 examine changes to 'subdir/f1'? [Ynesfdaq?] q
725 744
726 745 abort: user quit
727 746 [255]
728 747
729 748 Skip
730 749
731 750 $ hg commit -i <<EOF
732 751 > s
733 752 > EOF
734 753 diff --git a/subdir/f1 b/subdir/f1
735 754 1 hunks, 1 lines changed
736 755 examine changes to 'subdir/f1'? [Ynesfdaq?] s
737 756
738 757 diff --git a/subdir/f2 b/subdir/f2
739 758 1 hunks, 1 lines changed
740 759 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
741 760 [255]
742 761
743 762 No
744 763
745 764 $ hg commit -i <<EOF
746 765 > n
747 766 > EOF
748 767 diff --git a/subdir/f1 b/subdir/f1
749 768 1 hunks, 1 lines changed
750 769 examine changes to 'subdir/f1'? [Ynesfdaq?] n
751 770
752 771 diff --git a/subdir/f2 b/subdir/f2
753 772 1 hunks, 1 lines changed
754 773 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
755 774 [255]
756 775
757 776 f, quit
758 777
759 778 $ hg commit -i <<EOF
760 779 > f
761 780 > q
762 781 > EOF
763 782 diff --git a/subdir/f1 b/subdir/f1
764 783 1 hunks, 1 lines changed
765 784 examine changes to 'subdir/f1'? [Ynesfdaq?] f
766 785
767 786 diff --git a/subdir/f2 b/subdir/f2
768 787 1 hunks, 1 lines changed
769 788 examine changes to 'subdir/f2'? [Ynesfdaq?] q
770 789
771 790 abort: user quit
772 791 [255]
773 792
774 793 s, all
775 794
776 795 $ hg commit -i -d '18 0' -mx <<EOF
777 796 > s
778 797 > a
779 798 > EOF
780 799 diff --git a/subdir/f1 b/subdir/f1
781 800 1 hunks, 1 lines changed
782 801 examine changes to 'subdir/f1'? [Ynesfdaq?] s
783 802
784 803 diff --git a/subdir/f2 b/subdir/f2
785 804 1 hunks, 1 lines changed
786 805 examine changes to 'subdir/f2'? [Ynesfdaq?] a
787 806
788 807
789 808 $ hg tip -p
790 809 changeset: 20:b3df3dda369a
791 810 tag: tip
792 811 user: test
793 812 date: Thu Jan 01 00:00:18 1970 +0000
794 813 summary: x
795 814
796 815 diff -r 6e02d6c9906d -r b3df3dda369a subdir/f2
797 816 --- a/subdir/f2 Thu Jan 01 00:00:17 1970 +0000
798 817 +++ b/subdir/f2 Thu Jan 01 00:00:18 1970 +0000
799 818 @@ -1,1 +1,2 @@
800 819 b
801 820 +b
802 821
803 822
804 823 f
805 824
806 825 $ hg commit -i -d '19 0' -my <<EOF
807 826 > f
808 827 > EOF
809 828 diff --git a/subdir/f1 b/subdir/f1
810 829 1 hunks, 1 lines changed
811 830 examine changes to 'subdir/f1'? [Ynesfdaq?] f
812 831
813 832
814 833 $ hg tip -p
815 834 changeset: 21:38ec577f126b
816 835 tag: tip
817 836 user: test
818 837 date: Thu Jan 01 00:00:19 1970 +0000
819 838 summary: y
820 839
821 840 diff -r b3df3dda369a -r 38ec577f126b subdir/f1
822 841 --- a/subdir/f1 Thu Jan 01 00:00:18 1970 +0000
823 842 +++ b/subdir/f1 Thu Jan 01 00:00:19 1970 +0000
824 843 @@ -1,1 +1,2 @@
825 844 a
826 845 +a
827 846
828 847
829 848 #if execbit
830 849
831 850 Preserve chmod +x
832 851
833 852 $ chmod +x f1
834 853 $ echo a >> f1
835 854 $ hg commit -i -d '20 0' -mz <<EOF
836 855 > y
837 856 > y
838 857 > y
839 858 > EOF
840 859 diff --git a/subdir/f1 b/subdir/f1
841 860 old mode 100644
842 861 new mode 100755
843 862 1 hunks, 1 lines changed
844 863 examine changes to 'subdir/f1'? [Ynesfdaq?] y
845 864
846 865 @@ -1,2 +1,3 @@
847 866 a
848 867 a
849 868 +a
850 869 record this change to 'subdir/f1'? [Ynesfdaq?] y
851 870
852 871
853 872 $ hg tip --config diff.git=True -p
854 873 changeset: 22:3261adceb075
855 874 tag: tip
856 875 user: test
857 876 date: Thu Jan 01 00:00:20 1970 +0000
858 877 summary: z
859 878
860 879 diff --git a/subdir/f1 b/subdir/f1
861 880 old mode 100644
862 881 new mode 100755
863 882 --- a/subdir/f1
864 883 +++ b/subdir/f1
865 884 @@ -1,2 +1,3 @@
866 885 a
867 886 a
868 887 +a
869 888
870 889
871 890 Preserve execute permission on original
872 891
873 892 $ echo b >> f1
874 893 $ hg commit -i -d '21 0' -maa <<EOF
875 894 > y
876 895 > y
877 896 > y
878 897 > EOF
879 898 diff --git a/subdir/f1 b/subdir/f1
880 899 1 hunks, 1 lines changed
881 900 examine changes to 'subdir/f1'? [Ynesfdaq?] y
882 901
883 902 @@ -1,3 +1,4 @@
884 903 a
885 904 a
886 905 a
887 906 +b
888 907 record this change to 'subdir/f1'? [Ynesfdaq?] y
889 908
890 909
891 910 $ hg tip --config diff.git=True -p
892 911 changeset: 23:b429867550db
893 912 tag: tip
894 913 user: test
895 914 date: Thu Jan 01 00:00:21 1970 +0000
896 915 summary: aa
897 916
898 917 diff --git a/subdir/f1 b/subdir/f1
899 918 --- a/subdir/f1
900 919 +++ b/subdir/f1
901 920 @@ -1,3 +1,4 @@
902 921 a
903 922 a
904 923 a
905 924 +b
906 925
907 926
908 927 Preserve chmod -x
909 928
910 929 $ chmod -x f1
911 930 $ echo c >> f1
912 931 $ hg commit -i -d '22 0' -mab <<EOF
913 932 > y
914 933 > y
915 934 > y
916 935 > EOF
917 936 diff --git a/subdir/f1 b/subdir/f1
918 937 old mode 100755
919 938 new mode 100644
920 939 1 hunks, 1 lines changed
921 940 examine changes to 'subdir/f1'? [Ynesfdaq?] y
922 941
923 942 @@ -2,3 +2,4 @@
924 943 a
925 944 a
926 945 b
927 946 +c
928 947 record this change to 'subdir/f1'? [Ynesfdaq?] y
929 948
930 949
931 950 $ hg tip --config diff.git=True -p
932 951 changeset: 24:0b082130c20a
933 952 tag: tip
934 953 user: test
935 954 date: Thu Jan 01 00:00:22 1970 +0000
936 955 summary: ab
937 956
938 957 diff --git a/subdir/f1 b/subdir/f1
939 958 old mode 100755
940 959 new mode 100644
941 960 --- a/subdir/f1
942 961 +++ b/subdir/f1
943 962 @@ -2,3 +2,4 @@
944 963 a
945 964 a
946 965 b
947 966 +c
948 967
949 968
950 969 #else
951 970
952 971 Slightly bogus tests to get almost same repo structure as when x bit is used
953 972 - but with different hashes.
954 973
955 974 Mock "Preserve chmod +x"
956 975
957 976 $ echo a >> f1
958 977 $ hg commit -i -d '20 0' -mz <<EOF
959 978 > y
960 979 > y
961 980 > y
962 981 > EOF
963 982 diff --git a/subdir/f1 b/subdir/f1
964 983 1 hunks, 1 lines changed
965 984 examine changes to 'subdir/f1'? [Ynesfdaq?] y
966 985
967 986 @@ -1,2 +1,3 @@
968 987 a
969 988 a
970 989 +a
971 990 record this change to 'subdir/f1'? [Ynesfdaq?] y
972 991
973 992
974 993 $ hg tip --config diff.git=True -p
975 994 changeset: 22:0d463bd428f5
976 995 tag: tip
977 996 user: test
978 997 date: Thu Jan 01 00:00:20 1970 +0000
979 998 summary: z
980 999
981 1000 diff --git a/subdir/f1 b/subdir/f1
982 1001 --- a/subdir/f1
983 1002 +++ b/subdir/f1
984 1003 @@ -1,2 +1,3 @@
985 1004 a
986 1005 a
987 1006 +a
988 1007
989 1008
990 1009 Mock "Preserve execute permission on original"
991 1010
992 1011 $ echo b >> f1
993 1012 $ hg commit -i -d '21 0' -maa <<EOF
994 1013 > y
995 1014 > y
996 1015 > y
997 1016 > EOF
998 1017 diff --git a/subdir/f1 b/subdir/f1
999 1018 1 hunks, 1 lines changed
1000 1019 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1001 1020
1002 1021 @@ -1,3 +1,4 @@
1003 1022 a
1004 1023 a
1005 1024 a
1006 1025 +b
1007 1026 record this change to 'subdir/f1'? [Ynesfdaq?] y
1008 1027
1009 1028
1010 1029 $ hg tip --config diff.git=True -p
1011 1030 changeset: 23:0eab41a3e524
1012 1031 tag: tip
1013 1032 user: test
1014 1033 date: Thu Jan 01 00:00:21 1970 +0000
1015 1034 summary: aa
1016 1035
1017 1036 diff --git a/subdir/f1 b/subdir/f1
1018 1037 --- a/subdir/f1
1019 1038 +++ b/subdir/f1
1020 1039 @@ -1,3 +1,4 @@
1021 1040 a
1022 1041 a
1023 1042 a
1024 1043 +b
1025 1044
1026 1045
1027 1046 Mock "Preserve chmod -x"
1028 1047
1029 1048 $ chmod -x f1
1030 1049 $ echo c >> f1
1031 1050 $ hg commit -i -d '22 0' -mab <<EOF
1032 1051 > y
1033 1052 > y
1034 1053 > y
1035 1054 > EOF
1036 1055 diff --git a/subdir/f1 b/subdir/f1
1037 1056 1 hunks, 1 lines changed
1038 1057 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1039 1058
1040 1059 @@ -2,3 +2,4 @@
1041 1060 a
1042 1061 a
1043 1062 b
1044 1063 +c
1045 1064 record this change to 'subdir/f1'? [Ynesfdaq?] y
1046 1065
1047 1066
1048 1067 $ hg tip --config diff.git=True -p
1049 1068 changeset: 24:f4f718f27b7c
1050 1069 tag: tip
1051 1070 user: test
1052 1071 date: Thu Jan 01 00:00:22 1970 +0000
1053 1072 summary: ab
1054 1073
1055 1074 diff --git a/subdir/f1 b/subdir/f1
1056 1075 --- a/subdir/f1
1057 1076 +++ b/subdir/f1
1058 1077 @@ -2,3 +2,4 @@
1059 1078 a
1060 1079 a
1061 1080 b
1062 1081 +c
1063 1082
1064 1083
1065 1084 #endif
1066 1085
1067 1086 $ cd ..
1068 1087
1069 1088
1070 1089 Abort early when a merge is in progress
1071 1090
1072 1091 $ hg up 4
1073 1092 1 files updated, 0 files merged, 6 files removed, 0 files unresolved
1074 1093
1075 1094 $ touch iwillmergethat
1076 1095 $ hg add iwillmergethat
1077 1096
1078 1097 $ hg branch thatbranch
1079 1098 marked working directory as branch thatbranch
1080 1099 (branches are permanent and global, did you want a bookmark?)
1081 1100
1082 1101 $ hg ci -m'new head'
1083 1102
1084 1103 $ hg up default
1085 1104 6 files updated, 0 files merged, 2 files removed, 0 files unresolved
1086 1105
1087 1106 $ hg merge thatbranch
1088 1107 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1089 1108 (branch merge, don't forget to commit)
1090 1109
1091 1110 $ hg commit -i -m'will abort'
1092 1111 abort: cannot partially commit a merge (use "hg commit" instead)
1093 1112 [255]
1094 1113
1095 1114 $ hg up -C
1096 1115 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1097 1116
1098 1117 Editing patch (and ignoring trailing text)
1099 1118
1100 1119 $ cat > editor.sh << '__EOF__'
1101 1120 > sed -e 7d -e '5s/^-/ /' -e '/^# ---/i\
1102 1121 > trailing\nditto' "$1" > tmp
1103 1122 > mv tmp "$1"
1104 1123 > __EOF__
1105 1124 $ cat > editedfile << '__EOF__'
1106 1125 > This is the first line
1107 1126 > This is the second line
1108 1127 > This is the third line
1109 1128 > __EOF__
1110 1129 $ hg add editedfile
1111 1130 $ hg commit -medit-patch-1
1112 1131 $ cat > editedfile << '__EOF__'
1113 1132 > This line has changed
1114 1133 > This change will be committed
1115 1134 > This is the third line
1116 1135 > __EOF__
1117 1136 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-2 <<EOF
1118 1137 > y
1119 1138 > e
1120 1139 > EOF
1121 1140 diff --git a/editedfile b/editedfile
1122 1141 1 hunks, 2 lines changed
1123 1142 examine changes to 'editedfile'? [Ynesfdaq?] y
1124 1143
1125 1144 @@ -1,3 +1,3 @@
1126 1145 -This is the first line
1127 1146 -This is the second line
1128 1147 +This line has changed
1129 1148 +This change will be committed
1130 1149 This is the third line
1131 1150 record this change to 'editedfile'? [Ynesfdaq?] e
1132 1151
1133 1152 $ cat editedfile
1134 1153 This line has changed
1135 1154 This change will be committed
1136 1155 This is the third line
1137 1156 $ hg cat -r tip editedfile
1138 1157 This is the first line
1139 1158 This change will be committed
1140 1159 This is the third line
1141 1160 $ hg revert editedfile
1142 1161
1143 1162 Trying to edit patch for whole file
1144 1163
1145 1164 $ echo "This is the fourth line" >> editedfile
1146 1165 $ hg commit -i <<EOF
1147 1166 > e
1148 1167 > q
1149 1168 > EOF
1150 1169 diff --git a/editedfile b/editedfile
1151 1170 1 hunks, 1 lines changed
1152 1171 examine changes to 'editedfile'? [Ynesfdaq?] e
1153 1172
1154 1173 cannot edit patch for whole file
1155 1174 examine changes to 'editedfile'? [Ynesfdaq?] q
1156 1175
1157 1176 abort: user quit
1158 1177 [255]
1159 1178 $ hg revert editedfile
1160 1179
1161 1180 Removing changes from patch
1162 1181
1163 1182 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1164 1183 $ mv tmp editedfile
1165 1184 $ echo "This line has been added" >> editedfile
1166 1185 $ cat > editor.sh << '__EOF__'
1167 1186 > sed -e 's/^[-+]/ /' "$1" > tmp
1168 1187 > mv tmp "$1"
1169 1188 > __EOF__
1170 1189 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1171 1190 > y
1172 1191 > e
1173 1192 > EOF
1174 1193 diff --git a/editedfile b/editedfile
1175 1194 1 hunks, 3 lines changed
1176 1195 examine changes to 'editedfile'? [Ynesfdaq?] y
1177 1196
1178 1197 @@ -1,3 +1,3 @@
1179 1198 -This is the first line
1180 1199 -This change will be committed
1181 1200 -This is the third line
1182 1201 +This change will not be committed
1183 1202 +This is the second line
1184 1203 +This line has been added
1185 1204 record this change to 'editedfile'? [Ynesfdaq?] e
1186 1205
1187 1206 no changes to record
1188 1207 $ cat editedfile
1189 1208 This change will not be committed
1190 1209 This is the second line
1191 1210 This line has been added
1192 1211 $ hg cat -r tip editedfile
1193 1212 This is the first line
1194 1213 This change will be committed
1195 1214 This is the third line
1196 1215 $ hg revert editedfile
1197 1216
1198 1217 Invalid patch
1199 1218
1200 1219 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1201 1220 $ mv tmp editedfile
1202 1221 $ echo "This line has been added" >> editedfile
1203 1222 $ cat > editor.sh << '__EOF__'
1204 1223 > sed s/This/That/ "$1" > tmp
1205 1224 > mv tmp "$1"
1206 1225 > __EOF__
1207 1226 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1208 1227 > y
1209 1228 > e
1210 1229 > EOF
1211 1230 diff --git a/editedfile b/editedfile
1212 1231 1 hunks, 3 lines changed
1213 1232 examine changes to 'editedfile'? [Ynesfdaq?] y
1214 1233
1215 1234 @@ -1,3 +1,3 @@
1216 1235 -This is the first line
1217 1236 -This change will be committed
1218 1237 -This is the third line
1219 1238 +This change will not be committed
1220 1239 +This is the second line
1221 1240 +This line has been added
1222 1241 record this change to 'editedfile'? [Ynesfdaq?] e
1223 1242
1224 1243 patching file editedfile
1225 1244 Hunk #1 FAILED at 0
1226 1245 1 out of 1 hunks FAILED -- saving rejects to file editedfile.rej
1227 1246 abort: patch failed to apply
1228 1247 [255]
1229 1248 $ cat editedfile
1230 1249 This change will not be committed
1231 1250 This is the second line
1232 1251 This line has been added
1233 1252 $ hg cat -r tip editedfile
1234 1253 This is the first line
1235 1254 This change will be committed
1236 1255 This is the third line
1237 1256 $ cat editedfile.rej
1238 1257 --- editedfile
1239 1258 +++ editedfile
1240 1259 @@ -1,3 +1,3 @@
1241 1260 -That is the first line
1242 1261 -That change will be committed
1243 1262 -That is the third line
1244 1263 +That change will not be committed
1245 1264 +That is the second line
1246 1265 +That line has been added
1247 1266
1248 1267 Malformed patch - error handling
1249 1268
1250 1269 $ cat > editor.sh << '__EOF__'
1251 1270 > sed -e '/^@/p' "$1" > tmp
1252 1271 > mv tmp "$1"
1253 1272 > __EOF__
1254 1273 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1255 1274 > y
1256 1275 > e
1257 1276 > EOF
1258 1277 diff --git a/editedfile b/editedfile
1259 1278 1 hunks, 3 lines changed
1260 1279 examine changes to 'editedfile'? [Ynesfdaq?] y
1261 1280
1262 1281 @@ -1,3 +1,3 @@
1263 1282 -This is the first line
1264 1283 -This change will be committed
1265 1284 -This is the third line
1266 1285 +This change will not be committed
1267 1286 +This is the second line
1268 1287 +This line has been added
1269 1288 record this change to 'editedfile'? [Ynesfdaq?] e
1270 1289
1271 1290 abort: error parsing patch: unhandled transition: range -> range
1272 1291 [255]
1273 1292
1274 1293 random text in random positions is still an error
1275 1294
1276 1295 $ cat > editor.sh << '__EOF__'
1277 1296 > sed -e '/^@/i\
1278 1297 > other' "$1" > tmp
1279 1298 > mv tmp "$1"
1280 1299 > __EOF__
1281 1300 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1282 1301 > y
1283 1302 > e
1284 1303 > EOF
1285 1304 diff --git a/editedfile b/editedfile
1286 1305 1 hunks, 3 lines changed
1287 1306 examine changes to 'editedfile'? [Ynesfdaq?] y
1288 1307
1289 1308 @@ -1,3 +1,3 @@
1290 1309 -This is the first line
1291 1310 -This change will be committed
1292 1311 -This is the third line
1293 1312 +This change will not be committed
1294 1313 +This is the second line
1295 1314 +This line has been added
1296 1315 record this change to 'editedfile'? [Ynesfdaq?] e
1297 1316
1298 1317 abort: error parsing patch: unhandled transition: file -> other
1299 1318 [255]
1300 1319
1301 1320 $ hg up -C
1302 1321 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1303 1322
1304 1323 With win32text
1305 1324
1306 1325 $ echo '[extensions]' >> .hg/hgrc
1307 1326 $ echo 'win32text = ' >> .hg/hgrc
1308 1327 $ echo '[decode]' >> .hg/hgrc
1309 1328 $ echo '** = cleverdecode:' >> .hg/hgrc
1310 1329 $ echo '[encode]' >> .hg/hgrc
1311 1330 $ echo '** = cleverencode:' >> .hg/hgrc
1312 1331 $ echo '[patch]' >> .hg/hgrc
1313 1332 $ echo 'eol = crlf' >> .hg/hgrc
1314 1333
1315 1334 Ignore win32text deprecation warning for now:
1316 1335
1317 1336 $ echo '[win32text]' >> .hg/hgrc
1318 1337 $ echo 'warn = no' >> .hg/hgrc
1319 1338
1320 1339 $ echo d >> subdir/f1
1321 1340 $ hg commit -i -d '24 0' -mw1 <<EOF
1322 1341 > y
1323 1342 > y
1324 1343 > EOF
1325 1344 diff --git a/subdir/f1 b/subdir/f1
1326 1345 1 hunks, 1 lines changed
1327 1346 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1328 1347
1329 1348 @@ -3,3 +3,4 @@
1330 1349 a
1331 1350 b
1332 1351 c
1333 1352 +d
1334 1353 record this change to 'subdir/f1'? [Ynesfdaq?] y
1335 1354
1336 1355
1337 1356 $ hg tip -p
1338 1357 changeset: 28:* (glob)
1339 1358 tag: tip
1340 1359 user: test
1341 1360 date: Thu Jan 01 00:00:24 1970 +0000
1342 1361 summary: w1
1343 1362
1344 1363 diff -r ???????????? -r ???????????? subdir/f1 (glob)
1345 1364 --- a/subdir/f1 Thu Jan 01 00:00:23 1970 +0000
1346 1365 +++ b/subdir/f1 Thu Jan 01 00:00:24 1970 +0000
1347 1366 @@ -3,3 +3,4 @@
1348 1367 a
1349 1368 b
1350 1369 c
1351 1370 +d
1352 1371
1353 1372
1354 1373
1355 1374 Test --user when ui.username not set
1356 1375 $ unset HGUSER
1357 1376 $ echo e >> subdir/f1
1358 1377 $ hg commit -i --config ui.username= -d '8 0' --user xyz -m "user flag" <<EOF
1359 1378 > y
1360 1379 > y
1361 1380 > EOF
1362 1381 diff --git a/subdir/f1 b/subdir/f1
1363 1382 1 hunks, 1 lines changed
1364 1383 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1365 1384
1366 1385 @@ -4,3 +4,4 @@
1367 1386 b
1368 1387 c
1369 1388 d
1370 1389 +e
1371 1390 record this change to 'subdir/f1'? [Ynesfdaq?] y
1372 1391
1373 1392 $ hg log --template '{author}\n' -l 1
1374 1393 xyz
1375 1394 $ HGUSER="test"
1376 1395 $ export HGUSER
1377 1396
1378 1397
1379 1398 Moving files
1380 1399
1381 1400 $ hg update -C .
1382 1401 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1383 1402 $ hg mv plain plain3
1384 1403 $ echo somechange >> plain3
1385 1404 $ hg commit -i -d '23 0' -mmoving_files << EOF
1386 1405 > y
1387 1406 > y
1388 1407 > EOF
1389 1408 diff --git a/plain b/plain3
1390 1409 rename from plain
1391 1410 rename to plain3
1392 1411 1 hunks, 1 lines changed
1393 1412 examine changes to 'plain' and 'plain3'? [Ynesfdaq?] y
1394 1413
1395 1414 @@ -11,3 +11,4 @@
1396 1415 9
1397 1416 10
1398 1417 11
1399 1418 +somechange
1400 1419 record this change to 'plain3'? [Ynesfdaq?] y
1401 1420
1402 1421 $ hg tip
1403 1422 changeset: 30:542e1f362a22
1404 1423 tag: tip
1405 1424 user: test
1406 1425 date: Thu Jan 01 00:00:23 1970 +0000
1407 1426 summary: moving_files
1408 1427
1428 Editing patch of newly added file
1429
1430 $ hg update -C .
1431 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1432 $ cat > editor.sh << '__EOF__'
1433 > cat "$1" | sed "s/first/very/g" > tt
1434 > mv tt "$1"
1435 > __EOF__
1436 $ cat > newfile << '__EOF__'
1437 > This is the first line
1438 > This is the second line
1439 > This is the third line
1440 > __EOF__
1441 $ hg add newfile
1442 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-new <<EOF
1443 > y
1444 > e
1445 > EOF
1446 diff --git a/newfile b/newfile
1447 new file mode 100644
1448 examine changes to 'newfile'? [Ynesfdaq?] y
1449
1450 @@ -0,0 +1,3 @@
1451 +This is the first line
1452 +This is the second line
1453 +This is the third line
1454 record this change to 'newfile'? [Ynesfdaq?] e
1455
1456 $ hg cat -r tip newfile
1457 This is the very line
1458 This is the second line
1459 This is the third line
1460
1461 $ cat newfile
1462 This is the first line
1463 This is the second line
1464 This is the third line
1409 1465 $ cd ..
@@ -1,1327 +1,1339 b''
1 1 $ cat <<EOF >> $HGRCPATH
2 2 > [extensions]
3 3 > keyword =
4 4 > mq =
5 5 > notify =
6 6 > record =
7 7 > transplant =
8 8 > [ui]
9 9 > interactive = true
10 10 > EOF
11 11
12 12 hide outer repo
13 13 $ hg init
14 14
15 15 Run kwdemo before [keyword] files are set up
16 16 as it would succeed without uisetup otherwise
17 17
18 18 $ hg --quiet kwdemo
19 19 [extensions]
20 20 keyword =
21 21 [keyword]
22 22 demo.txt =
23 23 [keywordset]
24 24 svn = False
25 25 [keywordmaps]
26 26 Author = {author|user}
27 27 Date = {date|utcdate}
28 28 Header = {root}/{file},v {node|short} {date|utcdate} {author|user}
29 29 Id = {file|basename},v {node|short} {date|utcdate} {author|user}
30 30 RCSFile = {file|basename},v
31 31 RCSfile = {file|basename},v
32 32 Revision = {node|short}
33 33 Source = {root}/{file},v
34 34 $Author: test $
35 35 $Date: ????/??/?? ??:??:?? $ (glob)
36 36 $Header: */demo.txt,v ???????????? ????/??/?? ??:??:?? test $ (glob)
37 37 $Id: demo.txt,v ???????????? ????/??/?? ??:??:?? test $ (glob)
38 38 $RCSFile: demo.txt,v $
39 39 $RCSfile: demo.txt,v $
40 40 $Revision: ???????????? $ (glob)
41 41 $Source: */demo.txt,v $ (glob)
42 42
43 43 $ hg --quiet kwdemo "Branch = {branches}"
44 44 [extensions]
45 45 keyword =
46 46 [keyword]
47 47 demo.txt =
48 48 [keywordset]
49 49 svn = False
50 50 [keywordmaps]
51 51 Branch = {branches}
52 52 $Branch: demobranch $
53 53
54 54 $ cat <<EOF >> $HGRCPATH
55 55 > [keyword]
56 56 > ** =
57 57 > b = ignore
58 58 > i = ignore
59 59 > [hooks]
60 60 > EOF
61 61 $ cp $HGRCPATH $HGRCPATH.nohooks
62 62 > cat <<EOF >> $HGRCPATH
63 63 > commit=
64 64 > commit.test=cp a hooktest
65 65 > EOF
66 66
67 67 $ hg init Test-bndl
68 68 $ cd Test-bndl
69 69
70 70 kwshrink should exit silently in empty/invalid repo
71 71
72 72 $ hg kwshrink
73 73
74 74 Symlinks cannot be created on Windows.
75 75 A bundle to test this was made with:
76 76 hg init t
77 77 cd t
78 78 echo a > a
79 79 ln -s a sym
80 80 hg add sym
81 81 hg ci -m addsym -u mercurial
82 82 hg bundle --base null ../test-keyword.hg
83 83
84 84 $ hg pull -u "$TESTDIR"/bundles/test-keyword.hg
85 85 pulling from *test-keyword.hg (glob)
86 86 requesting all changes
87 87 adding changesets
88 88 adding manifests
89 89 adding file changes
90 90 added 1 changesets with 1 changes to 1 files
91 91 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
92 92
93 93 $ echo 'expand $Id$' > a
94 94 $ echo 'do not process $Id:' >> a
95 95 $ echo 'xxx $' >> a
96 96 $ echo 'ignore $Id$' > b
97 97
98 98 Output files as they were created
99 99
100 100 $ cat a b
101 101 expand $Id$
102 102 do not process $Id:
103 103 xxx $
104 104 ignore $Id$
105 105
106 106 no kwfiles
107 107
108 108 $ hg kwfiles
109 109
110 110 untracked candidates
111 111
112 112 $ hg -v kwfiles --unknown
113 113 k a
114 114
115 115 Add files and check status
116 116
117 117 $ hg addremove
118 118 adding a
119 119 adding b
120 120 $ hg status
121 121 A a
122 122 A b
123 123
124 124
125 125 Default keyword expansion including commit hook
126 126 Interrupted commit should not change state or run commit hook
127 127
128 128 $ hg --debug commit
129 129 abort: empty commit message
130 130 [255]
131 131 $ hg status
132 132 A a
133 133 A b
134 134
135 135 Commit with several checks
136 136
137 137 $ hg --debug commit -mabsym -u 'User Name <user@example.com>'
138 138 committing files:
139 139 a
140 140 b
141 141 committing manifest
142 142 committing changelog
143 143 overwriting a expanding keywords
144 144 running hook commit.test: cp a hooktest
145 145 committed changeset 1:ef63ca68695bc9495032c6fda1350c71e6d256e9
146 146 $ hg status
147 147 ? hooktest
148 148 $ hg debugrebuildstate
149 149 $ hg --quiet identify
150 150 ef63ca68695b
151 151
152 152 cat files in working directory with keywords expanded
153 153
154 154 $ cat a b
155 155 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
156 156 do not process $Id:
157 157 xxx $
158 158 ignore $Id$
159 159
160 160 hg cat files and symlink, no expansion
161 161
162 162 $ hg cat sym a b && echo
163 163 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
164 164 do not process $Id:
165 165 xxx $
166 166 ignore $Id$
167 167 a
168 168
169 169 $ diff a hooktest
170 170
171 171 $ cp $HGRCPATH.nohooks $HGRCPATH
172 172 $ rm hooktest
173 173
174 174 hg status of kw-ignored binary file starting with '\1\n'
175 175
176 176 >>> open("i", "wb").write("\1\nfoo")
177 177 $ hg -q commit -Am metasep i
178 178 $ hg status
179 179 >>> open("i", "wb").write("\1\nbar")
180 180 $ hg status
181 181 M i
182 182 $ hg -q commit -m "modify metasep" i
183 183 $ hg status --rev 2:3
184 184 M i
185 185 $ touch empty
186 186 $ hg -q commit -A -m "another file"
187 187 $ hg status -A --rev 3:4 i
188 188 C i
189 189
190 190 $ hg -q strip --no-backup 2
191 191
192 192 Test hook execution
193 193
194 194 bundle
195 195
196 196 $ hg bundle --base null ../kw.hg
197 197 2 changesets found
198 198 $ cd ..
199 199 $ hg init Test
200 200 $ cd Test
201 201
202 202 Notify on pull to check whether keywords stay as is in email
203 203 ie. if patch.diff wrapper acts as it should
204 204
205 205 $ cat <<EOF >> $HGRCPATH
206 206 > [hooks]
207 207 > incoming.notify = python:hgext.notify.hook
208 208 > [notify]
209 209 > sources = pull
210 210 > diffstat = False
211 211 > maxsubject = 15
212 212 > [reposubs]
213 213 > * = Test
214 214 > EOF
215 215
216 216 Pull from bundle and trigger notify
217 217
218 218 $ hg pull -u ../kw.hg
219 219 pulling from ../kw.hg
220 220 requesting all changes
221 221 adding changesets
222 222 adding manifests
223 223 adding file changes
224 224 added 2 changesets with 3 changes to 3 files
225 225 Content-Type: text/plain; charset="us-ascii"
226 226 MIME-Version: 1.0
227 227 Content-Transfer-Encoding: 7bit
228 228 Date: * (glob)
229 229 Subject: changeset in...
230 230 From: mercurial
231 231 X-Hg-Notification: changeset a2392c293916
232 232 Message-Id: <hg.a2392c293916*> (glob)
233 233 To: Test
234 234
235 235 changeset a2392c293916 in $TESTTMP/Test (glob)
236 236 details: $TESTTMP/Test?cmd=changeset;node=a2392c293916
237 237 description:
238 238 addsym
239 239
240 240 diffs (6 lines):
241 241
242 242 diff -r 000000000000 -r a2392c293916 sym
243 243 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
244 244 +++ b/sym Sat Feb 09 20:25:47 2008 +0100
245 245 @@ -0,0 +1,1 @@
246 246 +a
247 247 \ No newline at end of file
248 248 Content-Type: text/plain; charset="us-ascii"
249 249 MIME-Version: 1.0
250 250 Content-Transfer-Encoding: 7bit
251 251 Date:* (glob)
252 252 Subject: changeset in...
253 253 From: User Name <user@example.com>
254 254 X-Hg-Notification: changeset ef63ca68695b
255 255 Message-Id: <hg.ef63ca68695b*> (glob)
256 256 To: Test
257 257
258 258 changeset ef63ca68695b in $TESTTMP/Test (glob)
259 259 details: $TESTTMP/Test?cmd=changeset;node=ef63ca68695b
260 260 description:
261 261 absym
262 262
263 263 diffs (12 lines):
264 264
265 265 diff -r a2392c293916 -r ef63ca68695b a
266 266 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
267 267 +++ b/a Thu Jan 01 00:00:00 1970 +0000
268 268 @@ -0,0 +1,3 @@
269 269 +expand $Id$
270 270 +do not process $Id:
271 271 +xxx $
272 272 diff -r a2392c293916 -r ef63ca68695b b
273 273 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
274 274 +++ b/b Thu Jan 01 00:00:00 1970 +0000
275 275 @@ -0,0 +1,1 @@
276 276 +ignore $Id$
277 277 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
278 278
279 279 $ cp $HGRCPATH.nohooks $HGRCPATH
280 280
281 281 Touch files and check with status
282 282
283 283 $ touch a b
284 284 $ hg status
285 285
286 286 Update and expand
287 287
288 288 $ rm sym a b
289 289 $ hg update -C
290 290 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 291 $ cat a b
292 292 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
293 293 do not process $Id:
294 294 xxx $
295 295 ignore $Id$
296 296
297 297 Check whether expansion is filewise and file mode is preserved
298 298
299 299 $ echo '$Id$' > c
300 300 $ echo 'tests for different changenodes' >> c
301 301 #if unix-permissions
302 302 $ chmod 600 c
303 303 $ ls -l c | cut -b 1-10
304 304 -rw-------
305 305 #endif
306 306
307 307 commit file c
308 308
309 309 $ hg commit -A -mcndiff -d '1 0' -u 'User Name <user@example.com>'
310 310 adding c
311 311 #if unix-permissions
312 312 $ ls -l c | cut -b 1-10
313 313 -rw-------
314 314 #endif
315 315
316 316 force expansion
317 317
318 318 $ hg -v kwexpand
319 319 overwriting a expanding keywords
320 320 overwriting c expanding keywords
321 321
322 322 compare changenodes in a and c
323 323
324 324 $ cat a c
325 325 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
326 326 do not process $Id:
327 327 xxx $
328 328 $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
329 329 tests for different changenodes
330 330
331 331 record
332 332
333 333 $ echo '$Id$' > r
334 334 $ hg add r
335 335
336 336 record chunk
337 337
338 338 >>> lines = open('a', 'rb').readlines()
339 339 >>> lines.insert(1, 'foo\n')
340 340 >>> lines.append('bar\n')
341 341 >>> open('a', 'wb').writelines(lines)
342 342 $ hg record -d '10 1' -m rectest a<<EOF
343 343 > y
344 344 > y
345 345 > n
346 346 > EOF
347 347 diff --git a/a b/a
348 348 2 hunks, 2 lines changed
349 349 examine changes to 'a'? [Ynesfdaq?] y
350 350
351 351 @@ -1,3 +1,4 @@
352 352 expand $Id$
353 353 +foo
354 354 do not process $Id:
355 355 xxx $
356 356 record change 1/2 to 'a'? [Ynesfdaq?] y
357 357
358 358 @@ -2,2 +3,3 @@
359 359 do not process $Id:
360 360 xxx $
361 361 +bar
362 362 record change 2/2 to 'a'? [Ynesfdaq?] n
363 363
364 364
365 365 $ hg identify
366 366 5f5eb23505c3+ tip
367 367 $ hg status
368 368 M a
369 369 A r
370 370
371 371 Cat modified file a
372 372
373 373 $ cat a
374 374 expand $Id: a,v 5f5eb23505c3 1970/01/01 00:00:10 test $
375 375 foo
376 376 do not process $Id:
377 377 xxx $
378 378 bar
379 379
380 380 Diff remaining chunk
381 381
382 382 $ hg diff a
383 383 diff -r 5f5eb23505c3 a
384 384 --- a/a Thu Jan 01 00:00:09 1970 -0000
385 385 +++ b/a * (glob)
386 386 @@ -2,3 +2,4 @@
387 387 foo
388 388 do not process $Id:
389 389 xxx $
390 390 +bar
391 391
392 392 $ hg rollback
393 393 repository tip rolled back to revision 2 (undo commit)
394 394 working directory now based on revision 2
395 395
396 396 Record all chunks in file a
397 397
398 398 $ echo foo > msg
399 399
400 400 - do not use "hg record -m" here!
401 401
402 402 $ hg record -l msg -d '11 1' a<<EOF
403 403 > y
404 404 > y
405 405 > y
406 406 > EOF
407 407 diff --git a/a b/a
408 408 2 hunks, 2 lines changed
409 409 examine changes to 'a'? [Ynesfdaq?] y
410 410
411 411 @@ -1,3 +1,4 @@
412 412 expand $Id$
413 413 +foo
414 414 do not process $Id:
415 415 xxx $
416 416 record change 1/2 to 'a'? [Ynesfdaq?] y
417 417
418 418 @@ -2,2 +3,3 @@
419 419 do not process $Id:
420 420 xxx $
421 421 +bar
422 422 record change 2/2 to 'a'? [Ynesfdaq?] y
423 423
424 424
425 425 File a should be clean
426 426
427 427 $ hg status -A a
428 428 C a
429 429
430 430 rollback and revert expansion
431 431
432 432 $ cat a
433 433 expand $Id: a,v 78e0a02d76aa 1970/01/01 00:00:11 test $
434 434 foo
435 435 do not process $Id:
436 436 xxx $
437 437 bar
438 438 $ hg --verbose rollback
439 439 repository tip rolled back to revision 2 (undo commit)
440 440 working directory now based on revision 2
441 441 overwriting a expanding keywords
442 442 $ hg status a
443 443 M a
444 444 $ cat a
445 445 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
446 446 foo
447 447 do not process $Id:
448 448 xxx $
449 449 bar
450 450 $ echo '$Id$' > y
451 451 $ echo '$Id$' > z
452 452 $ hg add y
453 453 $ hg commit -Am "rollback only" z
454 454 $ cat z
455 455 $Id: z,v 45a5d3adce53 1970/01/01 00:00:00 test $
456 456 $ hg --verbose rollback
457 457 repository tip rolled back to revision 2 (undo commit)
458 458 working directory now based on revision 2
459 459 overwriting z shrinking keywords
460 460
461 461 Only z should be overwritten
462 462
463 463 $ hg status a y z
464 464 M a
465 465 A y
466 466 A z
467 467 $ cat z
468 468 $Id$
469 469 $ hg forget y z
470 470 $ rm y z
471 471
472 472 record added file alone
473 473
474 474 $ hg -v record -l msg -d '12 2' r<<EOF
475 475 > y
476 476 > y
477 477 > EOF
478 478 diff --git a/r b/r
479 479 new file mode 100644
480 480 examine changes to 'r'? [Ynesfdaq?] y
481 481
482 @@ -0,0 +1,1 @@
483 +$Id$
484 record this change to 'r'? [Ynesfdaq?] y
485
486 resolving manifests
487 patching file r
482 488 committing files:
483 489 r
484 490 committing manifest
485 491 committing changelog
486 492 committed changeset 3:82a2f715724d
487 493 overwriting r expanding keywords
488 494 $ hg status r
489 495 $ hg --verbose rollback
490 496 repository tip rolled back to revision 2 (undo commit)
491 497 working directory now based on revision 2
492 498 overwriting r shrinking keywords
493 499 $ hg forget r
494 500 $ rm msg r
495 501 $ hg update -C
496 502 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
497 503
498 504 record added keyword ignored file
499 505
500 506 $ echo '$Id$' > i
501 507 $ hg add i
502 508 $ hg --verbose record -d '13 1' -m recignored<<EOF
503 509 > y
504 510 > y
505 511 > EOF
506 512 diff --git a/i b/i
507 513 new file mode 100644
508 514 examine changes to 'i'? [Ynesfdaq?] y
509 515
516 @@ -0,0 +1,1 @@
517 +$Id$
518 record this change to 'i'? [Ynesfdaq?] y
519
520 resolving manifests
521 patching file i
510 522 committing files:
511 523 i
512 524 committing manifest
513 525 committing changelog
514 526 committed changeset 3:9f40ceb5a072
515 527 $ cat i
516 528 $Id$
517 529 $ hg -q rollback
518 530 $ hg forget i
519 531 $ rm i
520 532
521 533 amend
522 534
523 535 $ echo amend >> a
524 536 $ echo amend >> b
525 537 $ hg -q commit -d '14 1' -m 'prepare amend'
526 538
527 539 $ hg --debug commit --amend -d '15 1' -m 'amend without changes' | grep keywords
528 540 overwriting a expanding keywords
529 541 $ hg -q id
530 542 67d8c481a6be
531 543 $ head -1 a
532 544 expand $Id: a,v 67d8c481a6be 1970/01/01 00:00:15 test $
533 545
534 546 $ hg -q strip --no-backup tip
535 547
536 548 Test patch queue repo
537 549
538 550 $ hg init --mq
539 551 $ hg qimport -r tip -n mqtest.diff
540 552 $ hg commit --mq -m mqtest
541 553
542 554 Keywords should not be expanded in patch
543 555
544 556 $ cat .hg/patches/mqtest.diff
545 557 # HG changeset patch
546 558 # User User Name <user@example.com>
547 559 # Date 1 0
548 560 # Thu Jan 01 00:00:01 1970 +0000
549 561 # Node ID 40a904bbbe4cd4ab0a1f28411e35db26341a40ad
550 562 # Parent ef63ca68695bc9495032c6fda1350c71e6d256e9
551 563 cndiff
552 564
553 565 diff -r ef63ca68695b -r 40a904bbbe4c c
554 566 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
555 567 +++ b/c Thu Jan 01 00:00:01 1970 +0000
556 568 @@ -0,0 +1,2 @@
557 569 +$Id$
558 570 +tests for different changenodes
559 571
560 572 $ hg qpop
561 573 popping mqtest.diff
562 574 patch queue now empty
563 575
564 576 qgoto, implying qpush, should expand
565 577
566 578 $ hg qgoto mqtest.diff
567 579 applying mqtest.diff
568 580 now at: mqtest.diff
569 581 $ cat c
570 582 $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
571 583 tests for different changenodes
572 584 $ hg cat c
573 585 $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
574 586 tests for different changenodes
575 587
576 588 Keywords should not be expanded in filelog
577 589
578 590 $ hg --config 'extensions.keyword=!' cat c
579 591 $Id$
580 592 tests for different changenodes
581 593
582 594 qpop and move on
583 595
584 596 $ hg qpop
585 597 popping mqtest.diff
586 598 patch queue now empty
587 599
588 600 Copy and show added kwfiles
589 601
590 602 $ hg cp a c
591 603 $ hg kwfiles
592 604 a
593 605 c
594 606
595 607 Commit and show expansion in original and copy
596 608
597 609 $ hg --debug commit -ma2c -d '1 0' -u 'User Name <user@example.com>'
598 610 committing files:
599 611 c
600 612 c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292
601 613 committing manifest
602 614 committing changelog
603 615 overwriting c expanding keywords
604 616 committed changeset 2:25736cf2f5cbe41f6be4e6784ef6ecf9f3bbcc7d
605 617 $ cat a c
606 618 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
607 619 do not process $Id:
608 620 xxx $
609 621 expand $Id: c,v 25736cf2f5cb 1970/01/01 00:00:01 user $
610 622 do not process $Id:
611 623 xxx $
612 624
613 625 Touch copied c and check its status
614 626
615 627 $ touch c
616 628 $ hg status
617 629
618 630 Copy kwfile to keyword ignored file unexpanding keywords
619 631
620 632 $ hg --verbose copy a i
621 633 copying a to i
622 634 overwriting i shrinking keywords
623 635 $ head -n 1 i
624 636 expand $Id$
625 637 $ hg forget i
626 638 $ rm i
627 639
628 640 Copy ignored file to ignored file: no overwriting
629 641
630 642 $ hg --verbose copy b i
631 643 copying b to i
632 644 $ hg forget i
633 645 $ rm i
634 646
635 647 cp symlink file; hg cp -A symlink file (part1)
636 648 - copied symlink points to kwfile: overwrite
637 649
638 650 #if symlink
639 651 $ cp sym i
640 652 $ ls -l i
641 653 -rw-r--r--* (glob)
642 654 $ head -1 i
643 655 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
644 656 $ hg copy --after --verbose sym i
645 657 copying sym to i
646 658 overwriting i shrinking keywords
647 659 $ head -1 i
648 660 expand $Id$
649 661 $ hg forget i
650 662 $ rm i
651 663 #endif
652 664
653 665 Test different options of hg kwfiles
654 666
655 667 $ hg kwfiles
656 668 a
657 669 c
658 670 $ hg -v kwfiles --ignore
659 671 I b
660 672 I sym
661 673 $ hg kwfiles --all
662 674 K a
663 675 K c
664 676 I b
665 677 I sym
666 678
667 679 Diff specific revision
668 680
669 681 $ hg diff --rev 1
670 682 diff -r ef63ca68695b c
671 683 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
672 684 +++ b/c * (glob)
673 685 @@ -0,0 +1,3 @@
674 686 +expand $Id$
675 687 +do not process $Id:
676 688 +xxx $
677 689
678 690 Status after rollback:
679 691
680 692 $ hg rollback
681 693 repository tip rolled back to revision 1 (undo commit)
682 694 working directory now based on revision 1
683 695 $ hg status
684 696 A c
685 697 $ hg update --clean
686 698 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
687 699
688 700 #if symlink
689 701
690 702 cp symlink file; hg cp -A symlink file (part2)
691 703 - copied symlink points to kw ignored file: do not overwrite
692 704
693 705 $ cat a > i
694 706 $ ln -s i symignored
695 707 $ hg commit -Am 'fake expansion in ignored and symlink' i symignored
696 708 $ cp symignored x
697 709 $ hg copy --after --verbose symignored x
698 710 copying symignored to x
699 711 $ head -n 1 x
700 712 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
701 713 $ hg forget x
702 714 $ rm x
703 715
704 716 $ hg rollback
705 717 repository tip rolled back to revision 1 (undo commit)
706 718 working directory now based on revision 1
707 719 $ hg update --clean
708 720 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
709 721 $ rm i symignored
710 722
711 723 #endif
712 724
713 725 Custom keywordmaps as argument to kwdemo
714 726
715 727 $ hg --quiet kwdemo "Xinfo = {author}: {desc}"
716 728 [extensions]
717 729 keyword =
718 730 [keyword]
719 731 ** =
720 732 b = ignore
721 733 demo.txt =
722 734 i = ignore
723 735 [keywordset]
724 736 svn = False
725 737 [keywordmaps]
726 738 Xinfo = {author}: {desc}
727 739 $Xinfo: test: hg keyword configuration and expansion example $
728 740
729 741 Configure custom keywordmaps
730 742
731 743 $ cat <<EOF >>$HGRCPATH
732 744 > [keywordmaps]
733 745 > Id = {file} {node|short} {date|rfc822date} {author|user}
734 746 > Xinfo = {author}: {desc}
735 747 > EOF
736 748
737 749 Cat and hg cat files before custom expansion
738 750
739 751 $ cat a b
740 752 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
741 753 do not process $Id:
742 754 xxx $
743 755 ignore $Id$
744 756 $ hg cat sym a b && echo
745 757 expand $Id: a ef63ca68695b Thu, 01 Jan 1970 00:00:00 +0000 user $
746 758 do not process $Id:
747 759 xxx $
748 760 ignore $Id$
749 761 a
750 762
751 763 Write custom keyword and prepare multi-line commit message
752 764
753 765 $ echo '$Xinfo$' >> a
754 766 $ cat <<EOF >> log
755 767 > firstline
756 768 > secondline
757 769 > EOF
758 770
759 771 Interrupted commit should not change state
760 772
761 773 $ hg commit
762 774 abort: empty commit message
763 775 [255]
764 776 $ hg status
765 777 M a
766 778 ? c
767 779 ? log
768 780
769 781 Commit with multi-line message and custom expansion
770 782
771 783 $ hg --debug commit -l log -d '2 0' -u 'User Name <user@example.com>'
772 784 committing files:
773 785 a
774 786 committing manifest
775 787 committing changelog
776 788 overwriting a expanding keywords
777 789 committed changeset 2:bb948857c743469b22bbf51f7ec8112279ca5d83
778 790 $ rm log
779 791
780 792 Stat, verify and show custom expansion (firstline)
781 793
782 794 $ hg status
783 795 ? c
784 796 $ hg verify
785 797 checking changesets
786 798 checking manifests
787 799 crosschecking files in changesets and manifests
788 800 checking files
789 801 3 files, 3 changesets, 4 total revisions
790 802 $ cat a b
791 803 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
792 804 do not process $Id:
793 805 xxx $
794 806 $Xinfo: User Name <user@example.com>: firstline $
795 807 ignore $Id$
796 808 $ hg cat sym a b && echo
797 809 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
798 810 do not process $Id:
799 811 xxx $
800 812 $Xinfo: User Name <user@example.com>: firstline $
801 813 ignore $Id$
802 814 a
803 815
804 816 annotate
805 817
806 818 $ hg annotate a
807 819 1: expand $Id$
808 820 1: do not process $Id:
809 821 1: xxx $
810 822 2: $Xinfo$
811 823
812 824 remove with status checks
813 825
814 826 $ hg debugrebuildstate
815 827 $ hg remove a
816 828 $ hg --debug commit -m rma
817 829 committing files:
818 830 committing manifest
819 831 committing changelog
820 832 committed changeset 3:d14c712653769de926994cf7fbb06c8fbd68f012
821 833 $ hg status
822 834 ? c
823 835
824 836 Rollback, revert, and check expansion
825 837
826 838 $ hg rollback
827 839 repository tip rolled back to revision 2 (undo commit)
828 840 working directory now based on revision 2
829 841 $ hg status
830 842 R a
831 843 ? c
832 844 $ hg revert --no-backup --rev tip a
833 845 $ cat a
834 846 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
835 847 do not process $Id:
836 848 xxx $
837 849 $Xinfo: User Name <user@example.com>: firstline $
838 850
839 851 Clone to test global and local configurations
840 852
841 853 $ cd ..
842 854
843 855 Expansion in destination with global configuration
844 856
845 857 $ hg --quiet clone Test globalconf
846 858 $ cat globalconf/a
847 859 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
848 860 do not process $Id:
849 861 xxx $
850 862 $Xinfo: User Name <user@example.com>: firstline $
851 863
852 864 No expansion in destination with local configuration in origin only
853 865
854 866 $ hg --quiet --config 'keyword.**=ignore' clone Test localconf
855 867 $ cat localconf/a
856 868 expand $Id$
857 869 do not process $Id:
858 870 xxx $
859 871 $Xinfo$
860 872
861 873 Clone to test incoming
862 874
863 875 $ hg clone -r1 Test Test-a
864 876 adding changesets
865 877 adding manifests
866 878 adding file changes
867 879 added 2 changesets with 3 changes to 3 files
868 880 updating to branch default
869 881 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
870 882 $ cd Test-a
871 883 $ cat <<EOF >> .hg/hgrc
872 884 > [paths]
873 885 > default = ../Test
874 886 > EOF
875 887 $ hg incoming
876 888 comparing with $TESTTMP/Test (glob)
877 889 searching for changes
878 890 changeset: 2:bb948857c743
879 891 tag: tip
880 892 user: User Name <user@example.com>
881 893 date: Thu Jan 01 00:00:02 1970 +0000
882 894 summary: firstline
883 895
884 896 Imported patch should not be rejected
885 897
886 898 >>> import re
887 899 >>> text = re.sub(r'(Id.*)', r'\1 rejecttest', open('a').read())
888 900 >>> open('a', 'wb').write(text)
889 901 $ hg --debug commit -m'rejects?' -d '3 0' -u 'User Name <user@example.com>'
890 902 committing files:
891 903 a
892 904 committing manifest
893 905 committing changelog
894 906 overwriting a expanding keywords
895 907 committed changeset 2:85e279d709ffc28c9fdd1b868570985fc3d87082
896 908 $ hg export -o ../rejecttest.diff tip
897 909 $ cd ../Test
898 910 $ hg import ../rejecttest.diff
899 911 applying ../rejecttest.diff
900 912 $ cat a b
901 913 expand $Id: a 4e0994474d25 Thu, 01 Jan 1970 00:00:03 +0000 user $ rejecttest
902 914 do not process $Id: rejecttest
903 915 xxx $
904 916 $Xinfo: User Name <user@example.com>: rejects? $
905 917 ignore $Id$
906 918
907 919 $ hg rollback
908 920 repository tip rolled back to revision 2 (undo import)
909 921 working directory now based on revision 2
910 922 $ hg update --clean
911 923 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
912 924
913 925 kwexpand/kwshrink on selected files
914 926
915 927 $ mkdir x
916 928 $ hg copy a x/a
917 929 $ hg --verbose kwshrink a
918 930 overwriting a shrinking keywords
919 931 - sleep required for dirstate.normal() check
920 932 $ sleep 1
921 933 $ hg status a
922 934 $ hg --verbose kwexpand a
923 935 overwriting a expanding keywords
924 936 $ hg status a
925 937
926 938 kwexpand x/a should abort
927 939
928 940 $ hg --verbose kwexpand x/a
929 941 abort: outstanding uncommitted changes
930 942 [255]
931 943 $ cd x
932 944 $ hg --debug commit -m xa -d '3 0' -u 'User Name <user@example.com>'
933 945 committing files:
934 946 x/a
935 947 x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e
936 948 committing manifest
937 949 committing changelog
938 950 overwriting x/a expanding keywords
939 951 committed changeset 3:b4560182a3f9a358179fd2d835c15e9da379c1e4
940 952 $ cat a
941 953 expand $Id: x/a b4560182a3f9 Thu, 01 Jan 1970 00:00:03 +0000 user $
942 954 do not process $Id:
943 955 xxx $
944 956 $Xinfo: User Name <user@example.com>: xa $
945 957
946 958 kwshrink a inside directory x
947 959
948 960 $ hg --verbose kwshrink a
949 961 overwriting x/a shrinking keywords
950 962 $ cat a
951 963 expand $Id$
952 964 do not process $Id:
953 965 xxx $
954 966 $Xinfo$
955 967 $ cd ..
956 968
957 969 kwexpand nonexistent
958 970
959 971 $ hg kwexpand nonexistent
960 972 nonexistent:* (glob)
961 973
962 974
963 975 #if serve
964 976 hg serve
965 977 - expand with hgweb file
966 978 - no expansion with hgweb annotate/changeset/filediff
967 979 - check errors
968 980
969 981 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
970 982 $ cat hg.pid >> $DAEMON_PIDS
971 983 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/a/?style=raw'
972 984 200 Script output follows
973 985
974 986 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
975 987 do not process $Id:
976 988 xxx $
977 989 $Xinfo: User Name <user@example.com>: firstline $
978 990 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'annotate/tip/a/?style=raw'
979 991 200 Script output follows
980 992
981 993
982 994 user@1: expand $Id$
983 995 user@1: do not process $Id:
984 996 user@1: xxx $
985 997 user@2: $Xinfo$
986 998
987 999
988 1000
989 1001
990 1002 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'rev/tip/?style=raw'
991 1003 200 Script output follows
992 1004
993 1005
994 1006 # HG changeset patch
995 1007 # User User Name <user@example.com>
996 1008 # Date 3 0
997 1009 # Node ID b4560182a3f9a358179fd2d835c15e9da379c1e4
998 1010 # Parent bb948857c743469b22bbf51f7ec8112279ca5d83
999 1011 xa
1000 1012
1001 1013 diff -r bb948857c743 -r b4560182a3f9 x/a
1002 1014 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1003 1015 +++ b/x/a Thu Jan 01 00:00:03 1970 +0000
1004 1016 @@ -0,0 +1,4 @@
1005 1017 +expand $Id$
1006 1018 +do not process $Id:
1007 1019 +xxx $
1008 1020 +$Xinfo$
1009 1021
1010 1022 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'diff/bb948857c743/a?style=raw'
1011 1023 200 Script output follows
1012 1024
1013 1025
1014 1026 diff -r ef63ca68695b -r bb948857c743 a
1015 1027 --- a/a Thu Jan 01 00:00:00 1970 +0000
1016 1028 +++ b/a Thu Jan 01 00:00:02 1970 +0000
1017 1029 @@ -1,3 +1,4 @@
1018 1030 expand $Id$
1019 1031 do not process $Id:
1020 1032 xxx $
1021 1033 +$Xinfo$
1022 1034
1023 1035
1024 1036
1025 1037
1026 1038 $ cat errors.log
1027 1039 #endif
1028 1040
1029 1041 Prepare merge and resolve tests
1030 1042
1031 1043 $ echo '$Id$' > m
1032 1044 $ hg add m
1033 1045 $ hg commit -m 4kw
1034 1046 $ echo foo >> m
1035 1047 $ hg commit -m 5foo
1036 1048
1037 1049 simplemerge
1038 1050
1039 1051 $ hg update 4
1040 1052 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1041 1053 $ echo foo >> m
1042 1054 $ hg commit -m 6foo
1043 1055 created new head
1044 1056 $ hg merge
1045 1057 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1046 1058 (branch merge, don't forget to commit)
1047 1059 $ hg commit -m simplemerge
1048 1060 $ cat m
1049 1061 $Id: m 27d48ee14f67 Thu, 01 Jan 1970 00:00:00 +0000 test $
1050 1062 foo
1051 1063
1052 1064 conflict: keyword should stay outside conflict zone
1053 1065
1054 1066 $ hg update 4
1055 1067 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1056 1068 $ echo bar >> m
1057 1069 $ hg commit -m 8bar
1058 1070 created new head
1059 1071 $ hg merge
1060 1072 merging m
1061 1073 warning: conflicts during merge.
1062 1074 merging m incomplete! (edit conflicts, then use 'hg resolve --mark')
1063 1075 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
1064 1076 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
1065 1077 [1]
1066 1078 $ cat m
1067 1079 $Id$
1068 1080 <<<<<<< local: 88a80c8d172e - test: 8bar
1069 1081 bar
1070 1082 =======
1071 1083 foo
1072 1084 >>>>>>> other: 85d2d2d732a5 - test: simplemerge
1073 1085
1074 1086 resolve to local, m must contain hash of last change (local parent)
1075 1087
1076 1088 $ hg resolve -t internal:local -a
1077 1089 (no more unresolved files)
1078 1090 $ hg commit -m localresolve
1079 1091 $ cat m
1080 1092 $Id: m 88a80c8d172e Thu, 01 Jan 1970 00:00:00 +0000 test $
1081 1093 bar
1082 1094
1083 1095 Test restricted mode with transplant -b
1084 1096
1085 1097 $ hg update 6
1086 1098 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1087 1099 $ hg branch foo
1088 1100 marked working directory as branch foo
1089 1101 (branches are permanent and global, did you want a bookmark?)
1090 1102 $ mv a a.bak
1091 1103 $ echo foobranch > a
1092 1104 $ cat a.bak >> a
1093 1105 $ rm a.bak
1094 1106 $ hg commit -m 9foobranch
1095 1107 $ hg update default
1096 1108 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1097 1109 $ hg -y transplant -b foo tip
1098 1110 applying 4aa30d025d50
1099 1111 4aa30d025d50 transplanted to e00abbf63521
1100 1112
1101 1113 Expansion in changeset but not in file
1102 1114
1103 1115 $ hg tip -p
1104 1116 changeset: 11:e00abbf63521
1105 1117 tag: tip
1106 1118 parent: 9:800511b3a22d
1107 1119 user: test
1108 1120 date: Thu Jan 01 00:00:00 1970 +0000
1109 1121 summary: 9foobranch
1110 1122
1111 1123 diff -r 800511b3a22d -r e00abbf63521 a
1112 1124 --- a/a Thu Jan 01 00:00:00 1970 +0000
1113 1125 +++ b/a Thu Jan 01 00:00:00 1970 +0000
1114 1126 @@ -1,3 +1,4 @@
1115 1127 +foobranch
1116 1128 expand $Id$
1117 1129 do not process $Id:
1118 1130 xxx $
1119 1131
1120 1132 $ head -n 2 a
1121 1133 foobranch
1122 1134 expand $Id: a e00abbf63521 Thu, 01 Jan 1970 00:00:00 +0000 test $
1123 1135
1124 1136 Turn off expansion
1125 1137
1126 1138 $ hg -q rollback
1127 1139 $ hg -q update -C
1128 1140
1129 1141 kwshrink with unknown file u
1130 1142
1131 1143 $ cp a u
1132 1144 $ hg --verbose kwshrink
1133 1145 overwriting a shrinking keywords
1134 1146 overwriting m shrinking keywords
1135 1147 overwriting x/a shrinking keywords
1136 1148
1137 1149 Keywords shrunk in working directory, but not yet disabled
1138 1150 - cat shows unexpanded keywords
1139 1151 - hg cat shows expanded keywords
1140 1152
1141 1153 $ cat a b
1142 1154 expand $Id$
1143 1155 do not process $Id:
1144 1156 xxx $
1145 1157 $Xinfo$
1146 1158 ignore $Id$
1147 1159 $ hg cat sym a b && echo
1148 1160 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
1149 1161 do not process $Id:
1150 1162 xxx $
1151 1163 $Xinfo: User Name <user@example.com>: firstline $
1152 1164 ignore $Id$
1153 1165 a
1154 1166
1155 1167 Now disable keyword expansion
1156 1168
1157 1169 $ cp $HGRCPATH $HGRCPATH.backup
1158 1170 $ rm "$HGRCPATH"
1159 1171 $ cat a b
1160 1172 expand $Id$
1161 1173 do not process $Id:
1162 1174 xxx $
1163 1175 $Xinfo$
1164 1176 ignore $Id$
1165 1177 $ hg cat sym a b && echo
1166 1178 expand $Id$
1167 1179 do not process $Id:
1168 1180 xxx $
1169 1181 $Xinfo$
1170 1182 ignore $Id$
1171 1183 a
1172 1184
1173 1185 enable keyword expansion again
1174 1186
1175 1187 $ cat $HGRCPATH.backup >> $HGRCPATH
1176 1188
1177 1189 Test restricted mode with unshelve
1178 1190
1179 1191 $ cat <<EOF >> $HGRCPATH
1180 1192 > [extensions]
1181 1193 > shelve =
1182 1194 > EOF
1183 1195
1184 1196 $ echo xxxx >> a
1185 1197 $ hg diff
1186 1198 diff -r 800511b3a22d a
1187 1199 --- a/a Thu Jan 01 00:00:00 1970 +0000
1188 1200 +++ b/a * (glob)
1189 1201 @@ -2,3 +2,4 @@
1190 1202 do not process $Id:
1191 1203 xxx $
1192 1204 $Xinfo$
1193 1205 +xxxx
1194 1206 $ hg shelve -q --name tmp
1195 1207 $ hg shelve --list --patch
1196 1208 tmp (*) changes to 'localresolve' (glob)
1197 1209
1198 1210 diff --git a/a b/a
1199 1211 --- a/a
1200 1212 +++ b/a
1201 1213 @@ -2,3 +2,4 @@
1202 1214 do not process $Id:
1203 1215 xxx $
1204 1216 $Xinfo$
1205 1217 +xxxx
1206 1218
1207 1219 $ hg update -q -C 10
1208 1220 $ hg unshelve -q tmp
1209 1221 $ hg diff
1210 1222 diff -r 4aa30d025d50 a
1211 1223 --- a/a Thu Jan 01 00:00:00 1970 +0000
1212 1224 +++ b/a * (glob)
1213 1225 @@ -3,3 +3,4 @@
1214 1226 do not process $Id:
1215 1227 xxx $
1216 1228 $Xinfo$
1217 1229 +xxxx
1218 1230
1219 1231 Test restricted mode with rebase
1220 1232
1221 1233 $ cat <<EOF >> $HGRCPATH
1222 1234 > [extensions]
1223 1235 > rebase =
1224 1236 > EOF
1225 1237
1226 1238 $ hg update -q -C 9
1227 1239
1228 1240 $ echo xxxx >> a
1229 1241 $ hg commit -m '#11'
1230 1242 $ hg diff -c 11
1231 1243 diff -r 800511b3a22d -r b07670694489 a
1232 1244 --- a/a Thu Jan 01 00:00:00 1970 +0000
1233 1245 +++ b/a Thu Jan 01 00:00:00 1970 +0000
1234 1246 @@ -2,3 +2,4 @@
1235 1247 do not process $Id:
1236 1248 xxx $
1237 1249 $Xinfo$
1238 1250 +xxxx
1239 1251
1240 1252 $ hg diff -c 10
1241 1253 diff -r 27d48ee14f67 -r 4aa30d025d50 a
1242 1254 --- a/a Thu Jan 01 00:00:00 1970 +0000
1243 1255 +++ b/a Thu Jan 01 00:00:00 1970 +0000
1244 1256 @@ -1,3 +1,4 @@
1245 1257 +foobranch
1246 1258 expand $Id$
1247 1259 do not process $Id:
1248 1260 xxx $
1249 1261
1250 1262 $ hg rebase -q -s 10 -d 11 --keep
1251 1263 $ hg diff -r 9 -r 12 a
1252 1264 diff -r 800511b3a22d -r 1939b927726c a
1253 1265 --- a/a Thu Jan 01 00:00:00 1970 +0000
1254 1266 +++ b/a Thu Jan 01 00:00:00 1970 +0000
1255 1267 @@ -1,4 +1,6 @@
1256 1268 +foobranch
1257 1269 expand $Id$
1258 1270 do not process $Id:
1259 1271 xxx $
1260 1272 $Xinfo$
1261 1273 +xxxx
1262 1274
1263 1275 Test restricted mode with graft
1264 1276
1265 1277 $ hg graft -q 10
1266 1278 $ hg diff -r 9 -r 13 a
1267 1279 diff -r 800511b3a22d -r 01a68de1003a a
1268 1280 --- a/a Thu Jan 01 00:00:00 1970 +0000
1269 1281 +++ b/a Thu Jan 01 00:00:00 1970 +0000
1270 1282 @@ -1,4 +1,6 @@
1271 1283 +foobranch
1272 1284 expand $Id$
1273 1285 do not process $Id:
1274 1286 xxx $
1275 1287 $Xinfo$
1276 1288 +xxxx
1277 1289
1278 1290 Test restricted mode with backout
1279 1291
1280 1292 $ hg backout -q 11
1281 1293 $ hg diff a
1282 1294 diff -r 01a68de1003a a
1283 1295 --- a/a Thu Jan 01 00:00:00 1970 +0000
1284 1296 +++ b/a * (glob)
1285 1297 @@ -3,4 +3,3 @@
1286 1298 do not process $Id:
1287 1299 xxx $
1288 1300 $Xinfo$
1289 1301 -xxxx
1290 1302
1291 1303 Test restricted mode with histedit
1292 1304
1293 1305 $ cat <<EOF >> $HGRCPATH
1294 1306 > [extensions]
1295 1307 > histedit =
1296 1308 > EOF
1297 1309
1298 1310 $ hg commit -m 'backout #11'
1299 1311 $ hg histedit -q --command - 13 <<EOF
1300 1312 > pick 49f5f2d940c3 14 backout #11
1301 1313 > pick 01a68de1003a 13 9foobranch
1302 1314 > EOF
1303 1315
1304 1316 Test restricted mode with fetch (with merge)
1305 1317
1306 1318 $ cat <<EOF >> $HGRCPATH
1307 1319 > [extensions]
1308 1320 > fetch =
1309 1321 > EOF
1310 1322
1311 1323 $ hg clone -q -r 9 . ../fetch-merge
1312 1324 $ cd ../fetch-merge
1313 1325 $ hg -R ../Test export 10 | hg import -q -
1314 1326 $ hg fetch -q -r 11
1315 1327 $ hg diff -r 9 a
1316 1328 diff -r 800511b3a22d a
1317 1329 --- a/a Thu Jan 01 00:00:00 1970 +0000
1318 1330 +++ b/a * (glob)
1319 1331 @@ -1,4 +1,6 @@
1320 1332 +foobranch
1321 1333 expand $Id$
1322 1334 do not process $Id:
1323 1335 xxx $
1324 1336 $Xinfo$
1325 1337 +xxxx
1326 1338
1327 1339 $ cd ..
@@ -1,610 +1,620 b''
1 1 $ cat <<EOF >> $HGRCPATH
2 2 > [ui]
3 3 > commitsubrepos = Yes
4 4 > [extensions]
5 5 > mq =
6 6 > record =
7 7 > [diff]
8 8 > nodates = 1
9 9 > EOF
10 10
11 11 $ stdin=`pwd`/stdin.tmp
12 12
13 13 fn to create new repository w/dirty subrepo, and cd into it
14 14 $ mkrepo() {
15 15 > hg init $1
16 16 > cd $1
17 17 > hg qinit
18 18 > }
19 19
20 20 fn to create dirty subrepo
21 21 $ mksubrepo() {
22 22 > hg init $1
23 23 > cd $1
24 24 > echo a > a
25 25 > hg add
26 26 > cd ..
27 27 > }
28 28
29 29 $ testadd() {
30 30 > cat - > "$stdin"
31 31 > mksubrepo sub
32 32 > echo sub = sub >> .hgsub
33 33 > hg add .hgsub
34 34 > echo % abort when adding .hgsub w/dirty subrepo
35 35 > hg status -S
36 36 > echo '%' $*
37 37 > cat "$stdin" | hg $*
38 38 > echo [$?]
39 39 > hg -R sub ci -m0sub
40 40 > echo % update substate when adding .hgsub w/clean updated subrepo
41 41 > hg status -S
42 42 > echo '%' $*
43 43 > cat "$stdin" | hg $*
44 44 > hg debugsub
45 45 > }
46 46
47 47 $ testmod() {
48 48 > cat - > "$stdin"
49 49 > mksubrepo sub2
50 50 > echo sub2 = sub2 >> .hgsub
51 51 > echo % abort when modifying .hgsub w/dirty subrepo
52 52 > hg status -S
53 53 > echo '%' $*
54 54 > cat "$stdin" | hg $*
55 55 > echo [$?]
56 56 > hg -R sub2 ci -m0sub2
57 57 > echo % update substate when modifying .hgsub w/clean updated subrepo
58 58 > hg status -S
59 59 > echo '%' $*
60 60 > cat "$stdin" | hg $*
61 61 > hg debugsub
62 62 > }
63 63
64 64 $ testrm1() {
65 65 > cat - > "$stdin"
66 66 > mksubrepo sub3
67 67 > echo sub3 = sub3 >> .hgsub
68 68 > hg ci -Aqmsub3
69 69 > $EXTRA
70 70 > echo b >> sub3/a
71 71 > hg rm .hgsub
72 72 > echo % update substate when removing .hgsub w/dirty subrepo
73 73 > hg status -S
74 74 > echo '%' $*
75 75 > cat "$stdin" | hg $*
76 76 > echo % debugsub should be empty
77 77 > hg debugsub
78 78 > }
79 79
80 80 $ testrm2() {
81 81 > cat - > "$stdin"
82 82 > mksubrepo sub4
83 83 > echo sub4 = sub4 >> .hgsub
84 84 > hg ci -Aqmsub4
85 85 > $EXTRA
86 86 > hg rm .hgsub
87 87 > echo % update substate when removing .hgsub w/clean updated subrepo
88 88 > hg status -S
89 89 > echo '%' $*
90 90 > cat "$stdin" | hg $*
91 91 > echo % debugsub should be empty
92 92 > hg debugsub
93 93 > }
94 94
95 95
96 96 handle subrepos safely on qnew
97 97
98 98 $ mkrepo repo-2499-qnew
99 99 $ testadd qnew -X path:no-effect -m0 0.diff
100 100 adding a
101 101 % abort when adding .hgsub w/dirty subrepo
102 102 A .hgsub
103 103 A sub/a
104 104 % qnew -X path:no-effect -m0 0.diff
105 105 abort: uncommitted changes in subrepository 'sub'
106 106 [255]
107 107 % update substate when adding .hgsub w/clean updated subrepo
108 108 A .hgsub
109 109 % qnew -X path:no-effect -m0 0.diff
110 110 path sub
111 111 source sub
112 112 revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31
113 113
114 114 $ testmod qnew --cwd .. -R repo-2499-qnew -X path:no-effect -m1 1.diff
115 115 adding a
116 116 % abort when modifying .hgsub w/dirty subrepo
117 117 M .hgsub
118 118 A sub2/a
119 119 % qnew --cwd .. -R repo-2499-qnew -X path:no-effect -m1 1.diff
120 120 abort: uncommitted changes in subrepository 'sub2'
121 121 [255]
122 122 % update substate when modifying .hgsub w/clean updated subrepo
123 123 M .hgsub
124 124 % qnew --cwd .. -R repo-2499-qnew -X path:no-effect -m1 1.diff
125 125 path sub
126 126 source sub
127 127 revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31
128 128 path sub2
129 129 source sub2
130 130 revision 1f94c7611cc6b74f5a17b16121a1170d44776845
131 131
132 132 $ hg qpop -qa
133 133 patch queue now empty
134 134 $ testrm1 qnew -m2 2.diff
135 135 adding a
136 136 % update substate when removing .hgsub w/dirty subrepo
137 137 M sub3/a
138 138 R .hgsub
139 139 % qnew -m2 2.diff
140 140 % debugsub should be empty
141 141
142 142 $ hg qpop -qa
143 143 patch queue now empty
144 144 $ testrm2 qnew -m3 3.diff
145 145 adding a
146 146 % update substate when removing .hgsub w/clean updated subrepo
147 147 R .hgsub
148 148 % qnew -m3 3.diff
149 149 % debugsub should be empty
150 150
151 151 $ cd ..
152 152
153 153
154 154 handle subrepos safely on qrefresh
155 155
156 156 $ mkrepo repo-2499-qrefresh
157 157 $ hg qnew -m0 0.diff
158 158 $ testadd qrefresh
159 159 adding a
160 160 % abort when adding .hgsub w/dirty subrepo
161 161 A .hgsub
162 162 A sub/a
163 163 % qrefresh
164 164 abort: uncommitted changes in subrepository 'sub'
165 165 [255]
166 166 % update substate when adding .hgsub w/clean updated subrepo
167 167 A .hgsub
168 168 % qrefresh
169 169 path sub
170 170 source sub
171 171 revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31
172 172
173 173 $ hg qnew -m1 1.diff
174 174 $ testmod qrefresh
175 175 adding a
176 176 % abort when modifying .hgsub w/dirty subrepo
177 177 M .hgsub
178 178 A sub2/a
179 179 % qrefresh
180 180 abort: uncommitted changes in subrepository 'sub2'
181 181 [255]
182 182 % update substate when modifying .hgsub w/clean updated subrepo
183 183 M .hgsub
184 184 % qrefresh
185 185 path sub
186 186 source sub
187 187 revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31
188 188 path sub2
189 189 source sub2
190 190 revision 1f94c7611cc6b74f5a17b16121a1170d44776845
191 191
192 192 $ hg qpop -qa
193 193 patch queue now empty
194 194 $ EXTRA='hg qnew -m2 2.diff'
195 195 $ testrm1 qrefresh
196 196 adding a
197 197 % update substate when removing .hgsub w/dirty subrepo
198 198 M sub3/a
199 199 R .hgsub
200 200 % qrefresh
201 201 % debugsub should be empty
202 202
203 203 $ hg qpop -qa
204 204 patch queue now empty
205 205 $ EXTRA='hg qnew -m3 3.diff'
206 206 $ testrm2 qrefresh
207 207 adding a
208 208 % update substate when removing .hgsub w/clean updated subrepo
209 209 R .hgsub
210 210 % qrefresh
211 211 % debugsub should be empty
212 212 $ EXTRA=
213 213
214 214 $ cd ..
215 215
216 216
217 217 handle subrepos safely on qpush/qpop
218 218 (and we cannot qpop / qpush with a modified subrepo)
219 219
220 220 $ mkrepo repo-2499-qpush
221 221 $ mksubrepo sub
222 222 adding a
223 223 $ hg -R sub ci -m0sub
224 224 $ echo sub = sub > .hgsub
225 225 $ hg add .hgsub
226 226 $ hg commit -m0
227 227 $ hg debugsub
228 228 path sub
229 229 source sub
230 230 revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31
231 231 $ echo foo > ./sub/a
232 232 $ hg -R sub commit -m foo
233 233 $ hg commit -m1
234 234 $ hg qimport -r "0:tip"
235 235 $ hg -R sub id --id
236 236 aa037b301eba
237 237
238 238 qpop
239 239 $ hg -R sub update 0000
240 240 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
241 241 $ hg qpop
242 242 abort: local changed subrepos found, refresh first
243 243 [255]
244 244 $ hg revert sub
245 245 reverting subrepo sub
246 246 adding sub/a (glob)
247 247 $ hg qpop
248 248 popping 1.diff
249 249 now at: 0.diff
250 250 $ hg status -AS
251 251 C .hgsub
252 252 C .hgsubstate
253 253 C sub/a
254 254 $ hg -R sub id --id
255 255 b2fdb12cd82b
256 256
257 257 qpush
258 258 $ hg -R sub update 0000
259 259 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
260 260 $ hg qpush
261 261 abort: local changed subrepos found, refresh first
262 262 [255]
263 263 $ hg revert sub
264 264 reverting subrepo sub
265 265 adding sub/a (glob)
266 266 $ hg qpush
267 267 applying 1.diff
268 268 subrepository sub diverged (local revision: b2fdb12cd82b, remote revision: aa037b301eba)
269 269 (M)erge, keep (l)ocal or keep (r)emote? m
270 270 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
271 271 now at: 1.diff
272 272 $ hg status -AS
273 273 C .hgsub
274 274 C .hgsubstate
275 275 C sub/a
276 276 $ hg -R sub id --id
277 277 aa037b301eba
278 278
279 279 $ cd ..
280 280
281 281
282 282 handle subrepos safely on qrecord
283 283
284 284 $ mkrepo repo-2499-qrecord
285 285 $ testadd qrecord --config ui.interactive=1 -m0 0.diff <<EOF
286 286 > y
287 287 > y
288 288 > EOF
289 289 adding a
290 290 % abort when adding .hgsub w/dirty subrepo
291 291 A .hgsub
292 292 A sub/a
293 293 % qrecord --config ui.interactive=1 -m0 0.diff
294 294 diff --git a/.hgsub b/.hgsub
295 295 new file mode 100644
296 296 examine changes to '.hgsub'? [Ynesfdaq?] y
297 297
298 @@ -0,0 +1,1 @@
299 +sub = sub
300 record this change to '.hgsub'? [Ynesfdaq?] y
301
302 warning: subrepo spec file '.hgsub' not found
298 303 abort: uncommitted changes in subrepository 'sub'
299 304 [255]
300 305 % update substate when adding .hgsub w/clean updated subrepo
301 306 A .hgsub
302 307 % qrecord --config ui.interactive=1 -m0 0.diff
303 308 diff --git a/.hgsub b/.hgsub
304 309 new file mode 100644
305 310 examine changes to '.hgsub'? [Ynesfdaq?] y
306 311
312 @@ -0,0 +1,1 @@
313 +sub = sub
314 record this change to '.hgsub'? [Ynesfdaq?] y
315
316 warning: subrepo spec file '.hgsub' not found
307 317 path sub
308 318 source sub
309 319 revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31
310 320 $ testmod qrecord --config ui.interactive=1 -m1 1.diff <<EOF
311 321 > y
312 322 > y
313 323 > EOF
314 324 adding a
315 325 % abort when modifying .hgsub w/dirty subrepo
316 326 M .hgsub
317 327 A sub2/a
318 328 % qrecord --config ui.interactive=1 -m1 1.diff
319 329 diff --git a/.hgsub b/.hgsub
320 330 1 hunks, 1 lines changed
321 331 examine changes to '.hgsub'? [Ynesfdaq?] y
322 332
323 333 @@ -1,1 +1,2 @@
324 334 sub = sub
325 335 +sub2 = sub2
326 336 record this change to '.hgsub'? [Ynesfdaq?] y
327 337
328 338 abort: uncommitted changes in subrepository 'sub2'
329 339 [255]
330 340 % update substate when modifying .hgsub w/clean updated subrepo
331 341 M .hgsub
332 342 % qrecord --config ui.interactive=1 -m1 1.diff
333 343 diff --git a/.hgsub b/.hgsub
334 344 1 hunks, 1 lines changed
335 345 examine changes to '.hgsub'? [Ynesfdaq?] y
336 346
337 347 @@ -1,1 +1,2 @@
338 348 sub = sub
339 349 +sub2 = sub2
340 350 record this change to '.hgsub'? [Ynesfdaq?] y
341 351
342 352 path sub
343 353 source sub
344 354 revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31
345 355 path sub2
346 356 source sub2
347 357 revision 1f94c7611cc6b74f5a17b16121a1170d44776845
348 358
349 359 $ hg qpop -qa
350 360 patch queue now empty
351 361 $ testrm1 qrecord --config ui.interactive=1 -m2 2.diff <<EOF
352 362 > y
353 363 > y
354 364 > EOF
355 365 adding a
356 366 % update substate when removing .hgsub w/dirty subrepo
357 367 M sub3/a
358 368 R .hgsub
359 369 % qrecord --config ui.interactive=1 -m2 2.diff
360 370 diff --git a/.hgsub b/.hgsub
361 371 deleted file mode 100644
362 372 examine changes to '.hgsub'? [Ynesfdaq?] y
363 373
364 374 % debugsub should be empty
365 375
366 376 $ hg qpop -qa
367 377 patch queue now empty
368 378 $ testrm2 qrecord --config ui.interactive=1 -m3 3.diff <<EOF
369 379 > y
370 380 > y
371 381 > EOF
372 382 adding a
373 383 % update substate when removing .hgsub w/clean updated subrepo
374 384 R .hgsub
375 385 % qrecord --config ui.interactive=1 -m3 3.diff
376 386 diff --git a/.hgsub b/.hgsub
377 387 deleted file mode 100644
378 388 examine changes to '.hgsub'? [Ynesfdaq?] y
379 389
380 390 % debugsub should be empty
381 391
382 392 $ cd ..
383 393
384 394
385 395 correctly handle subrepos with patch queues
386 396 $ mkrepo repo-subrepo-with-queue
387 397 $ mksubrepo sub
388 398 adding a
389 399 $ hg -R sub qnew sub0.diff
390 400 $ echo sub = sub >> .hgsub
391 401 $ hg add .hgsub
392 402 $ hg qnew 0.diff
393 403
394 404 $ cd ..
395 405
396 406 check whether MQ operations can import updated .hgsubstate correctly
397 407 both into 'revision' and 'patch file under .hg/patches':
398 408
399 409 $ hg init importing-hgsubstate
400 410 $ cd importing-hgsubstate
401 411
402 412 $ echo a > a
403 413 $ hg commit -u test -d '0 0' -Am '#0 in parent'
404 414 adding a
405 415 $ hg init sub
406 416 $ echo sa > sub/sa
407 417 $ hg -R sub commit -u test -d '0 0' -Am '#0 in sub'
408 418 adding sa
409 419 $ echo 'sub = sub' > .hgsub
410 420 $ touch .hgsubstate
411 421 $ hg add .hgsub .hgsubstate
412 422
413 423 $ hg qnew -u test -d '0 0' import-at-qnew
414 424 $ hg -R sub parents --template '{node} sub\n'
415 425 b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
416 426 $ cat .hgsubstate
417 427 b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
418 428 $ hg diff -c tip
419 429 diff -r f499373e340c -r f69e96d86e75 .hgsub
420 430 --- /dev/null
421 431 +++ b/.hgsub
422 432 @@ -0,0 +1,1 @@
423 433 +sub = sub
424 434 diff -r f499373e340c -r f69e96d86e75 .hgsubstate
425 435 --- /dev/null
426 436 +++ b/.hgsubstate
427 437 @@ -0,0 +1,1 @@
428 438 +b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
429 439 $ cat .hg/patches/import-at-qnew
430 440 # HG changeset patch
431 441 # User test
432 442 # Date 0 0
433 443 # Parent f499373e340cdca5d01dee904aeb42dd2a325e71
434 444
435 445 diff -r f499373e340c -r f69e96d86e75 .hgsub
436 446 --- /dev/null
437 447 +++ b/.hgsub
438 448 @@ -0,0 +1,1 @@
439 449 +sub = sub
440 450 diff -r f499373e340c -r f69e96d86e75 .hgsubstate
441 451 --- /dev/null
442 452 +++ b/.hgsubstate
443 453 @@ -0,0 +1,1 @@
444 454 +b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
445 455 $ hg parents --template '{node}\n'
446 456 f69e96d86e75a6d4fd88285dc9697acb23951041
447 457 $ hg parents --template '{files}\n'
448 458 .hgsub .hgsubstate
449 459
450 460 check also whether qnew not including ".hgsubstate" explicitly causes
451 461 as same result (in node hash) as one including it.
452 462
453 463 $ hg qpop -a -q
454 464 patch queue now empty
455 465 $ hg qdelete import-at-qnew
456 466 $ echo 'sub = sub' > .hgsub
457 467 $ hg add .hgsub
458 468 $ rm -f .hgsubstate
459 469 $ hg qnew -u test -d '0 0' import-at-qnew
460 470 $ hg parents --template '{node}\n'
461 471 f69e96d86e75a6d4fd88285dc9697acb23951041
462 472 $ hg parents --template '{files}\n'
463 473 .hgsub .hgsubstate
464 474
465 475 check whether qrefresh imports updated .hgsubstate correctly
466 476
467 477 $ hg qpop
468 478 popping import-at-qnew
469 479 patch queue now empty
470 480 $ hg qpush
471 481 applying import-at-qnew
472 482 now at: import-at-qnew
473 483 $ hg parents --template '{files}\n'
474 484 .hgsub .hgsubstate
475 485
476 486 $ hg qnew import-at-qrefresh
477 487 $ echo sb > sub/sb
478 488 $ hg -R sub commit -u test -d '0 0' -Am '#1 in sub'
479 489 adding sb
480 490 $ hg qrefresh -u test -d '0 0'
481 491 $ hg -R sub parents --template '{node} sub\n'
482 492 88ac1bef5ed43b689d1d200b59886b675dec474b sub
483 493 $ cat .hgsubstate
484 494 88ac1bef5ed43b689d1d200b59886b675dec474b sub
485 495 $ hg diff -c tip
486 496 diff -r 05b056bb9c8c -r d987bec230f4 .hgsubstate
487 497 --- a/.hgsubstate
488 498 +++ b/.hgsubstate
489 499 @@ -1,1 +1,1 @@
490 500 -b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
491 501 +88ac1bef5ed43b689d1d200b59886b675dec474b sub
492 502 $ cat .hg/patches/import-at-qrefresh
493 503 # HG changeset patch
494 504 # User test
495 505 # Date 0 0
496 506 # Parent 05b056bb9c8c05ff15258b84fd42ab3527271033
497 507
498 508 diff -r 05b056bb9c8c .hgsubstate
499 509 --- a/.hgsubstate
500 510 +++ b/.hgsubstate
501 511 @@ -1,1 +1,1 @@
502 512 -b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
503 513 +88ac1bef5ed43b689d1d200b59886b675dec474b sub
504 514 $ hg parents --template '{files}\n'
505 515 .hgsubstate
506 516
507 517 $ hg qrefresh -u test -d '0 0'
508 518 $ cat .hgsubstate
509 519 88ac1bef5ed43b689d1d200b59886b675dec474b sub
510 520 $ hg diff -c tip
511 521 diff -r 05b056bb9c8c -r d987bec230f4 .hgsubstate
512 522 --- a/.hgsubstate
513 523 +++ b/.hgsubstate
514 524 @@ -1,1 +1,1 @@
515 525 -b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
516 526 +88ac1bef5ed43b689d1d200b59886b675dec474b sub
517 527 $ cat .hg/patches/import-at-qrefresh
518 528 # HG changeset patch
519 529 # User test
520 530 # Date 0 0
521 531 # Parent 05b056bb9c8c05ff15258b84fd42ab3527271033
522 532
523 533 diff -r 05b056bb9c8c .hgsubstate
524 534 --- a/.hgsubstate
525 535 +++ b/.hgsubstate
526 536 @@ -1,1 +1,1 @@
527 537 -b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
528 538 +88ac1bef5ed43b689d1d200b59886b675dec474b sub
529 539 $ hg parents --template '{files}\n'
530 540 .hgsubstate
531 541
532 542 $ hg update -C tip
533 543 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
534 544 $ hg qpop -a
535 545 popping import-at-qrefresh
536 546 popping import-at-qnew
537 547 patch queue now empty
538 548
539 549 $ hg -R sub update -C 0
540 550 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
541 551 $ echo 'sub = sub' > .hgsub
542 552 $ hg commit -Am '#1 in parent'
543 553 adding .hgsub
544 554 $ hg -R sub update -C 1
545 555 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
546 556 $ hg commit -Am '#2 in parent (but will be rolled back soon)'
547 557 $ hg rollback
548 558 repository tip rolled back to revision 1 (undo commit)
549 559 working directory now based on revision 1
550 560 $ hg status
551 561 M .hgsubstate
552 562 $ hg qnew -u test -d '0 0' checkstate-at-qnew
553 563 $ hg -R sub parents --template '{node} sub\n'
554 564 88ac1bef5ed43b689d1d200b59886b675dec474b sub
555 565 $ cat .hgsubstate
556 566 88ac1bef5ed43b689d1d200b59886b675dec474b sub
557 567 $ hg diff -c tip
558 568 diff -r 4d91eb2fa1d1 -r 1259c112d884 .hgsubstate
559 569 --- a/.hgsubstate
560 570 +++ b/.hgsubstate
561 571 @@ -1,1 +1,1 @@
562 572 -b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
563 573 +88ac1bef5ed43b689d1d200b59886b675dec474b sub
564 574 $ cat .hg/patches/checkstate-at-qnew
565 575 # HG changeset patch
566 576 # User test
567 577 # Date 0 0
568 578 # Parent 4d91eb2fa1d1b22ec513347b9cd06f6b49d470fa
569 579
570 580 diff -r 4d91eb2fa1d1 -r 1259c112d884 .hgsubstate
571 581 --- a/.hgsubstate
572 582 +++ b/.hgsubstate
573 583 @@ -1,1 +1,1 @@
574 584 -b6f6e9c41f3dfd374a6d2ed4535c87951cf979cf sub
575 585 +88ac1bef5ed43b689d1d200b59886b675dec474b sub
576 586 $ hg parents --template '{files}\n'
577 587 .hgsubstate
578 588
579 589 check whether qrefresh not including ".hgsubstate" explicitly causes
580 590 as same result (in node hash) as one including it.
581 591
582 592 $ hg update -C -q 0
583 593 $ hg qpop -a -q
584 594 patch queue now empty
585 595 $ hg qnew -u test -d '0 0' add-hgsub-at-qrefresh
586 596 $ echo 'sub = sub' > .hgsub
587 597 $ echo > .hgsubstate
588 598 $ hg add .hgsub .hgsubstate
589 599 $ hg qrefresh -u test -d '0 0'
590 600 $ hg parents --template '{node}\n'
591 601 7c48c35501aae6770ed9c2517014628615821a8e
592 602 $ hg parents --template '{files}\n'
593 603 .hgsub .hgsubstate
594 604
595 605 $ hg qpop -a -q
596 606 patch queue now empty
597 607 $ hg qdelete add-hgsub-at-qrefresh
598 608 $ hg qnew -u test -d '0 0' add-hgsub-at-qrefresh
599 609 $ echo 'sub = sub' > .hgsub
600 610 $ hg add .hgsub
601 611 $ rm -f .hgsubstate
602 612 $ hg qrefresh -u test -d '0 0'
603 613 $ hg parents --template '{node}\n'
604 614 7c48c35501aae6770ed9c2517014628615821a8e
605 615 $ hg parents --template '{files}\n'
606 616 .hgsub .hgsubstate
607 617
608 618 $ cd ..
609 619
610 620 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now