Trình quản lý phiên bản (Git)

Hệ thống quản lý phiên bản (Version Control Systems - VCS) là các công cụ để theo dõi những thay đổi trong mã nguồn (hay các thư mục và tập tin). Đúng như tên gọi của chúng, các công cụ này giúp ta lưu giữ lịch sử thay đổi và thậm chí là tạo điều kiện cho việc hợp tác với người khác. Các VCS theo dõi sự thay đổi của các tập và thư mục con bằng cách lưu giữ toàn bộ trạng thái của chúng qua các “snapshot” (cơ sở dữ liệu “ảnh chụp”). Những snapshot này được lưu trong một tập trên cùng hay tập gốc của dự án ta muốn quản lý phiên bản. Ngoài ra các VCS cũng chứa đựng các metadata (thông tin phụ) như tác giả của “snapshot”, tin nhắn và giải thích bởi tác giả cho “snapshot” đó, v.v.

Vì sao ta cần các trình quản lý phiên bản? Thập chí ngay khi bạn lập trình cho một dư án cá nhân, VCS có thể cho phép ta xem lại sự thay đổi, mốc thời gian của chúng, lí do cho các thay đổi đó và các tiến trình trên các branch (nhánh) khác nhau của cây lịch sử . Khi làm việc nhóm, đây lại là một công cụ vô cùng hiệu quả để theo dõi thay đổi từ các đồng sự và giải quyết các conflicts (mâu thuẫn) từ thay đổi mã nguồn của ta và họ.

Các VCS hiện đại cũng có thể trả lời các câu hỏi sau một cách dễ dàng và đa phần tự động:

Mặc dù có nhiều trình VCS, nhưng Git là công cụ thông dụng nhất cho việc quản lý phiên bản. Hình ảnh truyện tranh sau từ XKCD comic phần nào cho ta thấy danh tiếng của Git:

xkcd 1597

(dịch: A - Đây là Git, nó theo dõi việc hợp tác trong các dự án có mã nguồn bằng mộc biểu đồ cây xinh xắn. B - Ngon, thế dùng nó thế nào? A - Tôi không biết. Cứ nhớ các câu lệnh shell này và gõ chúng để cập nhật giữa mã của chúng ta. Nếu có lỗi thì lưu giữ thay đổi của bạn ở chỗ khác, rồi xóa nguyên dự án đó đi và tải về một bản lưu giữ mới toanh…)

Vì giao diện của Git là một dạng leaky abstraction (trừu tượng có rò rĩ), tìm hiểu về cách sử dụng Git theo phương pháp top-down (từ cao xuống thấp, bắt đầu từ giao điện câu lệnh của nó) có thể gây ra vô vàn sự mất phương hướng (đặc biệt cho người mới học). Bạn hoàn toàn có thể học thuộc một loạt các câu lệnh, coi chúng như thần chú và làm theo bức hình trên nếu có gì đó sai.

Mặc dù Git có một giao diện thật sự là tệ hại, triết lý thiệt kế và hoạt động của nó vô cùng ấn tượng. Trong khi một giao diện tệ hại này cần phải được học thuộc lòng, một thiết kế ấn tượng có thể được hiểu tận. Vì lí do này, chúng ta sẽ học Git theo cách bottom-up (từ dưới lên trên), bắt đầu từ data model (mô hình dữ liệu) rồi sau đó mới đến các câu lệnh. Khi ta đã hiểu mô hình dữ liệu của nó, ta hoàn toàn có thể giải thích cách các câu lênh Git hoạt động (bằng việc thay đổi mô hình dữ liệu trên).

Mô hình dữ liệu của Git

Có vô vàn cách để thiết kế một VCS. Tuy nhiên Git có một mô hình dữ liệu được thiết kế kỹ càng để tạo nên các tính năng tuyệt vời của một VCS như lưu giữ lịch sử, hỗ trợ các branch và cho phép hợp tác giữa người dùng.

Snapshots (“Ảnh chụp”)

Git mô phỏng lịch sử của các tập tin và thư mục nó theo dõi dưới dạng chuỗi các snapshot trong một thư mục top-level (thư mục gốc của dự án). Theo ngôn ngữ của Git, một tập tin được gọi là “blob”, và nó chỉ là một đống bytes dự liệu. Thư mục thì lại gọi là “tree” (cây), và nó lưu giữ tên đến các tree hay blob khác (thư mục có thể chứa thư mục con). Một snapshot là cây gốc trên cùng mà ta đang theo dõi. Ví dụ như ta có một cây thư mục như sau:

<root> (tree)
|
+- foo (tree)
|  |
|  + bar.txt (blob, nội dung = "hello world")
|
+- baz.txt (blob, nội dung = "git is wonderful")

