--- /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 <pacien.trangirard@pacien.net>
+#
+# 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)