1 điểm bởi GN⁺ 2 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Git đã thành công với vai trò kho mã nguồn phân tán, nhưng cách xử lý quy trình làm việc phân tán gần như là một giải pháp được vá thêm về sau nên dần bộc lộ giới hạn
  • Commit và branch của Git không thể tự biểu đạt commit kế tiếp, lịch sử amend, lịch sử rebase, hay trạng thái đã bị loại bỏ
  • Trong Stacked PR, cần tìm các PR kế tiếp và rebase mà vẫn giữ nguyên stack, nhưng Git khó nắm bắt mối quan hệ này một cách ổn định
  • Git đặt các trạng thái có thể thay đổi như staging, unstaged, file system và HEAD ra ngoài commit·branch, khiến việc học và sử dụng trở nên phức tạp hơn
  • Trong luồng phát triển bất đồng bộ, nơi nhiều PR cần được dùng cùng nhau trước khi merge, mô hình lịch sử bất biến chỉ nhìn về quá khứ của Git tạo ra các vấn đề lặp đi lặp lại

Hai vai trò của Git

  • Git vừa là kho mã nguồn phân tán, vừa được dùng như công cụ quy trình làm việc phân tán
  • Với vai trò kho mã nguồn, Git rất thành công, nhưng cách nó xử lý quy trình làm việc phân tán phần lớn gần như là các giải pháp được bổ sung về sau
  • Phát triển bất đồng bộ, theo cách diễn đạt của East River Source Control, gần như là điều kiện mặc định; điều này không chỉ xảy ra khi cộng tác khác múi giờ mà cả khi bạn làm việc với chính mình ở những thời điểm khác nhau
  • jj là công cụ làm lộ rõ hơn các giới hạn của Git, và những người cảm thấy Git là đủ thường có xu hướng không thử jj một cách nghiêm túc

Những mối quan hệ mà mô hình cơ bản của Git bỏ lỡ

  • Trọng tâm trong cách tư duy về Git là commit và branch
    • Commit là đối tượng bất biến chứa mã nguồn và lịch sử
    • Branch là con trỏ có thể thay đổi kèm log
  • Các sơ đồ Git điển hình vẽ commit như C1, C2, C3, khiến thứ tự và quan hệ có vẻ rất rõ ràng, nhưng trong kho thực tế tên commit gần với hash hoặc message hơn, nên quan hệ thứ tự đó không thực sự tồn tại trong hệ thống
  • Ký hiệu như C2C2’ sau rebase chỉ là cách giải thích dễ hiểu cho con người; Git không biết hai commit đó tương ứng với nhau
  • Muốn tìm commit kế tiếp của một commit cụ thể, phải quét mọi branch để tìm các commit trên đường dẫn nối tiếp từ commit đó, nên đây không phải việc đơn giản

Git không có “C”

  • Bản thân Git commit không thể tự biết các thông tin sau
    • Commit kế tiếp

      • Lịch sử chỉnh sửa nối từ commit cũ sang commit mới sau amend
    • Lịch sử rebase

      • Liệu commit đó đã bị loại bỏ hay chưa
      • Branch cũng có giới hạn
      • Branch có khái niệm lịch sử, nhưng khó tin rằng nó tương ứng 1:1 với thay đổi mã nguồn
      • Các branch không có quan hệ với nhau; ví dụ, không thể tìm wp/bugfix một cách ổn định từ trunk
      • Vì không có tham chiếu tiến từ trunk sang wp/bugfix, đây cũng không phải là quan hệ có thể truy cập được
      • Sơ đồ Git khiến con người cảm thấy có thứ tự và quan hệ tương ứng, nhưng có thể phóng đại năng lực mà công cụ thực sự cung cấp

Vì sao Stacked PR khó

  • Khi cộng tác với người ở múi giờ khác và không muốn merge trước khi review, bạn phải pipeline công việc như CPU
  • Thay vì tạo một PR rồi đợi review xong, bạn tạo PR thứ hai trên PR đầu tiên, rồi tiếp tục chồng các PR tiếp theo lên trên để nhiều PR tuần tự được đưa vào review cùng lúc; đó là Stacked PR
  • Git khiến việc xử lý cấu trúc Stacked PR trở nên khó ổn định
    • Bạn có thể tạo PR kế tiếp như Refactor key entry code trên Fix key entry race, rồi sau khi fetch và cập nhật trunk, bạn phải rebase mà vẫn giữ nguyên stack
    • Vì Git không biết commit kế tiếp, nên khó nhìn ra Refactor key entry code từ Fix key entry race
    • Commit cũng có thể đã bị loại bỏ, nên ngay cả khi thấy commit kế tiếp cũng khó biết đó có phải trạng thái mới nhất không
    • Branch được dùng như chính PR, nhưng trong luồng này rất dễ vô tình ghi đè
  • Các công cụ stacking như Graphite có thể làm việc này trên Git, nhưng không thể tăng cường chính commit hay branch của Git
    • Chúng phải tạo một kho metadata branch riêng và đồng bộ với Git
    • Nếu người dùng trực tiếp thao tác với chính Git, kho đó có thể lệch khỏi trạng thái Git

