git-wad

Manage files via git but not their content
git clone git://git.meso-star.fr/git-wad.git
Log | Files | Refs | README | LICENSE

git-wad (21964B)


      1 #!/bin/sh
      2 
      3 # Copyright (C) 2023-2025 |Méso|Star> (contact@meso-star.com)
      4 #
      5 # This program is free software: you can redistribute it and/or modify
      6 # it under the terms of the GNU General Public License as published by
      7 # the Free Software Foundation, either version 3 of the License, or
      8 # (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     13 # GNU General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU General Public License
     16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
     17 
     18 set -e
     19 
     20 die()
     21 {
     22   rm -rf "${git_wad_tmpdir}" # cleanup temporary files
     23   exit "${1:-1}" # return status code (default is 1)
     24 }
     25 
     26 # Configure signal processing
     27 trap 'die $?' EXIT
     28 
     29 # Check that git-wad does not run from a .git directory
     30 is_inside_git_dir="$(git rev-parse --is-inside-git-dir)"
     31 if [ "${is_inside_git_dir}" = "true" ]; then
     32   >&2 printf 'git-wad must be run in a work tree\n'
     33   exit 1
     34 fi
     35 
     36 # Force default locale when using sed and tr, i.e. treat all characters
     37 # as individual bytes. Otherwise, some implementations return multi-byte
     38 # encoding errors.
     39 alias sed__='LC_CTYPE=C sed'
     40 alias tr__='LC_CTYPE=C tr'
     41 
     42 # Check checksum command support
     43 if ! command -v sha256sum 1> /dev/null 2>&1 \
     44 && ! command -v shasum 1> /dev/null 2>&1 \
     45 && ! command -v sha256 1> /dev/null 2>&1; then
     46   >&2 printf 'No tool to process SHA256 checksum\n'
     47   exit 1
     48 fi
     49 
     50 # Check C compiler support
     51 if command -v c99 1> /dev/null 2>&1; then
     52   CC="c99"
     53 elif command -v cc 1> /dev/null 2>&1; then
     54   CC="cc"
     55 else
     56   >&2 printf 'No C compiler\n'
     57   exit 1
     58 fi
     59 
     60 # shellcheck disable=SC2310
     61 working_tree="$(git rev-parse --show-toplevel)"
     62 git_wad_tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/git_wad_XXXXXX")"
     63 
     64 # Path toward the next_timestamp command
     65 next_timestamp="${git_wad_tmpdir}/next_timestamp"
     66 
     67 # Load local config file
     68 config_file="${working_tree}/.git_wad.cfg"
     69 if [ -f "${config_file}" ]; then
     70   # shellcheck disable=SC1090
     71   . "${config_file}"
     72 fi
     73 
     74 GIT_WAD_HEADER="#\$# git-wad"
     75 GIT_WAD_OBJDIR="${GIT_WAD_OBJDIR:-${working_tree}/.git/wad}"
     76 GIT_WAD_VERBOSE="${GIT_WAD_VERBOSE:-0}"
     77 
     78 if [ -z "${GIT_WAD_REMOTE_FETCH}" ] \
     79 && git remote | grep -qe "^origin$"; then
     80   GIT_WAD_REMOTE_FETCH="$(git config remote.origin.url || true)"
     81 fi
     82 if [ -z "${GIT_WAD_REMOTE_PUSH}" ] \
     83 && git remote | grep -qe "^origin$"; then
     84   GIT_WAD_REMOTE_PUSH="$(git config remote.origin.pushurl || true)"
     85   if [ -z "${GIT_WAD_REMOTE_PUSH}" ]; then
     86     GIT_WAD_REMOTE_PUSH="${GIT_WAD_REMOTE_FETCH}"
     87   fi
     88 fi
     89 
     90 ########################################################################
     91 # Helper functions
     92 ########################################################################
     93 synopsis()
     94 {
     95   >&2 printf "usage: git-wad checkout\n"
     96   >&2 printf "       git-wad fetch [-1a]\n"
     97   >&2 printf "       git-wad fsck [-r]\n"
     98   >&2 printf "       git-wad init\n"
     99   >&2 printf "       git-wad prune [-1a]\n"
    100   >&2 printf "       git-wad pull [-1a]\n"
    101   >&2 printf "       git-wad push [-1a\n"
    102   >&2 printf "       git-wad status [-1a]\n"
    103 }
    104 
    105 is_init()
    106 {
    107      git config --get filter.wad.clean > /dev/null \
    108   && git config --get filter.wad.smudge > /dev/null
    109 }
    110 
    111 sizeof_header()
    112 {
    113   printf "%s" "${GIT_WAD_HEADER}" | wc -c
    114 }
    115 
    116 encode() # digest, size
    117 {
    118   printf "%s %s %d" "${GIT_WAD_HEADER}" "$1" "$2"
    119 }
    120 
    121 # List hashes of WAD file from the current HEAD
    122 wad_hashes()
    123 {
    124   # Regular expression of a WAD
    125   wad_re="${GIT_WAD_HEADER} [0-9a-z]\{64\} [0-9]\{1,\}"
    126 
    127   # The following command line can be translated as follows:
    128   #   Print the IDs of all objects in HEAD
    129   # | Get their hash
    130   # | Print object information of these hashes
    131   # | Keep only the hash of objects that are blobs
    132   # | Print blob content preceeded by its hash
    133   # | Print the blob hash if the following line indicates a WAD
    134    git rev-list --objects HEAD -1 \
    135  | cut -d' ' -f1 \
    136  | git cat-file --batch-check \
    137  | sed__ -n 's/\(^[a-z0-9]\{40\}\) blob [0-9]\{1,\}$/\1/p' \
    138  | git cat-file --batch='%(objectname)' \
    139  | sed__ -n "/^[0-9a-z]\{40\}$/{N;s/^\([0-9a-z]\{40\}\)\n${wad_re}$/\1/p;}"
    140 }
    141 
    142 # List paths of WAD file from the current commit
    143 wad_paths()
    144 {
    145   # Store the hash of WAD file
    146   hashes="${git_wad_tmpdir}/hashes"
    147   wad_hashes | sort -b > "${hashes}"
    148 
    149   #   Lists the current tree. Ensure verbatim filename (-z option)
    150   # | Ensure one entry per line (see the output section of git-ls-tree(1))
    151   # | Print only the hash and path of listed files
    152   # | Sort the result in ascending order
    153   # | Print path for WAD files only
    154     git ls-tree -zr HEAD "${working_tree}" \
    155   | tr__ '\0' '\n' \
    156   | cut -d' ' -f3- \
    157   | sort -b \
    158   | join -t'	' -o 2.2 "${hashes}" -
    159 }
    160 
    161 # List WAD objects from the current working tree
    162 wad_objects() # [-1a]
    163 {
    164   rev="HEAD" # Revision
    165 
    166   OPTIND=1
    167   while getopts ":a1" opt; do
    168     case "${opt}" in
    169       1) rev="HEAD -1" ;; # Last commit only
    170       a) rev="--all" ;;
    171       *)
    172         >&2 printf "Invalid option -- %s\n" "${OPTARG}"
    173         return 1
    174         ;;
    175     esac
    176   done
    177 
    178   # The following command line can be translated as follows:
    179   #   Print the IDs of all objects in "${rev}" (i.e. HEAD or --all)
    180   # | Get their hash
    181   # | Print object information of these hashes
    182   # | Keep only the hash of objects that are blobs
    183   # | Print information _and_ contents of these hashes
    184   # | Finally print digest of WAD object
    185   # shellcheck disable=SC2086 # ${rev} can have several arguments
    186    git rev-list --objects ${rev} \
    187  | cut -d' ' -f1 \
    188  | git cat-file --batch-check \
    189  | sed__ -n 's/\(^[a-z0-9]\{40\}\) blob [0-9]\{1,\}$/\1/p' \
    190  | git cat-file --batch \
    191  | sed__ -n "s/^${GIT_WAD_HEADER} \([0-9a-z]\{64\}\) [0-9]\{1,\}$/\1/p"
    192 }
    193 
    194 # List WAD objects staged for the next commit
    195 wad_objects_staged()
    196 {
    197     git diff --cached \
    198   | sed__ -n "s/^+${GIT_WAD_HEADER} \([0-9a-z]\{64\}\) [0-9]\{1,\}$/\1/p"
    199 }
    200 
    201 # List all stored WAD objects of the working tree. These may be WAD
    202 # objects from the current HEAD, WAD objects to be cleaned up, or dummy
    203 # files.
    204 all_objects()
    205 {
    206     find "${GIT_WAD_OBJDIR}" ! -path "${GIT_WAD_OBJDIR}" -prune -type f \
    207   | grep -e "${GIT_WAD_OBJDIR}/[0-9a-z]\{64\}" \
    208   | xargs -I {} basename {}
    209 }
    210 
    211 # List of all locally stored WAD objects to be pruned, i.e. not
    212 # referenced in the current work tree.
    213 unreferenced_objects() # [-1a]
    214 {
    215   # Lists all locally stored WAD objects in tmpfile
    216   local_wads="${git_wad_tmpdir}/local_wads"
    217   all_objects | sort > "${local_wads}"
    218 
    219   # List WAD objects in the current working tree or to commit
    220   wads="${git_wad_tmpdir}/wads"
    221   # shellcheck disable=SC2310
    222   { wad_objects "$@" && wad_objects_staged; } > "${wads}"
    223 
    224   # The following command line is translated as follows:
    225   #   Sort WAD objects in ascending order
    226   # | Subtract them from the list of locally stored WAD objects
    227   sort "${wads}" | comm -23 "${local_wads}" -
    228 }
    229 
    230 objects_to_push() # [-1a]
    231 {
    232   wads="${git_wad_tmpdir}/wad_objects"
    233   # shellcheck disable=SC2310
    234   wad_objects "$@" > "${wads}"
    235 
    236   xargs -I {} sh -c \
    237   "if [ -f \"${GIT_WAD_OBJDIR}/\$1\" ]; then echo \"\$1\"; fi" -- {} \
    238   < "${wads}"
    239 }
    240 
    241 objects_to_fetch() # [-1a]
    242 {
    243   wads="${git_wad_tmpdir}/wad_objects"
    244   # shellcheck disable=SC2310
    245   wad_objects "$@" > "${wads}"
    246 
    247   xargs -I {} sh -c \
    248   "if [ ! -f \"${GIT_WAD_OBJDIR}/\$1\" ]; then echo \"\$1\"; fi" -- {} \
    249   < "${wads}"
    250 }
    251 
    252 # Build the next_timestamp command, which returns the timestamp one
    253 # second into the future. The date is formatted as expected by the
    254 # restore function (see below), i.e. as the date "+%Y%m%D%H%M.%S" does
    255 # for the current date.
    256 restore_init()
    257 {
    258 
    259   # In shell, there is no standard method for converting a number of
    260   # seconds from January 1, 1970 to midnight, into a specific format
    261   # like the date command does for the current time. Some awk
    262   # implementations provide the strftime function, but others do not.
    263   # That said, the c99 C compiler has been standard since POSIX 2001,
    264   # and the strftime function in charge of converting a date is a
    265   # function of the standard C library. A C program can therefore be
    266   # used to format a date on any POSIX-compatible system. This is the
    267   # purpose of the program below.
    268   #
    269   # Note that it would be more efficient to pre-build the program and
    270   # install it with git-wad. Its on-demand compilation nevertheless
    271   # keeps the design more concise (everything is done in one place),
    272   # while the compilation overhead should remain negligible overall. All
    273   # the more so with several files to restore. Hence this choice of
    274   # implementation, until performance problems really came to light.
    275   cat << EOF > "${next_timestamp}.c"
    276 #define _POSIX_C_SOURCE 200809L
    277 #include <time.h>
    278 #include <stdio.h>
    279 
    280 int main(void)
    281 {
    282   char buf[32];
    283   time_t epoch;
    284   struct tm* tm;
    285 
    286   if((epoch = time(NULL)) == (time_t)-1) return 1;
    287   ++epoch;
    288   if((tm = localtime(&epoch)) == NULL) return 1;
    289   if((strftime(buf, sizeof(buf), "%Y%m%d%H%M.%S", tm)) == 0) return 1;
    290   printf("%s\n", buf);
    291   return 0;
    292 }
    293 EOF
    294   "${CC}" "${next_timestamp}.c" -o "${next_timestamp}" > /dev/null 2>&1
    295 }
    296 
    297 restore() # WAD file
    298 {
    299   wad="$1"
    300   digest=$(sed__ "s/^${GIT_WAD_HEADER} \([0-9a-z]\{64\}\) [0-9]\{1,\}$/\1/" "${wad}")
    301 
    302   if [ -z "${digest}" ]; then
    303     >&2 printf "Invalid WAD file %s\n" "$1"
    304     return 1
    305   fi
    306 
    307   if [ ! -f "${GIT_WAD_OBJDIR}"/"${digest}" ]; then
    308     >&2 printf "WAD object unavailable %s %s\n" "${digest}" "${wad}"
    309   else
    310     >&2 printf "Restoring %s\n" "${wad}"
    311 
    312     # Forces re-execution of the smudge filter to restore the WAD file.
    313     # Note that git caches the state of the last time the file was
    314     # smudged. And there's no specific way to invalidate this cache. The
    315     # easiest way seems to be to update the file's modification time by
    316     # touching it. But you have to make sure that this checkout doesn't
    317     # occur in the same second as the previous one. This is why we
    318     # explicitly set the file's timestamp to the next timestamp, i.e.
    319     # the timestamp one second in the future.
    320     timestamp="$("${next_timestamp}")"
    321     touch -t "${timestamp}" "${wad}"
    322     git checkout-index --index --force "${wad}"
    323   fi
    324 }
    325 
    326 checksum() # [-c]
    327 {
    328   if command -v sha256sum 1> /dev/null 2>&1; then
    329     sha256sum "$@"
    330 
    331   elif command -v shasum 1> /dev/null 2>&1; then
    332     shasum -a 256 "$@"
    333 
    334   elif command -v sha256 1> /dev/null 2>&1; then
    335 
    336     # Calculate checksum
    337     if [ "$#" -eq 0 ] || [ "$1" != "-c" ]; then
    338         sha256
    339 
    340     # Check checksum
    341     else
    342       shift 1 # Discard the -c option
    343 
    344       while read -r i; do
    345         ref="$(echo "${i}" | cut -d' ' -f1)"
    346         file="$(echo "${i}" | cut -d' ' -f3)"
    347 
    348         sum="$(sha256 < "${file}")"
    349         if [ "${ref}" = "${sum}" ]; then
    350           printf '%s: OK\n' "${file}"
    351         else
    352           printf '%s: FAILED\n' "${file}"
    353         fi
    354       done
    355     fi
    356   fi
    357 }
    358 
    359 ########################################################################
    360 # Git filters (plumbing)
    361 ########################################################################
    362 log() # str [, arg...]
    363 {
    364   if [ -n "${GIT_WAD_VERBOSE}" ] && [ ! "${GIT_WAD_VERBOSE}" -eq 0 ]; then
    365     # shellcheck disable=SC2059
    366     >&2 printf "$@"
    367   fi
    368 }
    369 
    370 clean() # stdin
    371 {
    372   tmpclean="${git_wad_tmpdir}/tmpclean"
    373   digest=$(cat - | tee "${tmpclean}" | checksum | cut -d' ' -f1)
    374   size=$(wc -c < "${tmpclean}")
    375 
    376   # Copy all bytes that could correspond to a WAD header. Note that null
    377   # bytes are replaced by a 0 to avoid a warning about ignored null
    378   # bytes: they cannot be stored in a variable. Replacing them is not a
    379   # problem, since the header string you're looking for doesn't contain
    380   # any. You just have to be careful not to replace them with a
    381   # character belonging to the WAD header, which could lead to random
    382   # bytes being mistakenly translated into a WAD header, as searched for
    383   # in the following.
    384   header_size="$(sizeof_header)"
    385   header="$(dd ibs=1 count="${header_size}" if="${tmpclean}" 2>/dev/null\
    386     | tr__ '\0' '0')"
    387 
    388   # Do not clean input stream if it is an un-smudged WAD
    389   if [ "${header}" = "${GIT_WAD_HEADER}" ]; then
    390     cat "${tmpclean}"
    391     return 0
    392   fi
    393 
    394   # The input stream is already managed by git-wad
    395   if [ -f "${GIT_WAD_OBJDIR}/${digest}" ]; then
    396     log "git-wad:filter-clean: cache already exists %s\n"\
    397       "${GIT_WAD_OBJDIR}/${digest}"
    398 
    399   # Store input stream to a file whose name is the stream digest
    400   else
    401     chmod 444 "${tmpclean}"
    402     mv "${tmpclean}" "${GIT_WAD_OBJDIR}/${digest}"
    403     log "git-wad:filter-clean: caching to %s\n" \
    404       "${GIT_WAD_OBJDIR}/${digest}"
    405   fi
    406 
    407   encode "${digest}" "${size}"
    408 }
    409 
    410 smudge() # stdin
    411 {
    412   header_size="$(sizeof_header)"
    413   header="$(dd ibs=1 count="${header_size}" 2> /dev/null | tr__ '\0' '0')"
    414 
    415   if [ "${header}" != "${GIT_WAD_HEADER}" ]; then  # It is not a WAD
    416     log "git-wad:filter-smudge: not a managed file"
    417     printf "%s" "${header}"
    418     cat -
    419 
    420   else  # It is a WAD
    421     # The sed directive remove the space before the digest
    422     digest_size="$(cat - | sed__ "1s/^ //")"
    423 
    424     digest="$(echo "${digest_size}" | cut -d' ' -f1)"
    425     size="$(echo "${digest_size}" | cut -d' ' -f2)"
    426     object="${GIT_WAD_OBJDIR}/${digest}"
    427 
    428     if [ -f "${object}" ]; then
    429       log "git-wad:filter-smudge: restoring from %s\n" "${object}"
    430       cat "${object}"
    431     else
    432       log "git-wad:filter-smudge: WAD object is missing %s\n" "${object}"
    433       encode "${digest}" "${size}"
    434     fi
    435   fi
    436 }
    437 
    438 ########################################################################
    439 # Git sub commands (porcelain)
    440 ########################################################################
    441 # Setup git filters
    442 init()
    443 {
    444   # shellcheck disable=SC2310
    445   if is_init; then
    446     >&2 printf "git-wad is already initialized (check .git/config)\n"
    447   else
    448     git config filter.wad.clean "git-wad filter-clean"
    449     git config filter.wad.smudge "git-wad filter-smudge"
    450     >&2 printf "git-wad is initialized\n"
    451   fi
    452 }
    453 
    454 # Transfert WAD objects to remote server
    455 push() # [-1a]
    456 {
    457   if [ -z "${GIT_WAD_REMOTE_PUSH}" ]; then
    458     >&2 printf "Remote undefined, i.e. variable GIT_WAD_REMOTE_PUSH is empty\n"
    459     exit 1
    460   fi
    461 
    462   >&2 printf "Pushing to %s\n" "${GIT_WAD_REMOTE_PUSH}"
    463 
    464   remote="${GIT_WAD_REMOTE_PUSH#file://}"
    465 
    466   objects_to_push="${git_wad_tmpdir}/objects_to_push"
    467   # shellcheck disable=SC2310
    468   objects_to_push "$@" > "${objects_to_push}"
    469 
    470   rsync -av --progress --ignore-existing \
    471   --files-from=- "${GIT_WAD_OBJDIR}" "${remote}" < "${objects_to_push}"
    472 }
    473 
    474 # Download WAD objects from remote server
    475 fetch() # [-1a]
    476 {
    477   if [ -z "${GIT_WAD_REMOTE_FETCH}" ]; then
    478     >&2 printf "Remote undefined, i.e. variable GIT_WAD_REMOTE_FETCH is empty\n"
    479     exit 1
    480   fi
    481 
    482   objects_to_fetch="${git_wad_tmpdir}/objects_to_fetch"
    483   objects_to_fetch "$@" > "${objects_to_fetch}"
    484 
    485   >&2 printf "Fetching from %s\n" "${GIT_WAD_REMOTE_FETCH}"
    486 
    487   # Use curl to download WAD objects via http[s] or gopher[s] protocol
    488   if echo "${GIT_WAD_REMOTE_FETCH}" | grep -q \
    489   -e "^http[s]\{0,1\}://" \
    490   -e "^gopher[s]\{0,1\}://"; then
    491     xargs -I {} curl -w "{}\n" \
    492       -o "${GIT_WAD_OBJDIR}/{}"  "${GIT_WAD_REMOTE_FETCH}/{}" \
    493       < "${objects_to_fetch}"
    494 
    495   # By default transfert WAD objects by rsync
    496   else
    497     remote="${GIT_WAD_REMOTE_FETCH#file://}"
    498     rsync -av --progress --ignore-existing \
    499       --files-from=- "${remote}" "${GIT_WAD_OBJDIR}" \
    500       < "${objects_to_fetch}"
    501   fi
    502 }
    503 
    504 # Check the integrity of the stored WAD content
    505 fsck()
    506 {
    507   remove=0
    508 
    509   OPTIND=1
    510   while getopts ":r" opt; do
    511     case "${opt}" in
    512       r) remove=1 ;;
    513       *)
    514         >&2 printf "Invalid option -- %s\n" "${OPTARG}"
    515         return 1
    516         ;;
    517     esac
    518   done
    519 
    520   wads="${git_wad_tmpdir}/wads"
    521   all_objects > "${wads}"
    522   n=$(wc -l "${wads}" | cut -d' ' -f1)
    523   [ "${n}" -eq 0 ] && return # No WAD, i.e. nothing to do
    524 
    525   # Prepare checksum verification of WADs, i.e. list one line per file
    526   # starting with the WAD's checksum (actually its name) followed by 2
    527   # spaces and its path.
    528   sums="${git_wad_tmpdir}/sums"
    529   xargs -I{} printf '%s  %s/%s\n' "{}" "${GIT_WAD_OBJDIR}" "{}" \
    530     < "${wads}" > "${sums}"
    531 
    532   # Check WAD checksum.
    533   #
    534   # Redirect the error stream to standard output to ensure that messages
    535   # are printed in the order in which they are sent. By using a pipe,
    536   # the shell starts a new process for the tee command which, if it only
    537   # processes normal messages, will intertwine with the error messages
    538   # from the checksum command.
    539   result="${git_wad_tmpdir}/result"
    540   checksum -c < "${sums}" 2>&1 | tee "${result}"
    541 
    542   # Remove corrupted files
    543   if [ ! "${remove}" -eq 0 ]; then
    544     corrupted_wads="${git_wad_tmpdir}/corrupted_wads"
    545     sed__ -n 's/^\(.\+\): FAILED$/\1/p' "${result}" > "${corrupted_wads}"
    546     sed__ 's/^/remove /g' "${corrupted_wads}"
    547     xargs -I{} rm -f "{}" < "${corrupted_wads}"
    548   fi
    549 }
    550 
    551 # Restore the content of WAD files
    552 checkout()
    553 {
    554   # shellcheck disable=SC2310
    555   if ! is_init; then
    556     >&2 printf "\e[0;31mgit-wad is not initialized\e[0m\n"
    557     >&2 printf "  (use \"git wad init\" to enable WAD management)\n"
    558     exit 1
    559   fi
    560 
    561   restore_init
    562 
    563   #   Lists the files. Ensure verbatim filename (-z option)
    564   # | Ensure one entry per line (see the output section of git-ls-tree(1))
    565   # | Restore content
    566     git ls-files -z "${working_tree}" \
    567   | tr__ '\0' '\n' \
    568   | while read -r i; do
    569     # Search for the git-wad header only in the first few bytes of the
    570     # file to avoid unnecessarily processing all of the data; the
    571     # git-wad header appears at the very beginning of the file, and once
    572     # restored, searching the entire file can take a long time because
    573     # it can be very large. Thus, considering only a few bytes is not
    574     # only reliable, but also significantly speeds up the "checkout"
    575     # command.
    576     bytes=$(dd if="${i}" bs=128 count=1 2> /dev/null | tr__ '\0' '0')
    577     if printf '%s' "${bytes}" \
    578      | grep -qe "^${GIT_WAD_HEADER} [0-9a-z]\{64\} [0-9]\{1,\}$"
    579     then
    580       restore "${i}"
    581     fi
    582   done
    583 }
    584 
    585 # Download WAD objects from remote server and restore the content of
    586 # working WAD files.
    587 pull() # [-1a]
    588 {
    589   fetch "$@"
    590   checkout
    591 }
    592 
    593 # Delete WAD objects not used in the current work tree from local
    594 # storage
    595 prune() # [-1a]
    596 {
    597   unreferenced="${git_wad_tmpdir}/unreferenced_objects"
    598   unreferenced_objects "$@" > "${unreferenced}"
    599 
    600   xargs -I {} sh -c \
    601     ">&2 printf \"Removing %s\n\" \"${GIT_WAD_OBJDIR}/\$1\"; \
    602     rm -f \"${GIT_WAD_OBJDIR}/\$1\"" -- {} < "${unreferenced}"
    603 }
    604 
    605 # Print WAD management status
    606 status() # [-1a]
    607 {
    608   # First, report the static of git wad initialization.
    609   # shellcheck disable=SC2310
    610   if ! is_init; then
    611     printf "\e[0;31mgit-wad is not initialized\e[0m\n"
    612     printf "  (use \"git wad init\" to enable WAD management)\n"
    613     printf "\n"
    614   fi
    615 
    616   # Check that a commit exists, otherwise there's no active HEAD and
    617   # therefore no more status to report. In fact, the function would
    618   # return an error if it queried the HEAD object.
    619   if ! git rev-parse HEAD > /dev/null 2>&1; then
    620     >&2 printf "No commits yet\n"
    621     return
    622   fi
    623 
    624   # Create 3 temp files to list resolved, unrestored and orphaned WADs
    625   resolved="${git_wad_tmpdir}/resolved"
    626   unrestored="${git_wad_tmpdir}/unrestored"
    627   orphaned="${git_wad_tmpdir}/orphaned"
    628   wad_paths | while read -r wad; do
    629     # Read the WAD bytes corresponding to its header before it is restored
    630     header_size="$(sizeof_header)"
    631     header="$(dd if="${wad}" ibs=1 count="${header_size}" 2> /dev/null \
    632       | tr__ '\0' '0')"
    633 
    634     # The header read is not a valid WAD header, i.e. the WAD has
    635     # already been restored
    636     if [ "${header}" != "${GIT_WAD_HEADER}" ]; then
    637       printf "%s\n" "${wad}" >> "${resolved}"
    638 
    639     else
    640       # Read the WAD digest from its header
    641       digest=$(sed__ \
    642         -e "1s/^${GIT_WAD_HEADER} \([0-9a-z]\{64\}\) [0-9]\{1,\}$/\1/" \
    643         "${wad}")
    644 
    645       # Check whether a file corresponding to the WAD digest exists
    646       # locally. If so, the WAD file is simply not restored. Otherwise,
    647       # it is orphaned of its data.
    648       if [ -f "${GIT_WAD_OBJDIR}/${digest}" ]; then
    649         printf "%s\n" "${wad}" >> "${unrestored}"
    650       else
    651         printf "%s\n" "${wad}" >> "${orphaned}"
    652       fi
    653     fi
    654   done
    655 
    656   # List resolved WADs, if any
    657   if [ -s "${resolved}" ]; then
    658     printf "Resolved WADs:\n"
    659     sort "${resolved}" | sed 's/./\\&/g' \
    660       | xargs -I{} printf "\t\e[0;32m%s\e[0m\n" {}
    661     printf "\n"
    662   fi
    663 
    664   # List un-restored WADs, if any
    665   if [ -s "${unrestored}" ]; then
    666     printf "Unrestored WADs:\n"
    667     printf "  (use \"git wad checkout\" to restore WADs)\n"
    668     sort "${unrestored}" | sed 's/./\\&/g' \
    669       | xargs -I {} printf "\t\e[0;33m%s\e[0m\n" {}
    670     printf "\n"
    671   fi
    672 
    673   # List orphaned WADs, if any
    674   if [ -s "${orphaned}" ]; then
    675     printf "Orphaned WADs:\n"
    676     printf "  (use \"git wad pull\" to download and restore WADs)\n"
    677     sort "${orphaned}" | sed 's/./\\&/g' \
    678       | xargs -I {} printf "\t\e[0;31m%s\e[0m\n" {}
    679     printf "\n"
    680   fi
    681 
    682   # Print number of WADs not referenced in current work tree
    683   unreferenced="${git_wad_tmpdir}/unreferenced"
    684   unreferenced_objects "$@" > "${unreferenced}"
    685   n="$(wc -l < "${unreferenced}" | cut -d' ' -f1)"
    686   if [ "${n}" -gt 0 ]; then
    687     printf "There are %d WADs not use in the current working tree\n" "${n}"
    688     printf "  (use \"git wad prune\" to remove them)\n"
    689   fi
    690 }
    691 
    692 ########################################################################
    693 # The command
    694 ########################################################################
    695 mkdir -p "${GIT_WAD_OBJDIR}"
    696 
    697 sub_cmd="$1"
    698 case "${sub_cmd}" in
    699   "checkout") shift 1; checkout ;;
    700   "fetch") shift 1; fetch "$@" ;;
    701   "filter-clean")  shift 1; clean "$@" ;;
    702   "filter-smudge") shift 1; smudge "$@" ;;
    703   "fsck") shift 1; fsck "$@" ;;
    704   "init") shift 1; init ;;
    705   "prune") shift 1; prune "$@" ;;
    706   "push") shift 1; push "$@" ;;
    707   "pull") shift 1; pull "$@" ;;
    708   "status") shift 1; status "$@" ;;
    709   *) synopsis; exit 1 ;;
    710 esac