- 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>
141 lines
3.9 KiB
Rust
141 lines
3.9 KiB
Rust
use std::fs;
|
|
use std::io::{self, Write};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
const INFO_PATH: &str = "./.tour/info";
|
|
|
|
struct TourInfo {
|
|
author: String,
|
|
description: String,
|
|
language: String,
|
|
created: String,
|
|
updated: String,
|
|
}
|
|
|
|
impl TourInfo {
|
|
fn parse(content: &str) -> Self {
|
|
let mut author = String::new();
|
|
let mut description = String::new();
|
|
let mut language = String::new();
|
|
let mut created = String::new();
|
|
let mut updated = String::new();
|
|
|
|
for line in content.lines() {
|
|
if let Some((key, value)) = line.split_once('=') {
|
|
match key {
|
|
"author" => author = value.to_string(),
|
|
"description" => description = value.to_string(),
|
|
"language" => language = value.to_string(),
|
|
"created" => created = value.to_string(),
|
|
"updated" => updated = value.to_string(),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
TourInfo { author, description, language, created, updated }
|
|
}
|
|
|
|
fn serialize(&self) -> String {
|
|
format!(
|
|
"author={}\ndescription={}\nlanguage={}\ncreated={}\nupdated={}\n",
|
|
self.author, self.description, self.language, self.created, self.updated
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn set_info() -> Result<(), io::Error> {
|
|
macro_rules! prompt {
|
|
($msg:expr) => {{
|
|
print!($msg);
|
|
io::stdout().flush()?;
|
|
let mut buf = String::new();
|
|
io::stdin().read_line(&mut buf)?;
|
|
buf.trim().to_string()
|
|
}};
|
|
}
|
|
|
|
let author = prompt!("Author: ");
|
|
let description = prompt!("Description: ");
|
|
let language = prompt!("Language: ");
|
|
let today = current_date();
|
|
|
|
let info = TourInfo {
|
|
author,
|
|
description,
|
|
language,
|
|
created: today.clone(),
|
|
updated: today,
|
|
};
|
|
|
|
fs::write(INFO_PATH, info.serialize())
|
|
}
|
|
|
|
pub fn get_info() -> Result<(), io::Error> {
|
|
let content = fs::read_to_string(INFO_PATH).map_err(|_| {
|
|
io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"No tour info found. Run `tour init` to set up a tour.",
|
|
)
|
|
})?;
|
|
let info = TourInfo::parse(&content);
|
|
println!("Author: {}", info.author);
|
|
println!("Description: {}", info.description);
|
|
println!("Language: {}", info.language);
|
|
println!("Created: {}", info.created);
|
|
println!("Updated: {}", info.updated);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_last_modified() -> Result<(), io::Error> {
|
|
let content = fs::read_to_string(INFO_PATH).unwrap_or_default();
|
|
let mut info = TourInfo::parse(&content);
|
|
info.updated = current_date();
|
|
fs::write(INFO_PATH, info.serialize())
|
|
}
|
|
|
|
pub fn info() -> Result<(), crate::error::TourError> {
|
|
crate::utils::require_tour()?;
|
|
get_info()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn current_date() -> String {
|
|
let secs = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs();
|
|
let (y, m, d) = days_to_ymd((secs / 86400) as u32);
|
|
format!("{:04}-{:02}-{:02}", y, m, d)
|
|
}
|
|
|
|
fn days_to_ymd(mut days: u32) -> (u32, u32, u32) {
|
|
let mut year = 1970u32;
|
|
loop {
|
|
let days_in_year = if is_leap(year) { 366 } else { 365 };
|
|
if days < days_in_year {
|
|
break;
|
|
}
|
|
days -= days_in_year;
|
|
year += 1;
|
|
}
|
|
let month_days = if is_leap(year) {
|
|
[31u32, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
} else {
|
|
[31u32, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
};
|
|
let mut month = 1u32;
|
|
for (i, &md) in month_days.iter().enumerate() {
|
|
if days < md {
|
|
month = i as u32 + 1;
|
|
break;
|
|
}
|
|
days -= md;
|
|
}
|
|
(year, month, days + 1)
|
|
}
|
|
|
|
fn is_leap(year: u32) -> bool {
|
|
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
|
|
}
|