tangled-cli 0.1.0

CLI for interacting with Tangled, an AT Protocol-based git collaboration platform
//! Domain types used by the CLI.
//!
//! These are deliberately more lenient than the generated lexicon types:
//! live PDS records frequently omit fields the lexicon marks required, and
//! several records still circulate in legacy shapes (inline pull patches,
//! at-uri repo references). The generated types in `tangled_api::lexicon`
//! are used at the wire boundary; these are what the commands work with.

use serde::{Deserialize, Serialize};
use tangled_api::types::BlobRef;

pub const REPO_COLLECTION: &str = "sh.tangled.repo";

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Repository {
    pub did: Option<String>,
    pub rkey: Option<String>,
    #[serde(default)]
    pub name: String,
    pub knot: Option<String>,
    pub description: Option<String>,
    pub source: Option<String>,
    pub spindle: Option<String>,
    #[serde(rename = "repoDid")]
    pub repo_did: Option<String>,
    #[serde(default)]
    pub private: bool,
}

// Issue record value
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Issue {
    pub repo: String,
    pub title: String,
    #[serde(default)]
    pub body: String,
    #[serde(rename = "createdAt", skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
    #[serde(rename = "$type", skip_serializing_if = "Option::is_none")]
    pub record_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub owner: Option<String>,
    #[serde(rename = "issueId", skip_serializing_if = "Option::is_none")]
    pub issue_id: Option<i64>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueRecord {
    pub author_did: String,
    pub rkey: String,
    pub issue: Issue,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssueState {
    pub issue: String,
    pub state: String,
    #[serde(rename = "createdAt", skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
}

// Pull record value (subset, tolerant of legacy shapes)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PullTarget {
    #[serde(default)]
    pub repo: String,
    #[serde(default)]
    pub branch: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pull {
    #[serde(default)]
    pub target: PullTarget,
    pub title: String,
    #[serde(default)]
    pub body: String,
    #[serde(default)]
    pub patch: String,
    #[serde(default)]
    pub rounds: Vec<PullRound>,
    #[serde(rename = "targetRepo", skip_serializing_if = "Option::is_none")]
    pub target_repo: Option<String>,
    #[serde(rename = "targetBranch", skip_serializing_if = "Option::is_none")]
    pub target_branch: Option<String>,
    #[serde(rename = "createdAt", default)]
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PullRound {
    #[serde(rename = "patchBlob")]
    pub patch_blob: BlobRef,
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

impl Pull {
    pub fn target_repo(&self) -> &str {
        if self.target.repo.is_empty() {
            self.target_repo.as_deref().unwrap_or("")
        } else {
            self.target.repo.as_str()
        }
    }

    pub fn target_branch(&self) -> &str {
        if self.target.branch.is_empty() {
            self.target_branch.as_deref().unwrap_or("")
        } else {
            self.target.branch.as_str()
        }
    }
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PullRecord {
    #[allow(dead_code)]
    pub author_did: String,
    pub rkey: String,
    pub pull: Pull,
}

#[derive(Debug, Clone)]
pub struct RepoRecord {
    pub did: String,
    pub name: String,
    pub rkey: String,
    pub knot: String,
    pub description: Option<String>,
    pub source: Option<String>,
    pub spindle: Option<String>,
    pub repo_did: Option<String>,
}

impl RepoRecord {
    pub fn issue_repo_ref(&self) -> String {
        self.repo_did.clone().unwrap_or_else(|| {
            format!("at://{}/{}/{}", self.did, REPO_COLLECTION, self.rkey)
        })
    }

    pub fn repo_at_uri(&self) -> String {
        format!("at://{}/{}/{}", self.did, REPO_COLLECTION, self.rkey)
    }
}

/// Knot-authoritative metadata for a repo DID (`sh.tangled.repo.describeRepo`).
pub type RepoDescribe =
    tangled_api::lexicon::sh::tangled::repo::describe_repo::Output;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultBranch {
    pub name: String,
    pub hash: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub short_hash: Option<String>,
    pub when: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Language {
    pub name: String,
    pub size: u64,
    pub percentage: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Languages {
    pub languages: Vec<Language>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_size: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_files: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StarRecord {
    pub subject: String,
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

/// A CI secret as returned by `sh.tangled.repo.listSecrets`.
pub type Secret = tangled_api::lexicon::sh::tangled::repo::list_secrets::Secret;

pub struct CreateRepoOptions<'a> {
    pub did: &'a str,
    pub name: &'a str,
    pub knot: &'a str,
    pub description: Option<&'a str>,
    pub default_branch: Option<&'a str>,
    pub source: Option<&'a str>,
    pub pds_base: &'a str,
    pub auth: &'a super::auth::PdsAuth,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TriggerMetadata {
    pub kind: String,
    pub repo: TriggerRepo,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TriggerRepo {
    pub knot: String,
    pub did: String,
    pub repo: String,
    #[serde(rename = "defaultBranch")]
    pub default_branch: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workflow {
    pub name: String,
    pub engine: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pipeline {
    #[serde(rename = "triggerMetadata")]
    pub trigger_metadata: TriggerMetadata,
    pub workflows: Vec<Workflow>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PipelineRecord {
    pub rkey: String,
    pub pipeline: Pipeline,
}