26 tháng 3, 2019

Chiến lược phân nhánh và quản lý release khi sử dụng git

Chiến lược phân nhánh và quản lý release khi sử dụng git

Trong bài viết này, tôi sẽ không đề cập chi tiết đến bất kỳ một dự án cụ thể nào, ở đây chỉ đơn thuần là nói về chiến lược phân nhánh và quản lý release.

Hình 1

Hình 1

Tại sao lại sử dụng git?

Để thảo luận kỹ về ưu và nhược điểm của Git so với các hệ thống kiểm soát mã nguồn tập trung, hãy xem tại web, có rất nhiều cuộc tranh cãi về vấn đề này. Là một nhà phát triển, tôi thích Git hơn tất cả các công cụ khác hiện nay. Git thực sự đã thay đổi cách nghĩ của các nhà phát triển về việc merge và phân nhánh.

Với Git, những hành động này cực kỳ đơn giản, và chúng thực sự được coi là một trong những phần cốt lõi trong quy trình làm việc hàng ngày của bạn.

Như một hệ quả của sự đơn giản và lặp đi lặp lại của nó, việc phân nhánh và merge không còn là điều đáng sợ (so với CVS/SVN).

Giờ hãy cùng xem qua mô hình phát triển. Mô hình mà tôi sắp trình bày ở đây về cơ bản không khác gì một bộ quy trình mà mọi thành viên trong nhóm phát triển bắt buộc phải tuân theo khi quản lý phát triển phần mềm.

Phân cấp nhưng tập trung

Thiết lập repo mà chúng tôi sử dụng và hoạt động tốt với mô hình phân nhánh này, đó là với một repo trung tâm. Lưu ý rằng repo này chỉ được coi là một trung tâm (vì Git là một DVCS, không có thứ gọi là repo trung tâm ở cấp độ kỹ thuật). Chúng tôi sẽ đề cập đến repo này như là origin, tên này quen thuộc với tất cả người dùng Git.

Hình 2

Hình 2

Các nhà phát triển thực hiện pullpush origin. Nhưng bên cạnh các mối quan hệ pull/push tập trung, mỗi nhà phát triển cũng có thể thực hiện lệnh pull các thay đổi từ các đồng nghiệp khác để tạo thành các nhóm phụ. Ví dụ, điều này có thể hữu ích khi làm việc cùng với hai hoặc nhiều nhà phát triển trên một tính năng mới, trước khi push lên origin. Trong hình trên, có các phần phụ của Alice và Bob, Alice và David, và Clair và David.

Về mặt kỹ thuật, điều này có nghĩa là Alice đã xác định một Git remote, được đặt tên bob, trỏ đến repo của Bob và ngược lại.

Các nhánh chính

Mô hình phát triển được lấy cảm hứng rất nhiều từ các mô hình hiện nay đang có. Repo trung tâm giữ hai nhánh chính sẽ tồn tại xuyên suốt dự án:

  • master
  • develop

Mọi người sử dụng Git đều cần phải hiểu về nhánh master tại origin. Song song với nhánh master, ta cũng nên cần một nhánh khác gọi là develop.

Hình 3

Hình 3

Chúng tôi quyết định origin/master là chi nhánh chính, đây là nhánh mà mã nguồn HEAD luôn phản ánh trạng thái có thể sẵn sàng đưa lên production.

Chúng tôi coi origin/develop là nhánh chính của mã nguồn, đây là nhánh mà HEAD phải luôn phản ánh trạng thái phát triển mới nhất cho lần release tiếp theo. Một số người sẽ gọi đây là integration branch (nhánh tích hợp) - nhánh này sẽ là nhánh để cơ sở để tạo ra các bản nightly-build.

Khi mã nguồn trong nhánh develop đã đạt đến ngưỡng ổn định và sẵn sàng để được phát hành, tất cả các thay đổi sẽ được merge trở lại vào nhánh master và sau đó được gắn tag với số vesion tương ứng.

Do đó, mỗi khi có bất kỳ thay đổi nào được merge vào nhánh master, theo định nghĩa thì đây được coi như là một bản phát hành sản phẩm. Chúng tôi sử dụng một Git hook script để build tự động và đưa phần mềm đến các server production mỗi khi có một commit mới trên master.

Phân nhánh

Bên cạnh các nhánh chính masterdevelop, mô hình phát triển của chúng tôi sử dụng nhiều nhánh hỗ trợ để hỗ trợ phát triển song song giữa các thành viên trong nhóm, dễ dàng theo dõi các tính năng, chuẩn bị phát hành sản xuất và hỗ trợ khắc phục nhanh các sự cố sản xuất trực tiếp. Không giống như các nhánh chính, các nhánh này luôn có thời gian tồn tại giới hạn, vì cuối cùng chúng cũng sẽ bị loại bỏ.

Có nhiều kiểu branch mà ta có thể sử dụng:

  • Feature branch
  • Release branch
  • Hotfix branch

