Bagaimana saya dapat dengan mudah memperbaiki komit yang lalu?

116

Saya baru saja membaca mengubah satu file di masa lalu komit di git tapi sayangnya solusi yang diterima 'menyusun ulang' komit, yang bukan yang saya inginkan. Jadi inilah pertanyaan saya:

Sesekali, saya melihat bug dalam kode saya saat mengerjakan fitur (tidak terkait). A quick git blamekemudian mengungkapkan bahwa bug telah diperkenalkan beberapa komit yang lalu (saya melakukan cukup banyak, jadi biasanya itu bukan komit terbaru yang memperkenalkan bug). Pada titik ini, saya biasanya melakukan ini:

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

Namun, hal ini sering terjadi sehingga urutan di atas semakin mengganggu. Terutama 'rebase interaktif' yang membosankan. Apakah ada jalan pintas ke urutan di atas, yang memungkinkan saya mengubah komit arbitrer di masa lalu dengan perubahan bertahap? Saya sangat sadar bahwa ini mengubah sejarah, tetapi saya sering melakukan kesalahan sehingga saya sangat ingin memiliki sesuatu seperti itu

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.

Mungkin skrip cerdas yang dapat menulis ulang komitmen menggunakan alat pipa atau lebih?

Frerich Raabe
sumber
Apa yang Anda maksud dengan "menyusun ulang" komit? Jika Anda mengubah riwayat, maka semua komit sejak perubahan komit harus berbeda, tetapi jawaban yang diterima untuk pertanyaan terkait tidak menyusun ulang komit dalam arti yang berarti.
CB Bailey
1
@Charles: Maksud saya pengubahan urutan seperti pada: jika saya perhatikan bahwa HEAD ~ 5 adalah komit yang rusak, berikut jawaban yang diterima dalam pertanyaan terkait akan membuat HEAD (ujung cabang) komit tetap. Namun, saya ingin HEAD ~ 5 menjadi komit tetap - yang Anda dapatkan saat menggunakan rebase interaktif dan mengedit komit tunggal untuk perbaikan.
Frerich Raabe
Ya, tetapi kemudian perintah rebase akan memeriksa ulang master dan melakukan rebase semua komit berikutnya ke komit tetap. Bukankah ini caramu mengemudikan rebase -i?
CB Bailey
Sebenarnya, ada potensi masalah dengan jawaban itu, saya pikir seharusnya begitu rebase --onto tmp bad-commit master. Seperti yang tertulis, ini akan mencoba menerapkan komit buruk ke status komit tetap.
CB Bailey
Berikut alat lain untuk mengotomatiskan proses fixup / rebase: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Jawaban:

166

JAWABAN DIPERBARUI

Beberapa waktu yang lalu, --fixupargumen baru telah ditambahkan git commityang dapat digunakan untuk membuat komit dengan pesan log yang sesuai git rebase --interactive --autosquash. Jadi cara termudah untuk memperbaiki komit sebelumnya adalah sekarang:

$ 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

JAWABAN ASLI

Ini sedikit skrip Python yang saya tulis beberapa waktu lalu yang mengimplementasikan git fixuplogika ini yang saya harapkan dalam pertanyaan awal saya. Skrip mengasumsikan bahwa Anda melakukan beberapa perubahan dan kemudian menerapkan perubahan tersebut ke komit yang diberikan.

CATATAN : Skrip ini khusus untuk Windows; itu mencari git.exedan menetapkan GIT_EDITORvariabel lingkungan menggunakan set. Sesuaikan ini sesuai kebutuhan untuk sistem operasi lain.

Dengan menggunakan skrip ini saya dapat mengimplementasikan dengan tepat 'memperbaiki sumber yang rusak, perbaikan tahap, menjalankan alur kerja git fixup' yang saya minta:

#!/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)
Frerich Raabe
sumber
2
Anda dapat menggunakan git stashdan git stash popsekitar rebase Anda untuk tidak lagi memerlukan direktori kerja yang bersih
Tobias Kienzler
@TobiasKienzler: Tentang menggunakan git stashdan git stash pop: Anda benar sedang, tapi sayangnya git stashini jauh lebih lambat pada Windows daripada di Linux atau OS / X. Karena direktori kerja saya biasanya bersih, saya menghilangkan langkah ini agar tidak memperlambat perintah.
Frerich Raabe
Saya dapat mengonfirmasi itu, terutama saat bekerja pada jaringan berbagi: - /
Tobias Kienzler
1
Bagus. Saya tidak sengaja melakukannya git rebase -i --fixup, dan itu didasarkan kembali dari komitmen tetap sebagai titik awal, jadi argumen sha tidak diperlukan dalam kasus saya.
fwielstra
1
Untuk orang yang sering menggunakan --autosquash, mungkin berguna untuk menyetelnya menjadi perilaku default: git config --global rebase.autosquash true
taktak004
31

