Git objects

    

Giới thiệu

    Hầu hết các developer đều ít nhiều sử dụng git trong công việc hàng ngày. Khi bắt đầu với git, chúng ta đều được học các câu lệnh quên thuộc như git add ., git commit -m '[Feat] Hello world'. Có bao giờ bạn thắc mắc, hệ thống git bên trong hoạt động như thế nào chưa. Trong bài viết này, mình và các bạn cùng tìm hiểu điều đó nhé.

Blob objects

    Git là một hệ thống tập tin theo dạng content-addressable( Mình để nguyên tên tiếng Anh do không tìm được từ phù hợp hoặc nghe sát nghĩa trong Tiếng Việt) . Vậy điều này có nghĩa là gì ? Nó có nghĩa là phần cốt lõi của Git là một kho dữ liệu theo dạng key-value. Bạn chèn bất kì nội dung nào một repo Git, Git sẽ trả lại bạn một key độc nhất cái mà bạn sau này dùng nó để lấy ra được nội dung đó.

    Bây giờ chúng ta hãy xem xét câu lệnh git hash-object, cái sẽ lấy dữ liệu và lưu trữ nó trong thư mục .git/objects và trả lại bạn key độc nhất để đề cập đến đối tượng dữ liệu đó.

    Đầu tiên bạn hãy khới tạo một repo Git mới, và xác nhận rằng không có thứ gì trong thư mục objects:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

    Git đã khởi tạo thư mục objects và tạo các thư mục con packinfo trong đó. Bây giờ chúng ta sẽ dùng git hash-object để tạo một đối tượng dữ liệu mới, và lưu nó bằng tay trong cơ sở dữ liệu Git:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

    Theo cách hiểu đơn giản nhất, git hash-object sẽ lấy nội dung bạn đưa cho nó và đơn thuần khóa độc nhất cái sẽ dùng để lưu nội dung đó trong cơ sở dữ liệu Git. Option -w dùng để ghi object tới database, không đơn thuần việc chỉ trả lại giá trị key.

    Đầu ra của câu lệnh trên là một đoạn hash 40 kí tự. Đây là kiểu hash SHA-1, một checksum của nội dung bạn lưu trữ. Bây giờ bạn có thể nhìn cách Git lưu trữ dữ liệu của bạn:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

    Bạn có thể thấy trong thư mục objects, nó đã có một file cho nội dung chúng ta mới tạo. Đây là cách Git lưu trữ nội dung, mỗi file mỗi mảnh dữ liệu, được đặt tên với mã SHA-1 checksum của nội dung và tiêu đề. Tên thư mục con là 2 kí tự đầu tiên của mã SHA-1, tên file là 38 kí tự còn lại.

    Bây giờ bạn có thể xem nội dung file với câu lệnh git cat-file. Câu lệnh này dùng cho việc giám sát các Git objects.

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

    Bây giờ chúng ta hãy thử tạo mới một file và lưu trữ chúng trong cơ sở dữ liệu Git.

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

    Sau đó ghi thêm nội dung mới tới file đó, và lưu nó lại lần nữa:

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

    Cơ sở dữ liệu đối tượng của bạn bây giờ sẽ bao gổm cả 2 phiên bản của file mới này ( cũng như nội dung bạn đã lưu trữ )

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

    Bây giờ bạn có thể xóa file test.txt, sau đó dùng Git để lấy lại từ cơ sở dữ liệu đối tượng. Đầu tiên là version đầu tiên:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

    hoặc version thứ hai:

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

