</>Học Dev
Bài học

Tuần 2 - Ngày 5: Fork/Upstream Sync — Agency & Contractor Pattern

Tuần 2 – Ngày 5

Tuần 2 - Ngày 5: Fork/Upstream Sync — Agency & Contractor Pattern

Mục tiêu học tập

  • Hiểu triangular workflow (fork ↔ upstream)
  • Setup remote upstream trỏ đến client repo
  • Thực hành sync flow: fetch upstream → rebase → force-push to fork an toàn
  • Xử lý conflict khi rebase nhiều commits lên upstream mới nhất
  • Nắm bidirectional sync khi client merge back

1. Bối Cảnh: Agency/Contractor Pattern

Tình huống thực tế: Công ty bạn (agency) làm dự án cho client. Client có repo riêng. Workflow:

GitHub:client-org/project-repo(upstreamclientshu)forkyour-agency/project-repo(originteambn)Localmachine(mideveloper):upstreamclient-org/project-repooriginyour-agency/project-repolocalclonecaoriginFlowcode:DeveloperpushoriginPRupstreamupstreamfetchrebaseorigin(keepinsync)

2. Setup Ban Đầu

# 1. Clone fork của agency (origin tự động được setup)
git clone https://github.com/your-agency/project-repo.git
cd project-repo

# 2. Thêm upstream (client repo)
git remote add upstream https://github.com/client-org/project-repo.git

# 3. Kiểm tra remotes
git remote -v
# origin    https://github.com/your-agency/project-repo.git (fetch)
# origin    https://github.com/your-agency/project-repo.git (push)
# upstream  https://github.com/client-org/project-repo.git (fetch)
# upstream  https://github.com/client-org/project-repo.git (push)

# Best practice: chỉ fetch từ upstream, không push thẳng vào upstream
# (PR mới là cách đúng để contribute code lên upstream)
git remote set-url --push upstream DISABLE
# Giờ "git push upstream" sẽ báo lỗi — tránh accident push

# Verify
git remote -v
# upstream  https://github.com/client-org/project-repo.git (fetch)
# upstream  DISABLE (push)

3. Daily Sync Flow

Mỗi ngày trước khi bắt đầu làm việc:

# Bước 1: Fetch toàn bộ cập nhật từ upstream
git fetch upstream
# remote: Enumerating objects: 23, done.
# remote: Counting objects: 100% (23/23), done.
# From https://github.com/client-org/project-repo
#    abc1234..def5678  main       -> upstream/main
#    xyz9012..uvw3456  develop    -> upstream/develop

# Bước 2: Rebase local main lên upstream/main
git switch main
git rebase upstream/main
# Successfully rebased and updated refs/heads/main.

# Bước 3: Sync origin (team fork) với upstream
git push origin main
# Nếu origin đã có divergent history (ai đó push trực tiếp), dùng --force-with-lease:
git push origin main --force-with-lease

# Bước 4: Rebase feature branch đang làm lên main mới nhất
git switch feature/my-task
git rebase main

Tại sao dùng rebase thay vì merge?

# Merge: tạo merge commit mỗi lần sync → lịch sử fork lộn xộn khi PR
#
# git merge upstream/main
#
# * abc1234 Merge remote-tracking branch 'upstream/main'  ← xấu, không cần thiết
# * def5678 feat: my feature

# Rebase: lịch sử tuyến tính, PR dễ review
#
# git rebase upstream/main
#
# * def5678 feat: my feature  ← commit của bạn nằm trên đỉnh upstream
# * [upstream commits]

4. Triangular Workflow — Push to Fork, PR to Upstream

fetch/pullupstream(PR,notdirectpush)fetchlocalpushoriginrebase/work
# Workflow chuẩn:
# 1. Sync (như trên)
# 2. Tạo feature branch
git switch -c feature/new-api-endpoint

# 3. Code và commit
git add . && git commit -m "feat: add /api/v2/users endpoint"
git add . && git commit -m "feat: add pagination to user list"

# 4. Trước khi push, rebase lên upstream mới nhất
git fetch upstream
git rebase upstream/main
# Nếu conflict → xử lý (xem section 5)

# 5. Push feature branch lên fork (origin)
git push origin feature/new-api-endpoint

# 6. Tạo PR từ fork → upstream trên GitHub
gh pr create \
  --repo client-org/project-repo \
  --head your-agency:feature/new-api-endpoint \
  --base main \
  --title "feat: add /api/v2/users endpoint with pagination" \
  --body "..."

