// SPDX-FileCopyrightText: V // SPDX-License-Identifier: OSL-3.0 use { anyhow::{anyhow, Error, Result}, git2::{Oid, Repository, Sort}, irc::client::prelude::*, pin_utils::pin_mut, std::{env, fs::remove_file, io::ErrorKind}, tokio::{ io::{AsyncBufRead, AsyncBufReadExt, BufReader, Lines}, net::UnixListener, select, spawn, sync::{mpsc, mpsc::UnboundedSender}, }, tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}, }; #[derive(Debug)] struct Batch { lines: Vec, } #[tokio::main] async fn main() -> Result<()> { let (tx, rx) = mpsc::unbounded_channel::(); let listener = bind(env::var("NAUT_SOCK")?.as_str())?; spawn(async move { loop { let (stream, _) = listener.accept().await.unwrap(); let tx = tx.clone(); let conn = async move { let lines = BufReader::new(stream).lines(); let repo = Repository::open("/var/lib/git/basin")?; handle(repo, lines, tx).await?; Ok::<(), Error>(()) }; spawn(async move { if let Err(e) = conn.await { eprintln!("Failed to handle request: {}", e); } }); } }); let channel = "#ripple"; let client_config = Config { server: Some("irc.libera.chat".to_owned()), password: Some(env::var("NAUT_PASS")?), nickname: Some("naut".to_owned()), realname: Some("blub blub".to_owned()), version: Some(format!("naut {}", env!("CARGO_PKG_VERSION"))), source: Some("https://src.unfathomable.blue/tree/fleet/pkgs/naut".to_owned()), channels: vec![channel.to_owned()], ..Default::default() }; let rx = UnboundedReceiverStream::new(rx).fuse(); pin_mut!(rx); loop { let mut client = Client::from_config(client_config.clone()).await?; client.identify()?; let sender = client.sender(); let stream = client.stream()?.fuse(); pin_mut!(stream); loop { select! { message = stream.next() => match message { Some(_) => {}, None => break, }, Some(batch) = rx.next() => { for line in batch.lines { sender.send_privmsg("#ripple", line.to_owned())?; } }, } } } } fn bind(path: &str) -> Result { match remove_file(path) { Ok(()) => (), Err(e) if e.kind() == ErrorKind::NotFound => (), Err(e) => return Err(e.into()), } UnixListener::bind(path).map_err(Error::from) } async fn handle( repo: Repository, mut lines: Lines, tx: UnboundedSender, ) -> Result<()> { while let Some(line) = lines.next_line().await? { let args: Vec<_> = line.splitn(3, ' ').collect(); let old = Oid::from_str(args[0])?; let new = Oid::from_str(args[1])?; let r#ref = repo.find_reference(args[2])?; let ref_name = r#ref.shorthand().unwrap(); let mut lines = vec![]; if r#ref.is_branch() { if new.is_zero() { lines.push(format!("branch {} deleted (was {})", ref_name, old)); } else { let mut walker = repo.revwalk()?; walker.set_sorting(Sort::REVERSE)?; walker.push(new)?; if old.is_zero() { lines.push(format!("new branch created: {}", ref_name)); // We cannot use repo.head directly, as that comes resolved already. let head = repo.find_reference("HEAD")?; // Hide commits also present from HEAD (unless this *is* HEAD, in which we do want them). // This avoids duplicating notifications for commits that we've already seen, provided we // only push branches that are forked directly from HEAD (or one of its ancestors). if ref_name != head.symbolic_target().unwrap() { if let Ok(base) = repo.merge_base(head.resolve()?.target().unwrap(), new) { walker.hide(base)?; } } } else { walker.hide(old)?; } let commits: Vec<_> = walker .map(|x| repo.find_commit(x.unwrap()).unwrap()) .filter(|c| { // TODO(edef): we need a saner model here // history that doesn't descend from the monorepo root shouldn't make it here at all c.parents() .next() .map(|parent| { let t1 = parent.tree().unwrap().get_name("ripple").map(|e| e.id()); let t2 = c.tree().unwrap().get_name("ripple").map(|e| e.id()); t1 != t2 }) .unwrap_or_default() }) .collect(); if !commits.is_empty() { lines.push(format!( "{} {} pushed to {}", commits.len(), if commits.len() == 1 { "commit" } else { "commits" }, ref_name )); for commit in commits { lines.push(format!( " {} \"{}\" by {}", commit.as_object().short_id()?.as_str().unwrap(), commit.summary().unwrap(), commit.author().name().unwrap() )); } } } } else if r#ref.is_tag() { if new.is_zero() { lines.push(format!("tag {} deleted (was {})", ref_name, old)) } else if old.is_zero() { lines.push(format!("commit {} tagged as {}", new, ref_name)) } else { lines.push(format!( "tag {} modified (was {}, now {})", ref_name, old, new )) } } else { return Err(anyhow!( "Received a reference that's neither a branch nor tag: {}", args[2] )); } tx.send(Batch { lines })?; } Ok(()) }