Try   HackMD

Git 讀書會

版本控制有很多方式可以實現,像是svn(Subversion)、csv、git等等
我認為學版本控制,學的不是這些指令,背後更重要的是團隊文化和規則
一個簡單的例子,今天是誰改的這行程式,你要告訴我你改什麼
改的程式碼commit的訊息要怎麼寫
為什麼要這樣改,這樣我程式碼合併程式碼會出問題嗎
所以為了不必要的後續麻煩
這都是要團隊先訂定規則,還有coding style等等
這才是版本控制最重要的核心
不過首先還是先學指令和流程吧 XD

事前準備

什麼是 Git?為什麼要學習它?

如果你問大部份正在使用 Git 這個工具的人「什麼是 Git」,他們大多可能會回答你「Git 是一種版本控制系統」,專業一點的可能會回答你說「Git 是一種分散式版本的版本控制系統」

什麼是版本控制?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

如圖所示,隨著時間的變化,一開始這個 resume 目錄裡只有 3 個檔案,過兩天增加到 5 個。不久之後,其中的 2 個被修改了,過了三個月後又增加到 7 個,最後又刪掉了 1 個,變成 6 個。這每一個「resume 目錄的狀態變化」,不管是新增或刪除檔案,亦或是修改檔案內容,都稱之為一個「版本」,例如上圖圖例的版本 1 ~ 5。而所謂的「版本控制系統」,就是指會幫你記錄這些所有的狀態變化,並且可以像搭乘時光機一樣,隨時切換到過去某個「版本」時候的狀態。

簡單的說,Git 就像玩遊戲的時候可以儲存進度一樣。舉例來說,為了避免打頭目打輸了而損失裝備,又或是打倒頭目卻沒有掉落期望的珍貴裝備,你也許在每次要去打頭目之前之前記錄一下,在發生狀況的時候可以載入舊進度,再來挑戰一次。

分散又是什麼?

Git 是分散式的版本控制系統,就算在深山裡或飛機上沒有網路可使用,也可正常的使用 Git,待有網路的時候再與其它人同步即可。Git 大部份的操作都是在自己電腦上就可完成,而且不管是遠端的伺服器或是自己的電腦,在同步之後大家都會有一份完整的檔案及歷史紀錄。

SVN 之類的集中式的版控系統(Centralize Version Control),都需要有一台專用的伺服器,所有的更新都需要跟這台伺服器溝通。也就是說,萬一這台伺服器壞了,或是沒有網路連線的環境,就沒辦法使用。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

找出老鼠屎

可以清楚的記錄每個檔案是誰在什麼時候加進來、什麼時候被修改或刪除。
出社會工作,有 Git 幫你保留這些歷史紀錄跟證據,萬一出事的時候你就能知道是從什麼時候開始就有問題,以及知道該找誰負責,不用自己背黑鍋!

處理檔案的方式

Git 是差異化備份,而不是完整備份

Git 與其它版控系統最大的差異,是在於處理檔案的方式。其它家的版控系統,大多是記錄每個版本之間的「異動」:
類似以下,沒有結構的有像圖

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

小總結 - Git 優缺

  • pros

    • 免費、開源
    • 速度快、檔案體積小
    • 分散式系統
  • cons

    • 易學難精:指令多,but 大概 20% 的指令就足以應付 80% 的工作 (80/20 法則)

Start Git

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

新增、初始 Repository

使用 git init 指令初始化這個目錄,主要目的是要讓 Git 開始對這個目錄進行版本控制,在當下資料夾產生 .git資料夾

$ mkdir git-practice
$ cd git-practice
$ git init

Git 完全就是只靠那個 .git 目錄在做事而已,也就是說,整個專案目錄裡,什麼檔案或目錄刪了都救得回來,但 .git 目錄只要刪了就沒辦法了。

把檔案交給 Git

用此命令查看目前這個目錄的「狀態」

$ git status

新增檔案

$ echo "hello, git" > index.html
# or
$ touch index.html; vim index.html
$ git status

交給 Git
把這個檔案交給 Git,讓 Git 開始「追蹤」它

$ git add index.html
$ git status
  • 狀況:如果在 git add 之後又修改了那個檔案的內容?

把暫存區的內容提交到倉庫裡存檔

僅是透過 git add 指令把異動加到暫存區是不夠的,這樣還不算是完成整個流程。要讓暫存區的內容永久的存下來的話,使用的是 git commit 指令:

