KeiSeiKit-1.0/_primitives/_rust/kei-git-forgejo/src/backend.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

185 lines
6.5 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 <author org>
//!
//! [`ForgejoBackend`] — `GitBackend` impl for public Forgejo / Codeberg.
//!
//! Network IO splits two ways:
//! - Repo lifecycle (exists / create / branch SHA) → REST via `ForgejoClient`.
//! - Working-copy ops (clone / push / mirror) → shell-out to `git`.
//!
//! `provider_name = "forgejo"`. `supports_auto_create = true` because the
//! `/api/v1/user/repos` POST will create on demand inside `ensure_repo`.
use crate::client::ForgejoClient;
use crate::error::{Error, Result};
use async_trait::async_trait;
use kei_runtime_core::traits::git::{CommitMeta, GitBackend, GitRemote};
use kei_runtime_core::{Dna, DnaBuilder, HasDna};
use std::path::Path;
use tokio::process::Command;
pub struct ForgejoBackend {
dna: Dna,
parent: Option<Dna>,
client: ForgejoClient,
}
impl ForgejoBackend {
/// Build a backend from a pre-configured client. The DNA is a fresh
/// primitive serial with caps `["PR", "AP", "FJ"]`.
pub fn new(client: ForgejoClient, parent: Option<Dna>) -> Result<Self> {
let dna = DnaBuilder::new("primitive")
.caps(["PR", "AP", "FJ"])
.scope("keiseikit.dev/primitives/kei-git-forgejo")
.body(b"forgejo-pub-v1")
.build()?;
Ok(Self { dna, parent, client })
}
/// Borrow the underlying client (for callers that need direct REST
/// access beyond the `GitBackend` trait surface).
pub fn client(&self) -> &ForgejoClient {
&self.client
}
/// Embed PAT into a clone/push URL. `https://host/o/r.git` →
/// `https://x-access-token:{tok}@host/o/r.git`. Non-https URLs are
/// passed through unchanged (SSH paths etc.).
fn auth_url(&self, url: &str) -> String {
if let Some(rest) = url.strip_prefix("https://") {
format!("https://x-access-token:{}@{}", self.client.token(), rest)
} else {
url.to_string()
}
}
}
impl HasDna for ForgejoBackend {
fn dna(&self) -> &Dna { &self.dna }
fn parent_dna(&self) -> Option<&Dna> { self.parent.as_ref() }
}
#[async_trait]
impl GitBackend for ForgejoBackend {
fn provider_name(&self) -> &'static str { "forgejo" }
fn supports_auto_create(&self) -> bool { true }
async fn ensure_repo(&self, remote: &GitRemote) -> kei_runtime_core::Result<()> {
let (owner, name) = parse_owner_repo(&remote.url).map_err(kei_runtime_core::Error::from)?;
let exists = self.client.repo_exists(&owner, &name).await.map_err(kei_runtime_core::Error::from)?;
if !exists {
self.client
.create_user_repo(&name, /* private */ false, &remote.branch)
.await
.map_err(kei_runtime_core::Error::from)?;
}
Ok(())
}
async fn clone(&self, remote: &GitRemote, dest: &Path) -> kei_runtime_core::Result<()> {
let url = self.auth_url(&remote.url);
let status = Command::new("git")
.arg("clone")
.arg("--branch").arg(&remote.branch)
.arg(&url)
.arg(dest)
.status()
.await
.map_err(|e| kei_runtime_core::Error::from(Error::Io(e)))?;
if !status.success() {
return Err(kei_runtime_core::Error::Provider(format!("git clone exit {status}")));
}
Ok(())
}
async fn push(&self, dir: &Path, remote: &GitRemote) -> kei_runtime_core::Result<CommitMeta> {
let url = self.auth_url(&remote.url);
let status = Command::new("git")
.current_dir(dir)
.arg("push")
.arg(&url)
.arg(&remote.branch)
.status()
.await
.map_err(|e| kei_runtime_core::Error::from(Error::Io(e)))?;
if !status.success() {
return Err(kei_runtime_core::Error::Provider(format!("git push exit {status}")));
}
let (owner, name) = parse_owner_repo(&remote.url).map_err(kei_runtime_core::Error::from)?;
let sha = self
.client
.get_default_branch_sha(&owner, &name, &remote.branch)
.await
.map_err(kei_runtime_core::Error::from)?;
Ok(CommitMeta {
sha,
message: String::new(),
author_email: String::new(),
committed_at_ms: 0,
})
}
async fn mirror(&self, src: &GitRemote, dst: &GitRemote) -> kei_runtime_core::Result<()> {
let tmp = std::env::temp_dir()
.join(format!("kei-git-forgejo-mirror-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let src_url = self.auth_url(&src.url);
let mirror_status = Command::new("git")
.arg("clone").arg("--mirror")
.arg(&src_url).arg(&tmp)
.status()
.await
.map_err(|e| kei_runtime_core::Error::from(Error::Io(e)))?;
if !mirror_status.success() {
return Err(kei_runtime_core::Error::Provider(format!("mirror clone exit {mirror_status}")));
}
let dst_url = self.auth_url(&dst.url);
let push_status = Command::new("git")
.current_dir(&tmp)
.arg("push").arg("--mirror").arg(&dst_url)
.status()
.await
.map_err(|e| kei_runtime_core::Error::from(Error::Io(e)))?;
let _ = std::fs::remove_dir_all(&tmp);
if !push_status.success() {
return Err(kei_runtime_core::Error::Provider(format!("mirror push exit {push_status}")));
}
Ok(())
}
}
/// Extract `(owner, repo)` from a Forgejo-style HTTPS URL. Accepts
/// `https://host/owner/repo.git` and `https://host/owner/repo`.
fn parse_owner_repo(url: &str) -> Result<(String, String)> {
let stripped = url.trim_end_matches(".git");
let segments: Vec<&str> = stripped.rsplit('/').take(2).collect();
if segments.len() != 2 || segments.iter().any(|s| s.is_empty()) {
return Err(Error::Config(format!("cannot parse owner/repo from {url}")));
}
Ok((segments[1].to_string(), segments[0].to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_https_dot_git() {
let (o, r) = parse_owner_repo("https://codeberg.org/me/demo.git").unwrap();
assert_eq!(o, "me");
assert_eq!(r, "demo");
}
#[test]
fn parse_https_no_suffix() {
let (o, r) = parse_owner_repo("https://codeberg.org/me/demo").unwrap();
assert_eq!(o, "me");
assert_eq!(r, "demo");
}
#[test]
fn parse_rejects_short() {
assert!(parse_owner_repo("https://codeberg.org/").is_err());
}
}