Skip to content
Go back

Cách sử dụng Git Hooks để thiết lập Ngày Tạo và Ngày Sửa Đổi

Updated:  at  01:59 AM

Trong bài viết này, tôi sẽ giải thích cách sử dụng pre-commit Git hook để tự động nhập ngày tạo (pubDatetime) và ngày sửa đổi (modDatetime) trong frontmatter của chủ đề blog cho markdown.

Table of contents

Open Table of contents

Sử dụng ở mọi nơi

Git hooks rất hữu ích để tự động hóa các tác vụ như thêm hoặc kiểm tra tên nhánh vào commit message hoặc ngăn bạn commit thông tin bí mật dạng văn bản thuần. Nhược điểm lớn nhất là các hook phía client chỉ áp dụng trên từng máy.

Bạn có thể khắc phục điều này bằng cách có một thư mục hooks và sao chép thủ công vào thư mục .git/hooks hoặc thiết lập symlink, nhưng tất cả đều yêu cầu bạn phải nhớ thiết lập, điều mà tôi không giỏi lắm.

Vì dự án này sử dụng npm, chúng ta có thể tận dụng một package tên là Husky (đã được cài sẵn trong cho markdown) để tự động cài đặt các hook cho chúng ta.

Cập nhật! Trong cho markdown v4.3.0, pre-commit hook đã bị loại bỏ để chuyển sang dùng GitHub Actions. Tuy nhiên, bạn vẫn có thể dễ dàng cài đặt Husky cho riêng mình.

Hook

Vì chúng ta muốn hook này chạy khi commit code để cập nhật ngày tháng và đưa thay đổi đó vào commit, chúng ta sẽ sử dụng hook pre-commit. Điều này đã được thiết lập sẵn trong dự án cho markdown, nhưng nếu chưa, bạn có thể chạy npx husky add .husky/pre-commit 'echo "This is our new pre-commit hook"'.

Đi tới file hooks/pre-commit, chúng ta sẽ thêm một hoặc cả hai đoạn mã sau.

Cập nhật ngày sửa đổi khi file được chỉnh sửa


CẬP NHẬT:

Phần này đã được cập nhật với phiên bản hook mới thông minh hơn. Nó sẽ không tăng modDatetime cho đến khi bài viết được xuất bản. Khi xuất bản lần đầu, đặt trạng thái draft thành first và xem điều kỳ diệu xảy ra.


# Các file đã chỉnh sửa, cập nhật 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 lấy các file đã được staged để commit. Kết quả sẽ như sau:

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

Chữ cái ở đầu dòng cho biết hành động đã thực hiện, ví dụ trên là file được thêm mới. File sửa đổi sẽ có M.

Chúng ta chuyển kết quả này vào lệnh grep để lọc các dòng bắt đầu bằng M (^(M)), theo sau là bất kỳ ký tự nào (.*) và kết thúc bằng đuôi .md (.(md)$). Điều này sẽ lọc ra các file markdown đã được sửa đổi.


Cải tiến - Rõ ràng hơn

Bạn có thể chỉ lọc các file markdown trong thư mục blog, vì chỉ những file này mới có frontmatter đúng định dạng.


Regex sẽ tách thành hai phần, chữ cái và đường dẫn file. Chúng ta sẽ chuyển danh sách này vào vòng lặp while để lặp qua từng dòng và gán chữ cái cho a, đường dẫn cho b. Hiện tại, chúng ta sẽ bỏ qua a.

Để biết trạng thái draft của file, chúng ta cần lấy frontmatter. Đoạn mã sau sử dụng cat để lấy nội dung file, sau đó dùng awk để tách theo dấu phân cách frontmatter (---) và lấy khối thứ hai (frontmatter, phần giữa hai dấu ---). Tiếp tục dùng awk để tìm dòng có key draft và in ra giá trị.

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

Bây giờ đã có giá trị của draft, chúng ta sẽ làm 1 trong 3 việc: cập nhật modDatetime thành thời điểm hiện tại (nếu draft là false if [ "$draft" = "false" ]; then), xóa modDatetime và đặt draft thành false (nếu draft là first if [ "$draft" = "first" ]; then), hoặc không làm gì (trong các trường hợp khác).

Phần tiếp theo với lệnh sed sẽ tìm trong frontmatter (---) của file để tìm key pubDatetime:, lấy toàn bộ dòng và thay thế bằng pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" với thời gian hiện tại đúng định dạng.

Sau khi thay thế, kết quả được ghi vào file tạm (> tmp), sau đó di chuyển (mv) file mới đè lên file cũ. Cuối cùng, file được thêm lại vào git để sẵn sàng commit.


LƯU Ý

Để lệnh sed hoạt động, frontmatter cần có sẵn key modDatetime. Có một số thay đổi khác bạn cần thực hiện để app có thể build với giá trị ngày rỗng, xem bên dưới


Thêm ngày cho file mới

Thêm ngày cho file mới cũng tương tự, nhưng lần này chúng ta lọc các dòng có ký tự A (thêm mới) và thay thế giá trị pubDatetime.

# File mới, thêm/cập nhật 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

Cải tiến - Chỉ lặp một lần

Chúng ta có thể dùng biến a để kiểm tra trong vòng lặp và cập nhật modDatetime hoặc thêm pubDatetime chỉ trong một vòng lặp.


Điền frontmatter

Nếu IDE của bạn hỗ trợ snippets, bạn có thể tạo snippet tùy chỉnh để điền frontmatter. Trong cho markdown v4 sẽ có sẵn snippet cho VSCode.

Thay đổi khi modDatetime rỗng

Để Astro có thể biên dịch markdown và xử lý đúng, nó cần biết các key trong frontmatter. Điều này được cấu hình trong file src/content/config.ts.

Để cho phép key có giá trị rỗng, bạn cần chỉnh dòng 10 để thêm hàm .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(),
    }),
});

Để tránh IDE báo lỗi trong các file engine blog, tôi cũng đã làm như sau:

  1. Thêm | null vào dòng 15 trong src/layouts/Layout.astro để thành như sau:
export interface Props {
  title?: string;
  author?: string;
  description?: string;
  ogImage?: string;
  canonicalURL?: string;
  pubDatetime?: Date;
  modDatetime?: Date | null;
}
  1. Thêm | null vào dòng 5 trong src/components/Datetime.tsx như sau:
interface DatetimesProps {
  pubDatetime: string | Date;
  modDatetime: string | Date | undefined | null;
}


Previous Post
So sánh React và Vue. Đâu là lựa chọn phù hợp cho bạn?