adnv 1.5.0

CLI that tracks, checks and auto-updates infrastructure dependency versions across Terraform, Helm, Docker, Git and npm
Documentation
use chrono::DateTime;

use super::{ListOptions, Tag};
use crate::{Error, Result};

pub async fn list_tags(source: &url::Url, options: &ListOptions<'_>) -> Result<Vec<Tag>> {
    let tmp_dir = tempfile::TempDir::with_prefix("adnv").map_err(Error::TempDirectory)?;
    let repo = tokio::task::spawn_blocking({
        // Both `git+https://host/repo` and the bare `git://host/repo` reach
        // this backend, and libgit2 understands the latter as is.
        let url = source
            .as_str()
            .strip_prefix("git+")
            .unwrap_or(source.as_str())
            .to_string();
        let path = tmp_dir.path().to_path_buf();

        move || {
            git2::build::RepoBuilder::new()
                .bare(true)
                .clone(&url, &path)
        }
    })
    .await
    .map_err(|err| Error::io(std::io::Error::other(err), "Clone task panicked"))?
    .map_err(|err| {
        Error::git(
            err,
            format!(
                "Could not clone source repository from {:?}",
                source.as_str()
            ),
        )
    })?;

    let versions: Vec<_> = repo
        .tag_names(None)
        .map_err(|err| {
            Error::git(
                err,
                format!("Could not list tags from {:?}", source.as_str()),
            )
        })?
        .iter_bytes()
        .filter_map(|name| match String::from_utf8(name.to_vec()) {
            Ok(name) => Some(name),
            Err(err) => {
                tracing::warn!("Bad tag name: {err}");
                None
            }
        })
        .filter_map(|name| match get_tag(&repo, &name) {
            Ok(version) => Some(version),
            Err(err) => {
                tracing::warn!("{}", miette::Report::new(err));
                None
            }
        })
        .collect();

    Ok(options.filter_tags(versions))
}

fn get_tag(repo: &git2::Repository, name: &str) -> Result<Tag> {
    let reference = repo
        .resolve_reference_from_short_name(name)
        .map_err(|err| Error::git(err, format!("Could not find reference `{name}`")))?;

    if let Some(tagger) = reference
        .peel_to_tag()
        .ok()
        .and_then(|tag| tag.tagger().map(|tagger| tagger.to_owned()))
    {
        let time = git_time_to_chrono(tagger.when()).ok_or_else(|| {
            Error::git(
                git2::Error::from_str("Invalid time"),
                "Could not convert time",
            )
        })?;
        Ok(Tag::new(name, time))
    } else if let Ok(commit) = reference.peel_to_commit() {
        let time = git_time_to_chrono(commit.time()).ok_or_else(|| {
            Error::git(
                git2::Error::from_str("Invalid time"),
                "Could not convert time",
            )
        })?;
        Ok(Tag::new(name, time))
    } else {
        Err(Error::git(
            git2::Error::from_str("Bad reference"),
            "Could not resolve repository version",
        ))
    }
}

fn git_time_to_chrono(time: git2::Time) -> Option<DateTime<chrono::Utc>> {
    use chrono::{DateTime, FixedOffset, TimeZone, Utc};

    let timestamp = time.seconds();
    let dt = DateTime::<Utc>::from_timestamp(timestamp, 0)?;

    Some(
        FixedOffset::west_opt(time.offset_minutes() * 60)?
            .from_utc_datetime(&dt.naive_utc())
            .into(),
    )
}