1 điểm bởi GN⁺ 2 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Lệnh git history mang tính thử nghiệm, được giới thiệu trong Git 2.54 và 2.55, tự động hóa việc sửa các commit trong quá khứ và tái cấu trúc các nhánh dẫn xuất mà vẫn giữ nguyên quy trình làm việc Git hiện có
  • fixup gộp các thay đổi đã được stage vào một commit trong quá khứ, rồi tự động rebase mọi nhánh cục bộ được tách ra từ commit đó theo mã băm commit mới
  • reword thay đổi thông điệp của commit trong quá khứ, còn split tách thay đổi của một commit thành hai commit theo từng hunk, rồi dựng lại các commit và nhánh theo sau
  • Ba lệnh con xử lý theo kiểu nguyên tử, từ chối các thao tác có thể gây xung đột để tránh làm cây làm việc rơi vào trạng thái hỏng dở dang, nhưng không hoạt động với lịch sử có merge commit
  • Dù không cung cấp khả năng xử lý xung đột như thực thể hạng nhất, hoàn tác dựa trên operation log, hay mô hình hóa working copy thành commit như jj, nó vẫn mang đến nhiều tính năng chỉnh sửa lịch sử kiểu jj ngay trong bản phân phối Git core mà không cần cài thêm gì

Vì sao việc chỉnh sửa lịch sử Git hiện nay lại bất tiện

  • Khi xử lý nhiều thay đổi song song, bạn phải liên tục qua lại giữa các nhánh và commit, và trong quá trình rebase -i cây làm việc có thể rơi vào trạng thái chưa hoàn chỉnh
  • jj, một lựa chọn thay thế cho Git, thường được nhắc đến như lời giải cho vấn đề này, nhưng dù đã thử vài ngày mỗi 3 tháng trong suốt một năm rưỡi thì cuối cùng vẫn quay lại quy trình làm việc Git cũ
  • git history là một lệnh thử nghiệm có trong bản phân phối Git core, nên có thể dùng thử mà không cần cài thêm công cụ riêng
    • Git 2.54 đã giới thiệu rewordsplit vào tháng 4
    • Git 2.55 đã bổ sung fixup vào tháng 6
  • Các lệnh con gồm ba loại: fixup, reword, split

fixup: sửa đồng thời commit cũ và các nhánh dẫn xuất

  • git history fixup sửa phần sai trong một commit quá khứ và tự động rebase các nhánh được tách ra từ commit đó
  • Chỉ cần stage thay đổi bằng git add như quy trình sửa thông thường rồi chạy git history fixup <commit>, các thay đổi đã stage sẽ được gộp vào commit mục tiêu
  • Nó giống như kết hợp git commit --fixup với autosquash rebase, nhưng khác ở chỗ còn cập nhật cả các nhánh khác chứa commit mục tiêu
  • Trong khi git rebase --update-refs chỉ di chuyển các tham chiếu nằm trong phạm vi rebase hiện tại, git history sẽ tìm và viết lại mọi nhánh cục bộ được tách ra từ commit mục tiêu
    • Nếu cần, có thể giới hạn phạm vi cập nhật chỉ còn nhánh hiện tại
    • Nó không hoạt động với lịch sử có merge commit, nên sẽ khó áp dụng với một số cách dùng Git
  • Khi gộp phần sửa vào B, một B* với mã băm mới sẽ được tạo ra, và C cùng D phía trên nó cũng lần lượt được tạo lại thành C*, D*
    • Đầu nhánh của feat-1feat-2 cũng sẽ di chuyển theo các commit mới

Xử lý nguyên tử và giới hạn xung đột

  • Cả ba lệnh con đều được xử lý theo kiểu nguyên tử, không để cây làm việc ở trạng thái hỏng dở dang
  • Tính nguyên tử được đảm bảo bằng cách từ chối ngay những thao tác có khả năng gây xung đột
  • Điều này bị giới hạn về mặt chức năng so với jj, công cụ xử lý xung đột như thực thể hạng nhất, cho phép giữ nguyên trạng thái xung đột trong lúc rebase và giải quyết sau
  • Các giới hạn của git history được áp dụng có chủ đích để việc viết lại lịch sử không biến thành một thao tác phải bảo toàn trạng thái
    • Nếu Git sau này hỗ trợ xung đột hạng nhất, các giới hạn này có thể được gỡ bỏ

reword: đổi thông điệp commit mà không đụng đến cây làm việc

  • git history reword thay đổi thông điệp của một commit trong quá khứ và tự động dựng lại các commit cùng nhánh phía trên nó
  • Khi chạy git history reword <commit>, trình soạn thảo sẽ mở thông điệp hiện tại; sau khi sửa và lưu, chồng commit theo sau sẽ được dựng lại theo mã băm mới
  • Nó hữu ích khi thiết kế đã thay đổi trong quá trình phát triển lặp, và bạn muốn chỉnh lại thông điệp của commit cũ cho phù hợp với cấu trúc hiện tại
  • Ngoại trừ việc chỉ đổi thông điệp commit thay vì nội dung cây, nó hoạt động theo cách giống fixup
  • reword không đụng đến index hay cây làm việc mà chỉ thay đổi đồ thị commit
    • Có thể sửa cả commit trên nhánh chưa checkout mà không làm gián đoạn công việc hiện tại
    • Nếu đổi thông điệp của B, mã băm sẽ thay đổi, nên C theo sau sẽ được tạo lại thành C*feat-1 cũng sẽ trỏ sang commit mới

split: tách một commit thành hai

  • git history split cho phép chọn tương tác các thay đổi của một commit để chia thành hai commit
  • Khi chạy git history split <commit>, một lời nhắc chọn từng hunk trong phần thay đổi của commit mục tiêu sẽ xuất hiện
    • Các hunk được chọn để giữ lại sẽ tạo thành commit thứ nhất
    • Các hunk còn lại sẽ đi vào commit thứ hai
  • Nó giống git add -p, nhưng không cần quy trình phức tạp phải kết hợp thêm git rebase
  • Nếu tách B vốn chứa hai thay đổi không liên quan, B1B2 sẽ được tạo ra, còn C theo sau sẽ được dựng lại thành C* trên hai commit đó
  • Đây là lệnh có phạm vi sử dụng chuyên biệt nhất trong ba lệnh, nhưng rất hữu ích khi thật sự cần tách commit

Khác biệt tính năng so với jj

