Chuyển đổi SVN sang Git với git-svn: Bảo toàn lịch sử commit và branches cho dự án legacy

Git tutorial - IT technology blog
Git tutorial - IT technology blog

Khi dự án legacy “mắc kẹt” với SVN

Nhận nhiệm vụ maintain một project chạy từ năm 2016, codebase đang nằm trên SVN server của công ty cũ — đó là tình huống mình gặp cách đây không lâu. Toàn bộ lịch sử 7 năm commit, hàng chục branches, mọi thứ đều ở định dạng SVN. Team mới muốn chuyển sang Git để tích hợp CI/CD, nhưng không ai muốn vứt đi lịch sử commit quý giá đó.

Sau khi thử vài phương án, mình chọn git-svn — bridge tool có sẵn trong Git, không cần cài thêm gì phức tạp. Nó không export code rồi import lại từ đầu mà chuyển đổi thực sự, revision từng revision, giữ nguyên từng commit, tác giả, branch structure.

git-svn hoạt động như thế nào?

git-svn có từ Git 1.5.x, đọc từng SVN revision và tạo Git commit tương ứng. Cụ thể, nó xử lý ba việc:

  • Map SVN revision number (r1, r2, r3…) thành Git commit SHA
  • Chuyển SVN username thành Git author format (name + email)
  • Convert cấu trúc thư mục SVN (trunk/branches/tags) sang Git branches và tags

Sự khác biệt cấu trúc giữa SVN và Git

SVN dùng revision number tuyến tính và lưu branches như thư mục thật trên server:

svn-repo/
├── trunk/              ← tương đương main branch
├── branches/
│   ├── feature-login/
│   └── hotfix-payment/
└── tags/
    ├── v1.0/
    └── v2.1/

Git thì khác: branches và tags chỉ là con trỏ, không phải thư mục thật. git-svn tự động map trunk thành main, branches/* thành Git branches, tags/* thành Git tags. Sau khi clone, bạn cần thêm một bước convert để chúng trở thành local branches và tags thực sự thay vì remote tracking refs.

Thực hành: Migrate SVN sang Git từng bước

Demo dưới đây dùng SVN repo tại https://svn.example.com/myproject với cấu trúc trunk/branches/tags chuẩn.

Bước 1: Cài đặt git-svn

Trên nhiều distro, git-svn không đi kèm mặc định với Git:

# Ubuntu/Debian
sudo apt-get install git-svn

# CentOS/RHEL
sudo yum install git-svn

# macOS (Homebrew)
brew install git-svn

# Xác nhận đã cài
git svn --version

Bước 2: Tạo file mapping authors

SVN chỉ lưu username (ví dụ: john_dev), còn Git cần cả tên đầy đủ và email. Đầu tiên, lấy danh sách toàn bộ SVN authors:

svn log https://svn.example.com/myproject --xml --quiet \
  | grep "<author>" \
  | sort -u \
  | sed 's/.*<author>\(.*\)<\/author>.*/\1/' > /tmp/svn-authors-raw.txt

cat /tmp/svn-authors-raw.txt

Từ danh sách đó, tạo file authors.txt:

john_dev = John Nguyen <[email protected]>
mary_tran = Mary Tran <[email protected]>
admin = Admin Bot <[email protected]>

Format mỗi dòng: svn_username = Full Name <email>. Đừng bỏ sót author nào — git-svn sẽ dừng ngay tại revision đầu tiên gặp username thiếu và báo lỗi, không tiếp tục được.

Bước 3: Clone SVN repo bằng git-svn

Đây là bước ngốn thời gian nhất. Repo 1.000 revisions mất khoảng 10–20 phút; 10.000+ revisions có thể chạy cả buổi tuỳ tốc độ mạng và server. Nên bật screen hoặc tmux trước:

screen -S svn-migration

git svn clone https://svn.example.com/myproject \
  --stdlayout \
  --authors-file=authors.txt \
  --no-metadata \
  myproject-git

# Ctrl+A, D để detach screen nếu cần

Các flag quan trọng:

  • --stdlayout: Tự nhận diện cấu trúc trunk/branches/tags chuẩn (viết tắt là -s)
  • --authors-file: File mapping đã tạo ở Bước 2
  • --no-metadata: Không thêm chuỗi git-svn-id: vào cuối mỗi commit message — commit history sẽ gọn hơn