Tree objects

    Kiểu Git object chúng ta sẽ xem xét sau đây là tree , cái giải quyết vấn đề về việc lưu trữ tên tệp, và cho phep bạn lưu trữ các tệp thành các nhóm. Git lưu trữ nội dung trong một phương thức tương tự với hệ thống tệp tin của UNIX, nhưng theo một cách đơn giản hơn. Tất cả nội dung đều lưu trữ nhiều các tree và blob objects. Một cây đơn bao gồm một hoặc nhiều lối vào, mỗi trong số chúng là một mã SHA-1 của blob, hoặc cây con cùng với mã truy cập. Ví dụ :

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

    Chúng ta thấy được thư mục con lib không phải là một blob object, nó trỏ vào 1 tree objects.

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb

    Về mặt khái niệm, dữ liệu đang lưu trữ như sau:

    Bạn có thể dễ dàng tạo riêng cây của bạn. Git thông thường tạo một tree băng cách lấy trạng thái hiện tại của vùng staging và viết một loạt cây từ đó. Để tạo một index với đầu vào đơn, phiên bản đầu của file test.txt, bạn có thể sử dụng câu lệnh git update-index. Bạn dùng câu lệnh này để thêm file test.txt vào vùng staging.

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

    Trong trường hợp này bạn bạn chỉ ra mode 100644, điều này có nghĩa đó là 1 file bình thường. Các lựa chọn khác đó là 100755, file có thể thực thi, 120000, symbolic link. Bây giờ bạn có thể sử dụng git write-tree để ghi vùng staging tới tree objects

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

    Bạn có thể verify đó là một tree bằng cách sử dụng git cat-file:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

    Bây giở bạn sẽ tạo một tree mới với phiên bản thứ hai của test.txt cũng như một file mới :

$ echo 'new file' > new.txt
$ git update-index --add --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt

    Vùng staging của bạn bây giờ có phiên bản mới của test.txt cũng như file new.txt.

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

    Trong trường hợp này bạn có thể đọc một trê đã có vào vùng staging:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

    Nếu bạn tạo ra một working directory từ cây bạn vừa ghi,bạn sẽ thấy 2 files ở phía trên, và 1 thư mục con bak . Git sẽ lưu trữ cấu trúc như sau: Bây giờ bạn sẽ 3 cây để đại diện cho các bản snapshot của thư mục của bạn. Nhưng vân đề là chúng ta phải ghi nhớ cả 3 mã SHA-1 để gọi lại bản snapshot. Bạn cũng không có thông tin gì về việc ai đã lưu nó. Bạn sẽ cần một loại object nữa đó là commit objects

Commit objects

    Để tạo một commit objects, ta sẽ dùng câu lệnh commit-tree, và chỉ ra mã SHA-1

$ echo 'First commit' | git commit-tree d8329f
861599522e1202127d2a60603f6a941f6dd1a3aa

    Bây giờ chúng ta hãy xem có gì bên trong commit object với git cat-file:

git cat-file -p 8615995
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author bui.nhat.duy <bui.nhat.duy@sun-asterisk.com> 1608524648 +0700
committer bui.nhat.duy <bui.nhat.duy@sun-asterisk.com> 1608524648 +0700

First commit

    Cấu trúc của commit-object khá đơn giản, nó chỉ ra top-level tree của bản snapshot của project. Thông tin người commit( sử dụng user.nameuser.email trong file config) và commit message. Tiếp theo bạn tạo ra 2 commit objects, mỗi cái sẽ trỏ đến commit ở phía trước nó:

 echo 'Second commit' | git commit-tree 0155eb -p 8615995
cb7991ef1875c9fc4e54178e8205498a707e2523
echo 'Third commit'  | git commit-tree 3c4e9c -p cb7991e
02307660f033eac36ef7c0ca82dff9f4faf6a29f

    Mỗi commit objects sẽ trỏ đến 1 trong 3 bản snapshot bạn tạo trước đó. Bây giờ bạn đã có một lịch sử Git commit thật sự, và có thể nhìn nó thông qua câu lệnh quen thuộc git log,

it log --stat 023076
commit 02307660f033eac36ef7c0ca82dff9f4faf6a29f
Author: bui.nhat.duy <bui.nhat.duy@sun-asterisk.com>
Date:   Mon Dec 21 11:31:48 2020 +0700

    Third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit cb7991ef1875c9fc4e54178e8205498a707e2523
Author: bui.nhat.duy <bui.nhat.duy@sun-asterisk.com>
Date:   Mon Dec 21 11:30:52 2020 +0700

    Second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit 861599522e1202127d2a60603f6a941f6dd1a3aa
