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

@@ -1,5 +1,4 @@
use clap::{Parser, Subcommand};
use std::error::Error;
use std::path::PathBuf;
mod add;
@@ -8,14 +7,19 @@ mod end;
mod error;
mod info;
mod init;
mod list;
mod reset;
mod rm;
mod status;
mod step;
mod unstage;
mod utils;
const TOUR_DIR: &str = "./.tour";
const SESSION_PATH: &str = "./.tour/session";
#[derive(Parser)]
#[command(author, version, about, long_about = None, disable_help_subcommand = true)]
#[command(author, version, about = "Create and navigate code tutorials as a series of snapshots", arg_required_else_help = true)]
struct Args {
#[command(subcommand)]
command: Option<Commands>,
@@ -23,15 +27,20 @@ struct Args {
#[derive(Subcommand)]
enum Commands {
// Create a new tour
/// Set up a new tour in the current directory
Init,
// Stage files for the next commit
/// Stage files for the next commit
Add {
files: Vec<PathBuf>,
},
// Add steps to the tour
/// Remove files from staging
Unstage {
files: Vec<PathBuf>,
},
/// Commit staged files as a new step
Commit {
files: Vec<PathBuf>,
@@ -39,76 +48,72 @@ enum Commands {
message: String,
},
// Finish the tour
/// Mark files for removal in the next commit
Rm {
files: Vec<PathBuf>,
},
/// Finalise the tour
End {
#[arg(short, long, value_name = "MESSAGE")]
message: String,
},
// Go to next step of tour
/// Advance n steps (default 1)
Next {
#[arg(short, value_name = "NUM STEPS")]
n: Option<i32>,
n: Option<u32>,
},
// Go to previous step of tour
/// Go back n steps (default 1)
Prev {
#[arg(short, value_name = "NUM STEPS")]
n: Option<i32>,
n: Option<u32>,
},
// Go to a specific step of tour
/// Jump to step n
Step {
#[arg(value_name = "STEP")]
n: i32,
n: u32,
},
// Go to beginning of tour
/// Load the first step
Start,
/// Show tour metadata
Info,
// Show help
Help,
/// Show current step and staged files
Status,
/// List all steps with messages
List,
/// Reset tour session and remove tracked files
Reset,
}
fn help() {
println!(
"\
\x1b[1mtour\x1b[0m — create and navigate code tutorials as a series of snapshots
\x1b[1mAUTHOR WORKFLOW\x1b[0m
tour init Set up a new tour in the current directory
tour add <files...> Stage files for the next commit
tour commit [-m <msg>] Commit staged files as a new step
tour commit <files...> -m <msg> Stage and commit files in one step
tour end -m <msg> Finalise the tour
\x1b[1mREADER WORKFLOW\x1b[0m
tour start Load the first step
tour next [n] Advance n steps (default 1)
tour prev [n] Go back n steps (default 1)
tour step <n> Jump to step n
\x1b[1mOTHER\x1b[0m
tour info Show tour metadata
tour help Show this help message"
);
}
fn main() -> Result<(), Box<dyn Error>> {
fn main() {
let args = Args::parse();
match args.command {
Some(Commands::Init) => crate::init::init()?,
Some(Commands::Add { files }) => crate::add::add(files)?,
Some(Commands::Commit { files, message }) => crate::commit::commit(files, message)?,
Some(Commands::End { message }) => crate::end::end(message)?,
Some(Commands::Next { n }) => crate::step::next(n)?,
Some(Commands::Prev { n }) => crate::step::prev(n)?,
Some(Commands::Step { n }) => crate::step::step_n(n)?,
Some(Commands::Start) => crate::step::step_n(0)?,
Some(Commands::Info) => crate::info::info()?,
Some(Commands::Help) | None => help(),
let result = match args.command {
Some(Commands::Init) => crate::init::init(),
Some(Commands::Add { files }) => crate::add::add(files),
Some(Commands::Unstage { files }) => crate::unstage::unstage(files),
Some(Commands::Commit { files, message }) => crate::commit::commit(files, message),
Some(Commands::Rm { files }) => crate::rm::rm(files),
Some(Commands::End { message }) => crate::end::end(message),
Some(Commands::Next { n }) => crate::step::next(n),
Some(Commands::Prev { n }) => crate::step::prev(n),
Some(Commands::Step { n }) => crate::step::step_n(n),
Some(Commands::Start) => crate::step::step_n(1),
Some(Commands::Info) => crate::info::info(),
Some(Commands::Status) => crate::status::status(),
Some(Commands::List) => crate::list::list(),
Some(Commands::Reset) => crate::reset::reset(),
None => Ok(()),
};
if let Err(e) = result {
eprintln!("Error: {}", e);
std::process::exit(1);
}
Ok(())
}