Cây thư mục gốc gồm hai thành phần, một tree (cây con) tên “foo” (và nó chứa một thành phần là blob “bar.txt”), và blob “baz.txt”.

Mô phỏng lịch sử: cách kết nối các snapshot

Các VCS nên kết nối các snapshot như thế nào để có nghĩa? Một mô hình đơn giản đó là linear history (lịch sử tuyến tính). Mô hình lịch sử này cấu thành từ các snapshot theo thứ tự thời gian mà chúng được tạo. Tuy nhiên, vì vô vàn lí do, Git không dùng một mô hình đơn giản như vậy.

Trong Git, lịch sử được mô phỏng bằng một Directed Acyclic Graph (Đồ thị định hướng không tuần hoàn - DAG). Đấy là một từ phức tạp và đầy toán học, nhưng đừng sợ. Điều này có nghĩa là mỗi snapshot trong Git thì được kết nối, chỉ hướng về một set (tập) các “bố mẹ”, những snapshot đi trước nó trong chuỗi thời gian. Gọi là một tập các bố mẹ thay cho một bố hoặc mẹ (như mô hình linear history nói trên) vì một snapshot có thể có nhiều tổ tiên khác nhau, như trong việc merging (hợp nhất) nhiều branch phát triển song song chẳng hạn.

Các snapshot này được gọi là commit (cam kết). Việc hình dung một history có thể cho ta một thứ như sau:

o <-- o <-- o <-- o
            ^
             \
              --- o <-- o

Trong bức hình ASCII ở trên, kí tự o tượng trưng cho commit (hay snapshot). Các mũi tên chỉ đến bố mẹ của mỗi commit (đó là mối quan hệ “có trước nó”, không phải là “có sau nó”). Đến cái commit thứ 3 thì biểu đồ lịch sử được chia làm hai nhánh riêng. Điều này có thể tương ứng với hai chức năng đang được phát triển song song, độc lập với nhau. Trong tương lai, các branch này có thể được hợp nhất tạo nên một snapshot có cả hai chức năng này. Điều này tạo ra một đồ thị lịch sử như sau:


o <-- o <-- o <-- o <---- o
            ^            /
             \          v
              --- o <-- o

Commit trong Git là bất biến, nghĩa là các lỗi trong commit không thể nào sữa được. Rất may những lỗi này chỉ có nghĩa là các thay đổi đến dòng lịch sử (nội dung của blob hay cấu trúc của tree được theo dõi chẳng hạn) sẽ tạo ra các commit hoàn toàn mới và các references (xem ở phần dưới đây) được cập nhật để chỉ đến các commit vừa tạo (để sửa lỗi sai trong thư mục hay tập tin chẳng hạn).

Mô hình dữ liệu viết theo pseudocode (mã giả)

Mã giả cho mô hình dữ liệu của git có thể có hình thái như sau:

// Một blob hay tập tin là một đống byte
type blob = array<byte>

// Một tree hay thư mục chứa các tập tin và thư mục con
type tree = map<string, tree | blob>

// Một commit có bố mẹ, các thông tin phụ và cây thư mục gốc nó theo dõi 
type commit = struct {
    parents: array<commit>
    author: string
    message: string
    snapshot: tree
}

Đây là một mô hình đơn giản và sạch cho lưu giữ lịch sử thay đổi.

Vật thể và content-addressing (truy cập địa chỉ từ nội dung)

Một “vật thể” là một blob, tree hay là commit:

type object = blob | tree | commit

Trong kho lưu trữ dữ liệu của Git, các vật thể đều có thể được xác định và truy cập từ nội dụng của chúng (đúng hơn là kết quả của hàm băm SHA-1 hash trên nội dung của chúng )

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

Các blob, tree và commit giống nhau theo hướng này: chúng là các vật thể. Khi chúng chỉ đến hay tham khảo các vật thể khác, chúng không trực tiếp lưu trữ hay chứa đựng chúng trên đĩa cứng, mà chỉ đến bằng kết quả của hàm băm trên nội dung của các vật thể này.

Ví dụ như, tree gốc của thư mục trong phần trên (được hình dung bằng câu lệnh git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d) sẽ có dạng nội dung như sau:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

Tree gốc này có nội dung là các con trỏ đến nội dung của nó (như trên), rồi baz.txt (một blob) và foo (một tree). Nếu ta dùng kết quả hàm băm tương ứng với con trỏ tới baz.txt bằng câu lệnh git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85, nội dung có được (văn bản trong baz.txt) là như sau:

git is wonderful

References - Các con trỏ tham khảo

Các snapshot đều có thể được xác định bằng kết quả hàm băm SHA-1 lên nội dung của chúng. Thật là bất tiện vì loài người không hề giỏi ghi nhớ các chuỗi 40 kí tự thập lục phân.

