mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-25 19:15:51 +03:00
Use version string helper from matrix-common (#11979)
* Require latest matrix-common * Use the common function
This commit is contained in:
parent
55113dd5e8
commit
4ae956c8bb
13 changed files with 42 additions and 112 deletions
1
changelog.d/11979.misc
Normal file
1
changelog.d/11979.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fetch Synapse's version using a helper from `matrix-common`.
|
|
@ -24,10 +24,10 @@ import traceback
|
||||||
from typing import Dict, Iterable, Optional, Set
|
from typing import Dict, Iterable, Optional, Set
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
|
|
||||||
import synapse
|
|
||||||
from synapse.config.database import DatabaseConnectionConfig
|
from synapse.config.database import DatabaseConnectionConfig
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.logging.context import (
|
from synapse.logging.context import (
|
||||||
|
@ -67,7 +67,6 @@ from synapse.storage.databases.state.bg_updates import StateBackgroundUpdateStor
|
||||||
from synapse.storage.engines import create_engine
|
from synapse.storage.engines import create_engine
|
||||||
from synapse.storage.prepare_database import prepare_database
|
from synapse.storage.prepare_database import prepare_database
|
||||||
from synapse.util import Clock
|
from synapse.util import Clock
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse_port_db")
|
logger = logging.getLogger("synapse_port_db")
|
||||||
|
|
||||||
|
@ -222,7 +221,9 @@ class MockHomeserver:
|
||||||
self.clock = Clock(reactor)
|
self.clock = Clock(reactor)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.hostname = config.server.server_name
|
self.hostname = config.server.server_name
|
||||||
self.version_string = "Synapse/" + get_version_string(synapse)
|
self.version_string = "Synapse/" + get_distribution_version_string(
|
||||||
|
"matrix-synapse"
|
||||||
|
)
|
||||||
|
|
||||||
def get_clock(self):
|
def get_clock(self):
|
||||||
return self.clock
|
return self.clock
|
||||||
|
|
|
@ -18,15 +18,14 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
|
|
||||||
import synapse
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
from synapse.storage import DataStore
|
from synapse.storage import DataStore
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
logger = logging.getLogger("update_database")
|
logger = logging.getLogger("update_database")
|
||||||
|
|
||||||
|
@ -39,7 +38,9 @@ class MockHomeserver(HomeServer):
|
||||||
config.server.server_name, reactor=reactor, config=config, **kwargs
|
config.server.server_name, reactor=reactor, config=config, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
self.version_string = "Synapse/" + get_version_string(synapse)
|
self.version_string = "Synapse/" + get_distribution_version_string(
|
||||||
|
"matrix-synapse"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_background_updates(hs):
|
def run_background_updates(hs):
|
||||||
|
|
|
@ -37,6 +37,7 @@ from typing import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from cryptography.utils import CryptographyDeprecationWarning
|
from cryptography.utils import CryptographyDeprecationWarning
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
|
|
||||||
import twisted
|
import twisted
|
||||||
from twisted.internet import defer, error, reactor as _reactor
|
from twisted.internet import defer, error, reactor as _reactor
|
||||||
|
@ -67,7 +68,6 @@ from synapse.util.caches.lrucache import setup_expire_lru_cache_entries
|
||||||
from synapse.util.daemonize import daemonize_process
|
from synapse.util.daemonize import daemonize_process
|
||||||
from synapse.util.gai_resolver import GAIResolver
|
from synapse.util.gai_resolver import GAIResolver
|
||||||
from synapse.util.rlimit import change_resource_limit
|
from synapse.util.rlimit import change_resource_limit
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -487,7 +487,8 @@ def setup_sentry(hs: "HomeServer") -> None:
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn=hs.config.metrics.sentry_dsn, release=get_version_string(synapse)
|
dsn=hs.config.metrics.sentry_dsn,
|
||||||
|
release=get_distribution_version_string("matrix-synapse"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# We set some default tags that give some context to this instance
|
# We set some default tags that give some context to this instance
|
||||||
|
|
|
@ -19,6 +19,8 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
|
|
||||||
from twisted.internet import defer, task
|
from twisted.internet import defer, task
|
||||||
|
|
||||||
import synapse
|
import synapse
|
||||||
|
@ -44,7 +46,6 @@ from synapse.server import HomeServer
|
||||||
from synapse.storage.databases.main.room import RoomWorkerStore
|
from synapse.storage.databases.main.room import RoomWorkerStore
|
||||||
from synapse.types import StateMap
|
from synapse.types import StateMap
|
||||||
from synapse.util.logcontext import LoggingContext
|
from synapse.util.logcontext import LoggingContext
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.admin_cmd")
|
logger = logging.getLogger("synapse.app.admin_cmd")
|
||||||
|
|
||||||
|
@ -223,7 +224,7 @@ def start(config_options: List[str]) -> None:
|
||||||
ss = AdminCmdServer(
|
ss = AdminCmdServer(
|
||||||
config.server.server_name,
|
config.server.server_name,
|
||||||
config=config,
|
config=config,
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
version_string="Synapse/" + get_distribution_version_string("matrix-synapse"),
|
||||||
)
|
)
|
||||||
|
|
||||||
setup_logging(ss, config, use_worker_options=True)
|
setup_logging(ss, config, use_worker_options=True)
|
||||||
|
|
|
@ -16,6 +16,8 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
|
|
||||||
from twisted.internet import address
|
from twisted.internet import address
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
|
|
||||||
|
@ -122,7 +124,6 @@ from synapse.storage.databases.main.ui_auth import UIAuthWorkerStore
|
||||||
from synapse.storage.databases.main.user_directory import UserDirectoryStore
|
from synapse.storage.databases.main.user_directory import UserDirectoryStore
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
from synapse.util.httpresourcetree import create_resource_tree
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.generic_worker")
|
logger = logging.getLogger("synapse.app.generic_worker")
|
||||||
|
|
||||||
|
@ -482,7 +483,7 @@ def start(config_options: List[str]) -> None:
|
||||||
hs = GenericWorkerServer(
|
hs = GenericWorkerServer(
|
||||||
config.server.server_name,
|
config.server.server_name,
|
||||||
config=config,
|
config=config,
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
version_string="Synapse/" + get_distribution_version_string("matrix-synapse"),
|
||||||
)
|
)
|
||||||
|
|
||||||
setup_logging(hs, config, use_worker_options=True)
|
setup_logging(hs, config, use_worker_options=True)
|
||||||
|
|
|
@ -18,6 +18,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, Iterable, Iterator, List
|
from typing import Dict, Iterable, Iterator, List
|
||||||
|
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
|
|
||||||
from twisted.internet.tcp import Port
|
from twisted.internet.tcp import Port
|
||||||
from twisted.web.resource import EncodingResourceWrapper, Resource
|
from twisted.web.resource import EncodingResourceWrapper, Resource
|
||||||
from twisted.web.server import GzipEncoderFactory
|
from twisted.web.server import GzipEncoderFactory
|
||||||
|
@ -70,7 +72,6 @@ from synapse.server import HomeServer
|
||||||
from synapse.storage import DataStore
|
from synapse.storage import DataStore
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
from synapse.util.httpresourcetree import create_resource_tree
|
||||||
from synapse.util.module_loader import load_module
|
from synapse.util.module_loader import load_module
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.homeserver")
|
logger = logging.getLogger("synapse.app.homeserver")
|
||||||
|
|
||||||
|
@ -350,7 +351,7 @@ def setup(config_options: List[str]) -> SynapseHomeServer:
|
||||||
hs = SynapseHomeServer(
|
hs = SynapseHomeServer(
|
||||||
config.server.server_name,
|
config.server.server_name,
|
||||||
config=config,
|
config=config,
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
version_string="Synapse/" + get_distribution_version_string("matrix-synapse"),
|
||||||
)
|
)
|
||||||
|
|
||||||
synapse.config.logger.setup_logging(hs, config, use_worker_options=False)
|
synapse.config.logger.setup_logging(hs, config, use_worker_options=False)
|
||||||
|
|
|
@ -22,6 +22,7 @@ from string import Template
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
|
|
||||||
from twisted.logger import (
|
from twisted.logger import (
|
||||||
|
@ -32,11 +33,9 @@ from twisted.logger import (
|
||||||
globalLogBeginner,
|
globalLogBeginner,
|
||||||
)
|
)
|
||||||
|
|
||||||
import synapse
|
|
||||||
from synapse.logging._structured import setup_structured_logging
|
from synapse.logging._structured import setup_structured_logging
|
||||||
from synapse.logging.context import LoggingContextFilter
|
from synapse.logging.context import LoggingContextFilter
|
||||||
from synapse.logging.filter import MetadataFilter
|
from synapse.logging.filter import MetadataFilter
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config, ConfigError
|
||||||
|
|
||||||
|
@ -347,6 +346,10 @@ def setup_logging(
|
||||||
|
|
||||||
# Log immediately so we can grep backwards.
|
# Log immediately so we can grep backwards.
|
||||||
logging.warning("***** STARTING SERVER *****")
|
logging.warning("***** STARTING SERVER *****")
|
||||||
logging.warning("Server %s version %s", sys.argv[0], get_version_string(synapse))
|
logging.warning(
|
||||||
|
"Server %s version %s",
|
||||||
|
sys.argv[0],
|
||||||
|
get_distribution_version_string("matrix-synapse"),
|
||||||
|
)
|
||||||
logging.info("Server hostname: %s", config.server.server_name)
|
logging.info("Server hostname: %s", config.server.server_name)
|
||||||
logging.info("Instance name: %s", hs.get_instance_name())
|
logging.info("Instance name: %s", hs.get_instance_name())
|
||||||
|
|
|
@ -24,9 +24,9 @@ from typing import (
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
import synapse
|
|
||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.api.room_versions import RoomVersions
|
from synapse.api.room_versions import RoomVersions
|
||||||
from synapse.api.urls import FEDERATION_UNSTABLE_PREFIX, FEDERATION_V2_PREFIX
|
from synapse.api.urls import FEDERATION_UNSTABLE_PREFIX, FEDERATION_V2_PREFIX
|
||||||
|
@ -42,7 +42,6 @@ from synapse.http.servlet import (
|
||||||
)
|
)
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
from synapse.util.ratelimitutils import FederationRateLimiter
|
from synapse.util.ratelimitutils import FederationRateLimiter
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -616,7 +615,12 @@ class FederationVersionServlet(BaseFederationServlet):
|
||||||
) -> Tuple[int, JsonDict]:
|
) -> Tuple[int, JsonDict]:
|
||||||
return (
|
return (
|
||||||
200,
|
200,
|
||||||
{"server": {"name": "Synapse", "version": get_version_string(synapse)}},
|
{
|
||||||
|
"server": {
|
||||||
|
"name": "Synapse",
|
||||||
|
"version": get_distribution_version_string("matrix-synapse"),
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ from typing import (
|
||||||
)
|
)
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
from prometheus_client import CollectorRegistry, Counter, Gauge, Histogram, Metric
|
from prometheus_client import CollectorRegistry, Counter, Gauge, Histogram, Metric
|
||||||
from prometheus_client.core import (
|
from prometheus_client.core import (
|
||||||
REGISTRY,
|
REGISTRY,
|
||||||
|
@ -43,14 +44,14 @@ from prometheus_client.core import (
|
||||||
|
|
||||||
from twisted.python.threadpool import ThreadPool
|
from twisted.python.threadpool import ThreadPool
|
||||||
|
|
||||||
import synapse.metrics._reactor_metrics
|
# This module is imported for its side effects; flake8 needn't warn that it's unused.
|
||||||
|
import synapse.metrics._reactor_metrics # noqa: F401
|
||||||
from synapse.metrics._exposition import (
|
from synapse.metrics._exposition import (
|
||||||
MetricsResource,
|
MetricsResource,
|
||||||
generate_latest,
|
generate_latest,
|
||||||
start_http_server,
|
start_http_server,
|
||||||
)
|
)
|
||||||
from synapse.metrics._gc import MIN_TIME_BETWEEN_GCS, install_gc_manager
|
from synapse.metrics._gc import MIN_TIME_BETWEEN_GCS, install_gc_manager
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -417,7 +418,7 @@ build_info = Gauge(
|
||||||
)
|
)
|
||||||
build_info.labels(
|
build_info.labels(
|
||||||
" ".join([platform.python_implementation(), platform.python_version()]),
|
" ".join([platform.python_implementation(), platform.python_version()]),
|
||||||
get_version_string(synapse),
|
get_distribution_version_string("matrix-synapse"),
|
||||||
" ".join([platform.system(), platform.release()]),
|
" ".join([platform.system(), platform.release()]),
|
||||||
).set(1)
|
).set(1)
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ REQUIREMENTS = [
|
||||||
# with the latest security patches.
|
# with the latest security patches.
|
||||||
"cryptography>=3.4.7",
|
"cryptography>=3.4.7",
|
||||||
"ijson>=3.1",
|
"ijson>=3.1",
|
||||||
"matrix-common==1.0.0",
|
"matrix-common~=1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
CONDITIONAL_REQUIREMENTS = {
|
CONDITIONAL_REQUIREMENTS = {
|
||||||
|
|
|
@ -20,7 +20,8 @@ import platform
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, Optional, Tuple
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
import synapse
|
from matrix_common.versionstring import get_distribution_version_string
|
||||||
|
|
||||||
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||||
from synapse.http.server import HttpServer, JsonResource
|
from synapse.http.server import HttpServer, JsonResource
|
||||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||||
|
@ -88,7 +89,6 @@ from synapse.rest.admin.users import (
|
||||||
WhoisRestServlet,
|
WhoisRestServlet,
|
||||||
)
|
)
|
||||||
from synapse.types import JsonDict, RoomStreamToken
|
from synapse.types import JsonDict, RoomStreamToken
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -101,7 +101,7 @@ class VersionServlet(RestServlet):
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.res = {
|
self.res = {
|
||||||
"server_version": get_version_string(synapse),
|
"server_version": get_distribution_version_string("matrix-synapse"),
|
||||||
"python_version": platform.python_version(),
|
"python_version": platform.python_version(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
from types import ModuleType
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
version_cache: Dict[ModuleType, str] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_version_string(module: ModuleType) -> str:
|
|
||||||
"""Given a module calculate a git-aware version string for it.
|
|
||||||
|
|
||||||
If called on a module not in a git checkout will return `__version__`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
module: The module to check the version of. Must declare a __version__
|
|
||||||
attribute.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The module version (as a string).
|
|
||||||
"""
|
|
||||||
|
|
||||||
cached_version = version_cache.get(module)
|
|
||||||
if cached_version is not None:
|
|
||||||
return cached_version
|
|
||||||
|
|
||||||
# We want this to fail loudly with an AttributeError. Type-ignore this so
|
|
||||||
# mypy only considers the happy path.
|
|
||||||
version_string = module.__version__ # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
try:
|
|
||||||
cwd = os.path.dirname(os.path.abspath(module.__file__))
|
|
||||||
|
|
||||||
def _run_git_command(prefix: str, *params: str) -> str:
|
|
||||||
try:
|
|
||||||
result = (
|
|
||||||
subprocess.check_output(
|
|
||||||
["git", *params], stderr=subprocess.DEVNULL, cwd=cwd
|
|
||||||
)
|
|
||||||
.strip()
|
|
||||||
.decode("ascii")
|
|
||||||
)
|
|
||||||
return prefix + result
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
git_branch = _run_git_command("b=", "rev-parse", "--abbrev-ref", "HEAD")
|
|
||||||
git_tag = _run_git_command("t=", "describe", "--exact-match")
|
|
||||||
git_commit = _run_git_command("", "rev-parse", "--short", "HEAD")
|
|
||||||
|
|
||||||
dirty_string = "-this_is_a_dirty_checkout"
|
|
||||||
is_dirty = _run_git_command("", "describe", "--dirty=" + dirty_string).endswith(
|
|
||||||
dirty_string
|
|
||||||
)
|
|
||||||
git_dirty = "dirty" if is_dirty else ""
|
|
||||||
|
|
||||||
if git_branch or git_tag or git_commit or git_dirty:
|
|
||||||
git_version = ",".join(
|
|
||||||
s for s in (git_branch, git_tag, git_commit, git_dirty) if s
|
|
||||||
)
|
|
||||||
|
|
||||||
version_string = f"{version_string} ({git_version})"
|
|
||||||
except Exception as e:
|
|
||||||
logger.info("Failed to check for git repository: %s", e)
|
|
||||||
|
|
||||||
version_cache[module] = version_string
|
|
||||||
|
|
||||||
return version_string
|
|
Loading…
Reference in a new issue