← 목록

Preparing for Git 3.0's SHA-256 Default: Find What Breaks in Your Tooling Before It Breaks

devto 2026-10-02 원문 보기 ↗


Tuần này trên Hacker News có một bài khá nóng: Git 3.0 dự kiến đổi object format mặc định từ SHA-1 sang SHA-256, và tác giả cho rằng đây là một quyết định tốn kém. Mình không định tranh luận chuyện đúng hay sai. Câu hỏi thực tế hơn với team mình là: nếu một ngày repo mới tạo ra mặc định là SHA-256 thì những gì trong hệ thống của mình sẽ gãy? Câu trả lời thường không nằm ở Git, mà nằm ở hàng tá script CI, regex, schema database và tool nội bộ đang ngầm giả định rằng commit hash luôn dài đúng 40 ký tự hex.

Bài này là checklist mình đã dùng để audit tooling của team. Mất khoảng nửa ngày, phát hiện được 7 chỗ hardcode, và giờ thì bọn mình ngủ ngon hơn.

SHA-256 trong Git: hiện trạng thực tế

Tóm tắt nhanh để có cùng mental model:

Điểm quan trọng: commit hash SHA-256 dài 64 ký tự hex thay vì 40. Abbreviated hash (git log --oneline) vẫn ngắn như thường, nên khi nhìn bằng mắt bạn sẽ không thấy khác biệt gì. Lỗi chỉ lộ ra khi tool nào đó parse full hash.

flowchart LR
    A[git commit] --> B{object format}
    B -->|sha1| C[40 hex chars]
    B -->|sha256| D[64 hex chars]
    C --> E[CI scripts]
    D --> E
    E --> F[Regex / DB schema / API]
    F -->|hardcode 40| G[Bug âm thầm]

Bước 1: Tạo sandbox SHA-256 để test

Đừng đoán, hãy chạy thử. Kiểm tra version trước:

git --version   # cần >= 2.42 để thoải mái

# Tạo repo SHA-256
mkdir /tmp/sha256-lab && cd /tmp/sha256-lab
git init --object-format=sha256
git rev-parse --show-object-format   # in ra: sha256

echo 'hello' > a.txt
git add a.txt && git commit -m 'init'

git rev-parse HEAD
# 64 ký tự, ví dụ: 3f1c9a...e7b2 (dài gấp rưỡi bình thường)
git rev-parse HEAD | wc -c   # 65 (64 + newline)

# Kiểm tra config của repo
git config extensions.objectFormat   # sha256

Giờ copy repo này vào pipeline của bạn: chạy thử pre-commit hook, script release, tool changelog, bất cứ thứ gì đọc commit hash. Đây là cách nhanh nhất để thấy chỗ gãy.

Một mẹo nhỏ: nếu bạn muốn script tự biết repo đang dùng format nào, đừng đoán theo độ dài string. Hãy hỏi Git trực tiếp bằng git rev-parse --show-object-format.

Bước 2: Grep tìm các giả định 40 ký tự

Đây là phần mang lại giá trị nhiều nhất. Các pattern hay gặp:

Script mình dùng với ripgrep 14.x:

#!/usr/bin/env bash
# audit-sha1-assumptions.sh
set -euo pipefail

PATTERNS=(
  '\{40\}'                       # regex quantifier {40}
  'length\s*(===?|==)\s*40'      # JS/TS
  'len\([^)]*\)\s*==\s*40'       # Python/Go
  '(VAR)?CHAR\(40\)'             # SQL schema
  "'0'\s*\*\s*40"                # Python null sha
  '0{40}'                        # hardcoded null sha
  '\[:40\]'                      # slicing
)

for p in "${PATTERNS[@]}"; do
  echo "=== $p ==="
  rg -n --hidden -g '!.git' -g '!node_modules' -e "$p" . || true
done

Chạy nó trên monorepo, repo infra (Terraform, Ansible), và repo chứa GitHub Actions/GitLab CI config. Ở team mình, kết quả gồm: 2 regex trong tool tạo release note, 1 cột CHAR(40) trong bảng deployments, 1 hook pre-receive so sánh với '0' * 40, và vài chỗ validate input trong API nội bộ.

flowchart TD
    A[Chạy audit script] --> B[Danh sách match]
    B --> C{Loại?}
    C -->|Regex / validate| D[Đổi sang 40 hoặc 64]
    C -->|DB schema| E[Migration VARCHAR 64]
    C -->|Null SHA| F[Lấy độ dài từ git]
    D --> G[Test với repo sha256]
    E --> G
    F --> G

Bước 3: Sửa đúng cách, đừng chỉ đổi 40 thành 64

Sai lầm phổ biến là sửa {40} thành {64}. Như vậy bạn chỉ chuyển bug sang phía ngược lại. Trong giai đoạn chuyển tiếp (có thể kéo dài nhiều năm), hệ thống sẽ phải xử lý cả hai format cùng lúc.

Ví dụ helper Python mình dùng chung cho các service:

import re
import subprocess

HASH_LEN = {'sha1': 40, 'sha256': 64}
_FULL_HASH = re.compile(r'^(?:[0-9a-f]{40}|[0-9a-f]{64})$')

def is_full_hash(value: str) -> bool:
    return bool(_FULL_HASH.match(value.lower()))

def object_format(repo: str = '.') -> str:
    out = subprocess.run(
        ['git', '-C', repo, 'rev-parse', '--show-object-format'],
        capture_output=True, text=True, check=True,
    )
    return out.stdout.strip()

def null_hash(repo: str = '.') -> str:
    # Thay cho '0' * 40 hardcode trong hook
    return '0' * HASH_LEN[object_format(repo)]

if __name__ == '__main__':
    assert is_full_hash('a' * 40)
    assert is_full_hash('b' * 64)
    assert not is_full_hash('c' * 50)
    print(object_format(), null_hash())

Phía JavaScript cũng tương tự:

const FULL_HASH = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;

export function isFullHash(s) {
  return FULL_HASH.test(s);
}

Với database, migration đơn giản nhất là đổi sang VARCHAR(64). Nếu bạn có index trên cột này thì nên lên kế hoạch migration ngoài giờ cao điểm, vì với bảng lớn ALTER TABLE có thể lock khá lâu tùy engine.

Một số chỗ khác cần để ý:

Kết luận

Dù Git 3.0 ra mắt lúc nào và cộng đồng còn tranh cãi bao lâu, việc chuẩn bị cho tooling không tốn bao nhiêu công mà giúp bạn tránh được những bug âm thầm rất khó debug sau này. Checklist mình đề xuất:

  1. Nâng Git lên >= 2.42 trên máy dev và CI runner, tạo một repo sandbox bằng git init --object-format=sha256.
  2. Chạy audit script ở trên cho mọi repo có code đụng đến commit hash: CI config, hook, tool release, API nội bộ.
  3. Sửa để chấp nhận cả 40 và 64, đừng thay thế cứng. Lấy độ dài thật bằng git rev-parse --show-object-format.
  4. Migrate schema DB sang VARCHAR(64) sớm, khi bảng còn nhỏ.
  5. Thêm một job CI chạy test suite trên một fixture repo SHA-256, để regression bị chặn ngay từ PR.
  6. Chưa vội chuyển repo production sang SHA-256 cho đến khi hosting provider và toàn bộ toolchain của bạn hỗ trợ đầy đủ.

Nửa ngày công hôm nay, đổi lại không phải hoảng loạn khi một đồng nghiệp tạo repo mới bằng Git 3.0 và cả pipeline đỏ lòm mà không ai hiểu vì sao.