← DevOps← DevOps
DevOpsDevOps18 Th7, 2026Jul 18, 202621 phút đọc18 min read

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:

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 directorystaging area (index)repository (lịch sử đã commit)

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:

ObjectLà gì
blobNội dung của một file (không có tên file, không metadata)
treeDanh sách thư mục: tên → blob và sub-tree (tên file và mode nằm ở đây)
commitMộ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):

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:

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ạnhgit mergegit 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 commitKhông đổiBị viết lại (hash mới, object commit mới)
Khả năng truy vếtMerge commit ghi lại chính xác thời điểm/nội dung tích hợpMất ngữ cảnh “đây là code phát triển song song”
Xử lý conflictMột lần, trong merge commitCó thể một lần cho mỗi commit được replay
An toàn trên branch dùng chungKhông — tuyệt đối không rebase commit mà người khác đã pull
Dùng khiGộp feature hoàn chỉnh vào mainCậ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ượcCách hoạt độngPhù hợp vớiĐánh đổi
Trunk-Based DevelopmentMọ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 FlowTạo branch từ main → PR → review → merge → deploy. Chỉ một branch dài hạn.Web app/service release thường xuyênKhông có branch release/staging riêng; dựa vào pipeline deploy
Git Flowmain + 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ịchQuá nặng cho SaaS; nhiều merge; feedback chậm
GitLab FlowGitHub 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àngEnvironment 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íGitHubGitLabBitbucket
CI/CD tích hợpGitHub 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-hostedGitHub Enterprise ServerGitLab CE/EE (rất phổ biến khi self-host)Bitbucket Data Center
Đơn vị reviewPull Request (PR)Merge Request (MR)Pull Request (PR)
Mô hình phân quyềnBranch protection + rulesetProtected branch + push ruleBranch permission
Hệ sinh tháiCộng đồng OSS lớn nhất, Marketplace, Copilot, CodespacesMộ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ếtCó (+ 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:

  1. Tạo branch từ main, commit nhỏ và tập trung.
  2. Mở PR/MR sớm (dạng draft) — CI tự động chạy mỗi lần push.
  3. Reviewer góp ý; tác giả push bản sửa (mỗi lần push chạy lại CI).
  4. Check bắt buộc pass → đủ approval → merge.
  5. 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ứcKết quả trên mainKhi nào dùng
Merge commitGiữ mọi commit của branch + một merge commitBạn coi trọng toàn bộ dấu vết phát triển
Squash and mergeMột commit gọn gàng cho mỗi PRCommit WIP lộn xộn; muốn main đọc như một thay đổi cho mỗi feature
Rebase and mergeReplay commit tuyến tính, không merge commitBạ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:

# .github/CODEOWNERS
/infra/            @org/platform-team
*.tf               @org/platform-team
/services/billing/ @org/billing-team @alice

Monorepo vs polyrepo

MonorepoPolyrepo
Chia sẻ codeDễ — 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 projectCó — một commit trải nhiều serviceKhông — phải phối hợp release nhiều repo
Quản lý dependencyMột lockfile, không có “diamond dependency”Mỗi repo pin version riêng (có thể lệch)
Độ phức tạp CICầ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ềnThô (tốt nhất theo thư mục)Theo từng repository, gọn gàng
Kích thước repo / toolingCó thể phình rất to; cần partial clone, sparse checkout, VFSMỗ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-receiveupdate (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

.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:

Các công cụ mạnh đáng biết

Best Practices

  1. 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.
  2. 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 main vào branch của bạn thường xuyên.
  3. 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ệ.
  4. Không bao giờ commit secret — dùng .gitignore, secret scanning (GitHub secret scanning/push protection, gitleaks, trufflehog) trong hook pre-commit và 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.
  5. 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ờ --force trần, khi buộc phải push branch đã rebase.
  6. Dùng git revert trê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.
  7. 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.
  8. Á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.
  9. 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).
  10. 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.
  11. Dùng .gitattributes cho line ending và Git LFS cho file binary lớn* text=auto tránh xáo trộn CRLF/LF giữa các OS; LFS tránh phình lịch sử.
  12. Viết .gitignore tố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.
  13. Học git bisectgit 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.
  14. 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.
  15. Ư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ữ main dễ đọc và revert dễ dàng, trong khi chi tiết đầy đủ vẫn nằm trên branch (đã xóa) và trong PR.
  16. 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

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:

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 directorystaging area (index)repository (committed history)

Git stores snapshots, not diffs. Internally there are four object types, each addressed by the hash of its content:

ObjectWhat it is
blobThe contents of a single file (no filename, no metadata)
treeA directory listing: names → blobs and sub-trees (this is where filenames and modes live)
commitA 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):

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:

Merge vs rebase

Both integrate changes from one branch into another; they differ in the history they produce.

