diff --git a/Cargo.lock b/Cargo.lock index f94b385..67a3726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,7 +540,7 @@ dependencies = [ [[package]] name = "notalterra" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "chrono", diff --git a/skills/release-workflow/SKILL.md b/skills/release-workflow/SKILL.md index 25fa8f5..2783f45 100644 --- a/skills/release-workflow/SKILL.md +++ b/skills/release-workflow/SKILL.md @@ -45,6 +45,7 @@ After CI completes: - **Sign once.** Wait for CI to go green on the unsigned commit, **then** amend with `--gpg-sign` + force-push. Do not amend between CI runs — each amend creates a new SHA, which triggers a new CI run and leaves stale Pages deployments that block the next run. - Normal push for fast-forward commits - `--force-with-lease` for amended commits or tag refreshes +- **Start from a clean tag.** Before re-tagging, always delete the old tag locally AND remotely (`git tag -d v0.x.y && git push --delete origin v0.x.y`). If the old tag still exists, `git tag -s` will fail with "tag already exists" and you'll end up with a tag pointing to the wrong commit. - If force-pushing, delete old tag and re-tag after the new commit lands - Tag once, push once. Re-signing an existing tag pushes a new SHA and triggers another CI run with the same deployment conflict risk. diff --git a/src/main.rs b/src/main.rs index fb1c1cf..9dbdbdf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,9 +143,7 @@ fn refresh_stats(tui_state: &mut tui::AppState, save_folder: Option<&Path>) { tui_state.live_save_count = live; tui_state.backup_count = bak; tui_state.has_ini_backup = ini; - tui_state.context_path = Some( - crate::config::get_backup_root().display().to_string(), - ); + tui_state.context_path = Some(crate::config::get_backup_root().display().to_string()); } // ── main loop ────────────────────────────────────────────────────────────── @@ -245,15 +243,17 @@ fn run_app(terminal: &mut Terminal) -> Result<()> { // Main menu: show path based on highlighted item { let sel = menu_state.selected().unwrap_or(0); - let br = crate::config::get_backup_root().to_string_lossy().to_string(); + let br = crate::config::get_backup_root() + .to_string_lossy() + .to_string(); let ini_path_str = app.save_folder.as_ref().and_then(|sf| { discovery::derive_ini_path(sf).map(|p| p.to_string_lossy().to_string()) }); app.tui_state.context_path = match sel { - 0 | 1 => None, // save path (fallback) - 3..=5 => Some(br), // backup root - 7 => ini_path_str, // ini Config\Windows path - _ => Some(String::new()), // blank / disclaimer / exit → no path + 0 | 1 => None, // save path (fallback) + 3..=5 => Some(br), // backup root + 7 => ini_path_str, // ini Config\Windows path + _ => Some(String::new()), // blank / disclaimer / exit → no path }; } terminal.draw(|f| { @@ -924,14 +924,18 @@ fn action_restore_backup(terminal: &mut Terminal, app: &mut App) let header = format!(" {:<38} {:>8}", "Backup", "Size"); let mut items: Vec = vec![header, String::new()]; - items.extend(backups - .iter() - .map(|p| { - let name = p.file_name().unwrap().to_string_lossy(); - let size = std::fs::metadata(p).map(|m| format_size(m.len())).unwrap_or("?".into()); - format!(" {:<42} {:>8}", format_backup_label(&name), size) - }) - .collect::>()); + items.extend( + backups + .iter() + .map(|p| { + let name = p.file_name().unwrap().to_string_lossy(); + let size = std::fs::metadata(p) + .map(|m| format_size(m.len())) + .unwrap_or("?".into()); + format!(" {:<42} {:>8}", format_backup_label(&name), size) + }) + .collect::>(), + ); // Prepend empty description entries for header + blank let descs: Vec = std::iter::once(String::new()) .chain(std::iter::once(String::new())) @@ -955,7 +959,9 @@ fn action_restore_backup(terminal: &mut Terminal, app: &mut App) loop { // Restore picker: show backup root in header app.tui_state.context_path = Some( - crate::config::get_backup_root().to_string_lossy().to_string(), + crate::config::get_backup_root() + .to_string_lossy() + .to_string(), ); terminal.draw(|f| { tui::draw_picker(f, &app.tui_state, &item_refs, &desc_refs, &mut state, true); @@ -968,21 +974,26 @@ fn action_restore_backup(terminal: &mut Terminal, app: &mut App) } KeyCode::Down => { let i = state.selected().unwrap_or(0); - state.select(Some((i + 1).min(backups.len().saturating_sub(1)))); + state.select(Some((i + 1).min(items.len().saturating_sub(1)))); } KeyCode::Enter => { let idx = state.selected().unwrap_or(2).max(2).saturating_sub(2); - if idx >= backups.len() { continue; } + if idx >= backups.len() { + continue; + } let chosen = &backups[idx]; let name = chosen.file_name().unwrap().to_string_lossy().to_string(); // Block restore on corrupt archives if !ops::check_tar_gz_integrity(chosen) { - ok_dialog(terminal, app, "Corrupt Backup", + ok_dialog( + terminal, + app, + "Corrupt Backup", "This backup archive appears to be corrupt or\n\ incomplete and cannot be restored.\n\ \n\ - Create a new full backup to replace it." + Create a new full backup to replace it.", )?; continue; } @@ -1071,18 +1082,21 @@ fn run_ini_submenu(terminal: &mut Terminal, app: &mut App) -> Res const INI_SKIP: &[usize] = &[3]; let ini_max = 4usize; - let ini_path_str = app.save_folder.as_ref().and_then(|sf| { - discovery::derive_ini_path(sf).map(|p| p.to_string_lossy().to_string()) - }); + let ini_path_str = app + .save_folder + .as_ref() + .and_then(|sf| discovery::derive_ini_path(sf).map(|p| p.to_string_lossy().to_string())); loop { // Ini submenu: show path based on highlighted item { let sel = state.selected().unwrap_or(0); - let br = crate::config::get_backup_root().to_string_lossy().to_string(); + let br = crate::config::get_backup_root() + .to_string_lossy() + .to_string(); app.tui_state.context_path = match sel { - 0 | 1 => Some(br), // backup root (backup/restore) - 2 => ini_path_str.clone(), // Config\Windows path (delete) - _ => Some(String::new()), // blank / back → nothing + 0 | 1 => Some(br), // backup root (backup/restore) + 2 => ini_path_str.clone(), // Config\Windows path (delete) + _ => Some(String::new()), // blank / back → nothing }; } terminal.draw(|f| { @@ -1212,20 +1226,27 @@ fn ini_restore_action( let mut items: Vec = vec![header, String::new()]; items.extend(backups.iter().map(|p| { let name = p.file_name().unwrap().to_string_lossy(); - let size = std::fs::metadata(p).map(|m| format_size(m.len())).unwrap_or("?".into()); + let size = std::fs::metadata(p) + .map(|m| format_size(m.len())) + .unwrap_or("?".into()); format!(" {:<42} {:>8}", format_backup_label(&name), size) })); let item_refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect(); let ini_descs: Vec<&str> = std::iter::once("") .chain(std::iter::once("")) - .chain(std::iter::repeat_n("Restore .ini files from this backup", backups.len())) + .chain(std::iter::repeat_n( + "Restore .ini files from this backup", + backups.len(), + )) .collect::>(); let mut state = ListState::default().with_selected(Some(2)); // skip header + blank loop { // Restore picker: show backup root in header app.tui_state.context_path = Some( - crate::config::get_backup_root().to_string_lossy().to_string(), + crate::config::get_backup_root() + .to_string_lossy() + .to_string(), ); terminal.draw(|f| { tui::draw_picker(f, &app.tui_state, &item_refs, &ini_descs, &mut state, true); @@ -1238,7 +1259,7 @@ fn ini_restore_action( } KeyCode::Down => { let i = state.selected().unwrap_or(0); - state.select(Some((i + 1).min(backups.len().saturating_sub(1)))); + state.select(Some((i + 1).min(items.len().saturating_sub(1)))); } KeyCode::Enter => { let idx = state.selected().unwrap_or(0); @@ -1693,7 +1714,7 @@ fn format_backup_label(filename: &str) -> String { let date_str = if let Some(pos) = rest.find(|c: char| c.is_ascii_digit()) { let slice = &rest[pos..]; if slice.len() >= 17 { - let date_part = &slice[..10]; // 2026-06-09 + let date_part = &slice[..10]; // 2026-06-09 let time_part = &slice[11..17]; // 125430 let h = &time_part[..2]; let m = &time_part[2..4];