Chương 8. Bí Quyết của Git

Chúng ta mổ xẻ để hiểu được làm thế nào mà Git có thể thi hành kỳ diệu như vậy. Tôi sẽ không thể nói quá chi tiết được. Nếu bạn muốn có được sự mô tả chỉ tiết thì hãy đọc sổ tay hướng dẫn sử dụng Git.

Tính Ẩn

Git làm việc có vẻ kín đáo? Chỉ cần nói riêng về việc sử dụng lệnh commitmerge, bạn có thể làm việc mà không cần biết đến sự tồn tại của hệ thống quản lý mã nguồn. Cho đến khi bạn cần nó, và cho đến khi bạn vui sướng vì Git đã trông coi mã nguồn cho bạn trong suốt thời gian qua.

Các hệ thống quản lý mã nguồn khác ép buộc bạn luôn luôn phải tranh đấu với thói quan liêu. Quyền truy cập của các tập tin có thể là chỉ cho phép đọc trừ phi bạn nói rõ với máy chủ trung tâm là các tập tin nào bạn muốn chỉnh sửa. Tốc độ làm việc của phần lớn các lệnh sẽ tỷ lệ nghịch với số lượng người sử dụng. Mọi công việc sẽ đình trệ khi mạng máy tính hay máy chủ ngừng hoạt động.

Đối lập với hạn chế trên, Git đơn giản giữ lịch sử của dự án của bạn tại thư mục .git trong thư mục làm việc của bạn. Đây là bản sao lịch sử của riêng bạn, do vậy bạn có thể làm việc không cần mạng cho đến khi cần truyền thông với những người khác. Bạn có toàn quyền quyết định với các tập tin của mình bởi vì Git có thể tạo lại trạng thái đã ghi lại từ .git bất kỳ lúc nào một cách dễ dàng.

Toàn Vẹn Dữ Liệu

Phần lớn mọi người sử dụng phương pháp mã hóa để giữ cho thông tin của mình không bị nhòm ngó, nhưng có thứ quan trọng không kém đó là giữ cho thông tin của mình được toàn vẹn. Chính việc sử dụng hàm băm mã hóa đã làm ngăn ngừa sự sai hỏng dữ liệu do rủi ro hay ác ý.

Giá trị SHA1 có thể coi như là một số định danh 160-bit không trùng lắp cho mỗi chuỗi ký tự bạn dùng trong đời sống của mình. Trên thực tế nó còn làm được nhiều hơn thế: nó có thể thực hiện điều trên với mọi chuỗi ký tự mà mọi người có thể sử dụng trong suốt cuộc đời của mình.

Bản thân giá trị SHA1 cũng là một chuỗi ký tự, chúng ta có thể băm chuỗi có chứa giá trị băm khác. Khả năng quan sát đơn giản này cực kỳ hữu dụng: tra cứu hash chains (tra cứu theo các chuỗi móc xích với nhau bằng giá trị băm). Sau này chúng ta sẽ thấy làm cách nào Git sử dụng nó để mà đảm bảo tính toàn vẹn của dữ liệu.

Tóm lại, Git lưu giữ dữ liệu của bạn trong thư mục con .git/objects, thay vì sử dụng tên tập tin như thông thường, bạn sẽ chỉ nhìn thấy ID của chúng. Bằng cách sử dụng ID để làm tên tập tin, cũng tốt như là cách sử dụng kỹ thuật lockfiles (khóa tập tin) và timestamp (theo dõi thời gian của tập tin), Git biến hệ thống tập tin thông thường bất kỳ nào trở thành một cơ sở dữ liệu hiệu quả và mạnh mẽ.

Thông Minh

Làm thể nào mà Git biết bạn đã đổi tên một tập tin, dù là bạn chẳng bao giờ đề cập đến điều này một cách rõ ràng? Chắc chắn rồi, bạn có lẽ đã chạy lệnh git mv, nhưng nó chính xác giống hệt như việc chạy lệnh git rm sau đó là lệnh git add.

Git khám phá ra cách truy tìm các tập tin đã được đổi tên hay sao chép giữa các phiên bản liên tiếp. Trên thực tế, nó có thể tìm ra từng đoạn mã nguồn đã bị di chuyển hay sao chép giữa các tập tin! Dẫu cho nó không thể xử lý được mọi trường hợp, nó làm khá tốt, và đặc tính này luôn luôn được phát triển. Nếu nó không làm việc với bạn, hãy thử bật các tùy chọn dành cho việc phát hiện sự sao chép, và nên cất nhắc đến việc cập nhật.

