michal/tit

Browse tree · Show commit · Download archive

Blob: src/main.rs

Raw · Blame

mod account;
mod admin;
#[allow(
    dead_code,
    reason = "the bootstrap command uses only part of the authentication API"
)]
mod auth;
mod bootstrap;
mod cli;
mod config;
mod control;
mod domain;
mod feed;
#[allow(dead_code, reason = "the server uses only part of the shared Git API")]
mod git;
#[allow(dead_code, reason = "the server uses only part of the shared HTTP API")]
mod http;
mod instance;
mod issue;
mod markdown;
mod policy;
mod repository;
mod serve;
mod session;
#[allow(dead_code, reason = "the server uses only part of the shared SSH API")]
mod ssh;
mod store;
mod watch;

use std::process::ExitCode;
use std::{io, io::Write};

use clap::Parser;

use crate::cli::{
    AccountCommand, AdminCommand, Cli, CollaboratorRole, Command, ObjectFormat, RepositoryCommand,
    RepositoryVisibility, SetupCommand,
};

#[tokio::main]
async fn main() -> ExitCode {
    let cli = match Cli::try_parse() {
        Ok(cli) => cli,
        Err(error) => {
            let code = error.exit_code();
            let _ = error.print();
            return ExitCode::from(u8::try_from(code).unwrap_or(2));
        }
    };

    match config::load(&cli) {
        Ok(config) => match cli.command {
            None => ExitCode::SUCCESS,
            Some(Command::Serve) => match serve::run(&config).await {
                Ok(()) => ExitCode::SUCCESS,
                Err(error) => {
                    eprintln!("tit: {error}");
                    ExitCode::FAILURE
                }
            },
            Some(Command::InviteCode) => {
                match control::request_invitation(&config.instance_dir).await {
                    Ok(code) => match writeln!(io::stdout().lock(), "Signup code: {code}") {
                        Ok(()) => ExitCode::SUCCESS,
                        Err(error) => {
                            eprintln!("tit: cannot write the signup code: {error}");
                            ExitCode::FAILURE
                        }
                    },
                    Err(error) => {
                        eprintln!("tit: {error}");
                        ExitCode::FAILURE
                    }
                }
            }
            Some(Command::Doctor) => match store::doctor(&config.instance_dir) {
                Ok(()) => ExitCode::SUCCESS,
                Err(error) => {
                    eprintln!("tit: {error}");
                    ExitCode::FAILURE
                }
            },
            Some(Command::Setup {
                command:
                    SetupCommand::Admin {
                        username,
                        ssh_public_key,
                    },
            }) => match bootstrap::setup_administrator(
                &config.instance_dir,
                &username,
                &ssh_public_key,
            ) {
                Ok(recovery_code) => {
                    let mut output = io::stdout().lock();
                    match writeln!(output, "Recovery code: {recovery_code}") {
                        Ok(()) => ExitCode::SUCCESS,
                        Err(error) => {
                            eprintln!("tit: cannot write the recovery code: {error}");
                            ExitCode::FAILURE
                        }
                    }
                }
                Err(error) => {
                    eprintln!("tit: {error}");
                    ExitCode::FAILURE
                }
            },
            Some(Command::Admin {
                command: AdminCommand::Repository { command },
            }) => run_repository_command(&config.instance_dir, command),
            Some(Command::Admin {
                command: AdminCommand::Account { command },
            }) => run_account_command(&config.instance_dir, command),
            Some(Command::Admin {
                command: AdminCommand::Audit { limit },
            }) => run_audit_command(&config.instance_dir, limit),
        },
        Err(error) => {
            eprintln!("tit: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run_audit_command(instance_dir: &std::path::Path, limit: usize) -> ExitCode {
    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
        let _lock = instance::InstanceLock::acquire(instance_dir)?;
        let database = instance::prepare_database(instance_dir)?;
        let events = store::Store::open(&database)?.audit_events(limit)?;
        let mut output = io::stdout().lock();
        for event in events {
            writeln!(output, "id={}", event.id)?;
            writeln!(output, "action={}", event.action)?;
            writeln!(output, "actor={}", event.actor)?;
            writeln!(output, "target={}", event.target)?;
            writeln!(output, "outcome={}", event.outcome)?;
            writeln!(output, "correlation-id={}", event.correlation_id)?;
            writeln!(output, "created-at={}", event.created_at)?;
            writeln!(output)?;
        }
        Ok(())
    })();
    match result {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("tit: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run_account_command(instance_dir: &std::path::Path, command: AccountCommand) -> ExitCode {
    let result = (|| -> Result<Option<String>, Box<dyn std::error::Error>> {
        let _lock = instance::InstanceLock::acquire(instance_dir)?;
        let database = instance::prepare_database(instance_dir)?;
        let accounts = account::AccountService::new(database);
        let correlation_id = format!("{:032x}", rand::random::<u128>());
        match command {
            AccountCommand::KeyAdd {
                username,
                label,
                ssh_public_key,
            } => {
                let fingerprint = accounts.add_key(
                    &username,
                    &label,
                    &ssh_public_key,
                    "admin-cli",
                    &correlation_id,
                )?;
                Ok(Some(fingerprint))
            }
            AccountCommand::KeyRevoke {
                username,
                fingerprint,
            } => {
                accounts.revoke_key(&username, &fingerprint, "admin-cli", &correlation_id)?;
                Ok(None)
            }
            AccountCommand::Suspend { username } => {
                accounts.suspend(&username, true, "admin-cli", &correlation_id)?;
                Ok(None)
            }
            AccountCommand::Resume { username } => {
                accounts.suspend(&username, false, "admin-cli", &correlation_id)?;
                Ok(None)
            }
        }
    })();
    match result {
        Ok(Some(fingerprint)) => match writeln!(io::stdout().lock(), "fingerprint={fingerprint}") {
            Ok(()) => ExitCode::SUCCESS,
            Err(error) => {
                eprintln!("tit: cannot write account information: {error}");
                ExitCode::FAILURE
            }
        },
        Ok(None) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("tit: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run_repository_command(instance_dir: &std::path::Path, command: RepositoryCommand) -> ExitCode {
    let result = match command {
        RepositoryCommand::Create {
            owner,
            slug,
            object_format,
        } => admin::create_repository(
            instance_dir,
            &owner,
            &slug,
            match object_format {
                ObjectFormat::Sha1 => gix::hash::Kind::Sha1,
                ObjectFormat::Sha256 => gix::hash::Kind::Sha256,
            },
        ),
        RepositoryCommand::Import {
            owner,
            slug,
            source,
        } => admin::import_repository(instance_dir, &owner, &slug, &source),
        RepositoryCommand::Rename {
            owner,
            old_slug,
            new_slug,
        } => admin::rename_repository(instance_dir, &owner, &old_slug, &new_slug),
        RepositoryCommand::Archive { owner, slug } => {
            admin::archive_repository(instance_dir, &owner, &slug)
        }
        RepositoryCommand::Visibility {
            owner,
            slug,
            visibility,
        } => admin::set_repository_visibility(
            instance_dir,
            &owner,
            &slug,
            match visibility {
                RepositoryVisibility::Public => "public",
                RepositoryVisibility::Private => "private",
            },
        ),
        RepositoryCommand::CollaboratorSet {
            owner,
            slug,
            username,
            role,
        } => admin::set_repository_collaborator(
            instance_dir,
            &owner,
            &slug,
            &username,
            match role {
                CollaboratorRole::Maintainer => "maintainer",
                CollaboratorRole::Writer => "writer",
                CollaboratorRole::Reader => "reader",
            },
        ),
        RepositoryCommand::CollaboratorRemove {
            owner,
            slug,
            username,
        } => admin::remove_repository_collaborator(instance_dir, &owner, &slug, &username),
        RepositoryCommand::Inspect { owner, slug } => {
            admin::inspect_repository(instance_dir, &owner, &slug)
        }
    };

    match result {
        Ok(repository) => {
            let path = match admin::repository_path(instance_dir, &repository) {
                Ok(path) => path,
                Err(error) => {
                    eprintln!("tit: {error}");
                    return ExitCode::FAILURE;
                }
            };
            let archived_at = repository
                .archived_at
                .map_or_else(|| "-".to_owned(), |value| value.to_string());
            let mut output = io::stdout().lock();
            let written = writeln!(output, "id={}", repository.id)
                .and_then(|()| writeln!(output, "owner={}", repository.owner))
                .and_then(|()| writeln!(output, "slug={}", repository.slug))
                .and_then(|()| writeln!(output, "visibility={}", repository.visibility))
                .and_then(|()| writeln!(output, "state={}", repository.state))
                .and_then(|()| writeln!(output, "object-format={}", repository.object_format))
                .and_then(|()| writeln!(output, "created-at={}", repository.created_at))
                .and_then(|()| writeln!(output, "archived-at={archived_at}"))
                .and_then(|()| writeln!(output, "path={}", path.display()));
            match written {
                Ok(()) => ExitCode::SUCCESS,
                Err(error) => {
                    eprintln!("tit: cannot write repository information: {error}");
                    ExitCode::FAILURE
                }
            }
        }
        Err(error) => {
            eprintln!("tit: {error}");
            ExitCode::FAILURE
        }
    }
}