Compare commits

..

4 Commits

Author SHA1 Message Date
6280c9dbe6 Release 2.19.1 2025-07-14 21:41:02 +03:00
3d1fdd5517 fix: fix migrations on empty repositories 2025-07-14 21:37:43 +03:00
ab022071e8 fix: trim provides/depends versions and lookup provides through pkgname
(#150)

Current implementation did it in wrong way. First of all, there was a
lookup through pkgbase instead of pkgname, which lead to errors, because
aur api doesn't allow to search by pkgbase (as well as provides is
basically pkgname instead)

It also was found that dependencies resolution lookup has been performed
by using raw packages array, which can include versions, descriptions
etc
2025-07-14 21:37:35 +03:00
a01f76df42 fix: separate ua by spaces 2025-07-14 21:37:25 +03:00
434 changed files with 1022 additions and 2375 deletions

View File

@ -7,7 +7,6 @@ on:
tags:
- '*'
- '!*rc*'
workflow_dispatch:
permissions:
contents: read

View File

@ -100,14 +100,6 @@ ahriman.application.handlers.rebuild module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.reload module
------------------------------------------
.. automodule:: ahriman.application.handlers.reload
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.remove module
------------------------------------------

View File

@ -1,21 +0,0 @@
ahriman.core.housekeeping package
=================================
Submodules
----------
ahriman.core.housekeeping.logs\_rotation\_trigger module
--------------------------------------------------------
.. automodule:: ahriman.core.housekeeping.logs_rotation_trigger
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.housekeeping
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -15,7 +15,6 @@ Subpackages
ahriman.core.distributed
ahriman.core.formatters
ahriman.core.gitremote
ahriman.core.housekeeping
ahriman.core.http
ahriman.core.log
ahriman.core.report

View File

@ -44,14 +44,6 @@ ahriman.web.schemas.changes\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.configuration\_schema module
------------------------------------------------
.. automodule:: ahriman.web.schemas.configuration_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.counters\_schema module
-------------------------------------------
@ -148,14 +140,6 @@ ahriman.web.schemas.logs\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.logs\_search\_schema module
-----------------------------------------------
.. automodule:: ahriman.web.schemas.logs_search_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.oauth2\_schema module
-----------------------------------------

View File

@ -12,14 +12,6 @@ ahriman.web.views.v1.service.add module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.config module
------------------------------------------
.. automodule:: ahriman.web.views.v1.service.config
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.logs module
----------------------------------------

View File

@ -40,7 +40,6 @@ This package contains everything required for the most of application actions an
* ``ahriman.core.distributed`` package with triggers and helpers for distributed build system.
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
* ``ahriman.core.housekeeping`` package provides few triggers for removing old data.
* ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes.
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and some wrappers.
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.

View File

@ -65,8 +65,6 @@ will try to read value from ``SECRET`` environment variable. In case if the requ
will eventually lead ``key`` option in section ``section1`` to be set to the value of ``HOME`` environment variable (if available).
Moreover, configuration can be read from environment variables directly by following the same naming convention, e.g. in the example above, one can have environment variable named ``section1:key`` (e.g. ``section1:key=$HOME``) and it will be substituted to the configuration with the highest priority.
There is also additional subcommand which will allow to validate configuration and print found errors. In order to do so, run ``service-config-validate`` subcommand, e.g.:
.. code-block:: shell
@ -83,6 +81,7 @@ Base configuration settings.
* ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually.
* ``database`` - path to the application SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
``alpm:*`` groups
@ -139,8 +138,6 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
Base repository settings.
* ``architecture`` - repository architecture, string. This field is read-only and generated automatically from run options if possible.
* ``name`` - repository name, string. This field is read-only and generated automatically from run options if possible.
* ``root`` - root path for application, string, required.
``sign:*`` groups
@ -169,7 +166,6 @@ Reporting to web service related settings. In most cases there is fallback to we
Web server settings. This feature requires ``aiohttp`` libraries to be installed.
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
* ``autorefresh_intervals`` - enable page auto refresh options, space separated list of integers, optional. The first defined interval will be used as default. If no intervals set, the auto refresh buttons will be disabled. If first element of the list equals ``0``, auto refresh will be disabled by default.
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
* ``host`` - host to bind, string, optional.
* ``index_url`` - full URL of the repository index page, string, optional.
@ -183,7 +179,7 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
``keyring`` group
-----------------
--------------------
Keyring package generator plugin.
@ -201,13 +197,6 @@ Keyring generator plugin
* ``revoked`` - list of revoked packagers keys, space separated list of strings, optional.
* ``trusted`` - list of master keys, space separated list of strings, optional, if not set, the ``key`` option from ``sign`` group will be used.
``housekeeping`` group
----------------------
This section describes settings for the ``ahriman.core.housekeeping.LogsRotationTrigger`` plugin.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
``mirrorlist`` group
--------------------

