0

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

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:

  • Git 2.29 (2020) đã cho phép tạo repo với --object-format=sha256, lúc đó còn gắn mác experimental.
    • Git 2.42 bỏ cảnh báo experimental, nghĩa là format này được coi là ổn định về mặt on-disk.
    • Vấn đề lớn nhất là interoperability: một repo SHA-256 không thể push/fetch trực tiếp với repo SHA-1. Phần chuyển đổi qua lại (compat object format) vẫn đang được phát triển dần.
    • Hosting provider hỗ trợ chưa đồng đều. Trước khi dùng thật, hãy kiểm tra docs của platform bạn đang dùng.

Đ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:
                            
                            ```bash
                            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:
                            
                            - Regex `[0-9a-f]{40}` hoặc `[a-f0-9]{40}`
                            - Kiểm tra `len(sha) == 40` hay `sha.length === 40`
                            - Database column `CHAR(40)` hoặc `VARCHAR(40)`
                            - Hằng số `'0' * 40` (null SHA dùng trong hook `pre-receive`)
                            - Cắt chuỗi kiểu `line[:40]` khi parse output của `git log --format=raw`
                            
                            Script mình dùng với `ripgrep` 14.x:
                            
                            ```bash
                            #!/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ộ.
                                              
                                              ```mermaid
                                              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:
                                                                              
                                                                              ```python
                                                                              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ự:
                                                                                                                                      
                                                                                                                                      ```javascript
                                                                                                                                      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 để ý:
                                                                                                                                        
                                                                                                                                        - **Short hash trong URL hoặc log**: thường an toàn, nhưng nếu bạn dùng short hash làm key thì hãy kiểm tra cơ chế tránh trùng.
                                                                                                                                        - **Hook phía server**: `pre-receive` và `post-receive` nhận old/new SHA qua stdin, null SHA có độ dài theo format của repo.
                                                                                                                                        - **Thư viện Git**: libgit2, go-git, JGit có mức hỗ trợ SHA-256 khác nhau. Kiểm tra changelog của version bạn đang pin trước khi giả định là nó chạy được.
                                                                                                                                        
                                                                                                                                        ## 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.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.