docs: update HANDOFF.md — age-native redesign progress (tasks 1–7 complete)

This commit is contained in:
Joseph O'Brien
2026-04-01 20:58:09 -04:00
parent 545a45b3d3
commit b4cba74ea3
24 changed files with 1069 additions and 99 deletions

View File

@@ -14,7 +14,9 @@ impl IgnoreMatcher {
// Match the pattern as a filename component and also as a path suffix.
let glob = Glob::new(pattern)
.or_else(|_| Glob::new(&format!("**/{pattern}")))
.map_err(|e| NotfilesError::Other(format!("invalid ignore pattern '{pattern}': {e}")))?;
.map_err(|e| {
NotfilesError::Other(format!("invalid ignore pattern '{pattern}': {e}"))
})?;
builder.add(glob);
// Also add a recursive variant so "foo" matches "a/foo" etc.
#[allow(clippy::collapsible_if)]

View File

@@ -10,11 +10,7 @@ use std::path::Path;
pub use linker::{LinkOptions, State};
pub use package::resolve_packages;
pub fn link(
dotfiles_dir: &Path,
packages: &[String],
opts: &LinkOptions,
) -> Result<State> {
pub fn link(dotfiles_dir: &Path, packages: &[String], opts: &LinkOptions) -> Result<State> {
let config = notcore::Config::load(dotfiles_dir)?;
let mut state = State::load(dotfiles_dir)?;
let pkgs = resolve_packages(dotfiles_dir, packages)?;

View File

@@ -4,8 +4,8 @@ use std::path::{Path, PathBuf};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use notcore::{Config, Method, NotfilesError, expand_tilde};
use crate::package::collect_files;
use notcore::{Config, Method, NotfilesError, expand_tilde};
const STATE_FILE: &str = ".notfiles-state.toml";
@@ -46,7 +46,10 @@ impl State {
}
pub fn entries_for_package(&self, package: &str) -> Vec<&StateEntry> {
self.entries.iter().filter(|e| e.package == package).collect()
self.entries
.iter()
.filter(|e| e.package == package)
.collect()
}
pub fn remove_package(&mut self, package: &str) {
@@ -55,7 +58,8 @@ impl State {
pub fn add_entry(&mut self, entry: StateEntry) {
// Remove existing entry for same source+target, then add new
self.entries.retain(|e| !(e.source == entry.source && e.target == entry.target));
self.entries
.retain(|e| !(e.source == entry.source && e.target == entry.target));
self.entries.push(entry);
}
}
@@ -113,10 +117,18 @@ pub fn link_package(
if !opts.no_backup {
let backup = backup_path(&target);
if opts.dry_run {
println!(" \x1b[33mwould backup\x1b[0m {} -> {}", target.display(), backup.display());
println!(
" \x1b[33mwould backup\x1b[0m {} -> {}",
target.display(),
backup.display()
);
} else {
if opts.verbose {
println!(" \x1b[33mbackup\x1b[0m {} -> {}", target.display(), backup.display());
println!(
" \x1b[33mbackup\x1b[0m {} -> {}",
target.display(),
backup.display()
);
}
fs::rename(&target, &backup)?;
}
@@ -147,7 +159,10 @@ pub fn link_package(
};
if opts.dry_run {
println!(" \x1b[36mwould {action_word}\x1b[0m {source_display} -> {}", target.display());
println!(
" \x1b[36mwould {action_word}\x1b[0m {source_display} -> {}",
target.display()
);
} else {
match method {
Method::Symlink => {
@@ -161,7 +176,10 @@ pub fn link_package(
}
}
if opts.verbose {
println!(" \x1b[32m{action_word}\x1b[0m {source_display} -> {}", target.display());
println!(
" \x1b[32m{action_word}\x1b[0m {source_display} -> {}",
target.display()
);
}
state.add_entry(StateEntry {
@@ -183,7 +201,11 @@ pub fn unlink_package(
package: &str,
opts: &LinkOptions,
) -> Result<(), NotfilesError> {
let entries: Vec<StateEntry> = state.entries_for_package(package).into_iter().cloned().collect();
let entries: Vec<StateEntry> = state
.entries_for_package(package)
.into_iter()
.cloned()
.collect();
if entries.is_empty() {
if opts.verbose {
@@ -278,7 +300,10 @@ fn cleanup_empty_parents(path: &Path) {
if Some(parent.to_path_buf()) == dirs::home_dir() || parent == Path::new("/") {
break;
}
if fs::read_dir(parent).map(|mut d| d.next().is_none()).unwrap_or(false) {
if fs::read_dir(parent)
.map(|mut d| d.next().is_none())
.unwrap_or(false)
{
let _ = fs::remove_dir(parent);
dir = parent.parent();
} else {

View File

@@ -2,11 +2,11 @@ use anyhow::{Context, Result};
use clap::Parser;
use std::fs;
use notcore::Config;
use notfiles::cli::{Cli, Command};
use notfiles::linker::{LinkOptions, State};
use notfiles::package::resolve_packages;
use notfiles::{linker, status};
use notcore::Config;
fn main() -> Result<()> {
let cli = Cli::parse();
@@ -46,7 +46,10 @@ fn main() -> Result<()> {
if !cli.dry_run {
state.save(&dotfiles_dir)?;
let count: usize = pkgs.iter().map(|p| state.entries_for_package(p).len()).sum();
let count: usize = pkgs
.iter()
.map(|p| state.entries_for_package(p).len())
.sum();
println!(
"\x1b[32mLinked {count} file{} across {} package{}.\x1b[0m",
if count == 1 { "" } else { "s" },

View File

@@ -1,8 +1,8 @@
use std::fs;
use std::path::{Path, PathBuf};
use notcore::{Config, NotfilesError};
use crate::ignore::IgnoreMatcher;
use notcore::{Config, NotfilesError};
/// Discover available packages (subdirectories of the dotfiles dir).
pub fn discover_packages(dotfiles_dir: &Path) -> Result<Vec<String>, NotfilesError> {

View File

@@ -1,9 +1,9 @@
use std::fs;
use std::path::{Path, PathBuf};
use notcore::{Config, Method, expand_tilde};
use crate::linker::State;
use crate::package::collect_files;
use notcore::{Config, Method, expand_tilde};
#[derive(Debug, PartialEq)]
pub enum FileStatus {
@@ -69,8 +69,7 @@ pub fn package_status(
}
Method::Copy => {
let has_state = state.entries.iter().any(|e| {
e.package == package
&& e.target == target.to_string_lossy().as_ref()
e.package == package && e.target == target.to_string_lossy().as_ref()
});
if has_state && target.exists() {
FileStatus::Copied
@@ -122,6 +121,11 @@ pub fn print_status(package: &str, entries: &[StatusEntry]) {
}
println!(" \x1b[1m{package}\x1b[0m:");
for entry in entries {
println!(" {} {} -> {}", entry.status, entry.source_display, entry.target.display());
println!(
" {} {} -> {}",
entry.status,
entry.source_display,
entry.target.display()
);
}
}

View File

@@ -94,7 +94,13 @@ fn test_link_and_status() {
let gitconfig = target.join(".gitconfig");
assert!(gitconfig.exists());
assert!(gitconfig.symlink_metadata().unwrap().file_type().is_symlink());
assert!(
gitconfig
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink()
);
// State file should exist
assert!(dotfiles.join(".notfiles-state.toml").exists());
@@ -199,7 +205,14 @@ fn test_force_with_backup() {
assert!(ok);
// The link should now exist
assert!(target.join(".gitconfig").symlink_metadata().unwrap().file_type().is_symlink());
assert!(
target
.join(".gitconfig")
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink()
);
// A backup should exist
let backups: Vec<_> = fs::read_dir(&target)
@@ -232,11 +245,7 @@ fn test_force_no_backup() {
let backups: Vec<_> = fs::read_dir(&target)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.contains("notfiles-backup")
})
.filter(|e| e.file_name().to_string_lossy().contains("notfiles-backup"))
.collect();
assert_eq!(backups.len(), 0);
}
@@ -272,8 +281,17 @@ method = "copy"
let ssh_config = target.join(".ssh/config");
assert!(ssh_config.exists());
// Should NOT be a symlink
assert!(!ssh_config.symlink_metadata().unwrap().file_type().is_symlink());
assert_eq!(fs::read_to_string(&ssh_config).unwrap(), "Host *\n AddKeysToAgent yes");
assert!(
!ssh_config
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink()
);
assert_eq!(
fs::read_to_string(&ssh_config).unwrap(),
"Host *\n AddKeysToAgent yes"
);
// Status should show "copied"
let (stdout, _, _) = run(&dotfiles, &["status", "ssh"]);