353 lines
12 KiB
Bash
Executable File
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
|