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