Cách giải quyết của Git cho vấn nạn này các tên dễ đọc cho các kết quả của hàm băm trên, gọi là “reference”. Reference là con trỏ đến commit. Khác với các objects (vật thể), bị bất biến, các reference là các biến số (được thay đổi để chỉ đến một commit khác trong chuỗi lịch sử). Ví dụ như master là một reference thường chỉ đến commit mới nhất của branch chính của dự án ta đang phát triển. <!–

References

Now, all snapshots can be identified by their SHA-1 hashes. That’s inconvenient, because humans aren’t good at remembering strings of 40 hexadecimal characters.

Git’s solution to this problem is human-readable names for SHA-1 hashes, called “references”. References are pointers to commits. Unlike objects, which are immutable, references are mutable (can be updated to point to a new commit). For example, the master reference usually points to the latest commit in the main branch of development. –>

references = map<string, string>

def update_reference(name, id):
    references[name] = id

def read_reference(name):
    return references[name]

def load_reference(name_or_id):
    if name_or_id in references:
        return load(references[name_or_id])
    else:
        return load(name_or_id)

Với cơ sở dữ liệu này, Git có thể dùng những cái tên dễ nhớ hơn như “master” để chỉ đến các snapshot (hay commit) nhất định trong lịch sử, thay vì chuỗi thập nhị phân nói trên.

Mội chi tiết thú vị là chúng ta thường cần một khái niệm cho việc “ta đang ở đâu trong hiện tại” của chuỗi lịch sử. Việc này rất cần thiết để ta biết khi ta tạo một snapshot hay commit mới, vị trí tương đối của chúng dựa trên bố mẹ nào. Trong Git, khái niệm “ta đang ở đâu hiện tại” là một reference đặc biệt gọi là “HEAD”. <!– With this, Git can use human-readable names like “master” to refer to a particular snapshot in the history, instead of a long hexadecimal string.

One detail is that we often want a notion of “where we currently are” in the history, so that when we take a new snapshot, we know what it is relative to (how we set the parents field of the commit). In Git, that “where we currently are” is a special reference called “HEAD”. –>

Repository

Cuối cùng ta có thể định nghĩa (tương đối) repository (kho chứa): đó là các dữ liệu của các objectsreferences.

Trên đĩa cứng, tất cả những gì Git lưu trữ là các object và reference: đó là những thứ quan trọng trong mô hình dữ liệu của Git. Tất cả câu lệnh bắt đầu bằng git tương ứng với việc thay đổi đồ thị DAG bằng các thêm object hay thêm và cập nhật các reference

Mỗi khi nhập một câu lệnh, hãy thử nghĩ về những thay đổi mà câu lệnh đang làm lên mô hình dữ liệu bên dưới. Ngược lại, nếu bạn đang nghĩ đến việc thay đổi đồ thị DAG, ví dụ như “bỏ đi những thay đổi chưa được commit (cam kết) và đưa con trỏ “master” đến commit 5d83f9e, thì chắc chắn rằng có một câu lệnh tương ứng với hành đông đó (trong trường hợp này là git checkout master; git reset --hard 5d83f9e) <!–

Repositories

Finally, we can define what (roughly) is a Git repository: it is the data objects and references.

On disk, all Git stores are objects and references: that’s all there is to Git’s data model. All git commands map to some manipulation of the commit DAG by adding objects and adding/updating references.

Whenever you’re typing in any command, think about what manipulation the command is making to the underlying graph data structure. Conversely, if you’re trying to make a particular kind of change to the commit DAG, e.g. “discard uncommitted changes and make the ‘master’ ref point to commit 5d83f9e”, there’s probably a command to do it (e.g. in this case, git checkout master; git reset --hard 5d83f9e). –>

Staging area - khu vực trung gian

Đây lại là một khái niệm riêng biệt với mô hình dữ liệu nêu trên, nhưng nó lại là một phần của giao diện để ta tạo các commit.

Hình dung thế này, để tạo nên một phương thức để “chụp lại” các snapshot như ở phần trên, ta có thể tạo một câu lệnh tên là “create snapshot”. Nó sẽ tạo nên snapshot từ trạng thái hiện tại của working directory (thư mục làm việc của ta) mà các VCS đang theo dõi. Nhiều VCS thì chỉ đơn giản như vậy, nhưng với Git thì không. Ta cần các snapshot “sạch” và không phải lúc nào bưng nguyên trạng thái hiện tại của working directory vào lịch sử cũng cần thiết cả.

