#!/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"