From de577c1263359d0634bf69a4d0403a8cd387bc0f Mon Sep 17 00:00:00 2001 From: tengel Date: Wed, 20 Mar 2024 11:04:51 -0500 Subject: [PATCH] adding gitsnips.sh --- gitsnips.sh | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100755 gitsnips.sh diff --git a/gitsnips.sh b/gitsnips.sh new file mode 100755 index 0000000..314b40b --- /dev/null +++ b/gitsnips.sh @@ -0,0 +1,214 @@ +#!/usr/bin/env bash +# +# Backup GitLab snippets to combined repo using git subtrees +# +# Required: +# +# - GitLab personal API token with read_api privilege +# - Automated git project for storing snippets +# - curl, git, jq, find, mktemp +# +# tl;dr +# +# 1. Create GitLab API token with 'read_api' access (private snippets) +# 2. Create a new private gitlab project, then clone and configure +# +# git clone git@gitlab.com:username/snippets.git +# cd snippets +# git config user.name "username" +# git config user.email "username@email.com" +# +# 3. If using multiple GitLab accounts/SSH keys: +# +# git config core.sshCommand "ssh -i ~/.ssh/username -F /dev/null" +# +# 4. Specify the token and project directory with '-t ... -d ...' +# +# ./gitsnips.sh -t ABCDEF1234 -d ../snippets/ -p +# +# 5. Review the above changes and `git push` the results when ready +# +# SPDX-License-Identifier: MIT + +_VERSION="0.0.3" +_NAME=$(basename "${0}") + +# usage +function usage() { + echo "${_NAME} version ${_VERSION}" + echo + echo "Usage: ${_NAME} [OPTIONS...]" + echo + echo " Options:" + echo " -d Git project directory (default: ./)" + echo " -t GitLab token (read_api)" + echo " -i Markdown index file (default: INDEX.md)" + echo " -s Snippets JSON file (default: snippets.json)" + echo " -p Prune removed snippets (git rm -r)" + echo " -q Suppress output (quiet)" + echo " -V Version of program" + echo " -h This help text" + echo +} + +# defaults +declare _GITDIR="./" +declare _TOKEN="" +declare _MDIDX="INDEX.md" +declare _SNJSON="snippets.json" +declare _PRUNE=0 +declare _VERBOSE=1 + +# user input +while getopts "d:t:i:s:pqVh" opt; do + case "$opt" in + d) + _GITDIR="${OPTARG}" ;; + t) + _TOKEN="${OPTARG}" ;; + i) + _MDIDX="${OPTARG}" ;; + s) + _SNJSON="${OPTARG}" ;; + p) + _PRUNE=1 ;; + q) + _VERBOSE=0 ;; + V) + echo "${_NAME} version ${_VERSION}" + exit 0 ;; + h) + usage + exit 0 ;; + *) + echo "Unrecognized option: $OPTARG (Run '$_NAME -h' for help)" + exit 1 ;; + esac +done +shift $((OPTIND-1)) + +# handy for verbose/quiet +function noise { + local _MSG="$*" + if [[ ${_VERBOSE} -eq 1 ]]; then + echo "${_MSG}" + fi +} + +# loop for checking apps +function checkapp() { + local _XAPPX="$1" + if [[ -z "$(command -v ${_XAPPX} 2>/dev/null)" ]]; then + noise "Application ${_XAPPX} not found, exiting." + exit 99 + fi +} + +# loop for checking directories +function checkdir() { + local _XDIRX="$1" + if [[ ! -d "${_XDIRX}" ]]; then + noise "Directory ${_XDIRX} not found, exiting." + exit 98 + fi + if [[ ! -w "${_XDIRX}" ]]; then + noise "Directory ${_XDIRX} not writable, exiting." + exit 97 + fi + _GDIRG="${_XDIRX%/}/.git" + if [[ ! -d "${_GDIRG}" ]]; then + noise "Directory ${_XDIRX} is not a git project, exiting." + exit 96 + fi +} + +# ensure we can run properly +function preflight() { + # apps + checkapp curl + checkapp jq + # dirs + checkdir "${_GITDIR}" + # token + if [[ -z "${_TOKEN}" ]]; then + noise "Token length zero; valid token required, exiting." + exit 9 + fi +} + +# this could exit +preflight + +# prep for work +noise "Using ${_GITDIR} as git repository" +pushd "${_GITDIR}" >/dev/null + +# fetch latest index +noise "Fetching latest snippet index" +curl --silent --header "PRIVATE-TOKEN: ${_TOKEN}" \ + "https://gitlab.com/api/v4/snippets?per_page=65534" \ + | jq > "${_SNJSON}" + +git add "${_SNJSON}" +git commit -q -m "snippet list update" "${_SNJSON}" + +# generate list of IDs +noise "Extracting list of snippets with jq" +# create an array string [1234]="title" for each item... +IDLIST=$(jq -r '.[] | "[\(.id)]=\"\(.title)\""' "${_SNJSON}") +# ...adding () around it creates a natural array input string +declare -A IDARRAY=$(echo "(${IDLIST})") + +# human friendly index +# - 'git subtree' needs a clean working directory +_TMPIDX=$(mktemp) +echo -e "# Snippet Index\n" > "${_TMPIDX}" + +# loop the IDs and either update or add +IDSORTED=$(echo "${!IDARRAY[@]}" | tr ' ' '\n' | sort -n | tr '\n' ' ') +# do not quote IDSORTED below +for id in ${IDSORTED}; do + if [[ -d "./${id}" ]]; then + noise "Updating snippet ${id}" + git subtree pull -q -P "${id}" -m "Updating ${id}" \ + "git@gitlab.com:snippets/${id}.git" HEAD + else + noise "Adding snippet ${id}" + git subtree add -q -P "${id}" -m "Adding ${id}" \ + "git@gitlab.com:snippets/${id}.git" HEAD + fi + echo " * [${id}](${id}) ${IDARRAY[$id]}" >> "${_TMPIDX}" + # be nice to gitlab + sleep 1 +done + +# commit the updated Markdown index +noise "Building ${_MDIDX}" +mv -f "${_TMPIDX}" "${_MDIDX}" +git add "${_MDIDX}" +git commit -q -m "markdown index update" "${_MDIDX}" + +# prune stale items +if [[ ${_PRUNE} -eq 1 ]]; then + noise "Pruning stale snippets" + _RGX='^[0-9]+$' + # this gives a list of top level dirs without "./" or any hidden subdirs + _LDIRS=$(find ./ -mindepth 1 -maxdepth 1 -type d -not -path '*/\.*' -printf '%P\n') + for dir in ${_LDIRS}; do + if [[ ! ${dir} =~ ${_RGX} ]]; then + # directory is not a number + noise "Directory ${dir} is NaN, skipping" + continue + fi + if [[ ! ${IDARRAY[$dir]+_} ]]; then + # if dir is not an array key, remove it + noise "Pruning ${dir} not in snippet index" + git rm -r ${dir} + git commit -q -m "Removing ${dir}" ${dir} + fi + done +fi + +# back to where we started +popd >/dev/null +noise "Done - review results and push updates to origin as appropriate"