Git原理與高階使用(4)

Desmonddai583發表於2018-04-22

Rebase

rebase與merge其實都是完成類似的工作的,但是背後的工作方式卻有很大的不同,讓我們用以下兩張圖來說明一下兩者的區別

Git原理與高階使用(4)
Git原理與高階使用(4)

第一張圖是merge的工作原理,當我們在mywork分支中呼叫git merge origin,git就會根據c2,c4,c6這三個點來計算合併出一個新的commitc點c7。

第二張圖則是rebase的工作原理,當我們在mywork分支中呼叫git rebase origin時,c5變成了c5',c6變成了c6',並且父commit指向了c4,而原來的c5和c6就沒有用了。

從上面就可以看出,在執行git rebase後會將兩條分支有變成在同一條commit鏈上,而merge則是依然可以看到分叉的commit鏈,背後呢其實就是等於rebase修改了git的提交歷史,把之前分支上的commit一個一個接到另一條分支上

當然rebase的過程中也一樣有可能會產生衝突,這個時候只要我們碰到一個衝突,就需要解決它,然後執行git add新增,接著執行git rebase --continue去轉接下一個commit,直到所有commit都轉接到了另一條分支上。

在rebase進行中的任何時刻,我們都可以通過執行git rebase --abort將分支恢復到rebase之前的狀態。

rebase的好處就是會讓我們的commit鏈變得整潔好看,但是對於使用rebase需要注意的點就是不要在master分支上執行,因為通常我們在master分支上的是相對穩定的程式碼,我們不會想要去修改到它的歷史,另外,不要對已經推送到遠端的分支進行rebase,通常只對本地還未推出的分支做,因為遠端的分支可能其他的使用者已經將它們拉取到各自的本地版本庫,我們不希望他們因為我們執行了rebase而要更改自己原本的提交歷史,這即便對git來說也很難處理,會出現一些意想不到的情況。如果是上述這些場景還是應該使用merge來完成。

指令

  1. rebase時直接使用根基分支的修改

    當我們執行rebase時,當遇到衝突時,我們除了可以手動的去解決衝突外,還可以通過執行git rebase --skip直接的使用根基分支的修改