Aspectgit mergegit rebase
HistoryPreserves both lines; adds a merge commit (unless fast-forward)Rewrites your commits on top of the target; linear history
Commit hashesUnchangedRewritten (new hashes, new commit objects)
TraceabilityMerge commit records exactly when/what was integratedLoses the “this was developed in parallel” context
Conflict resolutionOnce, in the merge commitPotentially once per replayed commit
Safe on shared branchesYesNo — never rebase commits others have pulled
Typical useIntegrating a finished feature into mainUpdating 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.

StrategyHow it worksBest forTrade-offs
Trunk-Based DevelopmentEveryone commits to main (or very short-lived branches, < 1 day). Feature flags hide unfinished work.Teams with strong CI, aiming for continuous deploymentDemands excellent test automation and feature-flag discipline
GitHub FlowBranch from main → PR → review → merge → deploy. One long-lived branch.Web apps and services with frequent releasesNo explicit release/staging branch; relies on deploy pipeline
Git FlowLong-lived main + develop, plus feature/, release/, hotfix/ branches.Versioned/packaged software (desktop apps, libraries) with scheduled releasesHeavyweight for SaaS; many merges; slow feedback
GitLab FlowGitHub Flow plus environment branches (staging, production) or release branches.Teams needing explicit environment promotionEnvironment 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

FeatureGitHubGitLabBitbucket
Built-in CI/CDGitHub Actions (huge marketplace)GitLab CI/CD (very mature, single-file .gitlab-ci.yml)Bitbucket Pipelines
Self-hosted optionGitHub Enterprise ServerGitLab CE/EE (very popular self-hosted)Bitbucket Data Center
Review unitPull Request (PR)Merge Request (MR)Pull Request (PR)
Access modelBranch protection + rulesetsProtected branches + push rulesBranch permissions
EcosystemLargest OSS community, Marketplace, Copilot, CodespacesSingle application for the whole DevOps lifecycle (SCM, CI, registry, security)Deep Jira/Atlassian integration
Fine-grained CODEOWNERSYesYes (+ 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:

  1. Branch from main, commit small focused changes.
  2. Open a PR/MR early (draft) — CI runs automatically on every push.
  3. Reviewers comment; author pushes fixes (each push re-runs CI).
  4. Required checks pass → approvals given → merge.
  5. Branch is deleted; deployment pipeline picks up main.

Merge methods — the choice shapes your history:

MethodResult on mainWhen to use
Merge commitPreserves every branch commit + a merge commitYou value the full development trail
Squash and mergeOne tidy commit per PRMessy WIP commits; you want main to read as one change per feature
Rebase and mergeReplays commits linearly, no merge commitYou 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:

# .github/CODEOWNERS
/infra/            @org/platform-team
*.tf               @org/platform-team
/services/billing/ @org/billing-team @alice

Monorepo vs polyrepo

MonorepoPolyrepo
Code sharingTrivial — same repo, one version of everythingVia published, versioned packages
Atomic cross-project changesYes — one commit spans many servicesNo — coordinated multi-repo releases needed
Dependency managementOne lockfile, no “diamond dependency” hellEach repo pins its own versions (can drift)
CI complexityNeeds selective builds (Bazel, Nx, Turborepo, path filters) or you rebuild the worldSimple per-repo pipelines
Access controlCoarse (directory-level at best)Per-repository, clean
Repo size / toolingCan grow huge; may need partial clone, sparse checkout, VFSEach repo stays small
ExamplesGoogle, Meta, UberMost 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:

Powerful tools worth knowing

Best Practices

  1. 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.
  2. 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 main into your branch frequently.
  3. 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.
  4. Never commit secrets — use .gitignore, secret scanning (GitHub secret scanning/push protection, gitleaks, trufflehog) in a pre-commit hook 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.
  5. 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.
  6. Use git revert on shared branches — it undoes changes with a new commit, keeping every clone consistent, instead of rewriting published history.
  7. 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.
  8. Adopt CODEOWNERS and required reviews — routing changes to domain owners raises review quality, spreads knowledge, and prevents lone-wolf changes to critical paths.
  9. Automate quality gates with hooks and CI — lint, format, test, and scan in pre-commit for fast local feedback, and again in CI as the authority (hooks can be skipped; required CI cannot).
  10. 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.
  11. Use .gitattributes for line endings and Git LFS for large binaries* text=auto prevents cross-OS CRLF/LF churn; LFS keeps history from ballooning.
  12. Write a good .gitignore early — 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.
  13. Learn git bisect and git 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.
  14. Sign your commits and tags — GPG/SSH signing (or GitHub’s verified commits) proves authorship and prevents spoofed commits in security-sensitive supply chains.
  15. Prefer squash-merge for feature PRs on a linear-history repo — one clean commit per feature keeps main readable and makes reverts trivial, while the full detail stays on the (now-deleted) branch and in the PR.
  16. 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