use crate::git_info::{self};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkspaceInfo {
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub remote_urls: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_root: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TurnMetadata {
pub workspace: WorkspaceInfo,
}
pub fn build_turn_metadata_header(cwd: &Path) -> Result<String> {
if !git_info::is_git_repo(cwd) {
return Ok(String::new());
}
let git_info = git_info::collect_git_info(cwd)?;
let metadata = TurnMetadata {
workspace: WorkspaceInfo {
remote_urls: git_info.remotes,
commit_hash: git_info.head_commit,
repo_root: git_info.repo_root,
},
};
let json = serde_json::to_string(&metadata)?;
Ok(json)
}
pub fn build_turn_metadata_value(cwd: &Path) -> Result<Value> {
if !git_info::is_git_repo(cwd) {
return Ok(Value::Null);
}
let git_info = git_info::collect_git_info(cwd)?;
let metadata = TurnMetadata {
workspace: WorkspaceInfo {
remote_urls: git_info.remotes,
commit_hash: git_info.head_commit,
repo_root: git_info.repo_root,
},
};
let value = serde_json::to_value(metadata)?;
Ok(value)
}
pub async fn build_turn_metadata_value_with_timeout(
cwd: &Path,
timeout: Duration,
) -> Result<Option<Value>> {
if !git_info::is_git_repo(cwd) {
return Ok(None);
}
let cwd = cwd.to_path_buf();
let handle = tokio::task::spawn_blocking(move || build_turn_metadata_value(&cwd));
match tokio::time::timeout(timeout, handle).await {
Ok(join_result) => {
let value = join_result.context("Turn metadata task failed")??;
if value.is_null() {
Ok(None)
} else {
Ok(Some(value))
}
}
Err(_) => {
tracing::debug!("Turn metadata collection timed out");
Ok(None)
}
}
}
pub const TURN_METADATA_HEADER: &str = "X-Turn-Metadata";
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_build_turn_metadata_header() {
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let metadata = build_turn_metadata_header(&repo_root).unwrap();
let parsed: Value = serde_json::from_str(&metadata).unwrap();
assert!(parsed.get("workspace").is_some());
}
#[test]
fn test_build_turn_metadata_value() {
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let value = build_turn_metadata_value(&repo_root).unwrap();
assert!(!value.is_null());
assert!(value.get("workspace").is_some());
}
#[test]
fn test_turn_metadata_header_constant() {
assert_eq!(TURN_METADATA_HEADER, "X-Turn-Metadata");
}
#[test]
fn test_workspace_info_serialization() {
let mut remotes = BTreeMap::new();
remotes.insert(
"origin".to_string(),
"https://github.com/user/repo.git".to_string(),
);
let workspace = WorkspaceInfo {
remote_urls: remotes,
commit_hash: Some("abc1234".to_string()),
repo_root: Some("/path/to/repo".to_string()),
};
let json = serde_json::to_string(&workspace).unwrap();
assert!(json.contains("origin"));
assert!(json.contains("abc1234"));
assert!(json.contains("/path/to/repo"));
}
#[test]
fn test_empty_remotes_skipped() {
let workspace = WorkspaceInfo {
remote_urls: BTreeMap::new(),
commit_hash: Some("abc1234".to_string()),
repo_root: None,
};
let json = serde_json::to_string(&workspace).unwrap();
assert!(!json.contains("remote_urls"));
assert!(json.contains("commit_hash"));
}
}