Trạng thái có thể thay đổi nằm ngoài commit

  • Nhiều vấn đề của Git bắt nguồn từ việc nó không trực tiếp mô hình hóa tính thay đổi được (mutability)
  • Trong workflow chỉnh sửa của Git, có các trạng thái riêng tồn tại ngoài commit và branch
    • Staging hoặc index là snapshot mã nguồn được tạo từ working copy, và commit mới được tạo từ đây
    • Unstaged là diff thứ hai biểu thị khác biệt giữa index và file system
    • File system chứa nội dung đã checkout, và các thay đổi staged cùng unstaged được chồng thêm vào đó
    • HEAD là vị trí nơi commit mới sẽ được tạo ra
  • stash hoạt động như một kho riêng để lưu và khôi phục các thay đổi staging và unstaged
  • Khi checkout sang commit hoặc branch khác, Git sẽ điều chỉnh file system theo vị trí mới đồng thời cố giữ lại các diff của staging hoặc unstaged
  • Quá trình này dùng lệnh khác, nhưng nếu chỉ nhìn quan hệ mũi tên thì có hình dạng giống rebase, tức là di chuyển staging lên một nền tảng mới

Vì sao khó mô hình hóa mọi thứ bằng commit

  • Staging và working copy cũng có tổ tiên rõ ràng và chứa mã nguồn, nên nếu chỉ nhìn trạng thái tĩnh thì có thể biểu diễn chúng như commit
  • Nhưng ID commit là hash của nội dung, nên nếu commit có thể thay đổi thì ID sẽ liên tục đổi
  • Muốn nhất quán chỉ ra staging và working copy “là gì”, ta phải coi chúng như branch thay vì commit, nhưng branch lại có các giới hạn đã nói ở trên
  • Sự phức tạp này dẫn đến các vấn đề thực tế
    • Việc học và dùng Git khó hơn, vì cùng một khái niệm tồn tại riêng ở hai phía
    • Toàn bộ trạng thái kho khác rất xa trạng thái được mang về bằng clone, khiến việc xuất ra trở nên gượng gạo
    • Các luồng bất đồng bộ, nơi tập thay đổi biến đổi theo thời gian, vận hành không tốt
    • Phía hệ thống có thể thay đổi không biểu đạt được merge, nên có lúc không thể biểu diễn workflow thực tế

Khi Git không thể biểu đạt workflow thực tế

  • Trong lúc đang phát triển trên một feature branch mới mà chưa commit, bạn có thể phát hiện một bug trên thiết bị gây cản trở việc phát triển
  • Nếu bug đó không chặn tính năng mới nhưng khiến quá trình phát triển trở nên phiền toái, bạn có thể stash công việc, chuyển sang branch mới, tạo test tái hiện lỗi và bản sửa, rồi mở PR
  • Sau đó khi quay lại feature branch mới, các lựa chọn sẽ bị giới hạn
    • Rebase new-feature lên trên bugfix dù thực tế không có phụ thuộc, rồi tiếp tục review
    • Trong quá trình phát triển thì dùng new-feature đã rebase lên bugfix, nhưng trước khi gửi branch đi thì hoàn tác rebase
  • Với Git, không thể biểu đạt trạng thái rằng “trong không gian chỉnh sửa cần đồng thời có toàn bộ mã của bugfix và phần mã new feature đã commit”
  • Yêu cầu kiểu này cũng xuất hiện với cùng cấu trúc trong các vấn đề khó hơn như kiểm thử tương thích với các PR chưa được merge
  • Dùng công cụ phù hợp như Jujutsu megamerges thì có thể duy trì nhiều PR song song mà vẫn dùng chúng cùng nhau trong không gian chỉnh sửa

Git không còn đủ nữa

  • Các công cụ quản lý phiên bản đầu những năm 2000 khó dùng, khó quản trị và chất lượng rất thất thường; Subversion cũng bị xem là đau khổ để sử dụng
  • Khi đó, nhu cầu có bản sao toàn bộ kho trên máy cục bộ không phải điều phổ biến, và nhu cầu tạo local branch cũng chưa phổ quát
  • Nhiều người khó chịu với file locking, nhưng cũng có người cho rằng file locking là cần thiết và đã hỏi liệu Git có thể khóa từng file hoặc thư mục riêng lẻ hay không
  • Với những người từng trực tiếp trải nghiệm workflow phân tán như trong thế giới mã nguồn mở, DVCS được đón nhận như một miếng băng cứu khỏi những vết thương cũ
  • Ngày nay, với những người dùng workflow phân tán theo nghĩa thực sự, mô hình lịch sử bất biến chỉ nhìn về phía sau của Git trở thành nguồn gốc của các vấn đề lặp đi lặp lại
  • Các công ty như Meta đã dùng hệ thống nội bộ vượt xa Git trong gần 10 năm
  • Xu hướng kiểu “giờ thì Claude thao tác Git thay bạn” không khiến các lựa chọn thay thế này trở nên vô nghĩa
  • Có vẻ khi dùng LLM, ngay cả trong một máy đơn, các kỹ sư cũng đang làm nhiều phát triển bất đồng bộ hơn trước

