radicle-cli 0.20.0

Radicle CLI
Documentation
use std::collections::HashSet;

use radicle::git::Url;
use radicle::identity::{Did, RepoId};
use radicle::node::{Alias, AliasStore as _, NodeId};
use radicle::storage::ReadStorage as _;
use radicle::Profile;
use radicle_term::{Element, Table};

use crate::git;
use crate::terminal as term;

#[derive(Debug)]
pub enum Direction {
    Push(Url),
    Fetch(Url),
}

#[derive(Debug)]
pub struct Tracked {
    name: String,
    direction: Option<Direction>,
}

impl Tracked {
    fn namespace(&self) -> Option<NodeId> {
        match self.direction.as_ref()? {
            Direction::Push(url) => url.namespace,
            Direction::Fetch(url) => url.namespace,
        }
    }
}

#[derive(Debug)]
pub struct Untracked {
    remote: NodeId,
    alias: Option<Alias>,
}

pub fn tracked(working: &git::Repository) -> anyhow::Result<Vec<Tracked>> {
    Ok(git::rad_remotes(working)?
        .into_iter()
        .flat_map(|remote| {
            [
                Tracked {
                    name: remote.name.clone(),
                    direction: Some(Direction::Fetch(remote.url)),
                },
                Tracked {
                    name: remote.name,
                    direction: remote.pushurl.map(Direction::Push),
                },
            ]
        })
        .collect())
}

pub fn untracked<'a>(
    rid: RepoId,
    profile: &Profile,
    tracked: impl Iterator<Item = &'a Tracked>,
) -> anyhow::Result<Vec<Untracked>> {
    let repo = profile.storage.repository(rid)?;
    let aliases = profile.aliases();
    let remotes = repo.remote_ids()?;
    let git_remotes = tracked
        .filter_map(|tracked| tracked.namespace())
        .collect::<HashSet<_>>();
    Ok(remotes
        .filter_map(|remote| {
            remote
                .map(|remote| {
                    (!git_remotes.contains(&remote)).then_some(Untracked {
                        remote,
                        alias: aliases.alias(&remote),
                    })
                })
                .transpose()
        })
        .collect::<Result<Vec<_>, _>>()?)
}

pub fn print_tracked<'a>(tracked: impl Iterator<Item = &'a Tracked>) {
    Table::from_iter(tracked.into_iter().flat_map(|Tracked { direction, name }| {
        let Some(direction) = direction else {
            return None;
        };

        let (dir, url) = match direction {
            Direction::Push(url) => ("push", url),
            Direction::Fetch(url) => ("fetch", url),
        };
        let description = url.namespace.map_or(
            term::format::dim("(canonical upstream)".to_string()).italic(),
            |namespace| term::format::tertiary(namespace.to_string()),
        );
        Some([
            term::format::bold(name.clone()),
            description,
            term::format::parens(term::format::secondary(dir.to_owned())),
        ])
    }))
    .print();
}

pub fn print_untracked<'a>(untracked: impl Iterator<Item = &'a Untracked>) {
    Table::from_iter(untracked.into_iter().map(|Untracked { remote, alias }| {
        [
            match alias {
                None => term::format::secondary("n/a".to_string()),
                Some(alias) => term::format::secondary(alias.to_string()),
            },
            term::format::highlight(Did::from(remote).to_string()),
        ]
    }))
    .print();
}