use heroforge_core::Repository;
use std::path::Path;
use std::process::Command;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let demo_dir = Path::new("/tmp/fossil_full_demo");
let repo_path = demo_dir.join("myproject.forge");
if demo_dir.exists() {
std::fs::remove_dir_all(demo_dir)?;
}
std::fs::create_dir_all(demo_dir)?;
println!("=== Creating Full Demo Repository ===\n");
println!("Repository: {}\n", repo_path.display());
let repo = Repository::init(&repo_path)?;
println!("✓ Repository initialized");
let init_hash = repo
.commit_builder()
.message("initial empty check-in")
.author("admin")
.initial()
.execute()?;
println!("✓ Initial check-in: {}", &init_hash[..12]);
println!("\n--- Phase 1: Project Setup (v0.1.0) ---");
let readme = r#"# MyProject
A demonstration project for heroforge.
## Features
- Feature A
- Feature B
## Installation
Run `cargo install myproject`
"#;
let cargo_toml = r#"[package]
name = "myproject"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let main_rs = r#"fn main() {
println!("MyProject v0.1.0");
}
"#;
let lib_rs = r#"//! MyProject library
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
assert_eq!(greet("World"), "Hello, World!");
}
}
"#;
let v010 = repo
.commit_builder()
.message("Initial project structure")
.author("alice")
.parent(&init_hash)
.file("README.md", readme.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs.as_bytes())
.file("src/lib.rs", lib_rs.as_bytes())
.execute()?;
println!(" Commit: Initial project structure");
repo.tags()
.create("v0.1.0")
.at_commit(&v010)
.author("alice")
.execute()?;
println!(" Tagged: v0.1.0");
println!("\n--- Phase 2: Add Configuration (v0.2.0) ---");
let config_rs = r#"//! Configuration module
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Config {
pub data_dir: PathBuf,
pub verbose: bool,
pub max_connections: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
data_dir: PathBuf::from("./data"),
verbose: false,
max_connections: 10,
}
}
}
impl Config {
pub fn load() -> Self {
// TODO: Load from file
Self::default()
}
}
"#;
let lib_rs_v2 = r#"//! MyProject library
pub mod config;
pub use config::Config;
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
pub fn init() -> Config {
Config::load()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
assert_eq!(greet("World"), "Hello, World!");
}
#[test]
fn test_config_default() {
let config = Config::default();
assert!(!config.verbose);
}
}
"#;
let main_rs_v2 = r#"use myproject::{greet, init};
fn main() {
let config = init();
println!("MyProject v0.2.0");
println!("{}", greet("User"));
println!("Config: {:?}", config);
}
"#;
let v020 = repo
.commit_builder()
.message("Add configuration module")
.author("alice")
.parent(&v010)
.file("README.md", readme.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs_v2.as_bytes())
.file("src/lib.rs", lib_rs_v2.as_bytes())
.file("src/config.rs", config_rs.as_bytes())
.execute()?;
println!(" Commit: Add configuration module");
repo.tags()
.create("v0.2.0")
.at_commit(&v020)
.author("alice")
.execute()?;
println!(" Tagged: v0.2.0");
println!("\n--- Phase 3: Feature Branch (feature/database) ---");
let feature_db = repo
.branches()
.create("feature/database")
.from_commit(&v020)
.author("bob")
.execute()?;
println!(" Branch created: feature/database");
let db_rs = r#"//! Database module
use std::collections::HashMap;
pub struct Database {
data: HashMap<String, String>,
}
impl Database {
pub fn new() -> Self {
Self {
data: HashMap::new(),
}
}
pub fn insert(&mut self, key: &str, value: &str) {
self.data.insert(key.to_string(), value.to_string());
}
pub fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
pub fn delete(&mut self, key: &str) -> Option<String> {
self.data.remove(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_database_crud() {
let mut db = Database::new();
db.insert("key1", "value1");
assert_eq!(db.get("key1"), Some(&"value1".to_string()));
db.delete("key1");
assert_eq!(db.get("key1"), None);
}
}
"#;
let lib_rs_db = r#"//! MyProject library
pub mod config;
pub mod db;
pub use config::Config;
pub use db::Database;
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
pub fn init() -> Config {
Config::load()
}
"#;
let db_commit1 = repo
.commit_builder()
.message("Add database module with CRUD operations")
.author("bob")
.parent(&feature_db)
.branch("feature/database")
.file("README.md", readme.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs_v2.as_bytes())
.file("src/lib.rs", lib_rs_db.as_bytes())
.file("src/config.rs", config_rs.as_bytes())
.file("src/db.rs", db_rs.as_bytes())
.execute()?;
println!(" Commit: Add database module");
let db_rs_v2 = r#"//! Database module with persistence
use std::collections::HashMap;
use std::fs;
use std::path::Path;
pub struct Database {
data: HashMap<String, String>,
path: Option<String>,
}
impl Database {
pub fn new() -> Self {
Self {
data: HashMap::new(),
path: None,
}
}
pub fn open(path: &str) -> std::io::Result<Self> {
let mut db = Self::new();
db.path = Some(path.to_string());
if Path::new(path).exists() {
let content = fs::read_to_string(path)?;
for line in content.lines() {
if let Some((key, value)) = line.split_once('=') {
db.data.insert(key.to_string(), value.to_string());
}
}
}
Ok(db)
}
pub fn save(&self) -> std::io::Result<()> {
if let Some(path) = &self.path {
let content: String = self.data
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("\n");
fs::write(path, content)?;
}
Ok(())
}
pub fn insert(&mut self, key: &str, value: &str) {
self.data.insert(key.to_string(), value.to_string());
}
pub fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
pub fn delete(&mut self, key: &str) -> Option<String> {
self.data.remove(key)
}
}
"#;
let _db_commit2 = repo
.commit_builder()
.message("Add persistence to database module")
.author("bob")
.parent(&db_commit1)
.branch("feature/database")
.file("README.md", readme.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs_v2.as_bytes())
.file("src/lib.rs", lib_rs_db.as_bytes())
.file("src/config.rs", config_rs.as_bytes())
.file("src/db.rs", db_rs_v2.as_bytes())
.execute()?;
println!(" Commit: Add persistence");
println!("\n--- Phase 4: Trunk Development (v0.3.0) ---");
let cli_rs = r#"//! Command-line interface
use std::env;
pub struct Cli {
pub command: String,
pub args: Vec<String>,
}
impl Cli {
pub fn parse() -> Self {
let args: Vec<String> = env::args().collect();
let command = args.get(1).cloned().unwrap_or_else(|| "help".to_string());
let args = args.into_iter().skip(2).collect();
Self { command, args }
}
}
"#;
let main_rs_v3 = r#"mod cli;
use myproject::{greet, init};
use cli::Cli;
fn main() {
let cli = Cli::parse();
let config = init();
match cli.command.as_str() {
"greet" => {
let name = cli.args.first().map(|s| s.as_str()).unwrap_or("World");
println!("{}", greet(name));
}
"config" => {
println!("Config: {:?}", config);
}
_ => {
println!("MyProject v0.3.0");
println!("Commands: greet [name], config");
}
}
}
"#;
let v030 = repo
.commit_builder()
.message("Add CLI interface")
.author("alice")
.parent(&v020)
.file("README.md", readme.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs_v3.as_bytes())
.file("src/lib.rs", lib_rs_v2.as_bytes())
.file("src/config.rs", config_rs.as_bytes())
.file("src/cli.rs", cli_rs.as_bytes())
.execute()?;
println!(" Commit: Add CLI interface");
repo.tags()
.create("v0.3.0")
.at_commit(&v030)
.author("alice")
.execute()?;
println!(" Tagged: v0.3.0");
println!("\n--- Phase 5: Hotfix (hotfix/config-panic) ---");
let hotfix_branch = repo
.branches()
.create("hotfix/config-panic")
.from_commit(&v020)
.author("charlie")
.execute()?;
println!(" Branch created: hotfix/config-panic");
let config_rs_fixed = r#"//! Configuration module (with panic fix)
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Config {
pub data_dir: PathBuf,
pub verbose: bool,
pub max_connections: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
data_dir: PathBuf::from("./data"),
verbose: false,
max_connections: 10,
}
}
}
impl Config {
pub fn load() -> Self {
// Fixed: Handle missing config file gracefully
match std::fs::read_to_string("config.toml") {
Ok(_content) => {
// TODO: Parse TOML
Self::default()
}
Err(_) => Self::default(),
}
}
}
"#;
let hotfix_commit = repo
.commit_builder()
.message("Fix panic when config file is missing")
.author("charlie")
.parent(&hotfix_branch)
.branch("hotfix/config-panic")
.file("README.md", readme.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs_v2.as_bytes())
.file("src/lib.rs", lib_rs_v2.as_bytes())
.file("src/config.rs", config_rs_fixed.as_bytes())
.execute()?;
println!(" Commit: Fix config panic");
repo.tags()
.create("v0.2.1")
.at_commit(&hotfix_commit)
.author("charlie")
.execute()?;
println!(" Tagged: v0.2.1");
println!("\n--- Phase 6: More Features (v0.4.0) ---");
let utils_rs = r#"//! Utility functions
/// Format a size in bytes to human-readable format
pub fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
/// Truncate a string to a maximum length
pub fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_size() {
assert_eq!(format_size(500), "500 B");
assert_eq!(format_size(1024), "1.00 KB");
assert_eq!(format_size(1536), "1.50 KB");
}
#[test]
fn test_truncate() {
assert_eq!(truncate("hello", 10), "hello");
assert_eq!(truncate("hello world", 8), "hello...");
}
}
"#;
let lib_rs_v4 = r#"//! MyProject library
pub mod config;
pub mod utils;
pub use config::Config;
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
pub fn init() -> Config {
Config::load()
}
"#;
let v040 = repo
.commit_builder()
.message("Add utility functions")
.author("alice")
.parent(&v030)
.file("README.md", readme.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs_v3.as_bytes())
.file("src/lib.rs", lib_rs_v4.as_bytes())
.file("src/config.rs", config_rs.as_bytes())
.file("src/cli.rs", cli_rs.as_bytes())
.file("src/utils.rs", utils_rs.as_bytes())
.execute()?;
println!(" Commit: Add utility functions");
repo.tags()
.create("v0.4.0")
.at_commit(&v040)
.author("alice")
.execute()?;
println!(" Tagged: v0.4.0");
let readme_v2 = r#"# MyProject
A demonstration project for heroforge.
## Features
- Configuration management
- CLI interface
- Utility functions
## Installation
```bash
cargo install myproject
```
## Usage
```bash
# Show help
myproject
# Greet someone
myproject greet Alice
# Show configuration
myproject config
```
## Development
```bash
# Run tests
cargo test
# Build release
cargo build --release
```
## License
MIT
"#;
let _docs_commit = repo
.commit_builder()
.message("Update documentation")
.author("alice")
.parent(&v040)
.file("README.md", readme_v2.as_bytes())
.file("Cargo.toml", cargo_toml.as_bytes())
.file("src/main.rs", main_rs_v3.as_bytes())
.file("src/lib.rs", lib_rs_v4.as_bytes())
.file("src/config.rs", config_rs.as_bytes())
.file("src/cli.rs", cli_rs.as_bytes())
.file("src/utils.rs", utils_rs.as_bytes())
.execute()?;
println!(" Commit: Update documentation");
println!("\n=== Repository Summary ===\n");
println!("Branches:");
for branch in repo.branches().list()? {
println!(" - {}", branch);
}
println!("\nTags:");
for tag in repo.tags().list()? {
let hash = repo.tags().get(&tag)?.commit_hash()?;
println!(" - {} -> {}", tag, &hash[..12]);
}
println!("\nRecent Check-ins:");
for ci in repo.history().recent(15)? {
println!(
" [{}] {} - {}",
&ci.hash[..8],
&ci.timestamp[..10],
ci.comment
);
}
println!("\n=== Opening Web UI ===\n");
let _ = Command::new("heroforge")
.args(["rebuild", repo_path.to_str().unwrap()])
.output();
println!("Starting heroforge web UI...");
println!("Repository: {}", repo_path.display());
println!("\nPress Ctrl+C to stop the server.\n");
let status = Command::new("heroforge")
.args(["ui", repo_path.to_str().unwrap()])
.status()?;
if !status.success() {
eprintln!("Failed to start heroforge ui");
}
Ok(())
}