gitのブランチ移動を簡単にする

はじめに

gitでバージョン管理をしている GUIの操作を覚えたくないのでCLIでやろうとするが長くなりがちなブランチ名をいちいちコピペするのもめんどくさい というところで、ブランチの移動とやりたいときはマージまでコマンドで簡単にできないかとシェルスクリプトを作成した。 これでブランチの移動にストレスがなくなりました `./bashrc`への追加でさらに使いやすくなります 何か意見改善点あればぜひ教えてください!!

制作品

### 完成品

#!/usr/bin/env bash

set -u

show_help() {
  cat <<'EOF'
usage: gcn [options] [branch_number]

options:
  -h  show help
  -p  pull the target branch after switch
  -m  merge previous branch into target branch after switch
  -d  delete previous branch after switch (confirmation required)

examples:
  gcn 3
  gcn -p 2
  gcn -md 5
  gcn -mpd 1
EOF
  exit 0
}

confirm_delete() {
  local branch_name="$1"

  echo
  echo "Delete previous branch: ${branch_name}"
  read -r -p "Type 'yes' to delete this branch: " answer

  [ "$answer" = "yes" ]
}

is_protected_branch() {
  local branch_name="$1"

  case "$branch_name" in
    main|master|develop|development)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

sort_branches() {
  local prioritized=(
    main
    master
    develop
    test
    staging
    clientdemo1
  )

  local sorted=()
  local branch
  local p
  local found

  for p in "${prioritized[@]}"; do
    for branch in "${branches[@]}"; do
      if [ "$branch" = "$p" ]; then
        sorted+=("$branch")
        break
      fi
    done
  done

  for branch in "${branches[@]}"; do
    found=0
    for p in "${prioritized[@]}"; do
      if [ "$branch" = "$p" ]; then
        found=1
        break
      fi
    done

    if [ "$found" -eq 0 ]; then
      sorted+=("$branch")
    fi
  done

  branches=("${sorted[@]}")
}

switch_branch() {
  local target_branch="$1"

  echo ">> git switch $target_branch"
  git switch "$target_branch"
}

pull_branch() {
  local target_branch="$1"

  echo ">> git pull origin $target_branch"
  git pull origin "$target_branch"
}

merge_previous_branch() {
  local previous_branch="$1"

  echo ">> git merge $previous_branch"
  git merge "$previous_branch"
}

delete_previous_branch() {
  local previous_branch="$1"

  if is_protected_branch "$previous_branch"; then
    echo "Refusing to delete protected branch: $previous_branch"
    return 1
  fi

  if confirm_delete "$previous_branch"; then
    echo ">> git branch -d $previous_branch"
    git branch -d "$previous_branch"
  else
    echo "Canceled branch deletion."
  fi
}

# option flags
do_pull=0
do_merge=0
do_delete=0

while getopts ":hpmd" opt; do
  case "$opt" in
    h)
      show_help
      ;;
    p)
      do_pull=1
      ;;
    m)
      do_merge=1
      ;;
    d)
      do_delete=1
      ;;
    \?)
      echo "Invalid option: -$OPTARG"
      echo
      show_help
      ;;
  esac
done

shift $((OPTIND - 1))

mapfile -t branches < <(git for-each-ref --format='%(refname:short)' refs/heads/)
if [ "${#branches[@]}" -eq 0 ]; then
  echo "No local branches found."
  exit 1
fi

sort_branches

before_branch="$(git rev-parse --abbrev-ref HEAD)"

echo "Available branches:"
for i in "${!branches[@]}"; do
  printf "%3d: %s\n" "$i" "${branches[$i]}"
done