Mục Lục

Với mọi tập tin được theo dõi, Git ghi lại các thông tin như là kích thước, thời gian tạo và lần cuối sửa đổi trong một tập tin được biết đến là một mục lục index. Để xác định rõ một tập tin có bị thay đổi hay không, Git so sánh nó ở thời điểm hiện tại với phần lưu giữ trong bộ nhớ. Nếu chúng giống nhau, thế thì Git có thể bỏ qua việc đọc tập tin đó lại lần nữa.

Bởi vì gọi lệnh “stat” nhanh hơn đáng kể so với đọc tập tin, nếu bạn chỉ chỉnh sửa vài tập tin, Git có thể cập nhật trạng thái của nó cực kỳ nhanh chóng.

Chúng ta đã nói trước rằng mục lục (index) là vùng làm việc của trạng thái. Tại sao lại là một chùm tập tin stat vùng stage? Bởi vì lệnh add đặt các tập tin vào trong cơ sở dữ liệu của Git và cập nhật những stat này, trong lúc lệnh commit được thực hiện, mà không có tùy chọn, tạo ra một commit trên cơ sở chỉ trên các stat và các tập tin đã sẵn có trong cơ sở dữ liệu.

Nguồn Gốc của Git

Linux Kernel Mailing List post này miêu tả các sự kiện nối tiếp nhau về Git. Toàn bộ tuyến này chỉ dành cho các sử gia đam mê Git.

Đối tượng Cơ Sở Dữ Liệu

Mỗi một phiên bản của dữ liệu của bạn được giữ trong đối tượng cơ sở dữ liệu (object database), mà nó nằm trong thư mục con .git/objects; cái khác nằm trong thư mục .git/ lưu giữ ít dữ liệu hơn: mục lục, tên các nhánh, các thẻ tag, các tùy chọn cấu hình, nhật ký, vị trí hiện tại của head của lần commit, và những thứ tương tự như thế. Đối tượng cơ sở dữ liệu cho đến bây giờ vẫn là phần tử cơ bản xuất sắc nhất, và là cội nguồn sức mạnh của Git.

Mỗi tập tin trong .git/objects là một đối tượng. Ở đây có 3 loại đối tượng liên quan đến chúng ta: đối tượng nội dung tập tin blob, đối tượng cây tree, và đối tượng lần chuyển giao commit.

Đối Tượng Blob

Đầu tiên, hãy tạo một tập tin bất kỳ. Đặt cho nó một cái tên, tên gì cũng được. Trong một thư mục rỗng:

$ echo sweet > YOUR_FILENAME
$ git init
$ git add .
$ find .git/objects -type f

Bạn sẽ thấy .git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d.

Làm sao mà tôi biết được tập tin khi không thấy tên của nó? Đó là bởi vì đó là giá trị băm SHA1 của:

"blob" SP "6" NUL "sweet" LF

là aa823728ea7d592acc69b36875a482cdf3fd5c8d, với SP là khoảng trắng, NUL là byte có giá trị bằng 0 và LF ký tự xuống dòng. Bạn có thể xác minh lại bằng lệnh sau:

$ printf "blob 6\000sweet\n" | sha1sum

Git sử dụng cách lấy nội dung để làm tên cho tập tin: tập tin không được lưu trữ như theo tên của chúng, mà bằng giá trị băm dữ liệu mà chúng chứa, trong tập tin chúng ta gọi là một đối tượng blob. Chúng ta có thể nghĩ giá trị băm như là một định danh duy nhất cho nội dung của tập tin, do vậy ta có tên tập tin được định danh bởi nội dung của nó. Phần khởi đầu blob 6 đơn thuần chỉ là phần đầu để thể hiện kiểu của đối tượng và độ dài của nó tính bằng byte; việc làm này làm đơn giản hóa việc vận hành bên trong Git.

Đến đây tôi có thể dễ dàng đoán được bạn nghĩ gì. Tên của tập tin là không thích hợp: chỉ khi có dữ liệu bên trong được sử dụng để xây dựng nên đối tượng blob.