Mỗi branch này dều có một mục đích cụ thể và bị ràng buộc với các quy tắc nghiêm ngặt về việc xác định nhánh nào là nhánh gốc, và nhánh nào là nhánh đích để merge. Các nhánh được phân loại theo cách chúng tôi sử dụng chúng.

Các nhánh feature

Có thể phân nhánh từ: develop

Phải được merge trở lại vào: develop

Quy ước đặt tên branch: tên bất kì nhưng ngoại trừ master, develop, release-*, hoặc hotfix-*

Hình 4

Hình 4

Các nhánh feature (hoặc đôi khi được gọi là nhánh chủ đề) được sử dụng để phát triển các tính năng mới cho bản phát hành sắp tới hoặc trong tương lai xa. Khi bắt đầu phát triển một tính năng, bản phát hành mục tiêu trong đó tính năng này sẽ được kết hợp có thể chưa được biết đến vào thời điểm đó. Bản chất của nhánh feature là nó tồn tại miễn là tính năng này đang được phát triển, nhưng cuối cùng sẽ được merge trở lại develop (để chắc chắn thêm tính năng mới vào bản phát hành sắp tới) hoặc bị loại bỏ (trong trường hợp thử nghiệm đáng thất vọng).

Các nhánh feature thường tồn tại trong các repo của nhà phát triển, không phải trong origin.

Tạo một nhánh feature

Khi bắt đầu làm việc trên một tính năng mới, hãy tạo một nhánh mới từ nhánh develop.

$ git checkout -b myfeature develop
Switched to a new branch "myfeature"

Tích hợp nhánh feature vào develop

Các tính năng đã hoàn thành, ta có thể merge vào nhánh develop, để có thể thêm những tính năng mới này vào bản phát hành sắp tới:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop

Thực hiện merge mà có sử dụng cờ --no-ff sẽ luôn tạo ra một commit merge, ngay cả khi việc merge có thể được thực hiện với fast-forward. Điều này sẽ làm tránh mất thông tin lịch sử về sự tồn tại của một nhánh feature và một nhóm tất cả các commit phát triển tính năng này. Hãy cùng so sánh:

Hình 5

Hình 5

Trong trường hợp thứ hai, chúng ta khi nhìn vào lịch sử git sẽ không thể thấy được các đối tượng commit cùng thực hiện một tính năng, chúng ta sẽ buộc phải đọc tất cả các git message để có thể hiểu được cả quá trình phát triển. Việc revert toàn bộ một tính năng (tức là một nhóm các commit), thực sự là một vấn đề đau đầu trong trường hợp thứ hai, trong khi nó được thực hiện rất dễ dàng nếu có sử dụng cờ --no-ff khi merge.

Tuy việc sử dụng cờ --no-ff sẽ tạo ra nhiều commit (empty) hơn, nhưng lợi ích mà nó mang lại thực sự rất lớn.

Các nhánh release

Có thể phân nhánh từ: develop

Phải merge trở lại vào: developmaster

Quy ước đặt tên branch: release-*

Các nhánh release hỗ trợ việc phát hành một phiên bản production mới. Chúng cho phép sửa lỗi nhỏ và chuẩn bị meta-data để phát hành (số phiên bản, ngày build, v.v.). Bằng cách thực hiện tất cả các công việc này trên một nhánh release, nhánh develop sẽ được dọn dẹp cho mục đích thực hiện các tính năng mới đợt big-release tiếp theo.

Thời điểm quan trọng để phân nhánh nhánh release mới từ develop là khi phát triển (gần như) phản ánh trạng thái mong muốn của bản phát hành mới. Ít nhất là tất cả các tính năng được nhắm đến cho bản phát hành đã được phát triển phải được merge vào develop ở thời điểm này.

Chính xác là khi khởi động một nhánh release, phiên bản sắp tới sẽ được gán một version-number, không trùng với phiên bản nào trước đó. Cho đến thời điểm đó, nhánh develop đã phản ánh những thay đổi cho bản phát hành tiếp theo, nhưng sẽ không rõ liệu phát hành tiếp theo sẽ là 0.3 hay 1.0. Quyết định đó được đưa ra khi khởi động nhánh release và được thực hiện theo các quy tắc của dự án về việc đánh số phiên bản.

Tạo một nhánh release

Chi nhánh release được tạo ra từ nhánh develop. Ví dụ: giả sử phiên bản 1.1.5 là bản phát hành sản xuất hiện tại và chúng tôi sẽ có một bản phát hành lớn sắp tới. Trạng thái develop đã sẵn sàng cho phiên bản phát hành tiếp theo của Wap và chúng tôi đã quyết định rằng phiên bản này sẽ trở thành phiên bản 1.2 (thay vì 1.1.6 hoặc 2.0). Vì vậy, chúng tôi rẽ nhánh và đặt tên cho nhánh release phản ánh số phiên bản mới:

$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

