---
# System prepended metadata

title: Git 介紹與常用指令
tags: [學習筆記, ' 版本控制']

---

---
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/

