194c44c
use serde::Deserialize;
use serde::de::Deserializer;
use std::path::Path;
use std::time::Duration;
/// Windows 上 `/opt/gitdata/repos` 会退化成盘符相对路径(没有盘符前缀),git 读得到
/// refs 却读不到 object,表现为 `fatal: not a tree object`。这里统一锚定成带盘符的
/// 绝对路径;POSIX 平台本来就是绝对路径,保持原样。
pub fn anchor_local_path(raw: &str) -> String {
let trimmed = raw.trim();
if trimmed.is_empty() {
return raw.to_string();
}
let path = Path::new(trimmed);
if path.is_absolute() {
return trimmed.to_string();
}
match std::path::absolute(path) {
Ok(anchored) => anchored.to_string_lossy().into_owned(),
Err(_) => trimmed.to_string(),
}
}
fn deserialize_anchored_path<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(|value| anchor_local_path(&value))
}
mod duration_secs {
use serde::{Deserialize, Deserializer};
use std::time::Duration;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
u64::deserialize(deserializer).map(Duration::from_secs)
}
}
mod opt_duration_secs {
use serde::{Deserialize, Deserializer};
use std::time::Duration;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
Option::<u64>::deserialize(deserializer).map(|o| o.map(Duration::from_secs))
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct GitConfig {
#[serde(deserialize_with = "deserialize_anchored_path")]
pub repo_root: String,
#[serde(deserialize_with = "deserialize_anchored_path")]
pub objects_root: String,
pub max_blob_size: u64,
pub git_command_timeout_secs: u64,
pub git_rpc_timeout_secs: u64,
pub sync: SyncSettings,
pub ssh: GitSshConfig,
pub base_url: String,
pub http_clone_base_url: String,
pub ssh_clone_host: String,
pub ssh_clone_port: u16,
pub ssh_clone_user: String,
pub graphql_complexity_limit: u64,
pub graphql_introspection: bool,
}
impl Default for GitConfig {
fn default() -> Self {
Self {
repo_root: anchor_local_path("/opt/gitdata/repos"),
objects_root: anchor_local_path("/opt/gitdata/objects"),
max_blob_size: 100 * 1024 * 1024,
git_command_timeout_secs: 300,
git_rpc_timeout_secs: 3600,
sync: SyncSettings::default(),
ssh: GitSshConfig::default(),
base_url: "http://localhost:3000".to_string(),
http_clone_base_url: String::new(),
ssh_clone_host: String::new(),
ssh_clone_port: 0,
ssh_clone_user: "git".to_string(),
graphql_complexity_limit: 1000,
graphql_introspection: false,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct GitSshConfig {
pub bind_addr: String,
pub port: u16,
pub host_key_path: String,
pub banner: Option<String>,
pub max_sessions: usize,
#[serde(deserialize_with = "duration_secs::deserialize")]
pub auth_rejection_time: Duration,
#[serde(deserialize_with = "opt_duration_secs::deserialize")]
pub inactivity_timeout: Option<Duration>,
#[serde(deserialize_with = "duration_secs::deserialize")]
pub keepalive_interval: Duration,
pub keepalive_max: u32,
}
impl Default for GitSshConfig {
fn default() -> Self {
Self {
bind_addr: "0.0.0.0".to_string(),
port: 22,
host_key_path: "/opt/gitdata/ssh_host_ed25519".to_string(),
banner: None,
max_sessions: 64,
auth_rejection_time: Duration::from_secs(2),
inactivity_timeout: Some(Duration::from_secs(300)),
keepalive_interval: Duration::from_secs(30),
keepalive_max: 3,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct SyncSettings {
pub lock_ttl_secs: u64,
pub max_commits_per_sync: usize,
pub concurrency: usize,
pub queue_topic: String,
pub dlq_topic: String,
pub max_lock_retries: u64,
pub max_languages: usize,
pub language_timeout_secs: u64,
pub brpop_timeout_secs: u64,
pub git_command_timeout_secs: u64,
}
impl Default for SyncSettings {
fn default() -> Self {
Self {
lock_ttl_secs: 120,
max_commits_per_sync: 5_000,
concurrency: 4,
queue_topic: "gitdata:tasks".to_string(),
dlq_topic: "gitdata:dlq".to_string(),
max_lock_retries: 5,
max_languages: 20,
language_timeout_secs: 30,
brpop_timeout_secs: 5,
git_command_timeout_secs: 120,
}
}
}