5. Xử Lý Conflict Khi Rebase Nhiều Commits

Khi rebase một feature branch dài lên upstream đã có nhiều commits mới:

git fetch upstream
git rebase upstream/main

# Git rebase nhiều commits — có thể conflict ở từng commit:
# CONFLICT (content): Merge conflict in src/api/users.ts
# error: could not apply a1b2c3d... feat: add /api/v2/users endpoint

# Xem conflict hiện tại đang ở commit nào
git status
# rebase in progress; onto upstream/main
# Currently rebasing branch 'feature/new-api-endpoint' on 'abc9876'

# Resolve conflict (xem bài day-03)
vim src/api/users.ts
git add src/api/users.ts
git rebase --continue

# Nếu conflict tiếp tục ở commit tiếp theo → lại resolve → continue
# ...cho đến hết

# Bỏ qua commit hiện tại (nếu nó đã được upstream xử lý rồi)
git rebase --skip

# Hủy toàn bộ rebase, quay về trạng thái trước
git rebase --abort

Rebase nhiều commits với rerere

# Bật rerere để tái dùng conflict resolution (quan trọng khi rebase thường xuyên)
git config rerere.enabled true

# Lần đầu resolve conflict ở commit A → rerere ghi nhớ
# Lần sau rebase lại, gặp conflict y hệt → rerere tự apply

6. Force-Push to Fork An Toàn

Sau khi rebase, cần force-push để cập nhật fork:

# KHÔNG an toàn — xoá commits của đồng nghiệp nếu họ đã push lên cùng branch
git push origin feature/new-api-endpoint --force

# AN TOÀN — kiểm tra remote-tracking ref trước khi push
git push origin feature/new-api-endpoint --force-with-lease
# Nếu đồng nghiệp đã push lên feature branch đó:
# error: failed to push some refs
# hint: Updates were rejected because the remote contains work that you do
# hint: not have locally.
# → Bạn phải fetch và giải quyết conflict trước

# Cấu hình alias cho tiện
git config --global alias.fpush 'push --force-with-lease'
git fpush origin feature/new-api-endpoint

Khi nào force-push được phép?

OK để force-push:
  ✓ Branch cá nhân (chỉ mình bạn làm việc trên đó)
  ✓ Feature branch chưa ai khác check out
  ✓ Sau khi thông báo team: "Tôi đang rebase + force push feature/X"