Yang saya lakukan adalah:

git add ... # Tambahkan perbaikan.
git commit # Berkomitmen, tetapi di tempat yang salah.
git rebase -i HEAD ~ 5 # Periksa 5 komit terakhir untuk rebasing.

Editor Anda akan terbuka dengan daftar 5 komit terakhir, siap untuk dicampuri. Perubahan:

pilih 08e833c Perubahan bagus 1.
pilih 9134ac9 Perubahan bagus 2.
pilih 5adda55 Perubahan buruk!
pilih 400bce4 Perubahan bagus 3.
pilih 2bc82n1 Perbaiki perubahan buruk.

...untuk:

pilih 08e833c Perubahan bagus 1.
pilih 9134ac9 Perubahan bagus 2.
pilih 5adda55 Perubahan buruk!
f 2bc82n1 Perbaikan perubahan buruk. # Pindah ke atas, dan ubah 'pick' menjadi 'f' untuk 'fixup'.
pilih 400bce4 Perubahan bagus 3.

Simpan & keluar dari editor Anda, dan perbaikan akan dipadatkan kembali ke komit miliknya.

Setelah Anda melakukannya beberapa kali, Anda akan melakukannya dalam beberapa detik dalam tidur Anda. Rebasing interaktif adalah fitur yang benar-benar menarik bagi saya di git. Ini sangat berguna untuk ini dan banyak lagi ...

Kris Jenkins
sumber
9
Jelas Anda dapat mengubah HEAD ~ 5 menjadi HEAD ~ n untuk kembali lebih jauh. Anda tidak akan ingin mencampuri riwayat apa pun yang Anda dorong ke atas, jadi saya biasanya mengetik 'git rebase -i origin / master' untuk memastikan bahwa saya hanya mengubah riwayat yang tidak didorong.
Kris Jenkins
4
Ini seperti apa yang selalu saya lakukan; FWIW, Anda mungkin tertarik dengan --autosquashsakelar untuk git rebase, yang secara otomatis menyusun ulang langkah-langkah di editor untuk Anda. Lihat tanggapan saya untuk skrip yang memanfaatkan ini untuk mengimplementasikan git fixupperintah.
Frerich Raabe
Saya tidak tahu Anda bisa memesan ulang hash komit, bagus!
Aaron Franke
Itu hebat! Hanya untuk memastikan semua pekerjaan rebase dilakukan adalah cabang fitur terpisah. Dan tidak mengacaukan cabang umum seperti tuan.
Jay Modi
22

Agak terlambat ke pesta, tetapi berikut adalah solusi yang berfungsi seperti yang dibayangkan penulis.

Tambahkan ini ke .gitconfig Anda:

