Last active
February 5, 2025 00:33
-
-
Save loopyd/874aef3467a1129050f205dd61c0cd36 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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