$ git commit -m "init commit"

在後面加上的 -m "init commit" 是指要要說明「你在這次的 Commit 做了什麼事」,只要使用簡單、清楚的文字說明就好,中、英文都可,重點是清楚,讓不久之後的你或是你的同事能很快的明白就行了。(一開始這樣就可以了)

當完成了這個動作後,對 Git 來說就是「把暫存區的東西存放到儲存庫(Repository)裡」

無意義的語法

$ git commit --allow-empty -m "空的"

什麼時候要 Commit?

這個問題沒有標準答案,你可以很多檔案修改好再一口氣全部一起 Commit,也可只改一個字就 Commit。常見的 Commit 的時間點有:

  1. 完成一個任務的時候:不管是大到完成一整個電子商務的金流系統,還是小至只加了一個頁面甚至只是改幾個字,都算是任務。
  2. 下班的時候:雖然可能還沒完全搞定任務,但至少先 Commit 今天的進度,除了備份之外,也讓公司知道你今天有在努力工作。(然後帶回家繼續苦命的做?)
  3. 你想要 Commit 的時候就可以 Commit。

檢視紀錄

每一筆 commit 都有一個世界上獨一無二的身份號碼,由SHA-1(Secure Hash Algorithm 1)演算法所計算的結果

$ git log
  1. Commit 作者是誰。(人是誰殺的)
  2. 什麼時候 Commit 的。(什麼時候殺的)
  3. 每次的 Commit 大概做了些什麼事。(怎麼殺的)
  • 狀況:我想要找某個人或某些人的 Commit…
  • 狀況:我想要找 Commit 訊息裡面有在罵髒話的
  • 狀況:你再混嘛!我看看你今天早上 Commit 了什麼!

  • 狀況:如何在 Git 裡刪除檔案或變更檔名?
  • 狀況:修改 Commit 紀錄
  • 狀況:追加檔案到最近一次的 Commit
  • 狀況:新增目錄?
  • 狀況:有些檔案我不想放在 Git 裡面…
  • 狀況:檢視特定檔案的 Commit 紀錄
  • 狀況:等等,這行程式誰寫的?
  • 狀況:啊!不小心把檔案或目錄刪掉了…
  • 狀況:剛才的 Commit 後悔了,想要拆掉重做…
  • 狀況:不小心使用 hard 模式 Reset 了某個 Commit,救得回來嗎?

為什麼要使用分支?

在開發的過程中,一路往前 Commit 也沒什麼問題,但當開始越來越多同伴一起在同一個專案工作的時候,可能就不能這麼隨興的想 Commit 就 Commit,這時候分支就很好用。例如想要增加新功能,或是修正 Bug,或是想實驗看看某些新的做法,都可以另外做一個分支來進行,待做完確認沒問題之後再合併回來,不會影響正在運行的產品線。

在多人團隊共同開發的時候,甚至也可引入像 Git Flow 之類的開發流程,讓同一個團隊的人都可以用相同的方式進行開發,減少不必要的溝通成本。

如何開 Branch 分支

  • 冷知識:為什麼大家都說在 Git 開分支「很便宜」?
    分枝就只是某個 Commit 的 SHA-1 值而已!就像貼紙一樣,也就是這 40 個字元
$ git branch

$ cat .git/refs/heads/master

# 刪除分枝
$ rm .git/refs/heads/dog

# 幫分之改名
$ mv .git/refs/heads/cat .git/refs/heads/bird

合併分支

任務執行的差不多了,就要準備合併回來了。如果我想要 master 分支來合併 cat 分支的話,我會先切回 master 分支:

$ git checkout master

# 將 cat 合併到 master 上
# 快轉模式(Fast Forward)直接收割 cat 成果
$ git merge cat

可能會遇到以下狀況
(想想看要你把你的家產跟你哥哥或姐姐的家產合併在一起…)
Imgur

假設我想用 cat 分支來合併 dog 分支:

# 非快轉模式
$ git merge dog

Imgur

事實上不管誰合併誰,這兩個分支上的 Commit 都是對等的。硬是要說哪裡不一樣,就是 cat 分支合併 dog 分支的時候,cat 分支會往前移動,反之亦然。不過前面曾經提到分支就像貼紙一樣,隨時要刪掉或改名都不會影響現在已經存在的 Commit。

硬要小耳朵

$ git merge cat --no-ff
  • 狀況:合併過的分支要留著嗎?
  • 狀況:不小心把還沒合併的分支砍掉了,救得回來嗎?

另一種合併方式(使用 rebase)- 用 rebase 來避免不必要的 merge 操作

前面介紹了使用 git merge 指令來合併分支,接下來介紹另一種合併分支的方式。假設我們現在的狀態是這樣:

Imgur

從字面上來看,「rebase」是「re」加上「base」,翻成中文大概是「重新定義分支的參考基準」的意思。
所謂「base」就是指「你這分支是從哪裡生出來的」,以上面這個例子來說,cat 跟 dog 這兩個分支的 base 都是 master。接著我們試著使用 git rebase 指令來「組合」cat 跟 dog 這兩個分支:

$ git rebase dog

這個指令翻成白話文,大概就是「我,就是 cat 分支,我現在要重新定義我的參考基準,並且將使用 dog 分支當做我新的參考基準」的意思。

Imgur

  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 值,就會慢慢被邊緣化了吧。

$ 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 相對的更簡單:

$ git rebase dog
$ git reset ORIG_HEAD --hard

合併發生衝突了,怎麼辦?

$ 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

收動改啊!幹
修改完後,別忘了把這個檔案加回暫存區:

$ git add .
$ git commit -m "conflict fixed"

如果是使用 Rebase 的合併造成衝突?

$ 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

$ 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 有衝突的內容修正完成後,把它加回暫存區:

$ git add index.html
$ git rebase --continue

那如果不是文字檔的衝突怎麼解?

$ 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.

還是手動啦幹!選要哪一張圖片

$ git checkout --ours cute_animal.jpg

Git 怎麼知道現在是在哪一個分支?

$ cat .git/HEAD

我可以從過去的某個 Commit 再長一個新的分支出來嗎?

可以

把多個 Commit 合併成一個 Commit

有時候 Commit 的太過「瑣碎」,舉個例子來說:

$ 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

cd82f291de2076 這兩個 Commit 都只有各加一個檔案(分別是 cat1.html 跟 cat2.html),2bab3e727f6ed6 也一樣,都只各加了一個檔案而已。如果想把這幾個 Commit 合併成一個,會讓 Commit 看起來更乾淨一些。同樣可以使用互動模式的 Rebase 來處理:

$ 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,把上面的內容修改成這樣:

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

更改為

Imgur

相對的另一個 commit msg 改成 all dog

Imgur

Git 基本指令 & 流程

  • Workspace:工作目錄
  • Index / Stage:索引
  • Repository:本地數據庫
  • Remote:遠端數據庫
  • Stash:暫存區

常用 Git 指令介紹

下指令前請先想好,免得被隊友殺掉

  1. 基礎設定
# 查詢版本
$ git version

# 查詢設定列表
$ git config --list

# 編輯Git配置文件
$ git config -e --global

# 輸入姓名
$ git config --global user.name "你的名字"

# 輸入email
$ git config --global user.email "你的email"
  1. 新增本地/遠端數據庫
# 在本地資料夾新增數據庫
$ git init

# 複製遠端數據庫
$ git clone 遠端數據庫網址

.git 資料夾:介紹

  1. 增加/刪除檔案到索引
# 增加檔案進入索引
$ git add 檔案名稱

# 增加全部檔案進入索引
$ git add .
$ git add --all

# 删除工作目錄文件,並將此次刪除加入索引
$ git rm 檔案名稱 ...

# 停止追蹤指定文件,但該文件會保留在工作目錄
$ git rm --cached 檔案名稱
  1. 提交到本地端數據庫
# 將索引提交到數據庫
$ 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 檔案名稱 ...
  1. 還原指令
# 還原工作目錄與索引,會跟最後一次 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 
  1. 分支
# 顯示所有本地分支
$ 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]

合併衝突

  1. 遠端數據庫操作
# 複製遠端數據庫
$ git clone 遠端數據庫網址

# 查詢遠端數據庫
$ git remote -v

# 顯示某個遠端數據庫的信息
$ git remote show 遠端數據庫名稱

# 將本地分支推送到遠端分支
$ git push 遠端數據庫名稱 遠端分支名稱

# 強制 push 目前分支到遠端數據庫,即使有衝突
# 沒事不要亂搞
$ git push [remote] --force