KHÔNG force-push:
  ✗ main, develop, release/* — shared branches
  ✗ Branch đang có open PR mà reviewer đã để comments theo line number
    (rebase thay đổi commit hash → comments bị mất context)
  ✗ Không có --force-with-lease (luôn dùng --force-with-lease)

7. Bidirectional Sync: Client Merges Back

Khi client accept PR của team bạn vào upstream, fork cần được sync lại:

# Sau khi PR được merged vào upstream/main:
git fetch upstream
git switch main
git rebase upstream/main

# Origin (fork) cần được cập nhật
git push origin main
# Nếu main đã diverge:
git push origin main --force-with-lease

# Xoá branch đã merged (local + remote)
git branch -d feature/new-api-endpoint
git push origin --delete feature/new-api-endpoint

Khi client có hotfix chưa được PR

Đôi khi client push trực tiếp lên upstream mà không qua PR của team:

# Phát hiện: upstream/main có commits mà origin/main không có
git fetch upstream
git log origin/main..upstream/main --oneline
# abc1234 hotfix: patch critical security issue (from client team)

# Sync fork với upstream
git switch main
git rebase upstream/main
git push origin main --force-with-lease

# Rebase tất cả feature branches đang active lên main mới
git switch feature/my-ongoing-work
git rebase main

8. Divergent History và Reflog Recovery

Khi history bị phân kỳ quá nhiều

# Xem mức độ diverge
git log origin/main..upstream/main --oneline | wc -l   # upstream ahead bao nhiêu commit
git log upstream/main..origin/main --oneline | wc -l   # fork ahead bao nhiêu commit

# Nếu fork có commits không nên có (accident commits trực tiếp vào main):
# Option 1: Reset fork/main về upstream/main
git switch main
git reset --hard upstream/main
git push origin main --force-with-lease

# Option 2: Nếu cần giữ lại vài commits cụ thể từ fork
git log origin/main..upstream/main --oneline  # xem commits cần cherry-pick
git reset --hard upstream/main
git cherry-pick <commit-hash>
git push origin main --force-with-lease

Lấy lại commits bị mất qua reflog

# Tình huống: rebase sai → commits "biến mất"
git reflog
# abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature/X
# def5678 HEAD@{3}: commit: feat: my important commit ← commit bị mất
# ...

# Khôi phục commit bị mất
git checkout -b recovery/feature-x def5678
# Hoặc cherry-pick:
git cherry-pick def5678

# Nếu rebase bị lỗi hoàn toàn, trở về ORIG_HEAD
git reset --hard ORIG_HEAD
# (ORIG_HEAD được Git set tự động trước khi rebase/merge)

Câu hỏi ôn tập

  1. Tại sao nên disable push URL cho upstream remote?

    Xem đáp án

    Để tránh accident push thẳng vào upstream (repo của client). Nguyên tắc: chỉ contribute code qua PR từ fork → upstream, không push trực tiếp. git remote set-url --push upstream DISABLE làm cho git push upstream báo lỗi ngay — nhắc nhở bạn phải dùng PR. Vẫn có thể git fetch upstream bình thường.

  2. Khi rebase feature branch lên upstream/main gặp conflict ở commit thứ 3, làm gì sau khi resolve?

    Xem đáp án

    (1) Chỉnh sửa file để resolve conflict, (2) git add <file>, (3) git rebase --continue. Git sẽ áp dụng commit thứ 3 xong rồi tiếp tục với các commits tiếp theo — có thể gặp thêm conflict ở commit sau. Nếu muốn bỏ qua commit hiện tại (đã được upstream handle rồi): git rebase --skip. Để hủy toàn bộ: git rebase --abort.

  3. --force-with-lease bảo vệ điều gì so với --force?

    Xem đáp án

    --force-with-lease kiểm tra remote-tracking ref local (ví dụ origin/feature/x) có khớp với remote thực hay không. Nếu đồng nghiệp đã push lên branch đó trong lúc bạn rebase, remote-tracking ref local đã lỗi thời → Git từ chối push. --force ghi đè vô điều kiện, có thể xoá commits của đồng nghiệp mà không biết.

  4. Sau khi PR được merged vào upstream, bạn phải làm gì để fork đồng bộ?

    Xem đáp án

    (1) git fetch upstream để cập nhật remote-tracking branches, (2) git switch main && git rebase upstream/main để sync local main, (3) git push origin main (có thể cần --force-with-lease) để cập nhật fork, (4) git branch -d feature/my-branch && git push origin --delete feature/my-branch để dọn branch đã merged.

  5. Lệnh nào giúp xem commits đang có trong upstream nhưng chưa có trong fork?

    Xem đáp án

    git log origin/main..upstream/main --oneline — hiện commits có trong upstream/main nhưng chưa có trong origin/main (fork). Nhớ git fetch upstream trước để cập nhật remote-tracking refs. Để xem chiều ngược lại (fork ahead upstream): git log upstream/main..origin/main --oneline.

Bài tập thực hành

# Mô phỏng triangular workflow với 2 repos local
mkdir upstream-sim && cd upstream-sim
git init --bare upstream.git

mkdir client-work && cd client-work
git clone ../upstream.git .
git config user.name "Client" && git config user.email "client@test.com"
echo "initial" > README.md
git add . && git commit -m "feat: initial commit"
git push origin main
cd ..

# "Fork" — clone upstream làm origin cho agency
mkdir agency-fork && cd agency-fork
git clone ../upstream.git .
git config user.name "Agency Dev" && git config user.email "dev@agency.com"
git remote add upstream ../upstream.git
git remote set-url --push upstream DISABLE
git remote -v

# Client thêm commits vào upstream
cd ../client-work
echo "client feature" >> README.md
git add . && git commit -m "feat: client adds new feature"
git push origin main

# Agency sync với upstream
cd ../agency-fork
git fetch upstream
git log upstream/main --oneline    # thấy commit mới từ client

git switch main
git rebase upstream/main           # sync
git log --oneline                  # history tuyến tính

# Agency tạo feature branch và làm việc
git switch -c feature/agency-work
echo "agency feature" >> README.md
git add . && git commit -m "feat: agency adds API endpoint"

# Trước khi push, sync với upstream mới nhất (giả sử client push thêm)
cd ../client-work
echo "another client commit" >> README.md
git add . && git commit -m "feat: client hotfix"
git push origin main

cd ../agency-fork
git fetch upstream
git rebase upstream/main  # rebase feature branch lên upstream mới nhất
git log --oneline --graph --all

Tài liệu tham khảo chính thức


Tiếp theo: Ngày 6 — Advanced Git Tools