View File

@ -2,7 +2,7 @@
pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.19.0
pkgver=2.19.1
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
@ -40,7 +40,6 @@ package_ahriman-core() {
'rsync: sync by using rsync')
install="$pkgbase.install"
backup=('etc/ahriman.ini'
'etc/ahriman.ini.d/00-housekeeping.ini'
'etc/ahriman.ini.d/logging.ini')
cd "$pkgbase-$pkgver"
@ -50,7 +49,6 @@ package_ahriman-core() {
# keep usr/share configs as reference and copy them to /etc
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini.d/00-housekeeping.ini" "$pkgdir/etc/ahriman.ini.d/00-housekeeping.ini"
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini"
install -Dm644 "$srcdir/$pkgbase.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgbase.conf"

View File

@ -5,7 +5,6 @@ After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ahriman web
ExecReload=/usr/bin/ahriman web-reload
User=ahriman
Group=ahriman

View File

@ -7,6 +7,8 @@ logging = ahriman.ini.d/logging.ini
;apply_migrations = yes
; Path to the application SQLite database.
database = ${repository:root}/ahriman.db
; Keep last build logs for each package
keep_last_logs = 5
[alpm]
; Path to pacman system database cache.
@ -43,11 +45,9 @@ triggers[] = ahriman.core.gitremote.RemotePullTrigger
triggers[] = ahriman.core.report.ReportTrigger
triggers[] = ahriman.core.upload.UploadTrigger
triggers[] = ahriman.core.gitremote.RemotePushTrigger
triggers[] = ahriman.core.housekeeping.LogsRotationTrigger
; List of well-known triggers. Used only for configuration purposes.
triggers_known[] = ahriman.core.gitremote.RemotePullTrigger
triggers_known[] = ahriman.core.gitremote.RemotePushTrigger
triggers_known[] = ahriman.core.housekeeping.LogsRotationTrigger
triggers_known[] = ahriman.core.report.ReportTrigger
triggers_known[] = ahriman.core.upload.UploadTrigger
; Maximal age in seconds of the VCS packages before their version will be updated with its remote source.

View File

@ -1,3 +0,0 @@
[logs-rotation]
; Keep last build logs for each package
keep_last_logs = 5

View File

@ -28,10 +28,6 @@ allow_read_only = yes
; External address of the web service. Will be used for some features like OAuth. If none set will be generated as
; address = http://${web:host}:${web:port}
;address = http://${web:host}:${web:port}
; Enable page auto refresh. Intervals are given in seconds. Default interval is always the first element of the list.
; If no intervals set, auto refresh will be disabled. 0 can only be the first element and will disable auto refresh
; by default.
autorefresh_intervals = 5 1 10 30 60
; Enable file upload endpoint used by some triggers.
;enable_archive_upload = no
; Address to bind the server.

View File

@ -80,28 +80,10 @@
<button type="button" class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
</button>
{% if autorefresh_intervals %}
<div class="btn-group">
<input id="table-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="toggleTableAutoReload()" checked>
<label for="table-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">select interval</span>
</button>
<ul id="table-autoreload-input" class="dropdown-menu">
{% for interval in autorefresh_intervals %}
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="toggleTableAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<table id="packages"
data-classes="table table-hover"
data-cookie="true"
data-cookie-id-table="ahriman-packages"
data-cookie-storage="localStorage"
data-export-options='{"fileName": "packages"}'
data-filter-control="true"
data-filter-control-visible="false"
@ -120,8 +102,8 @@
data-sortable="true"
data-sort-name="base"
data-sort-order="asc"
data-toolbar="#toolbar"
data-unique-id="id">
data-toggle="table"
data-toolbar="#toolbar">
<thead class="table-primary">
<tr>
<th data-checkbox="true"></th>

View File

@ -3,9 +3,7 @@
function createAlert(title, message, clz, action, id) {
id ??= md5(title + message); // MD5 id from the content
if (alertPlaceholder.querySelector(`#alert-${id}`)) {
return; // check if there are duplicates
}
if (alertPlaceholder.querySelector(`#alert-${id}`)) return; // check if there are duplicates
const wrapper = document.createElement("div");
wrapper.id = `alert-${id}`;

View File

@ -51,87 +51,6 @@
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
let dashboardPackagesCountChart = null;
function statusLoad() {
const badgeClass = status => {
if (status === "pending") return "btn-outline-warning";
if (status === "building") return "btn-outline-warning";
if (status === "failed") return "btn-outline-danger";
if (status === "success") return "btn-outline-success";
return "btn-outline-secondary";
};
makeRequest(
"/api/v1/status",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
dashboardButton.classList.remove(...dashboardButton.classList);
dashboardButton.classList.add("btn");
dashboardButton.classList.add(badgeClass(data.status.status));
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
dashboardModalHeader.classList.add("modal-header");
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
dashboardName.textContent = data.repository;
dashboardArchitecture.textContent = data.architecture;
dashboardStatus.textContent = data.status.status;
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
if (dashboardPackagesStatusesChart) {
const labels = [
"unknown",
"pending",
"building",
"failed",
"success",
];
dashboardPackagesStatusesChart.config.data = {
labels: labels,
datasets: [{
label: "packages in status",
data: labels.map(label => data.packages[label]),
backgroundColor: [
"rgb(55, 58, 60)",
"rgb(255, 117, 24)",
"rgb(255, 117, 24)",
"rgb(255, 0, 57)",
"rgb(63, 182, 24)", // copy-paste from current style
],
}],
};
dashboardPackagesStatusesChart.update();
}
if (dashboardPackagesCountChart) {
dashboardPackagesCountChart.config.data = {
labels: ["packages"],
datasets: [
{
label: "archives",
data: [data.stats.packages],
},
{
label: "bases",
data: [data.stats.bases],
},
],
};
dashboardPackagesCountChart.update();
}
dashboardCanvas.hidden = data.status.total > 0;
},
);
}
ready(_ => {
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
type: "pie",

View File

@ -148,19 +148,8 @@
packageAddInput.addEventListener("keyup", _ => {
clearTimeout(packageAddInput.requestTimeout);
// do not update datalist if search string didn't change yet
const value = packageAddInput.value;
const previousValue = packageAddInput.dataset.previousValue;
if (value === previousValue) {
return;
}
// store current search string in attributes
packageAddInput.dataset.previousValue = value;
// perform data list update
packageAddInput.requestTimeout = setTimeout(_ => {
const value = packageAddInput.value;
if (value.length >= 3) {
makeRequest(

View File

@ -80,7 +80,8 @@
data-classes="table table-hover"
data-sortable="true"
data-sort-name="timestamp"
data-sort-order="desc">
data-sort-order="desc"
data-toggle="table">
<thead class="table-primary">
<tr>
<th data-align="right" data-field="timestamp">date</th>
@ -97,24 +98,10 @@
<input id="package-info-refresh-input" type="checkbox" class="form-check-input" value="" checked>
<label for="package-info-refresh-input" class="form-check-label">update pacman databases</label>
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
{% endif %}
{% if autorefresh_intervals %}
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
<div class="btn-group dropup">
<input id="package-info-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="togglePackageInfoAutoReload()" checked>
<label for="package-info-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">select interval</span>
</button>
<ul id="package-info-autoreload-input" class="dropdown-menu">
{% for interval in autorefresh_intervals %}
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="togglePackageInfoAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
</div>
</div>
@ -153,10 +140,6 @@
const packageInfoRefreshInput = document.getElementById("package-info-refresh-input");
const packageInfoAutoReloadButton = document.getElementById("package-info-autoreload-button");
const packageInfoAutoReloadInput = document.getElementById("package-info-autoreload-input");
let packageInfoAutoReloadTask = null;
function clearChart() {
packageInfoEventsUpdateChartCanvas.hidden = true;
if (packageInfoEventsUpdateChart) {
@ -165,13 +148,6 @@
}
}
function convertLogs(data, filter) {
return data
.filter((filter || Boolean))
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`)
.join("\n");
}
async function copyChanges() {
const changes = packageInfoChangesInput.textContent;
await copyToClipboard(changes, packageInfoChangesCopyButton);
@ -315,69 +291,6 @@
}
function loadLogs(packageBase, onFailure) {
const sortFn = (left, right) => left.process_id.localeCompare(right.process_id) || left.version.localeCompare(right.version);
const compareFn = (left, right) => left.process_id === right.process_id && left.version === right.version;
makeRequest(
`/api/v2/packages/${packageBase}/logs`,
{
query: {
architecture: repository.architecture,
head: true,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
const currentVersions = Array.from(packageInfoLogsVersions.children)
.map(el => {
return {
process_id: el.dataset.processId,
version: el.dataset.version,
};
})
.sort(sortFn);
const newVersions = data
.map(el => {
return {
process_id: el.process_id,
version: el.version,
};
})
.sort(sortFn);
if (currentVersions.equals(newVersions, compareFn))
loadLogsActive(packageBase);
else
loadLogsAll(packageBase, onFailure);
},
)
}
function loadLogsActive(packageBase) {
const activeLogSelector = packageInfoLogsVersions.querySelector(".active");
if (activeLogSelector) {
makeRequest(
`/api/v2/packages/${packageBase}/logs`,
{
query: {
architecture: repository.architecture,
repository: repository.repository,
version: activeLogSelector.dataset.version,
process_id: activeLogSelector.dataset.processId,
},
convert: response => response.json(),
},
data => {
activeLogSelector.dataset.logs = convertLogs(data);
activeLogSelector.click();
},
);
}
}
function loadLogsAll(packageBase, onFailure) {
makeRequest(
`/api/v2/packages/${packageBase}/logs`,
{
@ -406,19 +319,15 @@
const link = document.createElement("a");
link.classList.add("dropdown-item");
link.dataset.version = version.version;
link.dataset.processId = version.process_id;
link.dataset.logs = convertLogs(data, log_record => log_record.version === version.version && log_record.process_id === version.process_id);
link.textContent = new Date(1000 * version.created).toISOStringShort();
link.href = "#";
link.onclick = _ => {
// check if we are at the bottom of the code block
const isScrolledToBottom = packageInfoLogsInput.scrollTop + packageInfoLogsInput.clientHeight >= packageInfoLogsInput.scrollHeight;
packageInfoLogsInput.textContent = link.dataset.logs;
const logs = data
.filter(log_record => log_record.version === version.version && log_record.process_id === version.process_id)
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`);
packageInfoLogsInput.textContent = logs.join("\n");
highlight(packageInfoLogsInput);
if (isScrolledToBottom)
packageInfoLogsInput.scrollTop = packageInfoLogsInput.scrollHeight; // scroll to the new end
Array.from(packageInfoLogsVersions.children).forEach(el => el.classList.remove("active"));
link.classList.add("active");
@ -494,12 +403,12 @@
}
function packageInfoRemove() {
const packageBase = packageInfoModal.dataset.package;
const packageBase = packageInfoModal.package;
packagesRemove([packageBase]);
}
function packageInfoUpdate() {
const packageBase = packageInfoModal.dataset.package;
const packageBase = packageInfoModal.package;
packagesAdd(packageBase, [], repository, {refresh: packageInfoRefreshInput.checked});
}
@ -507,10 +416,10 @@
const isPackageBaseSet = packageBase !== undefined;
if (isPackageBaseSet) {
// set package base as currently used
packageInfoModal.dataset.package = packageBase;
packageInfoModal.package = packageBase;
} else {
// read package base from the current window attribute
packageBase = packageInfoModal.dataset.package;
packageBase = packageInfoModal.package;
}
const onFailure = error => {
@ -529,27 +438,10 @@
if (isPackageBaseSet) {
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
{% if autorefresh_intervals %}
togglePackageInfoAutoReload();
{% endif %}
}
}
function togglePackageInfoAutoReload(interval) {
clearInterval(packageInfoAutoReloadTask);
packageInfoAutoReloadTask = toggleAutoReload(packageInfoAutoReloadButton, interval, packageInfoAutoReloadInput, _ => {
if (!hasActiveSelection()) {
const packageBase = packageInfoModal.dataset.package;
// we only poll status and logs here
loadPackage(packageBase);
loadLogs(packageBase);
}
});
}
ready(_ => {
packageInfoEventsTable.bootstrapTable({});
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
type: "line",
data: {},
@ -576,11 +468,6 @@
packageInfoChangesInput.textContent = "";
packageInfoEventsTable.bootstrapTable("load", []);
clearChart();
clearInterval(packageInfoAutoReloadTask);
packageInfoAutoReloadTask = null; // not really required (?) but lets clear everything
});
restoreAutoReloadSettings(packageInfoAutoReloadButton, packageInfoAutoReloadInput);
});
</script>

View File

@ -10,10 +10,6 @@
const dashboardButton = document.getElementById("dashboard-button");
const versionBadge = document.getElementById("badge-version");
const tableAutoReloadButton = document.getElementById("table-autoreload-button");
const tableAutoReloadInput = document.getElementById("table-autoreload-input");
let tableAutoReloadTask = null;
function doPackageAction(uri, packages, repository, successText, failureText, data) {
makeRequest(
uri,
@ -59,41 +55,6 @@
return table.bootstrapTable("getSelections").map(row => row.id);
}
function packagesLoad(onFailure) {
makeRequest(
"/api/v1/packages",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
const payload = data
.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote.web_url;
return {
id: package_base,
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")),
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
status: description.status.status,
};
});
updateTable(table, payload);
table.bootstrapTable("hideLoading");
},
onFailure,
);
}
function packagesRemove(packages) {
packages = packages ?? getSelection();
const onSuccess = update => `Packages ${update} have been removed`;
@ -127,22 +88,130 @@
function reload() {
table.bootstrapTable("showLoading");
const onFailure = error => {
if ((error.status === 401) || (error.status === 403)) {
// authorization error
const text = "In order to see statuses you must login first.";
table.find("tr.unauthorized").remove();
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
table.bootstrapTable("hideLoading");
} else {
// other errors
const message = details => `Could not load list of packages: ${details}`;
showFailure("Load failure", message, error);
}
const badgeClass = status => {
if (status === "pending") return "btn-outline-warning";
if (status === "building") return "btn-outline-warning";
if (status === "failed") return "btn-outline-danger";
if (status === "success") return "btn-outline-success";
return "btn-outline-secondary";
};
packagesLoad(onFailure);
statusLoad();
makeRequest(
"/api/v1/packages",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
const payload = data.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote.web_url;
return {
id: package_base,
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")),
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
status: description.status.status,
};
});
table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
},
error => {
if ((error.status === 401) || (error.status === 403)) {
// authorization error
const text = "In order to see statuses you must login first.";
table.find("tr.unauthorized").remove();
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
table.bootstrapTable("hideLoading");
} else {
// other errors
const message = details => `Could not load list of packages: ${details}`;
showFailure("Load failure", message, error);
}
},
);
makeRequest(
"/api/v1/status",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
dashboardButton.classList.remove(...dashboardButton.classList);
dashboardButton.classList.add("btn");
dashboardButton.classList.add(badgeClass(data.status.status));
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
dashboardModalHeader.classList.add("modal-header");
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
dashboardName.textContent = data.repository;
dashboardArchitecture.textContent = data.architecture;
dashboardStatus.textContent = data.status.status;
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
if (dashboardPackagesStatusesChart) {
const labels = [
"unknown",
"pending",
"building",
"failed",
"success",
];
dashboardPackagesStatusesChart.config.data = {
labels: labels,
datasets: [{
label: "packages in status",
data: labels.map(label => data.packages[label]),
backgroundColor: [
"rgb(55, 58, 60)",
"rgb(255, 117, 24)",
"rgb(255, 117, 24)",
"rgb(255, 0, 57)",
"rgb(63, 182, 24)", // copy-paste from current style
],
}],
};
dashboardPackagesStatusesChart.update();
}
if (dashboardPackagesCountChart) {
dashboardPackagesCountChart.config.data = {
labels: ["packages"],
datasets: [
{
label: "archives",
data: [data.stats.packages],
},
{
label: "bases",
data: [data.stats.bases],
},
],
};
dashboardPackagesCountChart.update();
}
dashboardCanvas.hidden = data.status.total > 0;
},
);
}
function selectRepository() {
@ -161,24 +230,7 @@
return {classes: cellClass(value)};
}
function toggleTableAutoReload(interval) {
clearInterval(tableAutoReloadTask);
tableAutoReloadTask = toggleAutoReload(tableAutoReloadButton, interval, tableAutoReloadInput, _ => {
if (!hasActiveModal() &&
!hasActiveDropdown()) {
packagesLoad();
statusLoad();
}
});
}
ready(_ => {
const onCheckFunction = function () {
if (packageRemoveButton) {
packageRemoveButton.disabled = !getSelection().length;
}
};
document.querySelectorAll("#repositories a").forEach(element => {
element.onclick = _ => {
repository = {
@ -193,55 +245,49 @@
};
});
table.bootstrapTable({
onCheck: onCheckFunction,
onCheckAll: onCheckFunction,
onClickRow: (data, row, cell) => {
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showPackageInfo(data.id);
},
onCreatedControls: _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
},
onUncheck: onCheckFunction,
onUncheckAll: onCheckFunction,
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => {
if (packageRemoveButton) {
packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length;
}
});
table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showPackageInfo(data.id);
});
table.on("created-controls.bs.table", _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
});
restoreAutoReloadSettings(tableAutoReloadButton, tableAutoReloadInput);
selectRepository();
{% if autorefresh_intervals %}
toggleTableAutoReload();
{% endif %}
});
</script>

