Trong git, làm thế nào để tạo phiên bản cho hàng tá thư viện hoạt động song song


11

Chúng tôi đang thực hiện các dự án, nhưng chúng tôi sử dụng lại rất nhiều mã giữa các dự án và có rất nhiều thư viện chứa mã chung của chúng tôi. Khi chúng tôi thực hiện các dự án mới, chúng tôi tìm thấy nhiều cách hơn để xác định mã chung và đưa nó vào thư viện. Các thư viện phụ thuộc lẫn nhau, và các dự án phụ thuộc vào các thư viện. Mỗi dự án và tất cả các thư viện được sử dụng trong dự án đó, cần sử dụng cùng một phiên bản của tất cả các thư viện mà chúng đang đề cập. Nếu chúng tôi phát hành một phần mềm, chúng tôi sẽ phải sửa lỗi và có thể thêm các tính năng mới trong nhiều năm, đôi khi trong nhiều thập kỷ. Chúng tôi có khoảng một tá thư viện, các thay đổi thường cắt ngang hơn hai và một số nhóm làm việc song song với một số dự án, thực hiện các thay đổi đồng thời cho tất cả các thư viện này.

Gần đây chúng tôi đã chuyển sang git và thiết lập kho lưu trữ cho từng thư viện và từng dự án. Chúng tôi sử dụng stash như một kho lưu trữ phổ biến, thực hiện các công cụ mới trên các nhánh tính năng, sau đó thực hiện các yêu cầu kéo và hợp nhất chúng sau khi xem xét.