場景

  1. 如果我已經rebase完了,怎麼回到原狀

    我們可以先通過git reflog找到之前rebase的那條記錄id

    Git原理與高階使用(4)
    然後執行git reset commit-id --hard就可以回到rebase前的狀態了。

    另外在每次做rebase前,git都會為我們建立一個ORIG_HEAD記錄rebase前的狀態讓我們不需要reflog就可以快速切回去,所以就可以直接執行git reset ORIG_HEAD --hard

  2. 如何我想去修改歷史資訊

    Git原理與高階使用(4)
    假設這裡我們想要從init commit開始重新整理git的歷史資訊,我們可以先定位到init commit那條commit id,接著通過git rebase -i commit-id進入互動模式
    Git原理與高階使用(4)
    這裡可以看到commit以一個倒序的順序排列出來了,這裡pick就代表的是不對commit進行改變,如果想改變的話可以將pick替換成r或者reword,然後儲存退出
    Git原理與高階使用(4)
    接著git就會迴圈跳出剛才我們改成r的每個commit讓我們去修改
    Git原理與高階使用(4)
    全部修改完後我們再看回git歷史,就發現我們剛才的改動全部應用上了
    Git原理與高階使用(4)
    這裡可以看到除了git歷史資訊被修改了,連對應的commit-id都被修改了,而且它們之後的所有commit也因此跟著被修改了

  3. 如果是非文字的檔案產生衝突了怎麼辦

    當非文字的檔案產生衝突時,其實我們是沒有辦法決定具體哪一行出了問題的,所以這時候我們可以執行git checkout 檔名 --theirs來使用別人的檔案或者執行git checkout 檔名 --ours來使用我們的檔案

  4. 如果我想把過去的多個commit合併成一個commit

    跟上面一樣第2個一樣我們先進入互動模式,將pick改為s或者squash,這樣連在一起的s就會與他們上一個pick合併成一個commit了,接著我們儲存退出,git依舊是迴圈讓我們去修改我們改動的commit,完成之後我們就做到了合併的效果了

  5. 如果我想把過去的一個commit拆成多個commit

    一樣是進入互動模式,接著將pick改為e或者edit,接著儲存退出,這裡我們就會看到git提示我們可以開始去修改commit,並且在結束後執行rebase continue的動作,其實這裡git就暫時將我們轉至我們要修改的那個commit

    Git原理與高階使用(4)
    這時候我們就可以執行git reset HEAD^,看過之前的文章就知道這裡預設其實就是mixed模式,所以當前commit前後的修改就被轉至工作區,接著我們就可以按我們想要的commit形式重新把工作區的檔案新增到暫存區並且提交,最後執行rebase continue,我們就成功完成了拆分commit的動作

  6. 如果我想在commit之間加入新的commit

    其實這個跟上面的場景幾乎差不多,唯一的區別是我們不需要再使用git reset,而是直接新增提交我們要的修改就可以了

  7. 如果我想調整commit的順序或者刪除掉幾個commit

    調整順序的話我們只需要在互動模式中直接把pick的每條commit調成我們要的順序即可,而對於刪除commit,我們也只需要把想刪掉的pick直接刪除就可以了。當然在做這些操作之前都要思考清楚,畢竟像調整順序的話如果你把對一個檔案的修改commit放到了這個檔案的建立commit之前,那就會出事了

  8. 我不小心把賬號密碼放到了git裡面,怎麼刪除呢

    git提供了一個filter-branch指令幫我們批量的對commit進行改動,假設我們的賬號資訊放在了config/database.yml中,我們可以執行git filter-branch --tree-filter "rm -f config/database.yml",這樣git會對每一條commit都執行刪除的操作,那如果我後悔了想要恢復回來怎麼辦呢,我們可以執行git reset refs/original/refs/heads/master --hard就可已恢復剛才的刪除操作了,這個refs/original/refs/heads/master其實就是我們之前說的ORIG_HEAD,是git在我們做一些危險操作時備份出來的一個指標以便我們撤回之前的操作。所以這裡如果我們真的要徹底刪除還需要把refs/original/refs/heads/master也一併刪除掉。接著還有一個找的回來的地方就是reflog,所以我們也要清理掉reflog,它預設是30天清除一次,我們可以通過執行git reflog expire --all --expire=now讓git馬上清除掉。最後我們在這一系列操作之後就會有一堆閒置沒有用的物件了,我們可以通過git fsck --unreachable列出來這些物件,當然因為他們已經沒有用了,我們就可以直接執行git gc --prune=now把它們即刻清理掉,這樣我們就徹底的把賬號資訊從git中刪除了。從這裡也看得出來其實對於git來說要真正意義上刪除記錄是非常困難的,所以我們都經常說git永遠有後悔藥吃,因為總是有辦法可以找回原來的歷史記錄。

Q&A

  1. reset,revert和rebase有什麼區別

    前面我們介紹過了reset和rebase,那我們先來說一下revert,我們可以通過執行git revert commit-id來還原某一個commit,但是這個還原不是刪除那個commit,其實revert是加了一個新的commit然後把內容修改為我們想要revert的那個commit之前的狀態(這裡如果在revert時想用預設的commit資訊不去修改的話可以加上--no-edit選項)。

    所以可以看出reset和rebase是會改變歷史記錄的,而revert則不會

    reset多用於還沒有推送到遠端的commit,我們可以將它還原至某個我們指定的commit

    rebase也多用於沒有推送到遠端的commit,當然他的能力更強大一些,不管你想做新增,修改或是刪除等都可以做到,很適合用來整理本地的commit歷史

    revert的話就比較適合已經推出去commit,或者一些團隊開發規範下不準使用reset和rebase的情況

相關文章