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