Một file được editor lưu bằng CRLF nhưng Git lại ghi LF vào index. Điều đó có thể đúng với chính sách repository, không phải Git làm mất nội dung. Muốn xử lý diff đổi cả file, bạn cần phân biệt byte trên đĩa, byte chuẩn bị commit và thuộc tính đang áp dụng cho đường dẫn.
Bài được đối chiếu ngày 29/10/2026 và thử bằng Git 2.54.0 trên macOS trong repository tạm. Demo chỉ viết hai file mới trong thư mục do mktemp tạo; không đổi cấu hình global, không sửa repository hiện tại. Cần Bash, Git và Python 3 để tạo byte xuống dòng một cách rõ ràng.
Kiểm tra trước khi thay đổi
Trong repository cần chẩn đoán, hai lệnh sau chỉ đọc thông tin. Thay example.txt bằng đường dẫn tương đối của file bạn đang xem:
git check-attr text eol -- example.txt
git ls-files --eol -- example.txt
Lệnh đầu trả thuộc tính có hiệu lực. Lệnh thứ hai, với file đã tracked, phân biệt i/... là nội dung trong index, w/... là nội dung ở working tree và attr/... là chính sách chuyển đổi. i/lf w/crlf không tự có nghĩa file sai hoặc cần bị ghi đè.
git-check-attr không quét byte để đo line ending; nó đọc attributes. git-ls-files mới có chế độ --eol để quan sát cả hai nơi. Cần cả chính sách lẫn trạng thái thực tế để giải thích kết quả.
Tạo fixture có LF trong index và CRLF trên đĩa
Chạy nguyên khối trong Bash. Subshell giữ các thay đổi thư mục và biến môi trường ở phạm vi demo. Template rỗng và config cách ly giúp tránh hook hoặc thiết lập cá nhân ảnh hưởng phép thử.
(
set -eu
demo_dir=$(mktemp -d)
export GIT_CONFIG_NOSYSTEM=1
export GIT_CONFIG_GLOBAL=/dev/null
git init -q --template= "$demo_dir"
cd "$demo_dir"
git config core.autocrlf false
git config core.safecrlf false
git config core.attributesFile /dev/null
python3 - <<'PY'
from pathlib import Path
Path('.gitattributes').write_text('example.txt text eol=lf\n')
Path('example.txt').write_bytes(b'alpha\r\nbeta\r\n')
PY
git add -- .gitattributes example.txt
git check-attr text eol -- example.txt
git ls-files --eol -- example.txt
git show :example.txt | python3 -c 'import sys; print(repr(sys.stdin.buffer.read()))'
python3 -c 'from pathlib import Path; print(repr(Path("example.txt").read_bytes()))'
)
Kết quả kiểm tra phần attributes là text: set, eol: lf. Dòng ls-files chứa i/lf, w/crlf và attr/text eol=lf. Hai phép đọc byte lần lượt cho b'alpha\nbeta\n' và b'alpha\r\nbeta\r\n'. Khoảng cách căn cột trong output có thể khác, nên đọc tên trường thay vì so nguyên dòng bằng mắt.
Không có commit nào được tạo. Ký hiệu :example.txt trong git show đọc blob đã stage. Đọc bằng Path.read_bytes() tránh việc chế độ text của Python tự chuẩn hóa newline rồi làm mất bằng chứng về CRLF.
Vì sao git add không sửa ngay file trên đĩa?
Với text, Git chuẩn hóa line ending về LF khi đưa nội dung vào index. eol=lf còn thể hiện chính sách khi checkout file. gitattributes phân biệt hai chiều chuyển đổi này.
Lệnh add trong fixture cập nhật index; không nên suy ra file đang mở trong editor lập tức bị viết lại. Editor vẫn có thể lưu CRLF sau đó. Nếu muốn working tree dùng LF, chỉnh chế độ lưu của editor theo chính sách và kiểm tra lại byte hoặc ls-files. Không cần hoàn tác file chỉ để đạt cùng một ký hiệu hiển thị.
Áp dụng chính sách vào repository đang có
Đọc git status --short và diff trước để biết đang có thay đổi nghiệp vụ hoặc nội dung staged nào khác. Thống nhất rule cho đúng loại file, tránh ép text lên ảnh hoặc dữ liệu nhị phân. .gitattributes nên được review cùng nhóm vì nó đi theo repository.
Với file text đã tracked từ trước, sau khi bổ sung rule, có thể cần chuẩn hóa lại nội dung trong index. Thực hiện ở thay đổi riêng đã được rà, giới hạn đúng đường dẫn:
git add -- .gitattributes
git add --renormalize -- example.txt
git diff --cached -- .gitattributes example.txt
git ls-files --eol -- example.txt
--renormalize là thao tác ghi index. Nó có thể stage cả chỉnh sửa nội dung đang có trong file đó, vì vậy không chạy khi chưa hiểu diff. Không thay bằng một lệnh áp dụng cả repository chỉ vì tiện copy/paste.
Lỗi thường gặp và bước tiếp theo
- Chỉ nhìn editor ghi CRLF rồi kết luận commit cũng chứa CRLF.
- Dùng check-attr như công cụ đọc byte; output của nó chỉ nói thuộc tính.
- Chỉnh core.autocrlf global để sửa một repository, gây ảnh hưởng các dự án khác.
- Trộn chuẩn hóa line ending với refactor lớn khiến review khó tách nguyên nhân.
Demo kiểm chứng cách Git stage text trong fixture, không bảo đảm mọi editor, filter hoặc repository có cấu hình lồng nhau sẽ cho cùng trạng thái. Khi chẩn đoán, đọc attributes hiệu lực của đúng đường dẫn, rồi so index và working tree. Ba thông tin đó thường giải thích được vì sao hai công cụ đang hiển thị khác nhau.