1 bình luận

 
Ý kiến trên Lobste.rs
  • Sẽ hay hơn nếu bài viết cho thấy jj giải quyết những vấn đề được nêu ra như thế nào
    Với người dùng jj thì có lẽ quá rõ, nhưng có khả năng họ không phải là đối tượng chính mà bài viết hướng tới

  • Cá nhân tôi chưa từng cần đến những tính năng mà bài viết đưa ra làm bằng chứng rằng Git không ổn
    Tự hỏi không biết có phải chỉ mình tôi như vậy không

    • Bạn hoàn toàn không cô đơn đâu
      Một trong những điểm quan trọng của công cụ là nó là một phần của hệ thống động. Những gì công cụ cho phép ảnh hưởng đến “những việc tôi tin là mình có thể làm”, và niềm tin đó lại tác động ngược tới cách ta nhìn nhận công cụ cũng như hướng tiến hóa của chính công cụ
      Khi một công cụ làm lung lay hiện trạng, niềm tin và kỳ vọng về những gì có thể làm được cũng thay đổi theo
  • Có vẻ thú vị, nhưng nhìn sơ đồ thì hoa cả mắt

  • Về nhận xét rằng tình hình hiện nay không nghiêm trọng như đầu những năm 2000 và giới hạn của các hệ thống quản lý phiên bản trước Git là khá rõ ràng, thì Darcs ra đời trước Git và từng có những điểm sửa tận gốc một số vấn đề của quản lý phiên bản dựa trên snapshot
    Lúc đầu nó thua vì hiệu năng kém, nhưng sau đó hiệu năng đã được cải thiện mà mọi người không quay lại kiểm chứng. Cũng có những hệ thống quản lý phiên bản khác đang làm những điều thú vị, nên tôi không muốn thấy chuyện bị mô tả như thể “nếu không phải Git thì chỉ còn Jujutsu” là lựa chọn duy nhất. Tôi thấy kiểu lập luận đó quá thường xuyên

    • Hơi buồn cười khi tác giả nói mô hình dữ liệu của Git là rất tốt, rồi lại lướt qua chuyện branch của Git chỉ là con trỏ trỏ tới đầu branch nên workflow không tốt
      Đó cũng là vấn đề của mô hình dữ liệu mà
  • jj xử lý chuyện này như thế nào? https://www.billjings.com/posts/title/git-is-not-fine/RealityEx23.png

    • Dùng jj new A B thì commit working copy có thể có nhiều parent, nên nó hoạt động như một merge commit
      Vì vậy working copy sẽ nhận thay đổi từ cả hai parent, và bạn có thể tiếp tục làm việc trên phần merge đó hoặc amend vào một trong hai commit
  • Tôi vẫn thích Git hơn vào lúc này, và tác giả có vẻ mang thiên kiến

    • Tôi tò mò không biết lý do thích Git là vì bạn chưa gặp những tình huống mà tác giả nói là khó trong Git và đã quen với Git rồi, hay là ngay cả khi gặp những tình huống đó bạn vẫn thấy Git cho workflow tốt hơn Jujutsu
    • Chỉ cần nhớ rằng phải chạy jj new thì bạn có thể dùng lẫn gitjj
      Git luôn trỏ tới commit cha, còn jj commit hiện tại sẽ được nhìn như các thay đổi chưa commit của working tree
      Tôi đã học jj theo cách đó. Tôi dùng jj cho những việc jj làm tốt như xử lý rebase hay di chuyển tree, còn với các công việc thường ngày mà tôi chưa biết lệnh tương ứng trong jj hoặc những việc như git blame khiến lệnh Git hiện lên đầu tiên trong đầu, tôi vẫn tiếp tục dùng lệnh git
      Thực ra trước khi dùng hằng ngày, tôi cũng không thật sự cảm nhận được vì sao jj tốt hơn; khi chỉ đọc về nó, tôi từng nghĩ “tính năng này có thật sự cần không” hoặc “Git cũng làm được rồi mà”
      Tất nhiên jj cũng có nhược điểm. Nếu không có .gitignore mới nhất thì file nhị phân có thể vô tình lọt vào commit. May là jj sẽ cảnh báo khi bạn thêm file rất lớn, nhưng file nhỏ thì vẫn có thể lọt qua
      Nếu trong lúc debug có file trace hoặc log trong thư mục hiện tại thì chúng cũng có thể bị cuốn vào, nên sau khi thao tác nhiều với tree, tốt nhất là xem lại toàn bộ diffstat
      Đặc biệt, nếu bạn đang bisect bằng jj mà lại kiểm thử một commit trước commit đã cập nhật .gitignore thì điều đó có thể thành vấn đề. Có lẽ bisect nên có chế độ chỉ đọc