YYJ76Q7V6G7FHSNZ25MU3ZZY5KGXIODBHLWOZUGB4FIKL7PUCNAAC D4SBRY6KHEAMGUEVWSUYHNCRYXCOC5JJ3N3DQ47ZXOXQGT73VERAC X7GJK2QD5LRK3KDE2YMCNDTASB3TAK6VAHSCY3SOZBHAYDQ5KJ7QC STNLKCHXVRQX3UDBTPAFV6OP4SW4D5LU4LSIED2MEIVZS3GC5HTAC LE34RHBBMJT2BWLU5BHZJSHNMHQWX66QQNM2MAEDWEARCWOCSYLAC IFYNLE5HO7JIZ5LJYNQ7ZQGK7C7RWPT54DFMCDHPT4CEGCZN7ACQC 5VX32NQS4TFOXTKH3UOMTMTCUZ2IGCKO2BFBXRZAEQ3GKTTNX5SQC JSRKEVVPN4R7IJ53AU4OYH5HJCXPLBHCYDD7CSN74W7HACAKTJMAC .hljs-emphasis {font-style: italic;}.hljs-strong {font-weight: 700;}@media (prefers-color-scheme: dark) {.hljs {color: #fff;background: #1c1b1b;}.hljs-subst {color: #fff;}.hljs-comment {color: #999;}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag {color: #88aece;}.hljs-attribute {color: #c59bc1;}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type {color: #f08d49;}.hljs-selector-class {color: #88aece;}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable {color: #b5bd68;}.hljs-meta,.hljs-selector-pseudo {color: #88aece;}.hljs-built_in,.hljs-literal,.hljs-title {color: #f08d49;}.hljs-bullet,.hljs-code {color: #ccc;}.hljs-meta .hljs-string {color: #b5bd68;}.hljs-deletion {color: #de7176;}.hljs-addition {color: #76c490;}}@media (prefers-color-scheme: light) {.hljs {color: #2f3337;background: #f6f6f6;}.hljs-subst {color: #2f3337;}.hljs-comment {color: #656e77;}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag {color: #015692;}.hljs-attribute {color: #803378;}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type {color: #b75501;}.hljs-selector-class {color: #015692;}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable {color: #54790d;}.hljs-meta,.hljs-selector-pseudo {color: #015692;}.hljs-built_in,.hljs-literal,.hljs-title {color: #b75501;}.hljs-bullet,.hljs-code {color: #535a60;}.hljs-meta .hljs-string {color: #54790d;}.hljs-deletion {color: #c02d2e;}.hljs-addition {color: #2f6f44;}}
@import 'tailwindcss';@plugin "daisyui";@plugin "@iconify/tailwind4";@layer base {}h1 {font-size: var(--text-2xl) !important;}h2 {font-size: var(--text-xl) !important;}h3 {font-size: var(--text-l) !important;}
:global {.hljs-emphasis {font-style: italic;}.hljs-strong {font-weight: 700;}@media (prefers-color-scheme: dark) {.hljs {color: #fff;background: #1c1b1b;}.hljs-subst {color: #fff;}.hljs-comment {color: #999;}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag {color: #88aece;}.hljs-attribute {color: #c59bc1;}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type {color: #f08d49;}.hljs-selector-class {color: #88aece;}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable {color: #b5bd68;}.hljs-meta,.hljs-selector-pseudo {color: #88aece;}.hljs-built_in,.hljs-literal,.hljs-title {color: #f08d49;}.hljs-bullet,.hljs-code {color: #ccc;}.hljs-meta .hljs-string {color: #b5bd68;}.hljs-deletion {color: #de7176;}.hljs-addition {color: #76c490;}}@media (prefers-color-scheme: light) {.hljs {color: #2f3337;background: #f6f6f6;}.hljs-subst {color: #2f3337;}.hljs-comment {color: #656e77;}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag {color: #015692;}.hljs-attribute {color: #803378;}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type {color: #b75501;}.hljs-selector-class {color: #015692;}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable {color: #54790d;}.hljs-meta,.hljs-selector-pseudo {color: #015692;}.hljs-built_in,.hljs-literal,.hljs-title {color: #b75501;}.hljs-bullet,.hljs-code {color: #535a60;}.hljs-meta .hljs-string {color: #54790d;}.hljs-deletion {color: #c02d2e;}.hljs-addition {color: #2f6f44;}}}
import type { PageLoad } from './$types';import { errorMsg, server } from '../../../../helpers';export const load: PageLoad = async ({ fetch, params }) => {const u = `${server}/api/job/${params.user}/${params.repo}/${params.job}`;const resp = await fetch(u, { credentials: 'include' });if (resp.status == 200) {return await resp.json();} else {const y = await resp.json();errorMsg(resp.status, y);}};
<script lang="ts">import Nav from '../../Nav.svelte';import Tabs from '../../Tabs.svelte';import { onMount } from 'svelte';import { server } from '../../../../helpers';const { data, params } = $props();let ended = $derived(data.ended);let status = $derived(data.status);import { AnsiUp } from 'ansi_up';let socket: WebSocket;let out: string[][] = $state([[], []]);onMount(() => {let ansi_up = new AnsiUp();socket = new WebSocket(`${server}/api/job/${params.user}/${params.repo}/${params.job}/ws`);socket.onmessage = (event) => {console.log(event);const m = JSON.parse(event.data);if ('Chunk' in m) {console.log(m);out[m.Chunk.channel].push(ansi_up.ansi_to_html(m.Chunk.content));} else if ('Status' in m) {console.log(m);ended = m.Status.ended;status = m.Status.status;}};socket.onopen = () => {// socket.send('{ "State": { "stdout": 0, "stderr": 0 } }');};});</script><svelte:head><title>{params.user} / {params.repo}</title></svelte:head><div class="p-3"><Nav user={params.user} repo={params.repo} path={[]} link={true} /><Tabs user={params.user} repo={params.repo} channel={data.channel} active="jobs" /><h1 class="pt-10">Job {data.id}</h1><div class="py-10">Started at {data.started}{#if ended}, ended at {ended} with status {status}{/if}.</div><h2 class="mt-5">Stdout</h2><div class="p-5"><div class="w-full whitespace-pre-wrap font-mono text-sm">{#each out[0] as m}{@html m}{/each}</div>{#if !out[0].length}<p>No stdout so far</p>{/if}</div><h2 class="mt-10">Stderr</h2><div class="p-5"><div class="w-full whitespace-pre-wrap font-mono text-sm">{#each out[1] as m}{@html m}{/each}</div>{#if !out[1].length}<p>No stderr so far</p>{/if}</div></div>
import type { PageLoad } from './$types';import { errorMsg, server } from '../../../helpers';export const load: PageLoad = async ({ fetch, params }) => {const u = `${server}/api/job/${params.user}/${params.repo}`;const resp = await fetch(u, { credentials: 'include' });if (resp.status == 200) {return await resp.json();} else {const y = await resp.json();errorMsg(resp.status, y);}};
<script lang="ts">import Nav from '../Nav.svelte';import Tabs from '../Tabs.svelte';import { resolve } from '$app/paths';const { data, params } = $props();</script><svelte:head><title>{data.owner} / {data.repo}</title></svelte:head><div class="p-3"><Nav user={params.user} repo={params.repo} path={[]} link={true} /><Tabsuser={params.user}repo={params.repo}login={data.user}channel={data.channel}active="jobs" /><div class="p-3"><h1 class="mt-5">Jobs</h1><table class="mt-10 table"><thead> <tr><td>Job</td><td>Started</td><td>Ended</td><td>Status</td> </tr></thead><tbody>{#each data.jobs as job (job.id)}<tr><td><aclass="link link-primary"href={resolve('/[user]/[repo]/jobs/[job]', {job: job.id,...params})}>{job.id}</a></td><td>{job.started}</td><td>{job.ended}</td><td>{job.status}</td></tr>{/each}</tbody></table></div></div>
export let data: {owner: string;repo: string;login: string;email: string;edit_comment?: string;d: DiscussionT;channels: string[];default_channel: string;comments: DiscussionItem[];uniq: number;token: string;};
const {data}: {data: {owner: string;repo: string;login: string;email: string;edit_comment?: string;d: DiscussionT;channels: string[];default_channel: string;comments: DiscussionItem[];uniq: number;token: string;};} = $props();
export let hash: string;export let id: string;export let disc: number;export let timestamp: number;export let pushed_by: string;export let removed: number | undefined;export let can_add: boolean = false;export let can_remove: boolean = false;export let owner: string;export let repo: string;export let token: string;export let authors: {login?: string;name?: string;key: string;}[] = [];export let header:| {message: string;timestamp: string;}| undefined;console.log('HEADER', JSON.stringify(header));
const {hash,id,disc,timestamp,pushed_by,removed,can_add = false,can_remove = false,owner,repo,token,authors,header}: {hash: string;id: string;disc: number;timestamp: number;pushed_by: string;removed?: number;can_add: boolean;can_remove: boolean;owner: string;repo: string;token: string;authors: {login?: string;name?: string;key: string;}[];header?: {message: string;timestamp: string;};} = $props();
// export let author = '';// export let id = '';export let disc: { closed?: number; n: number } | undefined = undefined;// export let timestamp: number;// export let value = '';// export let value_html = '';export let owner: string;export let repo: string;export let edit: boolean = false;export let login: string;export let token: string;export let uniq: number;
export let comment: Comment = {author: login,id: '',timestamp: 0,content: '',content_html: ''};
let {disc,owner,repo,edit = false,login,token,uniq,comment = {author: login,id: '',timestamp: 0,content: '',content_html: ''}}: {disc?: { closed?: number; n: number };owner: string;repo: string;edit: boolean;login: string;token: string;uniq: string;comment: Comment;} = $props();
(m.metadata & 0x80 ? 'w' : '-') +(m.metadata & 0x40 ? 'r' : '-') +(m.metadata & 0x20 ? 'x' : '-') +(m.metadata & 0x10 ? 'w' : '-') +(m.metadata & 0x8 ? 'r' : '-') +(m.metadata & 0x4 ? 'x' : '-') +(m.metadata & 0x2 ? 'w' : '-') +(m.metadata & 0x1 ? 'r' : '-');
(m.metadata & 0x80 ? 'w' : '-') +(m.metadata & 0x40 ? 'r' : '-') +(m.metadata & 0x20 ? 'x' : '-') +(m.metadata & 0x10 ? 'w' : '-') +(m.metadata & 0x8 ? 'r' : '-') +(m.metadata & 0x4 ? 'x' : '-') +(m.metadata & 0x2 ? 'w' : '-') +(m.metadata & 0x1 ? 'r' : '-'));
export let data: {deps: Deps;authors: { login?: string; key: string }[];hash: string;repo: string;owner: string;header: {message: string;timestamp: string;description: string | null;
const {data}: {data: {deps: Deps;authors: { login?: string; key: string }[];hash: string;repo: string;owner: string;header: {message: string;timestamp: string;description: string | null;};hunks: Hunk[];
hunks: Hunk[];};let date = new Intl.DateTimeFormat('en-US', {dateStyle: 'medium',timeStyle: 'short'}).format(new Date(data.header.timestamp));
} = $props();let date = $derived(new Intl.DateTimeFormat('en-US', {dateStyle: 'medium',timeStyle: 'short'}).format(new Date(data.header.timestamp)));
export let login: string;export let owner: string;export let repo: string;export let perms: number;export let everybody: boolean = false;export let n: number;export let token: string;
let props: {login: string;owner: string;repo: string;perms: number;everybody?: boolean;n: number;token: string;} = $props();
<form method="POST" action="/api/admin/{owner}/{repo}/permission"><input type="hidden" name="token" value={token} />
<form method="POST" action="/api/admin/{props.owner}/{props.repo}/permission"><input type="hidden" name="token" value={props.token} />
disabled={login == owner}checked={!!(perms & (1 << k))}id="{n}-{k}" /><label class="label ms-1" for="{n}-{k}"> {label} </label>
disabled={props.login == props.owner}checked={!!(props.perms & (1 << k))}id="{props.n}-{k}" /><label class="label ms-1" for="{props.n}-{k}"> {label} </label>
export let owner: string;export let repo: string;export let n: null | number = null;export let tagColor: number = colors[0];export let name = '';export let token: string;export let id: string | null = null;
let {owner,repo,n = null,tagColor = $bindable(colors[0]),name = $bindable(''),token,id = null}: {owner: string;repo: string;n: null | number;tagColor: number;name: string;token: string;id: string | null;} = $props();
import { page } from '$app/state';export let user: string;export let repo: string;export let login: string | undefined = page.data.login;export let channel: string | undefined = undefined;export let active: 'tree' | 'changes' | 'tags' | 'discussion' | 'ci' | 'admin';
const {user,repo,login,channel,active}: {user: string;repo: string;login?: string;channel?: string;active: 'tree' | 'changes' | 'tags' | 'discussion' | 'ci' | 'jobs' | 'admin';} = $props();
export let user = '';export let repo = '';export let path: { basename: string; pos: string }[] = [];export let link = false;
const {user,repo,path,link = false}: {user: string;repo: string;path: { basename: string; pos: string }[];link: boolean;} = $props();
export let owner: string;export let repo: string;export let channel: string;export let channels: string[];export let can_delete: boolean = false;export let token: string;export let tab: string;
let {owner,repo,channel = $bindable(),channels,can_delete = false,token,tab}: {owner: string;repo: string;channel: string;channels: string[];can_delete: boolean;token: string;tab: string;} = $props();
@import 'tailwindcss';@plugin "daisyui";@plugin "@iconify/tailwind4";@layer base {h1 {font-size: var(--text-2xl) !important;}h2 {font-size: var(--text-xl) !important;}h3 {font-size: var(--text-l) !important;}}
version: 5.5.4(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))
version: 5.5.4(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))'@sveltejs/vite-plugin-svelte':specifier: ^7.0.0version: 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
version: 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
version: 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
'@sveltejs/vite-plugin-svelte-inspector@5.0.2':resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==}
'@sveltejs/vite-plugin-svelte@7.0.0':resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==}
'@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))':
'@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))':
'@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
'@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
'@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))':
'@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))':
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
'@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
'@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))':dependencies:'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))obug: 2.1.1svelte: 5.55.4(@typescript-eslint/types@8.59.0)vite: 8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)
'@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))':
'@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))':
'@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.10(esbuild@0.27.7)(jiti@2.6.1)(sass@1.77.6))
CREATE TABLE jobs(id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),repo UUID NOT NULL REFERENCES repositories(id),started TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),ended TIMESTAMP WITH TIME ZONE,status INTEGER);UPDATE permissions SET perm = perm | 0x100;GRANT ALL PRIVILEGES ON jobs TO pijul;
DROP TABLE jobs;
diesel::dsl::sql::<Bool>("EXISTS (SELECT 1 FROM permissions WHERE permissions.user_id = '00000000-0000-0000-0000-000000000000' AND permissions.repo_id = repositories.id)")
diesel::dsl::sql::<Bool>("EXISTS (SELECT 1 FROM permissions WHERE permissions.user_id = '00000000-0000-0000-0000-000000000000' AND permissions.repo_id = repositories.id)",)
use libpijul::pristine::sanakirja::MutTxn;use libpijul::{ChannelMutTxnT, MutTxnT, MutTxnTExt, TxnT, TxnTExt};
use diesel::{ExpressionMethods, QueryDsl};use diesel_async::RunQueryDsl;use libpijul::changestore::ChangeStore;use libpijul::fs::FsErrorC;use libpijul::output::{FileError, OutputError};use libpijul::pristine::sanakirja::MutTxn0;use libpijul::pristine::sanakirja::SanakirjaError;use libpijul::pristine::{ForkError, TreeErr, TxnErr};use libpijul::{ApplyError, ArcTxn, ChannelMutTxnT, ChannelRef, MutTxnT, MutTxnTExt, TxnT, TxnTExt,UnrecordError,};use serde_derive::*;
pub fn new(locks: RepositoryLocks, db: crate::config::Db) -> Self {H { locks, db }
pub fn new(ci: crate::config_file::CiConfig,jobs: crate::config::Jobs,locks: RepositoryLocks,db: crate::config::Db,builders: std::sync::Arc<tokio::sync::Semaphore>,) -> Self {H {ci,jobs,locks,db,builders,}
Unrec(#[from] UnrecordError<crate::repository::changestore::Error, MutTxn<()>>),
Unrec(#[from] UnrecordError<crate::repository::changestore::Error, std::io::Error, MutTxn0>),#[error(transparent)]Apply(#[from] ApplyError<crate::repository::changestore::Error, MutTxn0>),
let mut txn = pri.mut_txn_begin()?;let channel = format!("{}_{}", repo, channel);let channel_ = txn.open_or_create_channel(&channel)?;let result =txn.apply_change(&repo_.changes, &mut *channel_.write(), &hash);
let txn = pri.arc_txn_begin()?;let channel_ = format!("{}_{}", repo, channel);let mut txn_ = txn.write();let channel_ = txn_.open_or_create_channel(&channel_)?;let mut channel__ = channel_.write();let result = txn_.apply_change(&repo_.changes, &mut *channel__, &hash);
Update::Eof { repo, channel } => {let repo_ = s.locks.get(&repo).await.unwrap();tokio::task::spawn_blocking(move || {let pri = repo_.pristine.blocking_write();let txn = pri.arc_txn_begin()?;let channel = format!("{}_{}", repo, channel);let channel_ = {let mut txn_ = txn.write();txn_.open_or_create_channel(&channel)?};s.deploy(&txn, &channel_, &repo_.changes, repo)?;Ok::<_, Error>(())});}
#[derive(Debug, Deserialize)]struct Config {deployment: Option<String>,}fn get_file<C: ChangeStore<Error = crate::repository::changestore::Error>>(txn: &ArcTxn<libpijul::pristine::sanakirja::MutTxn0>,channel: &ChannelRef<libpijul::pristine::sanakirja::MutTxn0>,changes: &C,path: &str,) -> Result<Option<String>, Error> {let txn_ = txn.read();let channel_ = channel.read();let (pos, is_dir) = txn_.follow_oldest_path(changes, &channel, path)?;if is_dir {return Ok(None);}let mut out = crate::repository::RawVertexBuf { out: Vec::new() };use libpijul::ChannelTxnT;let mut graph = libpijul::alive::retrieve(&*txn_, txn_.graph(&*channel_), pos, false)?;let mut forward = Vec::new();std::mem::drop(channel_);std::mem::drop(txn_);libpijul::alive::output_graph(changes, &txn, &channel, &mut out, &mut graph, &mut forward).map_err(|x| Error::File(x))?;debug!("{:?}", out);Ok(Some(String::from_utf8(out.out)?))}impl H {fn deploy<C: ChangeStore<Error = crate::repository::changestore::Error> + Clone + Send + Sync + 'static,>(&self,txn: &ArcTxn<libpijul::pristine::sanakirja::MutTxn0>,channel: &ChannelRef<libpijul::pristine::sanakirja::MutTxn0>,changes: &C,repo: uuid::Uuid,) -> Result<(), Error> {if let Some(config) = get_file(txn, channel, changes, "pijul.toml")? {debug!("config = {:?}", config);if let Ok(parsed) = toml::from_str::<Config>(&config) {if let Some(depl) = parsed.deployment {let db = self.db.clone();let txn = txn.clone();let channel = channel.clone();let changes = changes.clone();let jobs = self.jobs.clone();let builders = self.builders.clone();let ci = self.ci.clone();tokio::spawn(async move {let permit = builders.acquire().await.unwrap();use crate::db::jobs::dsl as jobs;let id = diesel::insert_into(jobs::jobs).values((jobs::repo.eq(repo),)).returning(jobs::id).get_result::<uuid::Uuid>(&mut db.get().await.unwrap()).await?;let tmp_dir = tempfile::tempdir()?;let wc = libpijul::working_copy::filesystem::FileSystem::from_root(tmp_dir.path(),);tokio::task::spawn_blocking(move || {libpijul::output::output_repository_no_pending(&wc, &changes, &txn, &channel, "", true, None, 1, 0,)?;Ok::<_, Error>(())}).await.unwrap()?;use std::process::Stdio;let (status_tx, status_rx) = tokio::sync::watch::channel(None);let (kill_tx, kill_rx) = tokio::sync::oneshot::channel();let mut cmd = tokio::process::Command::new(tmp_dir.path().join(depl)).current_dir(tmp_dir.path()).stderr(Stdio::piped()).stdout(Stdio::piped()).stdin(Stdio::null()).spawn()?;use tokio::io::AsyncBufReadExt;let stdout = tokio::io::BufReader::new(cmd.stdout.take().unwrap());let mut stdout = stdout.lines();let mut stdout_ok = true;let stderr = tokio::io::BufReader::new(cmd.stderr.take().unwrap());let mut stderr = stderr.lines();jobs.lock().unwrap().insert(id, (kill_tx, status_tx.clone(), status_rx));let mut stderr_ok = true;let mut buf_stdout = String::new();let mut last_stdout = std::time::UNIX_EPOCH;let mut buf_stderr = String::new();let mut last_stderr = std::time::UNIX_EPOCH;let bound = std::time::Duration::from_secs(1);let mut files =if let Some(ref path) = ci.filesystem{Some((OpenOptions::new().append(true).open(&path.join(&format!("{}.stdout", id))).await?,OpenOptions::new().append(true).open(&path.join(&format!("{}.stderr", id))).await?,))} else {None};while stdout_ok || stderr_ok {debug!("stdout || stderr {:?} {:?}",buf_stdout.len(),buf_stderr.len());tokio::select! {line = stdout.next_line(), if stdout_ok => {let n = if let Some(line) = line? {buf_stdout.push_str(&line);buf_stdout.push('\n');line.len()} else {0};if last_stdout.elapsed().unwrap() >= bound || n == 0 {debug!("sending stdout to db {:?} {:?}", buf_stdout.len(), buf_stderr.len());if let Some((ref mut stdout, _)) = files {stdout.write_all(buf_stdout.as_bytes()).await?;}buf_stdout.clear();debug!("stdout/stderr {:?} {:?}", buf_stdout.len(), buf_stderr.len());last_stdout = std::time::SystemTime::now();}if n == 0 {stdout_ok = false}}line = stderr.next_line(), if stderr_ok => {let n = if let Some(line) = line ?{buf_stderr.push_str(&line);buf_stderr.push('\n');line.len()} else {0};if last_stderr.elapsed().unwrap() >= bound || n == 0 {debug!("sending stderr to db {:?}", buf_stderr.len());if let Some((_, ref mut stderr)) = files {stderr.write_all(buf_stderr.as_bytes()).await?;}buf_stderr.clear();last_stderr = std::time::SystemTime::now();}debug!("{:?}", buf_stderr.len());if n == 0 {stderr_ok = false}}}}let status = tokio::select! {status = cmd.wait() => {status?.code()}_ = kill_rx => {cmd.kill().await?;None}};debug!("process exited with {:?}", status);debug!("stderr {}", buf_stderr);debug!("stdout {}", buf_stdout);let now = chrono::Utc::now();diesel::update(jobs::jobs.find(id)).set((jobs::status.eq(status), jobs::ended.eq(&now))).execute(&mut db.get().await.unwrap()).await?;status_tx.send(Some((now, status))).unwrap();jobs.lock().unwrap().remove(&id);std::mem::drop(permit);Ok::<_, Error>(())});}}} else {debug!("No pijul.toml");}Ok(())}}
use crate::permissions::Perm;use crate::{Config, get_user_login};use axum::{Router,extract::ws::{WebSocket, WebSocketUpgrade},extract::{Json, Path, State},response::Response,routing::{any, get},};use axum_extra::extract::SignedCookieJar;use diesel::{BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, OptionalExtension,QueryDsl, Queryable, QueryableByName, Selectable, SelectableHelper,};use diesel_async::RunQueryDsl;use futures::StreamExt;use inotify::WatchMask;use serde_derive::*;use tokio::io::{AsyncReadExt, AsyncSeekExt};use tracing::*;pub fn router() -> Router<Config> {Router::new().route("/{owner}/{repo}", get(list_jobs)).route("/{owner}/{repo}/{job_id}", get(job)).route("/{owner}/{repo}/{job_id}/ws", any(ws_handler))}#[derive(Debug, Deserialize)]pub struct JobPath {owner: String,repo: String,job_id: uuid::Uuid,}#[derive(Debug, Deserialize)]pub struct JobsPath {owner: String,repo: String,}#[derive(Debug, Selectable, Queryable, QueryableByName, Serialize)]#[diesel(check_for_backend(diesel::pg::Pg))]#[diesel(table_name = crate::db::jobs::dsl)]struct Job {id: uuid::Uuid,started: chrono::DateTime<chrono::Utc>,ended: Option<chrono::DateTime<chrono::Utc>>,status: Option<i32>,}#[derive(Debug, Serialize)]pub struct Jobs {login: Option<String>,jobs: Vec<Job>,}pub async fn list_jobs(State(config): State<Config>,jar: SignedCookieJar,Path(path): Path<JobsPath>,) -> Result<Json<Jobs>, crate::Error> {let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {(Some(a), Some(b))} else {(None, None)};let mut db = config.db.get().await?;use crate::db::jobs::dsl as jobs;use crate::db::repositories::dsl as repos;use crate::db::users::dsl as users;Ok(Json(Jobs {login,jobs: repos::repositories.inner_join(jobs::jobs).inner_join(users::users).filter(users::login.eq(path.owner)).filter(repos::name.eq(path.repo)).filter(repos::owner.nullable().eq(uid).or(crate::has_permissions!(uid.unwrap_or(uuid::Uuid::nil()),repos::id,Perm::READ_JOBS.bits()))).select(Job::as_select()).order_by(jobs::started.desc()).get_results::<Job>(&mut db).await?,}))}#[derive(Debug, Serialize)]pub struct Job_ {login: Option<String>,#[serde(flatten)]job: Job,}pub async fn job(State(config): State<Config>,jar: SignedCookieJar,Path(path): Path<JobPath>,) -> Result<Json<Job_>, crate::Error> {let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {(Some(a), Some(b))} else {(None, None)};let mut db = config.db.get().await?;use crate::db::jobs::dsl as jobs;use crate::db::repositories::dsl as repos;use crate::db::users::dsl as users;if let Some(job) = repos::repositories.inner_join(jobs::jobs).inner_join(users::users).filter(users::login.eq(path.owner)).filter(repos::name.eq(path.repo)).filter(repos::owner.nullable().eq(uid).or(crate::has_permissions!(uid.unwrap_or(uuid::Uuid::nil()),repos::id,Perm::READ_JOBS.bits()))).filter(jobs::id.eq(path.job_id)).select(Job::as_select()).order_by(jobs::started.desc()).get_result::<Job>(&mut db).await.optional()?{Ok(Json(Job_ { login, job }))} else {Err(crate::Error::NotFound)}}pub async fn ws_handler(State(config): State<Config>,Path(path): Path<JobPath>,ws: WebSocketUpgrade,) -> Response {ws.on_upgrade(move |socket| handle_socket(config, path.job_id, socket))}#[derive(Debug, Deserialize, Serialize)]enum Msg<'a> {State {stdout: usize,stderr: usize,},Chunk {channel: i32,offset: usize,content: &'a str,},Status {ended: chrono::DateTime<chrono::Utc>,status: Option<i32>,},}async fn handle_socket(config: Config, id: uuid::Uuid, mut socket: WebSocket) {let mut remote_stdout = 0;let mut remote_stderr = 0;let Some(mut status) = config.jobs.lock().unwrap().get(&id).map(|(_, _, w)| w.clone())else {debug!("closing");if let Some((a, b, c)) = send_all(&config, id, &mut remote_stdout, &mut remote_stderr).await.unwrap(){socket.send(a.into()).await.unwrap_or(());socket.send(b.into()).await.unwrap_or(());if let Some(c) = c {socket.send(c.into()).await.unwrap_or(());}}return;};let mut status_ok = true;let mut notify_buffer = [0; 1024];let mut notify = if let Some(ref path) = config.ci.filesystem{let inotify = inotify::Inotify::init().expect("Error while initializing inotify instance");let mut w = inotify.watches();w.add(&path.join(&format!("{}.stdout", id)),WatchMask::MODIFY | WatchMask::CLOSE,).unwrap();w.add(&path.join(&format!("{}.stderr", id)),WatchMask::MODIFY | WatchMask::CLOSE,).unwrap();inotify.into_event_stream(&mut notify_buffer).unwrap()} else {inotify::Inotify::init().unwrap().into_event_stream(&mut notify_buffer).unwrap()};let mut stdout = String::new();let mut stderr = String::new();loop {debug!("waiting job");tokio::select! {_ = notify.next() => {if let Some((a, b)) = send_remaining(&config, id, &mut remote_stdout, &mut remote_stderr, &mut stdout, &mut stderr).await.unwrap(){socket.send(a.into()).await.unwrap_or(());socket.send(b.into()).await.unwrap_or(());}}x = status.changed(), if status_ok => {debug!("status {:?}", x);if x.is_err() {status_ok = false}let status = status.borrow_and_update().clone();debug!("status {:?}", status);if let Some((ended, status)) = status {socket.send(serde_json::to_string(&Msg::Status {ended,status}).unwrap().into()).await.unwrap_or(());} else {debug!("Nothing to send");}},else => break}}}async fn send_all(config: &Config,id: uuid::Uuid,remote_stdout: &mut usize,remote_stderr: &mut usize,) -> Result<Option<(String, String, Option<String>)>, crate::Error> {use crate::db::jobs::dsl as jobs;if let Some((ended, status)) = jobs::jobs.find(id).select((jobs::ended, jobs::status)).get_result::<(Option<chrono::DateTime<chrono::Utc>>, Option<i32>)>(&mut config.db.get().await.unwrap(),).await.optional().unwrap(){let mut stdout = String::new();let mut stderr = String::new();debug!("send_all {:?} {:?}", stdout.len(), stderr.len());if let Some(ref path) = config.ci.filesystem {let mut outf = tokio::fs::File::open(&path.join(&format!("{}.stdout", id))).await?;let mut errf = tokio::fs::File::open(&path.join(&format!("{}.stderr", id))).await?;outf.seek(std::io::SeekFrom::Start(*remote_stdout as u64)).await?;errf.seek(std::io::SeekFrom::Start(*remote_stderr as u64)).await?;outf.read_to_string(&mut stdout).await?;errf.read_to_string(&mut stderr).await?;}*remote_stdout = stdout.len();*remote_stderr = stderr.len();Ok(Some((serde_json::to_string(&Msg::Chunk {channel: 0,offset: 0,content: &stdout,}).unwrap(),serde_json::to_string(&Msg::Chunk {channel: 1,offset: 0,content: &stderr,}).unwrap(),ended.map(|ended| serde_json::to_string(&Msg::Status { ended, status }).unwrap()),)))} else {Ok(None)}}async fn send_remaining(config: &Config,id: uuid::Uuid,remote_stdout: &mut usize,remote_stderr: &mut usize,stdout: &mut String,stderr: &mut String,) -> Result<Option<(String, String)>, crate::Error> {if let Some(ref path) = config.ci.filesystem {let mut outf = tokio::fs::File::open(&path.join(&format!("{}.stdout", id))).await?;let mut errf = tokio::fs::File::open(&path.join(&format!("{}.stderr", id))).await?;outf.seek(std::io::SeekFrom::Start(*remote_stdout as u64)).await?;errf.seek(std::io::SeekFrom::Start(*remote_stderr as u64)).await?;outf.read_to_string(stdout).await?;errf.read_to_string(stderr).await?;Ok(Some((serde_json::to_string(&Msg::Chunk {channel: 0,offset: 0,content: &stdout,}).unwrap(),serde_json::to_string(&Msg::Chunk {channel: 1,offset: 0,content: &stderr,}).unwrap(),)))} else {Ok(None)}}
#[error("This user is inactive. Please contact <a href=\"mailto:support@pijul.org\">support@pijul.org</a> to resolve this situation.")]
#[error("This user is inactive. Please contact <a href=\"mailto:support@pijul.org\">support@pijul.org</a> to resolve this situation.")]
};"inotify" = rec {crateName = "inotify";version = "0.11.1";edition = "2018";sha256 = "16fiffnqhfdwzgrv3wcnaih0a9xbx1a44nma1yn5idr83apkwnxx";authors = ["Hanno Braun <mail@hannobraun.de>""Félix Saparelli <me@passcod.name>""Cristian Kubis <cristian.kubis@tsunix.de>""Frank Denis <github@pureftpd.org>"];dependencies = [{name = "bitflags";packageId = "bitflags 2.10.0";}{name = "futures-core";packageId = "futures-core";optional = true;}{name = "inotify-sys";packageId = "inotify-sys";}{name = "libc";packageId = "libc";}{name = "tokio";packageId = "tokio";optional = true;features = [ "net" ];}];devDependencies = [{name = "tokio";packageId = "tokio";features = [ "macros" "rt-multi-thread" "time" ];}];features = {"default" = [ "stream" ];"futures-core" = [ "dep:futures-core" ];"stream" = [ "futures-core" "tokio" ];"tokio" = [ "dep:tokio" ];};resolvedDefaultFeatures = [ "default" "futures-core" "stream" "tokio" ];};"inotify-sys" = rec {crateName = "inotify-sys";version = "0.1.5";edition = "2015";sha256 = "1syhjgvkram88my04kv03s0zwa66mdwa5v7ddja3pzwvx2sh4p70";libName = "inotify_sys";authors = ["Hanno Braun <hb@hannobraun.de>"];dependencies = [{name = "libc";packageId = "libc";}];
"subtle","zeroize",][[package]]name = "curve25519-dalek"version = "4.1.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"dependencies = ["cfg-if","cpufeatures","curve25519-dalek-derive","fiat-crypto","rustc_version",
][[package]]name = "jiff-tzdb"version = "0.1.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"[[package]]name = "jiff-tzdb-platform"version = "0.1.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"dependencies = ["jiff-tzdb",
version = "0.38.44"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"dependencies = ["bitflags 2.10.0","errno","libc","linux-raw-sys 0.4.15","windows-sys 0.52.0",][[package]]name = "rustix"
name = "toml_edit"version = "0.22.27"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"dependencies = ["indexmap","serde","serde_spanned 0.6.9","toml_datetime 0.6.11","toml_write","winnow",][[package]]
[[package]]name = "tungstenite"version = "0.28.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"dependencies = ["bytes","data-encoding","http 1.4.0","httparse","log","rand 0.9.2","sha1","thiserror 2.0.17","utf-8",]
version = "0.48.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"dependencies = ["windows_aarch64_gnullvm 0.48.5","windows_aarch64_msvc 0.48.5","windows_i686_gnu 0.48.5","windows_i686_msvc 0.48.5","windows_x86_64_gnu 0.48.5","windows_x86_64_gnullvm 0.48.5","windows_x86_64_msvc 0.48.5",][[package]]name = "windows-targets"