Bạn có lẽ sẽ kinh ngạc với những gì xảy ra với các tập tin có cùng nội dung. Hãy thử thêm một bản sao một tập tin nào đó của bạn, với bất kỳ một cái tên nào cũng được. Nội dung của .git/objects ở tại cùng một chỗ cho dù bạn thêm vào bao nhiêu lần đi chăng nữa. Git chỉ lưu giữ dữ liệu một lần duy nhất.

Nhưng dẫu sao, các tập tin nằm trong .git/objects đã bị nén lại theo chuẩn zlib do vậy bạn không thể xem chúng một cách trực tiếp được. Đọc chúng thông qua zpipe -d, hay gõ:

$ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d

lệnh này trình bày đối tượng được cho ở dạng dễ đọc trên màn hình.

Đối Tượng Tree

Nhưng mà tên tập tin ở đâu? Chúng phải được lưu giữ ở đâu đó chứ. Git lấy tên tập tin trong quá trình commit:

$ git commit  # Gõ chú thích.
$ find .git/objects -type f

Bạn sẽ thấy ba đối tượng. Ở thời điểm này tôi không thể nói hai tập tin mới này là cái gì, hãy nghĩ nó là một phần của tên tập tin bạn đang xét. Chúng ta sẽ xuất phát từ giả định bạn chọn “rose”. Nếu bạn không làm thế, bạn có thể viết lại lịch sử để làm cho nó giống như bạn đã làm thế:

$ git filter-branch --tree-filter 'mv YOUR_FILENAME rose'
$ find .git/objects -type f

Bây giờ bạn có lẽ nhìn thấy tập tin .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9, bởi vì đây là giá trị băm SHA1 của nội dung của nó:

"tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d

Xác thực tập tin này chứa thông tin như trên bằng cách gõ:

$ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch

Với lệnh zpipe, ta có thể dễ dàng xác thực một giá trị băm:

$ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum

Sự thẩm tra giá trị băm thì rắc rối hơn thông qua lệnh cat-file bởi vì phần kết xuất của nó chứa nhiều thông tin hơn đối tượng tập tin thường không bị nén.

Tập tin này là một đối tượng cây tree: một danh sách các hàng bao gồm kiểu tập tin, tên tập tin, và giá trị băm. Trong ví dụ của chúng ta, kiểu tập tin là 100644, điều này có nghĩa ‘rose` là tập tin bình thường, và giá trị băm là một đối tượng blob mà nó chứa nội dung của `rose’. Dạng tập tin khác có thể là tập tin thực thi, liên kết mềm hay các thư mục. Trong trường hợp cuối, giá trị băm sẽ chỉ đến đối tượng cây tree.

Nếu bạn đã chạy lệnh filter-branch, bạn sẽ có các đối tượng cũ bạn không cần đến sau này nữa. Mặc dù chúng sẽ bị loại bỏ một cách tự động một khi thời hạn chấm dứt đã đến, nhưng chúng ta sẽ xóa chúng ngay bây giờ theo cách dưới đây:

$ rm -r .git/refs/original
$ git reflog expire --expire=now --all
$ git prune

Với các dự án thật bạn nên tránh việc sử dụng lệnh như trên, làm như thế bạn đã phá hủy dữ liệu sao lưu dự phòng. Nếu bạn muốn làm sạch kho chứa, cách hay nhất là tạo một bản sao mới. Cũng thế, hãy cẩn thận khi thao tác trực tiếp với thư mục .git: điều gì xảy ra nếu một lệnh Git khác cũng đang thực thi cùng lúc, hay là mất điện đột ngột? Đại khái, refs có thể được xóa bằng lệnh git update-ref -d, mặc dù thường thường nó an toàn hơn xóa refs/original bằng tay.

Commit

Chúng tôi đã giảng giải cho bạn 2 trong số 3 đối tượng của Git. Cái thứ 3 chính là commit. Nội dung của nó buộc chặt vào phần chú thích của lần commit cũng như thời gian, ngày tháng chúng được tạo ra. Để cho khớp với những thứ chúng ta có ở đây, chúng ta sẽ phải chỉnh nó một chút:

$ git commit --amend -m Shakespeare  # Thay đổi phần chú thích.
$ git filter-branch --env-filter 'export
    GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800"
    GIT_AUTHOR_NAME="Alice"
    GIT_AUTHOR_EMAIL="alice@example.com"
    GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800"
    GIT_COMMITTER_NAME="Bob"
    GIT_COMMITTER_EMAIL="bob@example.com"'  # dấu vết thời gian và tác giả đã bị gian lận.
$ find .git/objects -type f

Bạn có thể thấy .git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187 là giá trị băm SHA1 của nội dung của nó:

"commit 158" NUL
"tree 05b217bb859794d08bb9e4f7f04cbda4b207fbe9" LF
"author Alice <alice@example.com> 1234567890 -0800" LF
"committer Bob <bob@example.com> 1234567890 -0800" LF
LF
"Shakespeare" LF

Như ở phần trước, bạn có thể chạy lệnh zpipe hay cat-file để tự mình trông thấy.

Đây là lần commit đầu tiên, do vậy lần commit này không có cha, nhưng những lần commit sau sẽ luôn luôn chứa it nhất là một dòng chỉ định commit cha.

Khó Phân Biệt Được sự Thần Kỳ

Bí mật của Git dường như có vẻ đơn giản. Nó giống như bạn có thể trộn lẫn cùng nhau một ít kịch bản và một ít mã C mà đun trong vài giờ: nhào trộn của quá trình hoạt động của hệ thống tập tin và mã băm SHA1, bày biện thêm với các khóa và đồng bộ hóa tập tin để tăng vị ngon. Trên thực tế, những mô tả như thế với Git các phiên bản trước kia là chính xác. Tuy nhiên, ngoài chiêu bài đóng gói để tiết kiệm không gian lưu trữ, sử dụng mục lục để tiết kiệm thời gian ra, giờ chúng ta còn biết thêm làm cách nào Git khéo léo thay đổi hệ thống tập tin thành một cơ sở dữ liệu hoàn hảo cho việc quản lý mã nguồn.

Ví dụ, nếu một tập tin bất kỳ nào đó trong đối tượng cơ sở dữ liệu bị sai hỏng bởi lỗi do ổ đĩa, thế thì giá trị băm tương ứng của nó sẽ không đúng nữa, điều này sẽ mang lại rắc rối cho chúng ta. Bằng việc băm giá trị băm của đối tượng khác, chúng ta có thể duy trì tính toàn vẹn ở tất cả các mức. Commit là hạt nhân, thật vậy đấy, mỗi lần commit không bao giờ ghi lại nửa vời: chúng ta có thể chỉ tính toán mã băm của lần commit và lưu giữ giá trị của nó trong cơ sở dữ liệu sau khi chúng ta đã sẵn sàng lưu tất cả các đối tượng là trees, blobs và cha của các lần commit thích hợp. Đối tượng cơ sở dữ liệu không bị ảnh hưởng bởi các sự cố ngắt quãng bất ngờ như là mất điện đột ngột chẳng hạn.

Chúng ta có thể làm thất bại ngay cả những kẻ phá hoại ranh mãnh. Giả sử người nào đó lén lút sửa chữa nội dung của một tập tin trong một phiên bản cũ của dự án. Để giữ đối tượng cơ sở dữ liệu vẫn hoạt động tốt, họ đồng thời cũng phải thay đổi giá trị băm của đối tượng blob tương ứng vì lẽ rằng nó bây giờ đã khác trước. Điều đó có nghĩa là họ sẽ phải thay đổi giá trị băm của một đối tượng tree có liên quan đến tập tin, và việc chỉnh sửa giá trị băm của tất cả các đối tượng commit kéo theo như là tree, thêm nữa là các giá trị băm của toàn bộ các lần commit con cháu của nó. Cái này kéo theo giá trị băm của head tại kho trung tâm không giống với thứ đó tại kho chứa sai hỏng. Bằng cách theo dõi sự tương khớp giá trị băm chúng ta có thể xác định được tập tin bị sai hỏng, cũng như là lần commit nơi mà nó lần đầu bị hư hỏng.

Nói ngắn gọn, dùng 20 byte để đại diện cho lần commit cuối là an toàn, việc cố tình sửa đổi nội dung một kho chứa Git là điều không thể thực hiện được.

Đặc tính nào của Git là trứ danh nhất? Nhánh? Trộn? Hay Tags? Chỉ là chi tiết. Head hiện hành được giữ trong tập tin .git/HEAD, mà nó có chứa giá trị băm của một đối tượng commit. Giá trị băm được cập nhật trong quá trình commit cũng như là một số lệnh khác. Nhánh đại thể cũng tương tự: chúng là các tập tin trong thư mục .git/refs/heads. Tags cũng thế: chúng ở tại .git/refs/tags nhưng chúng được cập nhật bởi các lệnh khác nhau.