Nhiều vấn đề chúng ta phải giải quyết trong các dự án đòi hỏi chúng ta phải thay đổi trên một số thư viện và mã cụ thể của dự án. Chúng thường bao gồm những thay đổi của giao diện thư viện, một số trong đó không tương thích. (Nếu bạn nghĩ điều này nghe có vẻ tởm Ví dụ, hãy tưởng tượng một dự án P1sử dụng các thư viện L1, L2L3. L1cũng sử dụng L2L3, và L2sử dụng L3là tốt. Biểu đồ phụ thuộc trông như thế này:

   <-------L1<--+
P1 <----+  ^    |
   <-+  |  |    |
     |  +--L2   |
     |     ^    |
     |     |    |
     +-----L3---+

Bây giờ hãy tưởng tượng một tính năng cho dự án này đòi hỏi phải thay đổi P1L3thay đổi giao diện L3. Bây giờ thêm các dự án P2P3vào hỗn hợp, cũng tham khảo các thư viện này. Chúng tôi không thể đủ khả năng để chuyển tất cả chúng sang giao diện mới, chạy tất cả các thử nghiệm và triển khai phần mềm mới. Vì vậy, những gì thay thế?

  1. thực hiện giao diện mới trong L3
  2. thực hiện một yêu cầu kéo L3và chờ xem xét
  3. hợp nhất sự thay đổi
  4. tạo một bản phát hành mới của L3
  5. bắt đầu làm việc với tính năng này P1bằng cách làm cho nó tham khảo L3bản phát hành mới, sau đó triển khai tính năng này trên P1nhánh tính năng của
  6. thực hiện một yêu cầu kéo, xem xét và sáp nhập

(Tôi chỉ nhận thấy rằng tôi quên để chuyển đổi L1L2việc phát hành mới. Và tôi thậm chí không biết được nơi để dính vào trong này, bởi vì nó sẽ cần phải được thực hiện song song với P1...)

Đây là một quá trình tẻ nhạt, dễ bị lỗi và rất dài để thực hiện tính năng này, nó đòi hỏi phải đánh giá độc lập (khiến việc đánh giá khó khăn hơn nhiều), không mở rộng quy mô và có khả năng khiến chúng tôi không hoạt động vì chúng tôi bị sa lầy trong quá trình chúng tôi không bao giờ hoàn thành công việc.

Nhưng làm thế nào để chúng tôi sử dụng phân nhánh và gắn thẻ để tạo ra một quy trình cho phép chúng tôi thực hiện các tính năng mới trong các dự án mới mà không cần quá nhiều chi phí?


1
Thay đổi công cụ của bạn không nên ảnh hưởng đến quá trình bạn có tại chỗ quá nhiều. Vì vậy, làm thế nào bạn đối phó với vấn đề này trước khi bạn chuyển sang git?
Bart van Ingen Schenau

Có thể chỉ cần thêm một phương thức mới vào giao diện mà không phá vỡ phương thức hiện có khi có quá nhiều thư viện phụ thuộc vào nó? Thông thường đó không phải là ý tưởng tốt nhất nhưng ít nhất nó sẽ cho phép bạn "tiếp tục" thực hiện tính năng mới và bạn có thể phản đối đúng phương pháp cũ bất cứ khi nào có thời gian rảnh. Hay những giao diện này quá trạng thái để "giao diện song song" như thế hoạt động?
Ixrec

1
@Ixrec: Tôi đã nói rằng "đây không phải là cách làm". Mọi người đều sử dụng kho riêng cho các dự án riêng lẻ, vì vậy chúng tôi đã quyết định làm điều đó.
sbi

2
Tôi sẽ lập luận rằng chúng không phải là các dự án riêng biệt nếu chúng thường xuyên phải được thay đổi song song. Ranh giới giữa các "dự án" phải luôn có một số loại imo đảm bảo khả năng tương thích ngược dài hạn.
Ixrec

1
@Ixrec: Tích hợp nhiều phần cứng tương tự như chuyển mã sang nhiều nền tảng hơn: bạn càng làm điều này, bạn càng cần phải thay đổi ít hơn đối với phần cứng / nền tảng khác. Vì vậy, về lâu dài, mã sẽ ổn định. Tuy nhiên, ngay bây giờ chúng tôi sẽ cần tìm một quy trình cho phép chúng tôi ở lại thị trường đủ lâu để đến đó.
sbi

Câu trả lời:


5

Loại đưa ra rõ ràng ở đây, nhưng có lẽ đáng để đề cập đến nó.

Thông thường, git repos được tùy biến theo lib / dự án vì chúng có xu hướng độc lập. Bạn cập nhật dự án của mình và không quan tâm đến phần còn lại. Các dự án khác tùy thuộc vào nó sẽ chỉ cập nhật lib của họ bất cứ khi nào họ thấy phù hợp.

Tuy nhiên, trường hợp của bạn có vẻ phụ thuộc nhiều vào các thành phần tương quan, do đó một tính năng thường ảnh hưởng đến nhiều trong số chúng. Và toàn bộ phải được đóng gói như một gói. Vì việc triển khai một tính năng / thay đổi / lỗi thường đòi hỏi phải điều chỉnh nhiều thư viện / dự án khác nhau cùng một lúc, có lẽ sẽ hợp lý khi đặt tất cả chúng trong cùng một repo.

Có những ưu điểm / nhược điểm mạnh mẽ cho việc này.

Ưu điểm:

  • Tracability: chi nhánh hiển thị mọi thứ thay đổi trong mỗi dự án / lib liên quan đến tính năng / lỗi này.
  • Gói: chỉ cần chọn một thẻ và bạn sẽ nhận được tất cả các nguồn đúng.

Hạn chế:

  • Sáp nhập: ... đôi khi thật khó khăn với một dự án duy nhất. Với các nhóm khác nhau làm việc trên các chi nhánh được chia sẻ, hãy sẵn sàng để chuẩn bị cho tác động.
  • Yếu tố "oops" nguy hiểm: nếu một nhân viên làm hỏng kho lưu trữ bằng cách mắc một số lỗi, nó có thể ảnh hưởng đến tất cả các dự án & nhóm.

Tùy thuộc vào bạn để biết giá có xứng đáng với lợi ích hay không.

BIÊN TẬP:

Nó sẽ hoạt động như thế này:

  • Tính năng X phải được thực hiện
  • Tạo chi nhánh feature_x
  • Tất cả các nhà phát triển tham gia làm việc trên nhánh này và làm việc ngang hàng với nó, có thể trong các thư mục chuyên dụng liên quan đến dự án / lib của họ
  • Khi nó kết thúc, xem lại nó, kiểm tra nó, đóng gói nó, bất cứ điều gì
  • Hợp nhất nó trở lại trong bản gốc ... và đây có thể là phần khó khăn vì trong thời gian đó feature_yfeature_zcũng có thể được thêm vào. Nó trở thành một sự hợp nhất "nhóm chéo". Đây là lý do tại sao nó là một nhược điểm nghiêm trọng.

Chỉ dành cho hồ sơ: Tôi nghĩ rằng trong hầu hết các trường hợp, đây là một ý tưởng tồi và nên được thực hiện một cách thận trọng vì nhược điểm hợp nhất thường cao hơn so với cái bạn có được thông qua quản lý phụ thuộc / theo dõi tính năng phù hợp.


Cảm ơn, chúng tôi thực sự đang xem xét điều này. Điều tôi không hiểu dưới ánh sáng của điều này là cách chúng ta thực hiện phân nhánh với git. Trong SVN, các nhánh có nghĩa là sao chép một cây con (trong kho lưu trữ) sang một nơi khác (trong kho lưu trữ). Với điều này, thật dễ dàng để tạo các nhánh của bất kỳ cây con nào trong repo của bạn, vì vậy nếu bạn có nhiều dự án trong đó, bạn có thể tách từng nhánh trong số chúng. Có một cái gì đó như thế này trong git, hoặc chúng ta sẽ luôn luôn chỉ có thể phân nhánh toàn bộ repo?
sbi

@sbi: bạn chi nhánh toàn bộ repo. Bạn không thể làm việc với cây con trong các nhánh khác nhau, điều này sẽ đánh bại điểm trong trường hợp của bạn. Mặc dù vậy, Git sẽ không "sao chép" bất cứ điều gì, nó chỉ đơn giản là sẽ theo dõi những thay đổi trong nhánh bạn đang làm việc.
dagnelies

Vì vậy, điều này đòi hỏi ai đó tạo một nhánh tính năng cho một thư viện để hợp nhất tất cả các thư viện khác khi hợp nhất hoặc nổi loạn. Đây là một nhược điểm thực sự. (BTW, SVN cũng chỉ bao giờ thực hiện các bản sao lười biếng.)
sbi

@sbi: xem chỉnh sửa
dagnelies

1
Chà, hiện tại hầu hết chúng ta đều không thoải mái. :-/Hơn nữa, ngay cả những người (và những người thúc đẩy chuyển sang git), không biết làm thế nào để làm cho quá trình phát triển của chúng ta phù hợp với git. Thở dài. Sẽ là một vài tháng khó khăn, tôi sợ, cho đến khi mọi thứ bắt đầu trở nên suôn sẻ hơn. Dù sao cũng cảm ơn bạn là câu trả lời hữu ích nhất / duy nhất.
sbi

4

Giải pháp bạn đang tìm kiếm là một công cụ quản lý phụ thuộc phối hợp với các mô đun con git

Các công cụ như:

  • Maven
  • Con kiến
  • Nhà soạn nhạc

Bạn có thể sử dụng các công cụ đó để xác định các phụ thuộc của một dự án.

Bạn có thể yêu cầu một mô hình con tối thiểu là phiên bản > 2.xx hoặc biểu thị một loạt các phiên bản tương thích = 2.2. * Hoặc ít hơn một phiên bản cụ thể <2.2.3

Bất cứ khi nào bạn phát hành phiên bản mới của một trong các gói bạn có thể gắn thẻ đó với số phiên bản, theo cách đó bạn có thể kéo phiên bản mã cụ thể đó vào tất cả các dự án khác


Nhưng quản lý phụ thuộc không phải là vấn đề của chúng tôi, điều này đã được giải quyết. Hiện tại chúng tôi thường xuyên thực hiện các thay đổi trên nhiều thư viện và cần giảm thiểu chi phí tạo các phiên bản mới trong khi duy trì các bản phát hành dự án ổn định. Câu trả lời của bạn dường như không cung cấp giải pháp cho vấn đề này.
sbi

@sbi Điều này sẽ quản lý chi phí tạo các phiên bản mới và duy trì các bản phát hành dự án ổn định. Vì bạn có thể ra lệnh rằng dự án x dựa vào dự án y phiên bản 2.1.1, bạn có thể tạo các phiên bản mới của dự án y sẽ không ảnh hưởng đến dự án x.
Patrick

Một lần nữa, tuyên bố phụ thuộc không phải là vấn đề của chúng tôi. Chúng ta có thể làm điều này rồi. Vấn đề là làm thế nào để quản lý các thay đổi cắt ngang qua một số dự án / thư viện một cách hiệu quả. Câu trả lời của bạn không giải thích điều này.
sbi

@sbi: vậy chính xác vấn đề của bạn là gì? Bạn thực hiện các thay đổi của mình, nâng cấp phiên bản, cập nhật các phụ thuộc khi cần và voila. Những gì bạn mô tả trong bài viết ban đầu của bạn là maven & co điển hình. đồ đạc. Mỗi phân phối dựa trên libs phiên bản được xác định rõ ràng. Làm thế nào nó có thể rõ ràng hơn?
dagnelies

@arnaud: Thời gian quay vòng của một quá trình như vậy đối với những thay đổi (hiện tại khá phổ biến) cắt qua ba hoặc nhiều lớp sẽ giết chết chúng ta. Tôi nghĩ rằng câu hỏi của tôi mô tả rằng.
sbi

0

Submodules

Bạn nên thử dùng mô đun con git , như được đề xuất trong một bình luận.

Khi dự án P1đề cập đến ba mô hình con L1, L2L3, nó thực sự lưu trữ một tham chiếu đến các cam kết cụ thể trong cả ba kho lưu trữ: đó là các phiên bản làm việc của mỗi thư viện cho dự án đó .

Vì vậy, nhiều dự án có thể hoạt động với nhiều mô hình con: P1có thể tham khảo phiên bản cũ của thư viện L1trong khi dự án P2sử dụng phiên bản mới.

Điều gì xảy ra khi bạn cung cấp một phiên bản mới của L3?

  • thực hiện giao diện mới trong L3
  • cam kết, kiểm tra , thực hiện yêu cầu kéo, xem xét, hợp nhất, ... (bạn không thể tránh điều này)
  • đảm bảo L2làm việc với L3, cam kết, ...
  • đảm bảo L1hoạt động với mới L2, ...
  • đảm bảo P1hoạt động với các phiên bản mới của tất cả các thư viện:
    • bên trong P1bản sao làm việc tại địa phương của L1, L2L3, tìm nạp các thay đổi bạn quan tâm.
    • cam kết thay đổi, git add L1 L2 L3để cam kết tham chiếu mới cho các mô-đun
    • kéo yêu cầu P1, kiểm tra, xem xét, kéo yêu cầu, hợp nhất ...

Phương pháp luận

Đây là một quá trình tẻ nhạt, dễ bị lỗi và rất dài để thực hiện tính năng này, nó đòi hỏi phải đánh giá độc lập (khiến việc đánh giá khó khăn hơn nhiều), không mở rộng quy mô và có khả năng khiến chúng tôi không hoạt động vì chúng tôi bị sa lầy trong quá trình chúng tôi không bao giờ hoàn thành công việc.

Có, nó yêu cầu đánh giá độc lập, bởi vì bạn thay đổi:

  • thư viện
  • thư viện phụ thuộc vào nó
  • các dự án phụ thuộc vào nhiều thư viện

Bạn sẽ bị loại khỏi kinh doanh bởi vì bạn cung cấp crap? (Có lẽ không, thực sự). Nếu có, thì bạn cần thực hiện các bài kiểm tra và xem xét các thay đổi.

Với các công cụ git thích hợp (thậm chí gitk), bạn có thể dễ dàng xem phiên bản nào của thư viện mà mỗi dự án sử dụng và bạn có thể cập nhật chúng một cách độc lập theo nhu cầu của mình. Các mô hình con là hoàn hảo cho tình huống của bạn và sẽ không làm chậm quá trình của bạn.

Có thể bạn có thể tìm cách tự động hóa một phần của quá trình này, nhưng hầu hết các bước trên đều yêu cầu bộ não của con người. Cách hiệu quả nhất để cắt giảm thời gian sẽ là đảm bảo thư viện và dự án của bạn dễ dàng phát triển. Nếu cơ sở mã của bạn có thể xử lý các yêu cầu mới một cách duyên dáng, thì việc đánh giá mã sẽ đơn giản hơn và tốn ít thời gian của bạn.

(Chỉnh sửa) một điều khác có thể giúp bạn là nhóm các đánh giá mã liên quan. Bạn cam kết tất cả các thay đổi và đợi cho đến khi bạn tuyên truyền những thay đổi đó xuống tất cả các thư viện và dự án sử dụng chúng trước khi thực hiện các yêu cầu kéo (hoặc trước khi bạn xử lý chúng). Bạn cuối cùng thực hiện một đánh giá lớn hơn cho toàn bộ chuỗi phụ thuộc. Có lẽ điều này có thể giúp bạn tiết kiệm thời gian nếu mỗi thay đổi cục bộ là nhỏ.


Bạn mô tả cách giải quyết vấn đề phụ thuộc (mà như tôi đã nói trước đây, chúng tôi đã sắp xếp) và phủ nhận chính vấn đề chúng ta đang gặp phải. Tại sao bạn còn bận tâm? (FWIW, chúng tôi viết phần mềm điều khiển các nhà máy điện. Mã sạch, an toàn và được xem xét kỹ lưỡng là một tính năng chính.)
sbi

@sbi Subodules là gì nếu không phải là trường hợp đặc biệt của phân nhánh và gắn thẻ? Bạn nghĩ rằng các mô hình con là về quản lý phụ thuộc, bởi vì họ cũng theo dõi các phụ thuộc. Nhưng chắc chắn, xin vui lòng phát minh lại các mô hình con với các thẻ nếu bạn muốn, tôi không phiền. Tôi không hiểu vấn đề của bạn: nếu mã được đánh giá là một tính năng chính, bạn phải dành một chút thời gian để đánh giá. Bạn không bị sa lầy, bạn đi càng nhanh càng tốt với những ràng buộc đặt lên bạn.
coredump

Nhận xét là rất quan trọng đối với chúng tôi. Đó là một trong những lý do chúng tôi lo ngại về một vấn đề (và các đánh giá của nó) bị chia thành nhiều đánh giá cho một số thay đổi trong một số kho lưu trữ. Thêm vào đó, chúng tôi không muốn sa lầy vào việc quản lý chúng tôi đến chết vì một vấn đề là chúng tôi muốn dành thời gian viết và xem xét mã. Reodules: cho đến nay, điều duy nhất tôi đã nghe về họ là "đừng bận tâm, đây không phải là cách git". Chà, cho rằng các yêu cầu của chúng tôi dường như rất độc đáo, có lẽ chúng ta nên xem lại quyết định này ...
sbi

0

Vì vậy, những gì tôi hiểu là bạn cho P1 bạn muốn thay đổi giao diện L3 nhưng bạn muốn P2 và P3 khác phụ thuộc vào giao diện L3 để thay đổi ngay lập tức. Đây là một trường hợp điển hình của khả năng tương thích ngược. Có một bài viết hay về khả năng tương thích ngược bảo tồn này

Có một số cách bạn có thể giải quyết điều này:

  • Bạn phải tạo giao diện mới mỗi lần có thể mở rộng giao diện cũ.

HOẶC LÀ

  • Nếu bạn muốn nghỉ hưu giao diện cũ sau một thời gian, bạn có thể có một vài phiên bản giao diện và một khi tất cả các dự án phụ thuộc di chuyển, bạn loại bỏ các giao diện cũ hơn.

1
Không, khả năng tương thích ngược được đảm bảo thông qua các nhánh phát hành và không phải là vấn đề của chúng tôi. Vấn đề là chúng ta đang ngồi trên một cơ sở mã đang thay đổi nhanh chóng, hiện tại muốn tách ra thành các thư viện, mặc dù thực tế là các giao diện vẫn đang trong giai đoạn chúng thay đổi thường xuyên. Tôi biết cách quản lý những con thú như vậy trong SVN, nhưng không biết làm thế nào trong git mà không bị chìm trong chính quyền.
sbi

0

Nếu tôi hiểu đúng vấn đề của bạn:

  • bạn có 4 mô-đun liên quan đến nhau, từ P1 và L1 đến L3
  • bạn cần thay đổi thành P1, điều này sẽ ảnh hưởng đến L1 đến L3
  • nó được coi là một quá trình thất bại nếu bạn phải thay đổi cả 4 cùng nhau
  • nó được coi là một quá trình thất bại nếu bạn phải thay đổi tất cả từng cái một.
  • nó được tính là một quá trình thất bại nếu bạn phải xác định trước các khối trong đó phải thay đổi.

Vì vậy, mục tiêu là bạn có thể thực hiện P1 và L1 trong một lần, và sau đó một tháng sau đó thực hiện L2 và L3 trong một lần khác.

Trong thế giới Java, điều này là tầm thường và có lẽ là cách làm việc mặc định:

  • tất cả mọi thứ đi vào một kho lưu trữ mà không sử dụng phân nhánh
  • các mô-đun được biên dịch + liên kết với nhau bởi maven dựa trên số phiên bản, không phải thực tế là tất cả đều nằm trong cùng một cây thư mục.

Vì vậy, bạn có thể có mã trên đĩa cục bộ cho L3 sẽ không biên dịch nếu nó được biên dịch dựa trên bản sao của P1 trong thư mục khác trên đĩa của bạn; may mắn là nó không làm như vậy. Java có thể thực hiện điều này một cách đơn giản vì biên dịch / liên kết các câu chuyện dựa trên các tệp jar được biên dịch, không phải mã nguồn.

Tôi không biết về một giải pháp được sử dụng rộng rãi từ trước cho vấn đề này cho thế giới C / C ++ và tôi tưởng tượng bạn khó muốn chuyển đổi ngôn ngữ. Nhưng một cái gì đó có thể dễ dàng bị hack cùng với tạo các tệp đã làm điều tương tự:

  • thư viện đã cài đặt + tiêu đề vào thư mục đã biết với số phiên bản được nhúng
  • thay đổi đường dẫn trình biên dịch trên mỗi mô-đun sang thư mục cho các số phiên bản phù hợp

Bạn thậm chí có thể sử dụng hỗ trợ C / C ++ trong maven , mặc dù hầu hết các nhà phát triển C sẽ nhìn bạn một cách kỳ lạ nếu bạn đã ...


"Nó được tính là một quá trình thất bại nếu bạn phải thay đổi cả 4 cùng nhau" . Thật ra thì không. Trên thực tế, đây chính xác là những gì chúng tôi đã sử dụng SVN.
sbi

Trong trường hợp đó thì tôi đoán không có vấn đề gì với việc đơn giản là đưa tất cả các dự án vào kho lưu trữ.
soru

Chúng tôi hiện đang đánh giá việc đưa các thư viện vào chỉ hai kho lưu trữ. Đó vẫn là nhiều hơn một, nhưng ít hơn nhiều so với "một cho mỗi dự án" và các thư viện rất có thể được chia thành hai nhóm. Cảm ơn vì đầu vào của bạn!
sbi

PS: "Tôi tưởng tượng bạn hầu như không muốn chuyển đổi ngôn ngữ." Đây là công cụ nhúng. :)
sbi

