Skip to content

Instantly share code, notes, and snippets.

@isuke01
Last active February 19, 2026 10:40
Show Gist options
  • Select an option

  • Save isuke01/befc68905844b77abcf12f214e0818c2 to your computer and use it in GitHub Desktop.

Select an option

Save isuke01/befc68905844b77abcf12f214e0818c2 to your computer and use it in GitHub Desktop.
Auto create a branch from the current branch and push it to origin with confirmation
#!/usr/bin/env bash
# git-b β€” Auto create a branch from the current branch and push it to origin with confirmation
# Usage:
# git-b
# will get you prompt for prefix surfix and message,
# created branch will be in format: <prefix>/<surfix>/<message>
# names are auto escaped to be URL safe and lowercase, e.g. "My Feature" β†’ "my-feature"
#
# Flow:
# 1) Abort if there are uncommitted changes.
# 2) Ensure we're on stage (switch if needed).
# 3) Pull from origin/stage.
# 4) Prompt: Prefix, Suffix, Message
# 5) Create: prefix/suffix/message (all sanitized)
set -euo pipefail
# --- Ensure Git repo ---
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "❌ Error: not a git repository" >&2
exit 1
fi
# --- Colors ---
if [ -t 1 ]; then
YELLOW=$(printf '\033[1;33m')
GREEN=$(printf '\033[1;32m')
RED=$(printf '\033[1;31m')
BLUE=$(printf '\033[0;36m')
RESET=$(printf '\033[0m')
else
YELLOW=""; GREEN=""; RED=""; BLUE=""; RESET=""
fi
# --- TTY (for interactive prompts even if stdin is not a TTY) ---
TTY_IN="/dev/tty"
TTY_OUT="/dev/tty"
if [[ ! -r "$TTY_IN" || ! -w "$TTY_OUT" ]]; then
# fallback
TTY_IN="/dev/stdin"
TTY_OUT="/dev/stdout"
fi
# --- Sanitize function ---
sanitize_slug() {
local input="$1"
local out
# Normalize/strip problematic Unicode (copy/paste junk) BEFORE iconv/sed
out="$(
printf '%s' "$input" | perl -CS -pe '
s/^\s+|\s+$//g; # trim (unicode-aware)
s/[\x{200B}\x{200C}\x{200D}\x{2060}\x{FEFF}]//g; # zero-width (ZWSP/ZWNJ/ZWJ/WORD JOINER/BOM)
s/[\x{FE0E}\x{FE0F}]//g; # variation selectors (emoji style)
s/[\x{00A0}\x{202F}\x{2007}]//g; # NBSP / narrow NBSP / figure space
s/[\x{2010}\x{2011}\x{2012}\x{2013}\x{2014}\x{2212}\x{FE63}\x{FF0D}]/-/g; # unicode dashes -> "-"
'
)"
# Transliterate to ASCII (best effort)
out="$(
printf '%s' "$out" | iconv -f UTF-8 -t ASCII//TRANSLIT 2>/dev/null || printf '%s' "$out"
)"
# Hard strip to printable ASCII (also nukes weird controls like \r)
out="$(printf '%s' "$out" | LC_ALL=C tr -cd '\11\12\15\40-\176')"
# lowercase + collapse to [a-z0-9-]
out="$(printf '%s' "$out" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g' \
| sed -E 's/^-+|-+$//g' \
| sed -E 's/-+/-/g'
)"
printf '%s' "$out"
}
trim_trailing_ws() {
# removes trailing spaces/tabs/newlines + CR
printf '%s' "$1" | perl -CS -pe 's/\r//g; s/\s+\z//;'
}
normalize_suffix_input() {
local input="$1"
local cleaned ticket looks_like_url=0
# 1) Remove common invisible chars + trim
cleaned="$(
printf '%s' "$input" | perl -CS -pe '
s/[\x{200B}\x{200C}\x{200D}\x{2060}\x{FEFF}]//g; # zero-width
s/[\x{FE0E}\x{FE0F}]//g; # variation selectors
s/[\x{00A0}\x{202F}\x{2007}]//g; # NBSP variants
s/\r//g; # CR
s/^\s+|\s+$//g; # trim
'
)"
# 2) Only extract KEY-123 if it looks like a URL/path/query (typical browser address bar paste)
# You can tweak this heuristic if needed.
if printf '%s' "$cleaned" | grep -Eq '://|/browse/|[/?#]'; then
looks_like_url=1
fi
if [[ $looks_like_url -eq 1 ]]; then
ticket="$(
printf '%s' "$cleaned" \
| perl -ne 'while(/([A-Za-z][A-Za-z0-9]+-\d+)/g){$m=$1} END{print $m // ""}'
)"
if [[ -n "$ticket" ]]; then
printf '%s' "$ticket" | tr '[:lower:]' '[:upper:]'
return 0
fi
fi
# 3) Otherwise: return cleaned as-is (sanitization later will do the rest)
printf '%s' "$cleaned"
}
# --- Prefix selector (arrow navigation, reads from /dev/tty) ---
select_prefix() {
local options=("feature" "bugfix" "hubspot" "qa" "hubspot" "custom")
local idx=0
local key choice custom_input
while true; do
printf "\nSelect prefix (↑/↓, Enter):\n" >"$TTY_OUT"
while true; do
for i in "${!options[@]}"; do
if [[ $i -eq $idx ]]; then
printf " %s> %s%s\n" "$GREEN" "${options[$i]}" "$RESET" >"$TTY_OUT"
else
printf " %s\n" "${options[$i]}" >"$TTY_OUT"
fi
done
IFS= read -rsn1 key <"$TTY_IN" || true
if [[ $key == $'\x1b' ]]; then
IFS= read -rsn2 key <"$TTY_IN" || true
case "$key" in
"[A") ((idx = (idx - 1 + ${#options[@]}) % ${#options[@]})) ;; # up
"[B") ((idx = (idx + 1) % ${#options[@]})) ;; # down
esac
elif [[ $key == "" ]]; then
break
fi
# Move cursor up by number of options and clear down
printf "\033[%dA" "${#options[@]}" >"$TTY_OUT"
printf "\033[J" >"$TTY_OUT"
done
choice="${options[$idx]}"
printf "\n" >"$TTY_OUT"
if [[ "$choice" == "custom" ]]; then
while true; do
printf "Custom prefix %s(np. client, hotfix, release)%s: " "$BLUE" "$RESET" >"$TTY_OUT"
IFS= read -r custom_input <"$TTY_IN" || true
custom_input="$(sanitize_slug "$custom_input")"
if [[ -z "$custom_input" ]]; then
printf "⚠️ Custom prefix cannot be empty. Try again.\n\n" >"$TTY_OUT"
continue
fi
echo "$custom_input"
return 0
done
fi
echo "$choice"
return 0
done
}
# --- 1) Check clean working tree ---
if [[ -n "$(git status --porcelain)" ]]; then
echo "πŸ›‘ ${RED}Working tree is not clean.${RESET}"
git status --short
echo
echo "Commit/stash/discard changes first."
exit 1
fi
# --- 2) Ensure stage branch ---
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$current_branch" == "HEAD" ]]; then
echo "⚠️ Detached HEAD state."
exit 1
fi
if [[ "$current_branch" != "stage" ]]; then
echo "πŸ” Switching to ${YELLOW}stage${RESET}"
git fetch origin stage >/dev/null 2>&1 || true
if git show-ref --verify --quiet "refs/heads/stage"; then
git checkout stage
elif git show-ref --verify --quiet "refs/remotes/origin/stage"; then
git checkout -b stage origin/stage
else
echo "❌ Cannot find stage branch."
exit 1
fi
fi
# --- 3) Pull latest stage ---
echo "⬇️ Pulling ${YELLOW}origin/stage${RESET}"
if ! git pull origin stage; then
echo "πŸ›‘ ${RED}Pull failed. Resolve conflicts first.${RESET}"
exit 1
fi
if [[ -n "$(git status --porcelain)" ]]; then
echo "πŸ›‘ Repository not clean after pull."
git status --short
exit 1
fi
# --- 4) Ask for branch parts ---
prefix_in="$(select_prefix)"
printf "Suffix %s(np. 245590636737, JIRA-123)%s: " "$BLUE" "$RESET" >"$TTY_OUT"
IFS= read -r suffix_in <"$TTY_IN" || true
suffix_in="$(trim_trailing_ws "$suffix_in")"
while true; do
printf "Message %s(short description)%s: " "$BLUE" "$RESET" >"$TTY_OUT"
IFS= read -r message_in <"$TTY_IN" || true
message_in="$(trim_trailing_ws "$message_in")"
if [[ -z "$message_in" ]]; then
printf "⚠️ Message cannot be empty. Paste/type it again.\n" >"$TTY_OUT"
continue
fi
break
done
prefix="$(sanitize_slug "$prefix_in")"
suffix="$(normalize_suffix_input "$suffix_in")"
message="$(sanitize_slug "$message_in")"
if [[ -z "$prefix" || -z "$suffix" || -z "$message" ]]; then
echo "❌ Invalid values after sanitization."
exit 1
fi
new_branch="${prefix}/${suffix}/${message}"
if git show-ref --verify --quiet "refs/heads/$new_branch"; then
echo "❌ Branch already exists locally: $new_branch"
exit 1
fi
if git show-ref --verify --quiet "refs/remotes/origin/$new_branch"; then
echo "❌ Branch already exists on origin: $new_branch"
exit 1
fi
# --- 5) Create branch ---
echo "🌿 Creating branch: ${GREEN}${new_branch}${RESET}"
git checkout -B "$new_branch" origin/stage 2>/dev/null || git checkout -B "$new_branch" stage
echo "βœ… Done:"
echo " ${GREEN}${new_branch}${RESET}"
# sudo mv git-b.sh /usr/local/bin/git-b
# sudo chmod +x /usr/local/bin/git-b
# BASH:
# echo 'alias git-b="bash /usr/local/bin/git-b"' >> ~/.bashrc
# source ~/.bashrc
# ZSH:
# echo 'alias git-b="bash /usr/local/bin/git-b"' >> ~/.zshrc
# source ~/.zshrc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment