Fix bugs and improve robustness across the codebase

- Fix staged files being silently cleared when commit uses inline files
- Refactor step navigation to use direct go_to_step instead of fragile delta math
- Change step numbers from i32 to u32 (reject negative values at parse time)
- Add tour rm command to mark files for removal during carry-forward
- Add tour reset command to clear session and remove tracked files
- Consolidate duplicate recursive copy functions into shared copy_tree in utils
- Validate step directories are sequential (detect corruption)
- Detect binary files in diffs instead of showing garbage
- Use /// doc comments on enum variants so clap generates proper help text
- Remove custom Help subcommand in favor of clap's built-in --help
- Add CorruptedTour error variant for integrity checks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 21:23:39 +00:00
parent 399a72f380
commit 507c61fe5f
19 changed files with 1513 additions and 268 deletions

View File

@@ -2,11 +2,54 @@ use std::fs;
use std::io;
use std::path::Path;
use crate::error::TourError;
use crate::SESSION_PATH;
use crate::TOUR_DIR;
pub fn require_tour() -> Result<(), TourError> {
if !Path::new(TOUR_DIR).exists() {
return Err(TourError::NoTour);
}
Ok(())
}
pub fn get_current_step() -> Option<u32> {
fs::read_to_string(SESSION_PATH)
.ok()
.and_then(|s| {
s.split("STEP=")
.nth(1)
.and_then(|v| v.trim().parse::<u32>().ok())
})
}
pub fn get_tour_step() -> Result<u32, TourError> {
let steps_dir = Path::new(TOUR_DIR).join("steps");
let mut indices: Vec<u32> = fs::read_dir(&steps_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.filter_map(|e| e.file_name().to_str()?.parse::<u32>().ok())
.collect();
if indices.is_empty() {
return Ok(0);
}
indices.sort();
let count = indices.len() as u32;
for (i, &idx) in indices.iter().enumerate() {
if idx != i as u32 {
return Err(TourError::CorruptedTour(
format!("step directories are not sequential (expected {}, found {})", i, idx),
));
}
}
Ok(count)
}
/// Copies a file or directory into dest_dir, preserving relative path structure.
/// e.g. `src/main.rs` → `dest_dir/src/main.rs`
pub fn copy_path(src: &Path, dest_dir: &Path) -> Result<(), io::Error> {
let relative_src = if src.is_absolute() {
let cwd = std::env::current_dir()?;
@@ -32,6 +75,24 @@ pub fn copy_path(src: &Path, dest_dir: &Path) -> Result<(), io::Error> {
Ok(())
}
/// Recursively copies src to dest. If src is a directory, copies its contents
/// into dest. If src is a file, copies it to dest.
pub fn copy_tree(src: &Path, dest: &Path) -> Result<(), io::Error> {
if src.is_dir() {
fs::create_dir_all(dest)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
copy_tree(&entry.path(), &dest.join(entry.file_name()))?;
}
} else {
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(src, dest)?;
}
Ok(())
}
pub fn is_descendant_of_current_dir(file: &Path) -> Result<bool, io::Error> {
is_file_in_dir(file, &std::env::current_dir()?)
}
@@ -41,23 +102,3 @@ pub fn is_file_in_dir(file: &Path, dir: &Path) -> Result<bool, io::Error> {
let dir_canon = dir.canonicalize()?;
Ok(file_canon.starts_with(&dir_canon))
}
pub fn get_session_step() -> Result<u32, Box<dyn std::error::Error>> {
let session = fs::read_to_string(SESSION_PATH)?;
let step = session
.split("STEP=")
.nth(1)
.ok_or("no STEP in session")?
.trim()
.parse::<u32>()?;
Ok(step)
}
pub fn get_tour_step() -> Result<u32, Box<dyn std::error::Error>> {
let steps_dir = Path::new(TOUR_DIR).join("steps");
let count = fs::read_dir(&steps_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.count() as u32;
Ok(count)
}