refactor(build): reformat code and add tonic health dependency

- Reformatted build script with proper indentation and line breaks
- Added tonic-health dependency to Cargo.toml and updated lock file
- Improved error handling in disk cache with concurrent deletion checks
- Refactored conditional chains using && and let expressions
- Reformatted struct initialization and function parameter lists
- Added proper spacing and alignment in language stats processing
- Improved assertion formatting in test cases
- Reorganized import statements and code layout in multiple files
- Updated metrics functions with better parameter handling and formatting
This commit is contained in:
zhenyi
2026-06-11 13:56:15 +08:00
parent c32a7cad2f
commit a40da90ef9
31 changed files with 696 additions and 417 deletions
Generated
+14
View File
@@ -721,6 +721,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",
"tonic", "tonic",
"tonic-health",
"tonic-prost", "tonic-prost",
"tonic-prost-build", "tonic-prost-build",
"tracing", "tracing",
@@ -3075,6 +3076,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tonic-health"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcfab99db777fba2802f0dfa861d1628d1ae916fb199d29819941f139ae85082"
dependencies = [
"prost",
"tokio",
"tokio-stream",
"tonic",
"tonic-prost",
]
[[package]] [[package]]
name = "tonic-prost" name = "tonic-prost"
version = "0.14.6" version = "0.14.6"
+1
View File
@@ -33,6 +33,7 @@ thiserror = { version = "2", features = [] }
prost = "0.14" prost = "0.14"
prost-types = "0.14" prost-types = "0.14"
tonic = { version = "0.14", features = ["transport"] } tonic = { version = "0.14", features = ["transport"] }
tonic-health = "0.14.6"
tonic-prost = "0.14" tonic-prost = "0.14"
tempfile = "3" tempfile = "3"
dotenvy = "0.15" dotenvy = "0.15"
+36 -20
View File
@@ -1,7 +1,7 @@
use crate::actor::message::{ use crate::actor::message::{
AppendEntriesRequest, AppendEntriesResponse, ElectionRequest, ElectionResult, GitNodeMessage, AppendEntriesRequest, AppendEntriesResponse, ElectionRequest, ElectionResult, GitNodeMessage,
NodeHealth, ReadIndexResponse, RefUpdateEvent, RoleChangedEvent, RouteDecision, NodeHealth, RAFT_MSG_VERSION, ROLE_PRIMARY, ROLE_REPLICA, ReadIndexResponse, RefUpdateEvent,
ROLE_PRIMARY, ROLE_REPLICA, RAFT_MSG_VERSION, RoleChangedEvent, RouteDecision,
}; };
use crate::actor::raft_log::RaftLog; use crate::actor::raft_log::RaftLog;
use crate::pb::RepositoryHeader; use crate::pb::RepositoryHeader;
@@ -94,9 +94,8 @@ impl Actor for GitNodeActor {
// Initialize Raft log with disk persistence // Initialize Raft log with disk persistence
let raft_data_dir = args.data_dir.join("raft"); let raft_data_dir = args.data_dir.join("raft");
let raft_log = RaftLog::new(&raft_data_dir).map_err(|e| { let raft_log = RaftLog::new(&raft_data_dir)
ActorProcessingErr::from(format!("failed to init raft log: {e}")) .map_err(|e| ActorProcessingErr::from(format!("failed to init raft log: {e}")))?;
})?;
tracing::info!( tracing::info!(
storage_name = %args.storage_name, storage_name = %args.storage_name,
entries = raft_log.len(), entries = raft_log.len(),
@@ -451,9 +450,7 @@ fn should_accept_election(request: &ElectionRequest, state: &GitNodeState) -> bo
); );
return false; return false;
} }
if request.last_log_term == my_last_term if request.last_log_term == my_last_term && request.last_log_index < my_last_index {
&& request.last_log_index < my_last_index
{
tracing::warn!( tracing::warn!(
candidate_index = request.last_log_index, candidate_index = request.last_log_index,
my_index = my_last_index, my_index = my_last_index,
@@ -796,8 +793,8 @@ fn handle_append_entries(
}; };
} }
} }
if state.raft_log.term_at(entry.index) == 0 { if state.raft_log.term_at(entry.index) == 0
if let Some(raft_entry) = entry.to_entry() && let Some(raft_entry) = entry.to_entry()
&& let Err(e) = state.raft_log.append_reserved(raft_entry) && let Err(e) = state.raft_log.append_reserved(raft_entry)
{ {
tracing::error!(error = %e, "failed to append raft entry"); tracing::error!(error = %e, "failed to append raft entry");
@@ -811,7 +808,6 @@ fn handle_append_entries(
}; };
} }
} }
}
// Step 5: Update commit_index // Step 5: Update commit_index
if request.leader_commit > state.raft_log.commit_index() { if request.leader_commit > state.raft_log.commit_index() {
@@ -858,7 +854,10 @@ fn handle_read_index(state: &GitNodeState) -> ReadIndexResponse {
ReadIndexResponse { ReadIndexResponse {
commit_index: state.raft_log.commit_index(), commit_index: state.raft_log.commit_index(),
leader_term: state.current_term, leader_term: state.current_term,
is_leader: state.is_primary && state.leader_lease_deadline.is_some_and(|d| d > Instant::now()), is_leader: state.is_primary
&& state
.leader_lease_deadline
.is_some_and(|d| d > Instant::now()),
} }
} }
@@ -902,8 +901,12 @@ pub async fn broadcast_append_entries(
match ractor::call_t!(actor_ref, GitNodeMessage::AppendEntries, 5000, request) { match ractor::call_t!(actor_ref, GitNodeMessage::AppendEntries, 5000, request) {
Ok(response) if response.success => { Ok(response) if response.success => {
success_count += 1; success_count += 1;
state.match_index.insert(follower_id.clone(), response.match_index); state
state.next_index.insert(follower_id, response.match_index + 1); .match_index
.insert(follower_id.clone(), response.match_index);
state
.next_index
.insert(follower_id, response.match_index + 1);
} }
Ok(response) => { Ok(response) => {
// Follower rejected — update next_index for retry // Follower rejected — update next_index for retry
@@ -916,7 +919,9 @@ pub async fn broadcast_append_entries(
// Decrement next_index (optimization: use conflict info) // Decrement next_index (optimization: use conflict info)
let next = state.next_index.get(&follower_id).copied().unwrap_or(1); let next = state.next_index.get(&follower_id).copied().unwrap_or(1);
if response.conflict_index > 0 && response.conflict_index < next { if response.conflict_index > 0 && response.conflict_index < next {
state.next_index.insert(follower_id, response.conflict_index); state
.next_index
.insert(follower_id, response.conflict_index);
} else if next > 1 { } else if next > 1 {
state.next_index.insert(follower_id, next - 1); state.next_index.insert(follower_id, next - 1);
} }
@@ -933,7 +938,9 @@ pub async fn broadcast_append_entries(
/// Check if Leader lease is still valid. /// Check if Leader lease is still valid.
pub fn is_leader_lease_valid(state: &GitNodeState) -> bool { pub fn is_leader_lease_valid(state: &GitNodeState) -> bool {
state.is_primary state.is_primary
&& state.leader_lease_deadline.is_some_and(|d| d > Instant::now()) && state
.leader_lease_deadline
.is_some_and(|d| d > Instant::now())
} }
/// Update Leader lease after successful majority replication. /// Update Leader lease after successful majority replication.
@@ -1006,8 +1013,12 @@ async fn handle_raft_write(
match ractor::call_t!(actor_ref, GitNodeMessage::AppendEntries, 5000, request) { match ractor::call_t!(actor_ref, GitNodeMessage::AppendEntries, 5000, request) {
Ok(response) if response.success => { Ok(response) if response.success => {
success_count += 1; success_count += 1;
state.match_index.insert(follower_id.clone(), response.match_index); state
state.next_index.insert(follower_id, response.match_index + 1); .match_index
.insert(follower_id.clone(), response.match_index);
state
.next_index
.insert(follower_id, response.match_index + 1);
} }
Ok(response) => { Ok(response) => {
tracing::debug!( tracing::debug!(
@@ -1083,7 +1094,10 @@ fn apply_raft_command(state: &mut GitNodeState, command: &crate::actor::raft_log
storage_name = %storage_name, storage_name = %storage_name,
"applying RegisterRepo from Raft log" "applying RegisterRepo from Raft log"
); );
state.repos.entry(relative_path.clone()).or_insert_with(|| RepoEntry { state
.repos
.entry(relative_path.clone())
.or_insert_with(|| RepoEntry {
role: ROLE_REPLICA.to_string(), role: ROLE_REPLICA.to_string(),
last_commit: String::new(), last_commit: String::new(),
read_only: false, read_only: false,
@@ -1121,5 +1135,7 @@ fn apply_raft_command(state: &mut GitNodeState, command: &crate::actor::raft_log
} }
// Advance last_applied // Advance last_applied
state.raft_log.advance_last_applied(state.raft_log.commit_index()); state
.raft_log
.advance_last_applied(state.raft_log.commit_index());
} }
+23 -4
View File
@@ -159,7 +159,6 @@ pub enum GitNodeMessage {
TriggerElection, TriggerElection,
// ── Raft consensus messages ────────────────────────────── // ── Raft consensus messages ──────────────────────────────
/// AppendEntries RPC: Leader → Follower log replication. /// AppendEntries RPC: Leader → Follower log replication.
#[rpc] #[rpc]
AppendEntries(AppendEntriesRequest, RpcReplyPort<AppendEntriesResponse>), AppendEntries(AppendEntriesRequest, RpcReplyPort<AppendEntriesResponse>),
@@ -418,7 +417,16 @@ impl BytesConvertable for AppendEntriesRequest {
}); });
} }
let leader_commit = read_u64(&bytes, &mut offset); let leader_commit = read_u64(&bytes, &mut offset);
Self { version, term, leader_id, leader_grpc_addr, prev_log_index, prev_log_term, entries, leader_commit } Self {
version,
term,
leader_id,
leader_grpc_addr,
prev_log_index,
prev_log_term,
entries,
leader_commit,
}
} }
} }
@@ -457,7 +465,14 @@ impl BytesConvertable for AppendEntriesResponse {
let match_index = read_u64(&bytes, &mut offset); let match_index = read_u64(&bytes, &mut offset);
let conflict_index = read_u64(&bytes, &mut offset); let conflict_index = read_u64(&bytes, &mut offset);
let conflict_term = read_u64(&bytes, &mut offset); let conflict_term = read_u64(&bytes, &mut offset);
Self { version, term, success, match_index, conflict_index, conflict_term } Self {
version,
term,
success,
match_index,
conflict_index,
conflict_term,
}
} }
} }
@@ -502,7 +517,11 @@ impl BytesConvertable for ReadIndexResponse {
let commit_index = read_u64(&bytes, &mut offset); let commit_index = read_u64(&bytes, &mut offset);
let leader_term = read_u64(&bytes, &mut offset); let leader_term = read_u64(&bytes, &mut offset);
let is_leader = bytes.get(offset).copied().unwrap_or(0) == 1; let is_leader = bytes.get(offset).copied().unwrap_or(0) == 1;
Self { commit_index, leader_term, is_leader } Self {
commit_index,
leader_term,
is_leader,
}
} }
} }
+6 -7
View File
@@ -7,15 +7,14 @@ pub mod sync;
pub use handler::find_primary_in_cluster; pub use handler::find_primary_in_cluster;
pub use handler::{ pub use handler::{
broadcast_append_entries, broadcast_ref_update, broadcast_role_changed, GitNodeActor, GitNodeArgs, RepoEntry, broadcast_append_entries, broadcast_ref_update,
get_category_members, get_cluster_nodes, is_leader_lease_valid, list_all_groups, broadcast_role_changed, get_category_members, get_cluster_nodes, is_leader_lease_valid,
renew_leader_lease, route_group_for, start_node_actor, GitNodeActor, GitNodeArgs, RepoEntry, list_all_groups, renew_leader_lease, route_group_for, start_node_actor,
}; };
pub use message::{ pub use message::{
AppendEntriesRequest, AppendEntriesResponse, ElectionRequest, ElectionResult, AppendEntriesRequest, AppendEntriesResponse, ElectionRequest, ElectionResult, GitNodeMessage,
GitNodeMessage, NodeHealth, ReadIndexRequest, ReadIndexResponse, RefUpdateEvent, NodeHealth, RAFT_MSG_VERSION, ROLE_PRIMARY, ROLE_REPLICA, ReadIndexRequest, ReadIndexResponse,
RepoActorMessage, RoleChangedEvent, RouteDecision, SerializedRaftEntry, RefUpdateEvent, RepoActorMessage, RoleChangedEvent, RouteDecision, SerializedRaftEntry,
ROLE_PRIMARY, ROLE_REPLICA, RAFT_MSG_VERSION,
}; };
pub use raft_log::{Command as RaftCommand, LogEntry as RaftLogEntry, RaftLog}; pub use raft_log::{Command as RaftCommand, LogEntry as RaftLogEntry, RaftLog};
pub use server::init_actor_cluster; pub use server::init_actor_cluster;
+55 -24
View File
@@ -13,9 +13,9 @@ use std::sync::atomic::{AtomicU64, Ordering};
use ractor_cluster::BytesConvertable; use ractor_cluster::BytesConvertable;
use crate::error::{GitError, GitResult};
use crate::actor::snapshot::{RaftSnapshot, SnapshotStorage};
use crate::actor::handler::RepoEntry; use crate::actor::handler::RepoEntry;
use crate::actor::snapshot::{RaftSnapshot, SnapshotStorage};
use crate::error::{GitError, GitResult};
use std::collections::HashMap; use std::collections::HashMap;
/// Protocol version for forward/backward compatibility. /// Protocol version for forward/backward compatibility.
@@ -56,11 +56,19 @@ impl Command {
pub fn encode(&self) -> Vec<u8> { pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new(); let mut buf = Vec::new();
match self { match self {
Command::RefUpdate { relative_path, ref_name, old_oid, new_oid } => { Command::RefUpdate {
relative_path,
ref_name,
old_oid,
new_oid,
} => {
buf.push(0); // tag buf.push(0); // tag
encode_strings(&mut buf, &[relative_path, ref_name, old_oid, new_oid]); encode_strings(&mut buf, &[relative_path, ref_name, old_oid, new_oid]);
} }
Command::RegisterRepo { relative_path, storage_name } => { Command::RegisterRepo {
relative_path,
storage_name,
} => {
buf.push(1); buf.push(1);
encode_strings(&mut buf, &[relative_path, storage_name]); encode_strings(&mut buf, &[relative_path, storage_name]);
} }
@@ -68,7 +76,10 @@ impl Command {
buf.push(2); buf.push(2);
encode_strings(&mut buf, &[relative_path]); encode_strings(&mut buf, &[relative_path]);
} }
Command::SetPrimary { storage_name, relative_paths } => { Command::SetPrimary {
storage_name,
relative_paths,
} => {
buf.push(3); buf.push(3);
encode_string(&mut buf, storage_name); encode_string(&mut buf, storage_name);
buf.extend((relative_paths.len() as u32).to_be_bytes()); buf.extend((relative_paths.len() as u32).to_be_bytes());
@@ -192,7 +203,12 @@ pub struct LogEntry {
impl LogEntry { impl LogEntry {
pub fn new(term: u64, index: u64, command: Command) -> Self { pub fn new(term: u64, index: u64, command: Command) -> Self {
let checksum = Self::compute_checksum(term, index, &command); let checksum = Self::compute_checksum(term, index, &command);
Self { term, index, command, checksum } Self {
term,
index,
command,
checksum,
}
} }
fn compute_checksum(term: u64, index: u64, command: &Command) -> u32 { fn compute_checksum(term: u64, index: u64, command: &Command) -> u32 {
@@ -243,7 +259,12 @@ impl LogEntry {
return None; return None;
} }
Some(LogEntry { term, index, command, checksum }) Some(LogEntry {
term,
index,
command,
checksum,
})
} }
} }
@@ -320,7 +341,9 @@ impl RaftStorage {
.append(true) .append(true)
.open(&self.index_path) .open(&self.index_path)
.map_err(GitError::Io)?; .map_err(GitError::Io)?;
index_file.write_all(&index_entry.encode()).map_err(GitError::Io)?; index_file
.write_all(&index_entry.encode())
.map_err(GitError::Io)?;
index_file.flush().map_err(GitError::Io)?; index_file.flush().map_err(GitError::Io)?;
Ok(entry.index) Ok(entry.index)
@@ -383,7 +406,12 @@ impl RaftStorage {
); );
break; break;
} }
entries.push(LogEntry { term, index, command, checksum }); entries.push(LogEntry {
term,
index,
command,
checksum,
});
} }
None => { None => {
tracing::warn!(index, "failed to decode command during recovery, stopping"); tracing::warn!(index, "failed to decode command during recovery, stopping");
@@ -478,7 +506,10 @@ impl RaftLog {
let entries = storage.load_all()?; let entries = storage.load_all()?;
let next_index = entries.last().map(|e| e.index + 1).unwrap_or(snapshot_index + 1); let next_index = entries
.last()
.map(|e| e.index + 1)
.unwrap_or(snapshot_index + 1);
let last_applied = entries.last().map(|e| e.index).unwrap_or(snapshot_index); let last_applied = entries.last().map(|e| e.index).unwrap_or(snapshot_index);
Ok(Self { Ok(Self {
@@ -610,7 +641,9 @@ impl RaftLog {
return Ok(()); // Nothing to compact return Ok(()); // Nothing to compact
} }
let keep: Vec<LogEntry> = self.entries.iter() let keep: Vec<LogEntry> = self
.entries
.iter()
.filter(|e| e.index >= from_index) .filter(|e| e.index >= from_index)
.cloned() .cloned()
.collect(); .collect();
@@ -622,11 +655,7 @@ impl RaftLog {
self.storage.truncate_and_rebuild(&keep)?; self.storage.truncate_and_rebuild(&keep)?;
self.entries = keep; self.entries = keep;
tracing::info!( tracing::info!(from_index, kept = self.entries.len(), "raft log compacted");
from_index,
kept = self.entries.len(),
"raft log compacted"
);
Ok(()) Ok(())
} }
@@ -635,7 +664,9 @@ impl RaftLog {
/// when a follower detects a term mismatch, it must delete the conflicting entry /// when a follower detects a term mismatch, it must delete the conflicting entry
/// and all entries that follow. /// and all entries that follow.
pub fn truncate_from(&mut self, from_index: u64) -> GitResult<()> { pub fn truncate_from(&mut self, from_index: u64) -> GitResult<()> {
let keep: Vec<LogEntry> = self.entries.iter() let keep: Vec<LogEntry> = self
.entries
.iter()
.filter(|e| e.index < from_index) .filter(|e| e.index < from_index)
.cloned() .cloned()
.collect(); .collect();
@@ -677,11 +708,7 @@ impl RaftLog {
/// Create a snapshot of the current state and compact the log. /// Create a snapshot of the current state and compact the log.
pub fn create_snapshot(&mut self, repos: HashMap<String, RepoEntry>) -> GitResult<()> { pub fn create_snapshot(&mut self, repos: HashMap<String, RepoEntry>) -> GitResult<()> {
let snapshot = RaftSnapshot::new( let snapshot = RaftSnapshot::new(self.last_applied, self.term_at(self.last_applied), repos);
self.last_applied,
self.term_at(self.last_applied),
repos,
);
self.snapshot_storage.save(&snapshot)?; self.snapshot_storage.save(&snapshot)?;
self.snapshot_index = snapshot.last_included_index; self.snapshot_index = snapshot.last_included_index;
@@ -701,12 +728,16 @@ impl RaftLog {
} }
/// Restore state from a snapshot. /// Restore state from a snapshot.
pub fn restore_snapshot(&mut self, snapshot: RaftSnapshot) -> GitResult<HashMap<String, RepoEntry>> { pub fn restore_snapshot(
&mut self,
snapshot: RaftSnapshot,
) -> GitResult<HashMap<String, RepoEntry>> {
self.snapshot_index = snapshot.last_included_index; self.snapshot_index = snapshot.last_included_index;
self.snapshot_term = snapshot.last_included_term; self.snapshot_term = snapshot.last_included_term;
self.commit_index = snapshot.last_included_index; self.commit_index = snapshot.last_included_index;
self.last_applied = snapshot.last_included_index; self.last_applied = snapshot.last_included_index;
self.next_index.store(snapshot.last_included_index + 1, Ordering::SeqCst); self.next_index
.store(snapshot.last_included_index + 1, Ordering::SeqCst);
// Clear all entries (they're covered by the snapshot) // Clear all entries (they're covered by the snapshot)
self.entries.clear(); self.entries.clear();
+5 -2
View File
@@ -79,11 +79,14 @@ impl RaftSnapshot {
let last_commit = read_string(data, &mut offset)?; let last_commit = read_string(data, &mut offset)?;
let read_only = data.get(offset).copied().unwrap_or(0) == 1; let read_only = data.get(offset).copied().unwrap_or(0) == 1;
offset += 1; offset += 1;
repos.insert(path, RepoEntry { repos.insert(
path,
RepoEntry {
role, role,
last_commit, last_commit,
read_only, read_only,
}); },
);
} }
Some(Self { Some(Self {
+4 -5
View File
@@ -219,7 +219,9 @@ async fn sync_via_pack_service_to_file(
temp_dir: &Path, temp_dir: &Path,
) -> Result<Option<PathBuf>, String> { ) -> Result<Option<PathBuf>, String> {
use crate::pb::pack_service_client::PackServiceClient; use crate::pb::pack_service_client::PackServiceClient;
use crate::pb::{AdvertiseRefsRequest, PackObjectsOptions, PackObjectsRequest, RepositoryHeader}; use crate::pb::{
AdvertiseRefsRequest, PackObjectsOptions, PackObjectsRequest, RepositoryHeader,
};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
@@ -358,10 +360,7 @@ fn update_local_ref(repo_path: &Path, ref_name: &str, new_oid: &str) {
/// Apply a committed Raft command to the local git repository. /// Apply a committed Raft command to the local git repository.
/// This is called on followers when they receive committed entries from the leader. /// This is called on followers when they receive committed entries from the leader.
pub fn apply_raft_command_to_repo( pub fn apply_raft_command_to_repo(repo_prefix: &Path, command: &crate::actor::raft_log::Command) {
repo_prefix: &Path,
command: &crate::actor::raft_log::Command,
) {
match command { match command {
crate::actor::raft_log::Command::RefUpdate { crate::actor::raft_log::Command::RefUpdate {
relative_path, relative_path,
+50 -24
View File
@@ -120,7 +120,10 @@ fn generate_linguist(
(".m", "Objective-C"), // Mercury, MUF, etc. also claim .m (".m", "Objective-C"), // Mercury, MUF, etc. also claim .m
(".w", "CWeb"), // OpenSCAD also claims .w (".w", "CWeb"), // OpenSCAD also claims .w
(".q", "Q"), // KBD also claims .q (".q", "Q"), // KBD also claims .q
].iter().cloned().collect(); ]
.iter()
.cloned()
.collect();
for (ext, (lang, ltype)) in ext_primary { for (ext, (lang, ltype)) in ext_primary {
if let Some(&preferred) = priority_overrides.get(ext.as_str()) { if let Some(&preferred) = priority_overrides.get(ext.as_str()) {
@@ -140,7 +143,11 @@ fn generate_linguist(
if let Some(entry) = languages.get(preferred) if let Some(entry) = languages.get(preferred)
&& entry.extensions.iter().any(|e| e.to_lowercase() == ext) && entry.extensions.iter().any(|e| e.to_lowercase() == ext)
{ {
ext_map.push((ext.to_string(), preferred.to_string(), entry.lang_type.clone())); ext_map.push((
ext.to_string(),
preferred.to_string(),
entry.lang_type.clone(),
));
} }
} }
} }
@@ -172,8 +179,12 @@ fn generate_linguist(
code.push_str("/// Key is lowercase extension including the dot, e.g. \".rs\".\n"); code.push_str("/// Key is lowercase extension including the dot, e.g. \".rs\".\n");
code.push_str("pub static EXTENSION_MAP: &[(&str, &str, &str)] = &[\n"); code.push_str("pub static EXTENSION_MAP: &[(&str, &str, &str)] = &[\n");
for (ext, lang, ltype) in &ext_map { for (ext, lang, ltype) in &ext_map {
code.push_str(&format!(" (\"{}\", \"{}\", \"{}\"),\n", code.push_str(&format!(
escape_str(ext), escape_str(lang), escape_str(ltype))); " (\"{}\", \"{}\", \"{}\"),\n",
escape_str(ext),
escape_str(lang),
escape_str(ltype)
));
} }
code.push_str("];\n\n"); code.push_str("];\n\n");
@@ -182,8 +193,12 @@ fn generate_linguist(
code.push_str("/// Key is exact filename, e.g. \"Makefile\", \"Dockerfile\".\n"); code.push_str("/// Key is exact filename, e.g. \"Makefile\", \"Dockerfile\".\n");
code.push_str("pub static FILENAME_MAP: &[(&str, &str, &str)] = &[\n"); code.push_str("pub static FILENAME_MAP: &[(&str, &str, &str)] = &[\n");
for (fname, lang, ltype) in &fname_map { for (fname, lang, ltype) in &fname_map {
code.push_str(&format!(" (\"{}\", \"{}\", \"{}\"),\n", code.push_str(&format!(
escape_str(fname), escape_str(lang), escape_str(ltype))); " (\"{}\", \"{}\", \"{}\"),\n",
escape_str(fname),
escape_str(lang),
escape_str(ltype)
));
} }
code.push_str("];\n\n"); code.push_str("];\n\n");
@@ -191,8 +206,11 @@ fn generate_linguist(
code.push_str("/// Language name to type mapping.\n"); code.push_str("/// Language name to type mapping.\n");
code.push_str("pub static LANG_TYPE_MAP: &[(&str, &str)] = &[\n"); code.push_str("pub static LANG_TYPE_MAP: &[(&str, &str)] = &[\n");
for (lang, ltype) in &lang_type_map { for (lang, ltype) in &lang_type_map {
code.push_str(&format!(" (\"{}\", \"{}\"),\n", code.push_str(&format!(
escape_str(lang), escape_str(ltype))); " (\"{}\", \"{}\"),\n",
escape_str(lang),
escape_str(ltype)
));
} }
code.push_str("];\n\n"); code.push_str("];\n\n");
@@ -202,8 +220,11 @@ fn generate_linguist(
let mut group_vec: Vec<_> = lang_group_map.iter().collect(); let mut group_vec: Vec<_> = lang_group_map.iter().collect();
group_vec.sort_by(|a, b| a.0.cmp(b.0)); group_vec.sort_by(|a, b| a.0.cmp(b.0));
for (lang, group) in group_vec { for (lang, group) in group_vec {
code.push_str(&format!(" (\"{}\", \"{}\"),\n", code.push_str(&format!(
escape_str(lang), escape_str(group))); " (\"{}\", \"{}\"),\n",
escape_str(lang),
escape_str(group)
));
} }
code.push_str("];\n\n"); code.push_str("];\n\n");
@@ -213,23 +234,28 @@ fn generate_linguist(
code.push_str(" match ext {\n"); code.push_str(" match ext {\n");
// Image extensions // Image extensions
let image_exts = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", let image_exts = [
".webp", ".tiff", ".tif", ".psd", ".raw", ".heic", ".heif", ".avif", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp", ".tiff", ".tif", ".psd",
".apng", ".jfif", ".pjpeg", ".pjp"]; ".raw", ".heic", ".heif", ".avif", ".apng", ".jfif", ".pjpeg", ".pjp",
];
for ext in &image_exts { for ext in &image_exts {
code.push_str(&format!(" \"{}\" => \"Image\",\n", ext)); code.push_str(&format!(" \"{}\" => \"Image\",\n", ext));
} }
// Video extensions // Video extensions
let video_exts = [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", let video_exts = [
".m4v", ".mpg", ".mpeg", ".3gp", ".3g2", ".ogv", ".vob"]; ".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v", ".mpg", ".mpeg", ".3gp",
".3g2", ".ogv", ".vob",
];
for ext in &video_exts { for ext in &video_exts {
code.push_str(&format!(" \"{}\" => \"Video\",\n", ext)); code.push_str(&format!(" \"{}\" => \"Video\",\n", ext));
} }
// Audio extensions // Audio extensions
let audio_exts = [".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma", ".m4a", let audio_exts = [
".opus", ".aiff", ".ape", ".alac", ".mid", ".midi"]; ".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma", ".m4a", ".opus", ".aiff", ".ape", ".alac",
".mid", ".midi",
];
for ext in &audio_exts { for ext in &audio_exts {
code.push_str(&format!(" \"{}\" => \"Audio\",\n", ext)); code.push_str(&format!(" \"{}\" => \"Audio\",\n", ext));
} }
@@ -241,11 +267,12 @@ fn generate_linguist(
} }
// Other binary // Other binary
let binary_exts = [".exe", ".dll", ".so", ".dylib", ".a", ".lib", ".o", let binary_exts = [
".obj", ".bin", ".dat", ".db", ".sqlite", ".sqlite3", ".pyc", ".pyo", ".exe", ".dll", ".so", ".dylib", ".a", ".lib", ".o", ".obj", ".bin", ".dat", ".db",
".class", ".jar", ".war", ".ear", ".zip", ".tar", ".gz", ".sqlite", ".sqlite3", ".pyc", ".pyo", ".class", ".jar", ".war", ".ear", ".zip", ".tar",
".bz2", ".xz", ".7z", ".rar", ".pdf", ".doc", ".docx", ".xls", ".gz", ".bz2", ".xz", ".7z", ".rar", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt",
".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp", ".wasm", ".node"]; ".pptx", ".odt", ".ods", ".odp", ".wasm", ".node",
];
for ext in &binary_exts { for ext in &binary_exts {
code.push_str(&format!(" \"{}\" => \"Binary\",\n", ext)); code.push_str(&format!(" \"{}\" => \"Binary\",\n", ext));
} }
@@ -259,8 +286,7 @@ fn generate_linguist(
} }
fn escape_str(s: &str) -> String { fn escape_str(s: &str) -> String {
s.replace('\\', "\\\\") s.replace('\\', "\\\\").replace('"', "\\\"")
.replace('"', "\\\"")
} }
fn proto_files(proto_dir: &Path) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> { fn proto_files(proto_dir: &Path) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
+4 -5
View File
@@ -39,17 +39,16 @@ impl GitBare {
for oid_bytes in &request.oids { for oid_bytes in &request.oids {
let hex: String = oid_bytes.iter().map(|b| format!("{b:02x}")).collect(); let hex: String = oid_bytes.iter().map(|b| format!("{b:02x}")).collect();
if let Ok(oid) = gix::ObjectId::from_hex(hex.as_bytes()) { if let Ok(oid) = gix::ObjectId::from_hex(hex.as_bytes())
if let Ok(obj) = repo.find_object(oid) { && let Ok(obj) = repo.find_object(oid)
if let Ok(commit) = obj.try_into_commit() { && let Ok(commit) = obj.try_into_commit()
{
commits.push(crate::commit::get_commit::commit_to_pb( commits.push(crate::commit::get_commit::commit_to_pb(
self, self,
&commit, &commit,
request.include_stats, request.include_stats,
)); ));
} }
}
}
if commits.len() >= 100 { if commits.len() >= 100 {
break; break;
} }
+4 -5
View File
@@ -58,16 +58,15 @@ impl GitBare {
for line in stdout.lines().skip(request.offset as usize) { for line in stdout.lines().skip(request.offset as usize) {
let hex = line.trim(); let hex = line.trim();
if let Ok(oid) = gix::ObjectId::from_hex(hex.as_bytes()) { if let Ok(oid) = gix::ObjectId::from_hex(hex.as_bytes())
if let Ok(obj) = repo.find_object(oid) { && let Ok(obj) = repo.find_object(oid)
if let Ok(commit) = obj.try_into_commit() { && let Ok(commit) = obj.try_into_commit()
{
commits.push(crate::commit::get_commit::commit_to_pb( commits.push(crate::commit::get_commit::commit_to_pb(
self, &commit, false, self, &commit, false,
)); ));
} }
} }
}
}
Ok(CommitsByMessageResponse { commits }) Ok(CommitsByMessageResponse { commits })
} }
+21 -10
View File
@@ -271,10 +271,8 @@ impl DiskCache {
} }
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let path = self.cache_file_path(namespace, digest); let path = self.cache_file_path(namespace, digest);
if !path.exists() {
crate::metrics::record_cache_op("disk", "miss", start.elapsed()); // Check expiry before reading, but handle concurrent deletion gracefully.
return Ok(None);
}
if let Ok(metadata) = std::fs::metadata(&path) if let Ok(metadata) = std::fs::metadata(&path)
&& let Ok(modified) = metadata.modified() && let Ok(modified) = metadata.modified()
&& let Ok(age) = SystemTime::now().duration_since(modified) && let Ok(age) = SystemTime::now().duration_since(modified)
@@ -292,7 +290,9 @@ impl DiskCache {
crate::metrics::record_cache_op("disk", "expired", start.elapsed()); crate::metrics::record_cache_op("disk", "expired", start.elapsed());
return Ok(None); return Ok(None);
} }
let data = std::fs::read(&path).map_err(GitError::Io)?;
match std::fs::read(&path) {
Ok(data) => {
tracing::debug!( tracing::debug!(
namespace = %namespace, namespace = %namespace,
digest = %digest, digest = %digest,
@@ -303,6 +303,14 @@ impl DiskCache {
crate::metrics::record_cache_op("disk", "hit", start.elapsed()); crate::metrics::record_cache_op("disk", "hit", start.elapsed());
Ok(Some(data)) Ok(Some(data))
} }
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// File was deleted between metadata check and read — treat as miss.
crate::metrics::record_cache_op("disk", "miss", start.elapsed());
Ok(None)
}
Err(e) => Err(GitError::Io(e)),
}
}
/// Insert a cached response for the given namespace and digest. /// Insert a cached response for the given namespace and digest.
pub fn insert(&self, namespace: &str, digest: &str, data: &[u8]) -> GitResult<()> { pub fn insert(&self, namespace: &str, digest: &str, data: &[u8]) -> GitResult<()> {
@@ -338,9 +346,8 @@ impl DiskCache {
return Ok(None); return Ok(None);
} }
let path = self.cache_file_path(namespace, digest); let path = self.cache_file_path(namespace, digest);
if !path.exists() {
return Ok(None); // Check expiry; handle concurrent deletion gracefully.
}
if let Ok(metadata) = std::fs::metadata(&path) if let Ok(metadata) = std::fs::metadata(&path)
&& let Ok(modified) = metadata.modified() && let Ok(modified) = metadata.modified()
&& let Ok(age) = SystemTime::now().duration_since(modified) && let Ok(age) = SystemTime::now().duration_since(modified)
@@ -349,8 +356,12 @@ impl DiskCache {
std::fs::remove_file(&path).ok(); std::fs::remove_file(&path).ok();
return Ok(None); return Ok(None);
} }
let file = std::fs::File::open(&path).map_err(GitError::Io)?;
Ok(Some(file)) match std::fs::File::open(&path) {
Ok(file) => Ok(Some(file)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(GitError::Io(e)),
}
} }
/// Open a cache file for streaming write. /// Open a cache file for streaming write.
+10 -14
View File
@@ -128,10 +128,14 @@ pub fn run_hook_dir(
/// Run a single hook script with stdin data and timeout. /// Run a single hook script with stdin data and timeout.
fn run_single_script(script_path: &Path, stdin_data: &[u8], timeout: Duration) -> HookResult { fn run_single_script(script_path: &Path, stdin_data: &[u8], timeout: Duration) -> HookResult {
// Use Stdio::null() for stdout/stderr to prevent pipe-buffer deadlock.
// With Stdio::piped() + never reading, a hook that writes >64KB of output
// would block the child on write(), and the parent's try_wait() would
// loop until timeout before killing it.
let child = std::process::Command::new(script_path) let child = std::process::Command::new(script_path)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::null())
.stderr(Stdio::piped()) .stderr(Stdio::null())
.spawn(); .spawn();
match child { match child {
@@ -143,17 +147,12 @@ fn run_single_script(script_path: &Path, stdin_data: &[u8], timeout: Duration) -
let wait_result = c.wait_timeout(timeout); let wait_result = c.wait_timeout(timeout);
match wait_result { match wait_result {
Ok(Some(status)) => { Ok(Some(status)) => HookResult {
// Process exited within timeout, get its output
// Note: We already have the status, so we need to construct output differently
// Since wait_with_output would fail after try_wait, we return status-only output
HookResult {
accepted: status.success(), accepted: status.success(),
exit_code: status.code().unwrap_or(-1), exit_code: status.code().unwrap_or(-1),
stdout: String::new(), // stdout was consumed by the process stdout: String::new(),
stderr: String::new(), // stderr was consumed by the process stderr: String::new(),
} },
}
Ok(None) => { Ok(None) => {
tracing::warn!( tracing::warn!(
script = %script_path.display(), script = %script_path.display(),
@@ -161,7 +160,6 @@ fn run_single_script(script_path: &Path, stdin_data: &[u8], timeout: Duration) -
"hook script timed out, killing" "hook script timed out, killing"
); );
let _ = c.kill(); let _ = c.kill();
// Explicitly wait to reap the zombie process
let _ = c.wait(); let _ = c.wait();
HookResult::rejected(format!( HookResult::rejected(format!(
"hook script timed out after {}s: {}", "hook script timed out after {}s: {}",
@@ -171,7 +169,6 @@ fn run_single_script(script_path: &Path, stdin_data: &[u8], timeout: Duration) -
} }
Err(e) => { Err(e) => {
let _ = c.kill(); let _ = c.kill();
// Explicitly wait to reap the zombie process
let _ = c.wait(); let _ = c.wait();
HookResult::rejected(format!("hook script wait error: {e}")) HookResult::rejected(format!("hook script wait error: {e}"))
} }
@@ -183,7 +180,6 @@ fn run_single_script(script_path: &Path, stdin_data: &[u8], timeout: Duration) -
error = %e, error = %e,
"failed to spawn hook script" "failed to spawn hook script"
); );
// If the script can't be executed, treat as rejection
HookResult::rejected(format!("failed to spawn hook script: {e}")) HookResult::rejected(format!("failed to spawn hook script: {e}"))
} }
} }
+9 -1
View File
@@ -106,7 +106,15 @@ pub fn validate_hook_content(content: &str) -> GitResult<()> {
/// Check for common obfuscation attempts. /// Check for common obfuscation attempts.
fn check_obfuscation_attempts(content: &str) -> GitResult<()> { fn check_obfuscation_attempts(content: &str) -> GitResult<()> {
// Check for excessive use of special characters that might indicate obfuscation // Check for excessive use of special characters that might indicate obfuscation
let special_char_count = content.chars().filter(|c| matches!(c, '$' | '`' | '\\' | '|' | ';' | '&' | '(' | ')' | '{' | '}' | '[' | ']')).count(); let special_char_count = content
.chars()
.filter(|c| {
matches!(
c,
'$' | '`' | '\\' | '|' | ';' | '&' | '(' | ')' | '{' | '}' | '[' | ']'
)
})
.count();
let total_chars = content.chars().count(); let total_chars = content.chars().count();
// If more than 30% of content is special characters, it's suspicious // If more than 30% of content is special characters, it's suspicious
+11 -7
View File
@@ -75,7 +75,9 @@ fn init_tracing() -> Option<tracing_appender::non_blocking::WorkerGuard> {
builder = builder.max_log_files(retention); builder = builder.max_log_files(retention);
} }
let file_appender = builder.build(&log_dir).expect("failed to create log directory"); let file_appender = builder
.build(&log_dir)
.expect("failed to create log directory");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let file_layer = fmt::layer() let file_layer = fmt::layer()
@@ -143,10 +145,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
); );
if disk_cache_enabled { if disk_cache_enabled {
tracing::info!( tracing::info!(max_age_secs = disk_cache_max_age, "disk cache enabled");
max_age_secs = disk_cache_max_age,
"disk cache enabled"
);
disk_cache.cleanup_on_startup()?; disk_cache.cleanup_on_startup()?;
gitks::disk_cache::start_cache_cleanup_task(disk_cache.clone(), Duration::from_secs(300)); gitks::disk_cache::start_cache_cleanup_task(disk_cache.clone(), Duration::from_secs(300));
} else { } else {
@@ -290,8 +289,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
let raft_data_dir = repo_prefix.join(".gitks_raft"); let raft_data_dir = repo_prefix.join(".gitks_raft");
let (node_actor, node_handle) = let (node_actor, node_handle) = init_actor_cluster(
init_actor_cluster(svc.clone(), storage_name.clone(), grpc_addr.clone(), raft_data_dir).await?; svc.clone(),
storage_name.clone(),
grpc_addr.clone(),
raft_data_dir,
)
.await?;
let svc = svc let svc = svc
.with_actor(node_actor.clone()) .with_actor(node_actor.clone())
.with_grpc_addr(grpc_addr.clone()); .with_grpc_addr(grpc_addr.clone());
+137 -62
View File
@@ -20,7 +20,7 @@
//! - GET /debug/config — Runtime configuration //! - GET /debug/config — Runtime configuration
use dashmap::DashMap; use dashmap::DashMap;
use std::sync::atomic::{AtomicU64, AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -82,7 +82,9 @@ struct MetricsInner {
static METRICS: OnceLock<Arc<MetricsInner>> = OnceLock::new(); static METRICS: OnceLock<Arc<MetricsInner>> = OnceLock::new();
/// Handle for dynamic log level reload. /// Handle for dynamic log level reload.
static LOG_RELOAD_HANDLE: OnceLock<Option<tracing_subscriber::reload::Handle<EnvFilter, tracing_subscriber::Registry>>> = OnceLock::new(); static LOG_RELOAD_HANDLE: OnceLock<
Option<tracing_subscriber::reload::Handle<EnvFilter, tracing_subscriber::Registry>>,
> = OnceLock::new();
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@@ -141,7 +143,9 @@ fn record_duration_bucket(map: &DashMap<String, AtomicU64>, key_prefix: &str, du
} }
pub fn set_slow_request_threshold(ms: u64) { pub fn set_slow_request_threshold(ms: u64) {
metrics().slow_request_threshold_ms.store(ms, Ordering::Relaxed); metrics()
.slow_request_threshold_ms
.store(ms, Ordering::Relaxed);
} }
pub fn set_ready(ready: bool) { pub fn set_ready(ready: bool) {
@@ -254,12 +258,19 @@ pub fn record_hook_execution(hook_type: &str, result: &str, duration: Duration)
record_duration_bucket(&m.hook_duration_buckets, hook_type, duration_ms); record_duration_bucket(&m.hook_duration_buckets, hook_type, duration_ms);
} }
pub fn set_raft_state(term: u64, commit_index: u64, last_applied: u64, is_leader: bool, log_entries: u64) { pub fn set_raft_state(
term: u64,
commit_index: u64,
last_applied: u64,
is_leader: bool,
log_entries: u64,
) {
let m = metrics(); let m = metrics();
m.raft_term.store(term, Ordering::Relaxed); m.raft_term.store(term, Ordering::Relaxed);
m.raft_commit_index.store(commit_index, Ordering::Relaxed); m.raft_commit_index.store(commit_index, Ordering::Relaxed);
m.raft_last_applied.store(last_applied, Ordering::Relaxed); m.raft_last_applied.store(last_applied, Ordering::Relaxed);
m.raft_is_leader.store(if is_leader { 1 } else { 0 }, Ordering::Relaxed); m.raft_is_leader
.store(if is_leader { 1 } else { 0 }, Ordering::Relaxed);
m.raft_log_entries.store(log_entries, Ordering::Relaxed); m.raft_log_entries.store(log_entries, Ordering::Relaxed);
} }
@@ -268,7 +279,8 @@ pub fn inc_raft_append_entries(success: bool) {
let m = metrics(); let m = metrics();
m.raft_append_entries_total.fetch_add(1, Ordering::Relaxed); m.raft_append_entries_total.fetch_add(1, Ordering::Relaxed);
if success { if success {
m.raft_append_entries_success.fetch_add(1, Ordering::Relaxed); m.raft_append_entries_success
.fetch_add(1, Ordering::Relaxed);
} }
} }
@@ -296,14 +308,22 @@ fn prom_escape(value: &str) -> String {
out out
} }
fn render_counter_map(out: &mut String, name: &str, help: &str, map: &DashMap<String, AtomicU64>, labels: &[&str]) { fn render_counter_map(
out: &mut String,
name: &str,
help: &str,
map: &DashMap<String, AtomicU64>,
labels: &[&str],
) {
out.push_str(&format!("# HELP {name} {help}\n")); out.push_str(&format!("# HELP {name} {help}\n"));
out.push_str(&format!("# TYPE {name} counter\n")); out.push_str(&format!("# TYPE {name} counter\n"));
for entry in map { for entry in map {
let (key, count) = (entry.key(), entry.value().load(Ordering::Relaxed)); let (key, count) = (entry.key(), entry.value().load(Ordering::Relaxed));
let parts: Vec<&str> = key.split(':').collect(); let parts: Vec<&str> = key.split(':').collect();
if parts.len() == labels.len() { if parts.len() == labels.len() {
let label_str: String = labels.iter().zip(parts.iter()) let label_str: String = labels
.iter()
.zip(parts.iter())
.map(|(l, v)| format!("{l}=\"{}\"", prom_escape(v))) .map(|(l, v)| format!("{l}=\"{}\"", prom_escape(v)))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
@@ -324,7 +344,10 @@ fn render_histogram(out: &mut String, name: &str, help: &str, map: &DashMap<Stri
} else { } else {
bound_str.to_string() bound_str.to_string()
}; };
out.push_str(&format!("{name}_bucket{{method=\"{}\",le=\"{le}\"}} {count}\n", prom_escape(key))); out.push_str(&format!(
"{name}_bucket{{method=\"{}\",le=\"{le}\"}} {count}\n",
prom_escape(key)
));
} }
} }
out.push('\n'); out.push('\n');
@@ -350,16 +373,30 @@ pub fn render_metrics() -> String {
out.push_str(&format!("gitks_repository_count {repos}\n\n")); out.push_str(&format!("gitks_repository_count {repos}\n\n"));
// gRPC requests // gRPC requests
render_counter_map(&mut out, "gitks_requests_total", render_counter_map(
"Total gRPC requests by method and status", &m.request_count, &["method", "status"]); &mut out,
"gitks_requests_total",
"Total gRPC requests by method and status",
&m.request_count,
&["method", "status"],
);
// gRPC duration // gRPC duration
render_histogram(&mut out, "gitks_request_duration_milliseconds", render_histogram(
"Request duration histogram in ms", &m.duration_buckets); &mut out,
"gitks_request_duration_milliseconds",
"Request duration histogram in ms",
&m.duration_buckets,
);
// Slow requests // Slow requests
render_counter_map(&mut out, "gitks_slow_requests_total", render_counter_map(
"Slow gRPC requests by method", &m.slow_request_count, &["method"]); &mut out,
"gitks_slow_requests_total",
"Slow gRPC requests by method",
&m.slow_request_count,
&["method"],
);
// Cache // Cache
let hits = m.cache_hits.load(Ordering::Relaxed); let hits = m.cache_hits.load(Ordering::Relaxed);
@@ -372,26 +409,58 @@ pub fn render_metrics() -> String {
out.push_str(&format!("gitks_cache_misses_total {misses}\n\n")); out.push_str(&format!("gitks_cache_misses_total {misses}\n\n"));
// Errors // Errors
render_counter_map(&mut out, "gitks_errors_total", render_counter_map(
"Total errors by kind", &m.error_count, &["kind"]); &mut out,
"gitks_errors_total",
"Total errors by kind",
&m.error_count,
&["kind"],
);
// Git subprocess // Git subprocess
render_counter_map(&mut out, "gitks_git_cmd_total", render_counter_map(
"Git subprocess calls by command", &m.git_cmd_count, &["command"]); &mut out,
render_histogram(&mut out, "gitks_git_cmd_duration_milliseconds", "gitks_git_cmd_total",
"Git subprocess duration in ms", &m.git_cmd_duration_buckets); "Git subprocess calls by command",
&m.git_cmd_count,
&["command"],
);
render_histogram(
&mut out,
"gitks_git_cmd_duration_milliseconds",
"Git subprocess duration in ms",
&m.git_cmd_duration_buckets,
);
// Cache operations // Cache operations
render_counter_map(&mut out, "gitks_cache_ops_total", render_counter_map(
"Cache operations by cache and result", &m.cache_op_count, &["cache", "result"]); &mut out,
render_histogram(&mut out, "gitks_cache_op_duration_milliseconds", "gitks_cache_ops_total",
"Cache operation duration in ms", &m.cache_op_duration_buckets); "Cache operations by cache and result",
&m.cache_op_count,
&["cache", "result"],
);
render_histogram(
&mut out,
"gitks_cache_op_duration_milliseconds",
"Cache operation duration in ms",
&m.cache_op_duration_buckets,
);
// Hook execution // Hook execution
render_counter_map(&mut out, "gitks_hook_executions_total", render_counter_map(
"Hook executions by type and result", &m.hook_count, &["hook_type", "result"]); &mut out,
render_histogram(&mut out, "gitks_hook_duration_milliseconds", "gitks_hook_executions_total",
"Hook execution duration in ms", &m.hook_duration_buckets); "Hook executions by type and result",
&m.hook_count,
&["hook_type", "result"],
);
render_histogram(
&mut out,
"gitks_hook_duration_milliseconds",
"Hook execution duration in ms",
&m.hook_duration_buckets,
);
// Raft consensus metrics // Raft consensus metrics
let raft_term = m.raft_term.load(Ordering::Relaxed); let raft_term = m.raft_term.load(Ordering::Relaxed);
@@ -426,11 +495,15 @@ pub fn render_metrics() -> String {
out.push_str("# HELP gitks_raft_append_entries_total Total AppendEntries RPCs sent\n"); out.push_str("# HELP gitks_raft_append_entries_total Total AppendEntries RPCs sent\n");
out.push_str("# TYPE gitks_raft_append_entries_total counter\n"); out.push_str("# TYPE gitks_raft_append_entries_total counter\n");
out.push_str(&format!("gitks_raft_append_entries_total {raft_ae_total}\n\n")); out.push_str(&format!(
"gitks_raft_append_entries_total {raft_ae_total}\n\n"
));
out.push_str("# HELP gitks_raft_append_entries_success Successful AppendEntries RPCs\n"); out.push_str("# HELP gitks_raft_append_entries_success Successful AppendEntries RPCs\n");
out.push_str("# TYPE gitks_raft_append_entries_success counter\n"); out.push_str("# TYPE gitks_raft_append_entries_success counter\n");
out.push_str(&format!("gitks_raft_append_entries_success {raft_ae_success}\n\n")); out.push_str(&format!(
"gitks_raft_append_entries_success {raft_ae_success}\n\n"
));
out.push_str("# HELP gitks_raft_elections_total Total elections triggered\n"); out.push_str("# HELP gitks_raft_elections_total Total elections triggered\n");
out.push_str("# TYPE gitks_raft_elections_total counter\n"); out.push_str("# TYPE gitks_raft_elections_total counter\n");
@@ -438,7 +511,9 @@ pub fn render_metrics() -> String {
out.push_str("# HELP gitks_raft_elections_won Elections won by this node\n"); out.push_str("# HELP gitks_raft_elections_won Elections won by this node\n");
out.push_str("# TYPE gitks_raft_elections_won counter\n"); out.push_str("# TYPE gitks_raft_elections_won counter\n");
out.push_str(&format!("gitks_raft_elections_won {raft_elections_won}\n\n")); out.push_str(&format!(
"gitks_raft_elections_won {raft_elections_won}\n\n"
));
out out
} }
@@ -446,12 +521,12 @@ pub fn render_metrics() -> String {
use bytes::Bytes; use bytes::Bytes;
use http_body_util::Full; use http_body_util::Full;
use hyper::body::Incoming; use hyper::body::Incoming;
use hyper::{Request, Response, Method};
use hyper::service::Service; use hyper::service::Service;
use hyper::{Method, Request, Response};
use std::convert::Infallible; use std::convert::Infallible;
use std::future::Future; use std::future::Future;
use std::pin::Pin;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::pin::Pin;
/// Global cancel token for the HTTP server, set from main. /// Global cancel token for the HTTP server, set from main.
static HTTP_CANCEL: OnceLock<tokio_util::sync::CancellationToken> = OnceLock::new(); static HTTP_CANCEL: OnceLock<tokio_util::sync::CancellationToken> = OnceLock::new();
@@ -501,9 +576,7 @@ async fn handle_request(req: Request<Incoming>) -> Result<Response<Full<Bytes>>,
let body = render_metrics(); let body = render_metrics();
text_response(200, "text/plain; version=0.0.4; charset=utf-8", body) text_response(200, "text/plain; version=0.0.4; charset=utf-8", body)
} }
(Method::GET, "/health") => { (Method::GET, "/health") => json_response(200, r#"{"status":"healthy"}"#),
json_response(200, r#"{"status":"healthy"}"#)
}
(Method::GET, "/ready") => { (Method::GET, "/ready") => {
if metrics().ready.load(Ordering::Relaxed) { if metrics().ready.load(Ordering::Relaxed) {
json_response(200, r#"{"status":"ready"}"#) json_response(200, r#"{"status":"ready"}"#)
@@ -519,30 +592,28 @@ async fn handle_request(req: Request<Incoming>) -> Result<Response<Full<Bytes>>,
}; };
json_response(200, &format!(r#"{{"log_level":"{msg}"}}"#)) json_response(200, &format!(r#"{{"log_level":"{msg}"}}"#))
} }
(Method::PUT, "/debug/log-level") => { (Method::PUT, "/debug/log-level") => match handle_log_level_update(req).await {
match handle_log_level_update(req).await {
Ok(resp) => resp, Ok(resp) => resp,
Err(e) => json_response(400, &format!(r#"{{"error":"{e}"}}"#)), Err(e) => json_response(400, &format!(r#"{{"error":"{e}"}}"#)),
} },
}
(Method::GET, "/debug/config") => { (Method::GET, "/debug/config") => {
let threshold = metrics().slow_request_threshold_ms.load(Ordering::Relaxed); let threshold = metrics().slow_request_threshold_ms.load(Ordering::Relaxed);
let ready = metrics().ready.load(Ordering::Relaxed); let ready = metrics().ready.load(Ordering::Relaxed);
json_response(200, &format!( json_response(
r#"{{"slow_request_threshold_ms":{},"ready":{}}}"#, threshold, ready 200,
)) &format!(
} r#"{{"slow_request_threshold_ms":{},"ready":{}}}"#,
_ => { threshold, ready
json_response(404, r#"{"error":"not found"}"#) ),
)
} }
_ => json_response(404, r#"{"error":"not found"}"#),
}; };
Ok(response) Ok(response)
} }
async fn handle_log_level_update( async fn handle_log_level_update(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, String> {
req: Request<Incoming>,
) -> Result<Response<Full<Bytes>>, String> {
use http_body_util::BodyExt; use http_body_util::BodyExt;
let body_bytes = req let body_bytes = req
@@ -551,8 +622,8 @@ async fn handle_log_level_update(
.map_err(|e| format!("failed to read body: {e}"))? .map_err(|e| format!("failed to read body: {e}"))?
.to_bytes(); .to_bytes();
let new_filter = String::from_utf8(body_bytes.to_vec()) let new_filter =
.map_err(|e| format!("invalid UTF-8: {e}"))?; String::from_utf8(body_bytes.to_vec()).map_err(|e| format!("invalid UTF-8: {e}"))?;
let new_filter = new_filter.trim().to_string(); let new_filter = new_filter.trim().to_string();
if new_filter.is_empty() { if new_filter.is_empty() {
@@ -566,25 +637,29 @@ async fn handle_log_level_update(
Ok(json_response(500, &format!(r#"{{"error":"{e}"}}"#))) Ok(json_response(500, &format!(r#"{{"error":"{e}"}}"#)))
} else { } else {
tracing::info!(new_filter = %new_filter, "log level updated via HTTP"); tracing::info!(new_filter = %new_filter, "log level updated via HTTP");
Ok(json_response(200, &format!( Ok(json_response(
r#"{{"status":"ok","filter":"{}"}}"#, new_filter 200,
))) &format!(r#"{{"status":"ok","filter":"{}"}}"#, new_filter),
))
} }
} }
Err(e) => Ok(json_response(400, &format!( Err(e) => Ok(json_response(
r#"{{"error":"invalid filter: {e}"}}"# 400,
))), &format!(r#"{{"error":"invalid filter: {e}"}}"#),
)),
}, },
_ => Ok(json_response(501, r#"{"error":"dynamic log level not configured"}"#)), _ => Ok(json_response(
501,
r#"{"error":"dynamic log level not configured"}"#,
)),
} }
} }
/// Start the HTTP server (metrics + health + debug) using hyper 1.x. /// Start the HTTP server (metrics + health + debug) using hyper 1.x.
pub fn start_metrics_server(port: u16) -> tokio::task::JoinHandle<()> { pub fn start_metrics_server(port: u16) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move { tokio::spawn(async move {
let listener = match tokio::net::TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))) let listener =
.await match tokio::net::TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await {
{
Ok(l) => l, Ok(l) => l,
Err(e) => { Err(e) => {
tracing::error!(port, error = %e, "failed to bind HTTP server"); tracing::error!(port, error = %e, "failed to bind HTTP server");
+7 -3
View File
@@ -6,9 +6,9 @@ use tokio::process::Command;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use super::CancellableReceiverStream;
use crate::bare::GitBare; use crate::bare::GitBare;
use crate::pb::ReceivePackResponse; use crate::pb::ReceivePackResponse;
use super::CancellableReceiverStream;
/// Maximum time allowed for a git receive-pack process before it is killed. /// Maximum time allowed for a git receive-pack process before it is killed.
const RECEIVE_PACK_TIMEOUT: Duration = Duration::from_secs(1800); // 30 minutes const RECEIVE_PACK_TIMEOUT: Duration = Duration::from_secs(1800); // 30 minutes
@@ -28,7 +28,8 @@ impl GitBare {
input: impl tokio_stream::Stream<Item = Result<crate::pb::ReceivePackRequest, tonic::Status>> input: impl tokio_stream::Stream<Item = Result<crate::pb::ReceivePackRequest, tonic::Status>>
+ Send + Send
+ 'static, + 'static,
) -> Result<CancellableReceiverStream<Result<ReceivePackResponse, tonic::Status>>, tonic::Status> { ) -> Result<CancellableReceiverStream<Result<ReceivePackResponse, tonic::Status>>, tonic::Status>
{
let bare_dir = self.bare_dir.to_string_lossy().into_owned(); let bare_dir = self.bare_dir.to_string_lossy().into_owned();
tracing::info!( tracing::info!(
repo = %bare_dir, repo = %bare_dir,
@@ -186,6 +187,9 @@ impl GitBare {
let rx_stream = ReceiverStream::new(rx); let rx_stream = ReceiverStream::new(rx);
let cancel_guard = cancel_token_clone.clone().drop_guard(); let cancel_guard = cancel_token_clone.clone().drop_guard();
Ok(super::CancellableReceiverStream::new(rx_stream, cancel_guard)) Ok(super::CancellableReceiverStream::new(
rx_stream,
cancel_guard,
))
} }
} }
+7 -3
View File
@@ -6,9 +6,9 @@ use tokio::process::Command;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use super::CancellableReceiverStream;
use crate::bare::GitBare; use crate::bare::GitBare;
use crate::pb::UploadPackResponse; use crate::pb::UploadPackResponse;
use super::CancellableReceiverStream;
/// Maximum time allowed for a git upload-pack process before it is killed. /// Maximum time allowed for a git upload-pack process before it is killed.
const UPLOAD_PACK_TIMEOUT: Duration = Duration::from_secs(600); // 10 minutes const UPLOAD_PACK_TIMEOUT: Duration = Duration::from_secs(600); // 10 minutes
@@ -28,7 +28,8 @@ impl GitBare {
input: impl tokio_stream::Stream<Item = Result<crate::pb::UploadPackRequest, tonic::Status>> input: impl tokio_stream::Stream<Item = Result<crate::pb::UploadPackRequest, tonic::Status>>
+ Send + Send
+ 'static, + 'static,
) -> Result<CancellableReceiverStream<Result<UploadPackResponse, tonic::Status>>, tonic::Status> { ) -> Result<CancellableReceiverStream<Result<UploadPackResponse, tonic::Status>>, tonic::Status>
{
let bare_dir = self.bare_dir.to_string_lossy().into_owned(); let bare_dir = self.bare_dir.to_string_lossy().into_owned();
tracing::info!( tracing::info!(
repo = %bare_dir, repo = %bare_dir,
@@ -189,6 +190,9 @@ impl GitBare {
let rx_stream = ReceiverStream::new(rx); let rx_stream = ReceiverStream::new(rx);
let cancel_guard = cancel_token_clone.clone().drop_guard(); let cancel_guard = cancel_token_clone.clone().drop_guard();
Ok(super::CancellableReceiverStream::new(rx_stream, cancel_guard)) Ok(super::CancellableReceiverStream::new(
rx_stream,
cancel_guard,
))
} }
} }
+1 -2
View File
@@ -192,8 +192,7 @@ pub fn set_max_concurrent(max: usize) {
.collect(); .collect();
for key in keys { for key in keys {
l.semaphores l.semaphores.insert(key, Arc::new(Semaphore::new(max)));
.insert(key, Arc::new(Semaphore::new(max)));
} }
tracing::info!(max_concurrent = max, "rate limit max_concurrent updated"); tracing::info!(max_concurrent = max, "rate limit max_concurrent updated");
+1 -1
View File
@@ -69,7 +69,7 @@ impl GitBare {
// Sort direction // Sort direction
let sort_prefix = match SortDirection::try_from(request.sort_direction) { let sort_prefix = match SortDirection::try_from(request.sort_direction) {
Ok(SortDirection::Asc) => "", Ok(SortDirection::Asc) => "",
Ok(SortDirection::Desc) | _ => "-", _ => "-",
}; };
args.push(format!("--sort={sort_prefix}refname")); args.push(format!("--sort={sort_prefix}refname"));
+4 -3
View File
@@ -53,11 +53,12 @@ impl GitBare {
})?; })?;
} }
drop(child.stdin.take()); drop(child.stdin.take());
let output = child.wait_with_output().map_err(|e| { let output =
crate::error::GitError::CommandFailed { child
.wait_with_output()
.map_err(|e| crate::error::GitError::CommandFailed {
status_code: None, status_code: None,
stderr: e.to_string(), stderr: e.to_string(),
}
})?; })?;
let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
+3 -3
View File
@@ -113,15 +113,15 @@ impl GitBare {
]) ])
.output(); .output();
if let Ok(out) = head_output { if let Ok(out) = head_output
if !out.status.success() { && !out.status.success()
{
tracing::warn!( tracing::warn!(
repo = %self.bare_dir.display(), repo = %self.bare_dir.display(),
stderr = %String::from_utf8_lossy(&out.stderr).trim(), stderr = %String::from_utf8_lossy(&out.stderr).trim(),
"failed to auto-set remote HEAD" "failed to auto-set remote HEAD"
); );
} }
}
Ok(UpdateRemoteMirrorResponse { Ok(UpdateRemoteMirrorResponse {
ok: true, ok: true,
+13 -6
View File
@@ -5,9 +5,7 @@ use gix::object::tree::EntryKind;
use crate::bare::GitBare; use crate::bare::GitBare;
use crate::error::{GitError, GitResult}; use crate::error::{GitError, GitResult};
use crate::pb::{ use crate::pb::{GetLanguageStatsRequest, GetLanguageStatsResponse, LanguageStat, object_selector};
GetLanguageStatsRequest, GetLanguageStatsResponse, LanguageStat, object_selector,
};
// Include the generated linguist rules // Include the generated linguist rules
include!(concat!(env!("OUT_DIR"), "/linguist_generated.rs")); include!(concat!(env!("OUT_DIR"), "/linguist_generated.rs"));
@@ -181,7 +179,9 @@ impl GitBare {
let mut resolved: HashMap<String, LangStats> = HashMap::new(); let mut resolved: HashMap<String, LangStats> = HashMap::new();
for (lang, s) in stats { for (lang, s) in stats {
let target = resolve_group(&lang).unwrap_or(&lang); let target = resolve_group(&lang).unwrap_or(&lang);
let entry = resolved.entry(target.to_string()).or_insert_with(|| LangStats { let entry = resolved
.entry(target.to_string())
.or_insert_with(|| LangStats {
lang_type: s.lang_type.clone(), lang_type: s.lang_type.clone(),
..Default::default() ..Default::default()
}); });
@@ -214,7 +214,11 @@ impl GitBare {
}) })
.collect(); .collect();
languages.sort_by(|a, b| b.bytes.cmp(&a.bytes).then_with(|| a.language.cmp(&b.language))); languages.sort_by(|a, b| {
b.bytes
.cmp(&a.bytes)
.then_with(|| a.language.cmp(&b.language))
});
Ok(GetLanguageStatsResponse { Ok(GetLanguageStatsResponse {
languages, languages,
@@ -283,7 +287,10 @@ impl GitBare {
*ctx.total_bytes += size; *ctx.total_bytes += size;
*ctx.total_lines += lines; *ctx.total_lines += lines;
let s = ctx.stats.entry(lang_key.clone()).or_insert_with(|| LangStats { let s = ctx
.stats
.entry(lang_key.clone())
.or_insert_with(|| LangStats {
lang_type: lang_type.to_string(), lang_type: lang_type.to_string(),
..Default::default() ..Default::default()
}); });
+1 -1
View File
@@ -57,7 +57,7 @@ impl GitBare {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let oid = parts[0]; let oid = parts[0];
let found = parts.get(1).map_or(true, |&s| s != "missing"); let found = parts.get(1).is_none_or(|&s| s != "missing");
let size = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); let size = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
sizes.push(ObjectSize { sizes.push(ObjectSize {
oid: oid.to_string(), oid: oid.to_string(),
+6 -6
View File
@@ -19,14 +19,14 @@ impl GitBare {
let stats = self.get_repository_statistics()?; let stats = self.get_repository_statistics()?;
// Run commit-graph write if needed // Run commit-graph write if needed
if stats.commit_graph_size_bytes == 0 || strategy == OptimizeStrategy::Aggressive { if (stats.commit_graph_size_bytes == 0 || strategy == OptimizeStrategy::Aggressive)
if let Ok(resp) = write_commit_graph(self, false, false) { && let Ok(resp) = write_commit_graph(self, false, false)
{
if !resp.ok { if !resp.ok {
stderr_all.push_str(&resp.stderr); stderr_all.push_str(&resp.stderr);
} }
stdout_all.push_str(&resp.stdout); stdout_all.push_str(&resp.stdout);
} }
}
// Repack if many loose objects or packfiles // Repack if many loose objects or packfiles
let repack_needed = stats.loose_object_count > 1000 || stats.packfile_count > 10; let repack_needed = stats.loose_object_count > 1000 || stats.packfile_count > 10;
@@ -42,15 +42,15 @@ impl GitBare {
} }
// Prune if aggressive // Prune if aggressive
if strategy == OptimizeStrategy::Aggressive { if strategy == OptimizeStrategy::Aggressive
if let Ok(resp) = run_gc(self, true, true) { && let Ok(resp) = run_gc(self, true, true)
{
if !resp.ok { if !resp.ok {
stderr_all.push_str(&resp.stderr); stderr_all.push_str(&resp.stderr);
} }
stdout_all.push_str(&resp.stdout); stdout_all.push_str(&resp.stdout);
} }
} }
}
OptimizeStrategy::Incremental => { OptimizeStrategy::Incremental => {
// Just run commit-graph write incrementally // Just run commit-graph write incrementally
if let Ok(resp) = write_commit_graph(self, false, false) { if let Ok(resp) = write_commit_graph(self, false, false) {
+3 -3
View File
@@ -58,8 +58,9 @@ impl GitBare {
// Format: path:line:col:matched_text // Format: path:line:col:matched_text
if let Some((path_and_rest, matched)) = line.rsplit_once(':') { if let Some((path_and_rest, matched)) = line.rsplit_once(':') {
let prefix_parts: Vec<&str> = path_and_rest.rsplitn(3, ':').collect(); let prefix_parts: Vec<&str> = path_and_rest.rsplitn(3, ':').collect();
if prefix_parts.len() >= 3 { if prefix_parts.len() >= 3
if let Ok(line_num) = prefix_parts[0].parse::<u32>() { && let Ok(line_num) = prefix_parts[0].parse::<u32>()
{
results.push(SearchResult { results.push(SearchResult {
path: prefix_parts[2].to_string(), path: prefix_parts[2].to_string(),
line: line_num, line: line_num,
@@ -68,7 +69,6 @@ impl GitBare {
} }
} }
} }
}
Ok(SearchFilesByContentResponse { results }) Ok(SearchFilesByContentResponse { results })
} }
+15 -13
View File
@@ -7,10 +7,20 @@ use crate::error::GitError;
use crate::error::GitResult; use crate::error::GitResult;
/// Characters that are never allowed in git ref names / revision strings. /// Characters that are never allowed in git ref names / revision strings.
///
/// Git disallows: space, `~`, `^`, `:`, `?`, `*`, `[`, `\`, and all ASCII
/// control characters (bytes 031 and 127). The control characters are
/// checked separately via `is_ascii_control()`.
const FORBIDDEN_REF_CHARS: &[char] = &[ const FORBIDDEN_REF_CHARS: &[char] = &[
'~', '^', ':', '?', '*', '[', '\\', ' ', '\n', '\r', '\t', '\0', '~', '^', ':', '?', '*', '[', '\\', ' ',
]; ];
/// Returns true if `c` is an ASCII control character (bytes 031, 127).
fn is_ascii_control(c: char) -> bool {
let b = c as u32;
b <= 31 || b == 127
}
/// Validate a git reference name (branch, tag, etc.). /// Validate a git reference name (branch, tag, etc.).
/// ///
/// Git ref rules (from `git check-ref-format`): /// Git ref rules (from `git check-ref-format`):
@@ -44,7 +54,7 @@ pub fn validate_ref_name(name: &str) -> GitResult<()> {
"ref name cannot contain '@{{': {name}" "ref name cannot contain '@{{': {name}"
))); )));
} }
if name.contains(|c: char| FORBIDDEN_REF_CHARS.contains(&c)) { if name.contains(|c: char| FORBIDDEN_REF_CHARS.contains(&c) || is_ascii_control(c)) {
return Err(GitError::InvalidArgument(format!( return Err(GitError::InvalidArgument(format!(
"ref name contains forbidden character: {name}" "ref name contains forbidden character: {name}"
))); )));
@@ -267,13 +277,7 @@ pub fn validate_config_key(key: &str) -> GitResult<()> {
} }
/// Allowed URL schemes for git remotes. /// Allowed URL schemes for git remotes.
const ALLOWED_REMOTE_SCHEMES: &[&str] = &[ const ALLOWED_REMOTE_SCHEMES: &[&str] = &["http://", "https://", "ssh://", "git://", "git+ssh://"];
"http://",
"https://",
"ssh://",
"git://",
"git+ssh://",
];
/// Validate a remote URL for git operations. /// Validate a remote URL for git operations.
/// ///
@@ -309,16 +313,14 @@ pub fn validate_remote_url(url: &str) -> GitResult<()> {
/// Refspecs must not contain null bytes, newlines, or shell metacharacters. /// Refspecs must not contain null bytes, newlines, or shell metacharacters.
pub fn validate_refspec(refspec: &str) -> GitResult<()> { pub fn validate_refspec(refspec: &str) -> GitResult<()> {
if refspec.is_empty() { if refspec.is_empty() {
return Err(GitError::InvalidArgument( return Err(GitError::InvalidArgument("refspec cannot be empty".into()));
"refspec cannot be empty".into(),
));
} }
if refspec.contains('\0') || refspec.contains('\n') || refspec.contains('\r') { if refspec.contains('\0') || refspec.contains('\n') || refspec.contains('\r') {
return Err(GitError::InvalidArgument( return Err(GitError::InvalidArgument(
"refspec contains invalid characters".into(), "refspec contains invalid characters".into(),
)); ));
} }
if refspec.contains(|c: char| matches!(c, '$' | '`' | '(' | ')' | '{' | '}' | '|' | ';' | '&' | '<' | '>')) { if refspec.contains(['$', '`', '(', ')', '{', '}', '|', ';', '&', '<', '>']) {
return Err(GitError::InvalidArgument(format!( return Err(GitError::InvalidArgument(format!(
"refspec contains shell metacharacter: {refspec}" "refspec contains shell metacharacter: {refspec}"
))); )));
+86 -34
View File
@@ -122,12 +122,15 @@ impl GitksService {
pub fn cleanup_route_cache(&self) { pub fn cleanup_route_cache(&self) {
let before = self.route_cache.len(); let before = self.route_cache.len();
self.route_cache.retain(|_key, cached| { self.route_cache
cached.created_at.elapsed() < ROUTE_CACHE_TTL .retain(|_key, cached| cached.created_at.elapsed() < ROUTE_CACHE_TTL);
});
let removed = before - self.route_cache.len(); let removed = before - self.route_cache.len();
if removed > 0 { if removed > 0 {
tracing::debug!(removed, remaining = self.route_cache.len(), "route cache cleaned"); tracing::debug!(
removed,
remaining = self.route_cache.len(),
"route cache cleaned"
);
} }
} }
@@ -388,9 +391,10 @@ impl GitksService {
&self, &self,
command: crate::actor::raft_log::Command, command: crate::actor::raft_log::Command,
) -> Result<(), tonic::Status> { ) -> Result<(), tonic::Status> {
let actor = self.node_actor.as_ref().ok_or_else(|| { let actor = self
tonic::Status::failed_precondition("node actor not initialized") .node_actor
})?; .as_ref()
.ok_or_else(|| tonic::Status::failed_precondition("node actor not initialized"))?;
// Send the command to the actor for Raft processing // Send the command to the actor for Raft processing
let result = ractor::call_t!( let result = ractor::call_t!(
@@ -405,7 +409,9 @@ impl GitksService {
if success { if success {
Ok(()) Ok(())
} else { } else {
Err(tonic::Status::aborted("Raft consensus failed: not leader or timeout")) Err(tonic::Status::aborted(
"Raft consensus failed: not leader or timeout",
))
} }
} }
Err(e) => Err(tonic::Status::internal(format!("Raft write error: {e}"))), Err(e) => Err(tonic::Status::internal(format!("Raft write error: {e}"))),
@@ -415,20 +421,16 @@ impl GitksService {
/// Perform a ReadIndex check to ensure this node can serve consistent reads. /// Perform a ReadIndex check to ensure this node can serve consistent reads.
/// This confirms the Leader is still valid before reading from local state. /// This confirms the Leader is still valid before reading from local state.
pub async fn raft_read_index(&self) -> Result<(), tonic::Status> { pub async fn raft_read_index(&self) -> Result<(), tonic::Status> {
let actor = self.node_actor.as_ref().ok_or_else(|| { let actor = self
tonic::Status::failed_precondition("node actor not initialized") .node_actor
})?; .as_ref()
.ok_or_else(|| tonic::Status::failed_precondition("node actor not initialized"))?;
let request = crate::actor::message::ReadIndexRequest { let request = crate::actor::message::ReadIndexRequest {
relative_path: String::new(), relative_path: String::new(),
}; };
let result = ractor::call_t!( let result = ractor::call_t!(actor, GitNodeMessage::ReadIndex, 5000, request);
actor,
GitNodeMessage::ReadIndex,
5000,
request
);
match result { match result {
Ok(response) => { Ok(response) => {
@@ -436,7 +438,7 @@ impl GitksService {
Ok(()) Ok(())
} else { } else {
Err(tonic::Status::failed_precondition( Err(tonic::Status::failed_precondition(
"not leader, cannot serve consistent read" "not leader, cannot serve consistent read",
)) ))
} }
} }
@@ -649,23 +651,73 @@ pub async fn serve(
let span = tracing::info_span!("gitks.server", %addr); let span = tracing::info_span!("gitks.server", %addr);
let _enter = span.enter(); let _enter = span.enter();
tracing::info!("registering gRPC services"); tracing::info!("registering gRPC services");
let (health_reporter, health_service) = tonic_health::server::health_reporter();
let repo_svc = repository_service_server::RepositoryServiceServer::new(svc.clone());
let archive_svc = archive_service_server::ArchiveServiceServer::new(svc.clone());
let blame_svc = blame_service_server::BlameServiceServer::new(svc.clone());
let branch_svc = branch_service_server::BranchServiceServer::new(svc.clone());
let commit_svc = commit_service_server::CommitServiceServer::new(svc.clone());
let diff_svc = diff_service_server::DiffServiceServer::new(svc.clone());
let merge_svc = merge_service_server::MergeServiceServer::new(svc.clone());
let pack_svc = pack_service_server::PackServiceServer::new(svc.clone());
let ref_svc = ref_service_server::RefServiceServer::new(svc.clone());
let remote_svc = remote_service_server::RemoteServiceServer::new(svc.clone());
let tag_svc = tag_service_server::TagServiceServer::new(svc.clone());
let tree_svc = tree_service_server::TreeServiceServer::new(svc);
health_reporter
.set_serving::<repository_service_server::RepositoryServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<archive_service_server::ArchiveServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<blame_service_server::BlameServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<branch_service_server::BranchServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<commit_service_server::CommitServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<diff_service_server::DiffServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<merge_service_server::MergeServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<pack_service_server::PackServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<ref_service_server::RefServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<remote_service_server::RemoteServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<tag_service_server::TagServiceServer<GitksService>>()
.await;
health_reporter
.set_serving::<tree_service_server::TreeServiceServer<GitksService>>()
.await;
let server = tonic::transport::Server::builder() let server = tonic::transport::Server::builder()
.add_service(repository_service_server::RepositoryServiceServer::new( .add_service(health_service)
svc.clone(), .add_service(repo_svc)
)) .add_service(archive_svc)
.add_service(archive_service_server::ArchiveServiceServer::new( .add_service(blame_svc)
svc.clone(), .add_service(branch_svc)
)) .add_service(commit_svc)
.add_service(blame_service_server::BlameServiceServer::new(svc.clone())) .add_service(diff_svc)
.add_service(branch_service_server::BranchServiceServer::new(svc.clone())) .add_service(merge_svc)
.add_service(commit_service_server::CommitServiceServer::new(svc.clone())) .add_service(pack_svc)
.add_service(diff_service_server::DiffServiceServer::new(svc.clone())) .add_service(ref_svc)
.add_service(merge_service_server::MergeServiceServer::new(svc.clone())) .add_service(remote_svc)
.add_service(pack_service_server::PackServiceServer::new(svc.clone())) .add_service(tag_svc)
.add_service(ref_service_server::RefServiceServer::new(svc.clone())) .add_service(tree_svc);
.add_service(remote_service_server::RemoteServiceServer::new(svc.clone()))
.add_service(tag_service_server::TagServiceServer::new(svc.clone()))
.add_service(tree_service_server::TreeServiceServer::new(svc));
tracing::info!("server ready, starting to accept connections"); tracing::info!("server ready, starting to accept connections");
server.serve(addr).await server.serve(addr).await
} }
+1 -1
View File
@@ -1,9 +1,9 @@
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use crate::pack::CancellableReceiverStream;
use crate::pb::pack_service_client::PackServiceClient; use crate::pb::pack_service_client::PackServiceClient;
use crate::pb::*; use crate::pb::*;
use crate::pack::CancellableReceiverStream;
use super::{GitksService, into_status}; use super::{GitksService, into_status};
+9 -3
View File
@@ -12,11 +12,16 @@ impl RemoteService for GitksService {
) -> Result<tonic::Response<FindRemoteRepositoryResponse>, tonic::Status> { ) -> Result<tonic::Response<FindRemoteRepositoryResponse>, tonic::Status> {
let m = crate::metrics::RequestMetrics::new("gitks.RemoteService/FindRemoteRepository"); let m = crate::metrics::RequestMetrics::new("gitks.RemoteService/FindRemoteRepository");
let inner = request.into_inner(); let inner = request.into_inner();
let span = tracing::info_span!("remote.find_remote_repository", remote_url = %inner.remote_url); let span =
tracing::info_span!("remote.find_remote_repository", remote_url = %inner.remote_url);
let _enter = span.enter(); let _enter = span.enter();
tracing::info!(remote_url = %inner.remote_url, "find_remote_repository"); tracing::info!(remote_url = %inner.remote_url, "find_remote_repository");
let resp = find_remote_repository(inner).map_err(super::into_status)?; let resp = find_remote_repository(inner).map_err(super::into_status)?;
tracing::info!(refs_count = resp.refs.len(), exists = resp.exists, "find_remote_repository done"); tracing::info!(
refs_count = resp.refs.len(),
exists = resp.exists,
"find_remote_repository done"
);
m.record("ok"); m.record("ok");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -27,7 +32,8 @@ impl RemoteService for GitksService {
) -> Result<tonic::Response<FindRemoteRootRefResponse>, tonic::Status> { ) -> Result<tonic::Response<FindRemoteRootRefResponse>, tonic::Status> {
let m = crate::metrics::RequestMetrics::new("gitks.RemoteService/FindRemoteRootRef"); let m = crate::metrics::RequestMetrics::new("gitks.RemoteService/FindRemoteRootRef");
let inner = request.into_inner(); let inner = request.into_inner();
let span = tracing::info_span!("remote.find_remote_root_ref", remote_url = %inner.remote_url); let span =
tracing::info_span!("remote.find_remote_root_ref", remote_url = %inner.remote_url);
let _enter = span.enter(); let _enter = span.enter();
tracing::info!(remote_url = %inner.remote_url, "find_remote_root_ref"); tracing::info!(remote_url = %inner.remote_url, "find_remote_root_ref");
let resp = find_remote_root_ref(inner).map_err(super::into_status)?; let resp = find_remote_root_ref(inner).map_err(super::into_status)?;
+10 -6
View File
@@ -7,7 +7,11 @@ use gitks::repository::lang_stats::{EXTENSION_MAP, FILENAME_MAP};
fn test_extension_map_lookup() { fn test_extension_map_lookup() {
// Verify .md is in the map // Verify .md is in the map
let result = EXTENSION_MAP.binary_search_by(|&(e, _, _)| e.cmp(".md")); let result = EXTENSION_MAP.binary_search_by(|&(e, _, _)| e.cmp(".md"));
assert!(result.is_ok(), ".md should be in EXTENSION_MAP, got {:?}", result); assert!(
result.is_ok(),
".md should be in EXTENSION_MAP, got {:?}",
result
);
let idx = result.unwrap(); let idx = result.unwrap();
assert_eq!(EXTENSION_MAP[idx].1, "Markdown"); assert_eq!(EXTENSION_MAP[idx].1, "Markdown");
assert_eq!(EXTENSION_MAP[idx].2, "prose"); assert_eq!(EXTENSION_MAP[idx].2, "prose");
@@ -122,10 +126,7 @@ fn test_language_stats_with_path() {
// Should NOT find README.md (it's at root level) // Should NOT find README.md (it's at root level)
let md = resp.languages.iter().find(|l| l.language == "Markdown"); let md = resp.languages.iter().find(|l| l.language == "Markdown");
assert!( assert!(md.is_none(), "should not find Markdown in src/ directory");
md.is_none(),
"should not find Markdown in src/ directory"
);
} }
#[test] #[test]
@@ -145,6 +146,9 @@ fn test_language_stats_line_count_excludes_blank_lines() {
let md = resp.languages.iter().find(|l| l.language == "Markdown"); let md = resp.languages.iter().find(|l| l.language == "Markdown");
if let Some(md) = md { if let Some(md) = md {
// README.md: "# Test" and "Updated." are non-blank = 2 lines // README.md: "# Test" and "Updated." are non-blank = 2 lines
assert!(md.lines >= 2, "should count at least 2 code lines for README.md"); assert!(
md.lines >= 2,
"should count at least 2 code lines for README.md"
);
} }
} }