Author: bui.nhat.duy <bui.nhat.duy@sun-asterisk.com>
Date:   Mon Dec 21 11:24:08 2020 +0700

    First commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

    Thật thú vị phải không, bạn vừa sử dụng các phương thức ở mức low-level để tạo ra lịch sử commit Git. Đó là bản chất của git addgit commit - nó tạo ra blob objects cho các file thay đổi, update lại index, ghi lên cây, và ghi các commit objects để trỏ đến các top-level tree. Có 3 loại Git object chính đó là blob, tree, và commit. Đây là tất cả các object ở thời điểm hiện tại :

$ find .git/objects -type f
.git/objects/86/1599522e1202127d2a60603f6a941f6dd1a3aa
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92
.git/objects/02/307660f033eac36ef7c0ca82dff9f4faf6a29f
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
.git/objects/cb/7991ef1875c9fc4e54178e8205498a707e2523
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a

Object Storage

    Như mình đã đề cập trước đó, sẽ có 1 header với mỗi object bạn commit tới Git object. Vậy chúng ta cùng tìm hiểu xem cách Git lưu trữ các objects của nó. Bạn sẽ thấy cách để lưu một blob object - trong trường hợp này là đoạn string 'Hello World'. Chúng ta sẽ dùng Irb để khám phá :

$ irb
> content = "Hello World"
=> "Hello World" 

    Git đầu tiên khởi tạo một header để xác định loại object- trong trường hợp này một blob. Để làm điều dó, Git thêm một khoảng trống theo sau đó là kích cỡ byte của content, và cuối cùng là 1 byte null.

> header = "blob #{content.bytesize}\0"
 => "blob 11\u0000" 

    Git nối phần header trên với nội dung ban đầu, sau đó tính toán SHA-1 checksym của nội dung mới đó.

> store = header + content
=> "blob 11\u0000Hello World" 
> require 'digest/sha1'
=> true 
sha1 = Digest::SHA1.hexdigest(store)
=> "5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689" 

    Giờ chúng ta hãy so sánh với đầu ra của git hash-object.

echo -n "Hello World" | git hash-object --stdin
5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689

    Git sẽ nén nội dung mới với zlib, cái bạn có thể làm với thư viện zlib trong Ruby:

>  require 'zlib'
 => true 
> zlib_content = Zlib::Deflate.deflate(store)
 => "x\x9CK\xCA\xC9OR04d\xF0H\xCD\xC9\xC9W\b\xCF/\xCAI\x01\x00;{\x06>" 

    Cuối cùng bạn ghi nội dung của đoạn zlib tới 1 object trên đĩa. Bạn sẽ xác định đường dẫn của object bạn cần ghi ( 2 kí tự đầu là tên thư mục con, 38 kí tự còn lại là tên tệp ). Hãy làm nốt nó với Ruby nào :

> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
 => ".git/objects/5e/1c309dae7f45e0f39b1bf3ac3cd9db12e7d689" 
>  require 'fileutils'
 => true 
> FileUtils.mkdir_p(File.dirname(path))
 => [".git/objects/5e"] 
>  File.open(path, 'w') { |f| f.write zlib_content }
=> 27

    Bây giờ chúng ta hãy so sánh với nội dung của object sử dụng git cat-file

$ git cat-file -p 5e1c3f39b1bf3ac3cd9db12e7d689
Hello World

    Chính là nó, bạn vừa tạo ra một Git object hợp lệ.

Kết luận

    Trong bài viết này mình đã giới thiệu về cách những câu lệnh hàng ngày git add, git commit hoạt động. Hi vọng sau bài viết này các bạn có thể hiểu thêm về Git objects, cách Git hoạt động bên trong.

Link tham khảo

    https://medium.com/mindorks/what-is-git-object-model-6009c271ca66 https://aboullaite.me/deep-dive-into-git/

Nguồn: Viblo

Bình luận
Vui lòng đăng nhập để bình luận
Một số bài viết liên quan