[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' -"

Contoh penggunaan:

git add -p
git fixup HEAD~5

Namun jika Anda memiliki perubahan yang tidak bertahap, Anda harus menyimpannya sebelum rebase.

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

Anda dapat mengubah alias menjadi simpanan secara otomatis, alih-alih memberikan peringatan. Namun, jika perbaikan tidak berlaku dengan bersih, Anda perlu membuka simpanan secara manual setelah memperbaiki konflik. Melakukan penyimpanan dan pemunculan secara manual tampaknya lebih konsisten dan tidak terlalu membingungkan.

dschlyter.dll
sumber
Ini sangat membantu. Bagi saya, usecase yang paling umum adalah memperbaiki perubahan ke komit sebelumnya, jadi untuk git fixup HEADapa saya membuat alias. Saya juga bisa menggunakan amandemen untuk itu.
belalang
Terima kasih! Saya juga paling sering menggunakannya pada komit terakhir, tetapi saya memiliki alias lain untuk perbaikan cepat. amend = commit --amend --reuse-message=HEADKemudian Anda cukup mengetik git amendatau git amend -amelewati editor untuk pesan komit.
dschlyter
3
Masalah dengan amandemen adalah saya tidak ingat bagaimana mengejanya. Saya selalu harus berpikir, apakah ada perubahan atau perubahan dan itu tidak baik.
belalang
12

Untuk memperbaiki satu komit:

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

di mana a0b1c2d3 adalah komit yang ingin Anda perbaiki dan di mana 2 adalah jumlah komit +1 yang ditempelkan yang ingin Anda ubah.

Catatan: git rebase --autosquash tanpa -i tidak berfungsi tetapi dengan -i berfungsi, itu aneh.

Sérgio
sumber
2016 dan --autosquashtanpa -imasih tidak berhasil.
Jonathan Cross
1
Seperti yang dikatakan di halaman manual: Opsi ini hanya valid jika opsi --interactive digunakan. Tetapi ada cara mudah untuk melewati editor:EDITOR=true git rebase --autosquash -i
joeytwiddle
Langkah ke-2 tidak berhasil untuk saya, dengan mengatakan: Harap tentukan cabang mana yang ingin Anda rebase.
djangonaut
git rebase --autosquash -i HEAD ~ 2 (di mana 2 adalah jumlah komit +1 yang ditempelkan yang ingin Anda ubah.
Sérgio
6

PEMBARUAN: Versi skrip yang lebih bersih sekarang dapat ditemukan di sini: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

Saya telah mencari sesuatu yang serupa. Skrip Python ini tampaknya terlalu rumit, oleh karena itu saya telah menyusun solusi saya sendiri:

Pertama, alias git saya terlihat seperti itu (dipinjam dari sini ):

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

Sekarang fungsi bash menjadi sangat sederhana:

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
}

Kode ini tahap pertama semua perubahan saat ini (Anda dapat menghapus bagian ini, jika Anda ingin membuat file sendiri). Kemudian buat perbaikan (squash juga dapat digunakan, jika itu yang Anda butuhkan) komit. Setelah itu ia memulai rebase interaktif dengan --autosquashflag pada induk komit yang Anda berikan sebagai argumen. Itu akan membuka editor teks yang dikonfigurasi, sehingga Anda dapat memverifikasi bahwa semuanya seperti yang Anda harapkan dan cukup menutup editor akan menyelesaikan prosesnya.

Bagian if [[ "$1" == HEAD* ]](dipinjam dari sini ) digunakan, karena jika Anda menggunakan, misalnya, HEAD ~ 2 sebagai referensi komit Anda (komit yang ingin Anda perbaiki perubahan saat ini dengan) referensi maka HEAD akan dipindahkan setelah komit perbaikan dibuat dan Anda perlu menggunakan HEAD ~ 3 untuk merujuk ke komit yang sama.

Deiwin
sumber
Alternatif yang menarik. +1
VonC
4

Anda dapat menghindari tahapan interaktif dengan menggunakan editor "null":

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

Ini akan digunakan /bin/truesebagai editor, bukan /usr/bin/vim. Itu selalu menerima apa pun yang disarankan git, tanpa disuruh.

joeytwiddle
sumber
Memang, ini persis seperti yang saya lakukan dalam jawaban skrip Python 'jawaban asli' saya dari 30 September 2010 (perhatikan bagaimana di bagian bawah skrip, dikatakan call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe
4

Yang benar-benar mengganggu saya tentang alur kerja perbaikan adalah bahwa saya harus memikirkan sendiri komitmen mana yang ingin saya ubah setiap saat. Saya membuat perintah "git fixup" yang membantu dalam hal ini.

Perintah ini membuat perbaikan komit, dengan keajaiban tambahan yang menggunakan git-deps untuk secara otomatis menemukan komit yang relevan, sehingga alur kerja sering kali turun ke:

# 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

Ini hanya bekerja jika perubahan bertahap dapat dikaitkan dengan jelas ke komit tertentu pada pohon kerja (antara master dan HEAD). Saya menemukan bahwa kasus ini sangat sering terjadi untuk jenis perubahan kecil yang saya gunakan untuk ini, misalnya kesalahan ketik pada komentar atau nama metode yang baru diperkenalkan (atau diganti namanya). Jika tidak demikian, setidaknya daftar calon komit akan ditampilkan.

Saya menggunakan ini banyak di alur kerja saya sehari-hari, untuk segera mengintegrasikan perubahan kecil ke baris sebelumnya berubah menjadi komit pada cabang kerja saya. Naskahnya tidak seindah yang seharusnya, dan ditulis dalam zsh, tetapi telah melakukan pekerjaan yang cukup baik untuk saya selama beberapa waktu sekarang sehingga saya tidak pernah merasa perlu untuk menulis ulang:

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

Valodim
sumber
2

Anda dapat membuat perbaikan untuk file tertentu dengan menggunakan alias ini.

[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' -"

Jika Anda telah membuat beberapa perubahan myfile.txttetapi Anda tidak ingin memasukkannya ke dalam komit baru, git fixup-file myfile.txtakan membuat fixup!untuk komit di mana myfile.txtterakhir kali diubah, dan kemudian akan rebase --autosquash.

Alvaro
sumber
Sangat pintar, saya lebih suka yang git rebasetidak dipanggil secara otomatis.
hurikhan77
2

commit --fixupdan rebase --autosquashhebat, tetapi tidak cukup. Ketika saya memiliki urutan komit A-B-Cdan saya menulis beberapa perubahan lagi di pohon kerja saya yang termasuk dalam satu atau beberapa komit yang ada, saya harus melihat riwayat secara manual, memutuskan perubahan mana yang termasuk dalam komit mana, menetapkannya dan membuat fixup!melakukan. Tetapi git sudah memiliki akses ke informasi yang cukup untuk dapat melakukan semua itu untuk saya, jadi saya telah menulis skrip Perl yang melakukan hal itu.

Untuk setiap git diffbagian dalam skrip digunakan git blameuntuk menemukan komit yang terakhir menyentuh baris yang relevan, dan panggilan git commit --fixupuntuk menulis fixup!komit yang sesuai , pada dasarnya melakukan hal yang sama yang saya lakukan secara manual sebelumnya.

Jika Anda merasa berguna, silakan perbaiki dan ulangi dan mungkin suatu hari kami akan mendapatkan fitur seperti itu dengan gitbenar. Saya ingin melihat alat yang dapat memahami bagaimana konflik penggabungan harus diselesaikan ketika telah diperkenalkan oleh rebase interaktif.

Oktalis
sumber
Saya juga bermimpi tentang otomasi: git seharusnya mencoba meletakkannya sejauh mungkin dalam sejarah, tanpa merusak tambalan. Tapi metode Anda mungkin lebih waras. Senang melihat Anda telah mencobanya. Saya akan mencobanya! (Tentu saja ada kalanya patch perbaikan muncul di tempat lain dalam file, dan hanya pengembang yang tahu komit mana yang menjadi miliknya. Atau mungkin pengujian baru dalam rangkaian pengujian dapat membantu mesin untuk menentukan di mana perbaikan harus dilakukan.)
joeytwiddle
1

Saya menulis fungsi shell kecil yang dipanggil gcfuntuk melakukan perbaikan perbaikan dan rebase secara otomatis:

$ 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.

Misalnya, Anda dapat menambal komit kedua sebelum yang terbaru dengan: gcf HEAD~~

Ini fungsinya . Anda dapat menempelkannya ke file~/.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'

Ini digunakan --autostashuntuk menyimpan dan memunculkan setiap perubahan yang tidak mengikat jika perlu.

--autosquashmembutuhkan --interactiverebase, tapi kami menghindari interaksi dengan menggunakan dummy EDITOR.

--no-fork-pointmelindungi komit agar tidak dijatuhkan secara diam-diam dalam situasi yang jarang terjadi (saat Anda telah bercabang di cabang baru, dan seseorang telah melakukan rebased pada komitmen sebelumnya).

joeytwiddle
sumber
0

Saya tidak mengetahui cara otomatis, tetapi berikut adalah solusi yang mungkin dengan lebih mudah melakukan bot manusia:

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
Tobias Kienzler
sumber
Lihat tanggapan saya untuk skrip yang memanfaatkan ini untuk mengimplementasikan git fixupperintah.
Frerich Raabe
@Frerich Raabe: Kedengarannya bagus, saya tidak tahu tentang--autosquash
Tobias Kienzler
0

Saya akan merekomendasikan https://github.com/tummychow/git-absorb :

Promosi Lift

Anda memiliki cabang fitur dengan beberapa komit. Rekan satu tim Anda meninjau cabang dan menunjukkan beberapa bug. Anda memiliki perbaikan untuk bug, tetapi Anda tidak ingin mendorong semuanya ke dalam komit buram yang mengatakan perbaikan, karena Anda percaya pada komit atomic. Daripada mencari commit SHA secara manual git commit --fixup, atau menjalankan rebase interaktif manual, lakukan ini:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • atau: git rebase -i --autosquash master

git absorbakan secara otomatis mengidentifikasi komit mana yang aman untuk dimodifikasi, dan perubahan terindeks mana yang dimiliki oleh setiap komit tersebut. Ini kemudian akan menulis perbaikan! berkomitmen untuk setiap perubahan tersebut. Anda dapat memeriksa outputnya secara manual jika Anda tidak mempercayainya, dan kemudian melipat perbaikan ke dalam cabang fitur Anda dengan fungsionalitas autosquash bawaan git.

Petski
sumber