본문으로 건너뛰기
에이든의 블로그
뒤로가기

Git Hooks를 사용하여 생성일과 수정일을 설정하는 방법

이 글에서는 AstroPaper 블로그 테마의 frontmatter에서 생성일(pubDatetime)과 수정일(modDatetime)의 입력을 자동화하기 위해 pre-commit Git hook을 사용하는 방법을 설명합니다.

목차

어디서나 사용하기

Git hooks는 커밋 메시지에 브랜치 이름을 추가하거나 검증하는 것, 또는 평문 비밀키의 커밋을 방지하는 것과 같은 작업을 자동화하는 데 유용합니다. 가장 큰 단점은 클라이언트 측 hooks가 머신별로 설정해야 한다는 것입니다.

hooks 디렉토리를 만들고 .git/hooks 디렉토리에 수동으로 복사하거나 심볼릭 링크를 설정하여 이 문제를 해결할 수 있지만, 이 모든 것은 설정하는 것을 기억해야 하며, 그것은 제가 잘하는 일이 아닙니다.

이 프로젝트는 npm을 사용하므로, Husky라는 패키지를 활용하여 hooks를 자동으로 설치할 수 있습니다 (AstroPaper에는 이미 설치되어 있습니다).

업데이트! AstroPaper v4.3.0에서 pre-commit hook이 GitHub Actions로 대체되어 제거되었습니다. 하지만 직접 Husky를 설치할 수 있습니다.

Hook 설정

이 hook은 코드를 커밋할 때 날짜를 업데이트하고 변경 사항에 포함시키기 위한 것이므로, pre-commit hook을 사용합니다. 이 AstroPaper 프로젝트에서는 이미 설정되어 있지만, 그렇지 않다면 npx husky add .husky/pre-commit 'echo "This is our new pre-commit hook"'을 실행하면 됩니다.

hooks/pre-commit 파일로 이동하여, 다음 스니펫 중 하나 또는 둘 다를 추가합니다.

파일 수정 시 수정일 업데이트


업데이트:

이 섹션은 더 스마트한 새 버전의 hook으로 업데이트되었습니다. 이제 포스트가 게시될 때까지 modDatetime을 증가시키지 않습니다. 첫 게시 시 draft 상태를 first로 설정하면 자동으로 처리됩니다.


# 수정된 파일, modDatetime 업데이트
git diff --cached --name-status |
grep -i '^M.*\.md$' |
while read _ file; do
  filecontent=$(cat "$file")
  frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
  draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')
  if [ "$draft" = "false" ]; then
    echo "$file modDateTime updated"
    cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
    mv tmp $file
    git add $file
  fi
  if [ "$draft" = "first" ]; then
    echo "First release of $file, draft set to false and modDateTime removed"
    cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime:/" | sed "/---.*/,/---.*/s/^draft:.*$/draft: false/" > tmp
    mv tmp $file
    git add $file
  fi
done

git diff --cached --name-status는 커밋을 위해 스테이징된 파일을 git에서 가져옵니다. 출력은 다음과 같습니다:

A       src/content/blog/setting-dates-via-git-hooks.md

맨 앞의 문자는 수행된 작업을 나타냅니다. 위 예시에서는 파일이 추가된 것입니다. 수정된 파일은 M으로 표시됩니다.

이 출력을 grep 명령으로 파이프하여 수정된 파일을 찾습니다. 줄이 M으로 시작하고(^(M)), 그 뒤에 임의의 문자가 오며(.*), .md 파일 확장자로 끝나야 합니다(.(md)$). 이를 통해 수정된 마크다운 파일이 아닌 줄을 필터링합니다: egrep -i "^(M).*\.(md)$".


개선 사항 - 더 명시적으로

blog 디렉토리의 마크다운 파일만 찾도록 추가할 수 있습니다. 올바른 frontmatter를 가진 파일은 이 디렉토리의 파일뿐이기 때문입니다.


정규식은 문자와 파일 경로 두 부분을 캡처합니다. 이 목록을 while 루프로 파이프하여 일치하는 줄을 반복하고, 문자를 a에, 경로를 b에 할당합니다. 현재는 a를 무시합니다.

