Skip to content

Instantly share code, notes, and snippets.

@loopyd
Last active February 5, 2025 00:33
Show Gist options
  • Save loopyd/874aef3467a1129050f205dd61c0cd36 to your computer and use it in GitHub Desktop.
Save loopyd/874aef3467a1129050f205dd61c0cd36 to your computer and use it in GitHub Desktop.
#! /usr/bin/env bash
# breathmint.sh - "You need a breath mint!"
# [email protected]
# Get a fresh copy of a git repository in a specified folder, every time, and inject an .env file placed next to for config
# Checks
if [ "$EUID" -eq 0 ]; then
echo "This script must not be run as root" >&2
exit 1
fi
if [ "${BASH_SOURCE[0]}" != "$0" ]; then
echo "This script must not be sourced" >&2
exit 1
fi
CSCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# Environment variables
PROJECT_DIR=${PROJECT_DIR:-"$CSCRIPT_DIR/project"}
QUIET=${QUIET:-0}
LOG_FILE=${LOG_FILE:-"${CSCRIPT_DIR}/run.log"}
LOG_LEVEL=${LOG_LEVEL:-6}
GIT_URL=${GIT_URL:-"https://[email protected]:you/your_repo.git"}
GIT_BRANCH=${GIT_BRANCH:-"main"}
ENV_FILE=${ENV_FILE:-"${CSCRIPT_DIR}/.env"}
# Colors
C_RED="\033[0;31m"
C_GREEN="\033[0;32m"
C_YELLOW="\033[0;33m"
C_BLUE="\033[0;34m"
C_MAGENTA="\033[0;35m"
C_CYAN="\033[0;36m"
C_RESET="\033[0m"
C_BOLD="\033[1m"
C_UNDERLINE="\033[4m"
log() {
local _level="$1"
local _message="$2"
local _prefix _timestamp _target _color _target _level_num
_timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case $_level in
1) _prefix="DEBUG"; _color="${C_MAGENTA}"; _target="1"; _level_num=6 ;;
2) _prefix="INFO"; _color="${C_BLUE}"; _target="1"; _level_num=5 ;;
3) _prefix="SUCCESS"; _color="${C_GREEN}"; _target="1"; _level_num=4 ;;
4) _prefix="WARNING"; _color="${C_YELLOW}"; _target="2"; _level_num=3 ;;
5) _prefix="ERROR"; _color="${C_RED}"; _target="2"; _level_num=2 ;;
6) _prefix="CRITICAL"; _color="${C_RED}"; _target="2"; _level_num=1 ;;
*)
echo "Invalid log level. Valid levels: debug, info, success, warning, error, critical" >&2
return 1
;;
esac
while IFS=$'\n' read -r _line; do
_line=$(echo "$_line" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$LOG_FILE" ]; then
echo -e "[${_timestamp}] [${_prefix}] ${_line}" >> "${LOG_FILE}"
fi
if [[ $((_level_num)) -le $((LOG_LEVEL)) ]]; then
echo -e "${_color}[${_timestamp}] ${C_BOLD}[${_prefix}]${C_RESET} ${_color}${_line}${C_RESET}" >&${_target}
fi
done <<< "$_message"
}
critical() {
log 6 "$1"
return $?
}
error() {
log 5 "$1"
return $?
}
warning() {
log 4 "$1"
return $?
}
success() {
log 3 "$1"
return $?
}
info() {
log 2 "$1"
return $?
}
debug() {
log 1 "$1"
return $?
}
exit_trap() {
local _exit_code="$?"
if [ "$_exit_code" -ne 0 ]; then
error "Exited with code $_exit_code"
fi
exit "$_exit_code"
}
trap exit_trap EXIT SIGINT SIGTERM SIGQUIT
check_cmd() {
local _command="$1"
if ! command -v "$_command" &> /dev/null; then
debug "check_cmd: $_command could not be found"
return 1
fi
debug "check_cmd: $_command found"
return 0
}
run_cmd() {
local _command="$1"
local _exit_code=0
trim() {
local var="$*"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
echo -n "$var"
}
if [ -z "$_command" ]; then
error "run_cmd: command is required"
return 1
fi
while [ -n "$_command" ]; do
local _next_connector=""
local _current_cmd=""
if [[ "$_command" =~ (.*?)(\&\&|\|\||;)(.*) ]]; then
_current_cmd=$(trim "${BASH_REMATCH[1]}")
_next_connector="${BASH_REMATCH[2]}"
_command=$(trim "${BASH_REMATCH[3]}")
else
_current_cmd=$(trim "$_command")
_command=""
fi
local _first_cmd _dest_file
_first_cmd=$(echo "$_current_cmd" | cut -d' ' -f1)
if [ -z "$_first_cmd" ]; then
error "run_cmd: command is required"
return 1
fi
if [ "$_first_cmd" = "sudo" ]; then
sudo -v || {
error "run_cmd: failed to elevate privileges"
return 1
}
_current_cmd=$(echo "$_current_cmd" | cut -d' ' -f2-)
if [ -z "$_current_cmd" ]; then
error "run_cmd: command is required after sudo"
return 1
fi
_first_cmd=$(echo "$_current_cmd" | cut -d' ' -f1)
if [ -z "$_first_cmd" ]; then
error "run_cmd: command is required"
return 1
fi
fi
if [ "$_first_cmd" = "source" ] || [ "$_first_cmd" = "." ]; then
_dest_file=$(echo "$_current_cmd" | cut -d' ' -f2-)
if [ -z "$_dest_file" ]; then
error "run_cmd: destination file is required"
return 1
fi
if [ ! -f "$_dest_file" ]; then
error "run_cmd: destination file $_dest_file does not exist"
return 1
fi
source "${_dest_file}" || {
error "run_cmd: failed to source $_dest_file"
return 1
}
_exit_code=0
elif [ "$_first_cmd" = "eval" ]; then
_current_cmd=$(echo "$_current_cmd" | cut -d' ' -f2-)
if [ -z "$_current_cmd" ]; then
error "run_cmd: command is required after eval"
return 1
fi
run_cmd "$_current_cmd"
_exit_code=$?
else
if ! check_cmd "$_first_cmd"; then
return 1
fi
debug "run_cmd: $_current_cmd"
eval "$_current_cmd" 2> >(while IFS= read -r line; do warning "run_cmd(stderr): $line"; done) \
1> >(while IFS= read -r line; do debug "run_cmd(stdout): $line"; done)
_exit_code=$?
fi
if [ "$_next_connector" = "&&" ]; then
if [ "$_exit_code" -ne 0 ]; then
return $_exit_code
fi
elif [ "$_next_connector" = "||" ]; then
if [ "$_exit_code" -eq 0 ]; then
return $_exit_code
fi
fi
done
return $_exit_code
}
git_is_repo() {
local _repo_dir="$1"
if [ ! -d "$_repo_dir/.git" ]; then
error "git_is_repo: $_repo_dir is not a git repository"
return 1
fi
return 0
}
git_is_on_branch() {
local _repo_dir="$1"
local _branch_name="$2"
if [ -z "$_repo_dir" ]; then
error "git_is_on_branch: repository directory is required"
return 1
fi
if [ -z "$_branch_name" ]; then
error "git_is_on_branch: branch name is required"
return 1
fi
if ! git_is_repo "$_repo_dir"; then
return 1
fi
_branch_name=$(git -C "$_repo_dir" branch --show-current)
if [ -z "$_branch_name" ]; then
error "git_is_on_branch: $_repo_dir is not on branch $_branch_name"
return 1
fi
if [ "$_branch_name" != "$_branch_name" ]; then
debug "git_is_on_branch: $_repo_dir is not on branch $_branch_name"
return 1
fi
debug "git_is_on_branch: $_repo_dir is on branch $_branch_name"
return 0
}
git_fetch() {
local _repo_dir="$1"
if [ -z "$_repo_dir" ]; then
error "git_fetch: repository directory is required"
return 1
fi
if ! git_is_repo "$_repo_dir"; then
return 1
fi
run_cmd "git -C $_repo_dir fetch" || {
error "git_fetch: failed to fetch $_repo_dir"
return 1
}
debug "git_fetch: $_repo_dir fetched in folder $_repo_dir"
return 0
}
git_pull() {
local _repo_dir="$1"
if [ -z "$_repo_dir" ]; then
error "git_pull: repository directory is required"
return 1
fi
if ! git_is_repo "$_repo_dir"; then
return 1
fi
run_cmd "git -C $_repo_dir pull --force" || {
error "git_pull: failed to pull $_repo_dir"
return 1
}
debug "git_pull: $_repo_dir pulled in folder $_repo_dir"
return 0
}
git_is_dirty() {
local _repo_dir="$1"
if [ -z "$_repo_dir" ]; then
error "git_is_dirty: repository directory is required"
return 1
fi
if ! git_is_repo "$_repo_dir"; then
return 1
fi
# Check for any changes excluding ignored and untracked files
if run_cmd "git -C $_repo_dir status --porcelain --untracked-files=no | grep -q '.'"; then
debug "git_is_dirty: $_repo_dir is dirty"
return 0
else
debug "git_is_dirty: $_repo_dir is clean"
return 1
fi
}
git_clean() {
local _repo_dir="$1"
if [ -z "$_repo_dir" ]; then
error "git_clean: repository directory is required"
return 1
fi
if ! git_is_repo "$_repo_dir"; then
return 1
fi
if git_is_dirty "$_repo_dir"; then
run_cmd "git -C $_repo_dir reset --hard" || {
error "git_clean: failed to reset $_repo_dir"
return 1
}
run_cmd "git -C $_repo_dir clean -dxf" || {
error "git_clean: failed to clean $_repo_dir"
return 1
}
else
debug "git_clean: $_repo_dir is already clean"
return 0
fi
debug "git_clean: $_repo_dir cleaned in folder $_repo_dir"
return 0
}
git_checkout() {
local _repo_dir="$1"
local _branch_name="$2"
if [ -z "$_repo_dir" ]; then
error "git_checkout: repository directory is required"
return 1
fi
if [ -z "$_branch_name" ]; then
error "git_checkout: branch name is required"
return 1
fi
if ! git_is_repo "$_repo_dir"; then
return 1
fi
if git_is_on_branch "$_repo_dir" "$_branch_name"; then
warning "git_checkout: $_repo_dir is already on branch $_branch_name"
return 0
fi
run_cmd "git -C $_repo_dir checkout $_branch_name" || {
error "git_checkout: failed to checkout $_branch_name"
return 1
}
debug "git_checkout: $_repo_dir checked out in folder $_repo_dir"
return 0
}
git_clone() {
local _repo_url="$1"
local _repo_dir="$2"
local _branch_name="$3"
if [ -z "$_repo_dir" ]; then
error "git_clone: repository directory is required"
return 1
fi
if [ -z "$_branch_name" ]; then
error "git_clone: branch name is required"
return 1
fi
if [ -z "$_repo_url" ]; then
error "git_clone: repository URL is required"
return 1
fi
if ! git_is_repo "$_repo_dir"; then
run_cmd "git clone $_repo_url $_repo_dir" || {
error "git_clone: failed to clone $_repo_url"
return 1
}
fi
if ! git_is_on_branch "$_repo_dir" "$_branch_name"; then
git_checkout "$_repo_dir" "$_branch_name" || return $?
fi
if git_is_dirty "$_repo_dir"; then
warning "git_clone: $_repo_dir is dirty, cleaning up"
git_fetch "$_repo_dir" || return $?
git_clean "$_repo_dir" || return $?
git_pull "$_repo_dir" || return $?
fi
debug "git_clone: $_repo_dir cloned in folder $_repo_dir"
return $?
}
update_env_file() {
local _env_file="$1"
local _dest_dir="$2"
local _basename_env_file
_basename_env_file=$(basename "$_env_file")
if [ ! -f "$_dest_dir/${_basename_env_file}" ]; then
run_cmd "cp $_env_file $_dest_dir/${_basename_env_file}" || {
error "Failed to copy $_env_file to $_dest_dir/${_basename_env_file}"
return 1
}
else
warning "update_env_file: $_dest_dir/${_basename_env_file} already exists, replacing"
run_cmd "cp -f $_env_file $_dest_dir/${_basename_env_file}" || {
error "Failed to copy $_env_file to $_dest_dir/${_basename_env_file}"
return 1
}
fi
if [ -f "$_dest_dir/${_basename_env_file}.example" ]; then
run_cmd "rm -f $_dest_dir/${_basename_env_file}.example" || {
error "Failed to remove $_dest_dir/${_basename_env_file}.example"
return 1
}
fi
debug "update_env_file: $_dest_dir/${_basename_env_file} updated in folder $_dest_dir"
return 0
}
info "Refreshing remote repository: $GIT_URL at branch: $GIT_BRANCH"
git_clone "$GIT_URL" "$PROJECT_DIR" "$GIT_BRANCH" || exit $?
info "Updating environment file: $ENV_FILE"
update_env_file "$ENV_FILE" "$PROJECT_DIR" || exit $?
success "Refreshed remote repository: $GIT_URL at branch: $GIT_BRANCH"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment