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。
基本的開發流程是在 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目錄,代表 Git 開始對該目錄進行版控。
git clone git://XXXXXXXX
從目標專案複製到本地,這個複製幾乎包含了該專案的所有資料,包含歷史紀錄、分支、標籤等,而不僅僅是檔案,並會自動設定此遠端數據庫為 origin。
查看遠端數據庫,若目前的本地數據庫是從遠端數據庫 clone 來的,則會看到預設的 origin。
git remote -v
參數 -v 會在名稱後方顯示 URL
git remote add name git://XXXXXXXX
add 指令可以新增一個遠端數據庫,並取名為 name。
查看當前專案的狀態,是否有新增刪除檔案或修改程式碼。
git status -s
參數 -s 可以看到比較簡潔的輸出格式
M = modified
D = deleted
R = renamed
C = copied
U = updated but unmerged
直接輸入 git diff
可以看到當前工作目錄和暫存區的差別。
git diff <commitId1> <commitId2>
可以比較兩次 commit 的差別。
git diff --staged
參數 –staged 則顯示暫存區和上次 commit 的差別
從新到舊列出所有提交的歷史紀錄,包含 Commit 的作者、時間和description。
git log -p -2
參數 -p 可以列出提交的內容,-2則限制最近的兩筆更新
git log --stat
參數 –stat 則顯示更新內容的簡略內容,包含被更動的檔案、更動多少檔案、有多少行被修改
git log --graph
參數 –graph 會在 log 旁用 ASCII 畫出分支的分歧和合併
git add hey.php
可以將尚未被追蹤或有修改的檔案加入暫存區
git add *.php
也可以使用萬用字元
git add -A
參數 -A 可以將所有修改加入暫存區,等效於 git add .
或 git add --all
刪除檔案並將刪除記錄存進暫存區
git rm --cached
參數 –cached 將檔案從 git 中移除,並沒有真的刪除檔案,而是將檔案變成 untracked。
修改檔名並將修改記錄存進暫存區
git mv oldName newName
將暫存區提交到儲存庫,這個指令會打開編輯器,輸入這次的 commit description 後離開編輯器,git 便會完成提交。
git commit -m "This is commit descriptions"
參數 -m 可以直接輸入 commit description
git commit -a
參數 -a 可以省略 git add
的步驟,直接將修改存入暫存區並上傳
顯示分支列表,有*代表當前分支。
git branch myfeature
建立 myfeature 分支
git branch -m oldName newName
將 oldName 這個分支的名稱改為 newName
git branch -d myfeature
刪除 myfeature 分支
git merge myfeature
會將 myfeature 這個分支合併到當前分支,所以在 merge 前記得先 checkout 到(通常是) master 分支
git rebase
是一個很有趣的功能,也有其他不同功能的用法,這裡主要介紹它在合併上的使用,進階的使用請見下方「我可以修改以前的 commit 嗎?」。
base 是基準點,也就是分支從哪裡長出來,所以 rebase 的意思接近於「重新定義分支的參考基準」。
假設我們從 master 分出了兩條 branch ,分別是 feature01 跟 feature02。我如果在 feature02 這個分支輸入:
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 origin master
將分支 master 推上遠端數據庫 origin
若有設定 remote,不帶參數執行 git push
相當於 git push <remote>
若沒有設定 remote ,不帶參數執行 git push
相當於 git push origin
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 裡每一行的作者是誰。
先 git log
找到那次修改的 commit ID,你或許會用到
git log --author="Louis.Su"
來篩選出 commit 作者,或者是
git log --grep="Hey"
來找到 description 裡面包含 Hey 的 commit,又或者
git log -S "functionA"
來找到上傳檔案有包含 functionA 的 commit。
最後git show commitID
,就可以看到修改記錄了。
想修改最後一次的 commit,可以輸入
git commit --amend -m "This is right description"
對人類來說只是修改 commit 的訊息,但對 git 來說,這其實是一個全新的 commit 了,可以從 git log --oneline
中看到 commit 的 SHA-1 值是不同的。
這通常發生在自己開的 branch 裡 commit 雜亂無章的情況,想做好修剪之後再 push。盡量不要在已經 push 到中央伺服器的 commit 進行這種操作,除非你和你的團隊非常清楚自己在做什麼,這會造成協作者的混亂,因為同樣的程式碼有不同的版本,而且它會改變歷史 (這也是 rebase 屬於危險指令的原因)。
Git 本身並沒有修改歷史的工具。但可以使用 rebase 來做到對過去 commit 的修改。
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 reset HEAD <filename>
這樣的場景則出現在後悔剛剛那個 commit 的內容,想拆回來重做,你可以先下 git log
或 git log --oneline
來看到 SHA-1 值,然後:
git reset 6ee9b6b
也可以用相對的方式:
git reset HEAD^
git reset --hard HEAD~2
這行指令的意思是最後的兩個版本(HEAD、HEAD^)我都不要了,我也不想讓人看見他,專案會直接回到 HEAD^^ 的狀態,所以在下指令前請確保你的 HEAD、HEAD^ 都是不要的。
其實還是救的回來,先下:
git reflog
reflog 裡面存的是「HEAD移動的紀錄」,你會看到你剛剛 reset 的紀錄,所以要取消 reset 的話,就要「reset 到 reset 前的那個 commit 」:
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/