Giả dụ như bạn đang viết hai chức năng riêng biệt và cần tạo hai commit riêng biệt, cái thứ nhất giới thiệu chức năng một và cái thứ hai thì giới thiệu chức năng thứ hai. Hay trường hợp khác khi bạn có vô vàn câu print (in) để phát hiện lỗi và logic để sửa lỗi; bạn chắc chắn chỉ muốn commit phần sửa lỗi và bỏ qua phần print.

Git có thể cho phép ta thực hiện các trường hợp trên bằng cách cho phép ta nêu ra phần thay đổi nào nên được đưa vào snapshot tiếp theo qua một khu vực trung gian (cách biệt giữa tất cả các thay đổi (unstaged) và các thay đổi đã được lưu trữ vào lịch sử (commit)) <!–

Staging area

This is another concept that’s orthogonal to the data model, but it’s a part of the interface to create commits.

One way you might imagine implementing snapshotting as described above is to have a “create snapshot” command that creates a new snapshot based on the current state of the working directory. Some version control tools work like this, but not Git. We want clean snapshots, and it might not always be ideal to make a snapshot from the current state. For example, imagine a scenario where you’ve implemented two separate features, and you want to create two separate commits, where the first introduces the first feature, and the next introduces the second feature. Or imagine a scenario where you have debugging print statements added all over your code, along with a bugfix; you want to commit the bugfix while discarding all the print statements.

Git accommodates such scenarios by allowing you to specify which modifications should be included in the next snapshot through a mechanism called the “staging area”. –>

Giao diện dòng lệnh của Git

Để tránh việc lặp thông tin, chúng tôi sẽ không giải thích các câu lệnh ở dưới một cách chi tiết. Hãy đọc Pro Git hay xem video bài giảng nếu cần thiết. <!–

Git command-line interface

To avoid duplicating information, we’re not going to explain the commands below in detail. See the highly recommended Pro Git for more information, or watch the lecture video. –>

Cơ bản

Chia nhánh (branching) và hợp nhánh (merging)

Remotes - Dịch vụ luư trữ mã nguồn từ xa

Undo - Quay lại, Hủy hành động

Nâng cao

Khác

Các tài liệu khác

Bài tập

  1. Nếu bạn không có kinh nghiệm dùng Git, hãy thử đọc vài chương đầu của
    Pro Git hoặc học theo một bài hướng dẫn như Learn Git Branching. Khi bạn làm chúng, hãy thử liên hệ các câu lệnh Git với data model của nó.
  2. Clone repository cho trang web khóa học này.
    1. Tìm hiểu về version history của nó bằng cách hiển thị theo dạng biểu đồ.
    2. Ai là người cuối cùng thay đổi file README.md? (Gợi ý: dùng git log với một đối số)
    3. Lời nhắn liên quan đến lần thay đổi cuối cùng trong dòng collections: của file _config.yml là gì? (Gợi ý: dùng git blamegit show)
  3. Một trong những sai lầm khi sử dụng Git là khi commit một số lượng lớn file không nên được quản lý bởi Git, hay các thông tin nhạy cảm (passwords, mã Auth API, etc). Thử add một file vào repo, rồi tạo một vài commit rồi sau đó xóa chúng đi trong lịch sử. (Bạn có thể tham khảo thêm ở đây).

  4. Thử clone một vài repo từ trang Github, rồi thay đổi một ít files trong đó. Điều gì sẽ xảy ra khi ta dùng git stash? Bạn quan sát được gì khi chạy câu lệnh git log --all --oneline? Chạy git stash pop để xóa đi những tác vụ bạn vừa làm khi chạy git stash. Khi nào thì câu lệnh git stash sẽ có ít cho ta?

  5. Như các trình câu lệnh khác, Git có một file tùy chỉnh ( hay dotfile ) tên là ~/.gitconfig. Hãy tạo một alias (biệt hiệu) git graph trong file trên để thực hiện câu lệnh git log --all --graph --decorate --oneline một cách ngắn gọn.

  6. Bạn có thể tùy chỉnh các file hoặc thư mục mà git bỏ qua (ignore) trong dotfile ~/.gitignore_global sau khi chạy git config --global core.excludesfile ~/.gitignore_global. Hãy làm vậy và tạo một file ignore trên toàn hệ thống để bỏ qua việc theo dõi các file phụ liên quan đến hệ điều hành hay các file tạm của trình biên tập mã nguồn, như .DS_Store.

  7. Fork repo của khóa học này từ website, rồi tìm lỗi chính tả hay một điểm gì đó bạn có thể làm tốt hơn, rồi tạo một pull request trên Github. Scientists](https://eagain.net/articles/git-for-computer-scientists/) is a short explanation of Git’s data model, with less pseudocode and more fancy diagrams than these lecture notes.

Edit this page.

Licensed under CC BY-NC-SA.