diff --git a/src/bin/git-prolly.rs b/src/bin/git-prolly.rs index aa50b0a..af51cf6 100644 --- a/src/bin/git-prolly.rs +++ b/src/bin/git-prolly.rs @@ -14,13 +14,14 @@ limitations under the License. use clap::{Parser, Subcommand}; use prollytree::git::{DiffOperation, GitOperations, MergeResult, VersionedKvStore}; +use prollytree::tree::Tree; use std::env; use std::path::PathBuf; #[derive(Parser)] #[command(name = "git-prolly")] #[command(about = "KV-aware Git operations for ProllyTree")] -#[command(version = "1.0.0")] +#[command(version = "0.2.0")] struct Cli { #[command(subcommand)] command: Commands, @@ -58,6 +59,8 @@ enum Commands { List { #[arg(long, help = "Show values as well")] values: bool, + #[arg(long, help = "Show prolly tree structure")] + graph: bool, }, /// Show staging area status @@ -99,11 +102,8 @@ enum Commands { limit: Option, }, - /// Create a new branch - Branch { - #[arg(help = "Branch name")] - name: String, - }, + /// List all branches + Branch, /// Switch to a branch or commit Checkout { @@ -148,8 +148,8 @@ fn main() -> Result<(), Box> { Commands::Delete { key } => { handle_delete(key)?; } - Commands::List { values } => { - handle_list(values)?; + Commands::List { values, graph } => { + handle_list(values, graph)?; } Commands::Status => { handle_status()?; @@ -175,8 +175,8 @@ fn main() -> Result<(), Box> { } => { handle_log(kv_summary, keys, limit)?; } - Commands::Branch { name } => { - handle_branch(name)?; + Commands::Branch => { + handle_branch()?; } Commands::Checkout { target } => { handle_checkout(target)?; @@ -257,9 +257,15 @@ fn handle_delete(key: String) -> Result<(), Box> { Ok(()) } -fn handle_list(show_values: bool) -> Result<(), Box> { +fn handle_list(show_values: bool, show_graph: bool) -> Result<(), Box> { let current_dir = env::current_dir()?; - let store = VersionedKvStore::<32>::open(¤t_dir)?; + let mut store = VersionedKvStore::<32>::open(¤t_dir)?; + + if show_graph { + // Show the prolly tree structure + store.tree_mut().print(); + return Ok(()); + } let keys = store.list_keys(); @@ -294,9 +300,12 @@ fn handle_status() -> Result<(), Box> { let store = VersionedKvStore::<32>::open(¤t_dir)?; let status = store.status(); + let current_branch = store.current_branch(); + + println!("On branch {current_branch}"); if status.is_empty() { - println!("No staged changes"); + println!("nothing to commit, working tree clean"); return Ok(()); } @@ -522,10 +531,24 @@ fn handle_log( history.truncate(limit); } - for commit in history { - let date = chrono::DateTime::from_timestamp(commit.timestamp, 0) - .unwrap_or_default() - .format("%Y-%m-%d %H:%M:%S"); + // Check current branch for the first commit (HEAD) + let current_branch = store.current_branch(); + let head_commit_id = store.git_repo().head_id().ok(); + + for (index, commit) in history.iter().enumerate() { + let date = chrono::DateTime::from_timestamp(commit.timestamp, 0).unwrap_or_default(); + + // Format like git log: "Wed Jul 16 22:27:36 2025 -0700" + let formatted_date = date.format("%a %b %d %H:%M:%S %Y %z"); + + // Add branch reference for HEAD commit + let branch_ref = if index == 0 + && head_commit_id.as_ref().map(|id| id.as_ref()) == Some(commit.id.as_ref()) + { + format!(" (HEAD -> {current_branch})") + } else { + String::new() + }; if kv_summary { // Get changes for this commit - create a new store instance @@ -549,35 +572,47 @@ fn handle_log( .filter(|c| matches!(c.operation, DiffOperation::Modified { .. })) .count(); + println!("commit {}{}", commit.id, branch_ref); + println!("Author: {}", commit.author); + println!("Date: {formatted_date}"); + println!(); println!( - "{} - {} - {} (+{} ~{} -{})", - &commit.id.to_string()[..8], - date, - commit.message, - added, - modified, - removed + " {} (+{} ~{} -{})", + commit.message, added, modified, removed ); + println!(); } else { - println!( - "{} - {} - {}", - &commit.id.to_string()[..8], - date, - commit.message - ); + println!("commit {}{}", commit.id, branch_ref); + println!("Author: {}", commit.author); + println!("Date: {formatted_date}"); + println!(); + println!(" {}", commit.message); + println!(); } } Ok(()) } -fn handle_branch(name: String) -> Result<(), Box> { +fn handle_branch() -> Result<(), Box> { let current_dir = env::current_dir()?; - let mut store = VersionedKvStore::<32>::open(¤t_dir)?; + let store = VersionedKvStore::<32>::open(¤t_dir)?; + + let branches = store.list_branches()?; + let current_branch = store.current_branch(); - store.branch(&name)?; + if branches.is_empty() { + println!("No branches found"); + return Ok(()); + } - println!("✓ Created branch: {name}"); + for branch in branches { + if branch == current_branch { + println!("* {branch}"); + } else { + println!(" {branch}"); + } + } Ok(()) } @@ -645,6 +680,18 @@ fn handle_stats(commit: Option) -> Result<(), Box println!("ProllyTree Statistics for {target}:"); println!("═══════════════════════════════════"); + // Get dataset path (name) + let dataset_path = current_dir.display().to_string(); + let dataset_name = current_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("unknown"); + println!("Dataset: {dataset_name} ({dataset_path})"); + + // Get prolly tree depth + let tree_depth = store.tree().depth(); + println!("Tree Depth: {tree_depth}"); + // Get basic stats let keys = store.list_keys(); println!("Total Keys: {}", keys.len()); diff --git a/src/git/operations.rs b/src/git/operations.rs index d182263..0936514 100644 --- a/src/git/operations.rs +++ b/src/git/operations.rs @@ -111,21 +111,39 @@ impl GitOperations { // Parse commit ID let commit_id = self.parse_commit_id(commit)?; - // Get commit info (simplified) + // Get commit object from git + let mut buffer = Vec::new(); + let commit_obj = self + .store + .git_repo() + .objects + .find(&commit_id, &mut buffer) + .map_err(|e| GitKvError::GitObjectError(format!("Commit not found: {e}")))?; + + let commit = match commit_obj.decode() { + Ok(gix::objs::ObjectRef::Commit(commit)) => commit, + _ => { + return Err(GitKvError::GitObjectError( + "Object is not a commit".to_string(), + )) + } + }; + + // Extract commit info let info = CommitInfo { id: commit_id, - author: "Unknown".to_string(), - committer: "Unknown".to_string(), - message: "Commit".to_string(), - timestamp: 0, + author: commit.author().name.to_string(), + committer: commit.committer().name.to_string(), + message: commit.message().title.to_string(), + timestamp: commit.time().seconds, }; - // Get parent commits (simplified) - let parent_ids: Vec = vec![]; + // Get parent commits + let parent_ids: Vec = commit.parents().collect(); // Generate diff from parent (if exists) let changes = if let Some(parent_id) = parent_ids.first() { - self.diff(&parent_id.to_string(), commit)? + self.diff(&parent_id.to_string(), &commit_id.to_string())? } else { // Root commit - show all keys as added let state = self.get_kv_state_at_commit(&commit_id)?; @@ -302,10 +320,27 @@ impl GitOperations { /// Get KV state at a specific commit fn get_kv_state_at_commit( &self, - _commit_id: &gix::ObjectId, + commit_id: &gix::ObjectId, ) -> Result, Vec>, GitKvError> { - // This is a simplified implementation - // In reality, we'd need to reconstruct the ProllyTree from the Git objects + // Check if we're asking for the current HEAD + let current_head = self + .store + .git_repo() + .head_id() + .map_err(|e| GitKvError::GitObjectError(format!("Failed to get HEAD: {e}")))?; + + if *commit_id == current_head { + // For current HEAD, use the current state + return self.get_current_kv_state(); + } + + // For now, return empty state for non-HEAD commits + // This is a limitation - in a full implementation, we would need to: + // 1. Parse the commit object to get the tree + // 2. Reconstruct the ProllyTree from the Git objects + // 3. Extract key-value pairs from the reconstructed tree + // + // For the purpose of fixing the immediate issue, we'll focus on HEAD commits Ok(HashMap::new()) } @@ -320,8 +355,27 @@ impl GitOperations { /// Get current KV state fn get_current_kv_state(&self) -> Result, Vec>, GitKvError> { - // This would collect all current KV pairs - Ok(HashMap::new()) + self.get_current_kv_state_from_store(&self.store) + } + + /// Get current KV state from a specific store + fn get_current_kv_state_from_store( + &self, + store: &VersionedKvStore, + ) -> Result, Vec>, GitKvError> { + let mut state = HashMap::new(); + + // Get all keys from the store + let keys = store.list_keys(); + + // For each key, get its value + for key in keys { + if let Some(value) = store.get(&key) { + state.insert(key, value); + } + } + + Ok(state) } /// Perform a three-way merge @@ -374,17 +428,12 @@ impl GitOperations { /// Parse a commit ID from a string fn parse_commit_id(&self, commit: &str) -> Result { - // Handle special cases (simplified) - match commit { - "HEAD" => { - // Return a placeholder for HEAD - Ok(gix::ObjectId::null(gix::hash::Kind::Sha1)) - } - _ => { - // Try to parse as hex string - gix::ObjectId::from_hex(commit.as_bytes()) - .map_err(|e| GitKvError::InvalidCommit(format!("Invalid commit ID: {e}"))) - } + // Try to resolve using git's rev-parse functionality + match self.store.git_repo().rev_parse_single(commit) { + Ok(object) => Ok(object.into()), + Err(e) => Err(GitKvError::GitObjectError(format!( + "Cannot resolve commit {commit}: {e}" + ))), } } } diff --git a/src/git/versioned_store.rs b/src/git/versioned_store.rs index 8608352..5de036f 100644 --- a/src/git/versioned_store.rs +++ b/src/git/versioned_store.rs @@ -349,6 +349,42 @@ impl VersionedKvStore { &self.current_branch } + /// Get a reference to the underlying ProllyTree + pub fn tree(&self) -> &ProllyTree> { + &self.tree + } + + /// Get a mutable reference to the underlying ProllyTree + pub fn tree_mut(&mut self) -> &mut ProllyTree> { + &mut self.tree + } + + /// List all branches in the repository + pub fn list_branches(&self) -> Result, GitKvError> { + let mut branches = Vec::new(); + + // Get all refs under refs/heads/ + let refs = self + .git_repo + .refs + .iter() + .map_err(|e| GitKvError::GitObjectError(format!("Failed to iterate refs: {e}")))?; + + for reference in (refs + .all() + .map_err(|e| GitKvError::GitObjectError(format!("Failed to get refs: {e}")))?) + .flatten() + { + if let Some(name) = reference.name.as_bstr().strip_prefix(b"refs/heads/") { + let branch_name = String::from_utf8_lossy(name).to_string(); + branches.push(branch_name); + } + } + + branches.sort(); + Ok(branches) + } + /// Get access to the git repository (for internal use) pub fn git_repo(&self) -> &gix::Repository { &self.git_repo @@ -375,9 +411,16 @@ impl VersionedKvStore { if let Ok(commit_ref) = commit_obj.decode() { let commit_info = CommitInfo { id: commit_obj.id().into(), - author: String::from_utf8_lossy(commit_ref.author.name).to_string(), - committer: String::from_utf8_lossy(commit_ref.committer.name) - .to_string(), + author: format!( + "{} <{}>", + String::from_utf8_lossy(commit_ref.author.name), + String::from_utf8_lossy(commit_ref.author.email) + ), + committer: format!( + "{} <{}>", + String::from_utf8_lossy(commit_ref.committer.name), + String::from_utf8_lossy(commit_ref.committer.email) + ), message: String::from_utf8_lossy(commit_ref.message).to_string(), timestamp: commit_ref.author.time.seconds, };