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