Files
Rinjae 0ad5bffa4f
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
DJB 커스텀 소스 납품 스크립트 추가:
- Windows, Linux/macOS 지원 배치/셸 스크립트 구현
- 소스 필터 규칙 및 수집 파일/모듈 매니페스트 작성
2026-08-11 11:47:31 +09:00

353 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# DJB 커스텀 소스 납품본 수집 스크립트 (macOS / Linux)
#
# 기준 커밋(컷오프 직전 마지막 커밋) 이후 "신규로 추가된" 소스 파일만 모아
# 모듈별 디렉터리 구조를 유지한 채 출력 디렉터리에 복사하고,
# MANIFEST.csv / SUMMARY.md / (선택) zip 아카이브를 생성한다.
#
# 사용법:
# ./export-custom.sh # 기본값으로 실행
# ./export-custom.sh -c 2026-05-01 --zip
# ./export-custom.sh --dry-run
# ./export-custom.sh -m eapim-portal -m eapim-admin
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_FILE="$SCRIPT_DIR/modules.conf"
RULES_FILE="$SCRIPT_DIR/export-filter.rules"
# ---- 기본값 ---------------------------------------------------------------
CUTOFF="2026-05-01"
REPO_ROOT=""
OUT_DIR=""
MAKE_ZIP=0
DRY_RUN=0
INCLUDE_UNTRACKED=0
ONLY_MODULES=""
SHOW_EXCLUDED=0
usage() {
cat <<'EOF'
DJB 커스텀 소스 납품본 수집
옵션:
-c, --cutoff <YYYY-MM-DD> 기준 날짜. 이 날짜 직전 마지막 커밋을 base로 삼는다.
(기본: 2026-05-01)
-r, --root <DIR> 저장소 루트(djb-eapim). 미지정 시 스크립트 위치에서 자동 탐지.
-o, --out <DIR> 출력 디렉터리. (기본: <root>/build/djb-custom-export)
-m, --module <NAME> 특정 모듈만 수집. 여러 번 지정 가능.
--zip 수집 후 zip 아카이브 생성.
--include-untracked git에 아직 커밋되지 않은 신규 파일도 포함.
--dry-run 복사하지 않고 대상 목록만 stdout 으로 출력(로그는 stderr).
--show-excluded --dry-run 과 함께. 규칙에 걸려 제외된 목록을 대신 출력.
-h, --help 도움말.
포함/제외 규칙은 export-filter.rules (.gitignore 유사 문법) 에서 관리한다.
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
-c|--cutoff) CUTOFF="$2"; shift 2 ;;
-r|--root) REPO_ROOT="$2"; shift 2 ;;
-o|--out) OUT_DIR="$2"; shift 2 ;;
-m|--module) ONLY_MODULES="$ONLY_MODULES $2"; shift 2 ;;
--zip) MAKE_ZIP=1; shift ;;
--include-untracked) INCLUDE_UNTRACKED=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--show-excluded) SHOW_EXCLUDED=1; DRY_RUN=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "알 수 없는 옵션: $1" >&2; usage >&2; exit 1 ;;
esac
done
# ---- 저장소 루트 탐지 -----------------------------------------------------
# 스크립트는 <root>/eapim-portal/script/djb-custom-export/ 에 있다.
if [ -z "$REPO_ROOT" ]; then
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
fi
if [ ! -d "$REPO_ROOT" ]; then
echo "저장소 루트를 찾을 수 없다: $REPO_ROOT" >&2
exit 1
fi
[ -z "$OUT_DIR" ] && OUT_DIR="$REPO_ROOT/build/djb-custom-export"
if [ ! -f "$CONF_FILE" ]; then
echo "설정 파일 없음: $CONF_FILE" >&2
exit 1
fi
# ---- 설정 로드 ------------------------------------------------------------
ALLOW_EXT=""
EXCLUDE_PAT=""
MODULE_LINES=""
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
key="${line%%|*}"
rest="${line#*|}"
case "$key" in
MODULE) MODULE_LINES="$MODULE_LINES
$rest" ;;
EXT) ALLOW_EXT="$rest" ;;
EXCLUDE) EXCLUDE_PAT="$rest" ;;
esac
done < "$CONF_FILE"
# 빈 트리 해시 — base 커밋이 없을 때(= 모든 이력이 컷오프 이후) 사용한다.
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
is_allowed_ext() {
_f="$1"
_base="${_f##*/}"
case "$_base" in
*.*) _ext="${_base##*.}" ;;
*) return 1 ;; # 확장자 없는 파일 제외
esac
# 소문자 변환 (bash 3.2 호환)
_ext="$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')"
case ",$ALLOW_EXT," in
*",$_ext,"*) return 0 ;;
*) return 1 ;;
esac
}
# ---- 포함/제외 규칙 (.gitignore 유사) --------------------------------------
RULES=()
if [ -f "$RULES_FILE" ]; then
while IFS= read -r r || [ -n "$r" ]; do
# 앞뒤 공백 제거
r="${r#"${r%%[![:space:]]*}"}"
r="${r%"${r##*[![:space:]]}"}"
case "$r" in
''|'#'*) continue ;;
esac
RULES[${#RULES[@]}]="$r"
done < "$RULES_FILE"
fi
# 경로 하나에 규칙을 순서대로 적용한다. 마지막으로 매칭된 규칙이 이긴다.
# 기본은 포함(0), '!' 규칙은 다시 포함, 일반 규칙은 제외(1).
# 반환: 0 = 포함, 1 = 제외. 제외시킨 규칙은 MATCHED_RULE 에 담긴다.
MATCHED_RULE=""
rule_keep() {
_path="$1"
_leaf="${_path##*/}"
_keep=0
MATCHED_RULE=""
_i=0
while [ $_i -lt ${#RULES[@]} ]; do
_rule="${RULES[$_i]}"
_i=$((_i + 1))
_neg=0
case "$_rule" in
'!'*) _neg=1; _rule="${_rule#!}" ;;
esac
# 디렉터리 규칙: 하위 전체
case "$_rule" in
*/) _rule="${_rule}*" ;;
esac
# '/' 가 있으면 전체 경로, 없으면 파일명과 매칭
case "$_rule" in
*/*) _target="$_path" ;;
*) _target="$_leaf" ;;
esac
# shellcheck disable=SC2254 # 의도적으로 unquoted — glob 패턴으로 쓴다
case "$_target" in
$_rule)
if [ $_neg -eq 1 ]; then
_keep=0; MATCHED_RULE=""
else
_keep=1; MATCHED_RULE="$_rule"
fi
;;
esac
done
return $_keep
}
csv_escape() {
printf '"%s"' "$(printf '%s' "$1" | sed 's/"/""/g')"
}
# ---- 준비 -----------------------------------------------------------------
if [ "$DRY_RUN" -eq 0 ]; then
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR/src"
fi
MANIFEST="$OUT_DIR/MANIFEST.csv"
SUMMARY="$OUT_DIR/SUMMARY.md"
TMP_SUMMARY="$(mktemp)"
trap 'rm -f "$TMP_SUMMARY"' EXIT
if [ "$DRY_RUN" -eq 0 ]; then
printf '\357\273\277' > "$MANIFEST" # Excel 한글용 UTF-8 BOM
echo 'module,path,ext,added_commit,added_date,author' >> "$MANIFEST"
fi
# dry-run(목록 뽑기) 모드에서는 파일 목록만 stdout으로 내보내고
# 진행 로그는 전부 stderr로 보낸다. 그래야 파이프/리다이렉트로 목록을 바로 가공할 수 있다.
if [ "$DRY_RUN" -eq 1 ]; then LOG=2; else LOG=1; fi
{
echo "저장소 루트 : $REPO_ROOT"
echo "기준 날짜 : $CUTOFF (이 날짜 직전 마지막 커밋이 base)"
echo "출력 경로 : $OUT_DIR"
[ "$DRY_RUN" -eq 1 ] && echo "모드 : DRY-RUN (목록만 출력, 복사 안 함)"
echo
} >&$LOG
TOTAL=0
MISSING=0
EXCLUDED=0
# ---- 모듈 순회 ------------------------------------------------------------
# 파이프 대신 process substitution 을 써서 집계 변수가 서브셸에 갇히지 않게 한다.
while IFS= read -r mline; do
[ -z "$mline" ] && continue
MOD="${mline%%|*}"
PATHS="${mline#*|}"
if [ -n "$ONLY_MODULES" ]; then
case " $ONLY_MODULES " in
*" $MOD "*) : ;;
*) continue ;;
esac
fi
MOD_DIR="$REPO_ROOT/$MOD"
if [ ! -d "$MOD_DIR/.git" ]; then
echo "[건너뜀] $MOD — git 저장소 아님 ($MOD_DIR)" >&$LOG
continue
fi
# base 커밋 결정
BASE="$(git -C "$MOD_DIR" -c core.quotepath=false rev-list -1 --before="$CUTOFF" HEAD 2>/dev/null || true)"
if [ -z "$BASE" ]; then
BASE="$EMPTY_TREE"
BASE_DESC="(컷오프 이전 커밋 없음 → 전체를 신규로 간주)"
else
BASE_DESC="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --format='%h %ad %s' --date=short "$BASE" | cut -c1-80)"
fi
# 실제 존재하는 소스 경로만 pathspec으로 사용
SPEC=""
for p in $PATHS; do
[ -d "$MOD_DIR/$p" ] && SPEC="$SPEC $p"
done
if [ -z "$SPEC" ]; then
echo "[건너뜀] $MOD — 설정된 소스 경로가 존재하지 않음" >&$LOG
continue
fi
# 신규(Added) 파일만. -M 으로 rename은 신규에서 제외한다.
FILES="$(git -C "$MOD_DIR" -c core.quotepath=false diff --name-only --diff-filter=A -M "$BASE" HEAD -- $SPEC 2>/dev/null || true)"
if [ "$INCLUDE_UNTRACKED" -eq 1 ]; then
UNTRACKED="$(git -C "$MOD_DIR" -c core.quotepath=false ls-files --others --exclude-standard -- $SPEC 2>/dev/null || true)"
FILES="$FILES
$UNTRACKED"
fi
COUNT=0
MISS=0
while IFS= read -r f; do
[ -z "$f" ] && continue
is_allowed_ext "$f" || continue
# 규칙 매칭은 <모듈>/<경로> 전체 문자열 기준
if ! rule_keep "$MOD/$f"; then
EXCLUDED=$((EXCLUDED + 1))
[ "$SHOW_EXCLUDED" -eq 1 ] && printf '%s\t# %s\n' "$MOD/$f" "$MATCHED_RULE"
continue
fi
[ "$SHOW_EXCLUDED" -eq 1 ] && continue
SRC="$MOD_DIR/$f"
if [ ! -f "$SRC" ]; then
# 추가된 뒤 삭제/이동된 파일 — 납품 대상 아님
echo " [없음] $MOD/$f" >&2
MISS=$((MISS + 1))
continue
fi
COUNT=$((COUNT + 1))
if [ "$DRY_RUN" -eq 1 ]; then
echo "$MOD/$f" # 목록은 stdout으로 (가공하기 쉽게 접두 공백 없음)
continue
fi
DST="$OUT_DIR/src/$MOD/$f"
mkdir -p "$(dirname "$DST")"
cp -p "$SRC" "$DST"
META="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --diff-filter=A --format='%h|%ad|%an' --date=short -- "$f" 2>/dev/null || true)"
C_HASH="${META%%|*}"; _r="${META#*|}"
C_DATE="${_r%%|*}"
C_AUTH="${_r#*|}"
BASE_F="${f##*/}"
case "$BASE_F" in *.*) EXT="${BASE_F##*.}" ;; *) EXT="" ;; esac
{
csv_escape "$MOD"; printf ','
csv_escape "$f"; printf ','
csv_escape "$EXT"; printf ','
csv_escape "$C_HASH"; printf ','
csv_escape "$C_DATE"; printf ','
csv_escape "$C_AUTH"; printf '\n'
} >> "$MANIFEST"
done < <(printf '%s\n' "$FILES" | sort -u)
TOTAL=$((TOTAL + COUNT))
MISSING=$((MISSING + MISS))
printf '%s|%s|%s\n' "$MOD" "$COUNT" "$BASE_DESC" >> "$TMP_SUMMARY"
printf '[수집] %-22s %5s 개 base: %s\n' "$MOD" "$COUNT" "$BASE_DESC" >&$LOG
done < <(printf '%s\n' "$MODULE_LINES")
# ---- 요약 ------------------------------------------------------------------
if [ "$DRY_RUN" -eq 0 ]; then
{
echo "# DJB 커스텀 소스 납품본"
echo
echo "- 생성 일시: $(date '+%Y-%m-%d %H:%M:%S')"
echo "- 기준 날짜: \`$CUTOFF\` (이 날짜 직전 마지막 커밋을 base로 삼아, 이후 **신규 추가된** 파일만 수집)"
echo "- 수집 규칙: \`git diff --diff-filter=A -M <base> HEAD\` / 확장자 allowlist / \`export-filter.rules\` 필터"
echo "- 대상 확장자: \`$ALLOW_EXT\`"
echo "- 규칙 파일: \`$(basename "$RULES_FILE")\` (${#RULES[@]} 개 규칙, 제외 ${EXCLUDED} 건)"
echo
echo "## 모듈별 수집 결과"
echo
echo "| 모듈 | 파일 수 | base 커밋 |"
echo "|---|---:|---|"
while IFS='|' read -r m n d; do
[ -z "$m" ] && continue
echo "| $m | $n | $d |"
done < "$TMP_SUMMARY"
echo
echo "**합계: ${TOTAL} 개**"
[ "$MISSING" -gt 0 ] && echo "> 추가 후 삭제/이동되어 현재 트리에 없는 파일 ${MISSING} 개는 제외됨."
echo
echo "## 파일 목록"
echo
echo "\`MANIFEST.csv\` 참조 (module, path, ext, 최초 추가 커밋/일자/작성자)."
} > "$SUMMARY"
echo
echo "MANIFEST : $MANIFEST"
echo "SUMMARY : $SUMMARY"
if [ "$MAKE_ZIP" -eq 1 ]; then
ZIP_NAME="djb-custom-export-$(date '+%Y%m%d').zip"
ZIP_PATH="$(dirname "$OUT_DIR")/$ZIP_NAME"
rm -f "$ZIP_PATH"
( cd "$OUT_DIR" && zip -qr "$ZIP_PATH" . )
echo "ZIP : $ZIP_PATH"
fi
fi