View File

@ -53,7 +53,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
data-show-search-clear-button="true"
data-sortable="true"
data-sort-name="base"
data-sort-order="asc">
data-sort-order="asc"
data-toggle="table">
<thead class="table-primary">
<tr>
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
@ -127,38 +128,36 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
}
ready(_ => {
table.bootstrapTable({
onCreatedControls: _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
},
table.on("created-controls.bs.table", _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
});
});
</script>

View File

@ -1,24 +1,23 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.33.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.30.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/cookie/bootstrap-table-cookie.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script>
async function copyToClipboard(text, button) {
@ -59,20 +58,6 @@
return value.includes(dataList[index].toLowerCase());
}
function hasActiveSelection() {
return !document.getSelection().isCollapsed; // not sure if it is a valid way, but I guess so
}
function hasActiveDropdown() {
return Array.from(document.querySelectorAll(".dropdown-menu"))
.some(el => el.classList.contains("show"));
}
function hasActiveModal() {
return Array.from(document.querySelectorAll(".modal"))
.some(el => el.classList.contains("show"));
}
function headerClass(status) {
if (status === "pending") return ["bg-warning"];
if (status === "building") return ["bg-warning"];
@ -121,12 +106,6 @@
.catch(error => onFailure && onFailure(error));
}
function readOptional(extractor, callback) {
for (let value = extractor(); !!value; value = null) {
callback(value);
}
}
function ready(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 1);
@ -135,11 +114,6 @@
}
}
function restoreAutoReloadSettings(toggle, intervalSelector) {
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-enabled`), value => toggle.checked = value === "true");
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-interval`), value => toggleActiveElement(intervalSelector, "interval", value));
}
function safe(string) {
return String(string)
.replace(/&/g, "&amp;")
@ -159,86 +133,7 @@
return element;
}
function toggleActiveElement(selector, dataType, value) {
const targetElement = selector.querySelector(`a[data-${dataType}="${value}"]`);
if (targetElement?.classList?.contains("active")) {
return; // element is already active, skip processing
}
Array.from(selector.children).forEach(il => {
Array.from(il.children).forEach(el => el.classList.remove("active"));
});
targetElement?.classList?.add("active");
}
function toggleAutoReload(toggle, interval, intervalSelector, callback) {
if (interval) {
toggle.checked = true; // toggle reload
} else {
interval = intervalSelector.querySelector(".active")?.dataset?.interval; // find active element
}
let intervalId = null;
if (interval) {
if (toggle.checked) {
// refresh UI
toggleActiveElement(intervalSelector, "interval", interval);
// finally create timer task
intervalId = setInterval(callback, interval);
}
} else {
toggle.checked = false; // no active interval found, disable toggle
}
localStorage.setItem(`ahriman-${toggle.id}-refresh-enabled`, toggle.checked);
localStorage.setItem(`ahriman-${toggle.id}-refresh-interval`, interval);
return intervalId;
}
function updateTable(table, rows) {
// instead of using load method here, we just update rows manually to avoid table reinitialization
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
accumulator[row.id] = row["0"];
return accumulator;
}, {});
// insert or update rows
rows.forEach(row => {
if (Object.hasOwn(currentData, row.id)) {
row["0"] = currentData[row.id]; // copy checkbox state
table.bootstrapTable("updateByUniqueId", {
id: row.id,
row: row,
replace: true,
});
} else {
table.bootstrapTable("insertRow", {index: 0, row: row});
}
});
// remove old rows
const newData = rows.map(value => value.id);
Object.keys(currentData).forEach(id => {
if (!newData.includes(id)) {
table.bootstrapTable("removeByUniqueId", id);
}
});
}
Array.prototype.equals = function (right, comparator) {
let index = this.length;
if (index !== right.length) {
return false;
}
while (index--) {
if (!comparator(this[index], right[index])) {
return false;
}
}
return true;
}
Date.prototype.toISOStringShort = function () {
Date.prototype.toISOStringShort = function() {
const pad = number => String(number).padStart(2, "0");
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
}

View File

@ -1,15 +1,15 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.7/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
<style>
.pre-scrollable {

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2025\-06\-29" "ahriman 2.19.0" "ArcH linux ReposItory MANager"
.TH AHRIMAN "1" "2025\-07\-14" "ahriman 2.19.1" "ArcH linux ReposItory MANager"
.SH NAME
ahriman \- ArcH linux ReposItory MANager
.SH SYNOPSIS

View File

@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
@ -62,7 +62,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_REPOSITORY_SERVER: http://frontend/repo/$$repo/$$arch

View File

@ -12,7 +12,7 @@ services:
AHRIMAN_PACMAN_MIRROR: https://de.mirror.archlinux32.org/$$arch/$$repo
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@ -8,8 +8,8 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: ahriman --architecture x86_64 --repository another-demo service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'
AHRIMAN_POSTSETUP_COMMAND: ahriman --architecture x86_64 --repository another-demo service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@ -9,7 +9,7 @@ services:
AHRIMAN_OAUTH_CLIENT_SECRET: ${AHRIMAN_OAUTH_CLIENT_SECRET}
AHRIMAN_OUTPUT: console
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: sudo -u ahriman ahriman user-add ${AHRIMAN_OAUTH_USER} -R full -p ""
AHRIMAN_PRESETUP_COMMAND: sudo -u ahriman ahriman user-add ${AHRIMAN_OAUTH_USER} -R full -p ""
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@ -6,7 +6,7 @@ services:
environment:
AHRIMAN_DEBUG: yes
AHRIMAN_OUTPUT: console
AHRIMAN_POSTSETUP_COMMAND: sudo -u ahriman gpg --import /run/secrets/key
AHRIMAN_PRESETUP_COMMAND: sudo -u ahriman gpg --import /run/secrets/key
AHRIMAN_REPOSITORY: ahriman-demo
configs:

View File

@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "2.19.0"
__version__ = "2.19.1"

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,70 +0,0 @@
#
# Copyright (c) 2021-2026 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.models.repository_id import RepositoryId
class Reload(Handler):
"""
web server reload handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=True)
client = application.repository.reporter
client.configuration_reload()
@staticmethod
def _set_web_reload_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web reload subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("web-reload", help="reload configuration",
description="reload web server configuration",
epilog="This method forces the web server to reload its configuration. "
"Note, however, that this method does not apply all configuration changes "
"(like ports, authentication, etc)")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return parser
arguments = [_set_web_reload_parser]

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -28,7 +28,6 @@ from ahriman.core.alpm.remote import AUR, Official
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import OptionError
from ahriman.core.formatters import AurPrinter
from ahriman.core.types import Comparable
from ahriman.models.aur_package import AURPackage
from ahriman.models.repository_id import RepositoryId
@ -116,7 +115,7 @@ class Search(Handler):
raise OptionError(sort_by)
# always sort by package name at the last
# well technically it is not a string, but we can deal with it
comparator: Callable[[AURPackage], Comparable] = \
comparator: Callable[[AURPackage], tuple[str, str]] =\
lambda package: (getattr(package, sort_by), package.name)
return sorted(packages, key=comparator)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -72,17 +72,16 @@ class Setup(Handler):
application = Application(repository_id, configuration, report=report)
with application.repository.paths.preserve_owner():
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, repository_id)
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, repository_id)
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
@staticmethod
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
@ -281,5 +280,6 @@ class Setup(Handler):
command = Setup.build_command(paths.root, repository_id)
command.unlink(missing_ok=True)
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
paths.chown(command) # we would like to keep owner inside ahriman's home
arguments = [_set_service_setup_parser]

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -25,7 +25,6 @@ from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import PackagePrinter, StatusPrinter
from ahriman.core.types import Comparable
from ahriman.core.utils import enum_values
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
@ -65,7 +64,7 @@ class Status(Handler):
Status.check_status(args.exit_code, packages)
comparator: Callable[[tuple[Package, BuildStatus]], Comparable] = lambda item: item[0].base
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda item: item[0].base
filter_fn: Callable[[tuple[Package, BuildStatus]], bool] =\
lambda item: args.status is None or item[1].status == args.status
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -52,7 +52,7 @@ class Validate(Handler):
"""
from ahriman.core.configuration.validator import Validator
schema = Validate.schema(configuration)
schema = Validate.schema(repository_id, configuration)
validator = Validator(configuration=configuration, schema=schema)
if validator.validate(configuration.dump()):
@ -83,11 +83,12 @@ class Validate(Handler):
return parser
@staticmethod
def schema(configuration: Configuration) -> ConfigurationSchema:
def schema(repository_id: RepositoryId, configuration: Configuration) -> ConfigurationSchema:
"""
get schema with triggers
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
Returns:
@ -106,12 +107,12 @@ class Validate(Handler):
continue
# default settings if any
for schema_name, schema in trigger_class.configuration_schema(None).items():
for schema_name, schema in trigger_class.configuration_schema(repository_id, None).items():
erased = Validate.schema_erase_required(copy.deepcopy(schema))
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), erased)
# settings according to enabled triggers
for schema_name, schema in trigger_class.configuration_schema(configuration).items():
for schema_name, schema in trigger_class.configuration_schema(repository_id, configuration).items():
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), copy.deepcopy(schema))
return root

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -130,8 +130,8 @@ class Pacman(LazyLogging):
return # database for some reason deos not exist
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
with self.repository_paths.preserve_owner(dst.parent):
shutil.copy(src, dst)
shutil.copy(src, dst)
self.repository_paths.chown(dst)
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
"""

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -94,15 +94,6 @@ class Remote(SyncHttpClient):
for package in portion
if package.name in packages or not packages
}
# simple check for duplicates. This method will remove all packages under base if there is
# a package named exactly as its base
packages = {
package.name: package
for package in packages.values()
if package.package_base not in packages or package.package_base == package.name
}
return list(packages.values())
@classmethod

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2026 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

Some files were not shown because too many files have changed in this diff Show More