#!/usr/bin/env fish # # archive-coding-project — move a coding project into ~/.local/archives/ and # strip it down to minimize disk use. # # After moving the folder, this runs `git clean -dxf` to remove all # ignored/untracked files (build artifacts, node_modules, etc.). If the repo # uses git LFS, it also drops the local LFS object cache so those files must be # re-fetched on the next checkout. Finally it compresses the folder into a # single .7z archive with 7z and removes the uncompressed copy, reporting how # much disk space was reclaimed. # # Usage: # archive-coding-project Archive a project # archive-coding-project --restore Restore an archive into the cwd function usage echo "Usage:" >&2 echo " archive-coding-project Archive a project" >&2 echo " archive-coding-project --restore Restore into the cwd" >&2 echo "" >&2 echo "Archiving moves into ~/.local/archives/, runs 'git clean -dxf'," >&2 echo "drops git LFS objects, and compresses it into a .7z to minimize disk use." >&2 end # Format a size given in KB as a human-readable string. function human_kb --argument-names kb echo $kb | awk '{ k=$1 if (k < 1024) printf "%d KB", k else if (k < 1048576) printf "%.1f MB", k/1024 else printf "%.2f GB", k/1048576 }' end # Size of a path in KB (apparent disk usage). Works on files and directories. function size_kb --argument-names path du -sk -- $path | awk '{print $1}' end if not type -q 7z echo "Error: '7z' not found on PATH (install p7zip)" >&2 exit 1 end # --------------------------------------------------------------------------- # Restore mode # --------------------------------------------------------------------------- if test (count $argv) -ge 1; and test "$argv[1]" = --restore if test (count $argv) -ne 2 usage exit 1 end set -l archive $argv[2] if not test -f $archive echo "Error: archive '$archive' not found" >&2 exit 1 end set -l abs_archive (realpath -- $archive) # Figure out the top-level entries inside the archive so we can refuse to # clobber existing paths in the cwd. In `7z l -slt` output the first # `Path =` line is the archive's own filename, so we drop it (tail +2), then # reduce each entry to its first path component and dedupe. set -l tops (7z l -slt -- $abs_archive \ | string match -r '^Path = .+' \ | string replace 'Path = ' '' \ | tail -n +2 \ | string replace -r '/.*' '' \ | sort -u) if test (count $tops) -eq 0 echo "Error: could not read archive contents" >&2 exit 1 end for t in $tops if test -e ./$t echo "Error: '$t' already exists in the current directory" >&2 exit 1 end end echo "Restoring "(string join ', ' $tops)" from $abs_archive into "(pwd)"..." if 7z x -- $abs_archive echo "" for t in $tops echo "Restored "(pwd)"/$t" end echo "(The archive is left in place; delete it with: rm -- $abs_archive)" else echo "Error: extraction failed" >&2 exit 1 end exit 0 end # --------------------------------------------------------------------------- # Archive mode # --------------------------------------------------------------------------- if test (count $argv) -ne 1 usage exit 1 end set -l src $argv[1] # Strip a trailing slash so basename/dirname behave predictably. set src (string trim --right --chars=/ -- $src) if not test -d $src echo "Error: '$src' is not a directory" >&2 exit 1 end # Resolve to an absolute path. set -l abs_src (realpath -- $src) set -l name (basename -- $abs_src) set -l archive_dir "$HOME/.local/archives" set -l dest "$archive_dir/$name" set -l archive_file "$dest.7z" if test -e $dest echo "Error: destination '$dest' already exists" >&2 exit 1 end if test -e $archive_file echo "Error: archive '$archive_file' already exists" >&2 exit 1 end # Measure the original footprint before we touch anything, so we can report the # real space reclaimed (original folder vs. final compressed archive). set -l orig_kb (size_kb $abs_src) echo "About to archive:" echo " from: $abs_src" echo " to: $archive_file" echo "" echo "This will:" echo " - move the folder into $archive_dir" echo " - run 'git clean -dxf' (deletes all untracked & ignored files)" echo " - drop local git LFS objects if the repo uses LFS" echo " - compress it into a .7z and remove the uncompressed folder" echo "" read -l -P "Proceed? [y/N] " confirm if not string match -qi y -- $confirm echo "Aborted." exit 1 end mkdir -p $archive_dir echo "Moving folder..." mv -- $abs_src $dest # Operate on the archived copy via absolute paths. We deliberately avoid # pushd/popd: this script may have been launched from inside the folder we just # moved, in which case returning to the original cwd would fail. if test -d "$dest/.git" echo "Running 'git clean -dxf'..." git -C $dest clean -dxf # If the repo uses git LFS, drop the local object cache so the files have to # be re-fetched on the next checkout. We use `git lfs prune --recent`, which # is aggressive (it ignores the "recent commits" retention window) but still # safe: `--verify-remote` only deletes objects confirmed to exist on the # remote, so nothing unpushed is lost. Re-checkout pulls them back. if type -q git-lfs; and test -d "$dest/.git/lfs" echo "Dropping git LFS objects..." git -C $dest lfs prune --recent --verify-remote end else echo "Note: '$name' is not a git repository; skipping git clean / LFS steps." end # Compress into a single .7z, then drop the uncompressed folder. We cd into # archive_dir so the archive stores a clean relative path ($name/...) rather # than the absolute path. `-mx=9` is max compression. cd in this subprocess # does not affect the caller's shell. echo "Compressing into $archive_file..." if cd $archive_dir; and 7z a -mx=9 -- "$name.7z" "$name" rm -rf $dest set -l archive_kb (size_kb $archive_file) set -l saved_kb (math "$orig_kb - $archive_kb") set -l pct 0 if test $orig_kb -gt 0 set pct (math -s0 "100 * $saved_kb / $orig_kb") end echo "" echo "Archived to $archive_file" echo " original: "(human_kb $orig_kb) echo " archive: "(human_kb $archive_kb) echo " saved: "(human_kb $saved_kb)" ($pct%)" else echo "Error: 7z compression failed; leaving uncompressed folder at $dest" >&2 exit 1 end