#!/usr/bin/env bash # 4개 submodule: GitHub chatbot2.0-agent 동기화 → 로컬 변경 commit → push # # ./git_fetch_all.sh # fetch/merge + commit + push # ./git_fetch_all.sh --init # submodule 미초기화 시 clone 포함 # ./git_fetch_all.sh --dry-run # 실행 없이 상태만 출력 # ./git_fetch_all.sh --no-push # commit까지, push 생략 # ./git_fetch_all.sh --no-commit # fetch/merge만 (commit·push 생략) # # 대상 (.gitmodules) — 모두 branch=chatbot2.0-agent (main 사용 안 함): # chatbotApi-chatbot2.0-agent # chatbotApp-chatbot2.0-agent # exAiChatBot-chatbot2.0-agent # kakaoChatbotSkill-chatbot2.0-agent set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # bare repo 배포 시 상위에서 GIT_DIR 이 export 되면 submodule git 이 깨짐 MONOREPO_GIT_DIR="${MONOREPO_GIT_DIR:-${GIT_DIR:-}}" unset GIT_DIR GIT_WORK_TREE INIT=false DRY_RUN=false DO_COMMIT=true DO_PUSH=true TARGET_BRANCH="chatbot2.0-agent" # 명시 대상 (순서 고정). .gitmodules 와 일치해야 함. TARGET_PATHS=( "chatbotApi-chatbot2.0-agent" "chatbotApp-chatbot2.0-agent" "exAiChatBot-chatbot2.0-agent" "kakaoChatbotSkill-chatbot2.0-agent" ) for arg in "$@"; do case "$arg" in --init) INIT=true ;; --dry-run) DRY_RUN=true ;; --no-commit) DO_COMMIT=false; DO_PUSH=false ;; --no-push) DO_PUSH=false ;; -h|--help) sed -n '2,18p' "$0" exit 0 ;; *) echo "알 수 없는 옵션: $arg (--help)" exit 1 ;; esac done log() { echo "[git_fetch_all] $*"; } warn() { echo "[git_fetch_all] ⚠️ $*" >&2; } ok() { echo "[git_fetch_all] ✅ $*"; } cd "${ROOT_DIR}" if [[ ! -f .gitmodules ]]; then echo "ERROR: .gitmodules 없음 — submodule 미설정" exit 1 fi resolve_monorepo_git_dir() { if [[ -n "${MONOREPO_GIT_DIR}" && -d "${MONOREPO_GIT_DIR}" ]]; then echo "${MONOREPO_GIT_DIR}" elif git -C "${ROOT_DIR}" rev-parse --git-dir >/dev/null 2>&1; then git -C "${ROOT_DIR}" rev-parse --git-dir fi } submodule_pinned_sha() { local path="$1" local git_dir git_dir="$(resolve_monorepo_git_dir || true)" [[ -n "${git_dir}" ]] || return 0 git --git-dir="${git_dir}" ls-tree HEAD "${path}" 2>/dev/null | awk '{print $3}' } clone_submodule_repo() { local path="$1" url="$2" branch="$3" local target="${ROOT_DIR}/${path}" local sha sha="$(submodule_pinned_sha "${path}" || true)" log " clone ${url} → ${path} (branch=${branch})" rm -rf "${target}" if ! git clone --branch "${branch}" --single-branch "${url}" "${target}"; then warn "${path} — clone 실패 (서버 GitHub 미접속 시 sync-submodules-rsync.sh 사용)" return 1 fi if [[ -n "${sha}" ]]; then git -C "${target}" checkout "${sha}" 2>/dev/null || true fi } restore_monorepo_overlays() { if [[ -f "${ROOT_DIR}/local-dev/docker-compose.rag.local.yml" ]]; then cp "${ROOT_DIR}/local-dev/docker-compose.rag.local.yml" \ "${ROOT_DIR}/exAiChatBot-chatbot2.0-agent/docker-compose.local.yml" log "복원: exAiChatBot/docker-compose.local.yml (monorepo overlay)" fi if [[ -f "${ROOT_DIR}/server-dev/docker-compose.rag.server.yml" ]]; then cp "${ROOT_DIR}/server-dev/docker-compose.rag.server.yml" \ "${ROOT_DIR}/exAiChatBot-chatbot2.0-agent/docker-compose.server.yml" log "복원: exAiChatBot/docker-compose.server.yml (monorepo overlay)" fi } submodule_is_cloned() { local path="$1" [[ -e "${ROOT_DIR}/${path}/.git" ]] } repo_is_dirty() { ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]] } ensure_branch() { local branch="$1" local git_dir # submodule 은 .git 이 파일일 수 있음 → rev-parse 사용 git_dir="$(git rev-parse --git-dir 2>/dev/null || echo .git)" if [[ -d "${git_dir}/rebase-merge" || -d "${git_dir}/rebase-apply" ]]; then warn "rebase 진행 중 — 수동 해결 후 재실행 (git rebase --continue / --abort)" return 1 fi if [[ -f "${git_dir}/MERGE_HEAD" ]]; then warn "merge 진행 중 — 수동 해결 후 재실행" return 1 fi local cur cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo DETACHED)" if [[ "${cur}" == "${branch}" ]]; then return 0 fi if git show-ref --verify --quiet "refs/heads/${branch}"; then git checkout "${branch}" elif git show-ref --verify --quiet "refs/remotes/origin/${branch}"; then git checkout -B "${branch}" "origin/${branch}" else git checkout -B "${branch}" fi } sync_one() { local path="$1" local url branch old new stashed=0 ahead url="$(git config -f .gitmodules "submodule.${path}.url" 2>/dev/null || true)" if [[ -z "${url}" ]]; then warn "${path} — .gitmodules 에 없음, 건너뜀" return 1 fi # 사용자 요구: main 금지, chatbot2.0-agent 고정 branch="$(git config -f .gitmodules "submodule.${path}.branch" || echo "${TARGET_BRANCH}")" branch="${branch:-${TARGET_BRANCH}}" if [[ "${branch}" != "${TARGET_BRANCH}" ]]; then warn "${path} — .gitmodules branch=${branch} → ${TARGET_BRANCH} 로 강제" branch="${TARGET_BRANCH}" fi log ">>> ${path}" log " url=${url} branch=${branch}" if ! submodule_is_cloned "${path}"; then if $DRY_RUN; then log " [dry-run] submodule clone 필요 — ./git_fetch_all.sh --init" return 0 fi if $INIT; then clone_submodule_repo "${path}" "${url}" "${branch}" || return 1 else warn "${path} — submodule 미초기화. --init 옵션 사용" return 1 fi fi ( cd "${ROOT_DIR}/${path}" unset GIT_DIR GIT_WORK_TREE old="$(git rev-parse HEAD 2>/dev/null || echo none)" if $DRY_RUN; then ensure_branch "${branch}" || true git fetch origin "${branch}" 2>/dev/null || git fetch origin new="$(git rev-parse "origin/${branch}" 2>/dev/null || echo unknown)" if [[ "${old}" == "${new}" ]]; then log " [dry-run] 원격 최신: ${old:0:7}" else log " [dry-run] 원격 변경 ${old:0:7} → ${new:0:7}" git log --oneline "HEAD..origin/${branch}" 2>/dev/null | sed 's/^/ /' || true fi if repo_is_dirty; then log " [dry-run] 로컬 미커밋 변경 있음 → commit/push 대상" git status -sb | sed 's/^/ /' else log " [dry-run] 로컬 미커밋 변경 없음" fi exit 0 fi # 1) 원격 fetch + 브랜치 고정 git fetch origin "${branch}" ensure_branch "${branch}" || exit 1 git branch --set-upstream-to="origin/${branch}" "${branch}" 2>/dev/null || true # 2) 로컬 미커밋 변경이 pull 을 막으면 stash if repo_is_dirty; then log " 로컬 변경 stash 후 원격 반영..." git stash push -u -m "git_fetch_all auto-stash $(date '+%Y-%m-%d %H:%M:%S')" || true stashed=1 fi # 3) GitHub 변경사항 merge (ff → rebase → merge) if [[ "$(git rev-parse HEAD)" != "$(git rev-parse "origin/${branch}")" ]]; then log " 원격 변경 merge: origin/${branch}" if git merge-base --is-ancestor HEAD "origin/${branch}" 2>/dev/null; then git merge --ff-only "origin/${branch}" else if ! git pull --rebase origin "${branch}"; then warn "${path} — rebase 실패, merge 로 재시도" git rebase --abort 2>/dev/null || true if ! git pull --no-rebase origin "${branch}"; then warn "${path} — merge 충돌. 수동 해결 필요" git status -sb | sed 's/^/ /' if [[ "${stashed}" -eq 1 ]]; then git stash pop || true fi exit 1 fi fi fi ok "${path} 원격 반영 $(git log -1 --oneline)" else log " 원격과 동일 (fetch 완료)" fi # 4) stash 복원 if [[ "${stashed}" -eq 1 ]]; then log " stash 복원..." if ! git stash pop; then warn "${path} — stash pop 충돌. 수동 해결 후 commit/push" git status -sb | sed 's/^/ /' exit 1 fi fi # 5) 로컬 수정사항 commit if $DO_COMMIT && repo_is_dirty; then log " 로컬 변경 commit..." git add -A if git diff --cached --quiet; then log " staged 변경 없음 (ignore 된 파일만 있을 수 있음)" else git commit -m "$(cat </dev/null || echo 0)" if [[ "${ahead}" != "0" ]]; then log " push origin ${branch} (ahead ${ahead})..." if git push origin "${branch}"; then ok "${path} push 완료" else warn "${path} — push 실패 (대용량 파일/권한 등). 로그 확인" exit 1 fi else log " push 생략 (원격과 동일)" fi else log " [--no-push] push 생략" fi new="$(git rev-parse HEAD)" log " HEAD ${old:0:7} → ${new:0:7} $(git log -1 --oneline)" ) } failures=0 for path in "${TARGET_PATHS[@]}"; do if ! sync_one "${path}"; then failures=$((failures + 1)) fi echo "" done if ! $DRY_RUN; then restore_monorepo_overlays fi log "==========================================" log "submodule 상태:" git submodule status 2>/dev/null || true for path in "${TARGET_PATHS[@]}"; do if submodule_is_cloned "${path}"; then printf " %-36s %s\n" "${path}" "$(git -C "${ROOT_DIR}/${path}" log -1 --oneline 2>/dev/null || echo '?')" fi done log "==========================================" if [[ ${failures} -gt 0 ]]; then warn "실패 ${failures}개 — 로그 확인" exit 1 fi if $DRY_RUN; then log "완료 — dry-run (변경 없음)" elif $DO_PUSH; then log "완료 — ${TARGET_BRANCH}: fetch/merge → commit → push" else log "완료 — ${TARGET_BRANCH}: fetch/merge$($DO_COMMIT && echo ' → commit' || true) (push 생략)" fi log "monorepo 포인터 갱신: git add *-chatbot2.0-agent && git status"