47 |
47 |
48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
50 # be specifying the version(s) of Mercurial they are tested with, or |
50 # be specifying the version(s) of Mercurial they are tested with, or |
51 # leave the attribute unspecified. |
51 # leave the attribute unspecified. |
52 testedwith = 'ships-with-hg-core' |
52 testedwith = b'ships-with-hg-core' |
53 |
53 |
54 # storage format version; increment when the format changes |
54 # storage format version; increment when the format changes |
55 storageversion = 0 |
55 storageversion = 0 |
56 |
56 |
57 # namespaces |
57 # namespaces |
58 bookmarktype = 'bookmark' |
58 bookmarktype = b'bookmark' |
59 wdirparenttype = 'wdirparent' |
59 wdirparenttype = b'wdirparent' |
60 # In a shared repository, what shared feature name is used |
60 # In a shared repository, what shared feature name is used |
61 # to indicate this namespace is shared with the source? |
61 # to indicate this namespace is shared with the source? |
62 sharednamespaces = { |
62 sharednamespaces = { |
63 bookmarktype: hg.sharedbookmarks, |
63 bookmarktype: hg.sharedbookmarks, |
64 } |
64 } |
65 |
65 |
66 # Journal recording, register hooks and storage object |
66 # Journal recording, register hooks and storage object |
67 def extsetup(ui): |
67 def extsetup(ui): |
68 extensions.wrapfunction(dispatch, 'runcommand', runcommand) |
68 extensions.wrapfunction(dispatch, b'runcommand', runcommand) |
69 extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks) |
69 extensions.wrapfunction(bookmarks.bmstore, b'_write', recordbookmarks) |
70 extensions.wrapfilecache( |
70 extensions.wrapfilecache( |
71 localrepo.localrepository, 'dirstate', wrapdirstate |
71 localrepo.localrepository, b'dirstate', wrapdirstate |
72 ) |
72 ) |
73 extensions.wrapfunction(hg, 'postshare', wrappostshare) |
73 extensions.wrapfunction(hg, b'postshare', wrappostshare) |
74 extensions.wrapfunction(hg, 'copystore', unsharejournal) |
74 extensions.wrapfunction(hg, b'copystore', unsharejournal) |
75 |
75 |
76 |
76 |
77 def reposetup(ui, repo): |
77 def reposetup(ui, repo): |
78 if repo.local(): |
78 if repo.local(): |
79 repo.journal = journalstorage(repo) |
79 repo.journal = journalstorage(repo) |
80 repo._wlockfreeprefix.add('namejournal') |
80 repo._wlockfreeprefix.add(b'namejournal') |
81 |
81 |
82 dirstate, cached = localrepo.isfilecached(repo, 'dirstate') |
82 dirstate, cached = localrepo.isfilecached(repo, b'dirstate') |
83 if cached: |
83 if cached: |
84 # already instantiated dirstate isn't yet marked as |
84 # already instantiated dirstate isn't yet marked as |
85 # "journal"-ing, even though repo.dirstate() was already |
85 # "journal"-ing, even though repo.dirstate() was already |
86 # wrapped by own wrapdirstate() |
86 # wrapped by own wrapdirstate() |
87 _setupdirstate(repo, dirstate) |
87 _setupdirstate(repo, dirstate) |
93 return orig(lui, repo, cmd, fullargs, *args) |
93 return orig(lui, repo, cmd, fullargs, *args) |
94 |
94 |
95 |
95 |
96 def _setupdirstate(repo, dirstate): |
96 def _setupdirstate(repo, dirstate): |
97 dirstate.journalstorage = repo.journal |
97 dirstate.journalstorage = repo.journal |
98 dirstate.addparentchangecallback('journal', recorddirstateparents) |
98 dirstate.addparentchangecallback(b'journal', recorddirstateparents) |
99 |
99 |
100 |
100 |
101 # hooks to record dirstate changes |
101 # hooks to record dirstate changes |
102 def wrapdirstate(orig, repo): |
102 def wrapdirstate(orig, repo): |
103 """Make journal storage available to the dirstate object""" |
103 """Make journal storage available to the dirstate object""" |
104 dirstate = orig(repo) |
104 dirstate = orig(repo) |
105 if util.safehasattr(repo, 'journal'): |
105 if util.safehasattr(repo, b'journal'): |
106 _setupdirstate(repo, dirstate) |
106 _setupdirstate(repo, dirstate) |
107 return dirstate |
107 return dirstate |
108 |
108 |
109 |
109 |
110 def recorddirstateparents(dirstate, old, new): |
110 def recorddirstateparents(dirstate, old, new): |
111 """Records all dirstate parent changes in the journal.""" |
111 """Records all dirstate parent changes in the journal.""" |
112 old = list(old) |
112 old = list(old) |
113 new = list(new) |
113 new = list(new) |
114 if util.safehasattr(dirstate, 'journalstorage'): |
114 if util.safehasattr(dirstate, b'journalstorage'): |
115 # only record two hashes if there was a merge |
115 # only record two hashes if there was a merge |
116 oldhashes = old[:1] if old[1] == node.nullid else old |
116 oldhashes = old[:1] if old[1] == node.nullid else old |
117 newhashes = new[:1] if new[1] == node.nullid else new |
117 newhashes = new[:1] if new[1] == node.nullid else new |
118 dirstate.journalstorage.record( |
118 dirstate.journalstorage.record( |
119 wdirparenttype, '.', oldhashes, newhashes |
119 wdirparenttype, b'.', oldhashes, newhashes |
120 ) |
120 ) |
121 |
121 |
122 |
122 |
123 # hooks to record bookmark changes (both local and remote) |
123 # hooks to record bookmark changes (both local and remote) |
124 def recordbookmarks(orig, store, fp): |
124 def recordbookmarks(orig, store, fp): |
125 """Records all bookmark changes in the journal.""" |
125 """Records all bookmark changes in the journal.""" |
126 repo = store._repo |
126 repo = store._repo |
127 if util.safehasattr(repo, 'journal'): |
127 if util.safehasattr(repo, b'journal'): |
128 oldmarks = bookmarks.bmstore(repo) |
128 oldmarks = bookmarks.bmstore(repo) |
129 for mark, value in store.iteritems(): |
129 for mark, value in store.iteritems(): |
130 oldvalue = oldmarks.get(mark, node.nullid) |
130 oldvalue = oldmarks.get(mark, node.nullid) |
131 if value != oldvalue: |
131 if value != oldvalue: |
132 repo.journal.record(bookmarktype, mark, oldvalue, value) |
132 repo.journal.record(bookmarktype, mark, oldvalue, value) |
175 |
175 |
176 def wrappostshare(orig, sourcerepo, destrepo, **kwargs): |
176 def wrappostshare(orig, sourcerepo, destrepo, **kwargs): |
177 """Mark this shared working copy as sharing journal information""" |
177 """Mark this shared working copy as sharing journal information""" |
178 with destrepo.wlock(): |
178 with destrepo.wlock(): |
179 orig(sourcerepo, destrepo, **kwargs) |
179 orig(sourcerepo, destrepo, **kwargs) |
180 with destrepo.vfs('shared', 'a') as fp: |
180 with destrepo.vfs(b'shared', b'a') as fp: |
181 fp.write('journal\n') |
181 fp.write(b'journal\n') |
182 |
182 |
183 |
183 |
184 def unsharejournal(orig, ui, repo, repopath): |
184 def unsharejournal(orig, ui, repo, repopath): |
185 """Copy shared journal entries into this repo when unsharing""" |
185 """Copy shared journal entries into this repo when unsharing""" |
186 if ( |
186 if ( |
187 repo.path == repopath |
187 repo.path == repopath |
188 and repo.shared() |
188 and repo.shared() |
189 and util.safehasattr(repo, 'journal') |
189 and util.safehasattr(repo, b'journal') |
190 ): |
190 ): |
191 sharedrepo = hg.sharedreposource(repo) |
191 sharedrepo = hg.sharedreposource(repo) |
192 sharedfeatures = _readsharedfeatures(repo) |
192 sharedfeatures = _readsharedfeatures(repo) |
193 if sharedrepo and sharedfeatures > {'journal'}: |
193 if sharedrepo and sharedfeatures > {b'journal'}: |
194 # there is a shared repository and there are shared journal entries |
194 # there is a shared repository and there are shared journal entries |
195 # to copy. move shared date over from source to destination but |
195 # to copy. move shared date over from source to destination but |
196 # move the local file first |
196 # move the local file first |
197 if repo.vfs.exists('namejournal'): |
197 if repo.vfs.exists(b'namejournal'): |
198 journalpath = repo.vfs.join('namejournal') |
198 journalpath = repo.vfs.join(b'namejournal') |
199 util.rename(journalpath, journalpath + '.bak') |
199 util.rename(journalpath, journalpath + b'.bak') |
200 storage = repo.journal |
200 storage = repo.journal |
201 local = storage._open( |
201 local = storage._open( |
202 repo.vfs, filename='namejournal.bak', _newestfirst=False |
202 repo.vfs, filename=b'namejournal.bak', _newestfirst=False |
203 ) |
203 ) |
204 shared = ( |
204 shared = ( |
205 e |
205 e |
206 for e in storage._open(sharedrepo.vfs, _newestfirst=False) |
206 for e in storage._open(sharedrepo.vfs, _newestfirst=False) |
207 if sharednamespaces.get(e.namespace) in sharedfeatures |
207 if sharednamespaces.get(e.namespace) in sharedfeatures |
309 # is this working copy using a shared storage? |
309 # is this working copy using a shared storage? |
310 self.sharedfeatures = self.sharedvfs = None |
310 self.sharedfeatures = self.sharedvfs = None |
311 if repo.shared(): |
311 if repo.shared(): |
312 features = _readsharedfeatures(repo) |
312 features = _readsharedfeatures(repo) |
313 sharedrepo = hg.sharedreposource(repo) |
313 sharedrepo = hg.sharedreposource(repo) |
314 if sharedrepo is not None and 'journal' in features: |
314 if sharedrepo is not None and b'journal' in features: |
315 self.sharedvfs = sharedrepo.vfs |
315 self.sharedvfs = sharedrepo.vfs |
316 self.sharedfeatures = features |
316 self.sharedfeatures = features |
317 |
317 |
318 # track the current command for recording in journal entries |
318 # track the current command for recording in journal entries |
319 @property |
319 @property |
320 def command(self): |
320 def command(self): |
321 commandstr = ' '.join( |
321 commandstr = b' '.join( |
322 map(procutil.shellquote, journalstorage._currentcommand) |
322 map(procutil.shellquote, journalstorage._currentcommand) |
323 ) |
323 ) |
324 if '\n' in commandstr: |
324 if b'\n' in commandstr: |
325 # truncate multi-line commands |
325 # truncate multi-line commands |
326 commandstr = commandstr.partition('\n')[0] + ' ...' |
326 commandstr = commandstr.partition(b'\n')[0] + b' ...' |
327 return commandstr |
327 return commandstr |
328 |
328 |
329 @classmethod |
329 @classmethod |
330 def recordcommand(cls, *fullargs): |
330 def recordcommand(cls, *fullargs): |
331 """Set the current hg arguments, stored with recorded entries""" |
331 """Set the current hg arguments, stored with recorded entries""" |
346 return l |
346 return l |
347 |
347 |
348 def jlock(self, vfs): |
348 def jlock(self, vfs): |
349 """Create a lock for the journal file""" |
349 """Create a lock for the journal file""" |
350 if self._currentlock(self._lockref) is not None: |
350 if self._currentlock(self._lockref) is not None: |
351 raise error.Abort(_('journal lock does not support nesting')) |
351 raise error.Abort(_(b'journal lock does not support nesting')) |
352 desc = _('journal of %s') % vfs.base |
352 desc = _(b'journal of %s') % vfs.base |
353 try: |
353 try: |
354 l = lock.lock(vfs, 'namejournal.lock', 0, desc=desc) |
354 l = lock.lock(vfs, b'namejournal.lock', 0, desc=desc) |
355 except error.LockHeld as inst: |
355 except error.LockHeld as inst: |
356 self.ui.warn( |
356 self.ui.warn( |
357 _("waiting for lock on %s held by %r\n") % (desc, inst.locker) |
357 _(b"waiting for lock on %s held by %r\n") % (desc, inst.locker) |
358 ) |
358 ) |
359 # default to 600 seconds timeout |
359 # default to 600 seconds timeout |
360 l = lock.lock( |
360 l = lock.lock( |
361 vfs, |
361 vfs, |
362 'namejournal.lock', |
362 b'namejournal.lock', |
363 self.ui.configint("ui", "timeout"), |
363 self.ui.configint(b"ui", b"timeout"), |
364 desc=desc, |
364 desc=desc, |
365 ) |
365 ) |
366 self.ui.warn(_("got lock after %s seconds\n") % l.delay) |
366 self.ui.warn(_(b"got lock after %s seconds\n") % l.delay) |
367 self._lockref = weakref.ref(l) |
367 self._lockref = weakref.ref(l) |
368 return l |
368 return l |
369 |
369 |
370 def record(self, namespace, name, oldhashes, newhashes): |
370 def record(self, namespace, name, oldhashes, newhashes): |
371 """Record a new journal entry |
371 """Record a new journal entry |
404 self._write(vfs, entry) |
404 self._write(vfs, entry) |
405 |
405 |
406 def _write(self, vfs, entry): |
406 def _write(self, vfs, entry): |
407 with self.jlock(vfs): |
407 with self.jlock(vfs): |
408 # open file in amend mode to ensure it is created if missing |
408 # open file in amend mode to ensure it is created if missing |
409 with vfs('namejournal', mode='a+b') as f: |
409 with vfs(b'namejournal', mode=b'a+b') as f: |
410 f.seek(0, os.SEEK_SET) |
410 f.seek(0, os.SEEK_SET) |
411 # Read just enough bytes to get a version number (up to 2 |
411 # Read just enough bytes to get a version number (up to 2 |
412 # digits plus separator) |
412 # digits plus separator) |
413 version = f.read(3).partition('\0')[0] |
413 version = f.read(3).partition(b'\0')[0] |
414 if version and version != "%d" % storageversion: |
414 if version and version != b"%d" % storageversion: |
415 # different version of the storage. Exit early (and not |
415 # different version of the storage. Exit early (and not |
416 # write anything) if this is not a version we can handle or |
416 # write anything) if this is not a version we can handle or |
417 # the file is corrupt. In future, perhaps rotate the file |
417 # the file is corrupt. In future, perhaps rotate the file |
418 # instead? |
418 # instead? |
419 self.ui.warn( |
419 self.ui.warn( |
420 _("unsupported journal file version '%s'\n") % version |
420 _(b"unsupported journal file version '%s'\n") % version |
421 ) |
421 ) |
422 return |
422 return |
423 if not version: |
423 if not version: |
424 # empty file, write version first |
424 # empty file, write version first |
425 f.write(("%d" % storageversion) + '\0') |
425 f.write((b"%d" % storageversion) + b'\0') |
426 f.seek(0, os.SEEK_END) |
426 f.seek(0, os.SEEK_END) |
427 f.write(bytes(entry) + '\0') |
427 f.write(bytes(entry) + b'\0') |
428 |
428 |
429 def filtered(self, namespace=None, name=None): |
429 def filtered(self, namespace=None, name=None): |
430 """Yield all journal entries with the given namespace or name |
430 """Yield all journal entries with the given namespace or name |
431 |
431 |
432 Both the namespace and the name are optional; if neither is given all |
432 Both the namespace and the name are optional; if neither is given all |
465 for e in self._open(self.sharedvfs) |
465 for e in self._open(self.sharedvfs) |
466 if sharednamespaces.get(e.namespace) in self.sharedfeatures |
466 if sharednamespaces.get(e.namespace) in self.sharedfeatures |
467 ) |
467 ) |
468 return _mergeentriesiter(local, shared) |
468 return _mergeentriesiter(local, shared) |
469 |
469 |
470 def _open(self, vfs, filename='namejournal', _newestfirst=True): |
470 def _open(self, vfs, filename=b'namejournal', _newestfirst=True): |
471 if not vfs.exists(filename): |
471 if not vfs.exists(filename): |
472 return |
472 return |
473 |
473 |
474 with vfs(filename) as f: |
474 with vfs(filename) as f: |
475 raw = f.read() |
475 raw = f.read() |
476 |
476 |
477 lines = raw.split('\0') |
477 lines = raw.split(b'\0') |
478 version = lines and lines[0] |
478 version = lines and lines[0] |
479 if version != "%d" % storageversion: |
479 if version != b"%d" % storageversion: |
480 version = version or _('not available') |
480 version = version or _(b'not available') |
481 raise error.Abort(_("unknown journal file version '%s'") % version) |
481 raise error.Abort(_(b"unknown journal file version '%s'") % version) |
482 |
482 |
483 # Skip the first line, it's a version number. Normally we iterate over |
483 # Skip the first line, it's a version number. Normally we iterate over |
484 # these in reverse order to list newest first; only when copying across |
484 # these in reverse order to list newest first; only when copying across |
485 # a shared storage do we forgo reversing. |
485 # a shared storage do we forgo reversing. |
486 lines = lines[1:] |
486 lines = lines[1:] |
492 yield journalentry.fromstorage(line) |
492 yield journalentry.fromstorage(line) |
493 |
493 |
494 |
494 |
495 # journal reading |
495 # journal reading |
496 # log options that don't make sense for journal |
496 # log options that don't make sense for journal |
497 _ignoreopts = ('no-merges', 'graph') |
497 _ignoreopts = (b'no-merges', b'graph') |
498 |
498 |
499 |
499 |
500 @command( |
500 @command( |
501 'journal', |
501 b'journal', |
502 [ |
502 [ |
503 ('', 'all', None, 'show history for all names'), |
503 (b'', b'all', None, b'show history for all names'), |
504 ('c', 'commits', None, 'show commit metadata'), |
504 (b'c', b'commits', None, b'show commit metadata'), |
505 ] |
505 ] |
506 + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts], |
506 + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts], |
507 '[OPTION]... [BOOKMARKNAME]', |
507 b'[OPTION]... [BOOKMARKNAME]', |
508 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, |
508 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, |
509 ) |
509 ) |
510 def journal(ui, repo, *args, **opts): |
510 def journal(ui, repo, *args, **opts): |
511 """show the previous position of bookmarks and the working copy |
511 """show the previous position of bookmarks and the working copy |
512 |
512 |
531 |
531 |
532 `hg journal -T json` can be used to produce machine readable output. |
532 `hg journal -T json` can be used to produce machine readable output. |
533 |
533 |
534 """ |
534 """ |
535 opts = pycompat.byteskwargs(opts) |
535 opts = pycompat.byteskwargs(opts) |
536 name = '.' |
536 name = b'.' |
537 if opts.get('all'): |
537 if opts.get(b'all'): |
538 if args: |
538 if args: |
539 raise error.Abort( |
539 raise error.Abort( |
540 _("You can't combine --all and filtering on a name") |
540 _(b"You can't combine --all and filtering on a name") |
541 ) |
541 ) |
542 name = None |
542 name = None |
543 if args: |
543 if args: |
544 name = args[0] |
544 name = args[0] |
545 |
545 |
546 fm = ui.formatter('journal', opts) |
546 fm = ui.formatter(b'journal', opts) |
547 |
547 |
548 def formatnodes(nodes): |
548 def formatnodes(nodes): |
549 return fm.formatlist(map(fm.hexfunc, nodes), name='node', sep=',') |
549 return fm.formatlist(map(fm.hexfunc, nodes), name=b'node', sep=b',') |
550 |
550 |
551 if opts.get("template") != "json": |
551 if opts.get(b"template") != b"json": |
552 if name is None: |
552 if name is None: |
553 displayname = _('the working copy and bookmarks') |
553 displayname = _(b'the working copy and bookmarks') |
554 else: |
554 else: |
555 displayname = "'%s'" % name |
555 displayname = b"'%s'" % name |
556 ui.status(_("previous locations of %s:\n") % displayname) |
556 ui.status(_(b"previous locations of %s:\n") % displayname) |
557 |
557 |
558 limit = logcmdutil.getlimit(opts) |
558 limit = logcmdutil.getlimit(opts) |
559 entry = None |
559 entry = None |
560 ui.pager('journal') |
560 ui.pager(b'journal') |
561 for count, entry in enumerate(repo.journal.filtered(name=name)): |
561 for count, entry in enumerate(repo.journal.filtered(name=name)): |
562 if count == limit: |
562 if count == limit: |
563 break |
563 break |
564 |
564 |
565 fm.startitem() |
565 fm.startitem() |
566 fm.condwrite( |
566 fm.condwrite( |
567 ui.verbose, 'oldnodes', '%s -> ', formatnodes(entry.oldhashes) |
567 ui.verbose, b'oldnodes', b'%s -> ', formatnodes(entry.oldhashes) |
568 ) |
568 ) |
569 fm.write('newnodes', '%s', formatnodes(entry.newhashes)) |
569 fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes)) |
570 fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user) |
570 fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user) |
571 fm.condwrite( |
571 fm.condwrite( |
572 opts.get('all') or name.startswith('re:'), |
572 opts.get(b'all') or name.startswith(b're:'), |
573 'name', |
573 b'name', |
574 ' %-8s', |
574 b' %-8s', |
575 entry.name, |
575 entry.name, |
576 ) |
576 ) |
577 |
577 |
578 fm.condwrite( |
578 fm.condwrite( |
579 ui.verbose, |
579 ui.verbose, |
580 'date', |
580 b'date', |
581 ' %s', |
581 b' %s', |
582 fm.formatdate(entry.timestamp, '%Y-%m-%d %H:%M %1%2'), |
582 fm.formatdate(entry.timestamp, b'%Y-%m-%d %H:%M %1%2'), |
583 ) |
583 ) |
584 fm.write('command', ' %s\n', entry.command) |
584 fm.write(b'command', b' %s\n', entry.command) |
585 |
585 |
586 if opts.get("commits"): |
586 if opts.get(b"commits"): |
587 if fm.isplain(): |
587 if fm.isplain(): |
588 displayer = logcmdutil.changesetdisplayer(ui, repo, opts) |
588 displayer = logcmdutil.changesetdisplayer(ui, repo, opts) |
589 else: |
589 else: |
590 displayer = logcmdutil.changesetformatter( |
590 displayer = logcmdutil.changesetformatter( |
591 ui, repo, fm.nested('changesets'), diffopts=opts |
591 ui, repo, fm.nested(b'changesets'), diffopts=opts |
592 ) |
592 ) |
593 for hash in entry.newhashes: |
593 for hash in entry.newhashes: |
594 try: |
594 try: |
595 ctx = repo[hash] |
595 ctx = repo[hash] |
596 displayer.show(ctx) |
596 displayer.show(ctx) |
597 except error.RepoLookupError as e: |
597 except error.RepoLookupError as e: |
598 fm.plain("%s\n\n" % pycompat.bytestr(e)) |
598 fm.plain(b"%s\n\n" % pycompat.bytestr(e)) |
599 displayer.close() |
599 displayer.close() |
600 |
600 |
601 fm.end() |
601 fm.end() |
602 |
602 |
603 if entry is None: |
603 if entry is None: |
604 ui.status(_("no recorded locations\n")) |
604 ui.status(_(b"no recorded locations\n")) |