Lệnh `git history`
(lalitm.com)- Lệnh
git historymang 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ó fixupgộ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ớirewordthay đổi thông điệp của commit trong quá khứ, cònsplittá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ểujjngay 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 -icâ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 historylà 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- 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 fixupsử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 addnhư quy trình sửa thông thường rồi chạygit 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 --fixupvớ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-refschỉ di chuyển các tham chiếu nằm trong phạm vi rebase hiện tại,git historysẽ 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ộtB*với mã băm mới sẽ được tạo ra, vàCcùngDphía trên nó cũng lần lượt được tạo lại thànhC*,D*- Đầu nhánh của
feat-1vàfeat-2cũng sẽ di chuyển theo các commit mới
- Đầu nhánh của
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 rewordthay đổ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 rewordkhô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ênCtheo sau sẽ được tạo lại thànhC*vàfeat-1cũng sẽ trỏ sang commit mới
split: tách một commit thành hai
git history splitcho 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êmgit rebase - Nếu tách
Bvốn chứa hai thay đổi không liên quan,B1vàB2sẽ được tạo ra, cònCtheo sau sẽ được dựng lại thànhC*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
git historykhông thay thế toàn bộ mô hình làm việc màjjcung cấp- Ngược lại,
git historyhỗ trợ sửa commit cũ, đổi thông điệp, tách commit và cập nhật các nhánh dẫn xuất ngay trong Git hiện có, giúp đưa một phần ưu điểm củajjvào quy trình làm việc hiện tại - Vẫn còn khả năng nới lỏng các giới hạn trong tương lai, nên những bản phát hành Git sau này có thể tiếp tục cải thiện thêm
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
git checkoutkhi 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 commit -am “Changes”là chưa đủCó
git rebase --abort, và cũng có thể tạo tag trước khi rebase rồi quay lại bằnggit 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áctemphoặcbefore-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ầnrebase -ilớn tôi thường chạygit branch before-rebase/some-featureThực ra rất hiếm khi tôi cần
git reset before-rebase, nhưng tôi thường dùnggit 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 độtgit reflogkhông?jj undođượcgit rebase, khoảng một nửa số lần tôi gõ ngay--abortTô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ó
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
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 historyký 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ằnggit history reword HEAD~thì toàn bộ chữ ký đều biến mấtVì muốn giữ chữ ký commit, cuối cùng tôi quay lại dùng
git rebase -itruyền thốnggit historylà tính năng thử nghiệm, nên đáng để báo lỗiNgay 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 đầuTuy 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...
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ơnTuy 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 Computerfixupvốn đã làm được bằnggit commit --fixuphoặc trước đó làgit rebase -i, cònrewordcũng có thể làm bằnggit rebase -iCó 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
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
jjchỉ 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ệnNhư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âuNế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ínhjj, 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=48903054jjcũ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.jjkhô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
jjvà 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ủajj, sẽ khó tìm ra lý do để cài riêngjjCách
git history fixuptự độ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 reflogkhông thể thay thế việc nàyVí dụ, nếu vấn đề xuất hiện giữa
myfeature.58vàmyfeature.59, và mỗi feature có 15 commit, thì có thể dùnggit 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ợpGit mới nhất đã triển khai ba thao tác
git rebase --interactivethườ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 độtgit historycó 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 chogit historyKhô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
"oops"và"fix"thì tôi reject ngayCommit 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 ý đồ
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
Khi đang checkout
Dvà đổiBthànhB', tôi hiểu rằnggit rebase --update-refssẽ để nguyênEnằm ngoài phạm vi rebase, chỉ tạoB' → C' → D'và giữ lạiB → Ehiện cóCòn
git history fixupcó cập nhật cảEthànhE'không? Nếu vậy, tôi tò mò liệu vớigit 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ôngE. 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ằnggit rebase