mirror of https://codeberg.org/glenneth/stash.git
feat: Add intelligent stashing and restoration functionality
Major improvements toward GNU Stow replacement: - Add intelligent file vs directory level stashing - Add metadata tracking for proper restoration - Add restoration functionality (-R flag) - Detect existing symlinks and adapt behavior - Enhanced help with restoration examples - Path structure preservation for dotfiles management Still needs: - Fix path nesting issue in target creation - Complete directory-level intelligence testing - Conflict resolution improvements
This commit is contained in:
parent
47b78215d2
commit
31f44a5051
|
|
@ -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
|
||||
|
|
|
|||
169
stash.scm
169
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue