~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
138 lines · 5.4 KB Raw History
//! What repositories share for others to use, as the index keeps it
//! (`index::shared`): the files of their releases, read and kept as what
//! they are, and which repositories use each.
//!
//! A shared step is a release of the package `@step/<name>` whose one
//! file is a step file, published with the commit it was made from, as
//! `knotless workflow pin` takes one. Its file is read once, from the
//! release's blobs on its owner's PDS, checked against its record and kept
//! ([`Records`]), as a release's files never change. One that is not a
//! step file, or names no commit, is kept as not one, and never shown.
//!
//! Who uses a step is read from `.knotless/uses.lock` on each public
//! repository's default branch, as what it is written in is counted
//! ([`crate::languages::follow`]): once a list shows it, and again as its
//! refs move. A repository is counted once for each step it pins, at
//! whatever versions.

use crate::App;
use crate::index::{Index, STEP, Shared};
use crate::releases::Records;
use knotless::url::RepoUrl;
use knotless::uses::{self, Use};
use serde_json::{Value, json};
use std::sync::Arc;

/// Reads the files of the releases of things shared as the index asks
/// ([`Index::reading`]): first those it kept before and has yet to read,
/// then each as it is indexed, one at a time.
pub async fn follow(app: Arc<App>) {
    let taken = app.index.clone().and_then(|i| Some((i.reading()?, i)));
    let Some((mut asked, index)) = taken else {
        return;
    };
    for uri in index.unread().unwrap_or_default() {
        read(&app, &index, &uri).await;
    }
    while let Some(uri) = asked.recv().await {
        read(&app, &index, &uri).await;
    }
}

/// Reads the file of the release at `uri` and keeps what it is. One that
/// cannot be read now, its owner's PDS away, is left to be read when
/// next the server starts.
async fn read(app: &App, index: &Index, uri: &str) -> Option<()> {
    let (thing, release) = index.shared_release(uri).ok().flatten()?;
    let name = index.name(&thing.did, &thing.key).ok().flatten()?;
    let found = match (&release.files[..], &release.commit) {
        ([file], Some(_)) => {
            let url = RepoUrl::parse(&format!("knotless://{}/{name}", thing.did)).ok()?;
            let records = Records::new(app.ctx.clone(), url, release.package.clone());
            let bytes = records.read(file).await.ok()?;
            step(&file.name, &bytes)
        }
        _ => None,
    };
    index.set_shared(uri, found.as_ref()).ok()
}

/// The step file `bytes`, named `file`, as the index keeps it: what it
/// does, takes and asks for. None for one that is not a step file.
pub fn step(file: &str, bytes: &[u8]) -> Option<Value> {
    let parsed = uses::parse_step(file, bytes).ok()?;
    let inputs = parsed.inputs.iter().map(|(name, input)| {
        let mut shown = json!({"name": name, "required": input.required});
        if !input.description.is_empty() {
            shown["description"] = input.description.clone().into();
        }
        if let Some(default) = &input.default {
            shown["default"] = default.clone().into();
        }
        shown
    });
    let description = Some(parsed.description).filter(|d| !d.trim().is_empty());
    Some(json!({
        "description": description,
        "inputs": inputs.collect::<Vec<_>>(),
        "needs": parsed.needs,
        "secrets": parsed.secrets,
    }))
}

/// Keeps what `did`'s repository with `key` uses, from `lock`, the lock
/// file on its default branch: nothing, with none, or one that does not
/// parse. A step whose repository the index does not have is passed by,
/// for a count after it does.
pub async fn read_uses(app: &App, index: &Index, did: &str, key: &str, lock: Option<&[u8]>) {
    let lock = lock.and_then(|text| uses::parse_lock(text).ok());
    let mut used = Vec::new();
    for named in lock.unwrap_or_default().keys() {
        if let Some(thing) = step_of(app, index, named).await {
            used.push(thing);
        }
    }
    let _ = index.set_uses(did, key, &used);
}

/// The step a lock's `uses` names, if the index has its repository.
async fn step_of(app: &App, index: &Index, named: &str) -> Option<Shared> {
    let used = Use::parse(named).ok()?;
    let owner = knotless::identity::resolve(&app.ctx, &used.owner).await;
    let did = owner.ok()?.did;
    let key = index.key_named(&did, &used.repo).ok().flatten()?;
    Some(Shared {
        kind: STEP.into(),
        did,
        key,
        name: used.step,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_step_file_is_kept_as_what_it_takes_and_asks_for() {
        let file = b"description: Waits.\ninputs:\n  workflows: {description: Which, required: true}\n  minutes: {default: \"25\"}\nneeds: [curl]\nsecrets: [TOKEN]\nsteps:\n  - run: echo\n";
        let kept = step("wait.yml", file).unwrap();
        assert_eq!(
            kept,
            json!({
                "description": "Waits.",
                "inputs": [
                    {"name": "minutes", "required": false, "default": "25"},
                    {"name": "workflows", "required": true, "description": "Which"},
                ],
                "needs": ["curl"],
                "secrets": ["TOKEN"],
            })
        );
        assert_eq!(
            step("x.yml", b"steps: []\n").unwrap()["description"],
            Value::Null
        );
        assert!(step("x.yml", b"not: a step\n").is_none());
    }
}