mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-03-16 14:53:38 +00:00
Compare commits
16 Commits
2.20.0rc7
...
feature/ro
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c10b06e3c | |||
| a04b6c3b9c | |||
| 2e9837b70d | |||
| ac4a8fb2cd | |||
| 1db8eb0ac4 | |||
| dc394f7df9 | |||
| 058f784b05 | |||
| f688768ca7 | |||
| a09ad7617d | |||
| 81aeb56ba3 | |||
| 2cd4ef5e86 | |||
| 998ed48dde | |||
| 021d88dc4c | |||
| 9012ee7144 | |||
| 945ddb2942 | |||
| 9cd0926588 |
@@ -12,6 +12,14 @@ ahriman.application.handlers.add module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.archives module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.archives
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.backup module
|
ahriman.application.handlers.backup module
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
@@ -76,6 +84,14 @@ ahriman.application.handlers.help module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.hold module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.key\_import module
|
ahriman.application.handlers.key\_import module
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
@@ -92,6 +108,14 @@ ahriman.application.handlers.patch module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.pkgbuild module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.pkgbuild
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.rebuild module
|
ahriman.application.handlers.rebuild module
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ ahriman.core.alpm.pacman\_database module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.alpm.pacman\_handle module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.alpm.pacman_handle
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.alpm.pkgbuild\_parser module
|
ahriman.core.alpm.pkgbuild\_parser module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,22 @@ ahriman.core.database.migrations.m016\_archive module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m017\_pkgbuild module
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m017_pkgbuild
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m018\_package\_hold module
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m018_package_hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ ahriman.core.formatters.patch\_printer module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.formatters.pkgbuild\_printer module
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.formatters.pkgbuild_printer
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.formatters.printer module
|
ahriman.core.formatters.printer module
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ ahriman.web.schemas.file\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.hold\_schema module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.hold_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.info\_schema module
|
ahriman.web.schemas.info\_schema module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ ahriman.web.views.v1.packages package
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
ahriman.web.views.v1.packages.archives module
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.packages.archives
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.v1.packages.changes module
|
ahriman.web.views.v1.packages.changes module
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
|
|
||||||
@@ -20,6 +28,14 @@ ahriman.web.views.v1.packages.dependencies module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.views.v1.packages.hold module
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.packages.hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.v1.packages.logs module
|
ahriman.web.views.v1.packages.logs module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -425,6 +425,15 @@ The PKGBUILD class also provides some additional functions on top of that:
|
|||||||
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
||||||
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
||||||
|
|
||||||
|
HTTP client
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``ahriman.core.http`` package provides a HTTP client built on top of the ``requests`` library.
|
||||||
|
|
||||||
|
The base class ``ahriman.core.http.SyncHttpClient`` wraps ``requests.Session`` and provides common features for all HTTP interactions: configurable timeouts, retry policies with exponential backoff (using ``urllib3.util.retry.Retry``), basic authentication, custom User-Agent header, error processing, and ``make_request`` method. The session is lazily created (via ``cached_property``).
|
||||||
|
|
||||||
|
On top of that, ``ahriman.core.http.SyncAhrimanClient`` extends the base client for communication with the ahriman web service specifically. It adds automatic login on session creation (using configured credentials), ``X-Request-ID`` header injection and Unix socket transport support (via ``requests-unixsocket2``) if required.
|
||||||
|
|
||||||
Additional features
|
Additional features
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ahriman-frontend",
|
"name": "ahriman-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.20.0-rc7",
|
"version": "2.20.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -29,14 +29,13 @@
|
|||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.56.1",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.0"
|
||||||
"vite-tsconfig-paths": "^6.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,14 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import AppLayout from "components/layout/AppLayout";
|
import AppLayout from "components/layout/AppLayout";
|
||||||
import { AuthProvider } from "contexts/AuthProvider";
|
import { AuthProvider } from "contexts/AuthProvider";
|
||||||
import { ClientProvider } from "contexts/ClientProvider";
|
import { ClientProvider } from "contexts/ClientProvider";
|
||||||
import { NotificationProvider } from "contexts/NotificationProvider";
|
import { NotificationProvider } from "contexts/NotificationProvider";
|
||||||
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
||||||
|
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import Theme from "theme/Theme";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -39,8 +37,7 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
export default function App(): React.JSX.Element {
|
export default function App(): React.JSX.Element {
|
||||||
return <QueryClientProvider client={queryClient}>
|
return <QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={Theme}>
|
<ThemeProvider>
|
||||||
<CssBaseline />
|
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ClientProvider>
|
<ClientProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class Client {
|
|||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"X-Request-ID": crypto.randomUUID(),
|
"X-Request-ID": crypto.randomUUID?.() ?? Date.now().toString(),
|
||||||
};
|
};
|
||||||
if (json !== undefined) {
|
if (json !== undefined) {
|
||||||
headers["Content-Type"] = "application/json";
|
headers["Content-Type"] = "application/json";
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ export class ServiceClient {
|
|||||||
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
|
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async servicePackageHoldUpdate(packageBase: string, repository: RepositoryId, isHeld: boolean): Promise<void> {
|
||||||
|
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/hold`, {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: { is_held: isHeld },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
|
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
|
||||||
return this.client.request("/api/v1/service/rebuild", {
|
return this.client.request("/api/v1/service/rebuild", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import { blue } from "@mui/material/colors";
|
||||||
import type { Event } from "models/Event";
|
import type { Event } from "models/Event";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
@@ -33,6 +34,8 @@ export default function EventDurationLineChart({ events }: EventDurationLineChar
|
|||||||
{
|
{
|
||||||
label: "update duration, s",
|
label: "update duration, s",
|
||||||
data: updateEvents.map(event => event.data?.took ?? 0),
|
data: updateEvents.map(event => event.data?.took ?? 0),
|
||||||
|
borderColor: blue[500],
|
||||||
|
backgroundColor: blue[200],
|
||||||
cubicInterpolationMode: "monotone" as const,
|
cubicInterpolationMode: "monotone" as const,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function AutoRefreshControl({
|
|||||||
<Tooltip title="Auto-refresh">
|
<Tooltip title="Auto-refresh">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-label="Auto-refresh"
|
||||||
onClick={event => setAnchorEl(event.currentTarget)}
|
onClick={event => setAnchorEl(event.currentTarget)}
|
||||||
color={enabled ? "primary" : "default"}
|
color={enabled ? "primary" : "default"}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,47 +17,57 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box } from "@mui/material";
|
import "components/common/syntaxLanguages";
|
||||||
|
|
||||||
|
import { Box, useTheme } from "@mui/material";
|
||||||
import CopyButton from "components/common/CopyButton";
|
import CopyButton from "components/common/CopyButton";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import React, { type RefObject } from "react";
|
import React, { type RefObject } from "react";
|
||||||
|
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { githubGist, vs2015 } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
preRef?: RefObject<HTMLElement | null>;
|
content: string;
|
||||||
getText: () => string;
|
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
language?: string;
|
||||||
onScroll?: () => void;
|
onScroll?: () => void;
|
||||||
wordBreak?: boolean;
|
preRef?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CodeBlock({
|
export default function CodeBlock({
|
||||||
preRef,
|
content,
|
||||||
getText,
|
|
||||||
height,
|
height,
|
||||||
|
language = "text",
|
||||||
onScroll,
|
onScroll,
|
||||||
wordBreak,
|
preRef,
|
||||||
}: CodeBlockProps): React.JSX.Element {
|
}: CodeBlockProps): React.JSX.Element {
|
||||||
|
const { mode } = useThemeMode();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return <Box sx={{ position: "relative" }}>
|
return <Box sx={{ position: "relative" }}>
|
||||||
<Box
|
<Box
|
||||||
ref={preRef}
|
ref={preRef}
|
||||||
component="pre"
|
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
sx={{
|
sx={{ overflow: "auto", height }}
|
||||||
backgroundColor: "grey.100",
|
|
||||||
p: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
overflow: "auto",
|
|
||||||
height,
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<code>
|
<SyntaxHighlighter
|
||||||
{getText()}
|
language={language}
|
||||||
</code>
|
style={mode === "dark" ? vs2015 : githubGist}
|
||||||
</Box>
|
wrapLongLines
|
||||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
customStyle={{
|
||||||
<CopyButton getText={getText} />
|
padding: theme.spacing(2),
|
||||||
|
borderRadius: `${theme.shape.borderRadius}px`,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
margin: 0,
|
||||||
|
minHeight: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</SyntaxHighlighter>
|
||||||
</Box>
|
</Box>
|
||||||
|
{content && <Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||||
|
<CopyButton text={content} />
|
||||||
|
</Box>}
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,17 +23,17 @@ import { IconButton, Tooltip } from "@mui/material";
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
getText: () => string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyButton({ getText }: CopyButtonProps): React.JSX.Element {
|
export default function CopyButton({ text }: CopyButtonProps): React.JSX.Element {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
useEffect(() => () => clearTimeout(timer.current), []);
|
useEffect(() => () => clearTimeout(timer.current), []);
|
||||||
|
|
||||||
const handleCopy: () => Promise<void> = async () => {
|
const handleCopy: () => Promise<void> = async () => {
|
||||||
await navigator.clipboard.writeText(getText());
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
clearTimeout(timer.current);
|
clearTimeout(timer.current);
|
||||||
timer.current = setTimeout(() => setCopied(false), 2000);
|
timer.current = setTimeout(() => setCopied(false), 2000);
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ import type React from "react";
|
|||||||
export default function RepositorySelect({
|
export default function RepositorySelect({
|
||||||
repositorySelect,
|
repositorySelect,
|
||||||
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
|
|
||||||
return <FormControl fullWidth margin="normal">
|
return <FormControl fullWidth margin="normal">
|
||||||
<InputLabel>repository</InputLabel>
|
<InputLabel>repository</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={repositorySelect.selectedKey || (current?.key ?? "")}
|
value={repositorySelect.selectedKey || (currentRepository?.key ?? "")}
|
||||||
label="repository"
|
label="repository"
|
||||||
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
||||||
>
|
>
|
||||||
|
|||||||
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash";
|
||||||
|
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
||||||
|
import plaintext from "react-syntax-highlighter/dist/esm/languages/hljs/plaintext";
|
||||||
|
|
||||||
|
SyntaxHighlighter.registerLanguage("bash", bash);
|
||||||
|
SyntaxHighlighter.registerLanguage("diff", diff);
|
||||||
|
SyntaxHighlighter.registerLanguage("text", plaintext);
|
||||||
@@ -36,11 +36,11 @@ interface DashboardDialogProps {
|
|||||||
|
|
||||||
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
|
|
||||||
const { data: status } = useQuery<InternalStatus>({
|
const { data: status } = useQuery<InternalStatus>({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
|||||||
/>
|
/>
|
||||||
{keyBody &&
|
{keyBody &&
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
<CodeBlock getText={() => keyBody} height={300} />
|
<CodeBlock content={keyBody} height={300} />
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import EventsTab from "components/package/EventsTab";
|
|||||||
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
||||||
import PackageInfoActions from "components/package/PackageInfoActions";
|
import PackageInfoActions from "components/package/PackageInfoActions";
|
||||||
import PackagePatchesList from "components/package/PackagePatchesList";
|
import PackagePatchesList from "components/package/PackagePatchesList";
|
||||||
|
import PkgbuildTab from "components/package/PkgbuildTab";
|
||||||
|
import { type TabKey, tabs } from "components/package/TabKey";
|
||||||
import { QueryKeys } from "hooks/QueryKeys";
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
||||||
@@ -55,7 +57,7 @@ export default function PackageInfoDialog({
|
|||||||
autoRefreshIntervals,
|
autoRefreshIntervals,
|
||||||
}: PackageInfoDialogProps): React.JSX.Element {
|
}: PackageInfoDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -65,11 +67,11 @@ export default function PackageInfoDialog({
|
|||||||
setLocalPackageBase(packageBase);
|
setLocalPackageBase(packageBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [tabIndex, setTabIndex] = useState(0);
|
const [activeTab, setActiveTab] = useState<TabKey>("logs");
|
||||||
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||||
|
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
setTabIndex(0);
|
setActiveTab("logs");
|
||||||
setRefreshDatabase(true);
|
setRefreshDatabase(true);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -77,16 +79,17 @@ export default function PackageInfoDialog({
|
|||||||
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packageData } = useQuery<PackageStatus[]>({
|
const { data: packageData } = useQuery<PackageStatus[]>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
|
queryKey: localPackageBase && currentRepository ? QueryKeys.package(localPackageBase, currentRepository) : ["packages"],
|
||||||
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
|
queryFn: localPackageBase && currentRepository ?
|
||||||
|
() => client.fetch.fetchPackage(localPackageBase, currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: dependencies } = useQuery<Dependencies>({
|
const { data: dependencies } = useQuery<Dependencies>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
|
queryKey: localPackageBase && currentRepository ? QueryKeys.dependencies(localPackageBase, currentRepository) : ["dependencies"],
|
||||||
queryFn: localPackageBase && current
|
queryFn: localPackageBase && currentRepository ?
|
||||||
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
|
() => client.fetch.fetchPackageDependencies(localPackageBase, currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,11 +105,12 @@ export default function PackageInfoDialog({
|
|||||||
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
||||||
|
|
||||||
const handleUpdate: () => Promise<void> = async () => {
|
const handleUpdate: () => Promise<void> = async () => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageAdd(current, { packages: [localPackageBase], refresh: refreshDatabase });
|
await client.service.servicePackageAdd(
|
||||||
|
currentRepository, { packages: [localPackageBase], refresh: refreshDatabase });
|
||||||
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
||||||
@@ -114,11 +118,11 @@ export default function PackageInfoDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove: () => Promise<void> = async () => {
|
const handleRemove: () => Promise<void> = async () => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageRemove(current, [localPackageBase]);
|
await client.service.servicePackageRemove(currentRepository, [localPackageBase]);
|
||||||
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
@@ -126,6 +130,19 @@ export default function PackageInfoDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleHoldToggle: () => Promise<void> = async () => {
|
||||||
|
if (!localPackageBase || !currentRepository) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const newHeldStatus = !(status?.is_held ?? false);
|
||||||
|
await client.service.servicePackageHoldUpdate(localPackageBase, currentRepository, newHeldStatus);
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.package(localPackageBase, currentRepository) });
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `Could not update hold status: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeletePatch: (key: string) => Promise<void> = async key => {
|
const handleDeletePatch: (key: string) => Promise<void> = async key => {
|
||||||
if (!localPackageBase) {
|
if (!localPackageBase) {
|
||||||
return;
|
return;
|
||||||
@@ -156,25 +173,26 @@ export default function PackageInfoDialog({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
||||||
<Tabs value={tabIndex} onChange={(_, index: number) => setTabIndex(index)}>
|
<Tabs value={activeTab} onChange={(_, tab: TabKey) => setActiveTab(tab)}>
|
||||||
<Tab label="Build logs" />
|
{tabs.map(({ key, label }) => <Tab key={key} value={key} label={label} />)}
|
||||||
<Tab label="Changes" />
|
|
||||||
<Tab label="Events" />
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{tabIndex === 0 && localPackageBase && current &&
|
{activeTab === "logs" && localPackageBase && currentRepository &&
|
||||||
<BuildLogsTab
|
<BuildLogsTab
|
||||||
packageBase={localPackageBase}
|
packageBase={localPackageBase}
|
||||||
repository={current}
|
repository={currentRepository}
|
||||||
refreshInterval={autoRefresh.interval}
|
refreshInterval={autoRefresh.interval}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{tabIndex === 1 && localPackageBase && current &&
|
{activeTab === "changes" && localPackageBase && currentRepository &&
|
||||||
<ChangesTab packageBase={localPackageBase} repository={current} />
|
<ChangesTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
{tabIndex === 2 && localPackageBase && current &&
|
{activeTab === "pkgbuild" && localPackageBase && currentRepository &&
|
||||||
<EventsTab packageBase={localPackageBase} repository={current} />
|
<PkgbuildTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
|
}
|
||||||
|
{activeTab === "events" && localPackageBase && currentRepository &&
|
||||||
|
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -184,6 +202,8 @@ export default function PackageInfoDialog({
|
|||||||
isAuthorized={isAuthorized}
|
isAuthorized={isAuthorized}
|
||||||
refreshDatabase={refreshDatabase}
|
refreshDatabase={refreshDatabase}
|
||||||
onRefreshDatabaseChange={setRefreshDatabase}
|
onRefreshDatabaseChange={setRefreshDatabase}
|
||||||
|
isHeld={status?.is_held ?? false}
|
||||||
|
onHoldToggle={() => void handleHoldToggle()}
|
||||||
onUpdate={() => void handleUpdate()}
|
onUpdate={() => void handleUpdate()}
|
||||||
onRemove={() => void handleRemove()}
|
onRemove={() => void handleRemove()}
|
||||||
autoRefreshIntervals={autoRefreshIntervals}
|
autoRefreshIntervals={autoRefreshIntervals}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box, Container } from "@mui/material";
|
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||||
|
import Brightness7Icon from "@mui/icons-material/Brightness7";
|
||||||
|
import { Box, Container, IconButton, Tooltip } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import LoginDialog from "components/dialogs/LoginDialog";
|
import LoginDialog from "components/dialogs/LoginDialog";
|
||||||
import Footer from "components/layout/Footer";
|
import Footer from "components/layout/Footer";
|
||||||
@@ -27,6 +29,7 @@ import { QueryKeys } from "hooks/QueryKeys";
|
|||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useClient } from "hooks/useClient";
|
import { useClient } from "hooks/useClient";
|
||||||
import { useRepository } from "hooks/useRepository";
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import type { InfoResponse } from "models/InfoResponse";
|
import type { InfoResponse } from "models/InfoResponse";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { setAuthState } = useAuth();
|
const { setAuthState } = useAuth();
|
||||||
const { setRepositories } = useRepository();
|
const { setRepositories } = useRepository();
|
||||||
|
const { mode, toggleTheme } = useThemeMode();
|
||||||
const [loginOpen, setLoginOpen] = useState(false);
|
const [loginOpen, setLoginOpen] = useState(false);
|
||||||
|
|
||||||
const { data: info } = useQuery<InfoResponse>({
|
const { data: info } = useQuery<InfoResponse>({
|
||||||
@@ -58,6 +62,11 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Tooltip title="Toggle theme">
|
||||||
|
<IconButton aria-label="Toggle theme" onClick={toggleTheme}>
|
||||||
|
{mode === "dark" ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<PackageTable
|
<PackageTable
|
||||||
|
|||||||
@@ -22,14 +22,15 @@ import { useRepository } from "hooks/useRepository";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export default function Navbar(): React.JSX.Element | null {
|
export default function Navbar(): React.JSX.Element | null {
|
||||||
const { repositories, current, setCurrent } = useRepository();
|
const { repositories, currentRepository, setCurrentRepository } = useRepository();
|
||||||
|
|
||||||
if (repositories.length === 0 || !current) {
|
if (repositories.length === 0 || !currentRepository) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = repositories.findIndex(repository =>
|
const currentIndex = repositories.findIndex(repository =>
|
||||||
repository.architecture === current.architecture && repository.repository === current.repository,
|
repository.architecture === currentRepository.architecture &&
|
||||||
|
repository.repository === currentRepository.repository,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
@@ -38,7 +39,7 @@ export default function Navbar(): React.JSX.Element | null {
|
|||||||
onChange={(_, newValue: number) => {
|
onChange={(_, newValue: number) => {
|
||||||
const repository = repositories[newValue];
|
const repository = repositories[newValue];
|
||||||
if (repository) {
|
if (repository) {
|
||||||
setCurrent(repository);
|
setCurrentRepository(repository);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
|
|||||||
@@ -175,10 +175,9 @@ export default function BuildLogsTab({
|
|||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
preRef={preRef}
|
preRef={preRef}
|
||||||
getText={() => displayedLogs}
|
content={displayedLogs}
|
||||||
height={400}
|
height={400}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
wordBreak
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|||||||
@@ -17,19 +17,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box } from "@mui/material";
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
import CopyButton from "components/common/CopyButton";
|
|
||||||
import { QueryKeys } from "hooks/QueryKeys";
|
|
||||||
import { useClient } from "hooks/useClient";
|
|
||||||
import type { Changes } from "models/Changes";
|
|
||||||
import type { RepositoryId } from "models/RepositoryId";
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
||||||
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
|
||||||
import { githubGist } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
|
||||||
|
|
||||||
SyntaxHighlighter.registerLanguage("diff", diff);
|
|
||||||
|
|
||||||
interface ChangesTabProps {
|
interface ChangesTabProps {
|
||||||
packageBase: string;
|
packageBase: string;
|
||||||
@@ -37,34 +28,7 @@ interface ChangesTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
const { data } = useQuery<Changes>({
|
return <CodeBlock language="diff" content={data?.changes ?? ""} height={400} />;
|
||||||
queryKey: QueryKeys.changes(packageBase, repository),
|
|
||||||
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
|
||||||
enabled: !!packageBase,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changesText = data?.changes ?? "";
|
|
||||||
|
|
||||||
return <Box sx={{ position: "relative", mt: 1 }}>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
language="diff"
|
|
||||||
style={githubGist}
|
|
||||||
customStyle={{
|
|
||||||
padding: "16px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "auto",
|
|
||||||
height: 400,
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{changesText}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
|
||||||
<CopyButton getText={() => changesText} />
|
|
||||||
</Box>
|
|
||||||
</Box>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import PauseCircleIcon from "@mui/icons-material/PauseCircle";
|
||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import PlayCircleIcon from "@mui/icons-material/PlayCircle";
|
||||||
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
|
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
|
||||||
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
||||||
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
@@ -26,6 +28,8 @@ import type React from "react";
|
|||||||
|
|
||||||
interface PackageInfoActionsProps {
|
interface PackageInfoActionsProps {
|
||||||
isAuthorized: boolean;
|
isAuthorized: boolean;
|
||||||
|
isHeld: boolean;
|
||||||
|
onHoldToggle: () => void;
|
||||||
refreshDatabase: boolean;
|
refreshDatabase: boolean;
|
||||||
onRefreshDatabaseChange: (checked: boolean) => void;
|
onRefreshDatabaseChange: (checked: boolean) => void;
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
@@ -39,6 +43,8 @@ export default function PackageInfoActions({
|
|||||||
isAuthorized,
|
isAuthorized,
|
||||||
refreshDatabase,
|
refreshDatabase,
|
||||||
onRefreshDatabaseChange,
|
onRefreshDatabaseChange,
|
||||||
|
isHeld,
|
||||||
|
onHoldToggle,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onRemove,
|
onRemove,
|
||||||
autoRefreshIntervals,
|
autoRefreshIntervals,
|
||||||
@@ -52,6 +58,9 @@ export default function PackageInfoActions({
|
|||||||
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
|
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
|
||||||
label="update pacman databases"
|
label="update pacman databases"
|
||||||
/>
|
/>
|
||||||
|
<Button onClick={onHoldToggle} variant="outlined" color="warning" startIcon={isHeld ? <PlayCircleIcon /> : <PauseCircleIcon />} size="small">
|
||||||
|
{isHeld ? "unhold" : "hold"}
|
||||||
|
</Button>
|
||||||
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
|
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
|
||||||
update
|
update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function PackagePatchesList({
|
|||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
{editable &&
|
{editable &&
|
||||||
<IconButton size="small" color="error" onClick={() => onDelete(patch.key)}>
|
<IconButton size="small" color="error" aria-label="Remove patch" onClick={() => onDelete(patch.key)}>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
|
|||||||
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 CodeBlock from "components/common/CodeBlock";
|
||||||
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PkgbuildTabProps {
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PkgbuildTab({ packageBase, repository }: PkgbuildTabProps): React.JSX.Element {
|
||||||
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
|
return <CodeBlock language="bash" content={data?.pkgbuild ?? ""} height={400} />;
|
||||||
|
}
|
||||||
27
frontend/src/components/package/TabKey.ts
Normal file
27
frontend/src/components/package/TabKey.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
export type TabKey = "logs" | "changes" | "pkgbuild" | "events";
|
||||||
|
|
||||||
|
export const tabs: { key: TabKey; label: string }[] = [
|
||||||
|
{ key: "logs", label: "Build logs" },
|
||||||
|
{ key: "changes", label: "Changes" },
|
||||||
|
{ key: "pkgbuild", label: "PKGBUILD" },
|
||||||
|
{ key: "events", label: "Events" },
|
||||||
|
];
|
||||||
@@ -107,7 +107,8 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
|||||||
width: 120,
|
width: 120,
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
renderCell: (params: GridRenderCellParams<PackageRow>) => <StatusCell status={params.row.status} />,
|
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
||||||
|
<StatusCell status={params.row.status} isHeld={params.row.isHeld} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import PauseCircleIcon from "@mui/icons-material/PauseCircle";
|
||||||
import { Chip } from "@mui/material";
|
import { Chip } from "@mui/material";
|
||||||
import type { BuildStatus } from "models/BuildStatus";
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
@@ -24,10 +25,12 @@ import { StatusColors } from "theme/StatusColors";
|
|||||||
|
|
||||||
interface StatusCellProps {
|
interface StatusCellProps {
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
isHeld?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatusCell({ status }: StatusCellProps): React.JSX.Element {
|
export default function StatusCell({ status, isHeld }: StatusCellProps): React.JSX.Element {
|
||||||
return <Chip
|
return <Chip
|
||||||
|
icon={isHeld ? <PauseCircleIcon /> : undefined}
|
||||||
label={status}
|
label={status}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import { createContext } from "react";
|
|||||||
|
|
||||||
export interface RepositoryContextValue {
|
export interface RepositoryContextValue {
|
||||||
repositories: RepositoryId[];
|
repositories: RepositoryId[];
|
||||||
current: RepositoryId | null;
|
currentRepository: RepositoryId | null;
|
||||||
setRepositories: (repositories: RepositoryId[]) => void;
|
setRepositories: (repositories: RepositoryId[]) => void;
|
||||||
setCurrent: (repository: RepositoryId) => void;
|
setCurrentRepository: (repository: RepositoryId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ export function RepositoryProvider({ children }: { children: ReactNode }): React
|
|||||||
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
||||||
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
||||||
|
|
||||||
const current = useMemo(() => {
|
const currentRepository = useMemo(() => {
|
||||||
if (repositories.length === 0) {
|
if (repositories.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
||||||
}, [repositories, hash]);
|
}, [repositories, hash]);
|
||||||
|
|
||||||
const setCurrent = useCallback((repository: RepositoryId) => {
|
const setCurrentRepository = useCallback((repository: RepositoryId) => {
|
||||||
window.location.hash = repository.key;
|
window.location.hash = repository.key;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(() => ({
|
||||||
repositories, current, setRepositories, setCurrent,
|
repositories, currentRepository, setRepositories, setCurrentRepository,
|
||||||
}), [repositories, current, setCurrent]);
|
}), [repositories, currentRepository, setCurrentRepository]);
|
||||||
|
|
||||||
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
27
frontend/src/contexts/ThemeContext.ts
Normal file
27
frontend/src/contexts/ThemeContext.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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 { createContext } from "react";
|
||||||
|
|
||||||
|
export interface ThemeContextValue {
|
||||||
|
mode: "light" | "dark";
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
56
frontend/src/contexts/ThemeProvider.tsx
Normal file
56
frontend/src/contexts/ThemeProvider.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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 CssBaseline from "@mui/material/CssBaseline";
|
||||||
|
import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles";
|
||||||
|
import { defaults as chartDefaults } from "chart.js";
|
||||||
|
import { ThemeContext } from "contexts/ThemeContext";
|
||||||
|
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||||
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { createAppTheme } from "theme/Theme";
|
||||||
|
|
||||||
|
function systemPreference(): "light" | "dark" {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }): React.JSX.Element {
|
||||||
|
const [mode, setMode] = useLocalStorage<"light" | "dark">("theme-mode", systemPreference());
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setMode(prev => prev === "light" ? "dark" : "light");
|
||||||
|
}, [setMode]);
|
||||||
|
|
||||||
|
const theme = useMemo(() => createAppTheme(mode), [mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const textColor = mode === "dark" ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.7)";
|
||||||
|
const gridColor = mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
||||||
|
chartDefaults.color = textColor;
|
||||||
|
chartDefaults.borderColor = gridColor;
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ mode, toggleTheme }), [mode, toggleTheme]);
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</ThemeContext.Provider>;
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function usePackageActions(
|
|||||||
setSelectionModel: (model: string[]) => void,
|
setSelectionModel: (model: string[]) => void,
|
||||||
): UsePackageActionsResult {
|
): UsePackageActionsResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -50,13 +50,13 @@ export function usePackageActions(
|
|||||||
action: (repository: RepositoryId) => Promise<string>,
|
action: (repository: RepositoryId) => Promise<string>,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (!current) {
|
if (!currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const successMessage = await action(current);
|
const successMessage = await action(currentRepository);
|
||||||
showSuccess("Success", successMessage);
|
showSuccess("Success", successMessage);
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
setSelectionModel([]);
|
setSelectionModel([]);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
||||||
@@ -64,8 +64,8 @@ export function usePackageActions(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReload: () => void = () => {
|
const handleReload: () => void = () => {
|
||||||
if (current !== null) {
|
if (currentRepository !== null) {
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
36
frontend/src/hooks/usePackageChanges.ts
Normal file
36
frontend/src/hooks/usePackageChanges.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useQuery } from "@tanstack/react-query";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import type { Changes } from "models/Changes";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export function usePackageChanges(packageBase: string, repository: RepositoryId): Changes | undefined {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const { data } = useQuery<Changes>({
|
||||||
|
queryKey: QueryKeys.changes(packageBase, repository),
|
||||||
|
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
||||||
|
enabled: !!packageBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -39,20 +39,20 @@ export interface UsePackageDataResult {
|
|||||||
|
|
||||||
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
|
|
||||||
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packages = [], isLoading } = useQuery({
|
const { data: packages = [], isLoading } = useQuery({
|
||||||
queryKey: current ? QueryKeys.packages(current) : ["packages"],
|
queryKey: currentRepository ? QueryKeys.packages(currentRepository) : ["packages"],
|
||||||
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchPackages(currentRepository) : skipToken,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: status } = useQuery({
|
const { data: status } = useQuery({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export interface SelectedRepositoryResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectedRepository(): SelectedRepositoryResult {
|
export function useSelectedRepository(): SelectedRepositoryResult {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
const [selectedKey, setSelectedKey] = useState("");
|
const [selectedKey, setSelectedKey] = useState("");
|
||||||
|
|
||||||
let selectedRepository: RepositoryId | null = current;
|
let selectedRepository: RepositoryId | null = currentRepository;
|
||||||
if (selectedKey) {
|
if (selectedKey) {
|
||||||
const repository = repositories.find(repository => repository.key === selectedKey);
|
const repository = repositories.find(repository => repository.key === selectedKey);
|
||||||
if (repository) {
|
if (repository) {
|
||||||
|
|||||||
25
frontend/src/hooks/useThemeMode.ts
Normal file
25
frontend/src/hooks/useThemeMode.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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 { ThemeContext, type ThemeContextValue } from "contexts/ThemeContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useThemeMode(): ThemeContextValue {
|
||||||
|
return useContextNotNull(ThemeContext);
|
||||||
|
}
|
||||||
@@ -20,4 +20,5 @@
|
|||||||
export interface Changes {
|
export interface Changes {
|
||||||
changes?: string;
|
changes?: string;
|
||||||
last_commit_sha?: string;
|
last_commit_sha?: string;
|
||||||
|
pkgbuild?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export class PackageRow {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
timestampValue: number;
|
timestampValue: number;
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
isHeld: boolean;
|
||||||
|
|
||||||
constructor(descriptor: PackageStatus) {
|
constructor(descriptor: PackageStatus) {
|
||||||
this.id = descriptor.package.base;
|
this.id = descriptor.package.base;
|
||||||
@@ -45,6 +46,7 @@ export class PackageRow {
|
|||||||
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
|
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
|
||||||
this.timestampValue = descriptor.status.timestamp;
|
this.timestampValue = descriptor.status.timestamp;
|
||||||
this.status = descriptor.status.status;
|
this.status = descriptor.status.status;
|
||||||
|
this.isHeld = descriptor.status.is_held ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
|
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ import type { BuildStatus } from "models/BuildStatus";
|
|||||||
export interface Status {
|
export interface Status {
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
is_held?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ import { amber, green, grey, orange, red } from "@mui/material/colors";
|
|||||||
import type { BuildStatus } from "models/BuildStatus";
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
|
||||||
const base: Record<BuildStatus, string> = {
|
const base: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
unknown: grey[600],
|
||||||
pending: amber[900],
|
pending: amber[700],
|
||||||
building: orange[900],
|
building: orange[800],
|
||||||
failed: red[900],
|
failed: red[700],
|
||||||
success: green[800],
|
success: green[700],
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerBase: Record<BuildStatus, string> = {
|
const headerBase: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
unknown: grey[600],
|
||||||
pending: amber[700],
|
pending: amber[700],
|
||||||
building: orange[600],
|
building: orange[600],
|
||||||
failed: red[500],
|
failed: red[500],
|
||||||
|
|||||||
@@ -17,17 +17,28 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { createTheme } from "@mui/material/styles";
|
import { createTheme, type Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
const Theme = createTheme({
|
export function createAppTheme(mode: "light" | "dark"): Theme {
|
||||||
components: {
|
return createTheme({
|
||||||
MuiDialog: {
|
components: {
|
||||||
defaultProps: {
|
MuiButton: {
|
||||||
maxWidth: "lg",
|
styleOverrides: {
|
||||||
fullWidth: true,
|
startIcon: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDialog: {
|
||||||
|
defaultProps: {
|
||||||
|
fullWidth: true,
|
||||||
|
maxWidth: "lg",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
palette: {
|
||||||
});
|
mode,
|
||||||
|
},
|
||||||
export default Theme;
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, type Plugin } from "vite";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
function rename(oldName: string, newName: string): Plugin {
|
function rename(oldName: string, newName: string): Plugin {
|
||||||
return {
|
return {
|
||||||
@@ -16,8 +15,11 @@ function rename(oldName: string, newName: string): Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tsconfigPaths(), rename("index.html", "build-status.jinja2")],
|
plugins: [react(), rename("index.html", "build-status.jinja2")],
|
||||||
base: "/",
|
base: "/",
|
||||||
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 10000,
|
chunkSizeWarningLimit: 10000,
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
pkgbase='ahriman'
|
pkgbase='ahriman'
|
||||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||||
pkgver=2.20.0rc7
|
pkgver=2.20.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcH linux ReposItory MANager"
|
pkgdesc="ArcH linux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0rc7" "ArcH linux ReposItory MANager"
|
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0" "ArcH linux ReposItory MANager"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
ahriman \- ArcH linux ReposItory MANager
|
ahriman \- ArcH linux ReposItory MANager
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2.20.0rc7"
|
__version__ = "2.20.0"
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ class Application(ApplicationPackages, ApplicationRepository):
|
|||||||
for package_name, packager in missing.items():
|
for package_name, packager in missing.items():
|
||||||
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
|
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
|
||||||
# there is local cache, load package from it
|
# there is local cache, load package from it
|
||||||
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
|
leaf = Package.from_build(source_dir, self.repository.repository_id.architecture, packager)
|
||||||
else:
|
else:
|
||||||
leaf = Package.from_aur(package_name, packager, include_provides=True)
|
leaf = Package.from_aur(package_name, packager, include_provides=True)
|
||||||
portion[leaf.base] = leaf
|
portion[leaf.base] = leaf
|
||||||
|
|
||||||
# register package in the database
|
# register package in the database
|
||||||
self.repository.reporter.set_unknown(leaf)
|
self.reporter.set_unknown(leaf)
|
||||||
|
|
||||||
return portion
|
return portion
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class ApplicationRepository(ApplicationProperties):
|
|||||||
if last_commit_sha is None:
|
if last_commit_sha is None:
|
||||||
continue # skip check in case if we can't calculate diff
|
continue # skip check in case if we can't calculate diff
|
||||||
|
|
||||||
changes = self.repository.package_changes(package, last_commit_sha)
|
if (changes := self.repository.package_changes(package, last_commit_sha)) is not None:
|
||||||
self.repository.reporter.package_changes_update(package.base, changes)
|
self.reporter.package_changes_update(package.base, changes)
|
||||||
|
|
||||||
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -162,6 +162,10 @@ class ApplicationRepository(ApplicationProperties):
|
|||||||
self.on_result(build_result)
|
self.on_result(build_result)
|
||||||
result.merge(build_result)
|
result.merge(build_result)
|
||||||
|
|
||||||
|
# filter packages which were prebuilt
|
||||||
|
succeeded = {package.base for package in build_result.success}
|
||||||
|
updates = filter(lambda package: package.base not in succeeded, updates)
|
||||||
|
|
||||||
builder = Updater.load(self.repository_id, self.configuration, self.repository)
|
builder = Updater.load(self.repository_id, self.configuration, self.repository)
|
||||||
|
|
||||||
# ok so for now we split all packages into chunks and process each chunk accordingly
|
# ok so for now we split all packages into chunks and process each chunk accordingly
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import argparse
|
|||||||
|
|
||||||
from ahriman.application.application import Application
|
from ahriman.application.application import Application
|
||||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.application.handlers.update import Update
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import enum_values, extract_user
|
from ahriman.core.utils import enum_values, extract_user
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.packagers import Packagers
|
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
@@ -48,26 +48,7 @@ class Add(Handler):
|
|||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
application.on_start()
|
application.on_start()
|
||||||
|
Add.perform_action(application, args)
|
||||||
application.add(args.package, args.source, args.username)
|
|
||||||
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
|
||||||
for package in args.package: # for each requested package insert patch
|
|
||||||
for patch in patches:
|
|
||||||
application.reporter.package_patches_update(package, patch)
|
|
||||||
|
|
||||||
if not args.now:
|
|
||||||
return
|
|
||||||
|
|
||||||
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
|
|
||||||
if args.changes: # generate changes if requested
|
|
||||||
application.changes(packages)
|
|
||||||
|
|
||||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
|
||||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
|
||||||
|
|
||||||
application.print_updates(packages, log_fn=application.logger.info)
|
|
||||||
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
|
||||||
Add.check_status(args.exit_code, not result.is_empty)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@@ -103,14 +84,34 @@ class Add(Handler):
|
|||||||
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
|
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
|
||||||
action=argparse.BooleanOptionalAction, default=True)
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
||||||
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
|
||||||
"-yy to force refresh even if up to date",
|
|
||||||
action="count", default=False)
|
|
||||||
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
||||||
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
||||||
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
|
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
|
||||||
action="append")
|
action="append")
|
||||||
|
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||||
|
"-yy to force refresh even if up to date",
|
||||||
|
action="count", default=False)
|
||||||
|
parser.set_defaults(aur=False, check_files=False, dry_run=False, local=False, manual=True, vcs=False)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform add action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
application.add(args.package, args.source, args.username)
|
||||||
|
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
||||||
|
for package in args.package: # for each requested package insert patch
|
||||||
|
for patch in patches:
|
||||||
|
application.reporter.package_patches_update(package, patch)
|
||||||
|
|
||||||
|
if not args.now:
|
||||||
|
return
|
||||||
|
Update.perform_action(application, args)
|
||||||
|
|
||||||
arguments = [_set_package_add_parser]
|
arguments = [_set_package_add_parser]
|
||||||
|
|||||||
81
src/ahriman/application/handlers/archives.py
Normal file
81
src/ahriman/application/handlers/archives.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# 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.core.formatters import PackagePrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Archives(Handler):
|
||||||
|
"""
|
||||||
|
package archives handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
archives = application.repository.package_archives(args.package)
|
||||||
|
for package in archives:
|
||||||
|
PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info)
|
||||||
|
|
||||||
|
Archives.check_status(args.exit_code, bool(archives))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package archives subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-archives", help="list package archive versions",
|
||||||
|
description="list available archive versions for the package")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--info", help="show additional package information",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_archives_parser]
|
||||||
@@ -47,14 +47,13 @@ class Change(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
|
|
||||||
match args.action:
|
match args.action:
|
||||||
case Action.List:
|
case Action.List:
|
||||||
changes = client.package_changes_get(args.package)
|
changes = client.package_changes_get(args.package)
|
||||||
ChangesPrinter(changes)(verbose=True, separator="")
|
ChangesPrinter(changes)(verbose=True, separator="")
|
||||||
Change.check_status(args.exit_code, not changes.is_empty)
|
Change.check_status(args.exit_code, changes.changes is not None)
|
||||||
case Action.Remove:
|
case Action.Remove:
|
||||||
client.package_changes_update(args.package, Changes())
|
client.package_changes_update(args.package, Changes())
|
||||||
|
|
||||||
|
|||||||
93
src/ahriman/application/handlers/hold.py
Normal file
93
src/ahriman/application/handlers/hold.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#
|
||||||
|
# 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.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Hold(Handler):
|
||||||
|
"""
|
||||||
|
package hold handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.Remove:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=False)
|
||||||
|
case Action.Update:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_hold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for hold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-hold", help="hold package",
|
||||||
|
description="hold package from automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_unhold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for unhold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-unhold", help="unhold package",
|
||||||
|
description="remove package hold, allowing automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_package_hold_parser,
|
||||||
|
_set_package_unhold_parser,
|
||||||
|
]
|
||||||
100
src/ahriman/application/handlers/pkgbuild.py
Normal file
100
src/ahriman/application/handlers/pkgbuild.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#
|
||||||
|
# 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 dataclasses import replace
|
||||||
|
|
||||||
|
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 PkgbuildPrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Pkgbuild(Handler):
|
||||||
|
"""
|
||||||
|
package pkgbuild handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@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
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
PkgbuildPrinter(changes)(verbose=True, separator="")
|
||||||
|
Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
|
||||||
|
case Action.Remove:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
client.package_changes_update(args.package, replace(changes, pkgbuild=None))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
|
||||||
|
description="retrieve package PKGBUILD stored in database",
|
||||||
|
epilog="This command requests package status from the web interface "
|
||||||
|
"if it is available.")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild remove subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
|
||||||
|
description="remove the package PKGBUILD stored remotely")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
|
||||||
@@ -44,8 +44,7 @@ class Reload(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
client.configuration_reload()
|
client.configuration_reload()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
132
src/ahriman/application/handlers/rollback.py
Normal file
132
src/ahriman/application/handlers/rollback.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#
|
||||||
|
# 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 pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.add import Add
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
|
from ahriman.core.utils import extract_user
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Rollback(Handler):
|
||||||
|
"""
|
||||||
|
package rollback handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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=report)
|
||||||
|
application.on_start()
|
||||||
|
|
||||||
|
package = Rollback.package_load(application, args.package, args.version)
|
||||||
|
artifacts = Rollback.package_artifacts(application, package)
|
||||||
|
|
||||||
|
args.package = [str(artifact) for artifact in artifacts]
|
||||||
|
Add.perform_action(application, args)
|
||||||
|
|
||||||
|
if args.hold:
|
||||||
|
application.reporter.package_hold_update(package.base, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package archives subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-rollback", help="rollback package",
|
||||||
|
description="rollback package to specified version from archives")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("version", help="package version")
|
||||||
|
parser.add_argument("--hold", help="hold package afterwards",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False,
|
||||||
|
exit_code=False, increment=False, now=True, local=False, manual=False, refresh=False,
|
||||||
|
source=PackageSource.Archive, variable=None, vcs=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_artifacts(application: Application, package: Package) -> list[Path]:
|
||||||
|
"""
|
||||||
|
look for package artifacts and returns paths to them if any
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package(Package): package descriptor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Path]: paths to found artifacts
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if artifacts do not exist
|
||||||
|
"""
|
||||||
|
# lookup for built artifacts
|
||||||
|
artifacts = application.repository.package_archives_lookup(package)
|
||||||
|
if not artifacts:
|
||||||
|
raise UnknownPackageError(package.base) from None
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_load(application: Application, package_base: str, version: str) -> Package:
|
||||||
|
"""
|
||||||
|
load package from given arguments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package_base(str): package base
|
||||||
|
version(str): package version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: loaded package
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if package does not exist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
package, _ = next(iter(application.reporter.package_get(package_base)))
|
||||||
|
package.version = version
|
||||||
|
|
||||||
|
return package
|
||||||
|
except StopIteration:
|
||||||
|
raise UnknownPackageError(package_base) from None
|
||||||
|
|
||||||
|
arguments = [_set_package_archives_parser]
|
||||||
@@ -52,7 +52,7 @@ class Status(Handler):
|
|||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
# we are using reporter here
|
# we are using reporter here
|
||||||
client = Application(repository_id, configuration, report=True).repository.reporter
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
if args.ahriman:
|
if args.ahriman:
|
||||||
service_status = client.status_get()
|
service_status = client.status_get()
|
||||||
StatusPrinter(service_status.status)(verbose=args.info)
|
StatusPrinter(service_status.status)(verbose=args.info)
|
||||||
|
|||||||
@@ -47,8 +47,7 @@ class StatusUpdate(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
|
|
||||||
match args.action:
|
match args.action:
|
||||||
case Action.Update if args.package:
|
case Action.Update if args.package:
|
||||||
|
|||||||
@@ -48,22 +48,7 @@ class Update(Handler):
|
|||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
application.on_start()
|
application.on_start()
|
||||||
|
Update.perform_action(application, args)
|
||||||
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
|
||||||
check_files=args.check_files)
|
|
||||||
if args.changes: # generate changes if requested
|
|
||||||
application.changes(packages)
|
|
||||||
|
|
||||||
if args.dry_run: # exit from application if no build requested
|
|
||||||
Update.check_status(args.exit_code, packages) # status code check
|
|
||||||
return
|
|
||||||
|
|
||||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
|
||||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
|
||||||
|
|
||||||
application.print_updates(packages, log_fn=application.logger.info)
|
|
||||||
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
|
||||||
Update.check_status(args.exit_code, not result.is_empty)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@@ -153,6 +138,31 @@ class Update(Handler):
|
|||||||
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
|
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform update action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
||||||
|
check_files=args.check_files)
|
||||||
|
if args.changes: # generate changes if requested
|
||||||
|
application.changes(packages)
|
||||||
|
|
||||||
|
if args.dry_run: # exit from application if no build requested
|
||||||
|
Update.check_status(args.exit_code, packages) # status code check
|
||||||
|
return
|
||||||
|
|
||||||
|
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
||||||
|
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
||||||
|
|
||||||
|
application.print_updates(packages, log_fn=application.logger.info)
|
||||||
|
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
||||||
|
Update.check_status(args.exit_code, not result.is_empty)
|
||||||
|
|
||||||
arguments = [
|
arguments = [
|
||||||
_set_repo_check_parser,
|
_set_repo_check_parser,
|
||||||
_set_repo_update_parser,
|
_set_repo_update_parser,
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ import tarfile
|
|||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
from pyalpm import DB, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||||
|
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
from ahriman.core.utils import trim_package
|
from ahriman.core.utils import trim_package
|
||||||
@@ -61,16 +62,16 @@ class Pacman(LazyLogging):
|
|||||||
self.refresh_database = refresh_database
|
self.refresh_database = refresh_database
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def handle(self) -> Handle:
|
def handle(self) -> PacmanHandle:
|
||||||
"""
|
"""
|
||||||
pyalpm handle
|
pyalpm handle
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Handle: generated pyalpm handle instance
|
PacmanHandle: generated pyalpm handle instance
|
||||||
"""
|
"""
|
||||||
return self.__create_handle(refresh_database=self.refresh_database)
|
return self.__create_handle(refresh_database=self.refresh_database)
|
||||||
|
|
||||||
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
|
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> PacmanHandle:
|
||||||
"""
|
"""
|
||||||
create lazy handle function
|
create lazy handle function
|
||||||
|
|
||||||
@@ -78,14 +79,14 @@ class Pacman(LazyLogging):
|
|||||||
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Handle: fully initialized pacman handle
|
PacmanHandle: fully initialized pacman handle
|
||||||
"""
|
"""
|
||||||
pacman_root = self.configuration.getpath("alpm", "database")
|
pacman_root = self.configuration.getpath("alpm", "database")
|
||||||
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
||||||
|
|
||||||
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
||||||
root = self.configuration.getpath("alpm", "root")
|
root = self.configuration.getpath("alpm", "root")
|
||||||
handle = Handle(str(root), str(database_path))
|
handle = PacmanHandle(str(root), str(database_path))
|
||||||
|
|
||||||
for repository in self.configuration.getlist("alpm", "repositories"):
|
for repository in self.configuration.getlist("alpm", "repositories"):
|
||||||
database = self.database_init(handle, repository, self.repository_id.architecture)
|
database = self.database_init(handle, repository, self.repository_id.architecture)
|
||||||
@@ -99,12 +100,12 @@ class Pacman(LazyLogging):
|
|||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
def database_copy(self, handle: PacmanHandle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
||||||
"""
|
"""
|
||||||
copy database from the operating system root to the ahriman local home
|
copy database from the operating system root to the ahriman local home
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database copying
|
handle(PacmanHandle): pacman handle which will be used for database copying
|
||||||
database(DB): pacman database instance to be copied
|
database(DB): pacman database instance to be copied
|
||||||
pacman_root(Path): operating system pacman root
|
pacman_root(Path): operating system pacman root
|
||||||
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
||||||
@@ -133,12 +134,12 @@ class Pacman(LazyLogging):
|
|||||||
with self.repository_paths.preserve_owner():
|
with self.repository_paths.preserve_owner():
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
|
def database_init(self, handle: PacmanHandle, repository: str, architecture: str) -> DB:
|
||||||
"""
|
"""
|
||||||
create database instance from pacman handler and set its properties
|
create database instance from pacman handler and set its properties
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database initializing
|
handle(PacmanHandle): pacman handle which will be used for database initializing
|
||||||
repository(str): pacman repository name (e.g. core)
|
repository(str): pacman repository name (e.g. core)
|
||||||
architecture(str): repository architecture
|
architecture(str): repository architecture
|
||||||
|
|
||||||
@@ -164,12 +165,12 @@ class Pacman(LazyLogging):
|
|||||||
|
|
||||||
return database
|
return database
|
||||||
|
|
||||||
def database_sync(self, handle: Handle, *, force: bool) -> None:
|
def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
|
||||||
"""
|
"""
|
||||||
sync local database
|
sync local database
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database sync
|
handle(PacmanHandle): pacman handle which will be used for database sync
|
||||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
"""
|
"""
|
||||||
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
||||||
|
|||||||
81
src/ahriman/core/alpm/pacman_handle.py
Normal file
81
src/ahriman/core/alpm/pacman_handle.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
from pathlib import Path
|
||||||
|
from pyalpm import Handle, Package # type: ignore[import-not-found]
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any, ClassVar, Self
|
||||||
|
|
||||||
|
|
||||||
|
class PacmanHandle:
|
||||||
|
"""
|
||||||
|
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
handle(Handle): pyalpm handle instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ephemeral: ClassVar[Self | None] = None
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments for :class:`pyalpm.Handle`
|
||||||
|
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
|
||||||
|
"""
|
||||||
|
self.handle = Handle(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ephemeral(cls) -> Self:
|
||||||
|
"""
|
||||||
|
create temporary instance with no access to real databases
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: loaded class
|
||||||
|
"""
|
||||||
|
if cls._ephemeral is None:
|
||||||
|
# handle creates alpm version file, but we don't use it
|
||||||
|
# so it is ok to just remove it
|
||||||
|
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||||
|
cls._ephemeral = cls("/", dir_name)
|
||||||
|
return cls._ephemeral
|
||||||
|
|
||||||
|
def package_load(self, path: Path) -> Package:
|
||||||
|
"""
|
||||||
|
load package from path to the archive
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path(Path): path to package archive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: package instance
|
||||||
|
"""
|
||||||
|
return self.handle.load_pkg(str(path))
|
||||||
|
|
||||||
|
def __getattr__(self, item: str) -> Any:
|
||||||
|
"""
|
||||||
|
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item(str): property name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: attribute by its name
|
||||||
|
"""
|
||||||
|
return self.handle.__getattribute__(item)
|
||||||
@@ -26,6 +26,7 @@ from typing import ClassVar
|
|||||||
from ahriman.core.exceptions import CalledProcessError
|
from ahriman.core.exceptions import CalledProcessError
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
from ahriman.core.utils import check_output, utcnow, walk
|
from ahriman.core.utils import check_output, utcnow, walk
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild import Pkgbuild
|
from ahriman.models.pkgbuild import Pkgbuild
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
@@ -51,24 +52,25 @@ class Sources(LazyLogging):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def changes(source_dir: Path, last_commit_sha: str | None) -> str | None:
|
def changes(source_dir: Path, last_commit_sha: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
extract changes from the last known commit if available
|
extract changes from the last known commit if available
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_dir(Path): local path to directory with source files
|
source_dir(Path): local path to directory with source files
|
||||||
last_commit_sha(str | None): last known commit hash
|
last_commit_sha(str): last known commit hash
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: changes from the last commit if available or ``None`` otherwise
|
Changes: changes from the last commit if available
|
||||||
"""
|
"""
|
||||||
if last_commit_sha is None:
|
|
||||||
return None # no previous reference found
|
|
||||||
|
|
||||||
instance = Sources()
|
instance = Sources()
|
||||||
|
|
||||||
|
diff = None
|
||||||
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
||||||
return instance.diff(source_dir, last_commit_sha)
|
diff = instance.diff(source_dir, last_commit_sha)
|
||||||
return None
|
pkgbuild = instance.read(source_dir, "HEAD", Path("PKGBUILD"))
|
||||||
|
|
||||||
|
return Changes(last_commit_sha, diff, pkgbuild)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
||||||
@@ -413,3 +415,17 @@ class Sources(LazyLogging):
|
|||||||
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
||||||
else:
|
else:
|
||||||
patch.write(sources_dir / "PKGBUILD")
|
patch.write(sources_dir / "PKGBUILD")
|
||||||
|
|
||||||
|
def read(self, sources_dir: Path, commit_sha: str, path: Path) -> str:
|
||||||
|
"""
|
||||||
|
read file content from the specified commit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sources_dir(Path): local path to git repository
|
||||||
|
commit_sha(str): commit hash to read from
|
||||||
|
path(Path): path to file inside the repository
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: file content at specified commit
|
||||||
|
"""
|
||||||
|
return check_output(*self.git(), "show", f"{commit_sha}:{path}", cwd=sources_dir, logger=self.logger)
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
#
|
#
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.utils import package_like
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["migrate_data", "steps"]
|
__all__ = ["migrate_data", "steps"]
|
||||||
@@ -61,12 +59,9 @@ def migrate_package_depends(connection: Connection, configuration: Configuration
|
|||||||
if not configuration.repository_paths.repository.is_dir():
|
if not configuration.repository_paths.repository.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
package_list = []
|
package_list = []
|
||||||
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
||||||
base = Package.from_archive(full_path, pacman)
|
base = Package.from_archive(full_path)
|
||||||
for package, description in base.packages.items():
|
for package, description in base.packages.items():
|
||||||
package_list.append({
|
package_list.append({
|
||||||
"make_depends": description.make_depends,
|
"make_depends": description.make_depends,
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
#
|
#
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.utils import package_like
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["migrate_data", "steps"]
|
__all__ = ["migrate_data", "steps"]
|
||||||
@@ -58,12 +56,9 @@ def migrate_package_check_depends(connection: Connection, configuration: Configu
|
|||||||
if not configuration.repository_paths.repository.is_dir():
|
if not configuration.repository_paths.repository.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
package_list = []
|
package_list = []
|
||||||
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
||||||
base = Package.from_archive(full_path, pacman)
|
base = Package.from_archive(full_path)
|
||||||
for package, description in base.packages.items():
|
for package, description in base.packages.items():
|
||||||
package_list.append({
|
package_list.append({
|
||||||
"check_depends": description.check_depends,
|
"check_depends": description.check_depends,
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
#
|
#
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.utils import package_like
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["migrate_data", "steps"]
|
__all__ = ["migrate_data", "steps"]
|
||||||
@@ -64,12 +62,9 @@ def migrate_package_base_packager(connection: Connection, configuration: Configu
|
|||||||
if not configuration.repository_paths.repository.is_dir():
|
if not configuration.repository_paths.repository.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
package_list = []
|
package_list = []
|
||||||
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
||||||
package = Package.from_archive(full_path, pacman)
|
package = Package.from_archive(full_path)
|
||||||
package_list.append({
|
package_list.append({
|
||||||
"package_base": package.base,
|
"package_base": package.base,
|
||||||
"packager": package.packager,
|
"packager": package.packager,
|
||||||
|
|||||||
@@ -20,13 +20,11 @@
|
|||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.repository import Explorer
|
from ahriman.core.repository import Explorer
|
||||||
from ahriman.core.sign.gpg import GPG
|
from ahriman.core.sign.gpg import GPG
|
||||||
from ahriman.core.utils import atomic_move, package_like, symlink_relative
|
from ahriman.core.utils import atomic_move, package_like, symlink_relative
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
@@ -45,29 +43,27 @@ def migrate_data(connection: Connection, configuration: Configuration) -> None:
|
|||||||
|
|
||||||
for repository_id in Explorer.repositories_extract(configuration):
|
for repository_id in Explorer.repositories_extract(configuration):
|
||||||
paths = replace(configuration.repository_paths, repository_id=repository_id)
|
paths = replace(configuration.repository_paths, repository_id=repository_id)
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
# create archive directory if required
|
# create archive directory if required
|
||||||
if not paths.archive.is_dir():
|
if not paths.archive.is_dir():
|
||||||
with paths.preserve_owner():
|
with paths.preserve_owner():
|
||||||
paths.archive.mkdir(mode=0o755, parents=True)
|
paths.archive.mkdir(mode=0o755, parents=True)
|
||||||
|
|
||||||
move_packages(paths, pacman)
|
move_packages(paths)
|
||||||
|
|
||||||
|
|
||||||
def move_packages(repository_paths: RepositoryPaths, pacman: Pacman) -> None:
|
def move_packages(repository_paths: RepositoryPaths) -> None:
|
||||||
"""
|
"""
|
||||||
move packages from repository to archive and create symbolic links
|
move packages from repository to archive and create symbolic links
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository_paths(RepositoryPaths): repository paths instance
|
repository_paths(RepositoryPaths): repository paths instance
|
||||||
pacman(Pacman): alpm wrapper instance
|
|
||||||
"""
|
"""
|
||||||
for archive in filter(package_like, repository_paths.repository.iterdir()):
|
for archive in filter(package_like, repository_paths.repository.iterdir()):
|
||||||
if not archive.is_file(follow_symlinks=False):
|
if not archive.is_file(follow_symlinks=False):
|
||||||
continue # skip symbolic links if any
|
continue # skip symbolic links if any
|
||||||
|
|
||||||
package = Package.from_archive(archive, pacman)
|
package = Package.from_archive(archive)
|
||||||
artifacts = [archive]
|
artifacts = [archive]
|
||||||
# check if there are signatures for this package and append it here too
|
# check if there are signatures for this package and append it here too
|
||||||
if (signature := GPG.signature(archive)).exists():
|
if (signature := GPG.signature(archive)).exists():
|
||||||
|
|||||||
25
src/ahriman/core/database/migrations/m017_pkgbuild.py
Normal file
25
src/ahriman/core/database/migrations/m017_pkgbuild.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""alter table package_changes add column pkgbuild text""",
|
||||||
|
]
|
||||||
25
src/ahriman/core/database/migrations/m018_package_hold.py
Normal file
25
src/ahriman/core/database/migrations/m018_package_hold.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""alter table package_statuses add column is_held integer not null default 0""",
|
||||||
|
]
|
||||||
@@ -45,10 +45,10 @@ class ChangesOperations(Operations):
|
|||||||
def run(connection: Connection) -> Changes:
|
def run(connection: Connection) -> Changes:
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
Changes(row["last_commit_sha"], row["changes"] or None)
|
Changes(row["last_commit_sha"], row["changes"] or None, row["pkgbuild"] or None)
|
||||||
for row in connection.execute(
|
for row in connection.execute(
|
||||||
"""
|
"""
|
||||||
select last_commit_sha, changes from package_changes
|
select last_commit_sha, changes, pkgbuild from package_changes
|
||||||
where package_base = :package_base and repository = :repository
|
where package_base = :package_base and repository = :repository
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
@@ -77,16 +77,17 @@ class ChangesOperations(Operations):
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
"""
|
"""
|
||||||
insert into package_changes
|
insert into package_changes
|
||||||
(package_base, last_commit_sha, changes, repository)
|
(package_base, last_commit_sha, changes, pkgbuild, repository)
|
||||||
values
|
values
|
||||||
(:package_base, :last_commit_sha, :changes ,:repository)
|
(:package_base, :last_commit_sha, :changes, :pkgbuild, :repository)
|
||||||
on conflict (package_base, repository) do update set
|
on conflict (package_base, repository) do update set
|
||||||
last_commit_sha = :last_commit_sha, changes = :changes
|
last_commit_sha = :last_commit_sha, changes = :changes, pkgbuild = :pkgbuild
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"package_base": package_base,
|
"package_base": package_base,
|
||||||
"last_commit_sha": changes.last_commit_sha,
|
"last_commit_sha": changes.last_commit_sha,
|
||||||
"changes": changes.changes,
|
"changes": changes.changes,
|
||||||
|
"pkgbuild": changes.pkgbuild,
|
||||||
"repository": repository_id.id,
|
"repository": repository_id.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -211,13 +211,37 @@ class PackageOperations(Operations):
|
|||||||
dict[str, BuildStatus]: map of the package base to its status
|
dict[str, BuildStatus]: map of the package base to its status
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
|
row["package_base"]: BuildStatus(row["status"], row["last_updated"], is_held=bool(row["is_held"]))
|
||||||
for row in connection.execute(
|
for row in connection.execute(
|
||||||
"""select * from package_statuses where repository = :repository""",
|
"""select * from package_statuses where repository = :repository""",
|
||||||
{"repository": repository_id.id}
|
{"repository": repository_id.id}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, repository_id: RepositoryId | None = None, *,
|
||||||
|
enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
def run(connection: Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"""update package_statuses set is_held = :is_held
|
||||||
|
where package_base = :package_base and repository = :repository""",
|
||||||
|
{
|
||||||
|
"is_held": int(enabled),
|
||||||
|
"package_base": package_base,
|
||||||
|
"repository": repository_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.with_connection(run, commit=True)
|
||||||
|
|
||||||
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
|
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
remove package from database
|
remove package from database
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from ahriman.core.formatters.event_stats_printer import EventStatsPrinter
|
|||||||
from ahriman.core.formatters.package_printer import PackagePrinter
|
from ahriman.core.formatters.package_printer import PackagePrinter
|
||||||
from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
||||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||||
|
from ahriman.core.formatters.pkgbuild_printer import PkgbuildPrinter
|
||||||
from ahriman.core.formatters.printer import Printer
|
from ahriman.core.formatters.printer import Printer
|
||||||
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
||||||
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
|
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ChangesPrinter(Printer):
|
|||||||
Returns:
|
Returns:
|
||||||
list[Property]: list of content properties
|
list[Property]: list of content properties
|
||||||
"""
|
"""
|
||||||
if self.changes.is_empty:
|
if self.changes.changes is None:
|
||||||
return []
|
return []
|
||||||
return [Property("", self.changes.changes, is_required=True, indent=0)]
|
return [Property("", self.changes.changes, is_required=True, indent=0)]
|
||||||
|
|
||||||
@@ -57,6 +57,6 @@ class ChangesPrinter(Printer):
|
|||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and ``None`` otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
if self.changes.is_empty:
|
if self.changes.changes is None:
|
||||||
return None
|
return None
|
||||||
return self.changes.last_commit_sha
|
return self.changes.last_commit_sha
|
||||||
|
|||||||
62
src/ahriman/core/formatters/pkgbuild_printer.py
Normal file
62
src/ahriman/core/formatters/pkgbuild_printer.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
from ahriman.core.formatters.printer import Printer
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
|
from ahriman.models.property import Property
|
||||||
|
|
||||||
|
|
||||||
|
class PkgbuildPrinter(Printer):
|
||||||
|
"""
|
||||||
|
print content of the pkgbuild stored in changes
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
changes(Changes): package changes
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, changes: Changes) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
changes(Changes): package changes
|
||||||
|
"""
|
||||||
|
Printer.__init__(self)
|
||||||
|
self.changes = changes
|
||||||
|
|
||||||
|
def properties(self) -> list[Property]:
|
||||||
|
"""
|
||||||
|
convert content into printable data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Property]: list of content properties
|
||||||
|
"""
|
||||||
|
if self.changes.pkgbuild is None:
|
||||||
|
return []
|
||||||
|
return [Property("", self.changes.pkgbuild, is_required=True, indent=0)]
|
||||||
|
|
||||||
|
# pylint: disable=redundant-returns-doc
|
||||||
|
def title(self) -> str | None:
|
||||||
|
"""
|
||||||
|
generate entry title from content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
|
"""
|
||||||
|
if self.changes.pkgbuild is None:
|
||||||
|
return None
|
||||||
|
return self.changes.last_commit_sha
|
||||||
@@ -17,14 +17,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from collections.abc import Callable
|
|
||||||
from functools import cmp_to_key
|
|
||||||
|
|
||||||
from ahriman.core import context
|
from ahriman.core import context
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
from ahriman.core.triggers import Trigger
|
from ahriman.core.triggers import Trigger
|
||||||
from ahriman.core.utils import package_like
|
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.result import Result
|
from ahriman.models.result import Result
|
||||||
@@ -78,27 +74,20 @@ class ArchiveRotationTrigger(Trigger):
|
|||||||
"""
|
"""
|
||||||
return list(cls.CONFIGURATION_SCHEMA.keys())
|
return list(cls.CONFIGURATION_SCHEMA.keys())
|
||||||
|
|
||||||
def archives_remove(self, package: Package, pacman: Pacman) -> None:
|
def archives_remove(self, package: Package, repository: Repository) -> None:
|
||||||
"""
|
"""
|
||||||
remove older versions of the specified package
|
remove older versions of the specified package
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): package which has been updated to check for older versions
|
package(Package): package which has been updated to check for older versions
|
||||||
pacman(Pacman): alpm wrapper instance
|
repository(Repository): repository instance
|
||||||
"""
|
"""
|
||||||
# explicit guard to skip process in case if rotation is disabled
|
# explicit guard to skip process in case if rotation is disabled
|
||||||
# this guard is supposed to speedup process
|
# this guard is supposed to speedup process
|
||||||
if self.keep_built_packages == 0:
|
if self.keep_built_packages == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
packages: dict[tuple[str, str], Package] = {}
|
to_remove = repository.package_archives(package.base)
|
||||||
# we can't use here load_archives, because it ignores versions
|
|
||||||
for full_path in filter(package_like, self.paths.archive_for(package.base).iterdir()):
|
|
||||||
local = Package.from_archive(full_path, pacman)
|
|
||||||
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
|
|
||||||
|
|
||||||
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
|
||||||
to_remove = sorted(packages.values(), key=cmp_to_key(comparator))
|
|
||||||
|
|
||||||
for single in to_remove[:-self.keep_built_packages]:
|
for single in to_remove[:-self.keep_built_packages]:
|
||||||
self.logger.info("removing version %s of package %s", single.version, single.base)
|
self.logger.info("removing version %s of package %s", single.version, single.base)
|
||||||
@@ -115,7 +104,7 @@ class ArchiveRotationTrigger(Trigger):
|
|||||||
packages(list[Package]): list of all available packages
|
packages(list[Package]): list of all available packages
|
||||||
"""
|
"""
|
||||||
ctx = context.get()
|
ctx = context.get()
|
||||||
pacman = ctx.get(Pacman)
|
repository = ctx.get(Repository)
|
||||||
|
|
||||||
for package in result.success:
|
for package in result.success:
|
||||||
self.archives_remove(package, pacman)
|
self.archives_remove(package, repository)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from ahriman.core.build_tools.package_archive import PackageArchive
|
|||||||
from ahriman.core.build_tools.task import Task
|
from ahriman.core.build_tools.task import Task
|
||||||
from ahriman.core.repository.cleaner import Cleaner
|
from ahriman.core.repository.cleaner import Cleaner
|
||||||
from ahriman.core.repository.package_info import PackageInfo
|
from ahriman.core.repository.package_info import PackageInfo
|
||||||
from ahriman.core.utils import atomic_move, filelock, list_flatmap, package_like, safe_filename, symlink_relative
|
from ahriman.core.utils import atomic_move, filelock, safe_filename, symlink_relative
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.event import EventType
|
from ahriman.models.event import EventType
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@@ -41,35 +41,6 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
trait for common repository update processes
|
trait for common repository update processes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _archive_lookup(self, package: Package) -> list[Path]:
|
|
||||||
"""
|
|
||||||
check if there is a rebuilt package already
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package(Package): package to check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Path]: list of built packages and signatures if available, empty list otherwise
|
|
||||||
"""
|
|
||||||
archive = self.paths.archive_for(package.base)
|
|
||||||
if not archive.is_dir():
|
|
||||||
return []
|
|
||||||
|
|
||||||
for path in filter(package_like, archive.iterdir()):
|
|
||||||
# check if package version is the same
|
|
||||||
built = Package.from_archive(path, self.pacman)
|
|
||||||
if built.version != package.version:
|
|
||||||
continue
|
|
||||||
|
|
||||||
packages = built.packages.values()
|
|
||||||
# all packages must be either any or same architecture
|
|
||||||
if not all(single.architecture in ("any", self.architecture) for single in packages):
|
|
||||||
continue
|
|
||||||
|
|
||||||
return list_flatmap(packages, lambda single: archive.glob(f"{single.filename}*"))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _archive_rename(self, description: PackageDescription, package_base: str) -> None:
|
def _archive_rename(self, description: PackageDescription, package_base: str) -> None:
|
||||||
"""
|
"""
|
||||||
rename package archive removing special symbols
|
rename package archive removing special symbols
|
||||||
@@ -102,12 +73,12 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
"""
|
"""
|
||||||
self.reporter.set_building(package.base)
|
self.reporter.set_building(package.base)
|
||||||
|
|
||||||
task = Task(package, self.configuration, self.architecture, self.paths)
|
task = Task(package, self.configuration, self.repository_id.architecture, self.paths)
|
||||||
patches = self.reporter.package_patches_get(package.base, None)
|
patches = self.reporter.package_patches_get(package.base, None)
|
||||||
commit_sha = task.init(path, patches, local_version)
|
commit_sha = task.init(path, patches, local_version)
|
||||||
|
|
||||||
loaded_package = Package.from_build(path, self.architecture, None)
|
loaded_package = Package.from_build(path, self.repository_id.architecture, None)
|
||||||
if prebuilt := list(self._archive_lookup(loaded_package)):
|
if prebuilt := self.package_archives_lookup(loaded_package):
|
||||||
self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version)
|
self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version)
|
||||||
built = []
|
built = []
|
||||||
for artifact in prebuilt:
|
for artifact in prebuilt:
|
||||||
@@ -117,7 +88,7 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
else:
|
else:
|
||||||
built = task.build(path, PACKAGER=packager)
|
built = task.build(path, PACKAGER=packager)
|
||||||
|
|
||||||
package.with_packages(built, self.pacman)
|
package.with_packages(built)
|
||||||
for src in built:
|
for src in built:
|
||||||
dst = self.paths.packages / src.name
|
dst = self.paths.packages / src.name
|
||||||
atomic_move(src, dst)
|
atomic_move(src, dst)
|
||||||
@@ -204,7 +175,8 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
|
|
||||||
# update commit hash for changes keeping current diff if there is any
|
# update commit hash for changes keeping current diff if there is any
|
||||||
changes = self.reporter.package_changes_get(single.base)
|
changes = self.reporter.package_changes_get(single.base)
|
||||||
self.reporter.package_changes_update(single.base, Changes(commit_sha, changes.changes))
|
self.reporter.package_changes_update(
|
||||||
|
single.base, Changes(commit_sha, changes.changes, changes.pkgbuild))
|
||||||
|
|
||||||
# update dependencies list
|
# update dependencies list
|
||||||
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
||||||
@@ -217,7 +189,7 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(single.base)
|
self.reporter.set_failed(single.base)
|
||||||
result.add_failed(single)
|
result.add_failed(single)
|
||||||
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
|
self.logger.exception("%s (%s) build exception", single.base, self.repository_id.architecture)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,42 @@
|
|||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Callable, Iterable
|
||||||
|
from functools import cmp_to_key
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from ahriman.core.alpm.pacman import Pacman
|
||||||
from ahriman.core.build_tools.package_version import PackageVersion
|
from ahriman.core.build_tools.package_version import PackageVersion
|
||||||
from ahriman.core.build_tools.sources import Sources
|
from ahriman.core.build_tools.sources import Sources
|
||||||
from ahriman.core.repository.repository_properties import RepositoryProperties
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.status import Client
|
||||||
|
from ahriman.core.utils import list_flatmap, package_like
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
class PackageInfo(RepositoryProperties):
|
class PackageInfo(LazyLogging):
|
||||||
"""
|
"""
|
||||||
handler for the package information
|
handler for the package information
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
pacman(Pacman): alpm wrapper instance
|
||||||
|
paths(RepositoryPaths): repository paths instance
|
||||||
|
reporter(Client): build status reporter instance
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
configuration: Configuration
|
||||||
|
pacman: Pacman
|
||||||
|
paths: RepositoryPaths
|
||||||
|
reporter: Client
|
||||||
|
repository_id: RepositoryId
|
||||||
|
|
||||||
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
|
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
generate full dependencies list including transitive dependencies
|
generate full dependencies list including transitive dependencies
|
||||||
@@ -86,7 +105,7 @@ class PackageInfo(RepositoryProperties):
|
|||||||
# we are iterating over bases, not single packages
|
# we are iterating over bases, not single packages
|
||||||
for full_path in packages:
|
for full_path in packages:
|
||||||
try:
|
try:
|
||||||
local = Package.from_archive(full_path, self.pacman)
|
local = Package.from_archive(full_path)
|
||||||
if (source := sources.get(local.base)) is not None: # update source with remote
|
if (source := sources.get(local.base)) is not None: # update source with remote
|
||||||
local.remote = source
|
local.remote = source
|
||||||
|
|
||||||
@@ -102,27 +121,76 @@ class PackageInfo(RepositoryProperties):
|
|||||||
self.logger.exception("could not load package from %s", full_path)
|
self.logger.exception("could not load package from %s", full_path)
|
||||||
return list(result.values())
|
return list(result.values())
|
||||||
|
|
||||||
def package_changes(self, package: Package, last_commit_sha: str | None) -> Changes:
|
def package_archives(self, package_base: str) -> list[Package]:
|
||||||
|
"""
|
||||||
|
load list of packages known for this package base. This method unlike
|
||||||
|
:func:`ahriman.core.repository.package_info.PackageInfo.load_archives` scans archive directory and loads all
|
||||||
|
versions available for the ``package_base``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Package]: list of packages belonging to this base, sorted by version by ascension
|
||||||
|
"""
|
||||||
|
packages: dict[tuple[str, str], Package] = {}
|
||||||
|
# we can't use here load_archives, because it ignores versions
|
||||||
|
for full_path in filter(package_like, self.paths.archive_for(package_base).iterdir()):
|
||||||
|
local = Package.from_archive(full_path)
|
||||||
|
if not local.supports_architecture(self.repository_id.architecture):
|
||||||
|
continue
|
||||||
|
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
|
||||||
|
|
||||||
|
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
||||||
|
return sorted(packages.values(), key=cmp_to_key(comparator))
|
||||||
|
|
||||||
|
def package_archives_lookup(self, package: Package) -> list[Path]:
|
||||||
|
"""
|
||||||
|
check if there is a rebuilt package already
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package(Package): package to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Path]: list of built packages and signatures if available, empty list otherwise
|
||||||
|
"""
|
||||||
|
archive = self.paths.archive_for(package.base)
|
||||||
|
if not archive.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
for path in filter(package_like, archive.iterdir()):
|
||||||
|
# check if package version is the same
|
||||||
|
built = Package.from_archive(path)
|
||||||
|
if built.version != package.version:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# all packages must be either any or same architecture
|
||||||
|
if not built.supports_architecture(self.repository_id.architecture):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return list_flatmap(built.packages.values(), lambda single: archive.glob(f"{single.filename}*"))
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
|
||||||
"""
|
"""
|
||||||
extract package change for the package since last commit if available
|
extract package change for the package since last commit if available
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): package properties
|
package(Package): package properties
|
||||||
last_commit_sha(str | None): last known commit hash
|
last_commit_sha(str): last known commit hash
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Changes: changes if available
|
Changes | None: changes if available
|
||||||
"""
|
"""
|
||||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||||
dir_path = Path(dir_name)
|
dir_path = Path(dir_name)
|
||||||
patches = self.reporter.package_patches_get(package.base, None)
|
patches = self.reporter.package_patches_get(package.base, None)
|
||||||
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
|
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
|
||||||
|
|
||||||
changes: str | None = None
|
|
||||||
if current_commit_sha != last_commit_sha:
|
if current_commit_sha != last_commit_sha:
|
||||||
changes = Sources.changes(dir_path, last_commit_sha)
|
return Sources.changes(dir_path, last_commit_sha)
|
||||||
|
return None
|
||||||
return Changes(last_commit_sha, changes)
|
|
||||||
|
|
||||||
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class RepositoryProperties(EventLogger, LazyLogging):
|
|||||||
Attributes:
|
Attributes:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
database(SQLite): database instance
|
database(SQLite): database instance
|
||||||
ignore_list(list[str]): package bases which will be ignored during auto updates
|
|
||||||
pacman(Pacman): alpm wrapper instance
|
pacman(Pacman): alpm wrapper instance
|
||||||
paths(RepositoryPaths): repository paths instance
|
paths(RepositoryPaths): repository paths instance
|
||||||
repo(Repo): repo commands wrapper instance
|
repo(Repo): repo commands wrapper instance
|
||||||
@@ -69,35 +68,15 @@ class RepositoryProperties(EventLogger, LazyLogging):
|
|||||||
|
|
||||||
self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing
|
self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing
|
||||||
|
|
||||||
self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
|
self._ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
|
||||||
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
|
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
|
||||||
self.sign = GPG(configuration)
|
self.sign = GPG(configuration)
|
||||||
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
self.repo = Repo(self.repository_id.name, self.paths, self.sign.repository_sign_args)
|
||||||
self.reporter = Client.load(repository_id, configuration, database, report=report)
|
self.reporter = Client.load(repository_id, configuration, database, report=report)
|
||||||
self.triggers = TriggerLoader.load(repository_id, configuration)
|
self.triggers = TriggerLoader.load(repository_id, configuration)
|
||||||
|
|
||||||
self.scan_paths = ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
|
self.scan_paths = ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
|
||||||
|
|
||||||
@property
|
|
||||||
def architecture(self) -> str:
|
|
||||||
"""
|
|
||||||
repository architecture for backward compatibility
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: repository architecture
|
|
||||||
"""
|
|
||||||
return self.repository_id.architecture
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""
|
|
||||||
repository name for backward compatibility
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: repository name
|
|
||||||
"""
|
|
||||||
return self.repository_id.name
|
|
||||||
|
|
||||||
def packager(self, packagers: Packagers, package_base: str) -> User:
|
def packager(self, packagers: Packagers, package_base: str) -> User:
|
||||||
"""
|
"""
|
||||||
extract packager from configuration having username
|
extract packager from configuration having username
|
||||||
|
|||||||
@@ -58,12 +58,17 @@ class UpdateHandler(PackageInfo, Cleaner):
|
|||||||
continue
|
continue
|
||||||
raise UnknownPackageError(package.base)
|
raise UnknownPackageError(package.base)
|
||||||
|
|
||||||
|
ignore_list = self._ignore_list + [
|
||||||
|
package.base for package, status in self.reporter.package_get(None) if status.is_held
|
||||||
|
]
|
||||||
|
|
||||||
result: list[Package] = []
|
result: list[Package] = []
|
||||||
for local in self.packages(filter_packages):
|
for local in self.packages(filter_packages):
|
||||||
with self.in_package_context(local.base, local.version):
|
with self.in_package_context(local.base, local.version):
|
||||||
if not local.remote.is_remote:
|
if not local.remote.is_remote:
|
||||||
continue # avoid checking local packages
|
continue # avoid checking local packages
|
||||||
if local.base in self.ignore_list:
|
if local.base in ignore_list:
|
||||||
|
self.logger.info("package %s is held, skip update check", local.base)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -150,7 +155,7 @@ class UpdateHandler(PackageInfo, Cleaner):
|
|||||||
)
|
)
|
||||||
|
|
||||||
Sources.fetch(cache_dir, source)
|
Sources.fetch(cache_dir, source)
|
||||||
remote = Package.from_build(cache_dir, self.architecture, None)
|
remote = Package.from_build(cache_dir, self.repository_id.architecture, None)
|
||||||
|
|
||||||
local = packages.get(remote.base)
|
local = packages.get(remote.base)
|
||||||
if local is None:
|
if local is None:
|
||||||
|
|||||||
@@ -199,6 +199,19 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||||
"""
|
"""
|
||||||
post log record
|
post log record
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class LocalClient(Client):
|
|||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
database(SQLite): database instance:
|
database(SQLite): database instance
|
||||||
"""
|
"""
|
||||||
self.database = database
|
self.database = database
|
||||||
self.repository_id = repository_id
|
self.repository_id = repository_id
|
||||||
@@ -143,6 +143,16 @@ class LocalClient(Client):
|
|||||||
return packages
|
return packages
|
||||||
return [(package, status) for package, status in packages if package.base == package_base]
|
return [(package, status) for package, status in packages if package.base == package_base]
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
self.database.package_hold_update(package_base, self.repository_id, enabled=enabled)
|
||||||
|
|
||||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||||
"""
|
"""
|
||||||
post log record
|
post log record
|
||||||
|
|||||||
@@ -18,11 +18,13 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import replace
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Self
|
from typing import Any, Self
|
||||||
|
|
||||||
from ahriman.core.exceptions import UnknownPackageError
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.repository.package_info import PackageInfo
|
||||||
from ahriman.core.status import Client
|
from ahriman.core.status import Client
|
||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
@@ -39,15 +41,18 @@ class Watcher(LazyLogging):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
client(Client): reporter instance
|
client(Client): reporter instance
|
||||||
|
package_info(PackageInfo): package info instance
|
||||||
status(BuildStatus): daemon status
|
status(BuildStatus): daemon status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, client: Client) -> None:
|
def __init__(self, client: Client, package_info: PackageInfo) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
client(Client): reporter instance
|
client(Client): reporter instance
|
||||||
|
package_info(PackageInfo): package info instance
|
||||||
"""
|
"""
|
||||||
self.client = client
|
self.client = client
|
||||||
|
self.package_info = package_info
|
||||||
|
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
||||||
@@ -80,6 +85,18 @@ class Watcher(LazyLogging):
|
|||||||
|
|
||||||
logs_rotate: Callable[[int], None]
|
logs_rotate: Callable[[int], None]
|
||||||
|
|
||||||
|
def package_archives(self, package_base: str) -> list[Package]:
|
||||||
|
"""
|
||||||
|
get known package archives
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Package]: list of built package for this package base
|
||||||
|
"""
|
||||||
|
return self.package_info.package_archives(package_base)
|
||||||
|
|
||||||
package_changes_get: Callable[[str], Changes]
|
package_changes_get: Callable[[str], Changes]
|
||||||
|
|
||||||
package_changes_update: Callable[[str, Changes], None]
|
package_changes_update: Callable[[str, Changes], None]
|
||||||
@@ -113,6 +130,19 @@ class Watcher(LazyLogging):
|
|||||||
|
|
||||||
package_logs_remove: Callable[[str, str | None], None]
|
package_logs_remove: Callable[[str, str | None], None]
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
package, status = self.package_get(package_base)
|
||||||
|
with self._lock:
|
||||||
|
self._known[package_base] = (package, replace(status, is_held=enabled))
|
||||||
|
self.client.package_hold_update(package_base, enabled=enabled)
|
||||||
|
|
||||||
package_patches_get: Callable[[str, str | None], list[PkgbuildPatch]]
|
package_patches_get: Callable[[str, str | None], list[PkgbuildPatch]]
|
||||||
|
|
||||||
package_patches_remove: Callable[[str, str], None]
|
package_patches_remove: Callable[[str, str], None]
|
||||||
@@ -138,9 +168,9 @@ class Watcher(LazyLogging):
|
|||||||
package_base(str): package base to update
|
package_base(str): package base to update
|
||||||
status(BuildStatusEnum): new build status
|
status(BuildStatusEnum): new build status
|
||||||
"""
|
"""
|
||||||
package, _ = self.package_get(package_base)
|
package, current_status = self.package_get(package_base)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._known[package_base] = (package, BuildStatus(status))
|
self._known[package_base] = (package, BuildStatus(status, is_held=current_status.is_held))
|
||||||
self.client.package_status_update(package_base, status)
|
self.client.package_status_update(package_base, status)
|
||||||
|
|
||||||
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
|
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
|
||||||
@@ -152,7 +182,8 @@ class Watcher(LazyLogging):
|
|||||||
status(BuildStatusEnum): new build status
|
status(BuildStatusEnum): new build status
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._known[package.base] = (package, BuildStatus(status))
|
_, current_status = self._known.get(package.base, (package, BuildStatus()))
|
||||||
|
self._known[package.base] = (package, BuildStatus(status, is_held=current_status.is_held))
|
||||||
self.client.package_update(package, status)
|
self.client.package_update(package, status)
|
||||||
|
|
||||||
def status_update(self, status: BuildStatusEnum) -> None:
|
def status_update(self, status: BuildStatusEnum) -> None:
|
||||||
|
|||||||
@@ -314,6 +314,18 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self.make_request("POST", f"{self.address}/api/v1/packages/{url_encode(package_base)}/hold",
|
||||||
|
params=self.repository_id.query(), json={"is_held": enabled})
|
||||||
|
|
||||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||||
"""
|
"""
|
||||||
post log record
|
post log record
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ class BuildStatus:
|
|||||||
Attributes:
|
Attributes:
|
||||||
status(BuildStatusEnum): build status
|
status(BuildStatusEnum): build status
|
||||||
timestamp(int): build status update time
|
timestamp(int): build status update time
|
||||||
|
is_held(bool | None): whether package held or not
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status: BuildStatusEnum = BuildStatusEnum.Unknown
|
status: BuildStatusEnum = BuildStatusEnum.Unknown
|
||||||
timestamp: int = field(default_factory=lambda: int(utcnow().timestamp()))
|
timestamp: int = field(default_factory=lambda: int(utcnow().timestamp()))
|
||||||
|
is_held: bool | None = field(default=None, kw_only=True)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -83,7 +85,7 @@ class BuildStatus:
|
|||||||
Returns:
|
Returns:
|
||||||
str: print-friendly string
|
str: print-friendly string
|
||||||
"""
|
"""
|
||||||
return f"{self.status.value} ({pretty_datetime(self.timestamp)})"
|
return f"{self.status.value} ({pretty_datetime(self.timestamp)}){" (held)" if self.is_held else ""}"
|
||||||
|
|
||||||
def view(self) -> dict[str, Any]:
|
def view(self) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -94,5 +96,6 @@ class BuildStatus:
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"status": self.status.value,
|
"status": self.status.value,
|
||||||
"timestamp": self.timestamp
|
"timestamp": self.timestamp,
|
||||||
|
"is_held": self.is_held,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,20 +31,12 @@ class Changes:
|
|||||||
Attributes:
|
Attributes:
|
||||||
last_commit_sha(str | None): last commit hash
|
last_commit_sha(str | None): last commit hash
|
||||||
changes(str | None): package change since the last commit if available
|
changes(str | None): package change since the last commit if available
|
||||||
|
pkgbuild(str | None): original PKGBUILD content if available
|
||||||
"""
|
"""
|
||||||
|
|
||||||
last_commit_sha: str | None = None
|
last_commit_sha: str | None = None
|
||||||
changes: str | None = None
|
changes: str | None = None
|
||||||
|
pkgbuild: str | None = None
|
||||||
@property
|
|
||||||
def is_empty(self) -> bool:
|
|
||||||
"""
|
|
||||||
validate that changes are not empty
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: ``True`` in case if changes are not set and ``False`` otherwise
|
|
||||||
"""
|
|
||||||
return self.changes is None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from pyalpm import vercmp # type: ignore[import-not-found]
|
|||||||
from typing import Any, Self
|
from typing import Any, Self
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
from ahriman.core.alpm.pacman import Pacman
|
||||||
|
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||||
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
|
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list
|
from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list
|
||||||
@@ -136,16 +137,6 @@ class Package(LazyLogging):
|
|||||||
"""
|
"""
|
||||||
return list_flatmap(self.packages.values(), lambda package: package.groups)
|
return list_flatmap(self.packages.values(), lambda package: package.groups)
|
||||||
|
|
||||||
@property
|
|
||||||
def is_single_package(self) -> bool:
|
|
||||||
"""
|
|
||||||
is it possible to transform package base to single package or not
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: true in case if this base has only one package with the same name
|
|
||||||
"""
|
|
||||||
return self.base in self.packages and len(self.packages) == 1
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_vcs(self) -> bool:
|
def is_vcs(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -186,18 +177,17 @@ class Package(LazyLogging):
|
|||||||
return sorted(packages)
|
return sorted(packages)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_archive(cls, path: Path, pacman: Pacman) -> Self:
|
def from_archive(cls, path: Path) -> Self:
|
||||||
"""
|
"""
|
||||||
construct package properties from package archive
|
construct package properties from package archive
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path(Path): path to package archive
|
path(Path): path to package archive
|
||||||
pacman(Pacman): alpm wrapper instance
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Self: package properties
|
Self: package properties
|
||||||
"""
|
"""
|
||||||
package = pacman.handle.load_pkg(str(path))
|
package = PacmanHandle.ephemeral().package_load(path)
|
||||||
description = PackageDescription.from_package(package, path)
|
description = PackageDescription.from_package(package, path)
|
||||||
return cls(
|
return cls(
|
||||||
base=package.base or package.name,
|
base=package.base or package.name,
|
||||||
@@ -375,9 +365,22 @@ class Package(LazyLogging):
|
|||||||
Returns:
|
Returns:
|
||||||
str: print-friendly string
|
str: print-friendly string
|
||||||
"""
|
"""
|
||||||
details = "" if self.is_single_package else f" ({" ".join(sorted(self.packages.keys()))})"
|
is_single_package = self.base in self.packages and len(self.packages) == 1
|
||||||
|
details = "" if is_single_package else f" ({" ".join(sorted(self.packages.keys()))})"
|
||||||
return f"{self.base}{details}"
|
return f"{self.base}{details}"
|
||||||
|
|
||||||
|
def supports_architecture(self, architecture: str) -> bool:
|
||||||
|
"""
|
||||||
|
helper to check if the package belongs to the specified architecture
|
||||||
|
|
||||||
|
Args:
|
||||||
|
architecture(str): probe repository architecture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` if all packages are same architecture or any
|
||||||
|
"""
|
||||||
|
return all(single.architecture in ("any", architecture) for single in self.packages.values())
|
||||||
|
|
||||||
def vercmp(self, version: str) -> int:
|
def vercmp(self, version: str) -> int:
|
||||||
"""
|
"""
|
||||||
typed wrapper around :func:`pyalpm.vercmp()`
|
typed wrapper around :func:`pyalpm.vercmp()`
|
||||||
@@ -400,17 +403,16 @@ class Package(LazyLogging):
|
|||||||
"""
|
"""
|
||||||
return dataclass_view(self)
|
return dataclass_view(self)
|
||||||
|
|
||||||
def with_packages(self, packages: Iterable[Path], pacman: Pacman) -> None:
|
def with_packages(self, packages: Iterable[Path]) -> None:
|
||||||
"""
|
"""
|
||||||
replace packages descriptions with ones from archives
|
replace packages descriptions with ones from archives
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
packages(Iterable[Path]): paths to package archives
|
packages(Iterable[Path]): paths to package archives
|
||||||
pacman(Pacman): alpm wrapper instance
|
|
||||||
"""
|
"""
|
||||||
self.packages = {} # reset state
|
self.packages = {} # reset state
|
||||||
for package in packages:
|
for package in packages:
|
||||||
archive = self.from_archive(package, pacman)
|
archive = self.from_archive(package)
|
||||||
if archive.base != self.base:
|
if archive.base != self.base:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from ahriman.web.schemas.error_schema import ErrorSchema
|
|||||||
from ahriman.web.schemas.event_schema import EventSchema
|
from ahriman.web.schemas.event_schema import EventSchema
|
||||||
from ahriman.web.schemas.event_search_schema import EventSearchSchema
|
from ahriman.web.schemas.event_search_schema import EventSearchSchema
|
||||||
from ahriman.web.schemas.file_schema import FileSchema
|
from ahriman.web.schemas.file_schema import FileSchema
|
||||||
|
from ahriman.web.schemas.hold_schema import HoldSchema
|
||||||
from ahriman.web.schemas.info_schema import InfoSchema
|
from ahriman.web.schemas.info_schema import InfoSchema
|
||||||
from ahriman.web.schemas.info_v2_schema import InfoV2Schema
|
from ahriman.web.schemas.info_v2_schema import InfoV2Schema
|
||||||
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||||
|
|||||||
@@ -32,3 +32,6 @@ class ChangesSchema(Schema):
|
|||||||
"description": "Last recorded commit hash",
|
"description": "Last recorded commit hash",
|
||||||
"example": "f1875edca1eb8fc0e55c41d1cae5fa05b6b7c6",
|
"example": "f1875edca1eb8fc0e55c41d1cae5fa05b6b7c6",
|
||||||
})
|
})
|
||||||
|
pkgbuild = fields.String(metadata={
|
||||||
|
"description": "Original PKGBUILD content",
|
||||||
|
})
|
||||||
|
|||||||
30
src/ahriman/web/schemas/hold_schema.py
Normal file
30
src/ahriman/web/schemas/hold_schema.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
from ahriman.web.apispec import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class HoldSchema(Schema):
|
||||||
|
"""
|
||||||
|
request hold schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_held = fields.Boolean(required=True, metadata={
|
||||||
|
"description": "Package hold status",
|
||||||
|
})
|
||||||
@@ -33,3 +33,6 @@ class StatusSchema(Schema):
|
|||||||
"description": "Last update timestamp",
|
"description": "Last update timestamp",
|
||||||
"example": 1680537091,
|
"example": 1680537091,
|
||||||
})
|
})
|
||||||
|
is_held = fields.Boolean(metadata={
|
||||||
|
"description": "Package hold status",
|
||||||
|
})
|
||||||
|
|||||||
65
src/ahriman/web/views/v1/packages/archives.py
Normal file
65
src/ahriman/web/views/v1/packages/archives.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
from aiohttp.web import Response
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
|
from ahriman.web.schemas import PackageNameSchema, PackageSchema, RepositoryIdSchema
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
|
class Archives(StatusViewGuard, BaseView):
|
||||||
|
"""
|
||||||
|
package archives web view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
|
||||||
|
ROUTES = ["/api/v1/packages/{package}/archives"]
|
||||||
|
|
||||||
|
@apidocs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Get package archives",
|
||||||
|
description="Retrieve built package archives for the base",
|
||||||
|
permission=GET_PERMISSION,
|
||||||
|
error_404_description="Package base and/or repository are unknown",
|
||||||
|
schema=PackageSchema(many=True),
|
||||||
|
match_schema=PackageNameSchema,
|
||||||
|
query_schema=RepositoryIdSchema,
|
||||||
|
)
|
||||||
|
async def get(self) -> Response:
|
||||||
|
"""
|
||||||
|
get package archives
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 200 with package archives on success
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPNotFound: if no package was found
|
||||||
|
"""
|
||||||
|
package_base = self.request.match_info["package"]
|
||||||
|
|
||||||
|
archives = self.service(package_base=package_base).package_archives(package_base)
|
||||||
|
|
||||||
|
return self.json_response([archive.view() for archive in archives])
|
||||||
@@ -92,10 +92,11 @@ class ChangesView(StatusViewGuard, BaseView):
|
|||||||
data = await self.request.json()
|
data = await self.request.json()
|
||||||
last_commit_sha = data.get("last_commit_sha") # empty/null meant removal
|
last_commit_sha = data.get("last_commit_sha") # empty/null meant removal
|
||||||
change = data.get("changes")
|
change = data.get("changes")
|
||||||
|
pkgbuild = data.get("pkgbuild")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise HTTPBadRequest(reason=str(ex))
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
changes = Changes(last_commit_sha, change)
|
changes = Changes(last_commit_sha, change, pkgbuild)
|
||||||
self.service().package_changes_update(package_base, changes)
|
self.service().package_changes_update(package_base, changes)
|
||||||
|
|
||||||
raise HTTPNoContent
|
raise HTTPNoContent
|
||||||
|
|||||||
75
src/ahriman/web/views/v1/packages/hold.py
Normal file
75
src/ahriman/web/views/v1/packages/hold.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
|
from ahriman.web.schemas import HoldSchema, PackageNameSchema, RepositoryIdSchema
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
|
class HoldView(StatusViewGuard, BaseView):
|
||||||
|
"""
|
||||||
|
package hold web view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
|
||||||
|
ROUTES = ["/api/v1/packages/{package}/hold"]
|
||||||
|
|
||||||
|
@apidocs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Update package hold status",
|
||||||
|
description="Set package hold status",
|
||||||
|
permission=POST_PERMISSION,
|
||||||
|
error_400_enabled=True,
|
||||||
|
error_404_description="Package base and/or repository are unknown",
|
||||||
|
match_schema=PackageNameSchema,
|
||||||
|
query_schema=RepositoryIdSchema,
|
||||||
|
body_schema=HoldSchema,
|
||||||
|
)
|
||||||
|
async def post(self) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPBadRequest: if bad data is supplied
|
||||||
|
HTTPNoContent: in case of success response
|
||||||
|
HTTPNotFound: if no package was found
|
||||||
|
"""
|
||||||
|
package_base = self.request.match_info["package"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await self.request.json()
|
||||||
|
is_held = data["is_held"]
|
||||||
|
except Exception as ex:
|
||||||
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.service().package_hold_update(package_base, enabled=is_held)
|
||||||
|
except UnknownPackageError:
|
||||||
|
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
||||||
|
|
||||||
|
raise HTTPNoContent
|
||||||
@@ -23,12 +23,14 @@ import logging
|
|||||||
import socket
|
import socket
|
||||||
|
|
||||||
from aiohttp.web import Application, normalize_path_middleware, run_app
|
from aiohttp.web import Application, normalize_path_middleware, run_app
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from ahriman.core.auth import Auth
|
from ahriman.core.auth import Auth
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.database import SQLite
|
from ahriman.core.database import SQLite
|
||||||
from ahriman.core.distributed import WorkersCache
|
from ahriman.core.distributed import WorkersCache
|
||||||
from ahriman.core.exceptions import InitializeError
|
from ahriman.core.exceptions import InitializeError
|
||||||
|
from ahriman.core.repository.package_info import PackageInfo
|
||||||
from ahriman.core.spawn import Spawn
|
from ahriman.core.spawn import Spawn
|
||||||
from ahriman.core.status import Client
|
from ahriman.core.status import Client
|
||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
@@ -78,6 +80,35 @@ def _create_socket(configuration: Configuration, application: Application) -> so
|
|||||||
return sock
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _create_watcher(path: Path, repository_id: RepositoryId) -> Watcher:
|
||||||
|
"""
|
||||||
|
build watcher for selected repository
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path(Path): path to configuration file
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Watcher: watcher instance
|
||||||
|
"""
|
||||||
|
logging.getLogger(__name__).info("load repository %s", repository_id)
|
||||||
|
# load settings explicitly for architecture if any
|
||||||
|
configuration = Configuration.from_path(path, repository_id)
|
||||||
|
|
||||||
|
# load database instance, because it holds identifier
|
||||||
|
database = SQLite.load(configuration)
|
||||||
|
# explicitly load local client
|
||||||
|
client = Client.load(repository_id, configuration, database, report=False)
|
||||||
|
|
||||||
|
# load package info wrapper
|
||||||
|
package_info = PackageInfo()
|
||||||
|
package_info.configuration = configuration
|
||||||
|
package_info.paths = configuration.repository_paths
|
||||||
|
package_info.repository_id = repository_id
|
||||||
|
|
||||||
|
return Watcher(client, package_info)
|
||||||
|
|
||||||
|
|
||||||
async def _on_shutdown(application: Application) -> None:
|
async def _on_shutdown(application: Application) -> None:
|
||||||
"""
|
"""
|
||||||
web application shutdown handler
|
web application shutdown handler
|
||||||
@@ -168,18 +199,11 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
|
|||||||
# package cache
|
# package cache
|
||||||
if not repositories:
|
if not repositories:
|
||||||
raise InitializeError("No repositories configured, exiting")
|
raise InitializeError("No repositories configured, exiting")
|
||||||
watchers: dict[RepositoryId, Watcher] = {}
|
|
||||||
configuration_path, _ = configuration.check_loaded()
|
configuration_path, _ = configuration.check_loaded()
|
||||||
for repository_id in repositories:
|
application[WatcherKey] = {
|
||||||
application.logger.info("load repository %s", repository_id)
|
repository_id: _create_watcher(configuration_path, repository_id)
|
||||||
# load settings explicitly for architecture if any
|
for repository_id in repositories
|
||||||
repository_configuration = Configuration.from_path(configuration_path, repository_id)
|
}
|
||||||
# load database instance, because it holds identifier
|
|
||||||
database = SQLite.load(repository_configuration)
|
|
||||||
# explicitly load local client
|
|
||||||
client = Client.load(repository_id, repository_configuration, database, report=False)
|
|
||||||
watchers[repository_id] = Watcher(client)
|
|
||||||
application[WatcherKey] = watchers
|
|
||||||
# workers cache
|
# workers cache
|
||||||
application[WorkersKey] = WorkersCache(configuration)
|
application[WorkersKey] = WorkersCache(configuration)
|
||||||
# process spawner
|
# process spawner
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def test_changes(application_repository: ApplicationRepository, package_ahriman:
|
|||||||
"""
|
"""
|
||||||
must generate changes for the packages
|
must generate changes for the packages
|
||||||
"""
|
"""
|
||||||
changes = Changes("hash", "change")
|
changes = Changes("sha", "change")
|
||||||
hashes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
hashes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
||||||
changes_mock = mocker.patch("ahriman.core.repository.Repository.package_changes", return_value=changes)
|
changes_mock = mocker.patch("ahriman.core.repository.Repository.package_changes", return_value=changes)
|
||||||
report_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
report_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||||
@@ -42,6 +42,20 @@ def test_changes_skip(application_repository: ApplicationRepository, package_ahr
|
|||||||
report_mock.assert_not_called()
|
report_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_changes_no_update(application_repository: ApplicationRepository, package_ahriman: Package,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must skip update if package_changes returns None (no new commits)
|
||||||
|
"""
|
||||||
|
changes = Changes("sha", "change")
|
||||||
|
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.package_changes", return_value=None)
|
||||||
|
report_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||||
|
|
||||||
|
application_repository.changes([package_ahriman])
|
||||||
|
report_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must clean cache directory
|
must clean cache directory
|
||||||
|
|||||||
84
tests/ahriman/application/handlers/test_handler_archives.py
Normal file
84
tests/ahriman/application/handlers/test_handler_archives.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import argparse
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from ahriman.application.handlers.archives import Archives
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.database import SQLite
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
|
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
default arguments for these test cases
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line arguments fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.Namespace: generated arguments for these test cases
|
||||||
|
"""
|
||||||
|
args.action = Action.List
|
||||||
|
args.exit_code = False
|
||||||
|
args.info = False
|
||||||
|
args.package = "package"
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must run command
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
application_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
|
||||||
|
return_value=[package_ahriman])
|
||||||
|
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||||
|
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Archives.run(args, repository_id, configuration, report=False)
|
||||||
|
application_mock.assert_called_once_with(args.package)
|
||||||
|
check_mock.assert_called_once_with(False, True)
|
||||||
|
print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ")
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must raise ExitCode exception on empty archives result
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
args.exit_code = True
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
|
||||||
|
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Archives.run(args, repository_id, configuration, report=False)
|
||||||
|
check_mock.assert_called_once_with(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must create application object with native reporting
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
||||||
|
load_mock = mocker.patch("ahriman.core.repository.Repository.load")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Archives.run(args, repository_id, configuration, report=False)
|
||||||
|
load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disallow_multi_architecture_run() -> None:
|
||||||
|
"""
|
||||||
|
must not allow multi architecture run
|
||||||
|
"""
|
||||||
|
assert not Archives.ALLOW_MULTI_ARCHITECTURE_RUN
|
||||||
53
tests/ahriman/application/handlers/test_handler_hold.py
Normal file
53
tests/ahriman/application/handlers/test_handler_hold.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import argparse
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from ahriman.application.handlers.hold import Hold
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
|
||||||
|
|
||||||
|
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
default arguments for these test cases
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line arguments fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.Namespace: generated arguments for these test cases
|
||||||
|
"""
|
||||||
|
args.package = ["ahriman"]
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must run command
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
args.action = Action.Update
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Hold.run(args, repository_id, configuration, report=False)
|
||||||
|
hold_mock.assert_called_once_with("ahriman", enabled=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must remove held status
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
args.action = Action.Remove
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Hold.run(args, repository_id, configuration, report=False)
|
||||||
|
hold_mock.assert_called_once_with("ahriman", enabled=False)
|
||||||
100
tests/ahriman/application/handlers/test_handler_pkgbuild.py
Normal file
100
tests/ahriman/application/handlers/test_handler_pkgbuild.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import argparse
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from ahriman.application.handlers.pkgbuild import Pkgbuild
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.database import SQLite
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
|
|
||||||
|
|
||||||
|
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
default arguments for these test cases
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line arguments fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.Namespace: generated arguments for these test cases
|
||||||
|
"""
|
||||||
|
args.action = Action.List
|
||||||
|
args.exit_code = False
|
||||||
|
args.package = "package"
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must run command
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
application_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
|
||||||
|
return_value=Changes("sha", "change", "pkgbuild content"))
|
||||||
|
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||||
|
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||||
|
application_mock.assert_called_once_with(args.package)
|
||||||
|
check_mock.assert_called_once_with(False, True)
|
||||||
|
print_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must raise ExitCode exception on empty pkgbuild result
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
args.exit_code = True
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=Changes())
|
||||||
|
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||||
|
check_mock.assert_called_once_with(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must remove package pkgbuild
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
args.action = Action.Remove
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
changes = Changes("sha", "change", "pkgbuild content")
|
||||||
|
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
||||||
|
update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||||
|
update_mock.assert_called_once_with(args.package, Changes("sha", "change", None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must create application object with native reporting
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
||||||
|
load_mock = mocker.patch("ahriman.core.repository.Repository.load")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||||
|
load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disallow_multi_architecture_run() -> None:
|
||||||
|
"""
|
||||||
|
must not allow multi architecture run
|
||||||
|
"""
|
||||||
|
assert not Pkgbuild.ALLOW_MULTI_ARCHITECTURE_RUN
|
||||||
@@ -271,6 +271,22 @@ def test_subparsers_package_add_option_variable_multiple(parser: argparse.Argume
|
|||||||
assert args.variable == ["var1", "var2"]
|
assert args.variable == ["var1", "var2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_archives(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
package-archives command must imply action, exit code, info, lock, quiet, report and unsafe
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-archives", "ahriman"])
|
||||||
|
assert args.action == Action.List
|
||||||
|
assert args.architecture == "x86_64"
|
||||||
|
assert not args.exit_code
|
||||||
|
assert not args.info
|
||||||
|
assert args.lock is None
|
||||||
|
assert args.quiet
|
||||||
|
assert not args.report
|
||||||
|
assert args.repository == "repo"
|
||||||
|
assert args.unsafe
|
||||||
|
|
||||||
|
|
||||||
def test_subparsers_package_changes(parser: argparse.ArgumentParser) -> None:
|
def test_subparsers_package_changes(parser: argparse.ArgumentParser) -> None:
|
||||||
"""
|
"""
|
||||||
package-changes command must imply action, exit code, lock, quiet, report and unsafe
|
package-changes command must imply action, exit code, lock, quiet, report and unsafe
|
||||||
@@ -309,6 +325,44 @@ def test_subparsers_package_changes_remove_package_changes(parser: argparse.Argu
|
|||||||
assert dir(args) == dir(reference_args)
|
assert dir(args) == dir(reference_args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_pkgbuild(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
package-pkgbuild command must imply action, exit code, lock, quiet, report and unsafe
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild", "ahriman"])
|
||||||
|
assert args.action == Action.List
|
||||||
|
assert args.architecture == "x86_64"
|
||||||
|
assert not args.exit_code
|
||||||
|
assert args.lock is None
|
||||||
|
assert args.quiet
|
||||||
|
assert not args.report
|
||||||
|
assert args.repository == "repo"
|
||||||
|
assert args.unsafe
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_pkgbuild_remove(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
package-pkgbuild-remove command must imply action, lock, quiet, report and unsafe
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild-remove", "ahriman"])
|
||||||
|
assert args.action == Action.Remove
|
||||||
|
assert args.architecture == "x86_64"
|
||||||
|
assert args.lock is None
|
||||||
|
assert args.quiet
|
||||||
|
assert not args.report
|
||||||
|
assert args.repository == "repo"
|
||||||
|
assert args.unsafe
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_pkgbuild_remove_package_pkgbuild(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
package-pkgbuild-remove must have same keys as package-pkgbuild
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild-remove", "ahriman"])
|
||||||
|
reference_args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild", "ahriman"])
|
||||||
|
assert dir(args) == dir(reference_args)
|
||||||
|
|
||||||
|
|
||||||
def test_subparsers_package_copy_option_architecture(parser: argparse.ArgumentParser) -> None:
|
def test_subparsers_package_copy_option_architecture(parser: argparse.ArgumentParser) -> None:
|
||||||
"""
|
"""
|
||||||
package-copy command must correctly parse architecture list
|
package-copy command must correctly parse architecture list
|
||||||
@@ -410,6 +464,30 @@ def test_subparsers_package_status_update_package_status_remove(parser: argparse
|
|||||||
assert dir(args) == dir(reference_args)
|
assert dir(args) == dir(reference_args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_hold(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
package-hold command must imply action, lock, quiet, report and unsafe
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["package-hold", "ahriman"])
|
||||||
|
assert args.action == Action.Update
|
||||||
|
assert args.lock is None
|
||||||
|
assert args.quiet
|
||||||
|
assert not args.report
|
||||||
|
assert args.unsafe
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_unhold(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
package-unhold command must imply action, lock, quiet, report and unsafe
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["package-unhold", "ahriman"])
|
||||||
|
assert args.action == Action.Remove
|
||||||
|
assert args.lock is None
|
||||||
|
assert args.quiet
|
||||||
|
assert not args.report
|
||||||
|
assert args.unsafe
|
||||||
|
|
||||||
|
|
||||||
def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None:
|
def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None:
|
||||||
"""
|
"""
|
||||||
patch-add command must imply action, architecture list, exit code, lock, report and repository
|
patch-add command must imply action, architecture list, exit code, lock, report and repository
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ahriman.core.database import SQLite
|
|||||||
from ahriman.core.database.migrations import Migrations
|
from ahriman.core.database.migrations import Migrations
|
||||||
from ahriman.core.log.log_loader import LogLoader
|
from ahriman.core.log.log_loader import LogLoader
|
||||||
from ahriman.core.repository import Repository
|
from ahriman.core.repository import Repository
|
||||||
|
from ahriman.core.repository.package_info import PackageInfo
|
||||||
from ahriman.core.spawn import Spawn
|
from ahriman.core.spawn import Spawn
|
||||||
from ahriman.core.status import Client
|
from ahriman.core.status import Client
|
||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
@@ -688,4 +689,5 @@ def watcher(local_client: Client) -> Watcher:
|
|||||||
Returns:
|
Returns:
|
||||||
Watcher: package status watcher test instance
|
Watcher: package status watcher test instance
|
||||||
"""
|
"""
|
||||||
return Watcher(local_client)
|
package_info = PackageInfo()
|
||||||
|
return Watcher(local_client, package_info)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user