Hệ thống quản lý phiên bản & VCS HostingVersion Control Systems & VCS Hosting
Thuộc bộ kiến thức DevOps Roadmap.
Tổng quan
Hệ thống quản lý phiên bản (Version Control System — VCS) ghi lại mọi thay đổi của mã nguồn theo thời gian, giúp nhiều người cùng làm việc trên một codebase mà không ghi đè lên nhau, xem lại lịch sử, và quay về bất kỳ trạng thái nào trước đó. Git — một VCS phân tán do Linus Torvalds tạo ra năm 2005 để quản lý mã nguồn nhân Linux — đã trở thành chuẩn mặc định: mỗi developer giữ một bản sao đầy đủ của repository, nên có thể làm việc offline và không có điểm chết duy nhất (single point of failure).
Với DevOps, version control không chỉ là tiện ích cho developer — nó là nền móng của toàn bộ pipeline. Hệ thống CI/CD được kích hoạt bởi commit và pull request; infrastructure-as-code, manifest Kubernetes và định nghĩa pipeline đều nằm trong Git; các công cụ GitOps (Argo CD, Flux) coi Git repository là nguồn chân lý duy nhất (single source of truth) cho những gì đang chạy trên production. Cái gì không nằm trong version control thì coi như không tồn tại — bạn không thể review, audit, rollback hay tái tạo lại nó.
Các nền tảng VCS hosting (GitHub, GitLab, Bitbucket) bổ sung lớp cộng tác phía trên Git: pull/merge request, code review, branch protection, quản lý issue và CI/CD tích hợp sẵn. Việc chọn nền tảng và chiến lược branching quyết định tốc độ và độ an toàn khi team release sản phẩm. Trang này tập trung gần như hoàn toàn vào Git vì nó đã thắng thị trường một cách áp đảo — nhưng các khái niệm (lịch sử, branching, merging, review) đều áp dụng cho mọi VCS.
Lịch sử ngắn gọn của version control
Hiểu Git đến từ đâu sẽ giúp bạn hiểu vì sao nó hoạt động như vậy:
- Local VCS (RCS, ~1982) — version hóa từng file đơn lẻ trên một máy bằng patch set. Không có cộng tác.
- Centralized VCS / CVCS (CVS 1990, Subversion/SVN 2000) — một server trung tâm giữ toàn bộ lịch sử; client checkout một bản working copy. Mô hình tư duy đơn giản, nhưng server là điểm chết duy nhất và mọi thao tác (log, diff, commit) đều cần kết nối mạng.
- Distributed VCS / DVCS (Git 2005, Mercurial 2005) — mỗi bản clone là một repository đầy đủ với toàn bộ lịch sử. Branching và merging rẻ, đa số thao tác chạy local (nên tức thì), và không bắt buộc phải có server trung tâm (dù team quy ước một server, ví dụ
origin).
Mô hình phân tán của Git là điều quan trọng nhất cần thấm nhuần: máy của bạn giữ toàn bộ lịch sử, nên git log, git diff, git blame, git bisect và commit đều chạy offline ở tốc độ bộ nhớ.
Kiến thức nền tảng
Ba “cây” (tree) và mô hình object
Mô hình tư duy của Git dựa trên ba “cây” (trạng thái) và một cơ sở dữ liệu object định danh bằng nội dung.
File đi qua ba khu vực:
working directory → staging area (index) → repository (lịch sử đã commit)
- Working directory — các file thực tế bạn đang chỉnh sửa trên đĩa.
- Staging area / index — commit tiếp theo mà bạn dựng dần bằng
git add. Staging cho phép bạn tạo một commit sạch, logic từ một working tree lộn xộn (git add -pđể stage từng đoạn hunk). - Repository — lịch sử bất biến đã commit trong
.git/.
Git lưu snapshot, không lưu diff. Bên trong có bốn loại object, mỗi loại định danh bằng hash của nội dung:
| Object | Là gì |
|---|---|
| blob | Nội dung của một file (không có tên file, không metadata) |
| tree | Danh sách thư mục: tên → blob và sub-tree (tên file và mode nằm ở đây) |
| commit | Một snapshot: trỏ đến một root tree, commit cha, author/committer, message |
| tag (annotated) | Con trỏ có tên, có thể ký, trỏ đến một object (thường là commit) với message riêng |
Vì object định danh bằng nội dung, nội dung giống nhau chỉ lưu một lần, và mọi thay đổi đều làm đổi hash — lịch sử có tính chống giả mạo (tamper-evident). Git đang chuyển từ hash SHA-1 sang SHA-256 để loại bỏ rủi ro va chạm (collision) về lý thuyết.
Các reference chính (con trỏ vào đồ thị object):
- Commit — snapshot bất biến của toàn bộ cây, định danh bằng hash, trỏ đến commit cha. Merge commit có hai (hoặc nhiều) cha.
- Branch — con trỏ di động tới một commit, lưu dưới dạng file một dòng trong
.git/refs/heads/. Tạo và xóa gần như miễn phí — tạo branch chỉ là ghi một hash 40 ký tự vào file. - Tag — con trỏ cố định tới một commit, thường đánh dấu release (
v1.4.2). Lightweight tag chỉ là một ref; annotated tag là object đầy đủ có tác giả, ngày, message và chữ ký GPG tùy chọn (dùng annotated tag cho release). - HEAD — con trỏ tới branch (hoặc commit) đang checkout. Bình thường
HEAD→ một branch → một commit. KhiHEADtrỏ thẳng vào một commit, bạn đang ở trạng thái detached HEAD. - Remote — tham chiếu tới một bản sao khác của repository (mặc định là
origin). Remote-tracking branch nhưorigin/mainlà bản cache local của Git về vị trí branch trên remote tại lầnfetchgần nhất.
git init # tạo repository
git add file.py # đưa thay đổi vào staging
git add -p # stage từng hunk tương tác
git commit -m "Add parser" # ghi lại snapshot
git log --oneline --graph --all # trực quan hóa DAG các commit
git tag -a v1.0.0 -m "Release" # tạo annotated tag
git push origin main --tags # đẩy branch và tag lên remote
git cat-file -p HEAD # xem object commit thô
Đồ thị commit, fetch / pull / push
Lịch sử là một đồ thị có hướng không chu trình (DAG) của các commit, không phải một đường thẳng. Hiểu ba thao tác mạng sẽ tránh được đa số nhầm lẫn:
git fetch— tải object mới và cập nhật remote-tracking ref (origin/*). Không đụng đến working tree hay branch local. Luôn an toàn.git pull—fetch+ tích hợp (mặc định làmerge, hoặcrebasevới--rebase). Vì nó thay đổi branch của bạn, hãy ưu tiênfetchrồi xem trước khi tích hợp. Đặtgit config pull.rebase true(hoặcpull.ff only) để tránh merge commit bất ngờ.git push— đẩy commit của bạn và dời branch trên remote. Bị từ chối nếu không phải fast-forward (người khác đã push trước) — fetch, tích hợp, rồi push lại.
Merge vs rebase
Cả hai đều tích hợp thay đổi từ branch này vào branch khác; khác nhau ở lịch sử tạo ra.
| Khía cạnh | git merge | git rebase |
|---|---|---|
| Lịch sử | Giữ nguyên cả hai nhánh; thêm merge commit (trừ khi fast-forward) | Viết lại commit của bạn lên trên nhánh đích; lịch sử tuyến tính |
| Hash commit | Không đổi | Bị viết lại (hash mới, object commit mới) |
| Khả năng truy vết | Merge commit ghi lại chính xác thời điểm/nội dung tích hợp | Mất ngữ cảnh “đây là code phát triển song song” |
| Xử lý conflict | Một lần, trong merge commit | Có thể một lần cho mỗi commit được replay |
| An toàn trên branch dùng chung | Có | Không — tuyệt đối không rebase commit mà người khác đã pull |
| Dùng khi | Gộp feature hoàn chỉnh vào main | Cập nhật feature branch với main mới nhất; dọn lịch sử local trước khi mở PR |
Fast-forward merge xảy ra khi nhánh đích chưa rẽ nhánh: Git chỉ trượt con trỏ về phía trước, không tạo merge commit. Dùng --no-ff để ép tạo merge commit khi bạn muốn ghi lại ranh giới của feature.
# Cập nhật feature branch với main mới nhất, giữ lịch sử tuyến tính
git checkout feature/login
git fetch origin
git rebase origin/main # replay commit của mình lên trên main
# xử lý conflict xong thì:
git rebase --continue
git push --force-with-lease # force-push an toàn cho branch đã rebase
# Dọn lịch sử local trước khi mở PR
git rebase -i HEAD~5 # squash/reword/reorder 5 commit gần nhất
Quy tắc: rebase phần việc chưa publish của mình; merge mọi thứ đã chia sẻ. --force-with-lease an toàn hơn --force: nó từ chối ghi đè remote nếu người khác đã push trong lúc đó.
Xử lý conflict
Conflict xảy ra khi hai branch thay đổi cùng những dòng. Git đánh dấu và dừng lại:
<<<<<<< HEAD
timeout = 30
=======
timeout = 60
>>>>>>> feature/login
Xử lý bằng cách sửa về kết quả mong muốn, git add file đó, rồi tiếp tục (git merge --continue / git rebase --continue). Công cụ hữu ích: git mergetool, git checkout --ours/--theirs <file>, và git rerere (tái sử dụng cách xử lý đã ghi) cho các conflict lặp lại trên branch sống lâu.
Hoàn tác thay đổi
Điều đáng an tâm nhất về Git: công việc đã commit rất khó mất thật sự. Nắm được những lệnh này thì bạn có thể thử nghiệm không sợ hãi.
git restore file.py # bỏ thay đổi trong working tree (trước đây: git checkout -- file)
git restore --staged file.py # bỏ khỏi staging, giữ thay đổi trong working tree
git commit --amend # viết lại commit gần nhất (chỉ local)
git revert <sha> # tạo commit mới đảo ngược một commit (an toàn trên branch chung)
git reset --soft <sha> # dời con trỏ branch, giữ thay đổi trong staging
git reset --mixed <sha> # dời con trỏ, bỏ khỏi staging (mặc định)
git reset --hard <sha> # dời con trỏ, xóa mọi thứ (chỉ local, phá hủy)
git reflog # nhật ký mọi vị trí HEAD từng đi qua — lưới an toàn của bạn
git stash / git stash pop # cất tạm thay đổi để đổi ngữ cảnh
git reflog ghi lại mọi bước dịch chuyển của HEAD trong ~90 ngày, nên kể cả reset --hard hay branch đã xóa vẫn cứu được: tìm hash của commit đã mất trong reflog rồi git checkout hoặc git branch nó trở lại.
Khái niệm chính
Các chiến lược branching
Mô hình branching là quyết định ở cấp team, đánh đổi giữa tần suất release và chi phí quy trình.
| Chiến lược | Cách hoạt động | Phù hợp với | Đánh đổi |
|---|---|---|---|
| Trunk-Based Development | Mọi người commit vào main (hoặc branch rất ngắn hạn, < 1 ngày). Feature flag che tính năng chưa xong. | Team có CI mạnh, hướng tới continuous deployment | Đòi hỏi test tự động tốt và kỷ luật feature flag |
| GitHub Flow | Tạo branch từ main → PR → review → merge → deploy. Chỉ một branch dài hạn. | Web app/service release thường xuyên | Không có branch release/staging riêng; dựa vào pipeline deploy |
| Git Flow | main + develop dài hạn, kèm branch feature/, release/, hotfix/. | Phần mềm đóng gói theo phiên bản (desktop app, thư viện), release theo lịch | Quá nặng cho SaaS; nhiều merge; feedback chậm |
| GitLab Flow | GitHub Flow cộng thêm environment branch (staging, production) hoặc release branch. | Team cần quy trình promote qua từng môi trường rõ ràng | Environment branch có thể lệch khỏi main nếu không tự động hóa |
Xu hướng DevOps hiện đại (nghiên cứu DORA, sách Accelerate) nhất quán ủng hộ trunk-based development với branch ngắn hạn: ít conflict hơn, feedback nhanh hơn, tần suất deploy cao hơn và tỉ lệ lỗi khi thay đổi thấp hơn. Git Flow nặng nề hơn được thiết kế cho các đợt release desktop theo lịch và thường là thừa với service deploy nhiều lần mỗi ngày.
Feature flag là yếu tố khiến trunk-based development khả thi: merge code chưa xong vào main sau một flag đang tắt trên production, tách rời deploy (đưa binary lên) khỏi release (bật tính năng). Điều này cũng cho phép canary rollout và kill switch tức thì.
So sánh nền tảng VCS hosting
| Tiêu chí | GitHub | GitLab | Bitbucket |
|---|---|---|---|
| CI/CD tích hợp | GitHub Actions (marketplace khổng lồ) | GitLab CI/CD (rất trưởng thành, một file .gitlab-ci.yml) | Bitbucket Pipelines |
| Tùy chọn self-hosted | GitHub Enterprise Server | GitLab CE/EE (rất phổ biến khi self-host) | Bitbucket Data Center |
| Đơn vị review | Pull Request (PR) | Merge Request (MR) | Pull Request (PR) |
| Mô hình phân quyền | Branch protection + ruleset | Protected branch + push rule | Branch permission |
| Hệ sinh thái | Cộng đồng OSS lớn nhất, Marketplace, Copilot, Codespaces | Một ứng dụng cho toàn bộ vòng đời DevOps (SCM, CI, registry, security) | Tích hợp sâu với Jira/Atlassian |
| CODEOWNERS chi tiết | Có | Có (+ approval rule) | Có (bản Premium) |
Cả ba đều nói cùng giao thức Git, nên đổi host chủ yếu là chuyện migrate issue/PR và config CI — bản thân repository có tính di động.
Quy trình PR/MR và code review
Vòng đời điển hình của một pull request:
- Tạo branch từ
main, commit nhỏ và tập trung. - Mở PR/MR sớm (dạng draft) — CI tự động chạy mỗi lần push.
- Reviewer góp ý; tác giả push bản sửa (mỗi lần push chạy lại CI).
- Check bắt buộc pass → đủ approval → merge.
- Xóa branch; pipeline deploy chạy từ
main.
Phương thức merge — lựa chọn định hình lịch sử của bạn:
| Phương thức | Kết quả trên main | Khi nào dùng |
|---|---|---|
| Merge commit | Giữ mọi commit của branch + một merge commit | Bạn coi trọng toàn bộ dấu vết phát triển |
| Squash and merge | Một commit gọn gàng cho mỗi PR | Commit WIP lộn xộn; muốn main đọc như một thay đổi cho mỗi feature |
| Rebase and merge | Replay commit tuyến tính, không merge commit | Bạn giữ commit sạch và muốn lịch sử tuyến tính |
Văn hóa review quan trọng hơn công cụ: giữ PR nhỏ (lý tưởng < 400 dòng thay đổi), review nhanh, góp ý vào code chứ không vào con người, và dùng “request changes” chừng mực. Review lan tỏa kiến thức và bắt lỗi sớm hơn bất kỳ giai đoạn nào sau đó.
Protected branch và policy
Branch protection rule biến quy trình thành ràng buộc bắt buộc. Trên main (và các release branch) thường:
- Yêu cầu PR trước khi merge (cấm push thẳng).
- Yêu cầu N approval (thường 1–2), và loại bỏ approval cũ khi có push mới.
- Yêu cầu status check pass (CI, lint, security scan) và phải cập nhật với base.
- Yêu cầu giải quyết hết conversation, signed commit, và/hoặc lịch sử tuyến tính.
- Cấm force push và xóa branch.
- Tùy chọn yêu cầu review từ CODEOWNERS — file ánh xạ path tới team sở hữu để đúng người được tự động chỉ định.
# .github/CODEOWNERS
/infra/ @org/platform-team
*.tf @org/platform-team
/services/billing/ @org/billing-team @alice
Monorepo vs polyrepo
| Monorepo | Polyrepo | |
|---|---|---|
| Chia sẻ code | Dễ — cùng một repo, một phiên bản cho mọi thứ | Qua package đã publish, có version |
| Thay đổi atomic xuyên project | Có — một commit trải nhiều service | Không — phải phối hợp release nhiều repo |
| Quản lý dependency | Một lockfile, không có “diamond dependency” | Mỗi repo pin version riêng (có thể lệch) |
| Độ phức tạp CI | Cần build chọn lọc (Bazel, Nx, Turborepo, path filter) nếu không sẽ build lại toàn bộ | Pipeline đơn giản theo từng repo |
| Phân quyền | Thô (tốt nhất theo thư mục) | Theo từng repository, gọn gàng |
| Kích thước repo / tooling | Có thể phình rất to; cần partial clone, sparse checkout, VFS | Mỗi repo giữ nhỏ |
| Ví dụ | Google, Meta, Uber | Đa số shop microservice |
Không có lựa chọn nào tốt tuyệt đối. Monorepo tối ưu cho thay đổi atomic và chia sẻ code nhưng đòi hỏi tooling build nghiêm túc khi mở rộng; polyrepo cho isolation sạch nhưng đẩy độ phức tạp vào việc phối hợp version và quản lý dependency. Quy mô team, độ trưởng thành tooling và mức độ ràng buộc giữa các service quyết định lựa chọn.
Git hooks
Hook là các script Git chạy tại những sự kiện trong vòng đời — tự động hóa tự kích hoạt mà không cần ai nhớ chạy.
Phía client (local, có thể bị bỏ qua): pre-commit (lint/format trước khi tạo commit), commit-msg (kiểm tra message, ví dụ Conventional Commits), pre-push (chạy test trước khi publish). Phía server (có thẩm quyền, client không bỏ qua được): pre-receive và update (từ chối push vi phạm policy), post-receive (kích hoạt deploy, thông báo — nền tảng của nhiều hệ thống CI và GitOps).
# .git/hooks/pre-commit (phải chmod +x cho executable)
#!/bin/sh
ruff check . || exit 1 # chặn commit nếu lint fail
Vì .git/hooks/ không được version hóa và client hook có thể bị bỏ qua, các team quản lý hook bằng framework pre-commit hoặc Husky (Node.js) để hook được version hóa, chia sẻ và cài tự động — và luôn kiểm tra lại cùng luật đó trong CI, thứ không thể bỏ qua.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: detect-private-key # rào chắn secret rẻ, giá trị cao
- id: check-merge-conflict
Xử lý file lớn và quy mô monorepo
Git lưu mọi phiên bản của mọi file mãi mãi, nên commit file binary lớn (media, dataset, artifact biên dịch) sẽ phình lịch sử vĩnh viễn. Cách giảm thiểu:
- Git LFS — thay file lớn bằng object con trỏ nhỏ và lưu bytes thật trên server riêng.
.gitattributes— chuẩn hóa line ending (* text=auto), đánh dấu binary, và cấu hình filter LFS.- Partial/shallow clone —
git clone --depth 1(shallow),--filter=blob:none(blobless), và sparse checkout cho phép làm việc với một phần của monorepo khổng lồ.
Các công cụ mạnh đáng biết
git bisect— tìm kiếm nhị phân trên lịch sử để tìm commit gây bug trong O(log n) bước; có thể tự động hóa hoàn toàn với script test (git bisect run ./test.sh).git blame/git log -S "text"(pickaxe) — tìm khi nào và vì sao một dòng hoặc chuỗi thay đổi.git worktree— checkout nhiều branch vào các thư mục riêng từ một repo (ví dụ build hotfix mà không cần stash).git cherry-pick <sha>— sao chép một commit cụ thể sang branch hiện tại (ví dụ backport bản sửa sang release branch).git submodule/ subtree — nhúng một repo vào repo khác (submodule mạnh nhưng hay gây nhầm lẫn; dùng có chủ đích).
Best Practices
- Commit nhỏ, commit thường xuyên, message có ý nghĩa — commit nhỏ dễ review, dễ bisect, dễ revert. Viết subject theo lối mệnh lệnh (“Add retry to uploader”), dưới ~50 ký tự, và giải thích vì sao trong phần body. Theo chuẩn như Conventional Commits (
feat:,fix:,chore:) để tự động sinh changelog và semantic versioning. - Giữ branch ngắn hạn — merge trong một hai ngày. Branch sống lâu tích lũy conflict, che giấu công việc và làm chậm feedback tích hợp (kết luận cốt lõi của DORA). Tích hợp
mainvào branch của bạn thường xuyên. - Bảo vệ
main— bắt buộc PR, CI pass và ít nhất một review; cấm force push và push thẳng. Production chỉ được build từ branch được bảo vệ. - Không bao giờ commit secret — dùng
.gitignore, secret scanning (GitHub secret scanning/push protection, gitleaks, trufflehog) trong hookpre-commitvà trong CI, cùng một secrets manager thật. Secret đã lọt vào lịch sử Git thì phải rotate, không chỉ xóa — nó tồn tại mãi trong các clone và reflog. - Rebase việc local, merge việc đã chia sẻ — không viết lại lịch sử người khác có thể đã pull; dùng
--force-with-lease, không bao giờ--forcetrần, khi buộc phải push branch đã rebase. - Dùng
git reverttrên branch chung — hoàn tác bằng một commit mới, giữ mọi clone nhất quán, thay vì viết lại lịch sử đã publish. - Tag release và tự động hóa versioning — annotated tag (và có ký) cùng pipeline release chạy bằng CI giúp truy vết mọi artifact trên production về đúng một commit. Áp dụng SemVer.
- Áp dụng CODEOWNERS và required review — đưa thay đổi đến đúng người có chuyên môn, nâng chất lượng review, lan tỏa kiến thức và tránh thay đổi đơn phương lên các path quan trọng.
- Tự động hóa quality gate bằng hook và CI — lint, format, test và scan ở
pre-commitđể feedback nhanh tại local, và lặp lại trong CI như nơi có thẩm quyền (hook có thể bị bỏ qua; CI bắt buộc thì không). - Lưu mọi thứ dưới dạng code trong Git — code ứng dụng, hạ tầng (Terraform), pipeline, manifest Kubernetes, dashboard, alert và runbook. Đây là tiền đề của GitOps: repo là nguồn chân lý và mọi drift đều phát hiện và revert được.
- Dùng
.gitattributescho line ending và Git LFS cho file binary lớn —* text=autotránh xáo trộn CRLF/LF giữa các OS; LFS tránh phình lịch sử. - Viết
.gitignoretốt ngay từ đầu — loại trừ artifact build, dependency (node_modules), file env local và rác IDE. Dùng mẫu từ github.com/github/gitignore; commit file sinh tự động gây diff nhiễu và conflict. - Học
git bisectvàgit reflog— bisect tìm commit gây regression trong thời gian logarit; reflog cứu lại commit “đã mất” sau một reset sai hoặc branch bị xóa. Biết chúng tồn tại biến Git từ đáng sợ thành an toàn. - Ký commit và tag — ký GPG/SSH (hoặc verified commit của GitHub) chứng minh tác giả và tránh commit giả mạo trong chuỗi cung ứng nhạy cảm về bảo mật.
- Ưu tiên squash-merge cho feature PR trên repo lịch sử tuyến tính — một commit sạch cho mỗi feature giữ
maindễ đọc và revert dễ dàng, trong khi chi tiết đầy đủ vẫn nằm trên branch (đã xóa) và trong PR. - Giữ repository là nguồn chân lý, không phải bản sao chép — quyết định, config và quy trình nằm trong repo (qua file, PR template,
CODEOWNERS), không nằm trong kiến thức truyền miệng hay lịch sử chat.
Tài liệu tham khảo
- Sách Pro Git (miễn phí, chính thức): https://git-scm.com/book/en/v2
- Tài liệu/tham chiếu chính thức của Git: https://git-scm.com/docs
- Git internals (object, ref) — Pro Git chương 10: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
- roadmap.sh DevOps roadmap: https://roadmap.sh/devops
- GitHub Docs — About pull requests: https://docs.github.com/en/pull-requests
- GitHub Docs — About protected branches: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches
- GitLab Docs — Merge requests: https://docs.gitlab.com/ee/user/project/merge_requests/
- Trunk-Based Development: https://trunkbaseddevelopment.com/
- Atlassian Git tutorials (merge vs rebase, Git Flow): https://www.atlassian.com/git/tutorials
- Conventional Commits: https://www.conventionalcommits.org/
- Semantic Versioning: https://semver.org/
- Framework pre-commit: https://pre-commit.com/
- Git LFS: https://git-lfs.com/
- Bộ sưu tập mẫu .gitignore của GitHub: https://github.com/github/gitignore
- DORA / Google Cloud — Năng lực DevOps (version control, trunk-based dev): https://dora.dev/capabilities/
Part of the DevOps Roadmap knowledge base.
Overview
Version control systems (VCS) track every change made to a codebase over time, letting teams collaborate on the same files without overwriting each other’s work, review history, and roll back to any previous state. Git — a distributed VCS created by Linus Torvalds in 2005 to manage Linux kernel development — has become the de facto standard: every developer has a full copy of the repository, so work continues offline and no single server is a point of failure.
For DevOps, version control is not just a developer convenience — it is the foundation of the entire delivery pipeline. CI/CD systems trigger on commits and pull requests; infrastructure-as-code, Kubernetes manifests, and pipeline definitions all live in Git; GitOps tools (Argo CD, Flux) treat a Git repository as the single source of truth for what runs in production. If it isn’t in version control, it effectively doesn’t exist — you cannot review it, audit it, roll it back, or reproduce it.
VCS hosting platforms (GitHub, GitLab, Bitbucket) add the collaboration layer on top of Git: pull/merge requests, code review, branch protection, issue tracking, and built-in CI/CD. Choosing a platform and a branching strategy shapes how fast and safely a team can ship. This page focuses almost entirely on Git because it has won the market decisively — but the concepts (history, branching, merging, review) apply to any VCS.
A short history of version control
Understanding where Git came from clarifies why it works the way it does:
- Local VCS (RCS, ~1982) — versioned single files on one machine with patch sets. No collaboration.
- Centralized VCS / CVCS (CVS 1990, Subversion/SVN 2000) — one central server holds all history; clients check out a working copy. Simple mental model, but the server is a single point of failure and every operation (log, diff, commit) needs network access.
- Distributed VCS / DVCS (Git 2005, Mercurial 2005) — every clone is a full repository with complete history. Branching and merging are cheap, most operations are local (and therefore instant), and there is no mandatory central server (though teams designate one by convention, e.g.
origin).
Git’s distributed model is the single most important thing to internalize: your laptop holds the entire history, so git log, git diff, git blame, git bisect, and committing all work offline at memory speed.
Fundamentals
The three trees and the object model
Git’s mental model rests on three “trees” (states) and a content-addressable object database.
Files move through three areas:
working directory → staging area (index) → repository (committed history)
- Working directory — the actual files you edit on disk.
- Staging area / index — a proposed next commit you build up with
git add. Staging lets you craft a clean, logical commit out of a messy working tree (git add -pstages individual hunks). - Repository — the committed, immutable history in
.git/.
Git stores snapshots, not diffs. Internally there are four object types, each addressed by the hash of its content:
| Object | What it is |
|---|---|
| blob | The contents of a single file (no filename, no metadata) |
| tree | A directory listing: names → blobs and sub-trees (this is where filenames and modes live) |
| commit | A snapshot: pointer to one root tree, parent commit(s), author/committer, message |
| tag (annotated) | A named, signed pointer to an object (usually a commit) with its own message |
Because objects are content-addressed, identical content is stored once, and any tampering changes the hash — history is tamper-evident. Git is migrating from SHA-1 to SHA-256 hashes to remove a theoretical collision risk.
Key references (pointers into the object graph):
- Commit — an immutable snapshot of the whole tree, identified by a hash, pointing to its parent(s). A merge commit has two (or more) parents.
- Branch — a movable pointer to a commit, stored as a one-line file under
.git/refs/heads/. Cheap to create, cheap to delete — creating a branch just writes a 40-character hash to a file. - Tag — a fixed pointer to a commit, typically marking a release (
v1.4.2). Lightweight tags are just a ref; annotated tags are full objects carrying author, date, message, and optional GPG signature (use annotated tags for releases). - HEAD — a pointer to the branch (or commit) you currently have checked out. Normally
HEAD→ a branch → a commit. WhenHEADpoints directly at a commit you are in detached HEAD state. - Remote — a reference to another copy of the repository (
originby convention). Remote-tracking branches likeorigin/mainare Git’s local cache of where the remote’s branches were at your lastfetch.
git init # create a repository
git add file.py # stage changes
git add -p # stage selected hunks interactively
git commit -m "Add parser" # record a snapshot
git log --oneline --graph --all # visualise the commit DAG
git tag -a v1.0.0 -m "Release" # annotated tag
git push origin main --tags # publish branch and tags
git cat-file -p HEAD # inspect the raw commit object
Commit graph, fetch / pull / push
History is a directed acyclic graph (DAG) of commits, not a straight line. Understanding the three network operations prevents most confusion:
git fetch— download new objects and update remote-tracking refs (origin/*). Never touches your working tree or local branches. Always safe.git pull—fetch+ integrate (mergeby default, orrebasewith--rebase). Because it mutates your branch, preferfetchthen look before you integrate. Setgit config pull.rebase true(orpull.ff only) to avoid surprise merge commits.git push— upload your commits and move the remote branch. Rejected if it isn’t a fast-forward (someone else pushed first) — fetch and integrate, then push again.
Merge vs rebase
Both integrate changes from one branch into another; they differ in the history they produce.
| Aspect | git merge | git rebase |
|---|---|---|
| History | Preserves both lines; adds a merge commit (unless fast-forward) | Rewrites your commits on top of the target; linear history |
| Commit hashes | Unchanged | Rewritten (new hashes, new commit objects) |
| Traceability | Merge commit records exactly when/what was integrated | Loses the “this was developed in parallel” context |
| Conflict resolution | Once, in the merge commit | Potentially once per replayed commit |
| Safe on shared branches | Yes | No — never rebase commits others have pulled |
| Typical use | Integrating a finished feature into main | Updating a feature branch with the latest main; cleaning up local history before a PR |
A fast-forward merge happens when the target hasn’t diverged: Git just slides the pointer forward, no merge commit. Force a merge commit with --no-ff when you want the feature boundary recorded.
# Update a feature branch with latest main, keeping history linear
git checkout feature/login
git fetch origin
git rebase origin/main # replay my commits on top of main
# resolve conflicts, then:
git rebase --continue
git push --force-with-lease # safe force-push of a rewritten branch
# Clean up local history before opening a PR
git rebase -i HEAD~5 # squash/reword/reorder the last 5 commits
Rule of thumb: rebase your own unpublished work; merge everything shared. --force-with-lease is safer than --force: it refuses to overwrite the remote if someone else pushed in the meantime.
Resolving conflicts
A conflict occurs when two branches change the same lines. Git marks them and stops:
<<<<<<< HEAD
timeout = 30
=======
timeout = 60
>>>>>>> feature/login
Resolve by editing to the intended result, git add the file, then continue (git merge --continue / git rebase --continue). Useful helpers: git mergetool, git checkout --ours/--theirs <file>, and git rerere (reuse recorded resolutions) for repetitive conflicts on long-running branches.
Undoing things
The single most reassuring fact about Git: committed work is very hard to truly lose. Know these and you can experiment fearlessly.
git restore file.py # discard working-tree changes (was: git checkout -- file)
git restore --staged file.py # unstage, keep working-tree changes
git commit --amend # rewrite the most recent commit (local only)
git revert <sha> # new commit that undoes a commit (safe on shared branches)
git reset --soft <sha> # move branch pointer, keep changes staged
git reset --mixed <sha> # move pointer, unstage (default)
git reset --hard <sha> # move pointer, discard everything (local only, destructive)
git reflog # log of everywhere HEAD has been — your safety net
git stash / git stash pop # shelve dirty changes to switch context
git reflog records every move of HEAD for ~90 days, so even a reset --hard or a deleted branch is recoverable: find the lost commit’s hash in the reflog and git checkout or git branch it back.
Key Concepts
Branching strategies
The branching model is a team-level decision that trades release cadence against process overhead.
| Strategy | How it works | Best for | Trade-offs |
|---|---|---|---|
| Trunk-Based Development | Everyone commits to main (or very short-lived branches, < 1 day). Feature flags hide unfinished work. | Teams with strong CI, aiming for continuous deployment | Demands excellent test automation and feature-flag discipline |
| GitHub Flow | Branch from main → PR → review → merge → deploy. One long-lived branch. | Web apps and services with frequent releases | No explicit release/staging branch; relies on deploy pipeline |
| Git Flow | Long-lived main + develop, plus feature/, release/, hotfix/ branches. | Versioned/packaged software (desktop apps, libraries) with scheduled releases | Heavyweight for SaaS; many merges; slow feedback |
| GitLab Flow | GitHub Flow plus environment branches (staging, production) or release branches. | Teams needing explicit environment promotion | Environment branches can drift from main if not automated |
Modern DevOps guidance (DORA research, the Accelerate book) consistently favors trunk-based development with short-lived branches: fewer merge conflicts, faster feedback, higher deployment frequency, and lower change-failure rate. The heavier Git Flow was designed for scheduled desktop-software releases and is usually overkill for services deployed many times a day.
Feature flags are the enabler that makes trunk-based development possible: merge incomplete code to main behind a flag that is off in production, decoupling deploy (shipping the binary) from release (turning the feature on). This also enables canary rollouts and instant kill switches.
VCS hosting platforms
| Feature | GitHub | GitLab | Bitbucket |
|---|---|---|---|
| Built-in CI/CD | GitHub Actions (huge marketplace) | GitLab CI/CD (very mature, single-file .gitlab-ci.yml) | Bitbucket Pipelines |
| Self-hosted option | GitHub Enterprise Server | GitLab CE/EE (very popular self-hosted) | Bitbucket Data Center |
| Review unit | Pull Request (PR) | Merge Request (MR) | Pull Request (PR) |
| Access model | Branch protection + rulesets | Protected branches + push rules | Branch permissions |
| Ecosystem | Largest OSS community, Marketplace, Copilot, Codespaces | Single application for the whole DevOps lifecycle (SCM, CI, registry, security) | Deep Jira/Atlassian integration |
| Fine-grained CODEOWNERS | Yes | Yes (+ approval rules) | Yes (Premium) |
All three speak the same Git protocol, so switching hosts is mostly a matter of migrating issues/PRs and CI config — the repository itself is portable.
PR/MR workflow and code review
A typical pull request lifecycle:
- Branch from
main, commit small focused changes. - Open a PR/MR early (draft) — CI runs automatically on every push.
- Reviewers comment; author pushes fixes (each push re-runs CI).
- Required checks pass → approvals given → merge.
- Branch is deleted; deployment pipeline picks up
main.
Merge methods — the choice shapes your history:
| Method | Result on main | When to use |
|---|---|---|
| Merge commit | Preserves every branch commit + a merge commit | You value the full development trail |
| Squash and merge | One tidy commit per PR | Messy WIP commits; you want main to read as one change per feature |
| Rebase and merge | Replays commits linearly, no merge commit | You keep clean commits and want linear history |
Good review culture matters more than the tool: keep PRs small (ideally < 400 lines changed), review promptly, comment on the code not the person, and use “request changes” sparingly. Reviews spread knowledge and catch defects earlier than any later stage.
Protected branches and policy
Branch protection rules turn process into enforcement. On main (and release branches) typically:
- Require a PR before merging (no direct pushes).
- Require N approving reviews (often 1–2), and dismiss stale approvals on new pushes.
- Require status checks to pass (CI, lint, security scan) and to be up to date with the base.
- Require conversation resolution, signed commits, and/or a linear history.
- Forbid force pushes and branch deletion.
- Optionally require review from CODEOWNERS — a file mapping paths to owning teams so the right people are auto-requested.
# .github/CODEOWNERS
/infra/ @org/platform-team
*.tf @org/platform-team
/services/billing/ @org/billing-team @alice
Monorepo vs polyrepo
| Monorepo | Polyrepo | |
|---|---|---|
| Code sharing | Trivial — same repo, one version of everything | Via published, versioned packages |
| Atomic cross-project changes | Yes — one commit spans many services | No — coordinated multi-repo releases needed |
| Dependency management | One lockfile, no “diamond dependency” hell | Each repo pins its own versions (can drift) |
| CI complexity | Needs selective builds (Bazel, Nx, Turborepo, path filters) or you rebuild the world | Simple per-repo pipelines |
| Access control | Coarse (directory-level at best) | Per-repository, clean |
| Repo size / tooling | Can grow huge; may need partial clone, sparse checkout, VFS | Each repo stays small |
| Examples | Google, Meta, Uber | Most microservice shops |
Neither is universally better. Monorepos optimize for atomic changes and code sharing but demand serious build tooling at scale; polyrepos give clean isolation but push complexity into version coordination and dependency management. Team size, tooling maturity, and how coupled your services are should decide.
Git hooks
Hooks are scripts Git runs at lifecycle events — automation that fires without anyone remembering to run it.
Client-side (local, can be bypassed): pre-commit (lint/format before a commit is created), commit-msg (validate the message, e.g. Conventional Commits), pre-push (run tests before publishing). Server-side (authoritative, cannot be bypassed by clients): pre-receive and update (reject pushes that violate policy), post-receive (trigger deployments, notifications — the basis of many CI and GitOps systems).
# .git/hooks/pre-commit (must be executable: chmod +x)
#!/bin/sh
ruff check . || exit 1 # block commit if lint fails
Because .git/hooks/ isn’t versioned and client hooks are skippable, teams manage hooks with the pre-commit framework or Husky (Node.js) so hooks are versioned, shared, and installed automatically — and always re-check the same rules in CI, which cannot be skipped.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: detect-private-key # cheap, high-value secret guard
- id: check-merge-conflict
Handling large files and monorepo scale
Git stores every version of every file forever, so committing large binaries (media, datasets, compiled artifacts) permanently bloats history. Mitigations:
- Git LFS — replaces large files with small pointer objects and stores the real bytes on a separate server.
.gitattributes— normalize line endings (* text=auto), mark binaries, and configure LFS filters.- Partial/shallow clone —
git clone --depth 1(shallow),--filter=blob:none(blobless), and sparse checkout let you work with a slice of a huge monorepo.
Powerful tools worth knowing
git bisect— binary search over history to find the commit that introduced a bug in O(log n) steps; can be fully automated with a test script (git bisect run ./test.sh).git blame/git log -S "text"(pickaxe) — find when and why a line or string changed.git worktree— check out multiple branches into separate directories from one repo (e.g. build a hotfix without stashing).git cherry-pick <sha>— copy a specific commit onto the current branch (e.g. backport a fix to a release branch).git submodule/ subtree — embed one repo inside another (submodules are powerful but a common source of confusion; use deliberately).
Best Practices
- Commit small and often, with meaningful messages — small commits are easier to review, bisect, and revert. Write the subject in the imperative (“Add retry to uploader”), keep it under ~50 chars, and explain why in the body. Follow a convention such as Conventional Commits (
feat:,fix:,chore:) to enable automated changelogs and semantic versioning. - Keep branches short-lived — merge within a day or two. Long-lived branches accumulate conflicts, hide work, and delay integration feedback (a core DORA finding). Integrate
maininto your branch frequently. - Protect
main— require PRs, passing CI, and at least one review; disable force pushes and direct pushes. Production should only ever be built from a protected branch. - Never commit secrets — use
.gitignore, secret scanning (GitHub secret scanning/push protection, gitleaks, trufflehog) in apre-commithook and in CI, and a real secrets manager. A secret that touched Git history must be rotated, not just deleted — it lives forever in clones and reflogs. - Rebase local work, merge shared work — never rewrite history that others may have pulled; use
--force-with-lease, never bare--force, when you must push a rebased branch. - Use
git reverton shared branches — it undoes changes with a new commit, keeping every clone consistent, instead of rewriting published history. - Tag releases and automate versioning — annotated (and signed) tags plus CI-driven release pipelines make every production artifact traceable to an exact commit. Adopt SemVer.
- Adopt CODEOWNERS and required reviews — routing changes to domain owners raises review quality, spreads knowledge, and prevents lone-wolf changes to critical paths.
- Automate quality gates with hooks and CI — lint, format, test, and scan in
pre-commitfor fast local feedback, and again in CI as the authority (hooks can be skipped; required CI cannot). - Store everything as code in Git — application code, infrastructure (Terraform), pipelines, Kubernetes manifests, dashboards, alerts, and runbooks. This enables GitOps: the repo is the source of truth and drift becomes detectable and revertible.
- Use
.gitattributesfor line endings and Git LFS for large binaries —* text=autoprevents cross-OS CRLF/LF churn; LFS keeps history from ballooning. - Write a good
.gitignoreearly — exclude build artifacts, dependencies (node_modules), local env files, and IDE cruft. Use a starter from github.com/github/gitignore; committing generated files causes noisy diffs and merge conflicts. - Learn
git bisectandgit reflog— bisect finds a regressing commit in logarithmic time; reflog recovers “lost” commits after a bad reset or deleted branch. Knowing they exist turns Git from scary to safe. - Sign your commits and tags — GPG/SSH signing (or GitHub’s verified commits) proves authorship and prevents spoofed commits in security-sensitive supply chains.
- Prefer squash-merge for feature PRs on a linear-history repo — one clean commit per feature keeps
mainreadable and makes reverts trivial, while the full detail stays on the (now-deleted) branch and in the PR. - Keep the repository the source of truth, not a mirror — decisions, config, and process live in the repo (via files, PR templates,
CODEOWNERS), not in tribal knowledge or chat history.
References
- Pro Git book (free, official): https://git-scm.com/book/en/v2
- Git official documentation / reference: https://git-scm.com/docs
- Git internals (objects, refs) — Pro Git ch. 10: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
- roadmap.sh DevOps roadmap: https://roadmap.sh/devops
- GitHub Docs — About pull requests: https://docs.github.com/en/pull-requests
- GitHub Docs — About protected branches: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches
- GitLab Docs — Merge requests: https://docs.gitlab.com/ee/user/project/merge_requests/
- Trunk-Based Development: https://trunkbaseddevelopment.com/
- Atlassian Git tutorials (merge vs rebase, Git Flow): https://www.atlassian.com/git/tutorials
- Conventional Commits: https://www.conventionalcommits.org/
- Semantic Versioning: https://semver.org/
- pre-commit framework: https://pre-commit.com/
- Git LFS: https://git-lfs.com/
- GitHub’s collection of .gitignore templates: https://github.com/github/gitignore
- DORA / Google Cloud — DevOps capabilities (version control, trunk-based dev): https://dora.dev/capabilities/