Skip to content
115 changes: 81 additions & 34 deletions src/bin/git-prolly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -99,11 +102,8 @@ enum Commands {
limit: Option<usize>,
},

/// Create a new branch
Branch {
#[arg(help = "Branch name")]
name: String,
},
/// List all branches
Branch,

/// Switch to a branch or commit
Checkout {
Expand Down Expand Up @@ -148,8 +148,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Commands::Delete { key } => {
handle_delete(key)?;
}
Commands::List { values } => {
handle_list(values)?;
Commands::List { values, graph } => {
handle_list(values, graph)?;
}
Commands::Status => {
handle_status()?;
Expand All @@ -175,8 +175,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} => {
handle_log(kv_summary, keys, limit)?;
}
Commands::Branch { name } => {
handle_branch(name)?;
Commands::Branch => {
handle_branch()?;
}
Commands::Checkout { target } => {
handle_checkout(target)?;
Expand Down Expand Up @@ -257,9 +257,15 @@ fn handle_delete(key: String) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

fn handle_list(show_values: bool) -> Result<(), Box<dyn std::error::Error>> {
fn handle_list(show_values: bool, show_graph: bool) -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let store = VersionedKvStore::<32>::open(&current_dir)?;
let mut store = VersionedKvStore::<32>::open(&current_dir)?;

if show_graph {
// Show the prolly tree structure
store.tree_mut().print();
return Ok(());
}

let keys = store.list_keys();

Expand Down Expand Up @@ -294,9 +300,12 @@ fn handle_status() -> Result<(), Box<dyn std::error::Error>> {
let store = VersionedKvStore::<32>::open(&current_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(());
}

Expand Down Expand Up @@ -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
Expand All @@ -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<dyn std::error::Error>> {
fn handle_branch() -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let mut store = VersionedKvStore::<32>::open(&current_dir)?;
let store = VersionedKvStore::<32>::open(&current_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(())
}
Expand Down Expand Up @@ -645,6 +680,18 @@ fn handle_stats(commit: Option<String>) -> Result<(), Box<dyn std::error::Error>
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());
Expand Down
97 changes: 73 additions & 24 deletions src/git/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,39 @@ impl<const N: usize> GitOperations<N> {
// 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<gix::ObjectId> = vec![];
// Get parent commits
let parent_ids: Vec<gix::ObjectId> = 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)?;
Expand Down Expand Up @@ -302,10 +320,27 @@ impl<const N: usize> GitOperations<N> {
/// Get KV state at a specific commit
fn get_kv_state_at_commit(
&self,
_commit_id: &gix::ObjectId,
commit_id: &gix::ObjectId,
) -> Result<HashMap<Vec<u8>, Vec<u8>>, 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())
}

Expand All @@ -320,8 +355,27 @@ impl<const N: usize> GitOperations<N> {

/// Get current KV state
fn get_current_kv_state(&self) -> Result<HashMap<Vec<u8>, Vec<u8>>, 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<N>,
) -> Result<HashMap<Vec<u8>, Vec<u8>>, 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
Expand Down Expand Up @@ -374,17 +428,12 @@ impl<const N: usize> GitOperations<N> {

/// Parse a commit ID from a string
fn parse_commit_id(&self, commit: &str) -> Result<gix::ObjectId, GitKvError> {
// 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}"
))),
}
}
}
Expand Down
Loading