-1

Có một giải pháp đơn giản: cắt các nhánh phát hành trên toàn bộ kho lưu trữ, hợp nhất tất cả các bản sửa lỗi cho tất cả các bản phát hành được vận chuyển tích cực (rất dễ dàng trong trường hợp rõ ràng trong git).

Tất cả các lựa chọn thay thế sẽ tạo ra một mớ hỗn độn khủng khiếp theo thời gian và với sự phát triển của dự án.


Bạn có thể vui lòng giải thích? Tôi không chắc chắn những gì bạn đang đề nghị.
sbi

Trong trường hợp rõ ràng, bạn xác định một điểm nhánh là nhánh cơ sở và dấu thời gian. Bạn có phân cấp sau: nhánh cơ sở -> nhánh phát hành-phát triển -> nhánh phát triển riêng. Tất cả sự phát triển được thực hiện trên các nhánh riêng và sau đó được hợp nhất xuống hệ thống phân cấp. Chi nhánh phát hành khách hàng bị phá vỡ phát hành-chi nhánh phát triển. Tôi không quen thuộc với git nhưng có vẻ như điều gần nhất để xóa trường hợp trong số các hệ thống kiểm soát nguồn miễn phí.
zzz777

Đọc kỹ câu hỏi của tôi sẽ cho bạn thấy rằng chúng tôi có vấn đề với chi phí liên quan đến việc truyền bá các thay đổi giữa các kho lưu trữ. Điều này không có gì để làm với câu trả lời của bạn.
sbi

@sbi Tôi xin lỗi tôi đã hiểu nhầm câu hỏi của bạn. Và tôi sợ bạn sẽ phải đối mặt với một mớ hỗn độn khủng khiếp sớm hay muộn.
zzz777
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.