- vừa được xem lúc

Git objects

0 0 32

Người đăng: LiGhT

Theo Viblo Asia

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 <_@.com> 1608524648 +0700
committer bui.nhat.duy <_@.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 <_@.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 <_@.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 <_@.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/

Bình luận

Bài viết tương tự

- vừa được xem lúc

Đặt tên commit message sao cho "tình nghĩa anh em chắc chắn bền lâu"????

. Lời mở đầu. .

1 1 737

- vừa được xem lúc

Tập hợp những câu lệnh GIT hữu dụng

Dưới đây là một vài ví dụ về các câu lệnh Git mà tôi thường dùng. git config --global user.name "John Doe". git config --global user.

0 0 68

- vừa được xem lúc

Cấu hình CI/CD với Github (phần 2): Trigger một work flow

Events trigger. Bạn có thể cấu hình cho workflows chạy khi có một sự kiện nào đó xảy ra trên GitHub, theo một lịch có sẵn hoặc cũng có thể là một sự kiện nào đó xảy ra ngoài GitHub.

0 0 80

- vừa được xem lúc

Cấu hình CI/CD với Github (phần 1): Một ít lý thuyết

CI/CD là gì. Về mặt khái niệm là vậy nhưng về mặt triển khai thì CI/CD là quá trình tự động thực hiện các quá trình build, test, release, deploy khi có các trigger như commit/merge code lên một branch định sẵn hoặc có thể là tự động chạy theo một lịch cố định.

0 0 128

- vừa được xem lúc

Giới thiệu về Git LFS

. Git LFS là gì . Git LFS làm điều này bằng cách thay thế các tệp lớn trong repo của bạn bằng một con trỏ nhỏ.

0 0 37

- vừa được xem lúc

Git workflow được Google và Facebook sử dụng có gì hay ho

Với developer thì Git hẳn là công cụ rất quen thuộc và không thể thiếu rồi. Thế nhưng có mấy ai thực sự hiểu được Git.

0 0 85