Add staged files (so random files don't get removed)
This commit is contained in:
46
src/add.rs
Normal file
46
src/add.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use crate::error::CommitError;
|
||||
use crate::utils::{is_descendant_of_current_dir, is_file_in_dir};
|
||||
use crate::TOUR_DIR;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const STAGED_PATH: &str = "./.tour/staged";
|
||||
|
||||
pub fn add(files: Vec<PathBuf>) -> Result<(), CommitError> {
|
||||
let tour_dir = Path::new(TOUR_DIR);
|
||||
|
||||
for file in &files {
|
||||
if !is_descendant_of_current_dir(file)? {
|
||||
return Err(CommitError::NotADescendantOfCurrentDir(file.clone()));
|
||||
}
|
||||
if is_file_in_dir(file, tour_dir)? {
|
||||
return Err(CommitError::InsideTourDir(file.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut staged = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(STAGED_PATH)?;
|
||||
|
||||
for file in &files {
|
||||
writeln!(staged, "{}", file.display())?;
|
||||
println!("staged: {}", file.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_staged() -> Result<Vec<PathBuf>, std::io::Error> {
|
||||
let content = fs::read_to_string(STAGED_PATH).unwrap_or_default();
|
||||
Ok(content
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn clear_staged() -> Result<(), std::io::Error> {
|
||||
fs::write(STAGED_PATH, "")
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::add::{clear_staged, get_staged};
|
||||
use crate::error::CommitError;
|
||||
use crate::utils::{copy_path, is_descendant_of_current_dir, is_file_in_dir};
|
||||
use crate::TOUR_DIR;
|
||||
@@ -7,6 +8,16 @@ use std::path::{Path, PathBuf};
|
||||
pub fn commit(files: Vec<PathBuf>, message: String) -> Result<(), CommitError> {
|
||||
let tour_dir = Path::new(TOUR_DIR);
|
||||
|
||||
let files = if files.is_empty() {
|
||||
let staged = get_staged()?;
|
||||
if staged.is_empty() {
|
||||
return Err(CommitError::NothingToCommit);
|
||||
}
|
||||
staged
|
||||
} else {
|
||||
files
|
||||
};
|
||||
|
||||
for file in &files {
|
||||
if !is_descendant_of_current_dir(file)? {
|
||||
return Err(CommitError::NotADescendantOfCurrentDir(file.clone()));
|
||||
@@ -30,6 +41,7 @@ pub fn commit(files: Vec<PathBuf>, message: String) -> Result<(), CommitError> {
|
||||
}
|
||||
|
||||
fs::write(step_dir.join("message"), &message)?;
|
||||
clear_staged()?;
|
||||
crate::info::update_last_modified()?;
|
||||
|
||||
println!("Step {}: {}", step_num, message);
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||
pub enum CommitError {
|
||||
NotADescendantOfCurrentDir(PathBuf),
|
||||
InsideTourDir(PathBuf),
|
||||
NothingToCommit,
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
@@ -17,6 +18,9 @@ impl std::fmt::Display for CommitError {
|
||||
Self::InsideTourDir(path) => {
|
||||
write!(f, "File {:?} is inside a .tour directory, which is not allowed.", path)
|
||||
}
|
||||
Self::NothingToCommit => {
|
||||
write!(f, "Nothing to commit. Use `tour add <files>` to stage files first.")
|
||||
}
|
||||
Self::Io(e) => write!(f, "IO error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
40
src/main.rs
40
src/main.rs
@@ -2,19 +2,20 @@ use clap::{Parser, Subcommand};
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod add;
|
||||
mod commit;
|
||||
mod end;
|
||||
mod error;
|
||||
mod info;
|
||||
mod init;
|
||||
mod step;
|
||||
mod utils;
|
||||
mod info;
|
||||
|
||||
const TOUR_DIR: &str = "./.tour";
|
||||
const SESSION_PATH: &str = "./.tour/session";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(author, version, about, long_about = None, disable_help_subcommand = true)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
@@ -25,6 +26,11 @@ enum Commands {
|
||||
// Create a new tour
|
||||
Init,
|
||||
|
||||
// Stage files for the next commit
|
||||
Add {
|
||||
files: Vec<PathBuf>,
|
||||
},
|
||||
|
||||
// Add steps to the tour
|
||||
Commit {
|
||||
files: Vec<PathBuf>,
|
||||
@@ -61,12 +67,40 @@ enum Commands {
|
||||
Start,
|
||||
|
||||
Info,
|
||||
|
||||
// Show help
|
||||
Help,
|
||||
}
|
||||
|
||||
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>> {
|
||||
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)?,
|
||||
@@ -74,7 +108,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
Some(Commands::Step { n }) => crate::step::step_n(n)?,
|
||||
Some(Commands::Start) => crate::step::step_n(0)?,
|
||||
Some(Commands::Info) => crate::info::info()?,
|
||||
_ => println!("command not found"),
|
||||
Some(Commands::Help) | None => help(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
67
src/step.rs
67
src/step.rs
@@ -1,7 +1,7 @@
|
||||
use crate::utils::get_tour_step;
|
||||
use crate::SESSION_PATH;
|
||||
use crate::TOUR_DIR;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -62,19 +62,14 @@ fn step(delta: i32) -> Result<(), io::Error> {
|
||||
let new_step = new_step as u32;
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
let old_files = snapshot_files(&cwd)?;
|
||||
let tracked = get_tracked_files()?;
|
||||
let old_files = snapshot_tracked_files(&cwd, &tracked)?;
|
||||
|
||||
// Clear CWD except .tour/
|
||||
for entry in fs::read_dir(&cwd)? {
|
||||
let entry = entry?;
|
||||
if entry.file_name() == ".tour" {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
fs::remove_dir_all(&path)?;
|
||||
} else {
|
||||
fs::remove_file(&path)?;
|
||||
// Remove only tracked files from CWD
|
||||
for relative in &tracked {
|
||||
let full = cwd.join(relative);
|
||||
if full.is_file() {
|
||||
fs::remove_file(&full)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +86,7 @@ fn step(delta: i32) -> Result<(), io::Error> {
|
||||
// Persist the new step
|
||||
fs::write(SESSION_PATH, format!("STEP={}", new_step))?;
|
||||
|
||||
let new_files = snapshot_files(&cwd)?;
|
||||
let new_files = snapshot_tracked_files(&cwd, &tracked)?;
|
||||
print_changes(&old_files, &new_files);
|
||||
|
||||
let message = fs::read_to_string(step_dir.join("message")).unwrap_or_default();
|
||||
@@ -100,29 +95,51 @@ fn step(delta: i32) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot_files(root: &Path) -> Result<BTreeMap<PathBuf, String>, io::Error> {
|
||||
fn snapshot_tracked_files(root: &Path, tracked: &BTreeSet<PathBuf>) -> Result<BTreeMap<PathBuf, String>, io::Error> {
|
||||
let mut files = BTreeMap::new();
|
||||
collect_files(root, root, &mut files)?;
|
||||
for relative in tracked {
|
||||
let full = root.join(relative);
|
||||
if full.is_file() {
|
||||
let content = fs::read_to_string(&full).unwrap_or_default();
|
||||
files.insert(relative.clone(), content);
|
||||
}
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn collect_files(
|
||||
root: &Path,
|
||||
fn get_tracked_files() -> Result<BTreeSet<PathBuf>, io::Error> {
|
||||
let steps_dir = Path::new(TOUR_DIR).join("steps");
|
||||
let mut tracked = BTreeSet::new();
|
||||
|
||||
if !steps_dir.exists() {
|
||||
return Ok(tracked);
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(&steps_dir)? {
|
||||
let entry = entry?;
|
||||
if entry.path().is_dir() {
|
||||
collect_step_files(&entry.path(), &entry.path(), &mut tracked)?;
|
||||
}
|
||||
}
|
||||
Ok(tracked)
|
||||
}
|
||||
|
||||
fn collect_step_files(
|
||||
step_root: &Path,
|
||||
dir: &Path,
|
||||
files: &mut BTreeMap<PathBuf, String>,
|
||||
files: &mut BTreeSet<PathBuf>,
|
||||
) -> Result<(), io::Error> {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
if entry.file_name() == ".tour" {
|
||||
let path = entry.path();
|
||||
let relative = path.strip_prefix(step_root).unwrap_or(&path).to_path_buf();
|
||||
if relative == Path::new("message") {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect_files(root, &path, files)?;
|
||||
collect_step_files(step_root, &path, files)?;
|
||||
} else {
|
||||
let relative = path.strip_prefix(root).unwrap_or(&path).to_path_buf();
|
||||
let content = fs::read_to_string(&path).unwrap_or_default();
|
||||
files.insert(relative, content);
|
||||
files.insert(relative);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user