Làm cách nào tôi có thể dễ dàng sửa lỗi cam kết trước đây


116

Tôi chỉ đọc sửa đổi một tệp duy nhất trong một cam kết trước đây trong git nhưng không may là giải pháp được chấp nhận 'sắp xếp lại' các cam kết, đó không phải là điều tôi muốn. Vì vậy, đây là câu hỏi của tôi:

Thỉnh thoảng, tôi nhận thấy một lỗi trong mã của mình khi làm việc trên một tính năng (không liên quan). git blameSau đó nhanh chóng tiết lộ rằng lỗi đã được đưa vào một vài lần cam kết trước (tôi đã cam kết khá nhiều, vì vậy thường nó không phải là lần cam kết gần đây nhất đã giới thiệu lỗi). Tại thời điểm này, tôi thường làm điều này:

git stash                      # temporarily put my work aside
git rebase -i <bad_commit>~1   # rebase one step before the bad commit
                               # mark broken commit for editing
vim <affected_sources>         # fix the bug
git add <affected_sources>     # stage fixes
git commit -C <bad_commit>     # commit fixes using same log message as before
git rebase --continue          # base all later changes onto this

Tuy nhiên, điều này xảy ra thường xuyên khiến trình tự trên trở nên khó chịu. Đặc biệt là 'rebase tương tác' là nhàm chán. Có bất kỳ phím tắt nào cho chuỗi trên, cho phép tôi sửa đổi một cam kết tùy ý trong quá khứ với các thay đổi theo giai đoạn không? Tôi hoàn toàn biết rằng điều này làm thay đổi lịch sử, nhưng tôi thường xuyên mắc sai lầm nên tôi thực sự muốn có những thứ như

vim <affected_sources>             # fix bug
git add -p <affected_sources>      # Mark my 'fixup' hungs for staging
git fixup <bad_commit>             # amend the specified commit with staged changes,
                                   # rebase any successors of bad commit on rewritten 
                                   # commit.

Có thể là một tập lệnh thông minh có thể viết lại các cam kết bằng cách sử dụng các công cụ đường ống dẫn nước hoặc tương tự?


Bạn có nghĩa là gì khi "sắp xếp lại" các cam kết? Nếu bạn đang thay đổi lịch sử thì tất cả các cam kết vì các cam kết đã thay đổi phải khác, nhưng câu trả lời được chấp nhận cho câu hỏi được liên kết không sắp xếp lại các cam kết theo bất kỳ ý nghĩa nào.
CB Bailey

1
@Charles: Ý tôi là sắp xếp lại thứ tự như trong: nếu tôi nhận thấy HEAD ~ 5 là cam kết bị hỏng, câu trả lời được chấp nhận sau đây trong câu hỏi được liên kết sẽ làm cho HEAD (đầu nhánh) trở thành cam kết cố định. Tuy nhiên, tôi muốn HEAD ~ 5 là cam kết cố định - đó là những gì bạn nhận được khi sử dụng rebase tương tác và chỉnh sửa một cam kết duy nhất để sửa.
Frerich Raabe

Có, nhưng sau đó lệnh rebase sẽ kiểm tra lại tổng thể và căn cứ lại tất cả các cam kết tiếp theo vào cam kết cố định. Đây không phải là cách bạn đang lái xe rebase -isao?
CB Bailey

Trên thực tế, có một vấn đề tiềm ẩn với câu trả lời đó, tôi nghĩ nó nên như vậy rebase --onto tmp bad-commit master. Như đã viết, nó sẽ cố gắng áp dụng cam kết xấu vào trạng thái cam kết cố định.
CB Bailey

Đây là một công cụ khác để tự động hóa quy trình fixup / rebase: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Câu trả lời:


166

CẬP NHẬT CÂU TRẢ LỜI

Một thời gian trước, một --fixupđối số mới đã được thêm vào git commitmà có thể được sử dụng để xây dựng một cam kết với một thông báo nhật ký phù hợp git rebase --interactive --autosquash. Vì vậy, cách đơn giản nhất để sửa lỗi cam kết trong quá khứ là:

