--- tags: 學習筆記, 版本控制 --- Git 介紹與常用指令 === 什麼是 Git? --- Git 是一種分散式版本控制系統。 ### 版本控制系統? 未導入版本控制系統之前,我們對專案進行修改或整合時,必須耗費大量的人力來對程式碼做備份和比對的工作。這在專案規模逐漸擴大或是多人協作的場景時,每個人的程式碼進度不一,哪段程式碼被修改或覆蓋?這段程式碼和上一次的版本有什麼不同?導致程式碼混亂不堪,難以維護,而版本控制系統就是要解決這樣的問題。 ### 分散式? 版本控制主要分為集中式(Centralized Version Control Systems,CVCSs)和分散式(Distributed Version Control Systems, DVCSs)兩種,前者如 SVN,後者如 Git。 在 Git 中,每個協作者都會擁有一個自己完整的版本庫,你可以在自己的版本庫中盡情的開 branch、修改程式碼,只要不 push 到主要的版本庫,你不會影響任何一個與你協作的夥伴。 而只要將專案 clone 到自己的設備,你不需要連網,就可以在自己的版本庫中查看歷史紀錄(git log)、創建分支(git branch)、切換分支(git checkout)、甚至是提交(git commit)。也就是說假設你遇難隻身一人漂流到孤島,突然有一股修改專案的衝動,打開筆電就可以開始工作。 若您在公司使用的是SVN,但又想體驗 Git 自由自在的感覺,Git 也有和 SVN 溝通的 bridge,請 Google git svn。 概念 --- ![Git](https://i.imgur.com/94TZTAv.png "Git") Workspace 是工作目錄,index 或者 stage 是暫存區(所以也有一些文章將暫存區翻譯為索引),local repository 是本地數據庫,remote repository 是遠端數據庫。 基本的開發流程是在 workspace 修改程式碼完成功能或修改之後,分批的將修改 add 進 index(stage) 中,再將這部分的修改 commit 到 local repository 並寫好符合規定的 description。 或許 stage 對於初使用 Git 的人來說有點不直覺,可能會認為每一次 commit 之前還要先 add 有點多此一舉。但在工作目錄(workspace)和數據庫(local repository)多一個暫存區(stage)的設計讓 Git 有更多彈性。 實際使用時可能會有很多需要分批上傳的情景,例如目前的 workspace 有兩個 feature 的修改,我可以將 featureA 先 add 後 commit,再做 featureB 部份的上傳,這樣的好處是不同的修改都可以擁有自己的 commit description,在 commit tree 中可以明確地看出來。 另外有一個有趣的地方需要注意,如果先對 hello.php 做了 functionA 部份的修改後 add,再對同樣在 hello.php 中的 functionB 部份做修改,這時候 commit 的話只會對第一次修改做上傳,如果在上傳之前 git status 會看見 hello.php 同時出現在 Changes to be committed 和 Changes not staged for commit 中,這是因為 add 是對修改做快照,而不是檔案。 常用指令 --- ### git init 切換到專案目錄並輸入,會在該目錄新增一個 .git目錄,代表 Git 開始對該目錄進行版控。 --- ### git clone ```git= git clone git://XXXXXXXX ``` 從目標專案複製到本地,這個複製幾乎包含了該專案的所有資料,包含歷史紀錄、分支、標籤等,而不僅僅是檔案,並會自動設定此遠端數據庫為 origin。 --- ### git remote 查看遠端數據庫,若目前的本地數據庫是從遠端數據庫 clone 來的,則會看到預設的 origin。 <br> ```git= git remote -v ``` 參數 -v 會在名稱後方顯示 URL <br> ```git= git remote add name git://XXXXXXXX ``` add 指令可以新增一個遠端數據庫,並取名為 name。 --- ### git status 查看當前專案的狀態,是否有新增刪除檔案或修改程式碼。 <br> ```git= git status -s ``` 參數 -s 可以看到比較簡潔的輸出格式 M = modified D = deleted R = renamed C = copied U = updated but unmerged --- ### git diff 直接輸入 `git diff` 可以看到當前工作目錄和暫存區的差別。 ```git= git diff <commitId1> <commitId2> ``` 可以比較兩次 commit 的差別。 <br> ```git= git diff --staged ``` 參數 --staged 則顯示暫存區和上次 commit 的差別 --- ### git log 從新到舊列出所有提交的歷史紀錄,包含 Commit 的作者、時間和description。 <br> ```git= git log -p -2 ``` 參數 -p 可以列出提交的內容,-2則限制最近的兩筆更新 <br> ```git= git log --stat ``` 參數 --stat 則顯示更新內容的簡略內容,包含被更動的檔案、更動多少檔案、有多少行被修改 <br> ```git= git log --graph ``` 參數 --graph 會在 log 旁用 ASCII 畫出分支的分歧和合併 --- ### git add ```git= git add hey.php ``` 可以將尚未被追蹤或有修改的檔案加入暫存區 <br> ```git= git add *.php ``` 也可以使用萬用字元 <br> ```git= git add -A ``` 參數 -A 可以將所有修改加入暫存區,等效於 `git add .` 或 `git add --all` --- ### git rm 刪除檔案並將刪除記錄存進暫存區 <br> ```git= git rm --cached ``` 參數 --cached 將檔案從 git 中移除,並沒有真的刪除檔案,而是將檔案變成 untracked。 --- ### git mv 修改檔名並將修改記錄存進暫存區 ```git= git mv oldName newName ``` --- ### git commit 將暫存區提交到儲存庫,這個指令會打開編輯器,輸入這次的 commit description 後離開編輯器,git 便會完成提交。 <br> ```git= git commit -m "This is commit descriptions" ``` 參數 -m 可以直接輸入 commit description <br> ```git= git commit -a ``` 參數 -a 可以省略 `git add` 的步驟,直接將修改存入暫存區並上傳 --- ### git branch 顯示分支列表,有*代表當前分支。 <br> ```git= git branch myfeature ``` 建立 myfeature 分支 <br> ```git= git branch -m oldName newName ``` 將 oldName 這個分支的名稱改為 newName <br> ```git= git branch -d myfeature ``` 刪除 myfeature 分支 --- ### git merge ```git= git merge myfeature ``` 會將 myfeature 這個分支合併到當前分支,所以在 merge 前記得先 checkout 到(通常是) master 分支 --- ### git rebase `git rebase` 是一個很有趣的功能,也有其他不同功能的用法,這裡主要介紹它在合併上的使用,進階的使用請見下方「我可以修改以前的 commit 嗎?」。 base 是基準點,也就是分支從哪裡長出來,所以 rebase 的意思接近於「重新定義分支的參考基準」。 假設我們從 master 分出了兩條 branch ,分別是 feature01 跟 feature02。我如果在 feature02 這個分支輸入: ```git git rebase feature01 ``` 意思就是「我 (feature02) 要重新定義 (re) 我的參考基準 (base),那個參考基準就是 feature01 」。 以結果來說,feature02 的 commit 會直接接在 feature01 的後面,看起來很像剪下貼上,但對 Git 來說 feature02 的那些 commit 都是新的 commit ,可以從 `git log --oneline`看到他們和原本的 SHA-1 值不一樣。 至於在 feature01 時 rebase feature02 ,和在 feature02 時 rebase feature01 的差別,只在最後 log 上 commit 的順序不同。 --- ### git push ```git= git push origin master ``` 將分支 master 推上遠端數據庫 origin 若有設定 remote,不帶參數執行 `git push` 相當於 `git push <remote>` 若沒有設定 remote ,不帶參數執行 `git push` 相當於 `git push origin` --- ### git pull ```git= git pull origin master ``` 將遠端數據庫 origin 的 master 分支拉下並合併到本地數據庫 若有設定 remote,不帶參數執行 `git pull` 相當於 `git pull <remote>` 若沒有設定 remote ,不帶參數執行 `git pull` 相當於 `git pull origin` `git pull` 做的事情實際上等於 `git fetch` + `git merge`,`git fetch` 會比對本地與遠端的差別,並在本地形成分支,`git merge` 則會將分支合併。 --- 你遇到這樣的問題了嗎? --- ### 正確的使用姿勢? `git init` 或是 `git clone` 之後,你可能會手癢先 `git status` 看一下,你會看到一個乾淨的目錄,然後開始編寫程式碼,一邊寫一邊 `git diff` 看自己到底改了哪些東西,改了一個段落在 `git add` 之前或許會 `git branch` 確定一下自己在哪個分支,有錯的話趕快 `git checkout branchName` 到正確的分支,接著 `git add` 把修改存到暫存區,`git status` 看一下是不是正確的狀態,然後 `git commit -m "This is my new git description"`,最後 `git push` 。 --- ### 這行誰寫的! 一覺醒來發現網站掛了,怎麼多了這一行扣!`git blame index.php` 可以看到 index.php 裡每一行的作者是誰。 --- ### 這個 commit 到底修改了啥? 先 `git log` 找到那次修改的 commit ID,你或許會用到 ```git= git log --author="Louis.Su" ``` 來篩選出 commit 作者,或者是 ```git= git log --grep="Hey" ``` 來找到 description 裡面包含 Hey 的 commit,又或者 ```git= git log -S "functionA" ``` 來找到上傳檔案有包含 functionA 的 commit。 最後`git show commitID`,就可以看到修改記錄了。 --- ### 我想修改剛剛的 commit 想修改最後一次的 commit,可以輸入 ```git= git commit --amend -m "This is right description" ``` 對人類來說只是修改 commit 的訊息,但對 git 來說,這其實是一個全新的 commit 了,可以從 `git log --oneline` 中看到 commit 的 SHA-1 值是不同的。 --- ### 我可以修改更之前的 commit 嗎? 這通常發生在自己開的 branch 裡 commit 雜亂無章的情況,想做好修剪之後再 push。盡量不要在已經 push 到中央伺服器的 commit 進行這種操作,除非你和你的團隊非常清楚自己在做什麼,這會造成協作者的混亂,因為同樣的程式碼有不同的版本,而且它會改變歷史 (這也是 rebase 屬於危險指令的原因)。 Git 本身並沒有修改歷史的工具。但可以使用 rebase 來做到對過去 commit 的修改。 <br> ```git= git rebase -i ``` 可以開啟對話模式,要給他一個參數,這個參數可以是 `git rebase -i HEAD~3` 這樣從當前分支往前算的形式,也可以是 `git rebase -i 6ee9b6b` 這樣的 SHA-1 值,意思是「從這一個 commit 往後算到現在位子的所有 commit 」。 之後會出現像這樣的列表: ``` pick f7f3f6d fix some bugs pick 310154e add some files pick a5f4a0d feat: complete a feature # Rebase 710f0f8..a5f4a0d onto 710f0f8 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out ``` 前幾行 pick 的意思是保留 commit 不做修改,如果你是想修改 commit message 的話,把 pick 改成 reword : ``` reword f7f3f6d fix some bugs pick 310154e add some files pick a5f4a0d feat: complete a feature ``` 表示我要修改這個 commit ,存檔離開後會再跳出另一個編輯器畫面,將 fix some bugs 改成你想要的 commit message 之後存檔離開, git 就會處理完剩下的事情。 如同上文中對 rebase 的描述,聰明如你肯定也猜到了,是的,這個 commit 同樣是全新的一個 commit ,而在這裡要注意的是,因為修改了過去的歷史,在這個 commit 之後的所有 commit 因為歷史的改變,所以整串 commit 都會變成新的 commit 。 因為過去被改變了,所以未來也會產生相應的變化,~~這就是平行時空的概念~~。 而 edit 和 reword 不同的地方在於: edit 同時還能修改 commit 內容,他會在 rebase 的過程中暫停等待你修改,所以當你: ``` edit f7f3f6d fix some bugs pick 310154e add some files pick a5f4a0d feat: complete a feature ``` 存檔離開後, git 會 checkout 到你想修改的 commit ,當你進行完你想做的修改之後, `git add [你有修改的檔案]` ,之後再 `git commit --amend ` 修改 commit message ,最後再輸入 `git rebase --continue` 將 rebase 完成。 另外的 squash 和 fixup 則是用來合併 commit,不同的地方是當你用 squash 要合併時,會把多個要合併的 commit message 放進編輯器裡面讓你修改最後合併的 commit message ;而 fixup 則是把這個 commit 合併到上一個 commit 中,並直接捨棄這個 commit message。 --- ### 挫賽!我可以重來嗎? git 的強大之處在於他大部分的指令都是可逆的,但它同樣也是危險的,因為並不是所有操作都可逆,一不小心就會搞得整個環境亂七八糟,在實戰時執行指令前都要謹慎思考,清楚知道自己在幹嘛,強烈建議可以搭配 Github 開一個自己來玩玩看。 這一 part 會先介紹如何反悔,再說明 reset 這個指令的意義,建議看完再操作。 #### 如何將暫存區的檔案移出 這樣的場景會出現在你修改了兩個檔案,想要分別提交,但你手速太快不小心打了 `git add -A` 全部加到暫存區了。 ```git= git reset HEAD <filename> ``` #### 如何將剛剛的 commit 拆回來 這樣的場景則出現在後悔剛剛那個 commit 的內容,想拆回來重做,你可以先下 `git log` 或 `git log --oneline` 來看到 SHA-1 值,然後: ```git= git reset 6ee9b6b ``` 也可以用相對的方式: ```git= git reset HEAD^ ``` #### 哇塞我整個專案都玩壞了,怎麼回復成原本的樣子 ```git= git reset --hard HEAD~2 ``` 這行指令的意思是最後的兩個版本(HEAD、HEAD^)我都不要了,我也不想讓人看見他,專案會直接回到 HEAD^^ 的狀態,所以在下指令前請確保你的 HEAD、HEAD^ 都是不要的。 #### 我還想要 HEAD 或 HEAD^,能回復嗎? 其實還是救的回來,先下: ```git= git reflog ``` reflog 裡面存的是「HEAD移動的紀錄」,你會看到你剛剛 reset 的紀錄,所以要取消 reset 的話,就要「reset 到 reset 前的那個 commit 」: ```git= git reset --hard <commitID> ``` 講到這裡聰明的你一定已經隱約感覺到 reset 的意義了,在中文會比較接近「前往」。 而 `--hard` 是參數,有三種常見的: `--mixed` 、 `--soft` 、 `--hard` ,這個參數決定的是「那些我不要的東西最後要去哪裡」 `--mixed` 是預設,這個模式下的 reset commit 會把 commit 內容丟進工作目錄裡面 `--soft` 則會把 commit 內容丟進暫存區 `--hard` 則是工作目錄和暫存區都不會留 所以前文說的「拆」 commit 其實並不準確,而是「回到」那個 commit 的狀態,不同參數則決定了路過的那幾個檔案將何去何從。 參考資料 --- Git官方文件:https://git-scm.com/book/zh-tw/v2 為你自己學Git:https://gitbook.tw/ 連猴子都能懂的Git入門指南:https://backlog.com/git-tutorial/tw/