Skip to main content
Glama
8b-is
by 8b-is
git_temporal.rs14.8 kB
//! Git-based temporal timeline builder for MEM8 //! Extracts project history directly from git to create wave memories use crate::mem8::{ integration::{ContentType, DirectoryHealth, DirectoryMetadata}, wave::{FrequencyBand, MemoryWave}, SmartTreeMem8, }; use anyhow::{Context, Result}; use chrono::{DateTime, Datelike, Utc}; use std::path::Path; use std::process::Command; /// Git commit information #[derive(Debug, Clone)] pub struct GitCommit { pub hash: String, pub author: String, pub timestamp: DateTime<Utc>, pub message: String, pub files_changed: Vec<String>, pub additions: usize, pub deletions: usize, } /// Git file history #[derive(Debug)] pub struct GitFileHistory { pub path: String, pub commits: Vec<GitCommit>, pub total_changes: usize, pub authors: Vec<String>, pub first_seen: DateTime<Utc>, pub last_modified: DateTime<Utc>, } /// Git-based temporal analyzer for MEM8 pub struct GitTemporalAnalyzer { repo_path: String, } impl GitTemporalAnalyzer { pub fn new(repo_path: impl AsRef<Path>) -> Result<Self> { let repo_path = repo_path.as_ref().to_string_lossy().to_string(); // Verify it's a git repository Command::new("git") .arg("-C") .arg(&repo_path) .arg("rev-parse") .arg("--git-dir") .output() .context("Failed to verify git repository")?; Ok(Self { repo_path }) } /// Get complete project timeline pub fn get_project_timeline(&self) -> Result<Vec<GitCommit>> { let output = Command::new("git") .arg("-C") .arg(&self.repo_path) .arg("log") .arg("--pretty=format:%H|%an|%at|%s") .arg("--numstat") .arg("--no-merges") .output() .context("Failed to get git log")?; let stdout = String::from_utf8_lossy(&output.stdout); self.parse_git_log(&stdout) } /// Get file-specific history pub fn get_file_history(&self, file_path: &str) -> Result<GitFileHistory> { // Get commits that touched this file let output = Command::new("git") .arg("-C") .arg(&self.repo_path) .arg("log") .arg("--follow") .arg("--pretty=format:%H|%an|%at|%s") .arg("--") .arg(file_path) .output() .context("Failed to get file history")?; let commits = self.parse_simple_log(&String::from_utf8_lossy(&output.stdout))?; // Get unique authors let mut authors: Vec<String> = commits.iter().map(|c| c.author.clone()).collect(); authors.sort(); authors.dedup(); Ok(GitFileHistory { path: file_path.to_string(), total_changes: commits.len(), first_seen: commits.last().map(|c| c.timestamp).unwrap_or_else(Utc::now), last_modified: commits .first() .map(|c| c.timestamp) .unwrap_or_else(Utc::now), authors, commits, }) } /// Get activity heatmap (commits per day/week) pub fn get_activity_heatmap(&self, days: usize) -> Result<Vec<(DateTime<Utc>, usize)>> { let output = Command::new("git") .arg("-C") .arg(&self.repo_path) .arg("log") .arg(format!("--since={} days ago", days)) .arg("--pretty=format:%at") .output() .context("Failed to get activity data")?; let stdout = String::from_utf8_lossy(&output.stdout); let mut daily_commits = std::collections::HashMap::new(); for line in stdout.lines() { if let Ok(timestamp) = line.parse::<i64>() { let date = DateTime::<Utc>::from_timestamp(timestamp, 0).unwrap_or_else(Utc::now); let day = date.date_naive(); *daily_commits.entry(day).or_insert(0) += 1; } } let mut heatmap: Vec<_> = daily_commits .into_iter() .map(|(date, count)| { let datetime = date .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap(); (datetime, count) }) .collect(); heatmap.sort_by_key(|(date, _)| *date); Ok(heatmap) } /// Analyze code churn (files that change frequently) pub fn analyze_code_churn(&self, limit: usize) -> Result<Vec<(String, usize)>> { let output = Command::new("git") .arg("-C") .arg(&self.repo_path) .arg("log") .arg("--name-only") .arg("--pretty=format:") .arg("--no-merges") .output() .context("Failed to analyze code churn")?; let stdout = String::from_utf8_lossy(&output.stdout); let mut file_changes = std::collections::HashMap::new(); for line in stdout.lines() { if !line.is_empty() && !line.starts_with(' ') { *file_changes.entry(line.to_string()).or_insert(0) += 1; } } let mut churn: Vec<_> = file_changes.into_iter().collect(); churn.sort_by_key(|(_, count)| std::cmp::Reverse(*count)); churn.truncate(limit); Ok(churn) } /// Parse git log output fn parse_git_log(&self, output: &str) -> Result<Vec<GitCommit>> { let mut commits = Vec::new(); let mut current_commit: Option<GitCommit> = None; for line in output.lines() { if line.contains('|') && !line.starts_with(char::is_numeric) { // Commit header line if let Some(commit) = current_commit.take() { commits.push(commit); } let parts: Vec<&str> = line.split('|').collect(); if parts.len() >= 4 { let timestamp = parts[2].parse::<i64>().unwrap_or(0); current_commit = Some(GitCommit { hash: parts[0].to_string(), author: parts[1].to_string(), timestamp: DateTime::<Utc>::from_timestamp(timestamp, 0) .unwrap_or_else(Utc::now), message: parts[3..].join("|"), files_changed: Vec::new(), additions: 0, deletions: 0, }); } } else if let Some(ref mut commit) = current_commit { // File change line (numstat format) let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 3 { if let (Ok(adds), Ok(dels)) = (parts[0].parse::<usize>(), parts[1].parse::<usize>()) { commit.additions += adds; commit.deletions += dels; commit.files_changed.push(parts[2].to_string()); } } } } if let Some(commit) = current_commit { commits.push(commit); } Ok(commits) } /// Parse simple log output (without numstat) fn parse_simple_log(&self, output: &str) -> Result<Vec<GitCommit>> { let mut commits = Vec::new(); for line in output.lines() { let parts: Vec<&str> = line.split('|').collect(); if parts.len() >= 4 { let timestamp = parts[2].parse::<i64>().unwrap_or(0); commits.push(GitCommit { hash: parts[0].to_string(), author: parts[1].to_string(), timestamp: DateTime::<Utc>::from_timestamp(timestamp, 0) .unwrap_or_else(Utc::now), message: parts[3..].join("|"), files_changed: Vec::new(), additions: 0, deletions: 0, }); } } Ok(commits) } } /// Extension trait to integrate Git temporal data with MEM8 impl SmartTreeMem8 { /// Import git history as wave memories pub fn import_git_timeline(&mut self, repo_path: impl AsRef<Path>) -> Result<()> { let analyzer = GitTemporalAnalyzer::new(repo_path)?; // Get project timeline let timeline = analyzer.get_project_timeline()?; println!("Importing {} commits into wave memory...", timeline.len()); // Get activity heatmap for the last 90 days let heatmap = analyzer.get_activity_heatmap(90)?; let _max_daily_commits = heatmap.iter().map(|(_, count)| *count).max().unwrap_or(1) as f32; // Import each commit as a memory wave for (idx, commit) in timeline.iter().enumerate() { let days_ago = (Utc::now() - commit.timestamp).num_days() as f32; // Determine frequency based on commit characteristics let frequency = if commit.message.contains("fix") || commit.message.contains("bug") { FrequencyBand::Technical.frequency(0.7) // Bug fixes are technical } else if commit.message.contains("doc") || commit.message.contains("README") { FrequencyBand::Conversational.frequency(0.5) // Documentation } else if commit.additions > 500 || commit.deletions > 500 { FrequencyBand::Implementation.frequency(0.8) // Major changes } else if commit.files_changed.len() > 10 { FrequencyBand::DeepStructural.frequency(0.6) // Structural refactoring } else { FrequencyBand::Technical.frequency(0.5) // Default }; // Amplitude based on change size and recency let change_factor = ((commit.additions + commit.deletions) as f32).log10() / 4.0; let recency_factor = (-days_ago / 30.0).exp(); // Decay over 30 days let amplitude = (change_factor * recency_factor).clamp(0.1, 1.0); // Create memory wave let mut wave = MemoryWave::new(frequency, amplitude); // Emotional context based on commit patterns wave.valence = if commit.message.contains("fix") || commit.message.contains("bug") { -0.2 // Negative for bug fixes } else if commit.message.contains("feat") || commit.message.contains("add") { 0.6 // Positive for new features } else { 0.2 // Neutral positive }; // Arousal based on change magnitude wave.arousal = change_factor.clamp(0.1, 1.0); // Store in temporal layer based on age let z_layer = (idx as f32 / timeline.len() as f32 * 65535.0) as u16; // Use author name for spatial distribution let (x, y) = self.string_to_coordinates(&format!("{}-{}", commit.author, idx)); self.store_wave_at_coordinates(x, y, z_layer, wave)?; } // Import code churn patterns let churn = analyzer.analyze_code_churn(20)?; for (file_path, change_count) in churn { let metadata = DirectoryMetadata { primary_type: self.detect_content_type(&file_path), importance: (change_count as f32 / 100.0).clamp(0.1, 1.0), normalized_size: 0.5, // Unknown from git health: if change_count > 50 { DirectoryHealth::Warning // High churn might indicate instability } else { DirectoryHealth::Healthy }, activity_level: (change_count as f32 / 20.0).clamp(0.1, 1.0), days_since_modified: 0, // Will be overridden by actual file check }; self.store_directory_memory(Path::new(&file_path), metadata)?; } println!("Git timeline imported successfully!"); Ok(()) } /// Helper to detect content type from path fn detect_content_type(&self, path: &str) -> ContentType { if path.ends_with(".rs") || path.ends_with(".py") || path.ends_with(".js") { ContentType::Code } else if path.ends_with(".md") || path.contains("README") { ContentType::Documentation } else if path.ends_with(".toml") || path.ends_with(".json") || path.ends_with(".yaml") { ContentType::Configuration } else if path.contains("test") || path.contains("spec") { ContentType::Code // Tests are code } else { ContentType::Data } } } /// Create temporal "grooves" in wave space from git patterns pub fn create_temporal_grooves( mem8: &mut SmartTreeMem8, repo_path: impl AsRef<Path>, ) -> Result<()> { let analyzer = GitTemporalAnalyzer::new(&repo_path)?; // Get activity patterns let heatmap = analyzer.get_activity_heatmap(365)?; // Last year // Find periodic patterns (e.g., weekly sprints, monthly releases) let mut weekly_pattern = [0f32; 7]; for (date, count) in &heatmap { let weekday = date.weekday().num_days_from_monday() as usize; weekly_pattern[weekday] += *count as f32; } // Normalize weekly pattern let max_weekly = weekly_pattern.iter().copied().fold(0.0f32, f32::max); if max_weekly > 0.0 { for val in &mut weekly_pattern { *val /= max_weekly; } } // Create persistent wave patterns for discovered rhythms for (day, &intensity) in weekly_pattern.iter().enumerate() { if intensity > 0.2 { let mut wave = MemoryWave::new( FrequencyBand::Technical.frequency(intensity), // Use Technical for temporal patterns intensity * 0.5, ); wave.decay_tau = None; // Persistent pattern wave.valence = 0.3; // Slightly positive // Store in a "rhythm" layer let x = (day * 36) as u8; // Spread across x-axis let y = 128; // Middle of y-axis let z = 60000; // High z-layer for persistent patterns mem8.store_wave_at_coordinates(x, y, z, wave)?; } } println!("Temporal grooves created from git patterns!"); Ok(()) } #[cfg(test)] mod tests { use super::*; use std::env; #[test] fn test_git_timeline_import() { if env::var("CI").is_err() { // Only run locally, not in CI let mut mem8 = SmartTreeMem8::new(); if let Ok(()) = mem8.import_git_timeline(".") { assert!(mem8.active_memory_count() > 0); } } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server