mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-03-12 04:53:39 +00:00
Compare commits
6 Commits
2.20.0
...
feature/ar
| Author | SHA1 | Date | |
|---|---|---|---|
| 82c2218d08 | |||
| 81aeb56ba3 | |||
| 2cd4ef5e86 | |||
| 998ed48dde | |||
| 021d88dc4c | |||
| 9012ee7144 |
@@ -92,6 +92,14 @@ ahriman.application.handlers.patch module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.application.handlers.pkgbuild module
|
||||
--------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.application.handlers.pkgbuild
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.application.handlers.rebuild module
|
||||
-------------------------------------------
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ ahriman.core.alpm.pacman\_database module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
-----------------------------------------
|
||||
|
||||
|
||||
@@ -140,6 +140,14 @@ ahriman.core.database.migrations.m016\_archive module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.database.migrations.m017\_pkgbuild module
|
||||
------------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.database.migrations.m017_pkgbuild
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
||||
@@ -76,6 +76,14 @@ ahriman.core.formatters.patch\_printer module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
--------------------------------------
|
||||
|
||||
|
||||
@@ -17,16 +17,14 @@
|
||||
* 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 } from "@mui/material/styles";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import AppLayout from "components/layout/AppLayout";
|
||||
import { AuthProvider } from "contexts/AuthProvider";
|
||||
import { ClientProvider } from "contexts/ClientProvider";
|
||||
import { NotificationProvider } from "contexts/NotificationProvider";
|
||||
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import type React from "react";
|
||||
import Theme from "theme/Theme";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -39,8 +37,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
export default function App(): React.JSX.Element {
|
||||
return <QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={Theme}>
|
||||
<CssBaseline />
|
||||
<ThemeProvider>
|
||||
<NotificationProvider>
|
||||
<ClientProvider>
|
||||
<AuthProvider>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* 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 { blue } from "@mui/material/colors";
|
||||
import type { Event } from "models/Event";
|
||||
import type React from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
@@ -33,6 +34,8 @@ export default function EventDurationLineChart({ events }: EventDurationLineChar
|
||||
{
|
||||
label: "update duration, s",
|
||||
data: updateEvents.map(event => event.data?.took ?? 0),
|
||||
borderColor: blue[500],
|
||||
backgroundColor: blue[200],
|
||||
cubicInterpolationMode: "monotone" as const,
|
||||
tension: 0.4,
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ export default function AutoRefreshControl({
|
||||
<Tooltip title="Auto-refresh">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Auto-refresh"
|
||||
onClick={event => setAnchorEl(event.currentTarget)}
|
||||
color={enabled ? "primary" : "default"}
|
||||
>
|
||||
|
||||
@@ -17,47 +17,57 @@
|
||||
* 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 { Box } from "@mui/material";
|
||||
import "components/common/syntaxLanguages";
|
||||
|
||||
import { Box, useTheme } from "@mui/material";
|
||||
import CopyButton from "components/common/CopyButton";
|
||||
import { useThemeMode } from "hooks/useThemeMode";
|
||||
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 {
|
||||
preRef?: RefObject<HTMLElement | null>;
|
||||
getText: () => string;
|
||||
content: string;
|
||||
height?: number | string;
|
||||
language?: string;
|
||||
onScroll?: () => void;
|
||||
wordBreak?: boolean;
|
||||
preRef?: RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export default function CodeBlock({
|
||||
preRef,
|
||||
getText,
|
||||
content,
|
||||
height,
|
||||
language = "text",
|
||||
onScroll,
|
||||
wordBreak,
|
||||
preRef,
|
||||
}: CodeBlockProps): React.JSX.Element {
|
||||
const { mode } = useThemeMode();
|
||||
const theme = useTheme();
|
||||
|
||||
return <Box sx={{ position: "relative" }}>
|
||||
<Box
|
||||
ref={preRef}
|
||||
component="pre"
|
||||
onScroll={onScroll}
|
||||
sx={{
|
||||
backgroundColor: "grey.100",
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: "auto",
|
||||
height,
|
||||
fontSize: "0.8rem",
|
||||
fontFamily: "monospace",
|
||||
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
|
||||
}}
|
||||
sx={{ overflow: "auto", height }}
|
||||
>
|
||||
<code>
|
||||
{getText()}
|
||||
</code>
|
||||
</Box>
|
||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||
<CopyButton getText={getText} />
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={mode === "dark" ? vs2015 : githubGist}
|
||||
wrapLongLines
|
||||
customStyle={{
|
||||
padding: theme.spacing(2),
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
fontSize: "0.8rem",
|
||||
fontFamily: "monospace",
|
||||
margin: 0,
|
||||
minHeight: "100%",
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</SyntaxHighlighter>
|
||||
</Box>
|
||||
{content && <Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||
<CopyButton text={content} />
|
||||
</Box>}
|
||||
</Box>;
|
||||
}
|
||||
|
||||
@@ -23,17 +23,17 @@ import { IconButton, Tooltip } from "@mui/material";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
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 timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
useEffect(() => () => clearTimeout(timer.current), []);
|
||||
|
||||
const handleCopy: () => Promise<void> = async () => {
|
||||
await navigator.clipboard.writeText(getText());
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => setCopied(false), 2000);
|
||||
|
||||
@@ -25,12 +25,12 @@ import type React from "react";
|
||||
export default function RepositorySelect({
|
||||
repositorySelect,
|
||||
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
||||
const { repositories, current } = useRepository();
|
||||
const { repositories, currentRepository } = useRepository();
|
||||
|
||||
return <FormControl fullWidth margin="normal">
|
||||
<InputLabel>repository</InputLabel>
|
||||
<Select
|
||||
value={repositorySelect.selectedKey || (current?.key ?? "")}
|
||||
value={repositorySelect.selectedKey || (currentRepository?.key ?? "")}
|
||||
label="repository"
|
||||
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 {
|
||||
const client = useClient();
|
||||
const { current } = useRepository();
|
||||
const { currentRepository } = useRepository();
|
||||
|
||||
const { data: status } = useQuery<InternalStatus>({
|
||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
||||
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
||||
/>
|
||||
{keyBody &&
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<CodeBlock getText={() => keyBody} height={300} />
|
||||
<CodeBlock content={keyBody} height={300} />
|
||||
</Box>
|
||||
}
|
||||
</DialogContent>
|
||||
|
||||
@@ -27,6 +27,8 @@ import EventsTab from "components/package/EventsTab";
|
||||
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
||||
import PackageInfoActions from "components/package/PackageInfoActions";
|
||||
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 { useAuth } from "hooks/useAuth";
|
||||
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
||||
@@ -55,7 +57,7 @@ export default function PackageInfoDialog({
|
||||
autoRefreshIntervals,
|
||||
}: PackageInfoDialogProps): React.JSX.Element {
|
||||
const client = useClient();
|
||||
const { current } = useRepository();
|
||||
const { currentRepository } = useRepository();
|
||||
const { isAuthorized } = useAuth();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -65,11 +67,11 @@ export default function PackageInfoDialog({
|
||||
setLocalPackageBase(packageBase);
|
||||
}
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("logs");
|
||||
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||
|
||||
const handleClose = (): void => {
|
||||
setTabIndex(0);
|
||||
setActiveTab("logs");
|
||||
setRefreshDatabase(true);
|
||||
onClose();
|
||||
};
|
||||
@@ -77,16 +79,17 @@ export default function PackageInfoDialog({
|
||||
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||
|
||||
const { data: packageData } = useQuery<PackageStatus[]>({
|
||||
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
|
||||
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
|
||||
queryKey: localPackageBase && currentRepository ? QueryKeys.package(localPackageBase, currentRepository) : ["packages"],
|
||||
queryFn: localPackageBase && currentRepository ?
|
||||
() => client.fetch.fetchPackage(localPackageBase, currentRepository) : skipToken,
|
||||
enabled: open,
|
||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||
});
|
||||
|
||||
const { data: dependencies } = useQuery<Dependencies>({
|
||||
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
|
||||
queryFn: localPackageBase && current
|
||||
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
|
||||
queryKey: localPackageBase && currentRepository ? QueryKeys.dependencies(localPackageBase, currentRepository) : ["dependencies"],
|
||||
queryFn: localPackageBase && currentRepository ?
|
||||
() => client.fetch.fetchPackageDependencies(localPackageBase, currentRepository) : skipToken,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
@@ -102,11 +105,12 @@ export default function PackageInfoDialog({
|
||||
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
||||
|
||||
const handleUpdate: () => Promise<void> = async () => {
|
||||
if (!localPackageBase || !current) {
|
||||
if (!localPackageBase || !currentRepository) {
|
||||
return;
|
||||
}
|
||||
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}`);
|
||||
} catch (exception) {
|
||||
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
||||
@@ -114,11 +118,11 @@ export default function PackageInfoDialog({
|
||||
};
|
||||
|
||||
const handleRemove: () => Promise<void> = async () => {
|
||||
if (!localPackageBase || !current) {
|
||||
if (!localPackageBase || !currentRepository) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.service.servicePackageRemove(current, [localPackageBase]);
|
||||
await client.service.servicePackageRemove(currentRepository, [localPackageBase]);
|
||||
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
||||
onClose();
|
||||
} catch (exception) {
|
||||
@@ -156,25 +160,26 @@ export default function PackageInfoDialog({
|
||||
/>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
||||
<Tabs value={tabIndex} onChange={(_, index: number) => setTabIndex(index)}>
|
||||
<Tab label="Build logs" />
|
||||
<Tab label="Changes" />
|
||||
<Tab label="Events" />
|
||||
<Tabs value={activeTab} onChange={(_, tab: TabKey) => setActiveTab(tab)}>
|
||||
{tabs.map(({ key, label }) => <Tab key={key} value={key} label={label} />)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{tabIndex === 0 && localPackageBase && current &&
|
||||
{activeTab === "logs" && localPackageBase && currentRepository &&
|
||||
<BuildLogsTab
|
||||
packageBase={localPackageBase}
|
||||
repository={current}
|
||||
repository={currentRepository}
|
||||
refreshInterval={autoRefresh.interval}
|
||||
/>
|
||||
}
|
||||
{tabIndex === 1 && localPackageBase && current &&
|
||||
<ChangesTab packageBase={localPackageBase} repository={current} />
|
||||
{activeTab === "changes" && localPackageBase && currentRepository &&
|
||||
<ChangesTab packageBase={localPackageBase} repository={currentRepository} />
|
||||
}
|
||||
{tabIndex === 2 && localPackageBase && current &&
|
||||
<EventsTab packageBase={localPackageBase} repository={current} />
|
||||
{activeTab === "pkgbuild" && localPackageBase && currentRepository &&
|
||||
<PkgbuildTab packageBase={localPackageBase} repository={currentRepository} />
|
||||
}
|
||||
{activeTab === "events" && localPackageBase && currentRepository &&
|
||||
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
* 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 { 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 LoginDialog from "components/dialogs/LoginDialog";
|
||||
import Footer from "components/layout/Footer";
|
||||
@@ -27,6 +29,7 @@ import { QueryKeys } from "hooks/QueryKeys";
|
||||
import { useAuth } from "hooks/useAuth";
|
||||
import { useClient } from "hooks/useClient";
|
||||
import { useRepository } from "hooks/useRepository";
|
||||
import { useThemeMode } from "hooks/useThemeMode";
|
||||
import type { InfoResponse } from "models/InfoResponse";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
@@ -34,6 +37,7 @@ export default function AppLayout(): React.JSX.Element {
|
||||
const client = useClient();
|
||||
const { setAuthState } = useAuth();
|
||||
const { setRepositories } = useRepository();
|
||||
const { mode, toggleTheme } = useThemeMode();
|
||||
const [loginOpen, setLoginOpen] = useState(false);
|
||||
|
||||
const { data: info } = useQuery<InfoResponse>({
|
||||
@@ -58,6 +62,11 @@ export default function AppLayout(): React.JSX.Element {
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Navbar />
|
||||
</Box>
|
||||
<Tooltip title="Toggle theme">
|
||||
<IconButton aria-label="Toggle theme" onClick={toggleTheme}>
|
||||
{mode === "dark" ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<PackageTable
|
||||
|
||||
@@ -22,14 +22,15 @@ import { useRepository } from "hooks/useRepository";
|
||||
import type React from "react";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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" }}>
|
||||
@@ -38,7 +39,7 @@ export default function Navbar(): React.JSX.Element | null {
|
||||
onChange={(_, newValue: number) => {
|
||||
const repository = repositories[newValue];
|
||||
if (repository) {
|
||||
setCurrent(repository);
|
||||
setCurrentRepository(repository);
|
||||
}
|
||||
}}
|
||||
variant="scrollable"
|
||||
|
||||
@@ -175,10 +175,9 @@ export default function BuildLogsTab({
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<CodeBlock
|
||||
preRef={preRef}
|
||||
getText={() => displayedLogs}
|
||||
content={displayedLogs}
|
||||
height={400}
|
||||
onScroll={handleScroll}
|
||||
wordBreak
|
||||
/>
|
||||
</Box>
|
||||
</Box>;
|
||||
|
||||
@@ -17,19 +17,10 @@
|
||||
* 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 { Box } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import CopyButton from "components/common/CopyButton";
|
||||
import { QueryKeys } from "hooks/QueryKeys";
|
||||
import { useClient } from "hooks/useClient";
|
||||
import type { Changes } from "models/Changes";
|
||||
import CodeBlock from "components/common/CodeBlock";
|
||||
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||
import type { RepositoryId } from "models/RepositoryId";
|
||||
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 {
|
||||
packageBase: string;
|
||||
@@ -37,34 +28,7 @@ interface ChangesTabProps {
|
||||
}
|
||||
|
||||
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
||||
const client = useClient();
|
||||
const data = usePackageChanges(packageBase, repository);
|
||||
|
||||
const { data } = useQuery<Changes>({
|
||||
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>;
|
||||
return <CodeBlock language="diff" content={data?.changes ?? ""} height={400} />;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function PackagePatchesList({
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
{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" />
|
||||
</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" },
|
||||
];
|
||||
@@ -22,9 +22,9 @@ import { createContext } from "react";
|
||||
|
||||
export interface RepositoryContextValue {
|
||||
repositories: RepositoryId[];
|
||||
current: RepositoryId | null;
|
||||
currentRepository: RepositoryId | null;
|
||||
setRepositories: (repositories: RepositoryId[]) => void;
|
||||
setCurrent: (repository: RepositoryId) => void;
|
||||
setCurrentRepository: (repository: RepositoryId) => void;
|
||||
}
|
||||
|
||||
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
||||
|
||||
@@ -34,20 +34,20 @@ export function RepositoryProvider({ children }: { children: ReactNode }): React
|
||||
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
||||
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
||||
|
||||
const current = useMemo(() => {
|
||||
const currentRepository = useMemo(() => {
|
||||
if (repositories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
||||
}, [repositories, hash]);
|
||||
|
||||
const setCurrent = useCallback((repository: RepositoryId) => {
|
||||
const setCurrentRepository = useCallback((repository: RepositoryId) => {
|
||||
window.location.hash = repository.key;
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
repositories, current, setRepositories, setCurrent,
|
||||
}), [repositories, current, setCurrent]);
|
||||
repositories, currentRepository, setRepositories, setCurrentRepository,
|
||||
}), [repositories, currentRepository, setCurrentRepository]);
|
||||
|
||||
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,
|
||||
): UsePackageActionsResult {
|
||||
const client = useClient();
|
||||
const { current } = useRepository();
|
||||
const { currentRepository } = useRepository();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -50,13 +50,13 @@ export function usePackageActions(
|
||||
action: (repository: RepositoryId) => Promise<string>,
|
||||
errorMessage: string,
|
||||
): Promise<void> => {
|
||||
if (!current) {
|
||||
if (!currentRepository) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const successMessage = await action(current);
|
||||
const successMessage = await action(currentRepository);
|
||||
showSuccess("Success", successMessage);
|
||||
invalidate(current);
|
||||
invalidate(currentRepository);
|
||||
setSelectionModel([]);
|
||||
} catch (exception) {
|
||||
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
||||
@@ -64,8 +64,8 @@ export function usePackageActions(
|
||||
};
|
||||
|
||||
const handleReload: () => void = () => {
|
||||
if (current !== null) {
|
||||
invalidate(current);
|
||||
if (currentRepository !== null) {
|
||||
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 {
|
||||
const client = useClient();
|
||||
const { current } = useRepository();
|
||||
const { currentRepository } = useRepository();
|
||||
const { isAuthorized } = useAuth();
|
||||
|
||||
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||
|
||||
const { data: packages = [], isLoading } = useQuery({
|
||||
queryKey: current ? QueryKeys.packages(current) : ["packages"],
|
||||
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
|
||||
queryKey: currentRepository ? QueryKeys.packages(currentRepository) : ["packages"],
|
||||
queryFn: currentRepository ? () => client.fetch.fetchPackages(currentRepository) : skipToken,
|
||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||
});
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
||||
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||
});
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ export interface SelectedRepositoryResult {
|
||||
}
|
||||
|
||||
export function useSelectedRepository(): SelectedRepositoryResult {
|
||||
const { repositories, current } = useRepository();
|
||||
const { repositories, currentRepository } = useRepository();
|
||||
const [selectedKey, setSelectedKey] = useState("");
|
||||
|
||||
let selectedRepository: RepositoryId | null = current;
|
||||
let selectedRepository: RepositoryId | null = currentRepository;
|
||||
if (selectedKey) {
|
||||
const repository = repositories.find(repository => repository.key === selectedKey);
|
||||
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 {
|
||||
changes?: string;
|
||||
last_commit_sha?: string;
|
||||
pkgbuild?: string;
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@ import { amber, green, grey, orange, red } from "@mui/material/colors";
|
||||
import type { BuildStatus } from "models/BuildStatus";
|
||||
|
||||
const base: Record<BuildStatus, string> = {
|
||||
unknown: grey[800],
|
||||
pending: amber[900],
|
||||
building: orange[900],
|
||||
failed: red[900],
|
||||
success: green[800],
|
||||
unknown: grey[600],
|
||||
pending: amber[700],
|
||||
building: orange[800],
|
||||
failed: red[700],
|
||||
success: green[700],
|
||||
};
|
||||
|
||||
const headerBase: Record<BuildStatus, string> = {
|
||||
unknown: grey[800],
|
||||
unknown: grey[600],
|
||||
pending: amber[700],
|
||||
building: orange[600],
|
||||
failed: red[500],
|
||||
|
||||
@@ -17,17 +17,20 @@
|
||||
* 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 { createTheme } from "@mui/material/styles";
|
||||
import { createTheme, type Theme } from "@mui/material/styles";
|
||||
|
||||
const Theme = createTheme({
|
||||
components: {
|
||||
MuiDialog: {
|
||||
defaultProps: {
|
||||
maxWidth: "lg",
|
||||
fullWidth: true,
|
||||
export function createAppTheme(mode: "light" | "dark"): Theme {
|
||||
return createTheme({
|
||||
palette: {
|
||||
mode,
|
||||
},
|
||||
components: {
|
||||
MuiDialog: {
|
||||
defaultProps: {
|
||||
maxWidth: "lg",
|
||||
fullWidth: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default Theme;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ class ApplicationRepository(ApplicationProperties):
|
||||
if last_commit_sha is None:
|
||||
continue # skip check in case if we can't calculate diff
|
||||
|
||||
changes = self.repository.package_changes(package, last_commit_sha)
|
||||
self.repository.reporter.package_changes_update(package.base, changes)
|
||||
if (changes := self.repository.package_changes(package, last_commit_sha)) is not None:
|
||||
self.repository.reporter.package_changes_update(package.base, changes)
|
||||
|
||||
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
||||
"""
|
||||
|
||||
@@ -54,7 +54,7 @@ class Change(Handler):
|
||||
case Action.List:
|
||||
changes = client.package_changes_get(args.package)
|
||||
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:
|
||||
client.package_changes_update(args.package, Changes())
|
||||
|
||||
|
||||
101
src/ahriman/application/handlers/pkgbuild.py
Normal file
101
src/ahriman/application/handlers/pkgbuild.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#
|
||||
# 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
|
||||
"""
|
||||
application = Application(repository_id, configuration, report=True)
|
||||
client = application.repository.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]
|
||||
@@ -24,10 +24,11 @@ import tarfile
|
||||
from collections.abc import Iterable, Iterator
|
||||
from functools import cached_property
|
||||
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 ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import trim_package
|
||||
@@ -61,16 +62,16 @@ class Pacman(LazyLogging):
|
||||
self.refresh_database = refresh_database
|
||||
|
||||
@cached_property
|
||||
def handle(self) -> Handle:
|
||||
def handle(self) -> PacmanHandle:
|
||||
"""
|
||||
pyalpm handle
|
||||
|
||||
Returns:
|
||||
Handle: generated pyalpm handle instance
|
||||
PacmanHandle: generated pyalpm handle instance
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -78,14 +79,14 @@ class Pacman(LazyLogging):
|
||||
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||
|
||||
Returns:
|
||||
Handle: fully initialized pacman handle
|
||||
PacmanHandle: fully initialized pacman handle
|
||||
"""
|
||||
pacman_root = self.configuration.getpath("alpm", "database")
|
||||
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
||||
|
||||
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_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"):
|
||||
database = self.database_init(handle, repository, self.repository_id.architecture)
|
||||
@@ -99,12 +100,12 @@ class Pacman(LazyLogging):
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
pacman_root(Path): operating system pacman root
|
||||
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():
|
||||
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
|
||||
|
||||
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)
|
||||
architecture(str): repository architecture
|
||||
|
||||
@@ -164,12 +165,12 @@ class Pacman(LazyLogging):
|
||||
|
||||
return database
|
||||
|
||||
def database_sync(self, handle: Handle, *, force: bool) -> None:
|
||||
def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
|
||||
"""
|
||||
sync local database
|
||||
|
||||
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``)
|
||||
"""
|
||||
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.log import LazyLogging
|
||||
from ahriman.core.utils import check_output, utcnow, walk
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild import Pkgbuild
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
@@ -51,24 +52,25 @@ class Sources(LazyLogging):
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
Args:
|
||||
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:
|
||||
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()
|
||||
|
||||
diff = None
|
||||
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
||||
return instance.diff(source_dir, last_commit_sha)
|
||||
return None
|
||||
diff = instance.diff(source_dir, last_commit_sha)
|
||||
pkgbuild = instance.read(source_dir, "HEAD", Path("PKGBUILD"))
|
||||
|
||||
return Changes(last_commit_sha, diff, pkgbuild)
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
else:
|
||||
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 ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.utils import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
|
||||
|
||||
__all__ = ["migrate_data", "steps"]
|
||||
@@ -61,12 +59,9 @@ def migrate_package_depends(connection: Connection, configuration: Configuration
|
||||
if not configuration.repository_paths.repository.is_dir():
|
||||
return
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
||||
|
||||
package_list = []
|
||||
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():
|
||||
package_list.append({
|
||||
"make_depends": description.make_depends,
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
#
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.utils import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
|
||||
|
||||
__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():
|
||||
return
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
||||
|
||||
package_list = []
|
||||
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():
|
||||
package_list.append({
|
||||
"check_depends": description.check_depends,
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
#
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.utils import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
|
||||
|
||||
__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():
|
||||
return
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
||||
|
||||
package_list = []
|
||||
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_base": package.base,
|
||||
"packager": package.packager,
|
||||
|
||||
@@ -20,13 +20,11 @@
|
||||
from dataclasses import replace
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Explorer
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.utils import atomic_move, package_like, symlink_relative
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
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):
|
||||
paths = replace(configuration.repository_paths, repository_id=repository_id)
|
||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
||||
|
||||
# create archive directory if required
|
||||
if not paths.archive.is_dir():
|
||||
with paths.preserve_owner():
|
||||
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
|
||||
|
||||
Args:
|
||||
repository_paths(RepositoryPaths): repository paths instance
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
"""
|
||||
for archive in filter(package_like, repository_paths.repository.iterdir()):
|
||||
if not archive.is_file(follow_symlinks=False):
|
||||
continue # skip symbolic links if any
|
||||
|
||||
package = Package.from_archive(archive, pacman)
|
||||
package = Package.from_archive(archive)
|
||||
artifacts = [archive]
|
||||
# check if there are signatures for this package and append it here too
|
||||
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""",
|
||||
]
|
||||
@@ -45,10 +45,10 @@ class ChangesOperations(Operations):
|
||||
def run(connection: Connection) -> Changes:
|
||||
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(
|
||||
"""
|
||||
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
|
||||
""",
|
||||
{
|
||||
@@ -77,16 +77,17 @@ class ChangesOperations(Operations):
|
||||
connection.execute(
|
||||
"""
|
||||
insert into package_changes
|
||||
(package_base, last_commit_sha, changes, repository)
|
||||
(package_base, last_commit_sha, changes, pkgbuild, repository)
|
||||
values
|
||||
(:package_base, :last_commit_sha, :changes ,:repository)
|
||||
(:package_base, :last_commit_sha, :changes, :pkgbuild, :repository)
|
||||
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,
|
||||
"last_commit_sha": changes.last_commit_sha,
|
||||
"changes": changes.changes,
|
||||
"pkgbuild": changes.pkgbuild,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
|
||||
@@ -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_stats_printer import PackageStatsPrinter
|
||||
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.repository_printer import RepositoryPrinter
|
||||
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
|
||||
|
||||
@@ -45,7 +45,7 @@ class ChangesPrinter(Printer):
|
||||
Returns:
|
||||
list[Property]: list of content properties
|
||||
"""
|
||||
if self.changes.is_empty:
|
||||
if self.changes.changes is None:
|
||||
return []
|
||||
return [Property("", self.changes.changes, is_required=True, indent=0)]
|
||||
|
||||
@@ -57,6 +57,6 @@ class ChangesPrinter(Printer):
|
||||
Returns:
|
||||
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 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
|
||||
# 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.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.core.utils import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
@@ -78,27 +74,20 @@ class ArchiveRotationTrigger(Trigger):
|
||||
"""
|
||||
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
|
||||
|
||||
Args:
|
||||
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
|
||||
# this guard is supposed to speedup process
|
||||
if self.keep_built_packages == 0:
|
||||
return
|
||||
|
||||
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, 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))
|
||||
to_remove = repository.package_archives(package.base)
|
||||
|
||||
for single in to_remove[:-self.keep_built_packages]:
|
||||
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
|
||||
"""
|
||||
ctx = context.get()
|
||||
pacman = ctx.get(Pacman)
|
||||
repository = ctx.get(Repository)
|
||||
|
||||
for package in result.success:
|
||||
self.archives_remove(package, pacman)
|
||||
self.archives_remove(package, repository)
|
||||
|
||||
@@ -57,7 +57,7 @@ class Executor(PackageInfo, Cleaner):
|
||||
|
||||
for path in filter(package_like, archive.iterdir()):
|
||||
# check if package version is the same
|
||||
built = Package.from_archive(path, self.pacman)
|
||||
built = Package.from_archive(path)
|
||||
if built.version != package.version:
|
||||
continue
|
||||
|
||||
@@ -117,7 +117,7 @@ class Executor(PackageInfo, Cleaner):
|
||||
else:
|
||||
built = task.build(path, PACKAGER=packager)
|
||||
|
||||
package.with_packages(built, self.pacman)
|
||||
package.with_packages(built)
|
||||
for src in built:
|
||||
dst = self.paths.packages / src.name
|
||||
atomic_move(src, dst)
|
||||
@@ -204,7 +204,8 @@ class Executor(PackageInfo, Cleaner):
|
||||
|
||||
# update commit hash for changes keeping current diff if there is any
|
||||
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
|
||||
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
||||
|
||||
@@ -19,23 +19,36 @@
|
||||
#
|
||||
import copy
|
||||
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Callable, Iterable
|
||||
from functools import cmp_to_key
|
||||
from pathlib import Path
|
||||
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.sources import Sources
|
||||
from ahriman.core.repository.repository_properties import RepositoryProperties
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.utils import package_like
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class PackageInfo(RepositoryProperties):
|
||||
class PackageInfo(LazyLogging):
|
||||
"""
|
||||
handler for the package information
|
||||
|
||||
Attributes:
|
||||
configuration(Configuration): configuration instance
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
reporter(Client): build status reporter instance
|
||||
"""
|
||||
|
||||
configuration: Configuration
|
||||
pacman: Pacman
|
||||
reporter: Client
|
||||
|
||||
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
|
||||
"""
|
||||
generate full dependencies list including transitive dependencies
|
||||
@@ -86,7 +99,7 @@ class PackageInfo(RepositoryProperties):
|
||||
# we are iterating over bases, not single packages
|
||||
for full_path in packages:
|
||||
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
|
||||
local.remote = source
|
||||
|
||||
@@ -102,27 +115,48 @@ class PackageInfo(RepositoryProperties):
|
||||
self.logger.exception("could not load package from %s", full_path)
|
||||
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
|
||||
"""
|
||||
paths = self.configuration.repository_paths
|
||||
|
||||
packages: dict[tuple[str, str], Package] = {}
|
||||
# we can't use here load_archives, because it ignores versions
|
||||
for full_path in filter(package_like, paths.archive_for(package_base).iterdir()):
|
||||
local = Package.from_archive(full_path)
|
||||
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_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
|
||||
"""
|
||||
extract package change for the package since last commit if available
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
last_commit_sha(str | None): last known commit hash
|
||||
last_commit_sha(str): last known commit hash
|
||||
|
||||
Returns:
|
||||
Changes: changes if available
|
||||
Changes | None: changes if available
|
||||
"""
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||
dir_path = Path(dir_name)
|
||||
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.configuration.repository_paths)
|
||||
|
||||
changes: str | None = None
|
||||
if current_commit_sha != last_commit_sha:
|
||||
changes = Sources.changes(dir_path, last_commit_sha)
|
||||
|
||||
return Changes(last_commit_sha, changes)
|
||||
return Sources.changes(dir_path, last_commit_sha)
|
||||
return None
|
||||
|
||||
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
||||
"""
|
||||
@@ -134,7 +168,7 @@ class PackageInfo(RepositoryProperties):
|
||||
Returns:
|
||||
list[Package]: list of packages properties
|
||||
"""
|
||||
packages = self.load_archives(filter(package_like, self.paths.repository.iterdir()))
|
||||
packages = self.load_archives(filter(package_like, self.configuration.repository_paths.repository.iterdir()))
|
||||
if filter_packages:
|
||||
packages = [package for package in packages if package.base in filter_packages]
|
||||
|
||||
@@ -147,7 +181,7 @@ class PackageInfo(RepositoryProperties):
|
||||
Returns:
|
||||
list[Path]: list of filenames from the directory
|
||||
"""
|
||||
return list(filter(package_like, self.paths.packages.iterdir()))
|
||||
return list(filter(package_like, self.configuration.repository_paths.packages.iterdir()))
|
||||
|
||||
def packages_depend_on(self, packages: list[Package], depends_on: Iterable[str] | None) -> list[Package]:
|
||||
"""
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import Any, Self
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
@@ -39,15 +40,18 @@ class Watcher(LazyLogging):
|
||||
|
||||
Attributes:
|
||||
client(Client): reporter instance
|
||||
package_info(PackageInfo): package info instance
|
||||
status(BuildStatus): daemon status
|
||||
"""
|
||||
|
||||
def __init__(self, client: Client) -> None:
|
||||
def __init__(self, client: Client, package_info: PackageInfo) -> None:
|
||||
"""
|
||||
Args:
|
||||
client(Client): reporter instance
|
||||
package_info(PackageInfo): package info instance
|
||||
"""
|
||||
self.client = client
|
||||
self.package_info = package_info
|
||||
|
||||
self._lock = Lock()
|
||||
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
||||
@@ -80,6 +84,15 @@ class Watcher(LazyLogging):
|
||||
|
||||
logs_rotate: Callable[[int], None]
|
||||
|
||||
def package_archives(self, package_base: str) -> list[Package]:
|
||||
"""
|
||||
get known package archives
|
||||
|
||||
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_update: Callable[[str, Changes], None]
|
||||
|
||||
@@ -31,20 +31,12 @@ class Changes:
|
||||
Attributes:
|
||||
last_commit_sha(str | None): last commit hash
|
||||
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
|
||||
changes: 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
|
||||
pkgbuild: str | None = None
|
||||
|
||||
@classmethod
|
||||
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 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.log import LazyLogging
|
||||
from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list
|
||||
@@ -186,18 +187,17 @@ class Package(LazyLogging):
|
||||
return sorted(packages)
|
||||
|
||||
@classmethod
|
||||
def from_archive(cls, path: Path, pacman: Pacman) -> Self:
|
||||
def from_archive(cls, path: Path) -> Self:
|
||||
"""
|
||||
construct package properties from package archive
|
||||
|
||||
Args:
|
||||
path(Path): path to package archive
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
|
||||
Returns:
|
||||
Self: package properties
|
||||
"""
|
||||
package = pacman.handle.load_pkg(str(path))
|
||||
package = PacmanHandle.ephemeral().package_load(path)
|
||||
description = PackageDescription.from_package(package, path)
|
||||
return cls(
|
||||
base=package.base or package.name,
|
||||
@@ -400,17 +400,16 @@ class Package(LazyLogging):
|
||||
"""
|
||||
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
|
||||
|
||||
Args:
|
||||
packages(Iterable[Path]): paths to package archives
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
"""
|
||||
self.packages = {} # reset state
|
||||
for package in packages:
|
||||
archive = self.from_archive(package, pacman)
|
||||
archive = self.from_archive(package)
|
||||
if archive.base != self.base:
|
||||
continue
|
||||
|
||||
|
||||
@@ -32,3 +32,6 @@ class ChangesSchema(Schema):
|
||||
"description": "Last recorded commit hash",
|
||||
"example": "f1875edca1eb8fc0e55c41d1cae5fa05b6b7c6",
|
||||
})
|
||||
pkgbuild = fields.String(metadata={
|
||||
"description": "Original PKGBUILD content",
|
||||
})
|
||||
|
||||
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 changes
|
||||
|
||||
Returns:
|
||||
Response: 200 with package change on success
|
||||
|
||||
Raises:
|
||||
HTTPNotFound: if package base is unknown
|
||||
"""
|
||||
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()
|
||||
last_commit_sha = data.get("last_commit_sha") # empty/null meant removal
|
||||
change = data.get("changes")
|
||||
pkgbuild = data.get("pkgbuild")
|
||||
except Exception as 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)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
@@ -23,12 +23,14 @@ import logging
|
||||
import socket
|
||||
|
||||
from aiohttp.web import Application, normalize_path_middleware, run_app
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.distributed import WorkersCache
|
||||
from ahriman.core.exceptions import InitializeError
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.spawn import Spawn
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
@@ -78,6 +80,33 @@ def _create_socket(configuration: Configuration, application: Application) -> so
|
||||
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
|
||||
|
||||
return Watcher(client, package_info)
|
||||
|
||||
|
||||
async def _on_shutdown(application: Application) -> None:
|
||||
"""
|
||||
web application shutdown handler
|
||||
@@ -168,18 +197,11 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
|
||||
# package cache
|
||||
if not repositories:
|
||||
raise InitializeError("No repositories configured, exiting")
|
||||
watchers: dict[RepositoryId, Watcher] = {}
|
||||
configuration_path, _ = configuration.check_loaded()
|
||||
for repository_id in repositories:
|
||||
application.logger.info("load repository %s", repository_id)
|
||||
# load settings explicitly for architecture if any
|
||||
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
|
||||
application[WatcherKey] = {
|
||||
repository_id: _create_watcher(configuration_path, repository_id)
|
||||
for repository_id in repositories
|
||||
}
|
||||
# workers cache
|
||||
application[WorkersKey] = WorkersCache(configuration)
|
||||
# process spawner
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_changes(application_repository: ApplicationRepository, package_ahriman:
|
||||
"""
|
||||
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)
|
||||
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")
|
||||
@@ -42,6 +42,20 @@ def test_changes_skip(application_repository: ApplicationRepository, package_ahr
|
||||
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:
|
||||
"""
|
||||
must clean cache directory
|
||||
|
||||
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
|
||||
@@ -309,6 +309,44 @@ def test_subparsers_package_changes_remove_package_changes(parser: argparse.Argu
|
||||
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:
|
||||
"""
|
||||
package-copy command must correctly parse architecture list
|
||||
|
||||
37
tests/ahriman/core/alpm/test_pacman_handle.py
Normal file
37
tests/ahriman/core/alpm/test_pacman_handle.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||
|
||||
|
||||
def test_package_load() -> None:
|
||||
"""
|
||||
must load package from archive path
|
||||
"""
|
||||
local = Path("local")
|
||||
instance = PacmanHandle.ephemeral()
|
||||
handle_mock = instance.handle = MagicMock()
|
||||
|
||||
instance.package_load(local)
|
||||
handle_mock.load_pkg.assert_called_once_with(str(local))
|
||||
|
||||
PacmanHandle._ephemeral = None
|
||||
|
||||
|
||||
def test_getattr() -> None:
|
||||
"""
|
||||
must proxy attribute access to underlying handle
|
||||
"""
|
||||
instance = PacmanHandle.ephemeral()
|
||||
assert instance.dbpath
|
||||
|
||||
|
||||
def test_getattr_not_found() -> None:
|
||||
"""
|
||||
must raise AttributeError for missing handle attributes
|
||||
"""
|
||||
instance = PacmanHandle.ephemeral()
|
||||
with pytest.raises(AttributeError):
|
||||
assert instance.random_attribute
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.exceptions import CalledProcessError
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
@@ -19,35 +20,27 @@ def test_changes(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until")
|
||||
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff", return_value="diff")
|
||||
read_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.read", return_value="pkgbuild")
|
||||
local = Path("local")
|
||||
last_commit_sha = "sha"
|
||||
|
||||
assert Sources.changes(local, last_commit_sha) == "diff"
|
||||
assert Sources.changes(local, last_commit_sha) == Changes(last_commit_sha, "diff", "pkgbuild")
|
||||
fetch_mock.assert_called_once_with(local, commit_sha=last_commit_sha)
|
||||
diff_mock.assert_called_once_with(local, last_commit_sha)
|
||||
|
||||
|
||||
def test_changes_skip(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return none in case if commit sha is not available
|
||||
"""
|
||||
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until")
|
||||
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff")
|
||||
|
||||
assert Sources.changes(Path("local"), None) is None
|
||||
fetch_mock.assert_not_called()
|
||||
diff_mock.assert_not_called()
|
||||
read_mock.assert_called_once_with(local, "HEAD", Path("PKGBUILD"))
|
||||
|
||||
|
||||
def test_changes_unknown_commit(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return none in case if commit sha wasn't found at the required depth
|
||||
must return changes without diff in case if commit sha wasn't found at the required depth
|
||||
"""
|
||||
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until", return_value=None)
|
||||
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff")
|
||||
read_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.read", return_value="pkgbuild")
|
||||
|
||||
assert Sources.changes(Path("local"), "sha") is None
|
||||
assert Sources.changes(Path("local"), "sha") == Changes("sha", None, "pkgbuild")
|
||||
diff_mock.assert_not_called()
|
||||
read_mock.assert_called_once_with(Path("local"), "HEAD", Path("PKGBUILD"))
|
||||
|
||||
|
||||
def test_extend_architectures(mocker: MockerFixture) -> None:
|
||||
@@ -603,3 +596,12 @@ def test_patch_apply_function(sources: Sources, mocker: MockerFixture) -> None:
|
||||
|
||||
sources.patch_apply(local, patch)
|
||||
write_mock.assert_called_once_with(local / "PKGBUILD")
|
||||
|
||||
|
||||
def test_read(sources: Sources, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must read file from commit
|
||||
"""
|
||||
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.check_output", return_value="content")
|
||||
assert sources.read(Path("local"), "sha", Path("PKGBUILD")) == "content"
|
||||
check_output_mock.assert_called_once()
|
||||
|
||||
@@ -34,8 +34,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
|
||||
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
|
||||
|
||||
migrate_package_depends(connection, configuration)
|
||||
package_mock.assert_called_once_with(
|
||||
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
|
||||
package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
|
||||
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
|
||||
"make_depends": package_ahriman.packages[package_ahriman.base].make_depends,
|
||||
"opt_depends": package_ahriman.packages[package_ahriman.base].opt_depends,
|
||||
|
||||
@@ -34,8 +34,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
|
||||
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
|
||||
|
||||
migrate_package_check_depends(connection, configuration)
|
||||
package_mock.assert_called_once_with(
|
||||
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
|
||||
package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
|
||||
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
|
||||
"check_depends": package_ahriman.packages[package_ahriman.base].check_depends,
|
||||
"package": package_ahriman.base,
|
||||
|
||||
@@ -34,8 +34,7 @@ def test_migrate_package_base_packager(connection: Connection, configuration: Co
|
||||
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
|
||||
|
||||
migrate_package_base_packager(connection, configuration)
|
||||
package_mock.assert_called_once_with(
|
||||
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
|
||||
package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
|
||||
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
|
||||
"package_base": package_ahriman.base,
|
||||
"packager": package_ahriman.packager,
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlite3 import Connection
|
||||
from typing import Any
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.migrations.m016_archive import migrate_data, move_packages
|
||||
from ahriman.models.package import Package
|
||||
@@ -28,12 +27,12 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock
|
||||
|
||||
migrate_data(connection, configuration)
|
||||
migration_mock.assert_has_calls([
|
||||
MockCall(replace(configuration.repository_paths, repository_id=repository), pytest.helpers.anyvar(int))
|
||||
MockCall(replace(configuration.repository_paths, repository_id=repository))
|
||||
for repository in repositories
|
||||
])
|
||||
|
||||
|
||||
def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, package_ahriman: Package,
|
||||
def test_move_packages(repository_paths: RepositoryPaths, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must move packages to the archive directory
|
||||
@@ -57,9 +56,9 @@ def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, packag
|
||||
move_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.atomic_move")
|
||||
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||
|
||||
move_packages(repository_paths, pacman)
|
||||
move_packages(repository_paths)
|
||||
archive_mock.assert_has_calls([
|
||||
MockCall(repository_paths.repository / filename, pacman)
|
||||
MockCall(repository_paths.repository / filename)
|
||||
for filename in ("file.pkg.tar.xz", "file2.pkg.tar.xz")
|
||||
])
|
||||
move_mock.assert_has_calls([
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from ahriman.core.database.migrations.m017_pkgbuild import steps
|
||||
|
||||
|
||||
def test_migration_pkgbuild() -> None:
|
||||
"""
|
||||
migration must not be empty
|
||||
"""
|
||||
assert steps
|
||||
@@ -8,34 +8,35 @@ def test_changes_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must insert and get changes
|
||||
"""
|
||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1"))
|
||||
assert database.changes_get(package_ahriman.base).changes == "change1"
|
||||
changes1 = Changes("sha1", "change1", "pkgbuild1")
|
||||
database.changes_insert(package_ahriman.base, changes1)
|
||||
assert database.changes_get(package_ahriman.base) == changes1
|
||||
changes2 = Changes("sha2", "change2", "pkgbuild2")
|
||||
|
||||
database.changes_insert(package_ahriman.base, Changes("sha2", "change2"),
|
||||
RepositoryId("i686", database._repository_id.name))
|
||||
assert database.changes_get(package_ahriman.base).changes == "change1"
|
||||
assert database.changes_get(
|
||||
package_ahriman.base, RepositoryId("i686", database._repository_id.name)).changes == "change2"
|
||||
database.changes_insert(package_ahriman.base, changes2, RepositoryId("i686", database._repository_id.name))
|
||||
assert database.changes_get(package_ahriman.base) == changes1
|
||||
assert database.changes_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == changes2
|
||||
|
||||
|
||||
def test_changes_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must remove changes for the package
|
||||
"""
|
||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1"))
|
||||
database.changes_insert(package_python_schedule.base, Changes("sha3", "change3"))
|
||||
database.changes_insert(package_ahriman.base, Changes("sha2", "change2"),
|
||||
changes3 = Changes("sha3", "change3", "pkgbuild3")
|
||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1", "pkgbuild1"))
|
||||
database.changes_insert(package_python_schedule.base, changes3)
|
||||
database.changes_insert(package_ahriman.base, Changes("sha2", "change2", "pkgbuild2"),
|
||||
RepositoryId("i686", database._repository_id.name))
|
||||
|
||||
database.changes_remove(package_ahriman.base)
|
||||
assert database.changes_get(package_ahriman.base).changes is None
|
||||
assert database.changes_get(package_python_schedule.base).changes == "change3"
|
||||
assert database.changes_get(package_python_schedule.base) == changes3
|
||||
|
||||
# insert null
|
||||
database.changes_insert(package_ahriman.base, Changes(), RepositoryId("i686", database._repository_id.name))
|
||||
assert database.changes_get(
|
||||
package_ahriman.base, RepositoryId("i686", database._repository_id.name)).changes is None
|
||||
assert database.changes_get(package_python_schedule.base).changes == "change3"
|
||||
assert database.changes_get(package_python_schedule.base) == changes3
|
||||
|
||||
|
||||
def test_changes_insert_remove_full(database: SQLite, package_ahriman: Package,
|
||||
@@ -43,13 +44,12 @@ def test_changes_insert_remove_full(database: SQLite, package_ahriman: Package,
|
||||
"""
|
||||
must remove all changes for the repository
|
||||
"""
|
||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1"))
|
||||
database.changes_insert(package_python_schedule.base, Changes("sha3", "change3"))
|
||||
database.changes_insert(package_ahriman.base, Changes("sha2", "change2"),
|
||||
RepositoryId("i686", database._repository_id.name))
|
||||
changes2 = Changes("sha2", "change2", "pkgbuild2")
|
||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1", "pkgbuild1"))
|
||||
database.changes_insert(package_python_schedule.base, Changes("sha3", "change3", "pkgbuild3"))
|
||||
database.changes_insert(package_ahriman.base, changes2, RepositoryId("i686", database._repository_id.name))
|
||||
|
||||
database.changes_remove(None)
|
||||
assert database.changes_get(package_ahriman.base).changes is None
|
||||
assert database.changes_get(package_python_schedule.base).changes is None
|
||||
assert database.changes_get(
|
||||
package_ahriman.base, RepositoryId("i686", database._repository_id.name)).changes == "change2"
|
||||
assert database.changes_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == changes2
|
||||
|
||||
@@ -2,24 +2,26 @@ import pytest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.formatters import \
|
||||
AurPrinter, \
|
||||
ChangesPrinter, \
|
||||
ConfigurationPathsPrinter, \
|
||||
ConfigurationPrinter, \
|
||||
EventStatsPrinter, \
|
||||
PackagePrinter, \
|
||||
PackageStatsPrinter, \
|
||||
PatchPrinter, \
|
||||
RepositoryPrinter, \
|
||||
RepositoryStatsPrinter, \
|
||||
StatusPrinter, \
|
||||
StringPrinter, \
|
||||
TreePrinter, \
|
||||
UpdatePrinter, \
|
||||
UserPrinter, \
|
||||
ValidationPrinter, \
|
||||
from ahriman.core.formatters import (
|
||||
AurPrinter,
|
||||
ChangesPrinter,
|
||||
ConfigurationPathsPrinter,
|
||||
ConfigurationPrinter,
|
||||
EventStatsPrinter,
|
||||
PackagePrinter,
|
||||
PackageStatsPrinter,
|
||||
PatchPrinter,
|
||||
PkgbuildPrinter,
|
||||
RepositoryPrinter,
|
||||
RepositoryStatsPrinter,
|
||||
StatusPrinter,
|
||||
StringPrinter,
|
||||
TreePrinter,
|
||||
UpdatePrinter,
|
||||
UserPrinter,
|
||||
ValidationPrinter,
|
||||
VersionPrinter
|
||||
)
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.changes import Changes
|
||||
@@ -55,6 +57,17 @@ def changes_printer() -> ChangesPrinter:
|
||||
return ChangesPrinter(Changes("sha", "changes"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pkgbuild_printer() -> PkgbuildPrinter:
|
||||
"""
|
||||
fixture for pkgbuild printer
|
||||
|
||||
Returns:
|
||||
PkgbuildPrinter: pkgbuild printer test instance
|
||||
"""
|
||||
return PkgbuildPrinter(Changes("sha", "changes", "pkgbuild content"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configuration_paths_printer() -> ConfigurationPathsPrinter:
|
||||
"""
|
||||
|
||||
32
tests/ahriman/core/formatters/test_pkgbuild_printer.py
Normal file
32
tests/ahriman/core/formatters/test_pkgbuild_printer.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from ahriman.core.formatters import PkgbuildPrinter
|
||||
from ahriman.models.changes import Changes
|
||||
|
||||
|
||||
def test_properties(pkgbuild_printer: PkgbuildPrinter) -> None:
|
||||
"""
|
||||
must return non-empty properties list
|
||||
"""
|
||||
assert pkgbuild_printer.properties()
|
||||
|
||||
|
||||
def test_properties_empty() -> None:
|
||||
"""
|
||||
must return empty properties list if pkgbuild is empty
|
||||
"""
|
||||
assert not PkgbuildPrinter(Changes()).properties()
|
||||
assert not PkgbuildPrinter(Changes("sha", "changes")).properties()
|
||||
|
||||
|
||||
def test_title(pkgbuild_printer: PkgbuildPrinter) -> None:
|
||||
"""
|
||||
must return non-empty title
|
||||
"""
|
||||
assert pkgbuild_printer.title()
|
||||
|
||||
|
||||
def test_title_empty() -> None:
|
||||
"""
|
||||
must return empty title if change is empty
|
||||
"""
|
||||
assert not PkgbuildPrinter(Changes()).title()
|
||||
assert not PkgbuildPrinter(Changes("sha")).title()
|
||||
@@ -3,12 +3,11 @@ import pytest
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.housekeeping import ArchiveRotationTrigger
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
@@ -21,26 +20,24 @@ def test_configuration_sections(configuration: Configuration) -> None:
|
||||
|
||||
|
||||
def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
|
||||
pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove older packages
|
||||
"""
|
||||
def package(version: Any, *args: Any, **kwargs: Any) -> Package:
|
||||
generated = replace(package_ahriman, version=str(version))
|
||||
packages = []
|
||||
for i in range(5):
|
||||
generated = replace(package_ahriman, version=str(i))
|
||||
generated.packages = {
|
||||
key: replace(value, filename=str(version))
|
||||
key: replace(value, filename=str(i))
|
||||
for key, value in generated.packages.items()
|
||||
}
|
||||
return generated
|
||||
packages.append(generated)
|
||||
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=True)
|
||||
mocker.patch("ahriman.core.housekeeping.archive_rotation_trigger.package_like", return_value=True)
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=packages)
|
||||
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)])
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[Path(str(i)) for i in range(5)])
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package)
|
||||
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
|
||||
|
||||
archive_rotation_trigger.archives_remove(package_ahriman, pacman)
|
||||
archive_rotation_trigger.archives_remove(package_ahriman, repository)
|
||||
unlink_mock.assert_has_calls([
|
||||
MockCall(Path("0")),
|
||||
MockCall(Path("1")),
|
||||
@@ -48,28 +45,15 @@ def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, packa
|
||||
|
||||
|
||||
def test_archives_remove_keep(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
|
||||
pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must keep all packages if set to
|
||||
"""
|
||||
def package(version: Any, *args: Any, **kwargs: Any) -> Package:
|
||||
generated = replace(package_ahriman, version=str(version))
|
||||
generated.packages = {
|
||||
key: replace(value, filename=str(version))
|
||||
for key, value in generated.packages.items()
|
||||
}
|
||||
return generated
|
||||
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=True)
|
||||
mocker.patch("ahriman.core.housekeeping.archive_rotation_trigger.package_like", return_value=True)
|
||||
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)])
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[Path(str(i)) for i in range(5)])
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package)
|
||||
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
|
||||
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives")
|
||||
|
||||
archive_rotation_trigger.keep_built_packages = 0
|
||||
archive_rotation_trigger.archives_remove(package_ahriman, pacman)
|
||||
unlink_mock.assert_not_called()
|
||||
archive_rotation_trigger.archives_remove(package_ahriman, repository)
|
||||
archives_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_on_result(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
|
||||
|
||||
@@ -6,7 +6,6 @@ from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.repository.executor import Executor
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.repository.update_handler import UpdateHandler
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
|
||||
@@ -50,23 +49,6 @@ def executor(configuration: Configuration, database: SQLite, mocker: MockerFixtu
|
||||
refresh_pacman_database=PacmanSynchronization.Disabled)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_info(configuration: Configuration, database: SQLite) -> PackageInfo:
|
||||
"""
|
||||
fixture for package info
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
database(SQLite): database fixture
|
||||
|
||||
Returns:
|
||||
PackageInfo: package info test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return PackageInfo(repository_id, configuration, database, report=False,
|
||||
refresh_pacman_database=PacmanSynchronization.Disabled)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def update_handler(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> UpdateHandler:
|
||||
"""
|
||||
|
||||
@@ -118,7 +118,7 @@ def test_package_build(executor: Executor, package_ahriman: Package, mocker: Moc
|
||||
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
|
||||
package_mock.assert_called_once_with(Path("local"), executor.architecture, None)
|
||||
lookup_mock.assert_called_once_with(package_ahriman)
|
||||
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman)
|
||||
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)])
|
||||
rename_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any
|
||||
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
|
||||
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
|
||||
changes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
|
||||
return_value=Changes("commit", "change"))
|
||||
return_value=Changes("commit", "change", "pkgbuild"))
|
||||
commit_sha_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||
depends_on_mock = mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.depends_on",
|
||||
return_value=Dependencies())
|
||||
@@ -231,7 +231,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any
|
||||
build_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(Path, strict=True), None, None)
|
||||
depends_on_mock.assert_called_once_with()
|
||||
dependencies_mock.assert_called_once_with(package_ahriman.base, Dependencies())
|
||||
commit_sha_mock.assert_called_once_with(package_ahriman.base, Changes("sha", "change"))
|
||||
commit_sha_mock.assert_called_once_with(package_ahriman.base, Changes("sha", "change", "pkgbuild"))
|
||||
|
||||
|
||||
def test_process_build_bump_pkgrel(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
|
||||
@@ -4,12 +4,12 @@ from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_full_depends(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
|
||||
def test_full_depends(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
pyalpm_package_ahriman: MagicMock) -> None:
|
||||
"""
|
||||
must extract all dependencies from the package
|
||||
@@ -18,18 +18,18 @@ def test_full_depends(package_info: PackageInfo, package_ahriman: Package, packa
|
||||
|
||||
database_mock = MagicMock()
|
||||
database_mock.pkgcache = [pyalpm_package_ahriman]
|
||||
package_info.pacman = MagicMock()
|
||||
package_info.pacman.handle.get_syncdbs.return_value = [database_mock]
|
||||
repository.pacman = MagicMock()
|
||||
repository.pacman.handle.get_syncdbs.return_value = [database_mock]
|
||||
|
||||
assert package_info.full_depends(package_ahriman, [package_python_schedule]) == package_ahriman.depends
|
||||
assert repository.full_depends(package_ahriman, [package_python_schedule]) == package_ahriman.depends
|
||||
|
||||
package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base]
|
||||
expected = sorted(set(package_python_schedule.depends + package_ahriman.depends))
|
||||
assert package_info.full_depends(package_python_schedule, [package_python_schedule]) == expected
|
||||
assert repository.full_depends(package_python_schedule, [package_python_schedule]) == expected
|
||||
|
||||
|
||||
def test_load_archives(package_ahriman: Package, package_python_schedule: Package,
|
||||
package_info: PackageInfo, mocker: MockerFixture) -> None:
|
||||
def test_load_archives(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return all packages grouped by package base
|
||||
"""
|
||||
@@ -45,7 +45,7 @@ def test_load_archives(package_ahriman: Package, package_python_schedule: Packag
|
||||
(package_ahriman, None),
|
||||
])
|
||||
|
||||
packages = package_info.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
|
||||
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
|
||||
assert len(packages) == 2
|
||||
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
|
||||
|
||||
@@ -56,22 +56,22 @@ def test_load_archives(package_ahriman: Package, package_python_schedule: Packag
|
||||
assert set(archives) == expected
|
||||
|
||||
|
||||
def test_load_archives_failed(package_info: PackageInfo, mocker: MockerFixture) -> None:
|
||||
def test_load_archives_failed(repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip packages which cannot be loaded
|
||||
"""
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=Exception)
|
||||
assert not package_info.load_archives([Path("a.pkg.tar.xz")])
|
||||
assert not repository.load_archives([Path("a.pkg.tar.xz")])
|
||||
|
||||
|
||||
def test_load_archives_not_package(package_info: PackageInfo) -> None:
|
||||
def test_load_archives_not_package(repository: Repository) -> None:
|
||||
"""
|
||||
must skip not packages from iteration
|
||||
"""
|
||||
assert not package_info.load_archives([Path("a.tar.xz")])
|
||||
assert not repository.load_archives([Path("a.tar.xz")])
|
||||
|
||||
|
||||
def test_load_archives_different_version(package_info: PackageInfo, package_python_schedule: Package,
|
||||
def test_load_archives_different_version(repository: Repository, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load packages with different versions choosing maximal
|
||||
@@ -86,37 +86,61 @@ def test_load_archives_different_version(package_info: PackageInfo, package_pyth
|
||||
single_packages[0].version = "0.0.1-1"
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages)
|
||||
|
||||
packages = package_info.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")])
|
||||
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")])
|
||||
assert len(packages) == 1
|
||||
assert packages[0].version == package_python_schedule.version
|
||||
|
||||
|
||||
def test_package_changes(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
def test_package_archives(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load package archives sorted by version
|
||||
"""
|
||||
from dataclasses import replace
|
||||
from typing import Any
|
||||
|
||||
def package(version: Any, *args: Any, **kwargs: Any) -> Package:
|
||||
generated = replace(package_ahriman, version=str(version))
|
||||
generated.packages = {
|
||||
key: replace(value, filename=str(version))
|
||||
for key, value in generated.packages.items()
|
||||
}
|
||||
return generated
|
||||
|
||||
mocker.patch("ahriman.core.repository.package_info.package_like", return_value=True)
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[Path(str(i)) for i in range(5)])
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package)
|
||||
|
||||
result = repository.package_archives(package_ahriman.base)
|
||||
assert len(result) == 5
|
||||
assert [p.version for p in result] == [str(i) for i in range(5)]
|
||||
|
||||
|
||||
def test_package_changes(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load package changes
|
||||
"""
|
||||
changes = Changes("sha", "change")
|
||||
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha2")
|
||||
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes.changes)
|
||||
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes)
|
||||
|
||||
assert package_info.package_changes(package_ahriman, changes.last_commit_sha) == changes
|
||||
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], package_info.paths)
|
||||
assert repository.package_changes(package_ahriman, changes.last_commit_sha) == changes
|
||||
load_mock.assert_called_once_with(
|
||||
pytest.helpers.anyvar(int), package_ahriman, [], repository.configuration.repository_paths)
|
||||
changes_mock.assert_called_once_with(pytest.helpers.anyvar(int), changes.last_commit_sha)
|
||||
|
||||
|
||||
def test_package_changes_skip(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
def test_package_changes_skip(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip loading package changes if no new commits
|
||||
"""
|
||||
changes = Changes("sha")
|
||||
mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value=changes.last_commit_sha)
|
||||
mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha")
|
||||
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes")
|
||||
|
||||
assert package_info.package_changes(package_ahriman, changes.last_commit_sha) == changes
|
||||
assert repository.package_changes(package_ahriman, "sha") is None
|
||||
changes_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_packages(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
|
||||
def test_packages(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return repository packages
|
||||
@@ -124,12 +148,12 @@ def test_packages(package_info: PackageInfo, package_ahriman: Package, package_p
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert package_info.packages() == [package_ahriman, package_python_schedule]
|
||||
assert repository.packages() == [package_ahriman, package_python_schedule]
|
||||
# it uses filter object, so we cannot verify argument list =/
|
||||
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_packages_filter(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
|
||||
def test_packages_filter(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter result by bases
|
||||
@@ -137,33 +161,33 @@ def test_packages_filter(package_info: PackageInfo, package_ahriman: Package, pa
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert package_info.packages([package_ahriman.base]) == [package_ahriman]
|
||||
assert repository.packages([package_ahriman.base]) == [package_ahriman]
|
||||
|
||||
|
||||
def test_packages_built(package_info: PackageInfo, mocker: MockerFixture) -> None:
|
||||
def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return build packages
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")])
|
||||
assert package_info.packages_built() == [Path("b.pkg.tar.xz")]
|
||||
assert repository.packages_built() == [Path("b.pkg.tar.xz")]
|
||||
|
||||
|
||||
def test_packages_depend_on(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
|
||||
def test_packages_depend_on(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter packages by depends list
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert package_info.packages_depend_on([package_ahriman], {"python-srcinfo"}) == [package_ahriman]
|
||||
assert repository.packages_depend_on([package_ahriman], {"python-srcinfo"}) == [package_ahriman]
|
||||
|
||||
|
||||
def test_packages_depend_on_empty(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
|
||||
def test_packages_depend_on_empty(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return all packages in case if no filter is provided
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert package_info.packages_depend_on([package_ahriman, package_python_schedule], None) == \
|
||||
assert repository.packages_depend_on([package_ahriman, package_python_schedule], None) == \
|
||||
[package_ahriman, package_python_schedule]
|
||||
|
||||
@@ -89,22 +89,6 @@ def pkgbuild_ahriman(resource_path_root: Path) -> Pkgbuild:
|
||||
return Pkgbuild.from_file(pkgbuild)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
|
||||
"""
|
||||
mock object for pyalpm
|
||||
|
||||
Args:
|
||||
pyalpm_package_ahriman(MagicMock): mock object for pyalpm package
|
||||
|
||||
Returns:
|
||||
MagicMock: pyalpm mock
|
||||
"""
|
||||
mock = MagicMock()
|
||||
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
|
||||
"""
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
from ahriman.models.changes import Changes
|
||||
|
||||
|
||||
def test_is_empty() -> None:
|
||||
"""
|
||||
must check if changes are empty
|
||||
"""
|
||||
assert Changes().is_empty
|
||||
assert Changes("sha").is_empty
|
||||
|
||||
assert not Changes("sha", "change").is_empty
|
||||
assert not Changes(None, "change").is_empty # well, ok
|
||||
|
||||
|
||||
def test_changes_from_json_view() -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
|
||||
@@ -148,13 +148,14 @@ def test_packages_full(package_ahriman: Package) -> None:
|
||||
assert package_ahriman.packages_full == [package_ahriman.base, f"{package_ahriman.base}-git"]
|
||||
|
||||
|
||||
def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
|
||||
def test_from_archive(package_ahriman: Package, pyalpm_package_ahriman: MagicMock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must construct package from alpm library
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.pacman_handle.PacmanHandle.package_load", return_value=pyalpm_package_ahriman)
|
||||
mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
|
||||
return_value=package_ahriman.packages[package_ahriman.base])
|
||||
generated = Package.from_archive(Path("path"), pyalpm_handle)
|
||||
generated = Package.from_archive(Path("path"))
|
||||
generated.remote = package_ahriman.remote
|
||||
|
||||
assert generated == package_ahriman
|
||||
@@ -165,13 +166,12 @@ def test_from_archive_empty_base(package_ahriman: Package, pyalpm_package_ahrima
|
||||
"""
|
||||
must construct package with empty base from alpm library
|
||||
"""
|
||||
pyalpm_handle = MagicMock()
|
||||
type(pyalpm_package_ahriman).base = PropertyMock(return_value=None)
|
||||
pyalpm_handle.handle.load_pkg.return_value = pyalpm_package_ahriman
|
||||
mocker.patch("ahriman.core.alpm.pacman_handle.PacmanHandle.package_load", return_value=pyalpm_package_ahriman)
|
||||
|
||||
mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
|
||||
return_value=package_ahriman.packages[package_ahriman.base])
|
||||
generated = Package.from_archive(Path("path"), pyalpm_handle)
|
||||
generated = Package.from_archive(Path("path"))
|
||||
generated.remote = package_ahriman.remote
|
||||
|
||||
assert generated == package_ahriman
|
||||
@@ -362,7 +362,7 @@ def test_vercmp(package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
vercmp_mock.assert_called_once_with(package_ahriman.version, "version")
|
||||
|
||||
|
||||
def test_with_packages(package_ahriman: Package, package_python_schedule: Package, pacman: Pacman,
|
||||
def test_with_packages(package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly replace packages descriptions
|
||||
@@ -375,8 +375,8 @@ def test_with_packages(package_ahriman: Package, package_python_schedule: Packag
|
||||
result = copy.deepcopy(package_ahriman)
|
||||
package_ahriman.packages[package_ahriman.base].architecture = "i686"
|
||||
|
||||
result.with_packages(paths, pacman)
|
||||
result.with_packages(paths)
|
||||
from_archive_mock.assert_has_calls([
|
||||
MockCall(path, pacman) for path in paths
|
||||
MockCall(path) for path in paths
|
||||
])
|
||||
assert result.packages[result.base] == package_ahriman.packages[package_ahriman.base]
|
||||
|
||||
Reference in New Issue
Block a user