1 bình luận

 
Ý kiến trên Hacker News
  • Git từng khiến tôi thấy bất tiện, nhưng sau khi đọc ba chương đầu của sách Pro Git được phát hành miễn phí, mọi thứ đã khá hơn. Sách cung cấp một mô hình tinh thần rất tốt về cách Git hoạt động bên trong; dù tốt hay xấu, giao diện người dùng của Git phản ánh trực tiếp cấu trúc nội bộ, nên khi hiểu được điều đó thì mọi thứ trở nên ăn khớp

    • Tôi không đồng ý rằng giao diện người dùng phản ánh trực tiếp cấu trúc nội bộ. Chẳng hạn, git checkout khi chuyển nhánh thì không làm mất các thay đổi chưa đưa vào Git, nhưng khi khôi phục một phiên bản khác của tệp thì lại được thiết kế để hoàn tác thay đổi; vậy điều đó phản ánh trực tiếp cấu trúc nội bộ như thế nào?
      Ngoài ra, ngay cả khi biết mình muốn làm gì bên trong và biết lệnh cần dùng, sự thiếu nhất quán của CLI khiến việc tìm đúng cú pháp dòng lệnh trở nên khó khăn, và đó cũng là lý do Git khó dùng
    • Git không rối rắm như người ta vẫn nói; chỉ cần bỏ ra vài giờ để hiểu là được. Đây là công cụ thiết yếu của kỹ sư phần mềm, nên thật đáng tiếc nếu không học nó; với một người chuyên nghiệp thì chỉ biết git commit -am “Changes” là chưa đủ
    • Tôi cũng từng thấy các lệnh Git gây bối rối, nhưng sau khi xem một video YouTube trong đó Linus giải thích cấu trúc dữ liệu nội bộ của Git, tôi nhận ra nó rất đơn giản. Có thể dễ dàng đối chiếu cách các chức năng của công cụ thao tác lên cấu trúc dữ liệu, và ngược lại chính các trừu tượng cấp cao mà Git muốn cung cấp lại khiến tình hình rối hơn
    • Không phải vậy. Một nửa lệnh Git là cấp thấp, nửa còn lại là cấp cao, nên chúng không tương ứng 1:1 với cấu trúc nội bộ
    • Trải nghiệm người dùng của Mercurial rõ ràng là tốt nhất, và tôi có thể học mà không cần đọc tài liệu riêng. Ngay cả khi dùng Git, tôi vẫn dùng như Mercurial, rồi khi có vấn đề thì xóa kho lưu trữ cục bộ; tôi đã xoay xở như vậy suốt sự nghiệp
  • git rebase --abort, và cũng có thể tạo tag trước khi rebase rồi quay lại bằng git reset --hard $set_tag. Trạng thái trước đó không biến mất, nên không có lý do gì phải sợ rebase tương tác

    • Tôi tạo rất nhiều nhánh tên temp hoặc before-rebase đúng cho mục đích đó. Dùng như tag, nhưng nhánh dễ di chuyển hơn, nên trước một lần rebase -i lớn tôi thường chạy git branch before-rebase/some-feature
      Thực ra rất hiếm khi tôi cần git reset before-rebase, nhưng tôi thường dùng git log -p before-rebase để so sánh với nhánh sau rebase và kiểm tra những thay đổi ngoài ý muốn trong quá trình giải quyết xung đột
    • Thậm chí không cần tạo tag an toàn riêng; reflog tự động làm việc đó
    • Bạn có dùng git reflog không?
    • Không thể đơn giản hơn jj undo được
    • Đúng vậy. Sau khi gõ git rebase, khoảng một nửa số lần tôi gõ ngay --abort
  • Tôi không tự xem mình là coder hay lập trình viên, nhưng việc học Git giống như đã trao cho cách tư duy của tôi siêu năng lực sắp xếp. Tôi dùng nó cho vô số mục đích như website, dự án thiết kế, điện tử, sáng tác nhạc, kho tri thức cá nhân, script quản trị từ xa, quản lý cấu hình, đoạn mã, v.v.; dù không hoàn hảo, tôi vẫn khuyên mọi người làm việc cùng mình nên học nó

    • Khả năng sắp xếp của tôi cũng cải thiện đáng kể, nhưng điều đó gần với tác dụng của hệ thống quản lý phiên bản nói chung hơn là của riêng Git. Hệ thống quản lý phiên bản giúp sắp xếp và bảo tồn những gì đã tạo ra, cho phép lấy ra bao nhiêu bản sao tùy muốn bất cứ lúc nào, nhờ đó có thể làm việc mà không phải lo lắng
      Trong cuốn 『Getting Things Done』 của David Allen có nói rằng cần một hệ thống đáng tin cậy để thu thập và sắp xếp suy nghĩ. Tôi dùng OmniFocus cho công việc, dự án và danh sách, nhưng với việc sắp xếp và bảo tồn phần mềm cùng một số dữ liệu tạo ra trên máy tính, tôi tin dùng Git
    • Bạn dùng Git cho sáng tác nhạc như thế nào?
  • Về mặt lý thuyết tôi rất thích các lệnh này và cũng luôn dùng lệnh tương đương trong jj, nhưng tôi không tìm ra cách để git history ký các commit đã chỉnh sửa. Tôi đã xem hướng dẫn sử dụng và mã nguồn Git, nhưng nếu sửa một commit GPG đã ký sẵn bằng git history reword HEAD~ thì toàn bộ chữ ký đều biến mất
    Vì muốn giữ chữ ký commit, cuối cùng tôi quay lại dùng git rebase -i truyền thống

    • Đây có vẻ là một thiếu sót lớn và dường như cũng bỏ qua quy trình xử lý commit tiêu chuẩn. Ngay cả trong hướng dẫn sử dụng cũng nói git historytính năng thử nghiệm, nên đáng để báo lỗi
  • Ngay cả khi không có lệnh mới, trong hơn 10 năm qua tôi vẫn đã chỉnh sửa mạnh lịch sử commit Git cục bộ mà không gặp khó khăn lớn. Tôi tin chắc rằng lý do lớn khiến Git và rebase có cảm giác khó là vấn đề giao diện người dùng, khi người dùng phải tự nắm chính xác các tùy chọn dòng lệnh và cách chúng hoạt động
    Trên Windows, hộp thoại rebase của TortoiseGit vừa mạnh mẽ vừa dễ dùng. Chỉ cần nhấp chuột phải vào commit làm mốc trong log commit rồi chọn "Rebase onto this"; việc đổi thứ tự, tách, gộp, chỉnh sửa commit đều rõ ràng như ở https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm..., và nếu hủy giữa chừng thì sẽ quay lại trạng thái ban đầu
    Tuy vậy, vì là dạng shell extension nên phải nhấp quá nhiều; nếu là một ứng dụng độc lập có tab thì sẽ tốt hơn, nhưng có vẻ đây là nguyên tắc thiết kế kế thừa từ TortoiseSVN. Tôi cũng từng dùng thử Git Extensions một thời gian ngắn, nhưng không tìm được các chức năng hay dùng, hoặc vì đã quá quen với TortoiseGit nên không gắn bó được. Màn hình log có thể xem tại https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht...

    • Một trong những điều tôi tiếc sau khi chuyển từ Windows sang Linux là shell tích hợp, nơi mọi chương trình đều có thể thêm menu chuột phải cho file/thư mục, các overlay như biểu tượng trạng thái của TortoiseGit, và các đối tượng đặc biệt như My Computer. Tôi hiểu rằng nó đạt đỉnh ở XP, rồi từ Vista Microsoft đã đổi theo hướng khiến việc mở rộng trở nên khó hơn
      Tuy nhiên đến giờ tôi vẫn không hiểu vì sao một menu nào đó của VC++6 lại tạo ra một shell object trông như thư mục thay vì danh sách thông thường và hiển thị nó trong hộp thoại mở file. Đó là wizard để thêm thành phần vào project, và trong lúc hộp thoại mở, tôi thường thấy “thư mục” giả đó trong My Computer
    • Đúng là workflow hiện có hơi rườm rà hơn và lệnh mới tiện hơn. fixup vốn đã làm được bằng git commit --fixup hoặc trước đó là git rebase -i, còn reword cũng có thể làm bằng git rebase -i
      Có vẻ phản hồi rằng việc chỉnh sửa file để quyết định thao tác cần thực hiện cho từng commit là bất tiện đã tích lũy đủ nhiều, nên một lệnh cấp cao hơn bọc quanh chức năng hiện có đã được thêm vào
    • Trong các giao diện người dùng của Git, tôi chưa thấy công cụ nào tiệm cận Magit. Nó cung cấp đồng thời các chức năng sâu và một giao diện có trạng thái đã được đơn giản hóa
      Với công cụ dòng lệnh, khi chạy lệnh tiếp theo thì khó truy cập lại output trước đó. Dù muốn cherry-pick một commit đã thấy trong git log, nếu hash không còn trên terminal thì không thể nhập chỉ bằng trí nhớ; nhưng giao diện có trạng thái có thể duy trì song song nhiều tương tác và giữ lại thông tin trước đó
      Git có rất nhiều thông tin trạng thái cần nhớ, như branch hiện tại, branch trước đó, file đã sửa, commit chưa push, v.v. Nếu không quen với các công cụ xung quanh như hiển thị branch và tự động hoàn thành của shell plugin, nhiều tab/chia cửa sổ, sao chép giữa các tab, tìm kiếm bằng tự động hoàn thành, việc dùng Git có thể trở nên rất khó chịu
  • Tôi cho rằng điểm yếu thực chất của jj chỉ là tích hợp với Git. Nó chuẩn hóa các ngôn ngữ chuyên biệt theo miền cho tập hợp file, tập hợp revision và template, dùng nhất quán trong toàn bộ cấu hình và CLI, đồng thời không phơi bày trực tiếp cấu trúc nội bộ mà cung cấp các khái niệm mạnh mẽ qua giao diện
    Nhưng vì quá linh hoạt, khả năng tương tác với Git phụ thuộc rất nhiều vào quy ước và kỷ luật; gần như mọi trạng thái của jj đều có thể biểu diễn bằng Git nên rất dễ làm hỏng remote repository. Muốn khôi phục thì phải hiểu Git sâu
    Nếu là dự án phát triển độc lập mới, jj đáng để thử. Trong điều kiện thuận lợi, nó có cảm giác giống Git thời kỳ đầu khi so với Subversion; nếu trước hết quen với cách làm của chính jj, sau này bạn cũng sẽ chuẩn bị tốt hơn một chút cho phần tích hợp Git gập ghềnh. Ngữ cảnh liên quan có tại https://news.ycombinator.com/item?id=48903054

    • Về nguyên tắc, jj cũng có backend không phải Git, nhưng hiện chỉ backend Git được ghi rõ là phù hợp cho môi trường vận hành. jj không tích hợp Git hiện vẫn gần với khái niệm hơn là thực tế
      Tôi thích jj và mô hình tinh thần về quản lý phiên bản của nó nên sẽ tiếp tục dùng, nhưng đôi khi vẫn phải đi xuống Git để hoàn tất công việc, vì vậy nó có cảm giác giống một wrapper tiện lợi trên Git hơn là một hệ thống quản lý cấu hình mới. Nếu Git thêm các lệnh hỗ trợ workflow của jj, sẽ khó tìm ra lý do để cài riêng jj
  • Cách git history fixup tự động rebase mọi branch hậu duệ không hợp với workflow của tôi. Tôi giữ lại từng phiên bản của feature branch như myfeature.1, myfeature.2 để khi hành vi thay đổi ở phiên bản mới thì có thể tìm lại phiên bản cũ; git reflog không thể thay thế việc này
    Ví dụ, nếu vấn đề xuất hiện giữa myfeature.58myfeature.59, và mỗi feature có 15 commit, thì có thể dùng git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59 để xem khác biệt giữa hai phiên bản theo từng commit dù base commit khác nhau. Vì vậy tôi không muốn tự động rebase mọi branch chứa một commit cụ thể, và cách dùng tag cho các phiên bản cũ cũng không thật sự phù hợp

  • Git mới nhất đã triển khai ba thao tác git rebase --interactive thường dùng thành các lệnh riêng ít ma sát hơn, và có vẻ chỉ hoạt động khi không có xung đột

    • Tôi nghi ngờ liệu git history có thực sự hữu ích không. Tôi thích dùng rebase tương tác để di chuyển commit một cách trực quan, và không muốn lỡ chỉ định một hash commit mồ côi đã biến mất khỏi branch cục bộ hiện tại làm đối tượng cho git history
  • Không hiểu vì sao phải tốn nhiều công sức đến vậy để trau chuốt lịch sử Git cho hoàn hảo. Dù sao cũng không đọc lại từng commit riêng lẻ, nên chỉ cần squash tất cả trước khi merge là được

    • Phản đối mạnh. Khi review PR, ít nhất tôi đọc lịch sử commit để nắm luồng thay đổi; nếu đầy những commit như "oops""fix" thì tôi reject ngay
      Commit truyền tải câu chuyện của công việc, vì vậy cần cung cấp lịch sử commit dễ đọc để reviewer không phải vất vả hơn khi cố hiểu ý đồ
    • Tôi thường xuyên đọc từng commit riêng lẻ. Trong các repository được chăm chút kỹ, việc này rất hữu ích để truy vết bug hoặc regression và hiểu ý đồ thay đổi của tác giả
    • Khi thắc mắc vì sao code lại thành ra như vậy, xem ngữ cảnh của commit đã đưa đoạn code đó vào sẽ giúp ích rất nhiều. Nếu đó là một commit khổng lồ chứa nhiều thay đổi khác nhau cùng lúc thì giá trị này giảm đi đáng kể
    • Suy nghĩ này cho thấy bạn không làm việc trong một team lớn. Khi quay lại sau kỳ nghỉ, việc đầu tiên tôi làm là đọc lịch sử Git để xem trong thời gian đó đã thay đổi những gì; nó cũng hữu ích khi CI bị hỏng, nên thực tế là hầu như ngày nào tôi cũng xem lịch sử dưới một hình thức nào đó
      Nếu codebase và tính năng rất nhỏ thì squash hết cũng được, nhưng tôi thích cách có thể xem luồng thay đổi thực tế và cherry-pick hoặc revert commit một cách có chọn lọc
    • Một lịch sử được sắp xếp tốt, mỗi commit tương ứng với một thay đổi logic, cực kỳ hữu ích khi bisect để tìm regression
  • Khi đang checkout D và đổi B thành B', tôi hiểu rằng git rebase --update-refs sẽ để nguyên E nằm ngoài phạm vi rebase, chỉ tạo B' → C' → D' và giữ lại B → E hiện có
    Còn git history fixup có cập nhật cả E thành E' không? Nếu vậy, tôi tò mò liệu với git rebase đã quá quen tay suốt hàng chục năm có cách nào để cập nhật tất cả các branch hậu duệ hay không

    • Hash của commit cũng phản ánh hash của commit cha, nên không thể giữ nguyên E. Tôi chưa trực tiếp dùng --update-refs, nhưng nếu đúng như tài liệu thì kết quả sẽ là đồ thị thứ ba có cả E', vì vậy hành vi mong muốn vốn đã có thể thực hiện bằng git rebase
    • Có thể tham khảo https://blog.hot-coffee.dev/en/blog/git_update_refs/