Sau khi tạo một nhánh mới và chuyển sang nó, chúng tôi tăng số phiên bản. Ở đây, bump-version.sh là một tập lệnh shell hư cấu thay đổi một số tệp trong bản sao làm việc để phản ánh phiên bản mới. (Tất nhiên đây có thể là một thay đổi thủ công. Điểm quan trọng là một số tệp thay đổi.) Sau đó, số phiên bản bị lỗi được commit.

Nhánh mới này có thể tồn tại ở đó trong một thời gian đến khi việc phát hành có thể chắc chắn được triển khai. Trong thời gian đó, việc sửa lỗi có thể được áp dụng trong nhánh này (chứ không phải trên nhánh develop). Việc thêm các tính năng mới lớn ở đây sẽ bị nghiêm cấm. Chúng phải được merge vào develop, và do đó phải đợi release ở phiên bản lớn tiếp theo.

Hoàn thành release branch

Khi trạng thái của nhánh release đã sẵn sàng để trở thành một bản phát hành thực sự, ta cần thực hiện một số công việc nhất định:

  • Đầu tiên, nhánh release phải được merge vào master (vì theo định nghĩa mỗi commit trên master là một bản phát hành mới).

  • Commit trên master phải được gắn tag để dễ dàng tham chiếu lịch sử trong tương lai.

  • Cuối cùng, các thay đổi được thực hiện trên nhánh release cần được merge trở lại vào nhánh develop, để các bản phát hành trong tương lai cũng chứa các bản sửa lỗi này.

Hai bước đầu tiên trong Git:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2

Việc phát hành hiện đã được thực hiện và được gắn tag để tham khảo trong tương lai.

Note: Bạn cũng có thể muốn sử dụng cờ -s hoặc -u <key> để ký tag của bạn bằng mật mã.

Để giữ những thay đổi được thực hiện trong nhánh release, chúng ta cần merge những thay đổi đó vào develop. Trong Git:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)

Bước này cũng có thể dẫn đến xung đột merge (có thể là do chúng tôi đã thay đổi số phiên bản). Nếu vậy, sửa chữa nó và commit.

Bây giờ chúng tôi đã thực sự hoàn thành và nhánh release có thể bị xóa, vì chúng tôi không cần nó nữa:

$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

Các nhánh hotfix

Nhánh có thể tắt từ: master

Phải merge trở lại vào: developmaster

Quy ước đặt tên chi nhánh: hotfix-*

Các nhánh hotfix rất giống với các nhánh release ở chỗ chúng cũng có nghĩa là để chuẩn bị cho một bản release-production mới, mặc dù không có kế hoạch. Họ phát sinh từ sự cần thiết phải hành động ngay lập tức với trạng thái không mong muốn của phiên bản sản xuất trực tiếp. Khi một lỗi nghiêm trọng trong phiên bản production phải được khắc phục ngay lập tức, một nhánh hotfix có thể được phân nhánh từ tag tương ứng trên nhánh chính mà có đánh dấu phiên bản production.

Hình 6

Hình 6

Bản chất là công việc của các thành viên trong nhóm (trên nhánh develop) vẫn có thể tiếp tục, trong khi một người khác đang chuẩn bị một bản hotfix cho môi trường production

Creating the hotfix branch

Các nhánh hotfix được tạo từ nhánh master. Ví dụ: giả sử phiên bản 1.2 là bản release-production hiện tại đang chạy trực tiếp và gây rắc rối do lỗi nghiêm trọng. Nhưng những thay đổi trên develop vẫn chưa thực sự ổn định. Thì chúng ta có thể tạo một nhánh hotfix và bắt đầu khắc phục sự cố:

$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

Đừng quên tăng số phiên bản sau khi phân nhánh!

Sau đó, sửa lỗi và commit sửa lỗi trong một hoặc nhiều commit riêng biệt.

$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

Kết thúc một nhánh hotfix

Khi kết thúc, lỗi cần phải được merge trở lại master, nhưng cũng cần được merge trở lại develop, để bảo vệ rằng lỗi cũng được bao gồm trong bản phát hành tiếp theo. Điều này hoàn toàn tương tự như cách các nhánh release được hoàn thành.

Đầu tiên, cập nhật master và gắn tag release.

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1

Note: Bạn cũng có thể muốn sử dụng cờ -s hoặc -u <key> để ký thẻ của bạn bằng mật mã.

Tiếp theo, bao gồm cả lỗi trong develop:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

Một ngoại lệ cho quy tắc ở đây là, khi nhánh release hiện đang tồn tại, các thay đổi hotfix cần được merge vào nhánh release đó, thay vì develop. Bugfix được merge vào nhánh release cuối cùng cũng sẽ được merge vào develop khi nhánh release kết thúc. (Lưu ý là nếu công việc develop yêu cầu ngay lập tức cần sửa lỗi này và không thể đợi đến khi nhánh release kết thúc, bạn cũng có thể merge bugfix đó vào nhánh develop lúc này)

Cuối cùng là xóa nhánh hotfix khi đã được merge:

$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).