SVN repo không dùng cấu trúc chuẩn? Bỏ --stdlayout và chỉ định thủ công:

git svn clone https://svn.example.com/myproject \
  --trunk=code \
  --branches=feature-branches \
  --tags=releases \
  --authors-file=authors.txt \
  myproject-git

Bước 4: Convert branches và tags

Sau khi clone xong, branches và tags SVN xuất hiện dưới dạng remote tracking refs (refs/remotes/*), không phải Git local branches hay tags thực sự. Cần convert:

cd myproject-git

# Convert SVN tags thành Git tags thực sự
git for-each-ref refs/remotes/tags | cut -d / -f 4- | while read tagname; do
  git tag "$tagname" "refs/remotes/tags/$tagname"
  git branch -r -d "tags/$tagname"
done

# Convert SVN branches thành Git local branches
git for-each-ref refs/remotes | grep -v '@' | grep -v 'tags' | cut -d / -f 3- | while read branchname; do
  git branch "$branchname" "refs/remotes/$branchname"
  git branch -r -d "$branchname"
done

# Đổi tên trunk thành main
git branch -m trunk main

Kiểm tra kết quả:

git branch -a          # Xem tất cả local branches
git tag -l             # Xem tất cả tags
git log --oneline -15  # Kiểm tra lịch sử commit

Bước 5: Push lên remote Git repository

Tạo repo trống trên GitHub, GitLab hoặc Gitea, sau đó push toàn bộ lên:

git remote add origin https://github.com/yourorg/myproject.git

# Push tất cả branches
git push origin --all

# Push tất cả tags
git push origin --tags

Một điểm cần nhắc ở bước này: đừng dùng --force. Mình từng mất code quan trọng vì force push nhầm branch vào repo đang có người khác làm việc — từ đó luôn cẩn thận với git push --force. Repo remote ban đầu còn trống, không có lý do gì phải dùng force cả. Gặp lỗi thì tìm nguyên nhân thật sự thay vì dùng --force để qua mặt.

Những vấn đề hay gặp trong thực tế

Author bị thiếu trong file mapping

Clone dừng với thông báo:

Author: old_contractor not defined in authors.txt file

Thêm dòng thiếu vào authors.txt rồi resume — không cần clone lại từ đầu:

echo "old_contractor = Old Contractor <[email protected]>" >> authors.txt
cd myproject-git
git svn fetch

Repo có quá nhiều revision

10.000+ revisions mà lịch sử xa không cần thiết? Giới hạn từ một mốc cụ thể:

# Chỉ lấy từ revision 8000 đến HEAD
git svn clone https://svn.example.com/myproject \
  -r 8000:HEAD \
  --stdlayout \
  --authors-file=authors.txt \
  myproject-git

Xác minh kết quả sau migration

Sau khi xong, so sánh số lượng commit để đảm bảo không bị miss:

# Đếm revisions trong SVN (chạy từ máy có svn client)
svn log https://svn.example.com/myproject | grep -c "^r[0-9]"

# Đếm commits trong Git repo vừa migrate
cd myproject-git
git log --all --oneline | wc -l

Hai con số thường không khớp hoàn toàn — SVN có merge revisions và property-only changes không tạo commit riêng trong Git. Chênh lệch dưới 10% là bình thường; chênh lệch lớn hơn 30% thì nên kiểm tra lại quá trình convert.

Sau migration: Những việc cần làm ngay

Code đã lên Git rồi, nhưng đó mới là phần dễ. Việc quan trọng hơn là đảm bảo cả team chuyển hẳn — không ai vẫn commit nhầm lên SVN cũ:

  • Thông báo toàn team, gửi link Git repo mới và hướng dẫn clone về
  • Đặt SVN repo ở chế độ read-only (hoặc lock hoàn toàn) để tránh ai đó vẫn commit nhầm lên SVN
  • Cập nhật CI/CD pipeline từ SVN checkout sang Git clone
  • Kiểm tra .svn directory không lọt vào repo Git (thêm vào .gitignore nếu cần)

Tuần đầu sẽ có người hỏi “clone về thế nào”, “switch branch kiểu gì” — bình thường, chuẩn bị sẵn một doc ngắn về Git workflow cơ bản là đủ. Qua giai đoạn đó, thứ team thường nhận ra đầu tiên là branching trong Git không còn “đắt” như SVN — tạo branch mới chỉ tốn vài millisecond thay vì copy cả thư mục trên server.

Share: