use std::{collections::HashMap, sync::Arc}; use anyhow::{Context, Result}; use git2::Oid; use nostr::{ event::{EventBuilder, Tag}, signer::NostrSigner, }; use crate::client::{STATE_KIND, sign_event}; pub struct RepoState { pub identifier: String, pub state: HashMap, pub event: nostr::Event, } impl RepoState { pub fn try_from(mut state_events: Vec) -> Result { state_events.sort_by_key(|e| e.created_at); let event = state_events.last().context("no state events")?; let mut state = HashMap::new(); for tag in event.tags.iter() { if let Some(name) = tag.as_slice().first() { // include ^{} peeled refs for annotated tags: git requires // both " refs/tags/v1.0.0" and // " refs/tags/v1.0.0^{}" in the list output so // it can resolve the tag to a commit. without the ^{} line // git fetch --prune deletes the tag as unresolvable. if ["refs/heads/", "refs/tags", "HEAD"] .iter() .any(|s| name.starts_with(*s)) { if let Some(value) = tag.as_slice().get(1) { if Oid::from_str(value).is_ok() || value.contains("ref: refs/") { state.insert(name.to_owned(), value.to_owned()); } } } } } add_head(&mut state); Ok(RepoState { identifier: event .tags .identifier() .context("existing event must have an identifier")? .to_string(), state, event: event.clone(), }) } pub async fn build( identifier: String, mut state: HashMap, signer: &Arc, ) -> Result { add_head(&mut state); let mut tags = vec![Tag::identifier(identifier.clone())]; for (name, value) in &state { tags.push(Tag::custom( nostr_sdk::TagKind::Custom(name.into()), vec![value.clone()], )); } let event = sign_event( EventBuilder::new(STATE_KIND, "").tags(tags), signer, "git state".to_string(), ) .await?; Ok(RepoState { identifier, state, event, }) } } // Include a HEAD if one isn't listed to prevent errors when users git config // default branch isn't in the state event fn add_head(state: &mut HashMap) { if !state.contains_key("HEAD") { if state.contains_key("refs/heads/master") { state.insert("HEAD".to_string(), "ref: refs/heads/master".to_string()); } else if state.contains_key("refs/heads/main") { state.insert("HEAD".to_string(), "ref: refs/heads/main".to_string()); } else if let Some(k) = state.keys().find(|k| k.starts_with("refs/heads/")) { state.insert("HEAD".to_string(), format!("ref: {k}")); } } }