# 下载遠端數據庫的所有變動
$ git fetch 遠端數據庫名稱

# 將遠端分支拉下來與本地分支進行合併
$ git pull

-u:?

git push pull 默認行為

github: origin遠端數據庫預設名稱

pull = fetch + merge

.gitignore 大全

  1. 標籤
# 查詢標籤
$ 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]

版本號命名規則

  1. 暫存
# 暫時儲存當前目錄
$ git stash

# 瀏覽 stash 列表
$ git stash list 

# 還原暫存
$ git stash pop

# 清除最新暫存
$ git stash drop

# 清除全部暫存
$ git stash clear
  1. 查看
# 查詢狀態
$ 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

主要 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

問題大集合

如果在 git add 之後又修改了那個檔案的內容?

想像一下這個情境:

  1. 你新增了一個檔案叫做 abc.txt。
  2. 然後,執行 git add abc.txt 把檔案加至暫存區。
  3. 接著編輯 abc.txt 檔案。

接著你可能會想要進行 Commit,把剛剛修改的內容存下來。這是新手可能會犯的錯誤之一,以為 Commit 指令就會把所有的異動都存下來,事實上這樣的想法是不太正確的。執行一下 git status 指令,看一下目前的狀態:

$ 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

如果在 git add 之後又修改了那個檔案的內容?

對,很重要!很重要!很重要!(因為重要所以要說三次)

在 Commit 的時候,如果沒有輸入這個訊息,Git 預設是不會讓你完成 Commit 這件事的。它最主要的目的就是告訴你自己以及其它人「這次的修改做了什麼」。以下是幾點關於訊息的建議:

儘量不要使用太過情緒性的字眼(我知道開發者有時候工作會有低潮或遇到澳州來的客人),避免不必要的問題。
英文、中文都沒關係,重點是要簡單、清楚。
儘量不要使用像 bug fixed 這樣模糊的描述,因為沒人知道你修了什麼 bug。但如果有搭配其它的系統使用,則可使用 #34 bug fixed,因為這樣可以知道這次的 Commit 修正了第 34 號的 bug。

好的 commit message 很重要,原則如下:

  1. 用一行空白行分隔標題與內容
  2. 限制標題最多只有 50 字元
  3. 標題開頭要大寫
  4. 標題不以句點結尾
  5. 以祈使句撰寫標題
  6. 內文每行最多 72 字
  7. 用內文解釋 what 以及 why vs. how

參考其他開源專案:

commit-than-change

如何在 Git 裡刪除檔案或變更檔名?

請 Git 幫你砍

$ rm welcome.html
$ git add welcome.html

# 以上可以合併如下,使用 Git刪檔案
$ git rm welcome.html

不是真的想把這個檔案刪掉,只是不想讓這個檔案再被 Git 控管了

$ git rm welcome.html --cached

請 Git 幫你改名

 # 把 hello.html 改成 world.html
$ git mv hello.html world.html

修改 Commit 紀錄

要修改 Commit 紀錄有好幾種方法:

  1. .git 目錄整個刪除
  2. 使用 git rebase 來修改歷史。
  3. 先把 Commit 用 git reset 拆掉,整理後再重新 Commit。
  4. 使用 --amend 參數來修改最後一次的 Commit。
$ git log --oneline
$ git commit --amend -m "Welcome To Facebook"

追加檔案到最近一次的 Commit

# --no-edit我不要編輯 Commit 訊息
$ git add cinderella.html
$ git commit --amend --no-edit

新增目錄?

空的目錄無法被提交!

有些檔案我不想放在 Git 裡面…

# 忽略 secret.yml 檔案
secret.yml

# 忽略 config 目錄下的 database.yml 檔案
config/database.yml

