---
tags: git
---
# <center>Git 讀書會</center>
> 版本控制有很多方式可以實現,像是svn(Subversion)、csv、git等等
> 我認為學版本控制,學的不是這些指令,背後更重要的是團隊文化和規則
> 一個簡單的例子,今天是誰改的這行程式,你要告訴我你改什麼
> 改的程式碼commit的訊息要怎麼寫
> 為什麼要這樣改,這樣我程式碼合併程式碼會出問題嗎
> 所以為了不必要的後續麻煩
> 這都是要團隊先訂定規則,還有coding style等等
> 這才是版本控制最重要的核心
> 不過首先還是先學指令和流程吧 XD
## 事前準備
- Install Git
- for [Windows](https://gitbook.tw/chapters/environment/install-git-in-windows.html)
- for [Mac OSX](https://gitbook.tw/chapters/environment/install-git-in-mac.html)
- Sign up [Github] new Account
- Download Git GUI - [Sourcetree]
- 下載完要註冊喔 -> [流程](https://confluence.atlassian.com/get-started-with-sourcetree/install-sourcetree-847359094.html)
## 什麼是 Git?為什麼要學習它?
如果你問大部份正在使用 Git 這個工具的人「什麼是 Git」,他們大多可能會回答你「Git 是一種版本控制系統」,專業一點的可能會回答你說「Git 是一種分散式版本的版本控制系統」
### 什麼是版本控制?
![](https://i.imgur.com/i88OLlZ.png)
如圖所示,隨著時間的變化,一開始這個 resume 目錄裡只有 3 個檔案,過兩天增加到 5 個。不久之後,其中的 2 個被修改了,過了三個月後又增加到 7 個,最後又刪掉了 1 個,變成 6 個。這每一個「resume 目錄的狀態變化」,不管是新增或刪除檔案,亦或是修改檔案內容,都稱之為一個「版本」,例如上圖圖例的版本 1 ~ 5。而所謂的「版本控制系統」,就是指會幫你記錄這些所有的狀態變化,並且可以像搭乘時光機一樣,隨時切換到過去某個「版本」時候的狀態。
簡單的說,Git 就像玩遊戲的時候可以儲存進度一樣。舉例來說,為了避免打頭目打輸了而損失裝備,又或是打倒頭目卻沒有掉落期望的珍貴裝備,你也許在每次要去打頭目之前之前記錄一下,在發生狀況的時候可以載入舊進度,再來挑戰一次。
### 分散又是什麼?
Git 是分散式的版本控制系統,就算在深山裡或飛機上沒有網路可使用,也可正常的使用 Git,待有網路的時候再與其它人同步即可。Git 大部份的操作都是在自己電腦上就可完成,而且不管是遠端的伺服器或是自己的電腦,在同步之後大家都會有一份完整的檔案及歷史紀錄。
SVN 之類的集中式的版控系統(Centralize Version Control),都需要有一台專用的伺服器,所有的更新都需要跟這台伺服器溝通。也就是說,萬一這台伺服器壞了,或是沒有網路連線的環境,就沒辦法使用。
![](https://i.imgur.com/Z1RIgmg.png =500x500)
### 找出老鼠屎
可以清楚的記錄每個檔案是誰在什麼時候加進來、什麼時候被修改或刪除。
出社會工作,有 Git 幫你保留這些歷史紀錄跟證據,萬一出事的時候你就能知道是從什麼時候開始就有問題,以及知道該找誰負責,不用自己背黑鍋!
### 處理檔案的方式
> Git 是差異化備份,而不是完整備份
Git 與其它版控系統最大的差異,是在於處理檔案的方式。其它家的版控系統,大多是記錄每個版本之間的「異動」:
類似以下,沒有結構的有像圖
![Imgur](https://i.imgur.com/usuyoEN.png)
### 小總結 - Git 優缺
* pros
* 免費、開源
* 速度快、檔案體積小
* 分散式系統
* cons
* 易學難精:指令多,but 大概 20% 的指令就足以應付 80% 的工作 (<a style="color:red">80/20 法則<a>)
## Start Git
![](https://i.imgur.com/n3AEnNo.png)
### 新增、初始 Repository
使用 `git init` 指令初始化這個目錄,主要目的是要讓 Git 開始對這個目錄進行版本控制,在當下資料夾產生 .git資料夾
```bash
$ mkdir git-practice
$ cd git-practice
$ git init
```
Git 完全就是只靠那個 .git 目錄在做事而已,也就是說,整個專案目錄裡,什麼檔案或目錄刪了都救得回來,但 .git 目錄只要刪了就沒辦法了。
### 把檔案交給 Git
用此命令查看目前這個目錄的「狀態」
```bash
$ git status
```
新增檔案
```bash
$ echo "hello, git" > index.html
# or
$ touch index.html; vim index.html
$ git status
```
交給 Git
把這個檔案交給 Git,讓 Git 開始「追蹤」它
```bash
$ git add index.html
$ git status
```
* [狀況](#problem-add-after):如果在 git add 之後又修改了那個檔案的內容?
## 把暫存區的內容提交到倉庫裡存檔
僅是透過 `git add` 指令把異動加到暫存區是不夠的,這樣還不算是完成整個流程。要讓暫存區的內容永久的存下來的話,使用的是 `git commit` 指令:
```bash
$ git commit -m "init commit"
```
在後面加上的 -m "init commit" 是指要要說明「你在這次的 Commit 做了什麼事」,只要使用簡單、清楚的文字說明就好,中、英文都可,重點是清楚,讓不久之後的你或是你的同事能很快的明白就行了。(一開始這樣就可以了)
* [那個訊息是什麼?很重要嗎?](#commmit-message)
當完成了這個動作後,對 Git 來說就是「把暫存區的東西存放到儲存庫(Repository)裡」
無意義的語法
```bash
$ git commit --allow-empty -m "空的"
```
### 什麼時候要 Commit?
這個問題沒有標準答案,你可以很多檔案修改好再一口氣全部一起 Commit,也可只改一個字就 Commit。常見的 Commit 的時間點有:
1. 完成一個任務的時候:不管是大到完成一整個電子商務的金流系統,還是小至只加了一個頁面甚至只是改幾個字,都算是任務。
2. 下班的時候:雖然可能還沒完全搞定任務,但至少先 Commit 今天的進度,除了備份之外,也讓公司知道你今天有在努力工作。(然後帶回家繼續苦命的做?)
3. 你想要 Commit 的時候就可以 Commit。
### 檢視紀錄
每一筆 commit 都有一個世界上獨一無二的身份號碼,由SHA-1(Secure Hash Algorithm 1)演算法所計算的結果
```bash
$ git log
```
1. Commit 作者是誰。(人是誰殺的)
2. 什麼時候 Commit 的。(什麼時候殺的)
3. 每次的 Commit 大概做了些什麼事。(怎麼殺的)
* [狀況](#check):我想要找某個人或某些人的 Commit…
* [狀況](#check):我想要找 Commit 訊息裡面有在罵髒話的
* [狀況](#check):你再混嘛!我看看你今天早上 Commit 了什麼!
</br>
* [狀況](#commit-than-change):如何在 Git 裡刪除檔案或變更檔名?
* [狀況](#change-commit):修改 Commit 紀錄
* [狀況](#add-in-commit):追加檔案到最近一次的 Commit
* [狀況](#add-dir):新增目錄?
* [狀況](#ignore):有些檔案我不想放在 Git 裡面…
* [狀況](#check-some-commit):檢視特定檔案的 Commit 紀錄
* [狀況](#someone-rewrite):等等,這行程式誰寫的?
* [狀況](#Oop-delele):啊!不小心把檔案或目錄刪掉了…
* [狀況](#reset-commit):剛才的 Commit 後悔了,想要拆掉重做…
* [狀況](#after-reset-save):不小心使用 hard 模式 Reset 了某個 Commit,救得回來嗎?
### 為什麼要使用分支?
在開發的過程中,一路往前 Commit 也沒什麼問題,但當開始越來越多同伴一起在同一個專案工作的時候,可能就不能這麼隨興的想 Commit 就 Commit,這時候分支就很好用。例如想要增加新功能,或是修正 Bug,或是想實驗看看某些新的做法,都可以另外做一個分支來進行,待做完確認沒問題之後再**合併**回來,不會影響正在運行的產品線。
在多人團隊共同開發的時候,甚至也可引入像 Git Flow 之類的開發流程,讓同一個團隊的人都可以用相同的方式進行開發,減少不必要的溝通成本。
[如何開 Branch 分支](#branch)
* [冷知識](#cheaper):為什麼大家都說在 Git 開分支「很便宜」?
分枝就只是某個 Commit 的 SHA-1 值而已!就像貼紙一樣,也就是這 40 個字元
```bash
$ git branch
$ cat .git/refs/heads/master
# 刪除分枝
$ rm .git/refs/heads/dog
# 幫分之改名
$ mv .git/refs/heads/cat .git/refs/heads/bird
```
### 合併分支
任務執行的差不多了,就要準備合併回來了。如果我想要 master 分支來合併 cat 分支的話,我會先切回 master 分支:
```bash
$ git checkout master
# 將 cat 合併到 master 上
# 快轉模式(Fast Forward)直接收割 cat 成果
$ git merge cat
```
可能會遇到以下狀況
(想想看要你把你的家產跟你哥哥或姐姐的家產合併在一起…)
![Imgur](https://i.imgur.com/o9n2aG3.png)
假設我想用 cat 分支來合併 dog 分支:
```
# 非快轉模式
$ git merge dog
```
![Imgur](https://i.imgur.com/JXBW3bk.png)
事實上不管誰合併誰,這兩個分支上的 Commit 都是對等的。硬是要說哪裡不一樣,就是 cat 分支合併 dog 分支的時候,cat 分支會往前移動,反之亦然。不過前面曾經提到分支就像貼紙一樣,隨時要刪掉或改名都不會影響現在已經存在的 Commit。
### 硬要小耳朵
```
$ git merge cat --no-ff
```
* [狀況](#branch-merge-?):合併過的分支要留著嗎?
* [狀況](#delete-branch):不小心把還沒合併的分支砍掉了,救得回來嗎?
### 另一種合併方式(使用 rebase)- 用 rebase 來避免不必要的 merge 操作
前面介紹了使用 git merge 指令來合併分支,接下來介紹另一種合併分支的方式。假設我們現在的狀態是這樣:
![Imgur](https://i.imgur.com/o9n2aG3.png)
從字面上來看,「rebase」是「re」加上「base」,翻成中文大概是「重新定義分支的參考基準」的意思。
所謂「base」就是指「你這分支是從哪裡生出來的」,以上面這個例子來說,cat 跟 dog 這兩個分支的 base 都是 master。接著我們試著使用 git rebase 指令來「組合」cat 跟 dog 這兩個分支:
```bash
$ git rebase dog
```
這個指令翻成白話文,大概就是「我,就是 cat 分支,我現在要重新定義我的參考基準,並且將使用 dog 分支當做我新的參考基準」的意思。
![Imgur](https://i.imgur.com/gypRII2.png)
1. 「我先拿 c68537 這個 Commit 接到 053fb2 這個 Commit 上」,因為 c68537 原本的上一層 Commit 是 e12d8e,現在要接到 053fb2 上,所以需要重新計算這個 Commit 的 SHA-1 值,重新做出一顆新的 Commit 物件 35bc96。
2. 「我再拿 b174a5 這個 Commit 接到剛剛那個新做出來的 Commit 物件 35bc96 上」,同理,因為 b174a5 這顆 Commit 要接到新的 Commit 的原因,所以它也會重新計算 SHA-1 值,得到一個新的 Commit 物件 28a76d。
3. 最後,原本的 cat 是指向 b174a5 這個 Commit,現在要改指向最後做出來的那顆新的 Commit 物件 28a76d。
4. HEAD 還是繼續指向 cat 分支。
==那原本舊的那些…?==
原本的那兩個 Commit(灰色),也就是 c68537 跟 b174a5 這兩個,他們的下場會是怎麼樣?
是也不會怎麼樣,反正他們就還是在 Git 的空間裡佔有一席之地,只是因為它已經沒有分支指著它,如果沒有特別去記他們這兩個 Commit 的 SHA-1 值,就會慢慢被邊緣化了吧。
```bash
$ git reflog
28a76dc (HEAD -> cat) HEAD@{0}: rebase finished: returning to refs/heads/cat
28a76dc (HEAD -> cat) HEAD@{1}: rebase: add cat 2
35bc96e HEAD@{2}: rebase: add cat 1
053fb21 (dog) HEAD@{3}: rebase: checkout dog
b174a5a HEAD@{4}: checkout: moving from master to cat
e12d8ef (master) HEAD@{5}: checkout: moving from new_cat to master
b174a5a HEAD@{6}: checkout: moving from master to new_cat
e12d8ef (master) HEAD@{7}: checkout: moving from new_cat to master
b174a5a HEAD@{8}: checkout: moving from master to new_cat
...[略]...
```
```
$ git reset b174a5a --hard
```
這就一來就會回到 Rebase 前的狀態了。
### 使用 ORIG_HEAD
在 Git 有另一個特別的紀錄點叫做 ORIG_HEAD,這個 ORIG_HEAD 會記錄「危險操作」之前 HEAD 的位置。例如**分支合併**或是 **Reset** 之類的都算是所謂的「危險操作」。透過這個紀錄點來取消這次 Rebase 相對的更簡單:
```bash
$ git rebase dog
$ git reset ORIG_HEAD --hard
```
### 合併發生衝突了,怎麼辦?
```bash
$ git merge dog
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
# 看哪一支檔案衝突
$ git status
```
收動改啊!幹
修改完後,別忘了把這個檔案加回暫存區:
```bash
$ git add .
$ git commit -m "conflict fixed"
```
### 如果是使用 Rebase 的合併造成衝突?
```bash
$ git rebase dog
First, rewinding head to replay your work on top of it...
Applying: add cat 1
Applying: add cat 2
Applying: add 123
Applying: update index
Using index info to reconstruct a base tree...
M index.html
Falling back to patching base and 3-way merge...
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
error: Failed to merge in the changes.
Patch failed at 0004 update index
The copy of the patch that failed is found in: .git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
```
用圖例更明白:
![Imgur](https://i.imgur.com/InilRsC.png)
```bash
$ git status
rebase in progress; onto ed06d49
You are currently rebasing branch 'cat' on 'ed06d49'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
```
訊息寫著 rebase in progress,而且那個 index.html 的確也是被標記成「both modified」狀態。跟上面提到的方法一樣,把 index.html 有衝突的內容修正完成後,把它加回暫存區:
```bash
$ git add index.html
$ git rebase --continue
```
### 那如果不是文字檔的衝突怎麼解?
```bash
$ git merge dog
warning: Cannot merge binary files: cute_animal.jpg (HEAD vs. dog)
Auto-merging cute_animal.jpg
CONFLICT (add/add): Merge conflict in cute_animal.jpg
Automatic merge failed; fix conflicts and then commit the result.
```
還是手動啦幹!選要哪一張圖片
```bash
$ git checkout --ours cute_animal.jpg
```
### Git 怎麼知道現在是在哪一個分支?
```bash
$ cat .git/HEAD
```
### 我可以從過去的某個 Commit 再長一個新的分支出來嗎?
[可以](#branch)
### 把多個 Commit 合併成一個 Commit
有時候 Commit 的太過「瑣碎」,舉個例子來說:
```bash
$ git log --oneline
27f6ed6 (HEAD -> master) add dog 2
2bab3e7 add dog 1
ca40fc9 add 2 cats
1de2076 add cat 2
cd82f29 add cat 1
382a2a5 add database settings
bb0c9c2 init commit
```
在 `cd82f29` 跟 `1de2076` 這兩個 Commit 都只有各加一個檔案(分別是 cat1.html 跟 cat2.html),`2bab3e7` 跟 `27f6ed6` 也一樣,都只各加了一個檔案而已。如果想把這幾個 Commit 合併成一個,會讓 Commit 看起來更乾淨一些。同樣可以使用互動模式的 Rebase 來處理:
```bash
$ git rebase -i bb0c9c2
```
彈出 vim 視窗
```
pick 382a2a5 add database settings
pick cd82f29 add cat 1
pick 1de2076 add cat 2
pick ca40fc9 add 2 cats
pick 2bab3e7 add dog 1
pick 27f6ed6 add dog 2
# Rebase bb0c9c2..27f6ed6 onto bb0c9c2 (6 commands)
#
# Commands:
# ...[略]...
```
指令是 `squash`,把上面的內容修改成這樣:
```bash
pick 382a2a5 add database settings
pick cd82f29 add cat 1
squash 1de2076 add cat 2
squash ca40fc9 add 2 cats
pick 2bab3e7 add dog 1
squash 27f6ed6 add dog 2
```
1. 最後一行的 `27f6ed6` 會跟前一個 Commit `2bab3e7` 進行合併,也就是 add dog 1 跟 add dog 2 這個 Commit 會合在一起。
2. 倒數第三號的 `ca40fc9` 會跟前一個 Commit `1de2076` 合併,但因為 `1de2076` 又會再往前一個 Commit `cd82f29` 合併,所以整個跟 cat 有關的這三個 Commit 會併成同一個。
存檔並離開 Vim 編輯器後,它會開始進行 Rebase,而在 Squash 的過程中,它還會跳出 Vim 編輯器讓你編輯一下訊息:
![Imgur](https://i.imgur.com/1q2vPlt.png)
更改為
![Imgur](https://i.imgur.com/Rgh1fcV.png)
相對的另一個 commit msg 改成 all dog
![Imgur](https://i.imgur.com/SL0FZNJ.png)
## Git 基本指令 & 流程
![](https://i.imgur.com/XAqyD99.png)
* Workspace:工作目錄
* Index / Stage:索引
* Repository:本地數據庫
* Remote:遠端數據庫
* Stash:暫存區
### 常用 Git 指令介紹
> 下指令前請先想好,免得被隊友殺掉
1. 基礎設定
```bash
# 查詢版本
$ git version
# 查詢設定列表
$ git config --list
# 編輯Git配置文件
$ git config -e --global
# 輸入姓名
$ git config --global user.name "你的名字"
# 輸入email
$ git config --global user.email "你的email"
```
2. 新增本地/遠端數據庫
```bash
# 在本地資料夾新增數據庫
$ git init
# 複製遠端數據庫
$ git clone 遠端數據庫網址
```
==.git 資料夾:介紹==
3. 增加/刪除檔案到索引
```bash
# 增加檔案進入索引
$ git add 檔案名稱
# 增加全部檔案進入索引
$ git add .
$ git add --all
# 删除工作目錄文件,並將此次刪除加入索引
$ git rm 檔案名稱 ...
# 停止追蹤指定文件,但該文件會保留在工作目錄
$ git rm --cached 檔案名稱
```
4. 提交到本地端數據庫
```bash
# 將索引提交到數據庫
$ git commit -m '更新訊息'
# 提交自上次工作目錄 commit 之後的變化,直接到本地數據庫
$ git commit -a -m "update content"
# 提交時顯示所有 diff 訊息
$ git commit -v
# 使用一次新的 commit,替代上一次提交
# 如果 code 沒任何變化,則改寫上一次 commit 的提交訊息
$ git commit --amend -m '更新訊息'
# 重做上一次commit,包括指定文件的新變化
$ git commit --amend 檔案名稱 ...
```
5. 還原指令
```bash
# 還原工作目錄與索引,會跟最後一次 commit 保持一樣
$ git reset --hard
# 全部檔案取消索引
$ git reset HEAD
# 單一檔案取消索引
$ git reset HEAD 檔案名稱
# 恢復單一檔案到最新 commit 狀態
$ git checkout 檔案名稱
# 恢復某個 commit 的指定文件到索引和工作目錄
$ git checkout [commit] [file]
# 恢复索引的所有文件到工作目錄
$ git checkout .
# 刪除最近一次 commit
$ git reset --hard "HEAD^"
# 上面語法如果刪除錯了可以再用此語法還原
$ git reset --hard ORIG_HEAD
# 刪除最近一次 commit,但保留異動內容
$ git reset --soft "HEAD^"
# commit 後發現有幾個檔案忘了加入進去,想要補內容進去時
$ git commit --amend
```
6. <span id="branch">分支</span>
```bash
# 顯示所有本地分支
$ git branch
# 顯示所有遠端分支
$ git branch -r
# 顯示所有本地和遠端分支
$ git branch -a
# 新增分支
$ git branch 分支名稱
# 把 cat 分支改成 tiger 分支
# 即使是 master 分支想改也可以改
$ git branch -m cat tiger
# 切換分支
$ git checkout 分支名稱
# 新建一个分支,並切換到新分支
$ git checkout -b 分支名稱
# 切换到上一个分支
$ git checkout -
# 新建一个分支,指向指定 commit
$ git branch 分支名稱 [commit]
# 新建一个分支,和指定的遠端分支建立追蹤關係
$ git branch --track [branch] [remote-branch]
# 建立追蹤關係,到現有分支和指定的遠端分支之間
$ git branch --set-upstream [branch] [remote-branch]
# 合併指定分支到目前的分支
$ git merge 分支名稱
# 刪除分支
$ git branch -d 分支名稱
# 分支的內容還沒被合併,所以使用 -d 參數不給刪
$ git branch -D cat
# 删除遠端分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]
```
==合併衝突==
7. 遠端數據庫操作
```bash
# 複製遠端數據庫
$ git clone 遠端數據庫網址
# 查詢遠端數據庫
$ git remote -v
# 顯示某個遠端數據庫的信息
$ git remote show 遠端數據庫名稱
# 將本地分支推送到遠端分支
$ git push 遠端數據庫名稱 遠端分支名稱
# 強制 push 目前分支到遠端數據庫,即使有衝突
# 沒事不要亂搞
$ git push [remote] --force
# 下载遠端數據庫的所有變動
$ git fetch 遠端數據庫名稱
# 將遠端分支拉下來與本地分支進行合併
$ git pull
```
==-u:[?](https://www.zhihu.com/question/20019419)==
==[git push pull 默認行為](https://segmentfault.com/a/1190000002783245)==
==github: origin遠端數據庫預設名稱==
pull = fetch + merge
==[.gitignore 大全](https://github.com/github/gitignore)==
8. 標籤
```bash
# 查詢標籤
$ git tag
# 查詢詳細標籤
$ git tag -n
# 查看 tag 訊息
$ git show [tag]
# 刪除標籤
$ git tag -d 標籤名稱
# 删除遠端 tag
$ git push origin :refs/tags/[tagName]
# 提交指定 tag
$ git push [remote] [tag]
# 新增輕量標籤
$ git tag 標籤名稱
# 新增標示標籤
$ git tag -am "備註內容" 標籤名稱
# 新建一個分支,指向某個 tag
$ git checkout -b [branch] [tag]
```
==[版本號命名規則](http://www.slmt.tw/blog/2015/07/20/version-number-naming-convention/)==
9. 暫存
```bash
# 暫時儲存當前目錄
$ git stash
# 瀏覽 stash 列表
$ git stash list
# 還原暫存
$ git stash pop
# 清除最新暫存
$ git stash drop
# 清除全部暫存
$ git stash clear
```
10. <a id="check">查看</a>
```bash
# 查詢狀態
$ git status
# 顯示歷史紀錄
$ git log
# 顯示 commit 歷史,以及每次 commit 發生變更的文件
$ git log --stat
# 顯示簡易的圖形化分支
$ git log --oneline --graph
# 找出特定時間的 commit 紀錄
$ git log --oneline --since="9am" --until="12am" --after="2018-01-26"
# 關鍵字找 commit 紀錄
$ git log --oneline --grep="wtf"
# 找某人 commit 紀錄
$ git log --oneline --author="jayHuang0728" -2
# 所有參與用戶提交次數
$ git shortlog -sn
# 顯示指定文件被哪個人哪個時間改過
$ git blame [file]
# 顯示索引和工作目錄差異
$ git diff
# 顯示目前工作目錄和當前最新一次 commit 之間差異
$ git diff HEAD
# 2次 commit 差異
$ git diff [first-branch]...[second-branch]
```
Fork & Pull request
Collaborators
team?
Upstream Pull Request
==fork Upstream==
!!!==git rebase==
https://www.peterdavehello.org/2014/02/update_forked_repository/
## Git Flow: 專案版本控制策略
![Imgur](https://i.imgur.com/cuipDfb.png)
### 主要 branch
* master: 主要版本,只接受 develop 和 Release 的 merge
* develop: 所有 Feature 開發都從這分支出去,完成後 merge 回來
### 支援 branch
* feature branches:從 develop 分支出來,當功能開發修改完成後 merge 回 develop
* release branches:從 develop 分支出來,是準備釋出的版本,只修改版本號與 bug,完成後 merge 回 develop 與 master,並在 master 標上版本號的 tag
* hotfix branches:從 master 分支出來,主要是處理已釋出版本需要立即修改的錯誤,完成後 merge 回 develop 與 master,並在 master 標上版本號的 tag
## 問題大集合
### <span id = "problem-add-after">如果在 git add 之後又修改了那個檔案的內容?</span>
想像一下這個情境:
1. 你新增了一個檔案叫做 abc.txt。
2. 然後,執行 git add abc.txt 把檔案加至暫存區。
3. 接著編輯 abc.txt 檔案。
接著你可能會想要進行 Commit,把剛剛修改的內容存下來。這是新手可能會犯的錯誤之一,以為 Commit 指令就會把所有的異動都存下來,事實上這樣的想法是不太正確的。執行一下 git status 指令,看一下目前的狀態:
```bash
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: abc.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: abc.txt
```
### <span id = "commmit-message">如果在 git add 之後又修改了那個檔案的內容?</span>
對,很重要!很重要!很重要!(因為重要所以要說三次)
在 Commit 的時候,如果沒有輸入這個訊息,Git 預設是不會讓你完成 Commit 這件事的。它最主要的目的就是告訴你自己以及其它人「這次的修改做了什麼」。以下是幾點關於訊息的建議:
儘量不要使用太過情緒性的字眼(我知道開發者有時候工作會有低潮或遇到澳州來的客人),避免不必要的問題。
英文、中文都沒關係,重點是要簡單、清楚。
儘量不要使用像 bug fixed 這樣模糊的描述,因為沒人知道你修了什麼 bug。但如果有搭配其它的系統使用,則可使用 #34 bug fixed,因為這樣可以知道這次的 Commit 修正了第 34 號的 bug。
==好的 [commit message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/) 很重要==,原則如下:
1. 用一行空白行分隔標題與內容
2. 限制標題最多只有 50 字元
3. 標題開頭要大寫
3. 標題不以句點結尾
4. 以祈使句撰寫標題
6. 內文每行最多 72 字
7. 用內文解釋 what 以及 why vs. how
參考其他開源專案:
* [angular](https://github.com/angular/angular/commits/master)
* [vue](https://github.com/vuejs/vue/commits/dev)
* [django](https://github.com/django/django/commits/master)
</br>
* [nodejs](https://github.com/nodejs/node/issues)
* [golang](https://github.com/golang/go/commits/master)
* [meteor](https://github.com/meteor/meteor/commits/devel)
commit-than-change
### <span id = "commit-than-change">如何在 Git 裡刪除檔案或變更檔名?</span>
請 Git 幫你砍
```bash
$ rm welcome.html
$ git add welcome.html
# 以上可以合併如下,使用 Git刪檔案
$ git rm welcome.html
```
不是真的想把這個檔案刪掉,只是不想讓這個檔案再被 Git 控管了
```bash
$ git rm welcome.html --cached
```
請 Git 幫你改名
```bash
# 把 hello.html 改成 world.html
$ git mv hello.html world.html
```
### <span id = "change-commit">修改 Commit 紀錄</span>
要修改 Commit 紀錄有好幾種方法:
1. 把 `.git` 目錄整個刪除...
2. 使用 `git rebase` 來修改歷史。
3. 先把 Commit 用 `git reset` 拆掉,整理後再重新 Commit。
4. 使用 `--amend` 參數來修改最後一次的 Commit。
```bash
$ git log --oneline
$ git commit --amend -m "Welcome To Facebook"
```
### <span id = "add-in-commit">追加檔案到最近一次的 Commit</span>
```bash
# --no-edit我不要編輯 Commit 訊息
$ git add cinderella.html
$ git commit --amend --no-edit
```
### <span id = "add-dir">新增目錄?</span>
==空的目錄無法被提交!==
### <span id = "ignore">有些檔案我不想放在 Git 裡面…</span>
```bash
# 忽略 secret.yml 檔案
secret.yml
# 忽略 config 目錄下的 database.yml 檔案
config/database.yml
# 忽略所有 db 目錄下附檔名是 .sqlite3 的檔案
/db/*.sqlite3
# 忽略所有附檔名是 .tmp 的檔案
*.tmp
```
雖然 `.gitignore` 這個檔案有列了一些忽略的規則,但其實也是可以忽略這個忽略的規則,強迫闖關
```bash
$ git add -f 檔案名稱
```
==`.gitignore` 檔案設定的規則,只對在規則設定之後的有效,那些已經存在的檔案就像既得利益者一樣,這些規則是對他們沒有效果的。==
清除忽略的檔案
```bash
$ git clean -fX
```
### <span id = "check-some-commit">檢視特定檔案的 Commit 紀錄</span>
```bash
# 檢視單一檔案的紀錄
$ git log welcome.html
# 這個檔案到底每次的 Commit 做了什麼修改
$ git log -p welcome.html
```
### <span id = "someone-rewrite">等等,這行程式誰寫的?</span>
```bash
$ git blame index.html
# 檔案太大,可以指定行數
$ git blame -L 5,10 index.html
```
### <span id = "Oop-delele">啊!不小心把檔案或目錄刪掉了…</span>
```bash
$ rm *.html
# 救某檔案
$ git checkout cinderella.html
# or 全部救回來
$ git checkout .
```
### <span id = "reset-commit">剛才的 Commit 後悔了,想要拆掉重做…</span>
在最後面的那個 `^` 符號,每一個 `^` 符號表示「前一次」的意思
```bash
$ git log --oneline
# 相對:回此 commit 的上一步
$ git reset [commit]^
$ git reset HEAD^
# 絕對:直接回到那一步 commit
$ git reset [commit]
# 請幫我拆掉最後兩次的 Commit
$ git reset HEAD~2
```
<a style="color:red">重要</a>:Reset 的模式
`git reset` 指令可以搭配參數使用,常見到的三種參數,分別是 `--mixed`、`--soft` 以及 `--hard`,不同的參數執行之後會有稍微不太一樣的結果。
* mixed 模式
`--mixed` 是預設的參數,如果沒有特別加參數,`git reset` 指令將會使用 `--mixed` 模式。這個模式會把暫存區的檔案丟掉,但不會動到工作目錄的檔案,也就是說 Commit 拆出來的檔案會留在工作目錄,但不會留在暫存區。
* soft 模式
這個模式下的 reset,工作目錄跟暫存區的檔案都不會被丟掉,所以看起來就只有 HEAD 的移動而已。也因此,Commit 拆出來的檔案會直接放在暫存區。
* hard 模式
在這個模式下,不管是工作目錄以及暫存區的檔案都會丟掉。
![Imgur](https://i.imgur.com/40GSiXN.png)
另一個表格來解釋:
![Imgur](https://i.imgur.com/Fvwmloi.png)
### <span id = "after-reset-save">不小心使用 hard 模式 Reset 了某個 Commit,救得回來嗎?</span>
```bash
$ git log --oneline
e12d8ef (HEAD -> master) add database.yml in config folder
85e7e30 add hello
657fce7 add container
abb4f43 update index page
cef6e40 create index page
cc797cd init commit
```
```bash
# 拆前2個 commit
$ git reset HEAD~2
```
這時候 Commit 看起來就會少兩個,同時拆出來的檔案會被放置在工作目錄:
```bash
$ git log --oneline
657fce7 (HEAD -> master) add container
abb4f43 update index page
cef6e40 create index page
cc797cd init commit
```
```bash
# 強制救回 2 個 commit
$ git reset e12d8ef --hard
```
如果一開始沒有記下來 Commit 的 SHA-1 值也沒關係,Git 裡有個 reflog 指令有保留一些紀錄。再次借用上個章節的例子,但這次我改用 --hard 模式來進行 reset:
```bash
$ git reset HEAD~2 --hard
HEAD is now at 657fce7 add container
```
不僅 Commit 看起來不見了,檔案也消失了。接著可使用 reflog 指令來看一下紀錄:
```bash
$ git reflog
657fce7 (HEAD -> master) HEAD@{0}: reset: moving to HEAD~2
e12d8ef (origin/master, origin/HEAD, cat) HEAD@{1}: checkout: moving from cat to master
e12d8ef (origin/master, origin/HEAD, cat) HEAD@{2}: checkout: moving from master to cat
```
當 HEAD 有移動的時候(例如切換分支或是 reset 都會造成 HEAD 移動),Git 就會在 Reflog 裡記上一筆。從上面的這三筆記錄看起來大概可以猜得出來最近三次 HEAD 的移動,而最後一次的動作就是 Reset。
```bash
$ git reset e12d8ef --hard
```
就可以把剛剛 hard reset 的東西再次撿回來了。
### <span id = "branch-merge-?">合併過的分支要留著嗎?</span>
分支只要經過合併,合併過就代表「這些內容本來只有你有,現在我也有了
既然合併過之後,原本沒有的內容我都有了,分支本身又像一張貼紙一樣沒有舉足輕重的地位,所以老實說它已經沒有利用價值。
你想要刪掉,或是已經跟這個分支建立感情了,想留著做紀念也可,都好。
<span id = "delete-branch">不小心把還沒合併的分支砍掉了,救得回來嗎?</span>
###
![Imgur](https://i.imgur.com/XcXs554.png)
cat 分支是從 master 分支出去的,目前領先 master 分支兩次 Commit,而且也還沒有合併。這時候如果試著刪掉 cat 分支,它會提醒你:
```bash
$ git branch -d cat
error: The branch 'cat' is not fully merged.
If you are sure you want to delete it, run 'git branch -D cat'.
```
「嘿!這個分支還沒全部合併喔」,雖然 Git 這麼貼心的提醒你,你還是依舊把它砍了:
```bash
$ git branch -D cat
Deleted branch cat (was b174a5a).
```
> 分支只是一個指向某個 Commit 的指標,刪除這個指標並不會造成那些 Commit 消失。
所以,刪掉分支,那些 Commit 還是在,只是因為你可能不知道或沒記下那些 Commit 的 SHA-1 值,所以不容易再拿來利用。現在原本領先 master 分支的那兩個 Commit 就跟空氣一樣,你看不到空氣,但空氣是存在的。既然它還存在,那就把它「接回來」吧:
```bash
git branch new_cat b174a5a
```
這個指令的意思是「請幫我建立一個叫做 new_cat 的分支,讓它指向 `b174a5a` 這個 Commit」,就是再去拿一張新的貼紙貼回去的意思啦。
> 分支只是一個指向某個 Commit 的指標。
## 補充
### 使用平台 & 工具
* [Slack]
* [Github]
* [Sourcetree]
* [Github Desktop]
### 學習資源
* [連猴子都能懂的Git入門](https://backlog.com/git-tutorial/tw/)
* [Git flow](http://nvie.com/posts/a-successful-git-branching-model/)
* [鋒哥大全](http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html)
* [為你自己學 Git](https://gitbook.tw/)
### 練習工具
* [Code School - Try Git](https://try.github.io/levels/1/challenges/2)
[Github]: https://github.com/
[Slack]: https://slack.com/
[Sourcetree]: https://www.sourcetreeapp.com/
[Github Desktop]: https://desktop.github.com/