파일의 draft 상태를 알기 위해 frontmatter가 필요합니다. 다음 코드에서는 cat으로 파일 내용을 가져온 다음, awk를 사용하여 frontmatter 구분자(---)로 파일을 분할하고 두 번째 블록(frontmatter, --- 사이의 부분)을 가져옵니다. 여기서 다시 awk를 사용하여 draft 키를 찾고 그 값을 출력합니다.

  filecontent=$(cat "$file")
  frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
  draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')

이제 draft 값을 가지고 세 가지 중 하나를 수행합니다: modDatetime을 현재 시간으로 설정하거나(draft가 false일 때 if [ "$draft" = "false" ]; then), modDatetime을 비우고 draft를 false로 설정하거나(draft가 first일 때 if [ "$draft" = "first" ]; then), 아무것도 하지 않습니다(그 외의 경우).

다음의 sed 명령 부분은 자주 사용하지 않아 제게는 다소 마법 같은데, 비슷한 작업을 하는 다른 블로그 포스트에서 가져온 것입니다. 본질적으로, 파일의 frontmatter 태그(---) 내에서 pubDatetime: 키를 찾아 전체 줄을 가져온 다음, pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" 같은 키와 올바르게 포맷된 현재 날짜시간으로 교체합니다.

이 교체는 전체 파일의 컨텍스트에서 이루어지므로 임시 파일에 저장하고(> tmp), 새 파일을 기존 파일 위치로 이동(mv)하여 덮어씁니다. 그런 다음 직접 변경한 것처럼 커밋될 준비가 된 상태로 git에 추가됩니다.


참고

sed가 작동하려면 frontmatter에 이미 modDatetime 키가 있어야 합니다. 빈 날짜로 앱이 빌드되려면 몇 가지 추가 변경이 필요합니다. 아래를 참고하세요.


새 파일에 날짜 추가

새 파일에 날짜를 추가하는 것은 위와 동일한 과정이지만, 이번에는 추가된(A) 줄을 찾고 pubDatetime 값을 교체합니다.

# 새 파일, pubDatetime 추가/업데이트
git diff --cached --name-status | egrep -i "^(A).*\.(md)$" | while read a b; do
  cat $b | sed "/---.*/,/---.*/s/^pubDatetime:.*$/pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
  mv tmp $b
  git add $b
done

개선 사항 - 한 번만 루프 돌기

a 변수를 사용하여 루프 내에서 분기하여 modDatetime 업데이트 또는 pubDatetime 추가를 하나의 루프에서 처리할 수 있습니다.


Frontmatter 자동 채우기

IDE가 스니펫을 지원한다면, frontmatter를 자동으로 채워주는 커스텀 스니펫을 만들 수 있습니다. AstroPaper v4에서는 VSCode용 스니펫이 기본으로 제공될 예정입니다.

modDatetime 변경 사항

Astro가 마크다운을 컴파일하고 처리하려면, frontmatter에 어떤 내용이 예상되는지 알아야 합니다. 이는 src/content/config.ts의 설정을 통해 이루어집니다.

키가 값 없이 존재할 수 있도록 하려면 10번째 줄을 수정하여 .nullable() 함수를 추가해야 합니다.

const blog = defineCollection({
  type: "content",
  schema: ({ image }) =>
    z.object({
      author: z.string().default(SITE.author),
      pubDatetime: z.date(),
      modDatetime: z.date().optional(),
      modDatetime: z.date().optional().nullable(),
      title: z.string(),
      featured: z.boolean().optional(),
      draft: z.boolean().optional(),
      tags: z.array(z.string()).default(["others"]),
      ogImage: image().or(z.string()).optional(),
      description: z.string(),
      canonicalURL: z.string().optional(),
      readingTime: z.string().optional(),
    }),
});

IDE가 블로그 엔진 파일에서 경고를 표시하지 않도록 다음 변경도 수행했습니다:

  1. src/layouts/Layout.astro의 15번째 줄에 | null을 추가하여 다음과 같이 변경합니다.

    export interface Props {
      title?: string;
      author?: string;
      description?: string;
      ogImage?: string;
      canonicalURL?: string;
      pubDatetime?: Date;
      modDatetime?: Date | null;
    }
  2. src/components/Datetime.tsx의 5번째 줄에 | null을 추가하여 다음과 같이 변경합니다.

    interface DatetimesProps {
      pubDatetime: string | Date;
      modDatetime: string | Date | undefined | null;
    }