$ git add ...                           # Stage a fix
$ git commit --fixup=a0b1c2d3           # Perform the commit to fix broken a0b1c2d3
$ git rebase -i --autosquash a0b1c2d3~1 # Now merge fixup commit into broken commit

CÂU TRẢ LỜI GỐC

Đây là một tập lệnh Python nhỏ mà tôi đã viết cách đây ít lâu, thực hiện git fixuplogic này mà tôi hy vọng trong câu hỏi ban đầu của mình. Tập lệnh giả định rằng bạn đã tổ chức một số thay đổi và sau đó áp dụng những thay đổi đó cho cam kết đã cho.

LƯU Ý : Tập lệnh này dành riêng cho Windows; nó tìm kiếm git.exevà đặt GIT_EDITORbiến môi trường bằng cách sử dụng set. Điều chỉnh điều này nếu cần đối với các hệ điều hành khác.

Sử dụng tập lệnh này, tôi có thể triển khai chính xác quy trình làm việc 'sửa các nguồn bị hỏng, sửa lỗi vùng, chạy git fixup' mà tôi đã yêu cầu:

#!/usr/bin/env python
from subprocess import call
import sys

# Taken from http://stackoverflow.com/questions/377017/test-if-executable-exists-in python
def which(program):
    import os
    def is_exe(fpath):
        return os.path.exists(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None

if len(sys.argv) != 2:
    print "Usage: git fixup <commit>"
    sys.exit(1)

git = which("git.exe")
if not git:
    print "git-fixup: failed to locate git executable"
    sys.exit(2)

broken_commit = sys.argv[1]
if call([git, "rev-parse", "--verify", "--quiet", broken_commit]) != 0:
    print "git-fixup: %s is not a valid commit" % broken_commit
    sys.exit(3)

if call([git, "diff", "--staged", "--quiet"]) == 0:
    print "git-fixup: cannot fixup past commit; no fix staged."
    sys.exit(4)

if call([git, "diff", "--quiet"]) != 0:
    print "git-fixup: cannot fixup past commit; working directory must be clean."
    sys.exit(5)

call([git, "commit", "--fixup=" + broken_commit])
call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i", "--autosquash", broken_commit + "~1"], shell=True)

2
bạn có thể sử dụng git stashgit stash popxung quanh rebase của bạn không còn yêu cầu một thư mục làm việc sạch
Tobias KIENZLER

@TobiasKienzler: Về cách sử dụng git stashgit stash pop: bạn nói đúng, nhưng tiếc git stashlà trên Windows chậm hơn nhiều so với trên Linux hoặc OS / X. Vì thư mục làm việc của tôi thường sạch, tôi đã bỏ qua bước này để không làm chậm lệnh.
Frerich Raabe

Tôi có thể khẳng định rằng, đặc biệt là khi làm việc trên một mạng chia sẻ: - /
Tobias KIENZLER

1
Đẹp. Tôi đã vô tình làm vậy git rebase -i --fixupvà nó phục hồi từ cam kết cố định làm điểm bắt đầu, vì vậy đối số sha không cần thiết trong trường hợp của tôi.
fwielstra

1
Đối với những người sử dụng --autosquash thường, nó có thể hữu ích để thiết lập nó là hành vi mặc định: git config --global rebase.autosquash true
taktak004

31

Những gì tôi làm là:

git add ... # Thêm bản sửa lỗi.
git commit # Cam kết, nhưng không đúng chỗ.
git rebase -i HEAD ~ 5 # Kiểm tra 5 cam kết cuối cùng để giảm giá.

Trình chỉnh sửa của bạn sẽ mở ra với danh sách 5 cam kết cuối cùng, sẵn sàng được can thiệp. Thay đổi:

pick 08e833c Tốt đổi 1.
pick 9134ac9 Tốt thay đổi 2.
chọn 5adda55 Thay đổi không tốt!
chọn 400bce4 Tốt thay đổi 3.
pick 2bc82n1 Sửa lỗi thay đổi.

...đến:

pick 08e833c Tốt đổi 1.
pick 9134ac9 Tốt thay đổi 2.
chọn 5adda55 Thay đổi không tốt!
f 2bc82n1 Sửa lỗi thay đổi. # Di chuyển lên và thay đổi 'pick' thành 'f' cho 'fixup'.
chọn 400bce4 Tốt thay đổi 3.

Lưu & thoát khỏi trình chỉnh sửa của bạn và bản sửa lỗi sẽ được chuyển trở lại cam kết mà nó thuộc về.

Sau khi bạn làm điều đó một vài lần, bạn sẽ thực hiện nó trong vài giây trong giấc ngủ. Phục hồi tương tác là tính năng thực sự bán tôi trên git. Nó cực kỳ hữu ích cho việc này và hơn thế nữa ...


9
Rõ ràng là bạn có thể thay đổi HEAD ~ 5 thành HEAD ~ n để quay lại xa hơn. Bạn sẽ không muốn can thiệp vào bất kỳ lịch sử nào mà bạn đã đẩy ngược dòng, vì vậy tôi thường nhập 'git rebase -i origin / master' để đảm bảo rằng tôi chỉ thay đổi lịch sử chưa được đẩy.
Kris Jenkins

4
Điều này giống như những gì tôi đã luôn làm; FWIW, bạn có thể quan tâm đến công --autosquashtắc git rebase, tự động sắp xếp lại các bước trong trình chỉnh sửa cho bạn. Xem phản hồi của tôi cho một tập lệnh tận dụng điều này để triển khai một git fixuplệnh.
Frerich Raabe

Tôi không biết bạn có thể sắp xếp lại lệnh băm cam kết, tốt lắm!
Aaron Franke

Điều đó thật tuyệt! Chỉ để đảm bảo rằng tất cả các công việc rebase được thực hiện là nhánh tính năng riêng biệt. Và không để lộn xộn với nhánh chung như chủ.
Jay Modi

22

Đến bữa tiệc hơi muộn, nhưng đây là một giải pháp hiệu quả như tác giả đã tưởng tượng.

Thêm cái này vào .gitconfig của bạn:

[alias]
    fixup = "!sh -c '(git diff-files --quiet || (echo Unstaged changes, please commit or stash with --keep-index; exit 1)) && COMMIT=$(git rev-parse $1) && git commit --fixup=$COMMIT && git rebase -i --autosquash $COMMIT~1' -"

Ví dụ sử dụng:

git add -p
git fixup HEAD~5

Tuy nhiên, nếu bạn có các thay đổi chưa được phân giai đoạn, bạn phải lưu trữ chúng trước khi rebase.

git add -p
git stash --keep-index
git fixup HEAD~5
git stash pop

Bạn có thể sửa đổi bí danh để lưu trữ tự động, thay vì đưa ra cảnh báo. Tuy nhiên, nếu bản sửa lỗi không được áp dụng rõ ràng, bạn sẽ cần bật kho lưu trữ theo cách thủ công sau khi khắc phục các xung đột. Thực hiện cả lưu và bật theo cách thủ công có vẻ nhất quán hơn và ít gây nhầm lẫn hơn.


Điều này là khá hữu ích. Đối với tôi, usecase phổ biến nhất là sửa chữa các thay đổi đối với cam kết trước đó, vì vậy git fixup HEADtôi đã tạo một bí danh cho nó. Tôi cũng có thể sử dụng sửa đổi cho điều đó tôi cho là.
châu chấu

Cảm ơn bạn! Tôi cũng thường sử dụng nó trong lần cam kết cuối cùng, nhưng tôi có một bí danh khác để sửa đổi nhanh chóng. amend = commit --amend --reuse-message=HEADSau đó, bạn có thể chỉ cần nhập git amendhoặc git amend -avà bỏ qua trình chỉnh sửa cho thông báo cam kết.
dschlyter

3
Vấn đề với sửa đổi là tôi không nhớ cách đánh vần của nó. Tôi luôn phải suy nghĩ, đó là khen thưởng hay sửa đổi và điều đó là không tốt.
con châu chấu

12

Để sửa một cam kết:

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i HEAD~2

trong đó a0b1c2d3 là cam kết mà bạn muốn sửa chữa và trong đó 2 là số cam kết +1 được dán mà bạn muốn thay đổi.

Lưu ý: git rebase --autosquash mà không có -i không hoạt động nhưng với -i hoạt động, điều này thật kỳ lạ.


2016 và --autosquashkhông có -ivẫn không hoạt động.
Jonathan Cross

1
Như trang người đàn ông nói: Tùy chọn này chỉ hợp lệ khi tùy chọn - tương tác được sử dụng. Nhưng có một cách dễ dàng để bỏ qua các biên tập viên:EDITOR=true git rebase --autosquash -i
joeytwiddle

Bước thứ 2 không hoạt động với tôi, nói rằng: Vui lòng chỉ định nhánh nào bạn muốn căn cứ lại.
djangonaut 30/07/19

git rebase --autosquash -i HEAD ~ 2 (trong đó 2 là số cam kết +1 được dán mà bạn muốn thay đổi.
Sérgio

6

CẬP NHẬT: Hiện có thể tìm thấy phiên bản mới hơn của tập lệnh tại đây: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

Tôi đã tìm kiếm một cái gì đó tương tự. Tuy nhiên, tập lệnh Python này có vẻ quá phức tạp, do đó tôi đã đúc kết lại giải pháp của riêng mình:

Đầu tiên, bí danh git của tôi trông giống như vậy (mượn từ đây ):

[alias]
  fixup = !sh -c 'git commit --fixup=$1' -
  squash = !sh -c 'git commit --squash=$1' -
  ri = rebase --interactive --autosquash

Bây giờ hàm bash trở nên khá đơn giản:

function gf {
  if [ $# -eq 1 ]
  then
    if [[ "$1" == HEAD* ]]
    then
      git add -A; git fixup $1; git ri $1~2
    else
      git add -A; git fixup $1; git ri $1~1
    fi
  else
    echo "Usage: gf <commit-ref> "
  fi
}

Đoạn mã này đầu tiên sẽ xử lý tất cả các thay đổi hiện tại (bạn có thể loại bỏ phần này, nếu bạn muốn tự xử lý các tệp). Sau đó, tạo bản sửa lỗi (cũng có thể sử dụng bí đao, nếu đó là những gì bạn cần) cam kết. Sau đó, nó bắt đầu một rebase tương tác với --autosquashcờ trên cha của cam kết mà bạn đưa ra làm đối số. Thao tác đó sẽ mở trình chỉnh sửa văn bản đã định cấu hình của bạn, vì vậy bạn có thể xác minh rằng mọi thứ đều như bạn mong đợi và chỉ cần đóng trình chỉnh sửa sẽ hoàn tất quá trình.

Phần if [[ "$1" == HEAD* ]](mượn từ đây ) được sử dụng, vì nếu bạn sử dụng, ví dụ: HEAD ~ 2 làm tham chiếu cam kết của bạn (cam kết bạn muốn sửa các thay đổi hiện tại) thì HEAD sẽ bị thay thế sau khi cam kết sửa chữa đã được tạo. và bạn sẽ cần sử dụng HEAD ~ 3 để tham chiếu đến cùng một cam kết.


Thay thế thú vị. +1
VonC

4

Bạn có thể tránh giai đoạn tương tác bằng cách sử dụng trình chỉnh sửa "null":

$ EDITOR=true git rebase --autosquash -i ...

Điều này sẽ sử dụng /bin/truelàm trình chỉnh sửa, thay vì /usr/bin/vim. Nó luôn chấp nhận bất cứ điều gì git gợi ý mà không cần nhắc nhở.


Thật vậy, đây chính xác là những gì tôi đã làm trong câu trả lời kịch bản Python 'câu trả lời ban đầu' của tôi từ ngày 30 tháng 9 năm 2010 (lưu ý cách ở cuối tập lệnh, nó nói call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe

4

Điều thực sự khiến tôi bận tâm về quy trình sửa chữa là tôi phải tự tìm ra cam kết mà tôi muốn thực hiện mọi lúc. Tôi đã tạo một lệnh "git fixup" giúp thực hiện việc này.

Lệnh này tạo ra các cam kết sửa chữa, với điều kỳ diệu bổ sung là nó sử dụng git-deps để tự động tìm các cam kết có liên quan, vì vậy quy trình làm việc thường đi xuống:

# discover and fix typo in a previously committed change
git add -p # stage only typo fix
git fixup

# at some later point squash all the fixup commits that came up
git rebase --autosquash master

Điều này chỉ hoạt động nếu các thay đổi theo giai đoạn có thể được quy cho một cam kết cụ thể trên cây làm việc (giữa chính và HEAD). Tôi thấy đó là trường hợp rất thường xuyên đối với loại thay đổi nhỏ mà tôi sử dụng điều này, ví dụ như lỗi chính tả trong nhận xét hoặc tên của các phương thức mới được giới thiệu (hoặc đổi tên). Nếu không đúng như vậy, ít nhất nó sẽ hiển thị một danh sách các cam kết của ứng viên.

Tôi sử dụng điều này rất nhiều trong quy trình làm việc hàng ngày của mình, để nhanh chóng tích hợp các thay đổi nhỏ đối với các dòng đã thay đổi trước đó thành các cam kết trên nhánh làm việc của tôi. Kịch bản không đẹp như nó có thể, và nó được viết bằng zsh, nhưng nó đã làm công việc của tôi đủ tốt cho đến bây giờ tôi không bao giờ cảm thấy cần phải viết lại nó:

https://github.com/Valodim/git-fixup


2

Bạn có thể tạo bản sửa lỗi cho một tệp cụ thể bằng cách sử dụng bí danh này.

[alias]
...
# fixup for a file, using the commit where it was last modified
fixup-file = "!sh -c '\
        [ $(git diff          --numstat $1 | wc -l) -eq 1 ] && git add $1 && \
        [ $(git diff --cached --numstat $1 | wc -l) -eq 1 ] || (echo No changes staged. ; exit 1) && \
        COMMIT=$(git log -n 1 --pretty=format:"%H" $1) && \
            git commit --fixup=$COMMIT && \
            git rebase -i --autosquash $COMMIT~1' -"

Nếu bạn đã thực hiện một số thay đổi trong myfile.txtnhưng bạn không muốn đưa chúng vào một cam kết mới, hãy git fixup-file myfile.txttạo một fixup!cam kết nơi myfile.txtđược sửa đổi lần cuối và sau đó sẽ thực hiện rebase --autosquash.


Rất thông minh, tôi muốn nó git rebasekhông được gọi tự động.
hurikhan77

2

commit --fixuprebase --autosquashrất tuyệt, nhưng họ làm chưa đủ. Khi tôi có một chuỗi cam kết A-B-Cvà tôi viết thêm một số thay đổi trong cây làm việc của mình thuộc về một hoặc nhiều cam kết hiện có đó, tôi phải xem lịch sử theo cách thủ công, quyết định những thay đổi nào thuộc về cam kết nào, phân giai đoạn chúng và tạo fixup!cam kết. Nhưng git đã có đủ thông tin để có thể làm tất cả những điều đó cho tôi, vì vậy tôi đã viết một tập lệnh Perl để làm điều đó.

Đối với mỗi đoạn trong git diffscript sử dụng git blameđể tìm cam kết lần cuối chạm vào các dòng liên quan và gọi git commit --fixupđể viết các fixup!cam kết thích hợp , về cơ bản thực hiện cùng một việc mà tôi đã làm thủ công trước đây.

Nếu bạn thấy nó hữu ích, vui lòng cải thiện và lặp lại nó và có thể một ngày nào đó chúng ta sẽ nhận được một tính năng như vậy gitphù hợp. Tôi muốn thấy một công cụ có thể hiểu cách giải quyết xung đột hợp nhất khi nó được giới thiệu bởi một rebase tương tác.


Tôi cũng từng có ước mơ về tự động hóa: git chỉ nên cố gắng đưa nó trở lại lịch sử càng xa càng tốt, mà không cần bản vá lỗi. Nhưng phương pháp của bạn có lẽ là lành mạnh hơn. Thật tuyệt khi thấy bạn đã cố gắng. Tôi sẽ thử nó! (Tất nhiên, đôi khi bản vá sửa lỗi xuất hiện ở nơi khác trong tệp và chỉ nhà phát triển mới biết nó thuộc về cam kết nào. Hoặc có lẽ một thử nghiệm mới trong bộ thử nghiệm có thể giúp máy tìm ra nơi cần sửa.)
joeytwiddle

1

Tôi đã viết một hàm shell nhỏ được gọi gcfđể thực hiện cam kết sửa lỗi và rebase tự động:

$ git add -p

  ... select hunks for the patch with y/n ...

$ gcf <earlier_commit_id>

  That commits the fixup and does the rebase.  Done!  You can get back to coding.

Ví dụ: bạn có thể vá cam kết thứ hai trước cam kết mới nhất với: gcf HEAD~~

Đây là chức năng . Bạn có thể dán nó vào~/.bashrc

git_commit_immediate_fixup() {
  local commit_to_amend="$1"
  if [ -z "$commit_to_amend" ]; then
    echo "You must provide a commit to fixup!"; return 1
  fi

  # Get a static commit ref in case the commit is something relative like HEAD~
  commit_to_amend="$(git rev-parse "${commit_to_amend}")" || return 2

  #echo ">> Committing"
  git commit --no-verify --fixup "${commit_to_amend}" || return 3

  #echo ">> Performing rebase"
  EDITOR=true git rebase --interactive --autosquash --autostash \
                --rebase-merges --no-fork-point "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

Nó sử dụng --autostashđể lưu trữ và bật bất kỳ thay đổi nào chưa được cam kết nếu cần thiết.

--autosquashyêu cầu --interactiverebase, nhưng chúng tôi tránh tương tác bằng cách sử dụng giả EDITOR.

--no-fork-pointbảo vệ các cam kết không bị bỏ qua âm thầm trong các tình huống hiếm hoi (khi bạn đã tách nhánh mới và ai đó đã khôi phục các cam kết trước đây).


0

Tôi không biết về một cách tự động, nhưng đây là một giải pháp có thể dễ dàng hơn để con người kiếm tiền:

git stash
# write the patch
git add -p <file>
git commit -m"whatever"   # message doesn't matter, will be replaced via 'fixup'
git rebase -i <bad-commit-id>~1
# now cut&paste the "whatever" line from the bottom to the second line
# (i.e. below <bad-commit>) and change its 'pick' into 'fixup'
# -> the fix commit will be merged into the <bad-commit> without changing the
# commit message
git stash pop

Xem phản hồi của tôi cho một tập lệnh tận dụng điều này để triển khai một git fixuplệnh.
Frerich Raabe

@Frerich Raabe: âm thanh tốt, tôi dind't biết về--autosquash
Tobias KIENZLER

0

Tôi muốn giới thiệu https://github.com/tummychow/git-absorb :

Quảng cáo chiêu hàng thang máy

Bạn có một nhánh tính năng với một vài cam kết. Đồng đội của bạn đã xem xét chi nhánh và chỉ ra một vài lỗi. Bạn có các bản sửa lỗi cho các lỗi, nhưng bạn không muốn đẩy tất cả chúng vào một cam kết không rõ ràng cho biết các bản sửa lỗi, bởi vì bạn tin vào các cam kết nguyên tử. Thay vì tìm SHA cam kết theo cách thủ công git commit --fixuphoặc chạy rebase tương tác thủ công, hãy làm như sau:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • hoặc là: git rebase -i --autosquash master

git absorbsẽ tự động xác định những cam kết nào là an toàn để sửa đổi và những thay đổi được lập chỉ mục nào thuộc về từng cam kết đó. Sau đó nó sẽ viết sửa lỗi! cam kết cho mỗi thay đổi đó. Bạn có thể kiểm tra đầu ra của nó theo cách thủ công nếu bạn không tin tưởng vào nó, sau đó gấp các bản sửa lỗi vào nhánh tính năng của bạn với chức năng autosquash tích hợp sẵn của git.

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.