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::error::CommitError;
|
||||||
use crate::utils::{copy_path, is_descendant_of_current_dir, is_file_in_dir};
|
use crate::utils::{copy_path, is_descendant_of_current_dir, is_file_in_dir};
|
||||||
use crate::TOUR_DIR;
|
use crate::TOUR_DIR;
|
||||||
@@ -7,6 +8,16 @@ use std::path::{Path, PathBuf};
|
|||||||
pub fn commit(files: Vec<PathBuf>, message: String) -> Result<(), CommitError> {
|
pub fn commit(files: Vec<PathBuf>, message: String) -> Result<(), CommitError> {
|
||||||
let tour_dir = Path::new(TOUR_DIR);
|
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 {
|
for file in &files {
|
||||||
if !is_descendant_of_current_dir(file)? {
|
if !is_descendant_of_current_dir(file)? {
|
||||||
return Err(CommitError::NotADescendantOfCurrentDir(file.clone()));
|
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)?;
|
fs::write(step_dir.join("message"), &message)?;
|
||||||
|
clear_staged()?;
|
||||||
crate::info::update_last_modified()?;
|
crate::info::update_last_modified()?;
|
||||||
|
|
||||||
println!("Step {}: {}", step_num, message);
|
println!("Step {}: {}", step_num, message);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
|||||||
pub enum CommitError {
|
pub enum CommitError {
|
||||||
NotADescendantOfCurrentDir(PathBuf),
|
NotADescendantOfCurrentDir(PathBuf),
|
||||||
InsideTourDir(PathBuf),
|
InsideTourDir(PathBuf),
|
||||||
|
NothingToCommit,
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +18,9 @@ impl std::fmt::Display for CommitError {
|
|||||||
Self::InsideTourDir(path) => {
|
Self::InsideTourDir(path) => {
|
||||||
write!(f, "File {:?} is inside a .tour directory, which is not allowed.", 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),
|
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::error::Error;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod add;
|
||||||
mod commit;
|
mod commit;
|
||||||
mod end;
|
mod end;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod info;
|
||||||
mod init;
|
mod init;
|
||||||
mod step;
|
mod step;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod info;
|
|
||||||
|
|
||||||
const TOUR_DIR: &str = "./.tour";
|
const TOUR_DIR: &str = "./.tour";
|
||||||
const SESSION_PATH: &str = "./.tour/session";
|
const SESSION_PATH: &str = "./.tour/session";
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None, disable_help_subcommand = true)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Commands>,
|
command: Option<Commands>,
|
||||||
@@ -25,6 +26,11 @@ enum Commands {
|
|||||||
// Create a new tour
|
// Create a new tour
|
||||||
Init,
|
Init,
|
||||||
|
|
||||||
|
// Stage files for the next commit
|
||||||
|
Add {
|
||||||
|
files: Vec<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
// Add steps to the tour
|
// Add steps to the tour
|
||||||
Commit {
|
Commit {
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
@@ -61,12 +67,40 @@ enum Commands {
|
|||||||
Start,
|
Start,
|
||||||
|
|
||||||
Info,
|
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>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(Commands::Init) => crate::init::init()?,
|
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::Commit { files, message }) => crate::commit::commit(files, message)?,
|
||||||
Some(Commands::End { message }) => crate::end::end(message)?,
|
Some(Commands::End { message }) => crate::end::end(message)?,
|
||||||
Some(Commands::Next { n }) => crate::step::next(n)?,
|
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::Step { n }) => crate::step::step_n(n)?,
|
||||||
Some(Commands::Start) => crate::step::step_n(0)?,
|
Some(Commands::Start) => crate::step::step_n(0)?,
|
||||||
Some(Commands::Info) => crate::info::info()?,
|
Some(Commands::Info) => crate::info::info()?,
|
||||||
_ => println!("command not found"),
|
Some(Commands::Help) | None => help(),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/step.rs
67
src/step.rs
@@ -1,7 +1,7 @@
|
|||||||
use crate::utils::get_tour_step;
|
use crate::utils::get_tour_step;
|
||||||
use crate::SESSION_PATH;
|
use crate::SESSION_PATH;
|
||||||
use crate::TOUR_DIR;
|
use crate::TOUR_DIR;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -62,19 +62,14 @@ fn step(delta: i32) -> Result<(), io::Error> {
|
|||||||
let new_step = new_step as u32;
|
let new_step = new_step as u32;
|
||||||
|
|
||||||
let cwd = std::env::current_dir()?;
|
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/
|
// Remove only tracked files from CWD
|
||||||
for entry in fs::read_dir(&cwd)? {
|
for relative in &tracked {
|
||||||
let entry = entry?;
|
let full = cwd.join(relative);
|
||||||
if entry.file_name() == ".tour" {
|
if full.is_file() {
|
||||||
continue;
|
fs::remove_file(&full)?;
|
||||||
}
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
fs::remove_dir_all(&path)?;
|
|
||||||
} else {
|
|
||||||
fs::remove_file(&path)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +86,7 @@ fn step(delta: i32) -> Result<(), io::Error> {
|
|||||||
// Persist the new step
|
// Persist the new step
|
||||||
fs::write(SESSION_PATH, format!("STEP={}", 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);
|
print_changes(&old_files, &new_files);
|
||||||
|
|
||||||
let message = fs::read_to_string(step_dir.join("message")).unwrap_or_default();
|
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(())
|
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();
|
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)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_files(
|
fn get_tracked_files() -> Result<BTreeSet<PathBuf>, io::Error> {
|
||||||
root: &Path,
|
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,
|
dir: &Path,
|
||||||
files: &mut BTreeMap<PathBuf, String>,
|
files: &mut BTreeSet<PathBuf>,
|
||||||
) -> Result<(), io::Error> {
|
) -> Result<(), io::Error> {
|
||||||
for entry in fs::read_dir(dir)? {
|
for entry in fs::read_dir(dir)? {
|
||||||
let entry = entry?;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
collect_files(root, &path, files)?;
|
collect_step_files(step_root, &path, files)?;
|
||||||
} else {
|
} else {
|
||||||
let relative = path.strip_prefix(root).unwrap_or(&path).to_path_buf();
|
files.insert(relative);
|
||||||
let content = fs::read_to_string(&path).unwrap_or_default();
|
|
||||||
files.insert(relative, content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user