if [ $# -ge 1 ]; then
  branch_number="$1"
else
  read -r -p "Enter branch number to switch: " branch_number
fi

if [ -z "${branch_number:-}" ] || [ "$branch_number" = "q" ]; then
  echo "Canceled."
  exit 1
fi

if ! [[ "$branch_number" =~ ^[0-9]+$ ]]; then
  echo "Invalid branch number: $branch_number"
  exit 1
fi

target_branch="${branches[$branch_number]:-}"

if [ -z "$target_branch" ]; then
  echo "Invalid branch number: $branch_number"
  exit 1
fi

echo "chosen branch: <$target_branch>"

if [ "$before_branch" = "$target_branch" ]; then
  echo "You are already on '$target_branch'."
  exit 1
fi

if ! switch_branch "$target_branch"; then
  echo "Failed to switch branch."
  exit 1
fi

if [ "$do_pull" -eq 1 ]; then
  if ! pull_branch "$target_branch"; then
    echo "Failed to pull branch: $target_branch"
    exit 1
  fi
fi

if [ "$do_merge" -eq 1 ]; then
  if ! merge_previous_branch "$before_branch"; then
    echo "Failed to merge branch: $before_branch"
    exit 1
  fi
fi

if [ "$do_delete" -eq 1 ]; then
  delete_previous_branch "$before_branch"
fi

### 使い方

シェルスクリプトを実行するとブランチの一覧が表示され、数字を指定することで、そのブランチに移動することができる。

$ ./git_checkout_by_number.sh
  Available branches:
   0: main
   1: develop
   2: feature/branch1
   3: feature/branch2
 Enter branch number to switch:      
 >> git switch develop
 Switched to branch 'develop'

オプションを指定することで、移動後に追加の操作が可能です:

  • -p: 移動後に git pull を実行

  • -m: 移動前にいたブランチを移動先ブランチにマージ

  • -d: 移動前にいたブランチを削除(確認あり)

$ ./git_checkout_by_number.sh -p 2
$ ./git_checkout_by_number.sh -md 3
$ ./git_checkout_by_number.sh -mpd 1

スクリプトの説明

## オプション解析

getopts を使用してオプションを正しく解析します。

while getopts ":hpmd" opt; do
  case "$opt" in
    h) show_help ;;
    p) do_pull=1 ;;
    m) do_merge=1 ;;
    d) do_delete=1 ;;
    \?) echo "Invalid option: -$OPTARG"; show_help ;;
  esac
done

## ブランチの取得とソート

ローカルブランチを取得し、優先ブランチ(main, master, develop等)を先頭にソートします。

mapfile -t branches < <(git for-each-ref --format='%(refname:short)' refs/heads/)
sort_branches

## 入力の取得と例外処理

引数でブランチ番号が指定されない場合は対話的に入力を求めます。 入力なしまたは q を入力するとキャンセルされます。

if [ $# -ge 1 ]; then
  branch_number="$1"
else
  read -r -p "Enter branch number to switch: " branch_number
fi

if [ -z "${branch_number:-}" ] || [ "$branch_number" = "q" ]; then
  echo "Canceled."
  exit 1
fi

## ブランチの移動

git switch を使用してブランチを移動します。 同一ブランチへの移動は防止します。

if [ "$before_branch" = "$target_branch" ]; then
  echo "You are already on '$target_branch'."
  exit 1
fi

switch_branch "$target_branch"

## 保護されたブランチの削除防止

main, master, develop, development ブランチは削除対象から除外します。

is_protected_branch() {
  case "$branch_name" in
    main|master|develop|development)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

`.bashrc`への登録

gitは比較的どこでも使うので`./bashrc`に登録したほうが使いやすいです いちいちプロジェクトごとに置いとくのもignoreするのもめんどいですしね 自分の場合は`git_checkout_by_number.sh`を`/usr/local/bin/mine/`において、 .bashrcに下記を追記しました。

export PATH=/usr/local/bin/mine/:$PATH
alias gcn='git_checkout_by_number'
$ gcn

で利用できます

参考文献

ごめんなさい あったかもしれませんが忘れました 参考にした方ありがとうございました