New Upstream Release - ostree-push
Ready changes
Summary
Merged new upstream version: 1.1.0 (was: 1.0.1).
Diff
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..cf2976f
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,171 @@
+---
+name: Tests
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ lint:
+ name: Code style and lint checks
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Install system dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get -y install \
+ python3-pip
+
+ - name: Install python dependencies
+ run: |
+ python3 -m pip install flake8
+
+ - name: Run flake8
+ run: |
+ python3 -m flake8
+
+ tests:
+ strategy:
+ # Let other configurations continue if one fails.
+ fail-fast: false
+ matrix:
+ include:
+ - name: Arch Base
+ image: archlinux:base
+ setup: |
+ pacman -Sy --noconfirm \
+ base-devel \
+ cairo \
+ flatpak \
+ gobject-introspection \
+ openssh \
+ ostree \
+ python-pip \
+ python-setuptools \
+ python-wheel
+
+ - name: Debian Stable
+ image: debian:stable-slim
+ setup: |
+ apt-get update
+ apt-get -y install \
+ build-essential \
+ flatpak \
+ gir1.2-ostree-1.0 \
+ libcairo2-dev \
+ libgirepository1.0-dev \
+ openssh-client \
+ openssh-server \
+ ostree \
+ python3-dev \
+ python3-pip \
+ python3-setuptools \
+ python3-wheel
+
+ - name: Debian Testing
+ image: debian:testing-slim
+ setup: |
+ apt-get update
+ apt-get -y install \
+ build-essential \
+ flatpak \
+ gir1.2-ostree-1.0 \
+ libcairo2-dev \
+ libgirepository1.0-dev \
+ openssh-client \
+ openssh-server \
+ ostree \
+ python3-dev \
+ python3-pip \
+ python3-setuptools \
+ python3-wheel
+
+ - name: Fedora Stable
+ image: fedora:latest
+ setup: |
+ dnf -y install \
+ cairo-gobject-devel \
+ flatpak \
+ gobject-introspection-devel \
+ openssh-clients \
+ openssh-server \
+ ostree \
+ ostree-libs \
+ passwd \
+ python3-devel \
+ python3-pip
+
+ - name: Ubuntu LTS
+ image: ubuntu:latest
+ setup: |
+ apt-get update
+ apt-get -y install \
+ build-essential \
+ flatpak \
+ gir1.2-ostree-1.0 \
+ libcairo2-dev \
+ libgirepository1.0-dev \
+ openssh-client \
+ openssh-server \
+ ostree \
+ python3-dev \
+ python3-pip \
+ python3-setuptools \
+ python3-wheel
+
+ - name: Ubuntu Rolling
+ image: ubuntu:rolling
+ setup: |
+ apt-get update
+ apt-get -y install \
+ build-essential \
+ flatpak \
+ gir1.2-ostree-1.0 \
+ libcairo2-dev \
+ libgirepository1.0-dev \
+ openssh-client \
+ openssh-server \
+ ostree \
+ python3-dev \
+ python3-pip \
+ python3-setuptools \
+ python3-wheel
+
+ name: ${{ matrix.name }}
+ runs-on: ubuntu-latest
+ container: ${{ matrix.image }}
+ env:
+ DEBIAN_FRONTEND: noninteractive
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: System setup
+ run: ${{ matrix.setup }}
+
+ # sshd refuses to run if the hardcoded privilege separation
+ # directory doesn't exist.
+ - name: Create sshd privilege separation directory
+ run: |
+ mkdir -p /run/sshd
+
+ # sshd won't allow root login if the account is locked.
+ - name: Ensure root account unlocked
+ run: |
+ passwd -u root
+
+ - name: Install python dependencies
+ run: |
+ python3 -m pip install tox
+
+ - name: Run tests
+ run: |
+ python3 -m tox
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a59ed6b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+*~
+*.py[co]
+__pycache__
+/.pytest_cache/
+/.tox/
+/build/
+/dist/
+/ostree_push.egg-info/
diff --git a/NEWS b/NEWS
index 8f78d00..0adc146 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+# 1.1.0 (2022-12-02)
+
+ostree-receive now supports optional per-repository configuration. This
+is useful if you have repositories that require different settings such
+as the key IDs to sign commits with.
+
# 1.0.1 (2022-10-27)
ostree-receive now supports ostree ed25519 signing and verification. See
diff --git a/PKG-INFO b/PKG-INFO
deleted file mode 100644
index ed23cd6..0000000
--- a/PKG-INFO
+++ /dev/null
@@ -1,136 +0,0 @@
-Metadata-Version: 2.1
-Name: ostree-push
-Version: 1.0.1
-Summary: Push and receive OSTree commits
-Home-page: https://github.com/dbnicholson/ostree-push
-Author: Dan Nicholson
-Author-email: dbn@endlessos.org
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
-Classifier: Operating System :: POSIX
-Classifier: Topic :: Software Development :: Build Tools
-Classifier: Topic :: System :: Archiving :: Mirroring
-Classifier: Topic :: System :: Archiving :: Packaging
-Classifier: Topic :: System :: Software Distribution
-Requires-Python: >=3.7
-Description-Content-Type: text/markdown
-License-File: COPYING
-
-# ostree-push
-
-## Background
-
-`ostree-push` uses `ssh` to push commits from a local OSTree repo to a
-remote OSTree repo. This is to fill a gap where currently you can only
-pull commits in core ostree. To publish commits to a remote repository,
-you either have to `pull` from the local repo to the remote repo or use
-an out of band mechanism like `rsync`.
-
-Both approaches have significant limitations. To pull over the network,
-only http is supported. So, in addition to having to login on the remote
-machine and run `ostree pull`, the local repository needs to be served
-over http. This means your build machine needs to be an http server with
-appropriate configuration in addition to simply making commits. This
-pushes the builds to be done on the public repository server, which
-prevents reasonable separation of duties and makes multiarch
-repositories impossible.
-
-Using `rsync` for publishing has some major benefits since only updated
-objects are published. However, it has no concept of the OSTree object
-store or refs structures. There are a few problems deriving from this
-issue. First, objects are published in sort order, but this means that
-objects can be published before their children. In the most extreme
-case, a commit object could be published before it's complete. The
-remote repo would assume this commit object was valid even though some
-children might be missing. Second, the refs might get updated before the
-commit objects are in place. If a client pulls while `rsync` is
-publishing, it may attempt to pull an incomplete or entirely missing
-commit. Finally, `rsync` will push the objects directly into the store
-rather than using a staging directory like `pull` or `commit` do. If
-`rsync` is interrupted, it could leave partial objects in the store.
-
-`ostree-push` tries to offer functionality like `git` where commits can
-be pushed over `ssh` to avoid these issues.
-
-## Operation
-
-When `ostree-push` is started, it first starts a local HTTP server
-providing the contents of the local ostree repo. It then connects to the
-remote host with `ssh` and tunnels the HTTP server port through the SSH
-connection. Finally, it runs `ostree-receive` on the remote host with
-the URL of the tunneled HTTP server. `ostree-receive` then creates a
-temporary remote using this URL and pulls the desired refs from it.
-
-In essence, `ostree-push` and `ostree-receive` coordinate to pull from
-the local repo to a remote repo while avoiding the limitations described
-above. Namely, no HTTP server needs to be running and no port needs to
-be exposed on the local host. Both resources are created temporarily and
-only exposed to the remote host through the secure SSH connection.
-
-## Installation
-
-Use `pip` to install the `otpush` package and the `ostree-push` and
-`ostree-receive` scripts. From a git checkout, run:
-
-```
-pip install .
-```
-
-If `ostree-receive` is not in a default `PATH` location, it may not be
-located when run in the environment spawned by the SSH server. As a
-workaround, make a symbolic link in a standard location:
-
-```
-sudo ln -s /path/to/ostree-receive /usr/bin/ostree-receive
-```
-
-In order to restrict SSH usage to only running `ostree-receive`, the
-`ostree-receive-shell` script can be used as a login shell. This way
-someone with SSH access to the remote machine cannot run arbitrary
-commands as the user owning the repositories. To use it, set the login
-shell of the repo owner to `ostree-receive-shell`:
-
-```
-sudo chsh -s /path/to/ostree-receive-shell <user>
-```
-
-`ostree-receive-shell` will also append the directory it's installed in
-to `PATH` to allow `ostree-receive` to be found in non-standard
-locations. In that scenario, the symbolic link to `ostree-receive`
-described above is not needed.
-
-Both `ostree-push` and `ostree-receive` require the OSTree GObject
-Introspection bindings. Typically these would be installed from the host
-distro. On Debian systems the package is `gir1.2-ostree-1.0` while on
-RedHat systems they are in the `ostree-libs` package.
-
-`ostree-push` relies on the connection sharing and port forwarding
-features of OpenSSH and is unlikely to work with another SSH client.
-Similarly, `ostree-receive` has only be tested with the OpenSSH server,
-but it might work correctly with other SSH servers.
-
-## Configuration
-
-`ostree-receive` can be configured from YAML formatted files. It will
-load `~/.config/ostree/ostree-receive.conf` and
-`/etc/ostree/ostree-receive.conf` or a file specified in the
-`OSTREE_RECEIVE_CONF` environment variable. See the example
-[`ostree-receive.conf`](ostree-receive.conf) file for available options.
-
-## Testing
-
-A test suite is provided using [pytest][pytest]. Most of the time simply
-running `pytest` from a git checkout will run it correctly. [tox][tox]
-can also be used to automate running the test suite in a prepared Python
-environment.
-
-In addition to the `ostree-push` dependencies, many of the tests depend
-on using OpenSSH `sshd` locally. On both Debian and RedHat systems this
-is available in the `openssh-server` package.
-
-[pytest]: https://docs.pytest.org/en/stable/
-[tox]: https://tox.readthedocs.io/en/stable/
diff --git a/debian/changelog b/debian/changelog
index 15ad94d..105540b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ostree-push (1.1.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 13 May 2023 04:35:08 -0000
+
ostree-push (1.0.1-1) unstable; urgency=medium
[ Andrej Shadura ]
diff --git a/ostree-receive.conf b/ostree-receive.conf
index 15e2304..3108d90 100644
--- a/ostree-receive.conf
+++ b/ostree-receive.conf
@@ -57,6 +57,22 @@
# whitespace.
#update_hook: null
+# Optional per-repository configuration settings. All of the above settings
+# except for root can be set and will override the global value. The value is a
+# map of repository path to map of settings. The repository path can be
+# relative or absolute. If root is specified, relative paths are resolved below
+# it.
+#
+# For example:
+#
+# repos:
+# foo:
+# gpg_sign: ['76543210']
+# /path/to/bar:
+# update: no
+#
+#repos: {}
+
# Set the log level. See https://docs.python.org/3/library/logging.html#levels
# for the list of log levels.
#log_level: INFO
diff --git a/ostree_push.egg-info/PKG-INFO b/ostree_push.egg-info/PKG-INFO
deleted file mode 100644
index ed23cd6..0000000
--- a/ostree_push.egg-info/PKG-INFO
+++ /dev/null
@@ -1,136 +0,0 @@
-Metadata-Version: 2.1
-Name: ostree-push
-Version: 1.0.1
-Summary: Push and receive OSTree commits
-Home-page: https://github.com/dbnicholson/ostree-push
-Author: Dan Nicholson
-Author-email: dbn@endlessos.org
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
-Classifier: Operating System :: POSIX
-Classifier: Topic :: Software Development :: Build Tools
-Classifier: Topic :: System :: Archiving :: Mirroring
-Classifier: Topic :: System :: Archiving :: Packaging
-Classifier: Topic :: System :: Software Distribution
-Requires-Python: >=3.7
-Description-Content-Type: text/markdown
-License-File: COPYING
-
-# ostree-push
-
-## Background
-
-`ostree-push` uses `ssh` to push commits from a local OSTree repo to a
-remote OSTree repo. This is to fill a gap where currently you can only
-pull commits in core ostree. To publish commits to a remote repository,
-you either have to `pull` from the local repo to the remote repo or use
-an out of band mechanism like `rsync`.
-
-Both approaches have significant limitations. To pull over the network,
-only http is supported. So, in addition to having to login on the remote
-machine and run `ostree pull`, the local repository needs to be served
-over http. This means your build machine needs to be an http server with
-appropriate configuration in addition to simply making commits. This
-pushes the builds to be done on the public repository server, which
-prevents reasonable separation of duties and makes multiarch
-repositories impossible.
-
-Using `rsync` for publishing has some major benefits since only updated
-objects are published. However, it has no concept of the OSTree object
-store or refs structures. There are a few problems deriving from this
-issue. First, objects are published in sort order, but this means that
-objects can be published before their children. In the most extreme
-case, a commit object could be published before it's complete. The
-remote repo would assume this commit object was valid even though some
-children might be missing. Second, the refs might get updated before the
-commit objects are in place. If a client pulls while `rsync` is
-publishing, it may attempt to pull an incomplete or entirely missing
-commit. Finally, `rsync` will push the objects directly into the store
-rather than using a staging directory like `pull` or `commit` do. If
-`rsync` is interrupted, it could leave partial objects in the store.
-
-`ostree-push` tries to offer functionality like `git` where commits can
-be pushed over `ssh` to avoid these issues.
-
-## Operation
-
-When `ostree-push` is started, it first starts a local HTTP server
-providing the contents of the local ostree repo. It then connects to the
-remote host with `ssh` and tunnels the HTTP server port through the SSH
-connection. Finally, it runs `ostree-receive` on the remote host with
-the URL of the tunneled HTTP server. `ostree-receive` then creates a
-temporary remote using this URL and pulls the desired refs from it.
-
-In essence, `ostree-push` and `ostree-receive` coordinate to pull from
-the local repo to a remote repo while avoiding the limitations described
-above. Namely, no HTTP server needs to be running and no port needs to
-be exposed on the local host. Both resources are created temporarily and
-only exposed to the remote host through the secure SSH connection.
-
-## Installation
-
-Use `pip` to install the `otpush` package and the `ostree-push` and
-`ostree-receive` scripts. From a git checkout, run:
-
-```
-pip install .
-```
-
-If `ostree-receive` is not in a default `PATH` location, it may not be
-located when run in the environment spawned by the SSH server. As a
-workaround, make a symbolic link in a standard location:
-
-```
-sudo ln -s /path/to/ostree-receive /usr/bin/ostree-receive
-```
-
-In order to restrict SSH usage to only running `ostree-receive`, the
-`ostree-receive-shell` script can be used as a login shell. This way
-someone with SSH access to the remote machine cannot run arbitrary
-commands as the user owning the repositories. To use it, set the login
-shell of the repo owner to `ostree-receive-shell`:
-
-```
-sudo chsh -s /path/to/ostree-receive-shell <user>
-```
-
-`ostree-receive-shell` will also append the directory it's installed in
-to `PATH` to allow `ostree-receive` to be found in non-standard
-locations. In that scenario, the symbolic link to `ostree-receive`
-described above is not needed.
-
-Both `ostree-push` and `ostree-receive` require the OSTree GObject
-Introspection bindings. Typically these would be installed from the host
-distro. On Debian systems the package is `gir1.2-ostree-1.0` while on
-RedHat systems they are in the `ostree-libs` package.
-
-`ostree-push` relies on the connection sharing and port forwarding
-features of OpenSSH and is unlikely to work with another SSH client.
-Similarly, `ostree-receive` has only be tested with the OpenSSH server,
-but it might work correctly with other SSH servers.
-
-## Configuration
-
-`ostree-receive` can be configured from YAML formatted files. It will
-load `~/.config/ostree/ostree-receive.conf` and
-`/etc/ostree/ostree-receive.conf` or a file specified in the
-`OSTREE_RECEIVE_CONF` environment variable. See the example
-[`ostree-receive.conf`](ostree-receive.conf) file for available options.
-
-## Testing
-
-A test suite is provided using [pytest][pytest]. Most of the time simply
-running `pytest` from a git checkout will run it correctly. [tox][tox]
-can also be used to automate running the test suite in a prepared Python
-environment.
-
-In addition to the `ostree-push` dependencies, many of the tests depend
-on using OpenSSH `sshd` locally. On both Debian and RedHat systems this
-is available in the `openssh-server` package.
-
-[pytest]: https://docs.pytest.org/en/stable/
-[tox]: https://tox.readthedocs.io/en/stable/
diff --git a/ostree_push.egg-info/SOURCES.txt b/ostree_push.egg-info/SOURCES.txt
deleted file mode 100644
index e81d720..0000000
--- a/ostree_push.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-.flake8
-COPYING
-MANIFEST.in
-NEWS
-README.md
-TODO
-ostree-receive.conf
-pyproject.toml
-pytest.ini
-setup.cfg
-setup.py
-tox.ini
-ostree_push.egg-info/PKG-INFO
-ostree_push.egg-info/SOURCES.txt
-ostree_push.egg-info/dependency_links.txt
-ostree_push.egg-info/entry_points.txt
-ostree_push.egg-info/requires.txt
-ostree_push.egg-info/top_level.txt
-otpush/__init__.py
-otpush/push.py
-otpush/receive.py
-scripts/ostree-receive-shell
-tests/__init__.py
-tests/conftest.py
-tests/dumpenv
-tests/ostree-push
-tests/ostree-receive
-tests/test_full.py
-tests/test_push.py
-tests/test_receive.py
-tests/test_receive_shell.py
-tests/test_sshd.py
-tests/util.py
-tests/data/host_rsa_key
-tests/data/host_rsa_key.pub
-tests/data/id_rsa
-tests/data/id_rsa.pub
-tests/data/pgp-key.asc
-tests/data/pgp-pub.asc
-tests/data/pgp-pub.gpg
-tests/data/sshd_config
\ No newline at end of file
diff --git a/ostree_push.egg-info/dependency_links.txt b/ostree_push.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/ostree_push.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ostree_push.egg-info/entry_points.txt b/ostree_push.egg-info/entry_points.txt
deleted file mode 100644
index eb78bd6..0000000
--- a/ostree_push.egg-info/entry_points.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-[console_scripts]
-ostree-push = otpush.push:main
-ostree-receive = otpush.receive:main
diff --git a/ostree_push.egg-info/requires.txt b/ostree_push.egg-info/requires.txt
deleted file mode 100644
index 12e0019..0000000
--- a/ostree_push.egg-info/requires.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-PyGObject
-PyYAML
diff --git a/ostree_push.egg-info/top_level.txt b/ostree_push.egg-info/top_level.txt
deleted file mode 100644
index 9fab194..0000000
--- a/ostree_push.egg-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-otpush
diff --git a/otpush/__init__.py b/otpush/__init__.py
index 1dea037..0ad4a58 100644
--- a/otpush/__init__.py
+++ b/otpush/__init__.py
@@ -1 +1 @@
-VERSION = '1.0.1'
+VERSION = '1.1.1'
diff --git a/otpush/receive.py b/otpush/receive.py
index 5475da0..b439c50 100755
--- a/otpush/receive.py
+++ b/otpush/receive.py
@@ -70,9 +70,30 @@ class OTReceiveConfigError(OTReceiveError):
@dataclasses.dataclass
-class OTReceiveConfig:
+class OTReceiveRepoConfig:
"""OTReceiveRepo configuration
+ The path and url fields are required. See the OTReceiveConfig class for
+ details on the remaining optional fields.
+ """
+ path: Path
+ url: str
+ gpg_sign: list = dataclasses.field(default_factory=list)
+ gpg_homedir: str = None
+ gpg_verify: bool = False
+ gpg_trustedkeys: str = None
+ sign_type: str = 'ed25519'
+ sign_keyfiles: list = dataclasses.field(default_factory=list)
+ sign_verify: bool = False
+ sign_trustedkeyfile: str = None
+ update: bool = True
+ update_hook: str = None
+
+
+@dataclasses.dataclass
+class OTReceiveConfig:
+ """OTReceive configuration
+
Configuration can be provided from a file or command line arguments using
the load method. Config files are YAML mappings with the option names
below using hypens instead of underscores. By default, the paths
@@ -107,6 +128,11 @@ class OTReceiveConfig:
to the absolute path of the OSTree repository and the environment
variable OSTREE_RECEIVE_REFS set to the set of refs received separated
by whitespace.
+ repos: Optional per-repository configuration settings. All of the above
+ settings except for root can be set and will override the global value.
+ The value is a map of repository path to map of settings. The repository
+ path can be relative or absolute. If root is specified, relative paths
+ are resolved below it.
log_level: Set the log level. See the logging module for available levels.
force: Force receiving commits even if nothing changed or the remote
commits are not newer than the current commits.
@@ -124,6 +150,7 @@ class OTReceiveConfig:
sign_trustedkeyfile: str = None
update: bool = True
update_hook: str = None
+ repos: dict = dataclasses.field(default_factory=dict)
log_level: str = 'INFO'
force: bool = False
dry_run: bool = False
@@ -217,8 +244,70 @@ class OTReceiveConfig:
config_home / 'ostree/ostree-receive.conf',
]
+ def get_repo_config(self, path, url):
+ """Get OTReceiveRepoConfig instance for repo path and URL"""
+ repo_path = Path(path)
+ repo_root = (
+ Path(self.root).resolve() if self.root else None
+ )
+
+ if repo_root:
+ if not repo_path.is_absolute():
+ # Join the relative path to the root.
+ repo_path = repo_root.joinpath(repo_path)
+
+ # Make sure the path is below the root.
+ repo_path = repo_path.resolve()
+ try:
+ repo_path.relative_to(repo_root)
+ except ValueError:
+ raise OTReceiveError(f'repo {path} not found') from None
+
+ # Ensure the repository exists.
+ if not repo_path.exists():
+ raise OTReceiveError(f'repo {path} not found')
+
+ # See if there's a matching path in repos.
+ for key, values in self.repos.items():
+ config_path = Path(key)
+ if repo_root and not config_path.is_absolute():
+ config_path = repo_root.joinpath(config_path)
+ try:
+ matches = repo_path.samefile(config_path)
+ except FileNotFoundError:
+ matches = False
+
+ if matches:
+ logger.debug(f'Applying repos {key} configuration')
+ per_repo_config = values
+ break
+ else:
+ per_repo_config = {}
+
+ # Copy all but path and url from the per-repo or the global
+ # receive config.
+ repo_config_fields = {
+ field.name for field in dataclasses.fields(OTReceiveRepoConfig)
+ }
+ receive_config_fields = {
+ field.name for field in dataclasses.fields(self)
+ }
+ common_fields = repo_config_fields & receive_config_fields
+ repo_config_args = {
+ field: per_repo_config.get(field, getattr(self, field))
+ for field in common_fields
+ }
+ repo_config_args['path'] = repo_path
+ repo_config_args['url'] = url
+
+ return OTReceiveRepoConfig(**repo_config_args)
+
class OTReceiveRepo(OSTree.Repo):
+ """OSTree repository receiving pushed commits
+
+ An OTReceiveRepoConfig instance is required.
+ """
# The fake remote name
REMOTE_NAME = '_receive'
@@ -229,32 +318,18 @@ class OTReceiveRepo(OSTree.Repo):
OSTree.REPO_METADATA_REF,
)
- def __init__(self, path, url, config=None):
- self.path = Path(path)
- self.url = url
+ def __init__(self, config):
+ self.config = config
self.remotes_dir = None
- if config:
- if not isinstance(config, OTReceiveConfig):
- raise OTReceiveError(
- 'config is not an OTReceiveConfig instance'
- )
- self.config = config
- else:
- self.config = OTReceiveConfig()
-
- if self.config.root:
- repo_root = Path(self.config.root).resolve()
- if not self.path.is_absolute():
- # Join the relative path to the root.
- self.path = repo_root.joinpath(self.path)
+ if not isinstance(self.config, OTReceiveRepoConfig):
+ raise OTReceiveError(
+ 'config is not an OTReceiveRepoConfig instance'
+ )
- # Make sure the path is below the root.
- self.path = self.path.resolve()
- try:
- self.path.relative_to(repo_root)
- except ValueError:
- raise OTReceiveError(f'repo {path} not found') from None
+ # Ensure the repository exists.
+ if not self.path.exists():
+ raise OTReceiveError(f'repo {self.path} not found')
logger.debug('Using repo path %s', self.path)
@@ -292,6 +367,14 @@ class OTReceiveRepo(OSTree.Repo):
remotes_config_dir=self.remotes_dir.name)
self.open()
+ @property
+ def path(self):
+ return self.config.path
+
+ @property
+ def url(self):
+ return self.config.url
+
def __enter__(self):
return self
@@ -533,10 +616,18 @@ class OTReceiveRepo(OSTree.Repo):
safe_sign_opts.append(f'--sign=<key #{i} from {keyfile}>')
if self._is_flatpak_repo():
- cmd_prefix = ['flatpak', 'build-update-repo', str(self.path)]
+ cmd_prefix = [
+ 'flatpak',
+ 'build-update-repo',
+ str(self.path),
+ ]
else:
- cmd_prefix = ['ostree', f'--repo={self.path}', 'summary',
- '--update']
+ cmd_prefix = [
+ 'ostree',
+ f'--repo={self.path}',
+ 'summary',
+ '--update',
+ ]
logger.info('Updating repo metadata with %s',
' '.join(cmd_prefix + safe_sign_opts))
subprocess.check_call(cmd_prefix + sign_opts)
@@ -562,7 +653,7 @@ class OTReceiveRepo(OSTree.Repo):
logger.debug('OSTREE_RECEIVE_REFS=%s', env['OSTREE_RECEIVE_REFS'])
subprocess.check_call(cmd, env=env)
- def receive(self, refs):
+ def receive(self, refs, force=False, dry_run=False):
# See what revisions we're pulling.
_, remote_refs = self.remote_list_refs(self.REMOTE_NAME)
if len(refs) == 0:
@@ -596,7 +687,7 @@ class OTReceiveRepo(OSTree.Repo):
raise OTReceiveError(
f'Could not find ref {ref} in summary file')
- if self.config.force or remote_rev != current_rev:
+ if force or remote_rev != current_rev:
logger.debug('Pulling %s', ref)
refs_to_pull[ref] = remote_rev
@@ -635,16 +726,17 @@ class OTReceiveRepo(OSTree.Repo):
else:
if remote_timestamp <= current_timestamp:
logger.warning(
- 'ref %s remote rev %s is not newer than '
- 'current rev %s',
- ref, remote_rev, current_rev
+ 'received %s commit %s is not newer than '
+ 'current %s commit %s',
+ ref, remote_rev, ref, current_rev
)
if current_root.equal(remote_root):
logger.warning(
- 'ref %s remote commit %s root equals %s',
- ref, remote_rev, current_rev
+ 'received %s commit %s has the same content '
+ 'as current %s commit %s',
+ ref, remote_rev, ref, current_rev
)
- if self.config.force:
+ if force:
logger.info('Forcing merge of ref %s', ref)
refs_to_merge[ref] = remote_rev
@@ -654,7 +746,7 @@ class OTReceiveRepo(OSTree.Repo):
return set()
# For a dry run, exit now before creating the refs
- if self.config.dry_run:
+ if dry_run:
self.abort_transaction()
return refs_to_merge.keys()
@@ -680,6 +772,30 @@ class OTReceiveRepo(OSTree.Repo):
return refs_to_merge.keys()
+class OTReceiver:
+ """Pushed commit receiver
+
+ An OTReceiveConfig instance can be provided to configure the receiver.
+ """
+ def __init__(self, config=None):
+ self.config = config or OTReceiveConfig()
+
+ if not isinstance(self.config, OTReceiveConfig):
+ raise OTReceiveError(
+ 'config is not an OTReceiveConfig instance'
+ )
+
+ def receive(self, path, url, refs):
+ """Receive pushed commits
+
+ Creates an OTReceiveRepo at path and receives commits on refs
+ from url.
+ """
+ repo_config = self.config.get_repo_config(path, url)
+ with OTReceiveRepo(repo_config) as repo:
+ return repo.receive(refs, self.config.force, self.config.dry_run)
+
+
class OTReceiveArgParser(ArgumentParser):
"""ArgumentParse for ostree-receive"""
def __init__(self):
@@ -725,8 +841,8 @@ def main():
logging.basicConfig(level=config.log_level)
- with OTReceiveRepo(args.repo, args.url, config) as repo:
- repo.receive(args.refs)
+ receiver = OTReceiver(config)
+ receiver.receive(args.repo, args.url, args.refs)
if __name__ == '__main__':
diff --git a/setup.cfg b/setup.cfg
index dcfc94d..7f572fc 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,33 +8,28 @@ long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/dbnicholson/ostree-push
license_file = COPYING
-classifiers =
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.7
- Programming Language :: Python :: 3.8
- Programming Language :: Python :: 3.9
- Development Status :: 5 - Production/Stable
- License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
- Operating System :: POSIX
- Topic :: Software Development :: Build Tools
- Topic :: System :: Archiving :: Mirroring
- Topic :: System :: Archiving :: Packaging
- Topic :: System :: Software Distribution
+classifiers =
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Development Status :: 5 - Production/Stable
+ License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
+ Operating System :: POSIX
+ Topic :: Software Development :: Build Tools
+ Topic :: System :: Archiving :: Mirroring
+ Topic :: System :: Archiving :: Packaging
+ Topic :: System :: Software Distribution
[options]
packages = otpush
scripts = scripts/ostree-receive-shell
-install_requires =
- PyGObject
- PyYAML
+install_requires =
+ PyGObject
+ PyYAML
python_requires = >=3.7
[options.entry_points]
-console_scripts =
- ostree-push = otpush.push:main
- ostree-receive = otpush.receive:main
-
-[egg_info]
-tag_build =
-tag_date = 0
-
+console_scripts =
+ ostree-push = otpush.push:main
+ ostree-receive = otpush.receive:main
diff --git a/tests/conftest.py b/tests/conftest.py
index 1550f8b..e3dd607 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -120,10 +120,19 @@ def dest_repo(tmp_path):
@pytest.fixture
-def receive_repo(dest_repo, source_server):
- repo_path = str(dest_repo.path)
+def receiver():
config = receive.OTReceiveConfig(update=False)
- with receive.OTReceiveRepo(repo_path, source_server.url, config) as repo:
+ return receive.OTReceiver(config)
+
+
+@pytest.fixture
+def receive_repo(dest_repo, source_server):
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ update=False,
+ )
+ with receive.OTReceiveRepo(config) as repo:
yield repo
diff --git a/tests/test_receive.py b/tests/test_receive.py
index 9c4ded8..9ded4bc 100644
--- a/tests/test_receive.py
+++ b/tests/test_receive.py
@@ -29,7 +29,6 @@ from .util import (
oneshot_transaction,
random_commit,
wipe_repo,
- TmpRepo,
)
gi.require_version('OSTree', '1.0')
@@ -41,17 +40,26 @@ logger = logging.getLogger(__name__)
class TestReceiveRepo:
def test_cleanup(self, dest_repo):
url = 'http://example.com'
- repo = receive.OTReceiveRepo(dest_repo.path, url)
+ config = receive.OTReceiveRepoConfig(dest_repo.path, url)
+ repo = receive.OTReceiveRepo(config)
remotes_dir = Path(repo.remotes_dir.name)
assert remotes_dir.exists()
del repo
assert not remotes_dir.exists()
- with receive.OTReceiveRepo(dest_repo.path, url) as repo:
+ with receive.OTReceiveRepo(config) as repo:
remotes_dir = Path(repo.remotes_dir.name)
assert remotes_dir.exists()
assert not remotes_dir.exists()
+ def test_missing_repo(self, tmp_path):
+ repo_path = tmp_path / 'repo'
+ url = 'http://example.com'
+ config = receive.OTReceiveRepoConfig(repo_path, url)
+ with pytest.raises(receive.OTReceiveError) as excinfo:
+ receive.OTReceiveRepo(config)
+ assert str(excinfo.value) == f'repo {repo_path} not found'
+
def test_get_commit_timestamp(self, tmp_files_path, receive_repo):
with pytest.raises(GLib.Error) as excinfo:
receive_repo._get_commit_timestamp('missing')
@@ -307,22 +315,29 @@ class TestReceiveRepo:
monkeypatch):
# Specifying a missing GPG keyring should fail
keyring_path = str(tmp_path / 'missing.gpg')
- config = receive.OTReceiveConfig(gpg_verify=True,
- gpg_trustedkeys=keyring_path,
- update=False)
- repo_path = str(dest_repo.path)
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ gpg_verify=True,
+ gpg_trustedkeys=keyring_path,
+ update=False,
+ )
with pytest.raises(receive.OTReceiveConfigError) as excinfo:
- receive.OTReceiveRepo(repo_path, source_server.url, config)
+ receive.OTReceiveRepo(config)
assert str(excinfo.value) == (
f'gpg_trustedkeys keyring "{keyring_path}" does not exist'
)
# Receiving an unsigned commit should fail.
random_commit(source_repo, tmp_files_path, 'ref1')
- config = receive.OTReceiveConfig(gpg_verify=True,
- gpg_trustedkeys=str(PGP_PUB_KEYRING),
- update=False)
- repo = receive.OTReceiveRepo(repo_path, source_server.url, config)
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ gpg_verify=True,
+ gpg_trustedkeys=str(PGP_PUB_KEYRING),
+ update=False,
+ )
+ repo = receive.OTReceiveRepo(config)
with pytest.raises(GLib.Error) as excinfo:
repo.receive(['ref1'])
assert excinfo.value.matches(OSTree.gpg_error_quark(),
@@ -331,10 +346,14 @@ class TestReceiveRepo:
# Receiving a signed commit should succeed.
random_commit(source_repo, tmp_files_path, 'ref1',
gpg_key_id=PGP_KEY_ID, gpg_homedir=str(gpg_homedir))
- config = receive.OTReceiveConfig(gpg_verify=True,
- gpg_trustedkeys=str(PGP_PUB_KEYRING),
- update=False)
- repo = receive.OTReceiveRepo(repo_path, source_server.url, config)
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ gpg_verify=True,
+ gpg_trustedkeys=str(PGP_PUB_KEYRING),
+ update=False,
+ )
+ repo = receive.OTReceiveRepo(config)
wipe_repo(repo)
merged = repo.receive(['ref1'])
assert merged == {'ref1'}
@@ -345,10 +364,14 @@ class TestReceiveRepo:
# also work.
random_commit(source_repo, tmp_files_path, 'ref1',
gpg_key_id=PGP_KEY_ID, gpg_homedir=str(gpg_homedir))
- config = receive.OTReceiveConfig(gpg_verify=True,
- gpg_trustedkeys=str(PGP_PUB),
- update=False)
- repo = receive.OTReceiveRepo(repo_path, source_server.url, config)
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ gpg_verify=True,
+ gpg_trustedkeys=str(PGP_PUB),
+ update=False,
+ )
+ repo = receive.OTReceiveRepo(config)
wipe_repo(repo)
merged = repo.receive(['ref1'])
assert merged == {'ref1'}
@@ -362,8 +385,13 @@ class TestReceiveRepo:
keyring = tmp_path / 'ostree/ostree-receive-trustedkeys.gpg'
keyring.parent.mkdir(exist_ok=True)
keyring.symlink_to(PGP_PUB_KEYRING)
- config = receive.OTReceiveConfig(gpg_verify=True, update=False)
- repo = receive.OTReceiveRepo(repo_path, source_server.url, config)
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ gpg_verify=True,
+ update=False,
+ )
+ repo = receive.OTReceiveRepo(config)
wipe_repo(repo)
merged = repo.receive(['ref1'])
assert merged == {'ref1'}
@@ -403,23 +431,29 @@ class TestReceiveRepo:
ed25519_public_keyfile, monkeypatch):
# Specifying a missing keyfile should fail.
keyfile_path = str(tmp_path / 'missing')
- config = receive.OTReceiveConfig(sign_verify=True,
- sign_trustedkeyfile=keyfile_path,
- update=False)
- repo_path = str(dest_repo.path)
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ sign_verify=True,
+ sign_trustedkeyfile=keyfile_path,
+ update=False,
+ )
with pytest.raises(receive.OTReceiveConfigError,
match='sign_trustedkeyfile keyfile'
+ f' "{keyfile_path}" does not'
+ ' exist') as excinfo:
- receive.OTReceiveRepo(repo_path, source_server.url, config)
+ receive.OTReceiveRepo(config)
# Receiving an unsigned commit should fail.
random_commit(source_repo, tmp_files_path, 'ref1')
- config = receive.OTReceiveConfig(
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
sign_verify=True,
sign_trustedkeyfile=ed25519_public_keyfile,
- update=False)
- repo = receive.OTReceiveRepo(repo_path, source_server.url, config)
+ update=False,
+ )
+ repo = receive.OTReceiveRepo(config)
with pytest.raises(GLib.Error, match="Can't verify commit") as excinfo:
repo.receive(['ref1'])
assert excinfo.value.matches(Gio.io_error_quark(),
@@ -428,11 +462,14 @@ class TestReceiveRepo:
# Receiving a signed commit should succeed.
random_commit(source_repo, tmp_files_path, 'ref1',
ed25519_key=ED25519_PRIVATE_KEY)
- config = receive.OTReceiveConfig(
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
sign_verify=True,
sign_trustedkeyfile=ed25519_public_keyfile,
- update=False)
- repo = receive.OTReceiveRepo(repo_path, source_server.url, config)
+ update=False,
+ )
+ repo = receive.OTReceiveRepo(config)
wipe_repo(repo)
merged = repo.receive(['ref1'])
assert merged == {'ref1'}
@@ -446,8 +483,13 @@ class TestReceiveRepo:
keyring = tmp_path / 'ostree/ostree-receive-trustedkeyfile.ed25519'
keyring.parent.mkdir(exist_ok=True)
keyring.symlink_to(ed25519_public_keyfile)
- config = receive.OTReceiveConfig(sign_verify=True, update=False)
- repo = receive.OTReceiveRepo(repo_path, source_server.url, config)
+ config = receive.OTReceiveRepoConfig(
+ dest_repo.path,
+ source_server.url,
+ sign_verify=True,
+ update=False,
+ )
+ repo = receive.OTReceiveRepo(config)
wipe_repo(repo)
merged = repo.receive(['ref1'])
assert merged == {'ref1'}
@@ -634,36 +676,198 @@ class TestReceiveRepo:
refs = local_refs(receive_repo)
assert refs.keys() == {'ref1', 'ref2'}
- def test_root(self, tmp_path, tmp_files_path, source_server):
- url = source_server.url
- root = tmp_path / 'pub/repos'
- root.mkdir(parents=True)
- config = receive.OTReceiveConfig(root=str(root), update=False)
- root_tmp_repo = TmpRepo(root / 'root-dest')
- non_root_tmp_repo = TmpRepo(tmp_path / 'non-root-dest')
-
- # Requesting a repo outside the root should fail
- repo_path = non_root_tmp_repo.path
- logger.debug('Repo path %s', repo_path)
- with pytest.raises(receive.OTReceiveError) as excinfo:
- receive.OTReceiveRepo(str(repo_path), url, config)
- assert str(excinfo.value) == (
- f'repo {non_root_tmp_repo.path} not found'
+ def test_receive_dry_run(self, tmp_files_path, receive_repo, source_repo,
+ source_server):
+ random_commit(source_repo, tmp_files_path, 'ref1')
+ merged = receive_repo.receive(['ref1'], dry_run=True)
+ assert merged == {'ref1'}
+ refs = local_refs(receive_repo)
+ assert refs.keys() == set()
+
+ def test_receive_force(self, tmp_files_path, receive_repo, source_repo,
+ source_server, caplog):
+ caplog.set_level(logging.WARNING, receive.logger.name)
+
+ # First make a commit and pull it directly so the destination
+ # has the exact same commit.
+ checksum = random_commit(
+ source_repo,
+ tmp_files_path,
+ 'ref1',
+ timestamp=0,
+ )
+ opts = GLib.Variant('a{sv}', {
+ 'refs': GLib.Variant('as', ['ref1']),
+ })
+ receive_repo.pull_with_options(source_repo.path.as_uri(), opts)
+ refs = local_refs(receive_repo)
+ assert refs == {'ref1': checksum}
+
+ # Non-forced receive will get nothing. There should be no
+ # warnings since the commits are exactly the same.
+ caplog.clear()
+ merged = receive_repo.receive(['ref1'])
+ assert merged == set()
+ refs = local_refs(receive_repo)
+ assert refs == {'ref1': checksum}
+ assert caplog.record_tuples == []
+
+ # Forced merge will make a new commit. This will have warnings
+ # about both timestamp and content.
+ caplog.clear()
+ merged = receive_repo.receive(['ref1'], force=True)
+ assert merged == {'ref1'}
+ refs = local_refs(receive_repo)
+ assert refs.keys() == {'ref1'}
+ assert refs['ref1'] != checksum
+ assert caplog.record_tuples == [
+ (
+ receive.logger.name, logging.WARNING,
+ f'received ref1 commit {checksum} is not newer than '
+ f'current ref1 commit {checksum}'
+ ),
+ (
+ receive.logger.name, logging.WARNING,
+ f'received ref1 commit {checksum} has the same content as '
+ f'current ref1 commit {checksum}'
+ ),
+ ]
+
+ # Make a new commit with the same content and set the
+ # destination repo back to the original commit.
+ with oneshot_transaction(source_repo):
+ mtree = OSTree.MutableTree.new()
+ _, root, _ = source_repo.read_commit(checksum)
+ _, commit, _ = source_repo.load_commit(checksum)
+ source_repo.write_directory_to_mtree(root, mtree, None)
+ _, new_root = source_repo.write_mtree(mtree)
+ metadata = commit.get_child_value(0)
+ _, new_checksum = source_repo.write_commit_with_time(
+ checksum,
+ 'Test commit',
+ None,
+ metadata,
+ new_root,
+ 1,
+ )
+ source_repo.transaction_set_ref(None, 'ref1', new_checksum)
+ receive_repo.set_ref_immediate(None, 'ref1', checksum)
+
+ # Non-forced receive will get nothing but there will be a
+ # warning about the content.
+ caplog.clear()
+ merged = receive_repo.receive(['ref1'])
+ assert merged == set()
+ refs = local_refs(receive_repo)
+ assert refs == {'ref1': checksum}
+ assert caplog.record_tuples == [
+ (
+ receive.logger.name, logging.WARNING,
+ f'received ref1 commit {new_checksum} has the same content '
+ f'as current ref1 commit {checksum}'
+ ),
+ ]
+
+ # Forced merge will make a new commit.
+ caplog.clear()
+ merged = receive_repo.receive(['ref1'], force=True)
+ assert merged == {'ref1'}
+ refs = local_refs(receive_repo)
+ assert refs.keys() == {'ref1'}
+ assert refs['ref1'] != checksum
+
+ # Make a random commit in the destination so it's newer and has
+ # different content.
+ dest_checksum = random_commit(
+ receive_repo,
+ tmp_files_path,
+ 'ref1',
+ timestamp=2,
)
- # Absolute path under the root should work
- repo_path = root_tmp_repo.path.resolve()
- assert repo_path.is_absolute()
- logger.debug('Repo path %s', repo_path)
- with receive.OTReceiveRepo(str(repo_path), url, config):
- pass
+ # Non-forced receive will get nothing but there will be a
+ # warning about the timestamp.
+ caplog.clear()
+ merged = receive_repo.receive(['ref1'])
+ assert merged == set()
+ refs = local_refs(receive_repo)
+ assert refs == {'ref1': dest_checksum}
+ assert caplog.record_tuples == [
+ (
+ receive.logger.name, logging.WARNING,
+ f'received ref1 commit {new_checksum} is not newer than '
+ f'current ref1 commit {dest_checksum}'
+ ),
+ ]
+
+ # Forced merge will make a new commit.
+ caplog.clear()
+ merged = receive_repo.receive(['ref1'], force=True)
+ assert merged == {'ref1'}
+ refs = local_refs(receive_repo)
+ assert refs.keys() == {'ref1'}
+ assert refs['ref1'] != dest_checksum
- # Relative path under the root should work
- repo_path = root_tmp_repo.path.relative_to(root)
- assert not repo_path.is_absolute()
- logger.debug('Repo path %s', repo_path)
- with receive.OTReceiveRepo(str(repo_path), url, config):
- pass
+
+class TestReceiver:
+ """Tests for OTReceiver class"""
+ def test_default_config(self):
+ receiver = receive.OTReceiver()
+ assert receiver.config == receive.OTReceiveConfig()
+
+ def test_receive(self, receiver, tmp_files_path, source_repo, dest_repo,
+ source_server):
+ random_commit(source_repo, tmp_files_path, 'ref1')
+ source_refs = local_refs(source_repo)
+ assert source_refs.keys() == {'ref1'}
+
+ merged = receiver.receive(dest_repo.path, source_server.url, ['ref1'])
+ assert merged == {'ref1'}
+ dest_refs = local_refs(dest_repo)
+ assert dest_refs.keys() == {'ref1'}
+
+ merged = receiver.receive(dest_repo.path, source_server.url, ['ref1'])
+ assert merged == set()
+ dest_refs = local_refs(dest_repo)
+ assert dest_refs.keys() == {'ref1'}
+
+ # Test that repos override is applied.
+ summary_path = dest_repo.path / 'summary'
+ assert not summary_path.exists()
+ assert not receiver.config.update
+ receiver.config.repos = {str(dest_repo.path): {'update': True}}
+ random_commit(source_repo, tmp_files_path, 'ref2')
+ merged = receiver.receive(dest_repo.path, source_server.url, ['ref2'])
+ assert merged == {'ref2'}
+ assert summary_path.exists()
+
+
+class TestRepoConfig:
+ """Tests for OTReceiveRepoConfig"""
+ def test_defaults(self):
+ config = receive.OTReceiveRepoConfig(Path('foo'), 'http://bar')
+ assert dataclasses.asdict(config) == {
+ 'path': Path('foo'),
+ 'url': 'http://bar',
+ 'gpg_sign': [],
+ 'gpg_homedir': None,
+ 'gpg_verify': False,
+ 'gpg_trustedkeys': None,
+ 'sign_type': 'ed25519',
+ 'sign_keyfiles': [],
+ 'sign_verify': False,
+ 'sign_trustedkeyfile': None,
+ 'update': True,
+ 'update_hook': None,
+ }
+
+ def test_required(self):
+ with pytest.raises(TypeError):
+ receive.OTReceiveRepoConfig()
+ with pytest.raises(TypeError):
+ receive.OTReceiveRepoConfig(path=Path('foo'))
+ with pytest.raises(TypeError):
+ receive.OTReceiveRepoConfig(url='http://bar')
class TestConfig:
@@ -682,6 +886,7 @@ class TestConfig:
'sign_trustedkeyfile': None,
'update': True,
'update_hook': None,
+ 'repos': {},
'log_level': 'INFO',
'force': False,
'dry_run': False,
@@ -734,6 +939,14 @@ class TestConfig:
'sign_trustedkeyfile': str(tmp_path / 'trustedkey'),
'update': False,
'update_hook': '/foo/bar baz',
+ 'repos': {
+ 'foo': {
+ 'gpg_sign': ['76543210'],
+ },
+ 'bar': {
+ 'gpg_verify': False,
+ },
+ },
'log_level': 'DEBUG',
'force': True,
'dry_run': True,
@@ -876,6 +1089,7 @@ class TestConfig:
'sign_trustedkeyfile': None,
'update': True,
'update_hook': None,
+ 'repos': {},
'log_level': 'WARNING',
'force': False,
'dry_run': False,
@@ -917,6 +1131,117 @@ class TestConfig:
config = receive.OTReceiveConfig.load(paths=[path], args=args)
assert config.log_level == 'WARNING'
+ def test_repo_config(self, tmp_path, monkeypatch):
+ monkeypatch.chdir(tmp_path)
+ config = receive.OTReceiveConfig()
+ url = 'http://example.com'
+ rel_root = Path('root')
+ root = rel_root.resolve()
+ root.mkdir()
+ root_repo = root / 'repo'
+ rel_root_repo = root_repo.relative_to(root)
+ root_repo.mkdir()
+ rel_nonroot_repo = Path('repo')
+ nonroot_repo = rel_nonroot_repo.resolve()
+ nonroot_repo.mkdir()
+
+ # Non-existent repo should raise an exception.
+ repo_path = tmp_path / 'nonexistent'
+ with pytest.raises(receive.OTReceiveError) as excinfo:
+ config.get_repo_config(repo_path, url)
+ assert str(excinfo.value) == f'repo {repo_path} not found'
+
+ # Without root setup, the path should be passed back as is.
+ repo_config = config.get_repo_config(str(rel_nonroot_repo), url)
+ assert repo_config.path == rel_nonroot_repo
+ repo_config = config.get_repo_config(rel_nonroot_repo, url)
+ assert repo_config.path == rel_nonroot_repo
+ repo_config = config.get_repo_config(str(nonroot_repo), url)
+ assert repo_config.path == nonroot_repo
+ repo_config = config.get_repo_config(nonroot_repo, url)
+ assert repo_config.path == nonroot_repo
+
+ # Requesting a repo outside the root should fail.
+ config.root = str(root)
+ with pytest.raises(receive.OTReceiveError) as excinfo:
+ config.get_repo_config(nonroot_repo, url)
+ assert str(excinfo.value) == f'repo {nonroot_repo} not found'
+
+ # All combinations of root, repo path, and config override path.
+ base_expected_config = {
+ 'path': nonroot_repo,
+ 'url': url,
+ 'gpg_sign': config.gpg_sign,
+ 'gpg_homedir': config.gpg_homedir,
+ 'gpg_verify': config.gpg_verify,
+ 'gpg_trustedkeys': config.gpg_trustedkeys,
+ 'sign_type': config.sign_type,
+ 'sign_keyfiles': config.sign_keyfiles,
+ 'sign_verify': config.sign_verify,
+ 'sign_trustedkeyfile': config.sign_trustedkeyfile,
+ 'update': config.update,
+ 'update_hook': config.update_hook,
+ }
+ for root_path, repo_path, override_path, expected_repo_path in (
+ # Absolute repo path with no root and no override.
+ (None, nonroot_repo, None, nonroot_repo),
+ # Relative repo path with no root and no override.
+ (None, rel_nonroot_repo, None, rel_nonroot_repo),
+ # Absolute repo path with absolute root and no override.
+ (root, root_repo, None, root_repo),
+ # Relative repo path with absolute root and no override.
+ (root, rel_root_repo, None, root_repo),
+ # Absolute repo path with relative root and no override.
+ (rel_root, root_repo, None, root_repo),
+ # Relative repo path with relative root and no override.
+ (rel_root, rel_root_repo, None, root_repo),
+
+ # Absolute repo path with no root and absolute override.
+ (None, nonroot_repo, nonroot_repo, nonroot_repo),
+ # Relative repo path with no root and absolute override.
+ (None, rel_nonroot_repo, nonroot_repo, rel_nonroot_repo),
+ # Absolute repo path with absolute root and absolute override.
+ (root, root_repo, root_repo, root_repo),
+ # Relative repo path with absolute root and absolute override.
+ (root, rel_root_repo, root_repo, root_repo),
+ # Absolute repo path with relative root and absolute override.
+ (rel_root, root_repo, root_repo, root_repo),
+ # Relative repo path with relative root and absolute override.
+ (rel_root, rel_root_repo, root_repo, root_repo),
+
+ # Absolute repo path with no root and relative override.
+ (None, nonroot_repo, rel_nonroot_repo, nonroot_repo),
+ # Relative repo path with no root and relative override.
+ (None, rel_nonroot_repo, rel_nonroot_repo, rel_nonroot_repo),
+ # Absolute repo path with absolute root and relative override.
+ (root, root_repo, rel_root_repo, root_repo),
+ # Relative repo path with absolute root and relative override.
+ (root, rel_root_repo, rel_root_repo, root_repo),
+ # Absolute repo path with relative root and relative override.
+ (rel_root, root_repo, rel_root_repo, root_repo),
+ # Relative repo path with relative root and relative override.
+ (rel_root, rel_root_repo, rel_root_repo, root_repo),
+ ):
+ logger.debug(
+ f'Testing {root_path=}, {repo_path=}, {override_path=}, '
+ f'{expected_repo_path=}',
+ )
+
+ expected_config = base_expected_config.copy()
+ expected_config['path'] = expected_repo_path
+ config.root = str(root_path) if root_path else None
+ if override_path:
+ config.repos = {str(override_path): {'update': False}}
+ expected_config['update'] = False
+ else:
+ config.repos = {}
+ expected_config['update'] = True
+
+ repo_config = config.get_repo_config(repo_path, url)
+ assert dataclasses.asdict(repo_config) == expected_config
+ if override_path:
+ assert repo_config.update != config.update
+
class TestArgParser:
def test_no_repo(self, capsys):
More details
Historical runs
- failed: E subprocess.CalledProcessError: Command '('gpg', '--homedir', '/tmp/pytest-of-janitor/pytest-0/test_receive_gpg_sign0/gnupg', '--import', '/<<PKGBUILDDIR>>/.pybuild/cpython3_3.11_ostree-push/build/tests/data/pgp-key.asc')' returned non-zero exit status 2.
- worker-exception: W:GPG error: http://deb.debian.org/debian sid InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 648ACFD622F3D138 NO_PUBKEY 0E98404D386FA1D9, E:Release file for http://deb.debian.org/debian/dists/sid/InRelease is not valid yet (invalid for another 2d 0h 24min 4s). Updates for this repository will not be applied.
- failed: E subprocess.CalledProcessError: Command '('gpg', '--homedir', '/tmp/pytest-of-root/pytest-0/test_receive_gpg_verify0/gnupg', '--import', '/<<PKGBUILDDIR>>/.pybuild/cpython3_3.10_ostree-push/build/tests/data/pgp-key.asc')' returned non-zero exit status 2.
- failed: E subprocess.CalledProcessError: Command '('gpg', '--homedir', '/tmp/pytest-of-root/pytest-0/test_receive_gpg_verify0/gnupg', '--import', '/<<PKGBUILDDIR>>/.pybuild/cpython3_3.10_ostree-push/build/tests/data/pgp-key.asc')' returned non-zero exit status 2.
- push-failed: Failed to push result branch: Connection closed: Connection closed early The remote server unexpectedly closed the connection.
- nothing-to-do: Last upstream version 1.0.0 already imported.
- success: Merged new upstream version 0.20220306+git7ca167a