diff -r 44bc045a43ca -r 791050360486 contrib/pull_logger.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/pull_logger.py Mon Jul 25 22:47:15 2022 +0200 @@ -0,0 +1,129 @@ +# pull_logger.py - Logs pulls to a JSON-line file in the repo's VFS. +# +# Copyright 2022 Pacien TRAN-GIRARD +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + + +'''logs pull parameters to a file + +This extension logs the pull parameters, i.e. the remote and common heads, +when pulling from the local repository. + +The collected data should give an idea of the state of a pair of repositories +and allow replaying past synchronisations between them. This is particularly +useful for working on data exchange, bundling and caching-related +optimisations. + +The record is a JSON-line file located in the repository's VFS at +.hg/pull_log.jsonl. + +Log write failures are not considered fatal: log writes may be skipped for any +reason such as insufficient storage or a timeout. + +The timeouts of the exclusive lock used when writing to the lock file can be +configured through the 'timeout.lock' and 'timeout.warn' options of this +plugin. Those are not expected to be held for a significant time in practice.:: + + [pull-logger] + timeout.lock = 300 + timeout.warn = 100 + +Note: there is no automatic log rotation and the size of the log is not capped. +''' + + +import json +import time + +from mercurial.i18n import _ +from mercurial.utils import stringutil +from mercurial import ( + error, + extensions, + lock, + registrar, + wireprotov1server, +) + +EXT_NAME = b'pull-logger' +EXT_VERSION_CODE = 0 + +LOG_FILE = b'pull_log.jsonl' +LOCK_NAME = LOG_FILE + b'.lock' + +configtable = {} +configitem = registrar.configitem(configtable) +configitem(EXT_NAME, b'timeout.lock', default=600) +configitem(EXT_NAME, b'timeout.warn', default=120) + + +def wrap_getbundle(orig, repo, proto, others, *args, **kwargs): + heads, common = extract_pull_heads(others) + log_entry = { + 'timestamp': time.time(), + 'logger_version': EXT_VERSION_CODE, + 'heads': sorted(heads), + 'common': sorted(common), + } + + try: + write_to_log(repo, log_entry) + except (IOError, error.LockError) as err: + msg = stringutil.forcebytestr(err) + repo.ui.warn(_(b'unable to append to pull log: %s\n') % msg) + + return orig(repo, proto, others, *args, **kwargs) + + +def extract_pull_heads(bundle_args): + opts = wireprotov1server.options( + b'getbundle', + wireprotov1server.wireprototypes.GETBUNDLE_ARGUMENTS.keys(), + bundle_args.copy(), # this call consumes the args destructively + ) + + heads = opts.get(b'heads', b'').decode('utf-8').split(' ') + common = opts.get(b'common', b'').decode('utf-8').split(' ') + return (heads, common) + + +def write_to_log(repo, entry): + locktimeout = repo.ui.configint(EXT_NAME, b'timeout.lock') + lockwarntimeout = repo.ui.configint(EXT_NAME, b'timeout.warn') + + with lock.trylock( + ui=repo.ui, + vfs=repo.vfs, + lockname=LOCK_NAME, + timeout=locktimeout, + warntimeout=lockwarntimeout, + ): + with repo.vfs.open(LOG_FILE, b'a+') as logfile: + serialised = json.dumps(entry, sort_keys=True) + logfile.write(serialised.encode('utf-8')) + logfile.write(b'\n') + logfile.flush() + + +def reposetup(ui, repo): + if repo.local(): + repo._wlockfreeprefix.add(LOG_FILE) + + +def uisetup(ui): + del wireprotov1server.commands[b'getbundle'] + decorator = wireprotov1server.wireprotocommand( + name=b'getbundle', + args=b'*', + permission=b'pull', + ) + + extensions.wrapfunction( + container=wireprotov1server, + funcname='getbundle', + wrapper=wrap_getbundle, + ) + + decorator(wireprotov1server.getbundle)