# 忽略所有 db 目錄下附檔名是 .sqlite3 的檔案
/db/*.sqlite3

# 忽略所有附檔名是 .tmp 的檔案
*.tmp

雖然 .gitignore 這個檔案有列了一些忽略的規則,但其實也是可以忽略這個忽略的規則,強迫闖關

$ git add -f 檔案名稱

.gitignore 檔案設定的規則,只對在規則設定之後的有效,那些已經存在的檔案就像既得利益者一樣,這些規則是對他們沒有效果的。

清除忽略的檔案

$ git clean -fX

檢視特定檔案的 Commit 紀錄

# 檢視單一檔案的紀錄
$ git log welcome.html

# 這個檔案到底每次的 Commit 做了什麼修改
$ git log -p welcome.html

等等,這行程式誰寫的?

$ git blame index.html

# 檔案太大,可以指定行數
$ git blame -L 5,10 index.html

啊!不小心把檔案或目錄刪掉了…

$ rm *.html

# 救某檔案
$ git checkout cinderella.html

# or 全部救回來
$ git checkout .

剛才的 Commit 後悔了,想要拆掉重做…

在最後面的那個 ^ 符號,每一個 ^ 符號表示「前一次」的意思

$ git log --oneline

# 相對:回此 commit 的上一步
$ git reset [commit]^
$ git reset HEAD^

# 絕對:直接回到那一步 commit
$ git reset [commit]

# 請幫我拆掉最後兩次的 Commit
$ git reset HEAD~2

重要:Reset 的模式
git reset 指令可以搭配參數使用,常見到的三種參數,分別是 --mixed--soft 以及 --hard,不同的參數執行之後會有稍微不太一樣的結果。

  • mixed 模式
    --mixed 是預設的參數,如果沒有特別加參數,git reset 指令將會使用 --mixed 模式。這個模式會把暫存區的檔案丟掉,但不會動到工作目錄的檔案,也就是說 Commit 拆出來的檔案會留在工作目錄,但不會留在暫存區。

  • soft 模式
    這個模式下的 reset,工作目錄跟暫存區的檔案都不會被丟掉,所以看起來就只有 HEAD 的移動而已。也因此,Commit 拆出來的檔案會直接放在暫存區。

  • hard 模式
    在這個模式下,不管是工作目錄以及暫存區的檔案都會丟掉。

Imgur

另一個表格來解釋:
Imgur

不小心使用 hard 模式 Reset 了某個 Commit,救得回來嗎?

$ 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
# 拆前2個 commit
$ git reset HEAD~2

這時候 Commit 看起來就會少兩個,同時拆出來的檔案會被放置在工作目錄:

$ git log --oneline
657fce7 (HEAD -> master) add container
abb4f43 update index page
cef6e40 create index page
cc797cd init commit
# 強制救回 2 個 commit
$ git reset e12d8ef --hard

如果一開始沒有記下來 Commit 的 SHA-1 值也沒關係,Git 裡有個 reflog 指令有保留一些紀錄。再次借用上個章節的例子,但這次我改用 hard 模式來進行 reset:

$ git reset HEAD~2 --hard
HEAD is now at 657fce7 add container

不僅 Commit 看起來不見了,檔案也消失了。接著可使用 reflog 指令來看一下紀錄:

$ 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。

$ git reset e12d8ef --hard

就可以把剛剛 hard reset 的東西再次撿回來了。

合併過的分支要留著嗎?

分支只要經過合併,合併過就代表「這些內容本來只有你有,現在我也有了

既然合併過之後,原本沒有的內容我都有了,分支本身又像一張貼紙一樣沒有舉足輕重的地位,所以老實說它已經沒有利用價值。

你想要刪掉,或是已經跟這個分支建立感情了,想留著做紀念也可,都好。

不小心把還沒合併的分支砍掉了,救得回來嗎?

Imgur

cat 分支是從 master 分支出去的,目前領先 master 分支兩次 Commit,而且也還沒有合併。這時候如果試著刪掉 cat 分支,它會提醒你:

$ 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 這麼貼心的提醒你,你還是依舊把它砍了:

$ git branch -D cat
Deleted branch cat (was b174a5a).

分支只是一個指向某個 Commit 的指標,刪除這個指標並不會造成那些 Commit 消失。

所以,刪掉分支,那些 Commit 還是在,只是因為你可能不知道或沒記下那些 Commit 的 SHA-1 值,所以不容易再拿來利用。現在原本領先 master 分支的那兩個 Commit 就跟空氣一樣,你看不到空氣,但空氣是存在的。既然它還存在,那就把它「接回來」吧:

git branch new_cat b174a5a

這個指令的意思是「請幫我建立一個叫做 new_cat 的分支,讓它指向 b174a5a 這個 Commit」,就是再去拿一張新的貼紙貼回去的意思啦。

分支只是一個指向某個 Commit 的指標。

補充

使用平台 & 工具

學習資源

練習工具