Version Control & Cộng tácVersion Control & Collaboration
Thuộc bộ kiến thức Backend Roadmap.
Tổng quan
Version control là việc theo dõi và quản lý các thay đổi của source code (và mọi artifact dạng text) theo thời gian. Với một backend engineer, đây là công cụ quan trọng nhất trong công việc hằng ngày: nó ghi lại ai thay đổi cái gì, khi nào và tại sao; cho phép nhiều người cùng làm việc trên cùng một codebase mà không ghi đè lên nhau; và giúp bạn review, revert, branch, release cũng như audit mọi thay đổi.
Ngày nay, version control system (VCS) phổ biến nhất là Git — một distributed VCS do Linus Torvalds tạo ra năm 2005 để phát triển Linux kernel. Xoay quanh Git là một hệ sinh thái các nền tảng hosting và cộng tác (GitHub, GitLab, Bitbucket) cung cấp pull/merge request, code review, issue tracking, CI/CD và package registry.
Note này trình bày version control là gì và tại sao nó quan trọng, các kiến thức nền tảng về Git và object model, các branching strategy, các dịch vụ repo hosting, pull/merge request và code review, cùng với collaboration hygiene (commit message, .gitignore, bảo vệ main, semantic versioning). CI/CD được trình bày sâu hơn trong ./19-cicd-and-deployment.md.
Kiến thức nền tảng
Vì sao version control quan trọng
Không có VCS, các team phải copy folder (project-final-v2-REALLY-final/), gửi file zip qua email và làm mất công sức. Một VCS đúng nghĩa mang lại:
- Lịch sử và khả năng truy vết (traceability) — một log đầy đủ, chống giả mạo của mọi thay đổi.
- Cộng tác — nhiều developer làm việc song song rồi merge kết quả lại.
- Cô lập (isolation) — công việc feature diễn ra trên branch mà không phá vỡ mainline.
- Khôi phục — bạn có thể revert về bất kỳ trạng thái nào trước đó hoặc phục hồi code đã xóa.
- Trách nhiệm —
git blamecho thấy ai chạm vào từng dòng gần nhất và tại sao. - Nền tảng cho tự động hóa — CI/CD, code review và release đều dựa trên VCS.
Centralized vs distributed VCS
| Khía cạnh | Centralized (SVN, Perforce, CVS) | Distributed (Git, Mercurial) |
|---|---|---|
| Bản sao repository | Một server trung tâm; client chỉ có working copy | Mỗi clone là một repository đầy đủ với toàn bộ history |
| Làm việc offline | Hạn chế (cần server để commit/xem history) | Có — commit, branch, xem history đều offline được |
| Single point of failure | Server là điểm sống còn | Bất kỳ clone nào cũng khôi phục được project |
| Branching/merging | Trước đây nặng nề/chậm | Rẻ, nhanh, là “công dân hạng nhất” |
| Tốc độ commit | Phải round-trip qua mạng | Local, tức thì |
| Dùng phổ biến hiện nay | Legacy, binary asset lớn (Perforce trong game) | Tiêu chuẩn de facto cho hầu hết phần mềm |
Trong một VCS distributed như Git, khi git clone bạn nhận toàn bộ history. Bạn commit ở local, rồi push lên và pull về từ các remote. Điều này khiến branching rất rẻ và cho phép bạn làm việc trên máy bay rồi sync lại sau.
Ba khu vực trong Git
Git tổ chức công việc thành ba khu vực khái niệm, cộng thêm remote:
| Khu vực | Là gì | Đưa vào bằng |
|---|---|---|
| Working directory | Các file thật trên đĩa mà bạn đang sửa | (bạn sửa file) |
| Staging area (index) | Nội dung dự kiến cho commit tiếp theo | git add |
Repository (.git) | History đã commit (object, ref) | git commit |
| Remote | Bản sao chung trên server | git push / git fetch |
git status # cái gì đã đổi, cái gì đã staged
git add src/app.go # stage một file cụ thể
git add -p # stage tương tác theo từng hunk
git restore --staged f # bỏ stage
git commit -m "msg" # ghi các thay đổi đã staged vào history
Khái niệm chính
Repository, commit, branch
Repository là project cùng toàn bộ history, lưu trong thư mục .git. Một commit là một snapshot bất biến của toàn bộ cây file tại một thời điểm, được định danh bằng một hash SHA-1/SHA-256, mang theo author, committer, timestamp, message và một hoặc nhiều parent commit. Một branch đơn giản chỉ là một con trỏ di động (một ref) trỏ tới một commit; HEAD trỏ tới branch hiện tại của bạn.
git init # tạo repo mới
git clone git@github.com:org/repo.git
git log --oneline --graph --all
git branch feature/login # tạo branch
git switch feature/login # chuyển sang branch (cách hiện đại)
git switch -c feature/login # tạo + chuyển trong một bước
git checkout feature/login # tương đương switch, cách cũ
Git object model nhìn tổng quát
Git là một kho key-value định địa chỉ theo nội dung (content-addressable). Có bốn loại object, mỗi cái lưu trong .git/objects và được đặt tên bằng hash của nội dung:
| Object | Đại diện cho | Chứa gì |
|---|---|---|
| blob | Nội dung file | Byte thô của file (không có tên file) |
| tree | Một thư mục | Tên + mode + con trỏ tới các blob và sub-tree |
| commit | Một snapshot | Con trỏ tới root tree, parent commit, author, message |
| tag | Một annotated tag | Con trỏ tới một object (thường là commit) + metadata |
Điểm mấu chốt: một commit trỏ tới một tree (toàn bộ snapshot), chứ không trỏ tới diff. Diff được tính toán khi cần. Branch và tag chỉ là ref — các file trong .git/refs chứa một commit hash. Đó là lý do branching rất rẻ: tạo một branch chỉ là ghi một chuỗi hash 40 ký tự vào một file.
git cat-file -t <hash> # hiện loại object (blob/tree/commit/tag)
git cat-file -p HEAD # in đẹp nội dung commit object
git rev-parse HEAD # phân giải một ref thành hash của nó
Merge vs rebase
Cả hai đều tích hợp thay đổi từ branch này sang branch khác, nhưng theo cách khác nhau:
- Merge tạo một merge commit mới có hai parent, giữ nguyên history thật và điểm mà các branch tách nhánh. Không phá hủy (non-destructive).
- Rebase phát lại (replay) các commit của bạn lên trên một branch khác, tạo ra một history tuyến tính như thể bạn vừa tách nhánh từ tip mới nhất. Nó viết lại (rewrite) commit hash.
# Merge feature vào main
git switch main
git merge feature/login # có thể tạo merge commit
git merge --no-ff feature/login # luôn tạo merge commit
# Rebase feature lên main mới nhất
git switch feature/login
git rebase main # phát lại các commit của feature lên trên main
git rebase -i HEAD~3 # tương tác: squash/đổi thứ tự/sửa 3 commit gần nhất
# Xử lý conflict trong khi merge hoặc rebase
# ...sửa file...
git add <resolved-files>
git rebase --continue # hoặc: git merge --continue
git rebase --abort # hủy an toàn
Quy tắc vàng của rebase: không bao giờ rebase những commit mà người khác đã pull (tức là đừng rewrite history đã public/chia sẻ), vì nó đổi hash và làm hỏng history của họ.
Remote và đồng bộ
Một remote là một tham chiếu có tên tới một bản sao khác của repo (theo quy ước là origin).
git remote -v
git remote add origin git@github.com:org/repo.git
git fetch origin # tải ref/object về, không đụng working tree
git pull # fetch + merge (hoặc rebase nếu được cấu hình)
git pull --rebase # fetch + rebase các commit local lên trên
git push -u origin feature/login # push và set upstream
git push --force-with-lease # force push an toàn hơn sau khi rebase
Nên dùng --force-with-lease thay vì --force: nó từ chối ghi đè remote nếu có người khác đã push trong lúc đó.
Hoàn tác (undo) một cách an toàn
| Mục tiêu | Lệnh | Ghi chú |
|---|---|---|
| Bỏ thay đổi chưa staged của một file | git restore <file> | Mất phần sửa local |
| Bỏ stage một file | git restore --staged <file> | Giữ phần sửa |
| Sửa commit gần nhất | git commit --amend | Rewrite commit cuối |
| Hủy commit, giữ thay đổi ở staged | git reset --soft HEAD~1 | History lùi lại |
| Hủy commit, giữ thay đổi chưa staged | git reset HEAD~1 | Mixed (mặc định) |
| Hủy commit, bỏ luôn thay đổi | git reset --hard HEAD~1 | Phá hủy |
| Revert một commit đã public | git revert <hash> | Tạo commit đảo ngược, an toàn cho history chung |
| Tạm cất công việc | git stash / git stash pop | Lưu trạng thái dở dang |
| Khôi phục commit “bị mất” | git reflog | Cho thấy HEAD đã đi qua đâu |
git reflog là lưới an toàn của bạn: kể cả sau một lệnh reset --hard hay rebase sai, các commit cũ thường vẫn khôi phục được trong khoảng ~90 ngày.
Branching strategy
Mô hình branching định hình cách một team cộng tác và release. Ba mô hình phổ biến:
| Strategy | Cách hoạt động | Ưu điểm | Nhược điểm | Phù hợp cho |
|---|---|---|---|---|
| Trunk-based | Mọi người commit vào main (hoặc branch cực ngắn, merge trong ngày); feature ẩn sau feature flag | Ít đau đầu về merge, integration liên tục, luồng nhanh | Cần CI, test và feature flag mạnh | Team tốc độ cao làm CI/CD |
| GitHub Flow | main luôn deploy được; mỗi feature một branch → PR → review → merge → deploy | Đơn giản, xoay quanh PR, hợp cho web app | Không có release branch rõ ràng; giả định deploy liên tục | Team nhỏ/vừa, SaaS |
| Git Flow | main + develop sống lâu, cộng thêm branch feature/*, release/*, hotfix/* | Release có cấu trúc, hỗ trợ release song song/theo version | Nặng nề, nhiều branch sống lâu, integration chậm | Phần mềm theo version, release theo lịch |
Trunk-based development ngày càng được khuyến nghị cho các team làm continuous delivery, vì các branch sống lâu tích tụ merge conflict và làm chậm integration. GitHub Flow là lựa chọn mặc định thực dụng cho hầu hết backend web. Git Flow hợp với sản phẩm ship theo các bản release rời rạc, có version (desktop app, phần mềm on-prem, mobile).
# Ví dụ GitHub Flow
git switch -c feat/rate-limit # tách branch từ main
# ...commit công việc...
git push -u origin feat/rate-limit # mở PR từ branch này
# review, approve, rồi squash-merge vào main; xóa branch
Các dịch vụ repo hosting
Các nền tảng này host repo Git và bổ sung các tính năng cộng tác lên trên.
| Tính năng | GitHub | GitLab | Bitbucket |
|---|---|---|---|
| Đề xuất merge | Pull Request (PR) | Merge Request (MR) | Pull Request (PR) |
| CI/CD tích hợp sẵn | GitHub Actions | GitLab CI/CD (rất trưởng thành) | Bitbucket Pipelines |
| Issue tracking | Issues, Projects | Issues, Epics, Boards | Tích hợp Jira (native) |
| Package registry | GitHub Packages | GitLab Package/Container Registry | hạn chế |
| Self-hosting | GitHub Enterprise | GitLab CE/EE (self-host mạnh) | Bitbucket Data Center |
| Code review | Reviews, CODEOWNERS | Approvals, CODEOWNERS | Reviewers, tasks |
| Thế mạnh nổi bật | Hệ sinh thái lớn nhất, open source | Nền tảng DevOps trọn gói | Tích hợp sâu Atlassian/Jira |
Cả ba đều hỗ trợ branch protection, bắt buộc review, status check và code owner. Việc chọn lựa thường phụ thuộc vào hệ sinh thái: GitHub cho open source và cộng đồng, GitLab cho một toolchain DevOps end-to-end tích hợp, Bitbucket cho các team đã đầu tư vào Jira/Confluence.
Pull/merge request và code review
Một pull request (PR) / merge request (MR) đề xuất merge một branch vào một branch khác, và là nơi chính diễn ra code review, thảo luận và các CI check.
Luồng điển hình:
- Tách branch từ
main, tạo các commit tập trung, push branch lên. - Mở PR với title và description rõ ràng (làm gì, tại sao, cách test, screenshot).
- Các check tự động chạy (build, test, lint, security scan).
- Reviewer comment, yêu cầu chỉnh sửa, hoặc approve.
- Author xử lý feedback bằng các commit mới.
- Khi đã được approve và CI xanh, merge (thường là squash hoặc rebase merge), rồi xóa branch.
Cách review tốt:
- Xem xét tại sao trước: thay đổi này có giải quyết đúng vấn đề không?
- Cụ thể và tử tế — phê bình code, không phê bình con người. Gợi ý, đừng ra lệnh.
- Phân biệt vấn đề chặn (blocking) với nit (chuyện nhỏ) — thêm tiền tố “nit:” cho các góp ý nhỏ.
- Tìm lỗi về tính đúng đắn, edge case, security, test và khả năng đọc — không chỉ style (để style cho linter/formatter).
- Approve khi nó đủ tốt để ship, chứ không phải khi hoàn hảo. Đừng để sự hoàn hảo chặn tiến độ.
- Review kịp thời; một PR bị treo sẽ chặn đồng đội.
Cách nhận review tốt:
- Giữ PR nhỏ (lý tưởng là < 400 dòng thay đổi) — PR nhỏ được review kỹ và nhanh hơn.
- Giải thích ngữ cảnh trong description để reviewer không phải đoán.
- Trả lời mọi comment; phản biện có lý lẽ khi không đồng ý.
- Đừng nhận feedback theo hướng cá nhân; đó là về code.
Checklist review:
| Nhóm | Câu hỏi |
|---|---|
| Tính đúng đắn | Nó có làm đúng như tuyên bố không? Edge case, xử lý lỗi, null? |
| Test | Có test không? Có phủ hành vi mới và các đường lỗi không? |
| Security | Validate input, authz/authn, secret, injection, dependency? |
| Hiệu năng | N+1 query, vòng lặp không giới hạn, cấp phát thừa? |
| Khả năng đọc | Tên rõ ràng, hàm nhỏ, comment ở chỗ khó hiểu? |
| Thiết kế | Có hợp với pattern hiện có? Có abstraction rò rỉ hay trùng lặp? |
| Tương thích ngược | Migration API/DB có an toàn? Breaking change có được ghi chú? |
Collaboration hygiene
Commit message — Conventional Commits. Một commit message tốt giải thích tại sao, không chỉ cái gì. Chuẩn Conventional Commits đưa ra một format máy đọc được, đồng thời phục vụ tự động tạo changelog và semantic versioning:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
git commit -m "feat(auth): add refresh-token rotation"
git commit -m "fix(api): return 404 when order not found"
git commit -m "refactor(db): extract query builder"
git commit -m "feat!: drop support for API v1
BREAKING CHANGE: v1 endpoints removed; migrate to v2."
Các type phổ biến: feat, fix, docs, style, refactor, perf, test, build, ci, chore. Dấu ! hoặc footer BREAKING CHANGE: báo hiệu một breaking change (tăng major version).
Quy tắc chung cho message: dùng câu mệnh lệnh (“add”, không phải “added”), subject ngắn gọn (~50 ký tự), một dòng trống, rồi phần body ngắt dòng giải thích lý do và trade-off.
.gitignore. Không bao giờ commit build artifact, dependency, secret hay file đặc thù của máy. Loại chúng ra bằng .gitignore:
# Dependencies
node_modules/
vendor/
# Build output
dist/
target/
*.o
# Secrets & env
.env
.env.local
*.pem
# OS / editor
.DS_Store
.idea/
*.swp
Nếu một secret đã lỡ được commit, thêm nó vào .gitignore là chưa đủ — nó vẫn nằm trong history. Hãy xoay (rotate) secret đó và dọn history bằng công cụ như git filter-repo hoặc BFG.
Bảo vệ main. Cấu hình branch protection trên nền tảng hosting để không ai push code hỏng lên mainline:
- Bắt buộc pull request trước khi merge (không push trực tiếp vào
main). - Bắt buộc status check xanh (build, test, lint) trước khi merge.
- Bắt buộc ít nhất một (hoặc N) approving review.
- Dùng file
CODEOWNERSđể tự động request review từ đúng người. - Bắt buộc signed commit và/hoặc linear history nếu muốn.
- Hạn chế force-push và xóa trên
main.
Semantic versioning và tag. SemVer định dạng release là MAJOR.MINOR.PATCH:
| Loại tăng | Khi nào | Ví dụ |
|---|---|---|
| MAJOR | Thay đổi API không tương thích/breaking | 1.4.2 → 2.0.0 |
| MINOR | Thêm tính năng tương thích ngược | 1.4.2 → 1.5.0 |
| PATCH | Sửa bug tương thích ngược | 1.4.2 → 1.4.3 |
Đánh dấu các release bằng annotated tag, tốt nhất là signed tag:
git tag -a v1.5.0 -m "Release 1.5.0" # annotated tag
git tag -s v1.5.0 -m "Release 1.5.0" # signed tag
git push origin v1.5.0 # push một tag
git push origin --tags # push tất cả tag
git describe --tags # phiên bản dễ đọc suy ra từ tag
Conventional Commits + SemVer + tag cho phép các công cụ như semantic-release tự tính version tiếp theo và sinh changelog tự động.
Best Practices
- Commit nhỏ và thường xuyên với các commit tập trung, mạch lạc về mặt logic — mỗi commit một mối quan tâm.
- Viết commit message có ý nghĩa (Conventional Commits); giải thích tại sao, không chỉ cái gì.
- Không bao giờ commit secret — dùng
.gitignore, environment variable và secret manager; quét bằng công cụ nhưgitleaks. - Giữ branch sống ngắn và rebase/merge từ
mainthường xuyên để tránh conflict đau đớn. - Giữ PR nhỏ và một mục đích để review nhanh và kỹ.
- Bảo vệ
main: bắt buộc review và CI xanh; không force-push branch chung (chỉ dùng--force-with-leasetrên branch của chính bạn). - Không bao giờ rewrite public history — dùng
git revertcho các thay đổi mà người khác đã pull. - Pull trước khi push (
git pull --rebase) để tích hợp công việc của người khác một cách gọn gàng. - Dùng
.gitignorengay từ ngày đầu (xem gitignore.io / template của GitHub). - Tag release theo SemVer và tự động hóa changelog từ Conventional Commits.
- Tự động hóa các quality gate trong CI (lint, test, security scan) để review tập trung vào thiết kế — xem ./19-cicd-and-deployment.md.
- Review kịp thời và tử tế; dùng
CODEOWNERSđể định tuyến review tới đúng người.
Tài liệu tham khảo
Part of the Backend Roadmap knowledge base.
Overview
Version control is the practice of tracking and managing changes to source code (and any text-based artifacts) over time. For a backend engineer, it is the single most important day-to-day tool: it records who changed what, when, and why; it lets many people work on the same codebase without overwriting each other; and it makes it possible to review, revert, branch, release, and audit every change.
Today the dominant version control system (VCS) is Git — a distributed VCS created by Linus Torvalds in 2005 for Linux kernel development. Around Git, an ecosystem of hosting and collaboration platforms (GitHub, GitLab, Bitbucket) provides pull/merge requests, code review, issue tracking, CI/CD, and package registries.
This note covers what version control is and why it matters, Git fundamentals and its object model, branching strategies, repo hosting services, pull/merge requests and code review, and collaboration hygiene (commit messages, .gitignore, protecting main, semantic versioning). CI/CD is covered more deeply in ./19-cicd-and-deployment.md.
Fundamentals
Why version control matters
Without a VCS, teams resort to copying folders (project-final-v2-REALLY-final/), emailing zip files, and losing work. A proper VCS gives you:
- History and traceability — a complete, tamper-evident log of every change.
- Collaboration — multiple developers work in parallel and merge results.
- Isolation — feature work happens on branches without breaking the mainline.
- Recovery — you can revert to any previous state or recover deleted code.
- Accountability —
git blameshows who last touched each line and why. - Foundation for automation — CI/CD, code review, and releases all hang off the VCS.
Centralized vs distributed VCS
| Aspect | Centralized (SVN, Perforce, CVS) | Distributed (Git, Mercurial) |
|---|---|---|
| Repository copies | One central server; clients have working copies | Every clone is a full repository with complete history |
| Works offline | Limited (needs server for commit/history) | Yes — commit, branch, view history all offline |
| Single point of failure | Server is critical | Any clone can restore the project |
| Branching/merging | Historically heavyweight/slow | Cheap, fast, first-class |
| Commit speed | Network round-trip | Local, instant |
| Typical use today | Legacy, large binary assets (Perforce in games) | The de facto standard for most software |
In a distributed VCS like Git, when you git clone you get the entire history. You commit locally, then push to and pull from remotes. This makes branching cheap and lets you work on a plane and sync later.
The three areas in Git
Git organizes work into three conceptual areas, plus the remote:
| Area | What it is | Moves in via |
|---|---|---|
| Working directory | The actual files on disk you edit | (you edit files) |
| Staging area (index) | The proposed next commit’s contents | git add |
Repository (.git) | Committed history (objects, refs) | git commit |
| Remote | A shared copy on a server | git push / git fetch |
git status # what changed, what's staged
git add src/app.go # stage a specific file
git add -p # interactively stage hunks
git restore --staged f # unstage
git commit -m "msg" # record staged changes into history
Key Concepts
Repository, commit, branch
A repository is the project plus its full history, stored in the .git directory. A commit is an immutable snapshot of the entire tree at a point in time, identified by a SHA-1/SHA-256 hash, carrying author, committer, timestamp, message, and one or more parent commits. A branch is simply a movable pointer (a ref) to a commit; HEAD points to your current branch.
git init # create a new repo
git clone git@github.com:org/repo.git
git log --oneline --graph --all
git branch feature/login # create a branch
git switch feature/login # switch to it (modern)
git switch -c feature/login # create + switch in one step
git checkout feature/login # older equivalent of switch
The Git object model at a glance
Git is a content-addressable key-value store. There are four object types, each stored under .git/objects and named by the hash of its content:
| Object | Represents | Contains |
|---|---|---|
| blob | File contents | Raw bytes of a file (no filename) |
| tree | A directory | Names + modes + pointers to blobs and sub-trees |
| commit | A snapshot | Pointer to a root tree, parent commit(s), author, message |
| tag | An annotated tag | Pointer to an object (usually a commit) + metadata |
The key insight: a commit points to a tree (the whole snapshot), not to diffs. Diffs are computed on demand. Branches and tags are just refs — files under .git/refs containing a commit hash. This is why branching is cheap: creating a branch just writes a 40-character hash to a file.
git cat-file -t <hash> # show object type (blob/tree/commit/tag)
git cat-file -p HEAD # pretty-print the commit object
git rev-parse HEAD # resolve a ref to its hash
Merge vs rebase
Both integrate changes from one branch into another, differently:
- Merge creates a new merge commit with two parents, preserving the true history and where branches diverged. Non-destructive.
- Rebase replays your commits on top of another branch, producing a linear history as if you’d branched from the latest tip. It rewrites commit hashes.
# Merge feature into main
git switch main
git merge feature/login # may create a merge commit
git merge --no-ff feature/login # always create a merge commit
# Rebase feature onto latest main
git switch feature/login
git rebase main # replay feature commits on top of main
git rebase -i HEAD~3 # interactive: squash/reorder/edit last 3
# Resolve conflicts during either operation
# ...edit files...
git add <resolved-files>
git rebase --continue # or: git merge --continue
git rebase --abort # bail out safely
Golden rule of rebase: never rebase commits that others have already pulled (i.e., don’t rewrite shared/public history), because it changes hashes and breaks their history.
Remotes and syncing
A remote is a named reference to another copy of the repo (conventionally origin).
git remote -v
git remote add origin git@github.com:org/repo.git
git fetch origin # download refs/objects, don't touch working tree
git pull # fetch + merge (or rebase, if configured)
git pull --rebase # fetch + rebase local commits on top
git push -u origin feature/login # push and set upstream
git push --force-with-lease # safer forced push after a rebase
Prefer --force-with-lease over --force: it refuses to overwrite the remote if someone else pushed in the meantime.
Undoing things safely
| Goal | Command | Notes |
|---|---|---|
| Discard unstaged changes to a file | git restore <file> | Loses local edits |
| Unstage a file | git restore --staged <file> | Keeps edits |
| Amend the last commit | git commit --amend | Rewrites the last commit |
| Undo commit, keep changes staged | git reset --soft HEAD~1 | History moves back |
| Undo commit, keep changes unstaged | git reset HEAD~1 | Mixed (default) |
| Undo commit, discard changes | git reset --hard HEAD~1 | Destructive |
| Revert a commit publicly | git revert <hash> | Creates an inverse commit, safe for shared history |
| Temporarily shelve work | git stash / git stash pop | Save dirty state |
| Recover “lost” commits | git reflog | Shows where HEAD has been |
git reflog is your safety net: even after a bad reset --hard or rebase, the old commits are usually recoverable for ~90 days.
Branching strategies
The branching model shapes how a team collaborates and releases. The three common ones:
| Strategy | How it works | Pros | Cons | Best for |
|---|---|---|---|---|
| Trunk-based | Everyone commits to main (or very short-lived branches merged within a day); features hidden behind feature flags | Minimal merge pain, continuous integration, fast flow | Requires strong CI, tests, and feature flags | High-velocity teams doing CI/CD |
| GitHub Flow | main is always deployable; branch per feature → PR → review → merge → deploy | Simple, PR-centric, great for web apps | No explicit release branches; assumes continuous deploy | Small/medium teams, SaaS |
| Git Flow | Long-lived main + develop, plus feature/*, release/*, hotfix/* branches | Structured releases, supports versioned/parallel releases | Heavy, many long-lived branches, slower integration | Versioned software, scheduled releases |
Trunk-based development is increasingly recommended for teams practicing continuous delivery, because long-lived branches accumulate merge conflicts and delay integration. GitHub Flow is the pragmatic default for most web backends. Git Flow suits products that ship discrete, versioned releases (desktop apps, on-prem software, mobile).
# GitHub Flow example
git switch -c feat/rate-limit # branch off main
# ...commit work...
git push -u origin feat/rate-limit # open a PR from this branch
# review, approve, then squash-merge into main; delete the branch
Repo hosting services
These platforms host Git repos and add collaboration features on top.
| Feature | GitHub | GitLab | Bitbucket |
|---|---|---|---|
| Merge proposal | Pull Request (PR) | Merge Request (MR) | Pull Request (PR) |
| Built-in CI/CD | GitHub Actions | GitLab CI/CD (very mature) | Bitbucket Pipelines |
| Issue tracking | Issues, Projects | Issues, Epics, Boards | Jira integration (native) |
| Package registry | GitHub Packages | GitLab Package/Container Registry | limited |
| Self-hosting | GitHub Enterprise | GitLab CE/EE (strong self-host) | Bitbucket Data Center |
| Code review | Reviews, CODEOWNERS | Approvals, CODEOWNERS | Reviewers, tasks |
| Notable strength | Largest ecosystem, open source | All-in-one DevOps platform | Deep Atlassian/Jira integration |
All three offer branch protection, required reviews, status checks, and code owners. Choice usually depends on ecosystem: GitHub for open source and community, GitLab for an integrated end-to-end DevOps toolchain, Bitbucket for teams already invested in Jira/Confluence.
Pull/merge requests and code review
A pull request (PR) / merge request (MR) proposes merging one branch into another, and is the primary place code review, discussion, and CI checks happen.
The typical flow:
- Branch off
main, make focused commits, push the branch. - Open a PR with a clear title and description (what, why, how to test, screenshots).
- Automated checks run (build, tests, lint, security scan).
- Reviewers comment, request changes, or approve.
- Author addresses feedback with new commits.
- Once approved and green, merge (often squash or rebase merge), then delete the branch.
How to give a good review:
- Review the why first: does this change solve the right problem?
- Be specific and kind — critique the code, not the person. Suggest, don’t demand.
- Distinguish blocking issues from nits (prefix nits with “nit:”).
- Look for correctness, edge cases, security, tests, and readability — not just style (leave style to linters/formatters).
- Approve when it’s good enough to ship, not when it’s perfect. Don’t let perfect block progress.
- Review promptly; a stalled PR blocks a teammate.
How to receive a review well:
- Keep PRs small (ideally < 400 lines changed) — small PRs get better, faster reviews.
- Explain context in the description so reviewers don’t have to guess.
- Respond to every comment; push back with reasoning when you disagree.
- Don’t take feedback personally; it’s about the code.
Review checklist:
| Category | Questions |
|---|---|
| Correctness | Does it do what it claims? Edge cases, error handling, nulls? |
| Tests | Are there tests? Do they cover the new behavior and failure paths? |
| Security | Input validation, authz/authn, secrets, injection, dependencies? |
| Performance | N+1 queries, unbounded loops, unnecessary allocations? |
| Readability | Clear names, small functions, comments where non-obvious? |
| Design | Fits existing patterns? Any leaky abstractions or duplication? |
| Backward compat | API/DB migrations safe? Breaking changes documented? |
Collaboration hygiene
Commit messages — Conventional Commits. A good commit message explains why, not just what. The Conventional Commits spec gives a machine-parseable format that also drives automated changelogs and semantic versioning:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
git commit -m "feat(auth): add refresh-token rotation"
git commit -m "fix(api): return 404 when order not found"
git commit -m "refactor(db): extract query builder"
git commit -m "feat!: drop support for API v1
BREAKING CHANGE: v1 endpoints removed; migrate to v2."
Common types: feat, fix, docs, style, refactor, perf, test, build, ci, chore. A ! or a BREAKING CHANGE: footer signals a breaking change (major version bump).
General message rules: imperative mood (“add”, not “added”), concise subject (~50 chars), blank line, then wrapped body explaining rationale and trade-offs.
.gitignore. Never commit build artifacts, dependencies, secrets, or machine-specific files. Keep them out with .gitignore:
# Dependencies
node_modules/
vendor/
# Build output
dist/
target/
*.o
# Secrets & env
.env
.env.local
*.pem
# OS / editor
.DS_Store
.idea/
*.swp
If a secret is already committed, adding it to .gitignore is not enough — it stays in history. Rotate the secret and scrub history with tools like git filter-repo or BFG.
Protecting main. Configure branch protection on the hosting platform so nobody pushes broken code to the mainline:
- Require pull requests before merging (no direct pushes to
main). - Require passing status checks (build, tests, lint) before merge.
- Require at least one (or N) approving reviews.
- Use a
CODEOWNERSfile to auto-request reviews from the right people. - Require signed commits and/or a linear history if desired.
- Restrict force-pushes and deletions on
main.
Semantic versioning and tags. SemVer formats releases as MAJOR.MINOR.PATCH:
| Bump | When | Example |
|---|---|---|
| MAJOR | Incompatible/breaking API changes | 1.4.2 → 2.0.0 |
| MINOR | Backward-compatible new features | 1.4.2 → 1.5.0 |
| PATCH | Backward-compatible bug fixes | 1.4.2 → 1.4.3 |
Mark releases with annotated, ideally signed, tags:
git tag -a v1.5.0 -m "Release 1.5.0" # annotated tag
git tag -s v1.5.0 -m "Release 1.5.0" # signed tag
git push origin v1.5.0 # push a tag
git push origin --tags # push all tags
git describe --tags # human-readable version from tags
Conventional Commits + SemVer + tags enable tools like semantic-release to compute the next version and generate changelogs automatically.
Best Practices
- Commit small and often with focused, logically coherent commits — one concern per commit.
- Write meaningful commit messages (Conventional Commits); explain why, not just what.
- Never commit secrets — use
.gitignore, environment variables, and secret managers; scan with tools likegitleaks. - Keep branches short-lived and rebase/merge from
mainfrequently to avoid painful conflicts. - Keep PRs small and single-purpose so reviews are fast and thorough.
- Protect
main: require reviews and green CI; never force-push shared branches (use--force-with-leaseonly on your own). - Never rewrite public history — use
git revertfor changes others have pulled. - Pull before you push (
git pull --rebase) to integrate others’ work cleanly. - Use
.gitignorefrom day one (see gitignore.io / GitHub templates). - Tag releases with SemVer and automate changelogs from Conventional Commits.
- Automate quality gates in CI (lint, tests, security scans) so review focuses on design — see ./19-cicd-and-deployment.md.
- Review promptly and kindly; use
CODEOWNERSto route reviews to the right people.