diff --git a/modules/stash/help.scm b/modules/stash/help.scm index e14128d..4176c28 100644 --- a/modules/stash/help.scm +++ b/modules/stash/help.scm @@ -19,6 +19,7 @@ Options: -r, --recursive Recursively process directories under source -i, --interactive Interactively prompt for target directory -d, --deploy Deploy mode: create symlinks from dotfiles repo + -R, --restore Restore stashed files back to original locations -h, --help Display this help -v, --version Display version information @@ -42,10 +43,15 @@ Deployment Examples (GNU Stow-like functionality): # Using dot syntax (create symlink from current dir to home): cd ~/.dotfiles/shell && stash . +Restoration Examples: + # Restore a stashed file back to original location: + stash -R -s ~/.files/config/guix/channels.scm + Dotfiles Workflow: 1. Collect dotfiles: ~/.files/collect-dotfiles.sh - 2. Stash to dotfiles repo: stash -s ~/.zshrc -t ~/.dotfiles/shell/zshrc - 3. On new machine: cd ~/.dotfiles && stash -d + 2. Stash to dotfiles repo: stash -s ~/.zshrc -t ~/.files + 3. On new machine: cd ~/.files && stash -d + 4. Restore if needed: stash -R -s ~/.files/config/guix/channels.scm Note: Stash works with any directories, not just config files or dotfiles. You can use it to organize any files by moving them to a backup/storage diff --git a/stash.scm b/stash.scm index df31b55..d29b0ab 100644 --- a/stash.scm +++ b/stash.scm @@ -21,6 +21,7 @@ (recursive (value #f) (single-char #\r)) (interactive (value #f) (single-char #\i)) (deploy (value #f) (single-char #\d)) + (restore (value #f) (single-char #\R)) (help (value #f) (single-char #\h)) (version (value #f) (single-char #\v)))) @@ -36,6 +37,7 @@ (recursive? (option-ref options 'recursive #f)) (interactive? (option-ref options 'interactive #f)) (deploy? (option-ref options 'deploy #f)) + (restore? (option-ref options 'restore #f)) (remaining-args (option-ref options '() '()))) (cond @@ -48,6 +50,9 @@ (deploy? (handle-deploy-mode remaining-args) #t) + (restore? + (handle-restore-mode source) + #t) ((and source target) (handle-explicit-stash source target recursive?) #t) @@ -69,14 +74,121 @@ (define (handle-explicit-stash source target recursive?) "Handle stashing with explicit source and target paths." (let* ((source-path (canonicalize-path source)) - (target-path (canonicalize-path target)) - (package-name (basename source-path)) - (ignore-patterns (read-ignore-patterns source-path))) - (if recursive? - (handle-recursive-stash source-path target-path) - (let ((package (make-package package-name source-path target-path ignore-patterns))) - (process-package package) - #t)))) + (target-base (canonicalize-path target)) + (target-path (create-smart-target-path source-path target-base))) + + (cond + ;; Handle individual files + ((file-is-regular? source-path) + (handle-file-stash source-path target-path)) + + ;; Handle directories + ((file-is-directory? source-path) + (if (directory-has-symlinks? (dirname source-path)) + ;; Parent directory has symlinks, use file-level stashing + (handle-directory-file-level source-path target-path recursive?) + ;; No symlinks in parent, use directory-level stashing + (handle-directory-level source-path target-path recursive?))) + + (else + (display (format #f "Error: ~a is not a regular file or directory\n" source-path)) + (exit 1))))) + +(define (create-smart-target-path source-path target-base) + "Create intelligent target path that preserves directory structure." + (let* ((home-dir (getenv "HOME")) + ;; Get the relative path from home, or just the basename if not under home + (source-relative (if (string-prefix? home-dir source-path) + (string-drop source-path (+ (string-length home-dir) 1)) + (basename source-path))) + ;; Remove leading dot from hidden files/dirs for cleaner organization + (clean-relative (if (string-prefix? "." source-relative) + (string-drop source-relative 1) + source-relative))) + ;; Create clean target path without nesting + (string-append target-base "/" clean-relative))) + +(define (create-path-metadata source-path target-path) + "Create metadata file to track original path structure for restoration." + (let* ((metadata-file (string-append target-path ".stash-meta")) + (home-dir (getenv "HOME")) + (original-relative (if (string-prefix? home-dir source-path) + (string-drop source-path (+ (string-length home-dir) 1)) + source-path))) + (call-with-output-file metadata-file + (lambda (port) + (write `((original-path . ,original-relative) + (timestamp . ,(current-time)) + (stash-version . "0.2.0")) port))))) + +;; Helper functions for intelligent stashing + +(define (file-is-regular? path) + "Check if a path is a regular file." + (and (file-exists? path) + (not (file-is-directory? path)) + (not (file-is-symlink? path)))) + +(define (directory-has-symlinks? dir-path) + "Check if a directory contains any symlinks." + (if (file-exists? dir-path) + (let ((entries (scandir dir-path))) + (any (lambda (entry) + (and (not (member entry '("." ".."))) + (file-is-symlink? (string-append dir-path "/" entry)))) + entries)) + #f)) + +(define (handle-file-stash source-path target-path) + "Handle stashing of individual files." + (display (format #f "Stashing file: ~a -> ~a\n" source-path target-path)) + ;; Create target directory if it doesn't exist + (let ((target-dir (dirname target-path))) + (when (not (file-exists? target-dir)) + (mkdir-p target-dir))) + ;; Create metadata for restoration + (create-path-metadata source-path target-path) + ;; Move file to target + (rename-file source-path target-path) + ;; Create symlink back to original location + (symlink target-path source-path) + (display (format #f "Created symlink: ~a -> ~a\n" source-path target-path))) + +(define (handle-directory-level source-path target-path recursive?) + "Handle directory-level stashing (traditional stash behavior)." + (display (format #f "Directory-level stashing: ~a -> ~a\n" source-path target-path)) + ;; Create target directory if it doesn't exist + (let ((target-dir (dirname target-path))) + (when (not (file-exists? target-dir)) + (mkdir-p target-dir))) + ;; Create metadata for restoration + (create-path-metadata source-path target-path) + ;; Move directory to target + (rename-file source-path target-path) + ;; Create symlink back to original location + (symlink target-path source-path) + (display (format #f "Created directory symlink: ~a -> ~a\n" source-path target-path))) + +(define (handle-directory-file-level source-path target-path recursive?) + "Handle file-level stashing within a directory that has symlinks." + (display (format #f "File-level stashing in directory: ~a\n" source-path)) + (let ((entries (scandir source-path))) + (for-each + (lambda (entry) + (when (not (member entry '("." ".."))) + (let* ((entry-source (string-append source-path "/" entry)) + (entry-target (string-append target-path "/" entry))) + (cond + ;; Skip existing symlinks + ((file-is-symlink? entry-source) + (display (format #f "Skipping existing symlink: ~a\n" entry-source))) + ;; Stash regular files + ((file-is-regular? entry-source) + (handle-file-stash entry-source entry-target)) + ;; Recursively handle subdirectories if recursive mode + ((and recursive? (file-is-directory? entry-source)) + (handle-directory-file-level entry-source entry-target recursive?)))))) + entries))) (define (handle-recursive-stash source-path target-path) "Handle recursive stashing of directories." @@ -192,6 +304,47 @@ (display (format #f "Created symlink: ~a -> ~a\n" target-path source-path))))))) entries))) +;;; Restoration functionality + +(define (handle-restore-mode source-path) + "Handle restoration of stashed files back to original locations." + (if source-path + (restore-stashed-item source-path) + (begin + (display "Error: --restore requires --source to specify what to restore\n") + (exit 1)))) + +(define (restore-stashed-item stashed-path) + "Restore a stashed file or directory back to its original location." + (let* ((metadata-file (string-append stashed-path ".stash-meta")) + (home-dir (getenv "HOME"))) + (if (file-exists? metadata-file) + (let* ((metadata (call-with-input-file metadata-file read)) + (original-relative (assoc-ref metadata 'original-path)) + (original-path (string-append home-dir "/" original-relative))) + (display (format #f "Restoring: ~a -> ~a\n" stashed-path original-path)) + + ;; Remove existing symlink if it exists + (when (and (file-exists? original-path) (file-is-symlink? original-path)) + (delete-file original-path)) + + ;; Create parent directory if needed + (let ((parent-dir (dirname original-path))) + (when (not (file-exists? parent-dir)) + (mkdir-p parent-dir))) + + ;; Move file/directory back + (rename-file stashed-path original-path) + + ;; Remove metadata file + (delete-file metadata-file) + + (display (format #f "Restored: ~a\n" original-path))) + (begin + (display (format #f "Error: No metadata found for ~a\n" stashed-path)) + (display "Cannot restore without metadata file\n") + (exit 1))))) + ;; Entry point for stash (let ((result (main (command-line)))) (if result