Skip to content

It's my own note for git. This repository is just for learning and making me more convenient to find out git CLI commands as well as git basic core concepts. Use Git v2.29.0 (for macOS Catalina 10.15.6).

Hans-Tsai/gitLearn

Repository files navigation

Git Learn
自主學習Git的觀念和指令後做的統整&學習筆記

目錄


安裝方式

Windows系統

  • 連結: https://git-scm.com/download/win
  • 安裝完後,使用Git Bash就可以操作Git了
  • $ which git // /mingw/bin/git
  • $ git --version // git version 2.28.0.windows.1
  • GUI client推薦: SourceTree, GitHub Desktop
  • 補充: Git Bash不同於Windows內建的"命令提示字元",它本身模擬了Linux的 Bash

MacOS系統

  • 連結: https://git-scm.com/download/mac
  • 或是利用Homebrew安裝
  • $ brew install git
  • 補充: Homebrew是一個MacOS專屬的套件管理包工具,有點像是Linux的apt-get之類的安裝工具,通常只要一行指令就可以完成下載.編譯.安裝
  • GUI client推薦: SourceTree, GitHub Desktop

Linux系統

  • 連結: https://git-scm.com/download/linux
  • 利用apt-get安裝
  • $ sudo apt-get install git // 在Linux系統中要安裝軟體要先切換成root權限
  • GUI client推薦: gitk
    • $ sudo apt-get install gitk

設定Git

git config - Get and set repository or global options
git log - Show commit logs

  • 所有Git相關的設定都會儲存在 ~/.gitconfig 這個檔案裡
  • 設定使用者的Email&username
    • $ git config --global user.name "Hans-Tsai"
    • $ git config --global user.email "lgs840522@gmail.com"
  • 檢視目前的設定
    • $ cat ~/.gitconfig
    • $ git config --list
  • 設定Git要使用的編輯器(預設是使用Vim)
    • $ git config --global core.editor emacs
  • 設定Git客製化縮寫指令
    • $ git config --global alias.co checkout
    • $ git config --global alias.br branch
    • $ git config --global alias.st status
    • $ git config --global alias.ls 'log --graph --pretty=format:"%h <%an> %ar %s"'

Git 基礎常用指令

git init - Create an empty Git repository or reinitialize an existing one
git status - Show the working tree status
git add - Add file contents to the index
git commit - Record changes to the repository
git log - Show commit logs
git rm - Remove files from the working tree and from the index
git mv - Move or rename a file, a directory, or a symlink
gitignore - Specifies intentionally untracked files to ignore
git clean - Remove untracked files from the working tree
git blame - Show what revision and author last modified each line of a file
git checkout - Switch branches or restore working tree files
git switch - Switch branches
git restore - Restore working tree files
git reset - Reset current HEAD to the specified state
git reflog - Manage reflog information
git tag - Create, list, delete or verify a tag object signed with GPG
git merge - Join two or more development histories together
git rebase - Reapply commits on top of another base tip
git revert - Revert some existing commits

初始化該目錄,主要目的是讓Git開始對這個目錄進行版本控制

  • $ git init
  • 會在該目錄裡面建立一個 .git/ 隱藏檔目錄,整個Git的精華都會在這個目錄裡面
  • 如果該目錄不想再被Git做版本控制,只要將.git/目錄整個刪除就可以
    • $ rm -rf .git/
    • 注意:整個專案目錄裡,什麼檔案或目錄刪了都救得回來,但 .git/ 目錄只要刪了就沒辦法了 !
  • 查詢現在這個目錄的"狀態"
    • $ git status
    • Untracked files => 代表這個檔案尚未被加到Git版控系統裡,還沒開始正式被"追蹤",它只是剛剛才加入到這個目錄裡而已
    • Untracked_files圖解說明

把這個檔案交給Git來控管

  • $ git add welcome.html
  • 剛才的檔案 welcome.html從Untracked變成new file狀態 => 表示這個檔案已經被安置到暫存區(Staging Area),等待稍後跟其他檔案一起被存到儲存庫裡面
  • git_add將檔案加入版控中圖解說明
  • 如果想將全部檔案一口氣加入暫存區,可以使用--all參數;不論檔案狀態是Untracked files或是Changes not staged for commit,都會一口氣變成Changes to committed
    • $ git add --all or $ git add -A
    • $ git add --all =>會將整個專案裡的全部異動檔案加到暫存區,不受限這個指令在哪一層目錄執行
    • $ git add .=>只會把當前執行command的那個目錄以及它的子目錄的異動檔案加到暫存區,所以在哪一層目錄執行這個command很重要
  • 如果想要取消剛剛加入到暫存區(Staging Area)的檔案的話,並將該檔案丟回到工作目錄(Working Directory)中,可以利用以下指令
    • $ git restore --staged <要從暫存區退回到工作目錄的檔案>: 這樣就可以把剛剛加入到暫存區(Staging Area)的檔案丟回到工作目錄(Working Directory)中
      • -S (=> --staged): 指定要從哪個位置(location)回復(Restore)到工作目錄(Working Directory)中
    • 利用git restore --staged 來將剛剛加入到暫存區的檔案丟回到工作目錄中,讓該檔案不要繼續被Git追蹤到_圖解說明

把暫存區(staging area)的檔案提交到倉庫存檔

  • $ git commit -m "git commit練習"
  • -m = (--messge)參數:代表在這次commit做了什麼事情的說明字串,中英文皆可,言簡意賅就好; $ git commit預設-m參數是必填的
  • git commit練習圖解說明
  • 可以不用二段式commit,也可以用$ git commit -a -m "update content"來達到$ git add + $ git commit -m
  • Git每次commit都只會處理暫存區(staging area)裡的內容,也就是說,如果在執行 $ git commit 之前還沒被加到暫存區裡的檔案,就不會被commit到儲存庫了
  • 提醒:要完成commit才算是完成整個流程喔!

想檢視Git紀錄

  • 使用$ git log指令,越新的資訊會在越上面,並會得到以下三個資訊
  • Commit作者是誰? => 人是誰殺的?
  • 什麼時候commit的? => 什麼時候殺的?
  • 每次的commit大概做了哪些事? => 怎麼殺的?
  • git log圖解說明越新的資訊會在越上面
  • 也可以使用參數將$ git log輸出成不同的形式
    • $ git log --oneline --graph
    • --oneline:每個commit物件僅顯示成一行
    • --graph:用圖表來表示commit物件之間的線性關係
    • git log --oneline --graph圖解說明
  • 可以透過$ git log尋找特定author的commit物件
    • $ git log --oneline --author="Hans"
    • 作者(author)是最初修改的人
    • 提交者(committer)是最後套用該工作成果的人
  • 可以透過$ git log尋找符合特定字串的commit"訊息(-m)"
    • $ git log --oneline --grep="initial"
  • 可以透過$ git log搜尋在所有commit物件中,有哪些符合特定條件的
    • $ git log -S "Ruby"
  • 可以透過$ git log搭配參數來查詢特定時間內的commit物件
    • $ git log --oneline --since "9am" --until "12am" --after="2017-01"
    • --since:"hham/pm"
    • --until:"hham/pm"
    • --after:"yyyy-mm"

刪除Git檔案

  • 刪除檔案對Git來說都是一種"修改"
  • 可以透過系統指令刪除檔案$ rm welcome.html
    • 這時候的檔案會是deleted狀態
    • rm刪除檔案顯示為deleted狀態
    • 還需要再執行$ git add xxx.txt 才會將這個"刪除"加到暫存區
  • 可以透過$ git rm welcome.html
    • $ git rm = rm xxx.txt + git add xxx.txt
    • $ git rm 相當於先$ rm刪除檔案後再$ git add 加入暫存區的兩段式動作
    • git rm=rm+git add 圖解說明
    • 不管是系統指令的$ rm或是$ git rm都真的會把檔案從工作目錄刪掉,但如果只是不想讓檔案再被Git控管,可以加上--cached參數
    • $ git rm xxx.html --cached
    • --cached:不會真的將檔案刪除掉,僅將檔案脫離Git控管,成為Untracked file

變更檔名

  • 跟"刪除"檔案一樣,變更檔名也是一種"修改"
  • mv檔名也算是一種修改圖解說明
  • 雖然只是透過系統指令$ rm 來改名,但對Git來說會被認為是兩個動作,後續仍須使用$ git add world.html指令來把這些異動加入暫存區
    • 刪除hello.html檔案
    • 新增world.html檔案(變成Untracked file)
  • 可以透過$ git mv world.html hans_world.html
    • $ git mv = mv 新檔名.html + git add 新檔名.html
    • $ git mv 相當於先$ mv修改檔名後再$ git add 加入暫存區的兩段式動作
    • git mv=mv+git add圖解說明
    • 這樣hans_world.html就會直接變成renamed file
  • 其實Git是根據檔案的"內容"去算出SHA-1的值,所以Git不是很在乎你的檔案叫什麼名稱,只在乎檔案的內容是什麼。所以當進行更改檔名的時候,Git並沒有為此做出一個新的Blob物件,而僅是指向原來舊的那顆Blob物件;但因為檔案名稱改變了,所以會做出一顆新的Tree物件喔!

修改Commit紀錄

  • 想修改commit紀錄有以下4種方法
    • .git/目錄整個刪除 -> 不建議,等於砍掉重練
    • 使用$ git rebase 來修改歷史
    • 先把commit用$ git reset拆掉,整理後再重新commit
    • 使用git commit --amend參數來修改最後一次的commit物件 -> 較推薦此作法!
  • 可以透過$ git commit --amend -m "新的commit message"來修改commit紀錄
    • --amend:只能修改"最近一次"的commit紀錄
    • git commit --amend -m修改最近一次commit message練習圖解說明
  • 如果在完成commit後,卻發現有一個檔案忘了加到這次的commit中,但不想因為這個檔案再重新發送一次commit,想把這個檔案加入最近一次commit,此時有兩個做法
    • 使用$ git reset 把最後一次的commit拆掉,加入新檔案後再重新commit
    • 使用--amend參數進行commit
      • $ 先 git add pizza.html,將Untracked file 加入追蹤
      • $ 再 git commit --amend --no-edit,把這個檔案併入最後一次的commit中,而後面的--no-edit參數就是不要開啟vim編輯視窗來編輯commit message的意思
      • git commit --amend --no-edit參數圖解說明
  • 提醒:即便"只是修改commit message",仍然會產生新的commit id,因為這樣對Git來說commit物件的內容是有"變動"的,所以Git會重新計算並產生一顆新的Commit物件,也就是說這其實算是一次全新的commit
  • 如果想修改更早的commit紀錄,就必須使用$ git rebase 指令了
  • 團隊開發守則:即便只是修改commit message,不管如何它就是修改了一次歷史,所以請盡量"不要"在已經push出去之後再修改,否則可能會造成其他人的困擾

如果有特定檔案不想放在Git裡面一起備份或是上傳到Git Server的話,例如:資料庫密碼,雲端伺服器的金鑰...可以加入 .gitignore

  • 可以在專案中建立一個 .gitignore 的檔案,裡面可以設定想要忽略的規則
  • .gitignore只要一被建立並符合規則就會生效,即使這個檔案還沒被commit或是還沒被push到Git Server,這要一來整個專案的人都可以共享這個"忽略規則"的設定
  • 想查詢在使⽤的⼯具或程式語⾔通常會忽略哪些檔案,可以到GitHub上有整理了⼀份各種程式語⾔常⾒的 .gitignore 檔案
  • 可以利用-f參數,來無視 .gitignore的忽略,強制將檔案加入Git追蹤範圍中
    • $ git add -f xxx.tmp
    • git add -f參數來無視gitignore的設定
    • 提醒: .gitignore 檔案設定的規則,只對"在規則設定之後"的檔案有效
  • 想一次將被 .gitignore 忽略的檔案們一次刪除,可以使用$ git clean來完成
    • $ git clean 可以將工作目錄(working directory)中的所有Untracked files都一次刪除
    • $ git clean -fX
      • -f: 強制執行
      • -X: 只移除被Git忽略的檔案(有在.gitignore中提到的)

檢視特定檔案的commit紀錄

  • $ git log -p xxx.html
    • -p: 可以更詳細的檢視每一次的commit到底做了哪些修改
    • 補充: 前面的"+"是新增,"-"是刪除
    • git log -p 檢視特定檔案詳細的每一筆commit紀錄

想要知道某個檔案的某一行是誰寫的

  • $ git blame xxx.html
  • 可以詳細地看出來每一行是誰在什麼時候寫的
  • 每一行前面的SHA-1值就是每一個Commit物件的識別代碼
  • git blame圖解說明
  • $ git blame -L xxx.html
    • -L ,: 可以只顯示指定行數的內容

在工作目錄(working directory)想要復原不小心透過 rm 指令刪除的檔案

  • $ git checkout可以用來 (在Git Version 2.23.0 之後)
    • 切換分支(= $ git switch)
    • 恢復工作目錄(working directory)裡的文件(= $ git restore)
    • 當$ git checkout "分支名稱" 時,就會切換到指定的分支
      • 例如:$ git checkout -b tiger: 切換到tiger分支,如果沒有該分支的話,就新增一個tiger分支
        • -b <新的分支名稱>: (=> branch): 切換到<新的分支名稱>,如果沒有的話,就新增一個<該分支>
    • 當$ git checkout "檔案名稱 or 路徑" 時,Git就會到 .git/ 目錄裡拉一份到目前的工作目錄(working directory)
  • $ git checkout xxx.html 可以將工作目錄(working directory)中不小心刪除(rm)掉的指定檔案復原回來
  • 例如: pizza.html 從deleted status變回原本的狀態
  • 也可以利用 $ git checkout . 一口氣把所有工作目錄(working directory)中被刪掉的檔案救回來
  • git checkout復原工作目錄不小心rm的檔案圖解說明
  • $ git checkout HEAD~2 yyy.html
    • 這樣就會到 .git/ 裡拿距離現在兩個版本以前的 yyy.html 來覆蓋現在工作目錄(working directory)的 yyy.html ,但要注意的是,這同時也會更新暫存區(staging area)的狀態喔
  • 以此類推, $ git checkout HEAD~2
    • 這樣就會拿距離現在兩個版本以前的檔案來覆蓋現在工作目錄(working directory)裡的檔案,同時也更新暫存區(staging area)裡的狀態

如果想重新編輯剛才的commit

  • $ git log --oneline 顯示的歷史commit紀錄是由新到舊(上到下)
    • git log由新到舊的歷史commit紀錄
  • 利用 $ git reset f06546a 來回溯到過去指定的Commit物件中
    • 可以使用"相對"或是"絕對"的表示方式
      • ^ : 前一次
      • ~次數 : 要倒退至幾次commit以前
      • SHA-1值: 絕對的表示方式
      • 所以$ git reset f06546a^^ = $ git reset f06546a~2
    • 例如: $ git reset f06546a^ =>代表回到該commit的前一次commit紀錄中
    • 例如: $ git reset master^ =>代表回到master分支的前一次commit紀錄中
    • 例如: $ git reset HEAD^ =>代表回到HEAD目前指向的分支(該分支指向的commit)的前一次commit紀錄中
  • $ git reset <模式> ,有三種模式可以選擇,預設為 mixed模式
    • mixed 模式: 這模式下的reset會把暫存區(staging area)的檔案丟掉,但不會動到工作目錄(working directory)的檔案,也就是說commit拆出來的檔案會留在工作目錄,但不會留在暫存區
    • soft 模式: 這模式下的reset,工作目錄(working directory)和暫存區(staging area)的檔案都不會被丟掉,因此看起來就只有HEAD在移動而已; 所以commit拆出來的檔案都會直接被存放在暫存區
    • hard 模式: 這模式下的reset,不管是暫存區(staging area)和工作目錄(working directory)都會被丟掉
    • 小統整: git reset <三種模式>統整圖解說明
      參考圖片出處https://gitbook.tw/
      • mixed 模式=> index移除staged標記,變成Modified或是Untracked file,內容是新版的
      • soft 模式=> 僅移除commit變成新版"未"commit,內容仍是新版的
      • hard 模式=> 回到上一版本,這期間的所有變更完全移除,內容及狀態皆是上一版

新增Tag物件Commmit物件

  • $ git tag -a wow -m "Tag物件-annotated tag 練習"
    • $ git cat-file -t <Tag物件的SHA-1值>
    • Tag物件介紹
  • 統整: Tag物件中的有附註Tag(Annotated Tag)會指向某個Commit物件

分支(branch)操作

  • 分支基礎操作可參考
  • 合併分支的兩種方式($ git merge & $ git rebase)
    • 方法一 : $ git merge

    • $ git merge <要合併掉的分支名稱>: 可以用來將目前所在的分支(branch)合併(= 吃)掉 <要合併掉的分支名稱>

      • 若要合併掉別人的分支是從要被合併掉的分支的開出來的,若要被合併掉的分支沒有修改的話,Git會預設使用快轉模式(fast-forward)來做合併
      • 但若原本的那個分支有修改過的話,這時候Git就不會使用快轉模式(fast-forward),此時Git會額外再做出一個新的Commit物件來合併這兩個分支
      • $ git merge --ff-only <要合併掉的分支名稱>
        • --ff-only (=> 盡可能的優先使用fast-forward模式合併,此時如果無法做到使用fast-forward模式合併的話,Git就會拒絕這次的指令並回傳一個"失敗"的錯誤狀態)
    • 情境說明(要用master分支來合併pig分支)

    • 先新增一個分支(branch): pig

    • 建立一個新的檔案,並commit: pig.html

    • 新增一行文字後,再commit一次: hello, pig

    • $ git log: 檢視一下目前的commit歷史紀錄(確認目前是在pig分支並且有兩次commit的紀錄)

    • git merge分支練習統整的圖解說明

    • 要先切回master分支

      • $ git checkout master
    • $ git merge pig: 將pig分支合併回到master分支

      • 本來落後pig分支兩次commit紀錄的master分支,現在也已經跟上pig分支的進度了
      • 將pig分支合併回到master分支
    • 方法二 : $ git rebase

    • $ git rebase <要被rebase的分支名稱>

    • $ git rebase -i <HEAD的相對位置>

    • $ git rebase 是一種會修改到歷史紀錄的指令,盡量不要隨便對已經推出去給別人內容進行rebase,這樣很容易造成其他人的困擾

    • 用$ git rebase 的方式合併分支 & 用$ git merge 的方式合併分支,有一個很明顯的差別是用Rebase方式合併分支的話,Git並不會特別做出一個專門用來合併分支的Commit物件

    • $ git rebase 的原理其實是做多次"Apply"到新的基準點branch指向的Commit物件

    • Rebase合併分支過後,原來舊的那個分支原本所用到的那些Commit物件,並"不會立即被Git刪除"; 只是因為那些Commit物件沒有分支(branch)指向他們,所以如果我們也沒特別去記下這些Commit物件的id的話,這些Commit物件就會慢慢地被邊緣化,直到有一天被Git的內建資源回收機制帶走

    • 其實誰Rebase誰以結果來看是沒什麼差別的,但以歷史紀錄來說的話,被rebase的分支會寫在後面

    • 想取消合併分支的話,有以下兩種方式

      • 如果是用 $ git merge 來合併分支的話: 用 $ git reset HEAD~2 --hard => 也就是拆掉這個合併的Commit物件,大家就會退回到合併前的狀態
      • 如果是用 $ git rebase 來合併分支的話,因為這個方法Git並不會特別做出一個Commit物件來合併分支,所以不能用上述的方法;但可以用以下兩種方式來取消合併分支
        • 先檢視目前的Reflog紀錄
          • $ git reflog
            • 透過git reflog來還原到Rebase分支前的狀態
            • 找出對應到最久以前的 rebase關鍵字的前一次Commit物件的id
            • 以這個例子是: ce76004這個Commit物件id
          • 用 $ git reset <要回到過去的哪個Commit物件id> --hard
          • 這樣就可以回到Rebase以前的狀態了
        • 先檢視目前的ORIG_HEAD指向的Commit物件的id
          • 可先參考ORIG_HEAD是什麼?
          • $ cat .git/ORIG_HEAD: 回傳Git危險操作以前的ORIG_HEAD指向的Commit物件`的id
          • git reset ORIG_HEAD --hard 來回到危險操作以前的ORIG_HEAD指向的Commit物件的SHA-1值_統整
  • 使用Rebase方式來合併分支的優缺點分析
    • 好處1: 不會特別產生一個為了合併分支的Commit物件
    • 好處2: 合併的歷史順序可以自由決定(依照誰Rebase誰而定)
    • 缺點1: 相對來說,比起Merge方式來合併分支,並沒有那麼直覺
    • 缺點2: 如果一個不小心可能會弄壞掉而且還不知道該怎麼Reset回來
    • 建議: 可以在還沒推出去前(Push),如果覺得Commit物件們太凌亂or瑣碎,可以先用Rebase合併分支的方式來整理這些Commit物件,再推出去
    • 提醒: Rebase等於是修改歷史紀錄,如果修改已經推出去(Push)的歷史紀錄可能會造成其他人的困擾,所以如果是已經推出去(Push)的內容,非必要盡量不要使用Rebase
  • $ git rebase -i <過去的一個Commit物件id>
    • 可以使用像是pickeditsquashdrop 等相關Git指令來編輯這些Commit物件
    • Rebase指令的互動模式(-i)也可以用來編輯過去的所有Commit物件
      • -p (=> --pick): use commit
      • -r (=> reword): use commit, but edit the commit message(當想修改過去歷史紀錄中的commit message時)
      • -e (=> edit): use commit, but stop for amending(適用以下情境)
        • 當想把一個commit紀錄拆解成多個commit紀錄
        • 當想在某些commit之間再加新的commit紀錄
        • 當想要刪除某幾個commit紀錄
        • 當想要調整過去commit紀錄的順序
        • Git會再重新執行一次Rebase時,停在edit那行的Commit物件
        • 這時Git的狀態應該會類似這樣
        • 這時Git會詢問要如何修改這個Commit物件,通常這時候要先把要做"修改"的Commit物件的調整好,接著繼續Rebase下去
          • $ git commit --amend: "修改"該Commit物件
          • $ git rebase --continue: 繼續Rebase下去
        • 如果這時候不是要修改該Commit物件,而是想要將該Commit物件拆分成兩個Commit物件的話,可以用Reset指令來完成
      • -s (=> squash): use commit, but meld into previous commit(當想把多個commit紀錄合併成一個commit紀錄`時)
      • -d (=> drop): remove commit
    • 注意! $ git rebase -i 與 $ git log 顯示commit紀錄的順序是相反的,若都是"從上到下"檢視的話
      • $ git rebase -i: 從舊->新
      • $ git log: 從新->舊
      • $ git reflog: 從新->舊
    • git rebase -i <過去的Commit物件id>,利用Rebase互動模式來編輯過去的commit message的圖解說明
    • 情境說明
    • $ git rebase -i <ca7a70e>
      • ca7a70e: 為專案最初始的Commit物件的id
      • 找到要編輯commit messageCommit物件,將pick修改為reword,就會跳進去該Commit物件裡面,可以利用 Vim模式 來編輯該物件的commit message後,並存檔 & 離開 即可
      • 在Rebase互動模式中利用reword來編輯過去的commit message
    • 提醒: 經過Rebase模式修改後,因為前後要接的Commit物件都不同(其實時間也不同),所以會重新計算並做出一顆新的Commit物件;
      而因為在Git歷史紀錄上的這兩顆Commit物件被換掉了,在它們兩個之後的一整串Commit物件全部都必須做出新的Commit物件出來替代舊的Commit物件們
    • 提醒: 在執行Rebase操作時,要多注意"相依性問題", 否則一定會出問題的
      • 情境說明
      • 某次commit紀錄修改了zebra.html,結果卻不小心把這次commit紀錄移動到建立zebra.html的那次commit紀錄之前
      • 刪除了某次建立sheep.htmlcommit紀錄,但後面的commit紀錄都需要用到sheep.html這個檔案

該如何回復到上一次commit紀錄的狀態?

  • $ git revert HEAD --no-edit: 會取消最後一次的commit紀錄,並"回復"到前一次commit紀錄的狀態
    • --no-edit: 指定Revert指令不要開啟commit message編輯器
  • 情境說明
    • 編輯pig.html,並新增"abc"字串
      • git revert 練習_編輯pig.html檔案
    • git revert HEAD --no-edit: 會取消最後一次的commit紀錄,並回復到HEAD的前一次commit紀錄
      • git revert練習_Revert後abc字串不見了
    • 透過Sourcetree可以發現,Git會為了Revert操作,新增一個Commit物件
      • Sourcetree檢視git revert HEAD --no-edit執行後的狀況_圖解說明
  • 那要如何取消Revert呢? 有以下2種方法
    • $ git revert HEAD --no-edit: 這樣一來剛剛被刪除的pig.html中的"abc"字串又復活了; 但同時Git又會新增一個Revert_"Revert" xxxxxCommit物件,會造成Commit物件越來越多
      • 取消Revert操作會產生Revert_Revert的Commit物件
    • $ git reset HEAD^ --hard: 適用於直接放棄HEAD的前一次commit紀錄的所有操作(包括工作目錄 和 暫存區)
  • Revert指令的建議使用情境
    • 如果是個人開發的專案,其實可以直接用Reset切回去就好
    • 但如果是多人共同協作的專案,就不一定有機會可以使用Reset指令,這時候就可以用Revert指令來做出一個"取消"的Commit物件; 這樣對其他人來說也不算是"修改歷史",只是建立一個跟某個commit紀錄反向的操作的Commit物件而已

觀念介紹

The HEAD: HEAD is the pointer to the current branch reference, which is in turn a pointer to the last commit made on that branch. That means HEAD will be the parent of the next commit that is created. It’s generally simplest to think of HEAD as the snapshot of your last commit on that branch.
ORIG_HEAD: Git在危險操作以前,會先自動記錄當下HEAD指向的Commit物件的id
git branch - List, create, or delete branches
git cat-file - Provide content or type and size information for repository object
git count-objects - Count unpacked number of objects and their disk consumption
git tag - Create, list, delete or verify a tag object signed with GPG
git show - Show various types of objects
git prune - Prune all unreachable objects from the object database

Git 其實是一種分散式的版本控制系統

  • Git是一種分散式的版本控制系統,而所謂的"版本控制系統"就是指會幫你記錄所有的狀態變化,隨時可以切換到過去某個版本的狀態

Git 的優點

  • Git的優點
    • 免費.開源
    • Git是記錄檔案內容的快照(snapshot),而不是記錄版本之間的差異,它可以讓Git更快速地切換版本
    • Git是一款分散式的版控系統(Distributed Version Control),雖然也會有共同的伺服器,但即使在沒有伺服器或是沒有網路的環境,依舊可以使用Git來進行版控,待伺服器恢復正常運作或是在有網路的環境後再同步,不會受影響
    • 註: Git和SVN相比,最大的不同點就是,Git可以在local端做一些修改,然後commit到本地的版本庫,最後push到伺服器; 而SVN只要一commit,更改就已經提交到伺服器了

Git 是用快照(snapshot)在做版本控制的

  • Git在每次版本變化的時候,有點像拍照(snapshot)一樣,Git會更新並記錄整個目錄跟檔案的樹狀結構

Git 的四大物件(Blob,Tree,Commit,Tag)觀念介紹

  • Git的四大物件結構: 在.git/ 的資料結構裡面會有
    • Blob物件
      • 情境說明
      • 當檔案被加入暫存區(staging area)時,Git便會在.git/目錄裡產生一個Blob(Binary large object)物件,並且依照它的"規則"擺放到它的目錄裡,這個Blob物件是用來存放該檔案的"內容"
      • 接下來,就會到 .git/objects/目錄裡存放該檔案,Git會用SHA-1的40個字中的前2個字作為目錄,剩餘的38個就是檔案名稱
      • 檢視.git/objects/中的Blob物件的SHA-1值-新
      • 透過$ git cat-file來檢視Git repository內各物件的 or 型態 or 大小 or 相關詳細資訊
        • -t: 顯示該物件的型態(type)
        • -p: 顯示該物件的內容(content)
        • -s: 顯示該物件的大小(size)
        • git cat-file檢視該檔案的型態or內容or大小圖解說明
      • 統整:
        • 步驟一: 當使用$ git add把檔案加入至暫存區時,Git會根據這個物件的"內容"計算出SHA-1
        • 步驟二: Git接著會用這個SHA-1值的前2個字當作目錄名稱,後38個字當作檔案名稱;接著Git會把目錄及檔案建立在.git/objects/ 目錄裡面
        • 步驟三: 該檔案的內容則是Git使用壓縮演算法,把原本的"內容"壓縮之後的結果
      • 補充: Blob物件檔名是由SHA-1演算法決定的,Blob物件的內容則是壓縮演算法決定的
    • Tree物件
      • Tree物件會指向某個或是某些Blob物件,或是其他Tree物件
      • Git官方網站提供的Tree物件和Blob物件的關係圖解說明
        參考圖片出處: https://git-scm.com/book/zh-tw/v2/Git-Internals-Git-Objects
      • Tree物件介紹
      • 統整:
        • 步驟一: 檔案在Git裡會以Blob物件的方式存放
        • 步驟二: 目錄及檔案名稱會以Tree物件的形式存放
        • 步驟三: Tree物件會指向某個或是某些Blob物件,或是其他Tree物件
    • Commit物件
      • 通常包含以下 4 項資訊
        • 某個Tree物件
        • parent: 會指向前一次的Commit物件
        • 作者跟這次commit的人&該次的commit時間
        • 本次的commit訊息($ git commit -m)
      • Git官方網站提供的Commit物件與Tree物件和Blob物件的關係圖解說明
        參考圖片出處: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
      • 統整Commit物件的規則:
        • Commit物件會指向某個Tree物件
        • Tree物件會指向某個或是某些Blob物件,或是其他Tree物件
        • 除了第一個Commit物件以外,所有的Commit物件都會指向它的前一次的Commit物件
    • Tag物件
      • $ git tag -a wow -m "Tag物件 annotated tag 練習"
      • $ git cat-file -t <Tag物件的SHA-1值>
      • Tag物件介紹
      • 統整:
        • Tag物件中的有附註Tag(Annotated Tag)會指向某個Commit物件
    • Git四大物件(Blob,Tree,Commit,Tag)觀念整合:
      • 把檔案加入Git之後,檔案的內容會被轉成Blob物件儲存
      • 目錄以及檔名會存放在Tree物件內,Tree物件會指向Blob物件,或是其它的Tree物件
      • Commit物件會指向某個Tree物件;除了第一個Commit物件以外,其它的commit都會指向前一次的Commit物件
      • Tag物件(Annotated Tag)會指向某個Commit物件
      • 分支(branch)雖然不屬於四個物件之一,但它會指向某個Commit物件
      • 當開始往Git Server上推送之後,在 .git/refs/ 底下就會多出一個 remote/目錄,裡面放的是遠端的分支,基本上跟本地的分支是差不多的概念,同樣也會指向某個Commit物件
        • 透過檢視.git/refs/remotes/ 來看遠端分支有哪些
      • HEAD也不屬於Git四大物件之一,它會指向某個分支(branch)

$ git count-objects: 可以檢視目前有多少物件和用掉多少儲存空間

  • $ git count-objects: 可以檢視目前有多少物件和用掉多少儲存空間
    • git count-objects可以檢視目前有多少物件和用掉多少儲存空間的圖解說明
    • -v (=> --verbose): 顯示這個Git管控的專案中所有物件的更多詳細資訊
    • -H (=> --human-readable): 將所有在這個Git管控的專案中所有物件所佔用的儲存空間用人類可閱讀的格式來顯示(MB)

在使用Git時,指令要在正確的目錄下才能正常運作

  • 在使用Git時,指令要在正確的目錄下才能正常運作

在Git術語中,暫存區(staging area)=索引(index)

  • 暫存區(Staging Area)又可稱為索引(index)

Git 世界裡的的三大區域---工作目錄(working directory),暫存區(staging area),儲存庫(repository)

  • 在Git裡,主要可以分成三個區域,透過不同的git指令可以把檔案移動往不同的區域
    • 工作目錄(Working Directory)
    • 暫存區域(Staging Area) or (index)
    • 儲存庫(Repository)
    • 工作目錄_暫存區_儲存庫的關係圖解說明
      參考圖片出處https://gitbook.tw/

為什麼每次都要先$ git add 再 $ git commit

  • 可以想像你有一個倉庫,倉庫門口有個小廣場,這個廣場的概念就像跟暫存區一樣,你把要存放到倉庫的貨物先放到這邊($ git add),然後等收集的差不多了就可以打開倉庫門,把放在廣場上的貨物送進倉庫裡($ git commit -m,並記錄下來這批貨是什麼用途的? & 誰送來的?)

Git物件的id是怎麼計算出來的? (SHA-1演算法)

  • 在Git的Commit物件裡每串看起來像亂碼的文字,都是透過SHA-1演算法計算出來的結果,是一種重複率極低的演算法;Git使用這樣的字串作為識別,每個Commit物件都有一個這樣的值,你可以把它想像成是每個Commit物件的身分證字號,不會重複

HEAD是什麼?

  • HEAD 是一個指標,會指向某一個分支,我們通常可以把HEAD當作"目前分支"來; 也可以在.git/HEAD這個檔案裡看到記錄著HEAD的內容
    • $ cat .git/HEAD: 顯示目前HEAD指向的分支
      • 在.git/HEAD中檢視目前HEAD指向的分支
    • $ cat .git/refs/<分支名稱>: 檢視目前HEAD指向的分支的commit物件
      • 檢視目前HEAD指向的分支的commit物件
    • 當切換分支時,HEAD指向的分支也會轉變
      • $ git branch: 檢視目前所有的分支有哪些
      • $ git branch bird: 建立一個新的分支名稱為bird
      • $ git checkout bird:切換到bird分支
      • $ cat .git/HEAD : 檢視目前HEAD指向的是哪個分支名稱 //
        • // ref: refs/heads/bird
      • 總結: .git/HEAD的內容(.git/refs/heads/<目前所在分支的名稱>會隨著$ git checkout <分支名稱> 而改變)
    • 補充: 在Git v1.8.5 之後的版本開始支援,可以用 這個符號來代表HEAD
      • 情境說明
      • $ git reset HEAD~2 = $ git reset @~2
      • $ git reset HEAD^^= $ git reset @^^
      • 以上四種指令都是相同的意思,代表要回到目前的HEAD指標指向的Commit物件以前的2次``commit紀錄的那時候
      • 可參考如果想重新編輯剛才的commit

ORIG_HEAD是什麼?

  • ORIG_HEAD 是Git特別的一個紀錄點,這個紀錄點會自動記錄在"Git的危險操作"以前,當下的HEAD指向的Commit物件的id
    • Git的"危險操作"有: mergerebasereset
    • $ cat .git/ORIG_HEAD
      • 檢視ORIG_HEAD指向的Commit物件的SHA-1值
  • 可以檢視目前的 ORIG_HEAD 紀錄點是指向哪個Commit物件
    • $ cat .git/ORIG_HEAD
      • 透過cat .git/ORIG_HEAD 這個檔案來檢視在Git危險操作以前的那個ORIG_HEAD會指向哪個Commit物件

分支(branch)是什麼?

  • 在Git裡面,分支(branch)就像貼紙一樣,它會貼在某個Commit物件上,並且會隨著每次的commit跟著移動
    • 所以HEAD會指向一個分支(branch),並且分支會指向一個Commit物件
    • 當 $ git checkout <分支名稱orCommit物件>時,Git會依據當下的這個Commit物件來還原工作目錄(working directory)的內容,並參考 .git/objects/ 目錄裡的內容像拎葡萄一樣整串從頭的地方拎起來,只要從源頭的Commit物件拎起來,整串內容都可以被拿出來
      • git checkout到過去的Commit物件造成Deatched HEAD的官方圖解說明 參考圖片出處https://git-scm.com/docs/git-checkout
      • 注意! 這時候有可能會發生Detached HEAD的情況!
        • 當使用 $ git checkout <Commit物件的id> 後,該Commit物件剛好目前沒有分支(branch)指向它
    • $ git branch: 檢視目前所有的分支有哪些
      • 用git branch來檢視目前的分支(master)
    • $ git branch bird: 建立一個新的分支名稱為bird
      • 用git branch <新的分支名稱> 來新增新的分支
    • $ git checkout bird: 切換到bird分支
      • 當Git在切換分支 ($ git checkout <branch name>) 的時候會做以下兩件事情
        • 用該分支指向的那個Commit物件的內容來"更新"暫存區(staging area)以及工作目錄(working directory),當切換分支"之前"所做的修改內容仍會留在工作目錄(working directory)不受影響
        • 同時HEAD也會跟著一起前進,指向剛剛切換過去的那個分支(branch)
    • $ git branch -m bird fish: 將bird分支更名為fish分支
      • -m (=> --move): 移動or更名 分支(branch)和其相對應的reflog
      • git branch -m 修改分支名稱的圖解說明
    • $ git branch -d fish: 將fish分支刪除
      • -d (=> --delete): 刪除該分支
      • 補充: 只有目前所在的分支不能刪除,只要先$ git checkout 切換到別的分支後也可以刪除原本所在的那個分支(儘管是master分支)
      • git branch -d 刪除分支後的圖解說明
    • 或是 $ git branch -D fish:強制將尚未合併(merged)的分支刪除
      • -D = -d + -f (=> --delete --force 的縮寫): 強制刪除該分支( 不管該分支是否已經合併(merged)到其上游分支(upstream branch) )
    • 合併過後的分支,想刪除就可以刪除,因為分支只是一個40字元的檔案而已,它會標記出它目前是指向哪一個Commit物件; 所以刪除分支這個動作就像是把一張貼紙撕起來的概念而已,原來被這張貼紙貼著的東西並"不會"因此而不見
  • 在Git開分支其實很"便宜",因為分支(branch)其實就只是一張40字元(某個Commit物件的SHA-1值)的貼紙,而這個貼紙會指向它對應的Commit物件的id
    • 可檢視 .git/refs/heads/<分支名稱> 這個檔案裡的內容就是該分支(branch)指向它對應的Commit物件的id
      • $ ls -al .git/refs/heads/
        • 檢視.git/refs/heads/ 來得知目前有哪些分支
      • 檢視.git/refs/heads/<分支名稱>裡面的檔案內容是該分支指向的Commit物件id
    • 如果把 .git/refs/heads/ 目錄裡面的其中一個分支刪掉的話,就相當於刪除分支了
      • $ rm .git/refs/heads/<要刪除的分支名稱>
      • rm .git/refs/heads/ 目錄裡面的其中一個分支就是相當於刪除該分支了
    • 如果把 .git/refs/heads 目錄裡面的其中一個分支名稱改掉的話,就相當於修改分支名稱了
      • $ mv .git/refs/heads/<原分支名稱> .git/refs/heads/<新分支名稱>
      • mv .git/refs/heads/ 目錄裡面的其中一個分支名稱就是相當於修改該分支的名稱了
  • Git如何知道現在是在哪一個分支(branch)?
    • 可以透過Git指令 $ git branch -l
      • -l (=> --list): 顯示目前有哪些分支(branch)
    • 可以透過檢視 .git/HEAD 這個檔案,來得知HEAD指標目前指向的是哪一個分支(branch)
      • $ cat .git/HEAD
        • 透過指令cat .git/HEAD 來檢視目前的HEAD指標是指向哪個分支
      • 當切換分支(brach)時, .git/HEAD 的內容也會同時變動
        • 先 $ git checkout <另一個分支名稱>
        • 再 $ cat .git/HEAD
        • 透過git checkout後來檢視.git/HEAD的檔案內容也同時跟著變動且指向新的分支名稱

Detached HEAD (斷頭) 是什麼?

  • Detached HEAD (斷頭): 正常情況下,HEAD會指向某一個分支,而分支會指向某一個Commit物件。但有時候HEAD會發生"沒有辦法指到某個分支"的情況,這個狀態的HEAD就稱為 "detached HEAD(斷頭)"
    • 以下是3種常見的原因可能會造成Detached HEAD的狀態
      • 使用 $ git checkout <Commit物件的id> 後,該Commit物件剛好目前沒有分支(branch)指向它
      • $ git rebase 的過程其實也是不斷地處於Detached HEAD的狀態
      • 切換到某個遠端分支(remote branch)的時候
        • 例如: $ git checkout -b test origin/test
    • Detached HEAD 狀態下其實跟平常一樣操作Git,也可以進行commit,只是commit後,當下的那個Commit物件就只會有HEAD指標指向它,這時如果將HEAD指標切換到其他分支的話,這個Commit物件就很不容以被找回來了(除非你有記下這個Commit物件的id); 如果一直沒有人來找它的話,就要等到Git自動觸發資源回收機制來回收它了
      • 這情況會有點類似於要從過去的commit紀錄,再長一個新的分支出來的情境
      • 這時如果想留下這個Commit物件的話,可以在該Commit物件上建立一個分支(branch)貼紙貼在上面
        • $ git branch <要新增的分支名稱> <該Commit物件的id>
      • 也可以建立新的分支後,直接切換到該分支上面
        • $ git checkout -b <要新增的分支名稱> <該Commit物件的id>
    • 其實嚴格來說,當HEAD指標沒有指到某個"本地"分支的話,就會呈現Detached HEAD的狀態
      • $ git branch -r: 列出目前所有的遠端分支(remote branch)
        • -r (=> --remotes): 列出目前所有的遠端分支(remote branch)
        • git branch -r 檢視目前所有的遠端分支有哪些
      • 情境說明
        • 如果要切換到遠端分支,而不希望呈現Detached HEAD狀態,可以使用以下指令
          • $ git checkout -t origin/<遠端分支名稱>
          • -t (=> --track): Git會在本機建立一個追蹤分支(tracking branch)
        • 如果想要簡單一點也可以把前面的"origin"拿掉,並直接切換到遠端分支(remote branch)去
          • $ git checkout <遠端分支名稱>
    • 如何離開Detached HEAD狀態呢?
      • 既然已經知道所謂的Detached HEAD狀態只是HEAD指標沒有指向任何分支所造成的,要脫離這個狀態,只要讓HEAD能指向任何一個分支(branch)就可以了
      • 例如: $ git checkout master
        • 將目前的HEAD指標指向master分支就可以解除Detached HEAD的狀態了

Tag (標籤) 是什麼?

  • Tag (標籤): 是一個指向某一個Commit物件的"指標"
    • 可先參考Git 的四大物件(Blob,Tree,Commit,Tag)觀念介紹
    • Tag (標籤)依據官方文件有以下3種適合作為的用途

      Annotated tags are meant for release while lightweight tags are meant for private or temporary object labels.

      • 軟體版本號(release version)
      • 個人使用(private)
      • 暫時標記用途(temporary object labels)
    • Tag (標籤)可分為兩種形式
      • 輕量標籤(lightweight tag): 主要用來作為軟體版本號(release version)
        • 輕量標籤(lightweight tag)會指向一個Commit物件
        • $ git tag <輕量標籤的名稱> <要標記在哪個Commit物件上的id>
        • 例如: $ git tag refresh_hyperlink 19468c5: 這樣Git就會在"19468c5"這個Commit物件上,建立一個輕量標籤(lightweight tag)叫做"refresh_hyperlink"
        • git tag建立輕量標籤的圖解說明_統整
      • 有附註標籤(annotated tag): 用來作為個人使用(private)或是暫時標記用途(temporary object labels)
        • 注意: 有附註標籤(annotated tag)會指向一個Tag物件,而這個Tag物件才會指向一個Commit物件
        • $ git tag <輕量標籤的名稱> <要標記在哪個Commit物件上的id> -a -m "refresh_form_width"
        • 例如: $git tag refresh_form_width 6894635 -a -m "有附註標籤的練習": 這樣Git就會在"6894635"這個Commit物件上,建立一個有附註標籤(annotated tag)叫做"refresh_form_width",並且新增一個tag message叫做"有附註標籤的練習"
          • -a (=> --annotate): 請Git幫忙建立一個有附註標籤
          • -m (=> --message): 建立一個有附註標籤訊息(tag message)
        • 新增有附註標籤的練習_統整
        • 有附註標籤(annotated tag)會有記錄更詳細的標籤相關資訊,通常會有以下資訊
          • 誰貼的標籤?
          • 什麼時候貼的標籤?
          • 為什麼要貼這張標籤?
      • 新增完標籤(tag)以後,可以用$ git log --oneline來檢視目前的commit紀錄的情況
      • 有附註標籤(annotated tag)的訊息量也會比輕量標籤(lightweight tag)的訊息量多
        • $ git show <標籤的名稱>: 可以用來檢視各種物件的型態
          • 例如: $ git show refresh_hyperlink => 檢視輕量標籤(lightweight tag)的型態
            • 透過git show <輕量標籤的名稱> 來檢視輕量標籤目前的狀況_統整
          • 例如: $ git show refresh_form_width => 檢視有附註標籤(annotated tag)的型態
            • 透過git show <有附註標籤的名稱> 來檢視有附註標籤目前的狀況_統整
      • 也可以到 .git/refs/tags/ 目錄裡面,檢視所有的標籤
        • $ cat .git/refs/tags/<標籤的名稱>
          • 如果是輕量標籤,會顯示該標籤(tag)指向的Commit物件的id
          • 如果是有附註標籤(annotated tag)會顯示其指向的Tag物件,而這個Tag物件才會指向一個Commit物件的id
      • 刪除標籤
        • $ git tag -d <標籤的名稱>: 將該標籤刪除
          • 例如: $ git tag -d refresh_hyperlink
          • -d (=> --delete): 刪除已經存在的標籤

Git的資源回收機制($ git gc)是怎麼運作的呢?

  • 在Git裡,每當把檔案加入暫存區(staging area)時,Git便會根據"檔案內容"製作出Blob物件; 每當完成commit,便會跟著產生所需的Tree物件以及Commit物件
    • 當物件越來越多,並達到觸發Git自動資源回收的機制(Garbage Collection)時,Git會利用這個機制來自動整理這些物件,同時也會把Unreachable狀態的物件清掉
    • 可以先參考Git其實不是在做差異備份,而是在為當時的專案建立快照(snapshot)的$ git gc章節
    • Git的資源回收機制($ git gc)會自動回收掉Unreachable狀態的物件,但需要加上 --prune=now 來搭配使用,才會當下立即就生效
    • $ git gc其實會默默地呼叫$ git prune指令來清除Unreachable物件,但$ git prune指令也是要給它設定到期日,所以剛才的指令其實原理如下
      • $ git gc --prune=now = $ git gc + $ git prune --expire=now
  • 還有什麼方式會產生出Unreachable狀態的物件?
    • 當已經將檔案加入暫存區(staging area)後,卻又將該檔案從Git版控中移除,脫離Git的控管,變為Untracked File狀態
    • 因為一旦Git物件產生後,除非手動進 .git/objects/ 目錄處理掉,不然該物件就會一直留在那邊了
    • 可先參考Git其實不是在做差異備份,而是在為當時的專案建立快照(snapshot)的 $ git ls-files -s 章節
    • 可先參考$ git rm --cached V.S. .gitignore 比較的 $ git rm --cached 部分
    • 情境說明
      • $ echo "手動產生一個Unreachable物件" > unreachable.html
      • $ git add unreachable.html
      • $ git ls-files -s
        • 把檔案加到暫存區(staging area)後,檢視一下目前的Git物件列表(包含工作目錄 & 暫存區 中的)
      • $ git rm --cached unreachable.html
      • unreachable.html這個檔案從Git控管中移除,脫離Git控管,變為Untracked file狀態
      • $ git fsck --unreachable
        • 會顯示目前unreachable.html這個檔案成為Unreachable狀態的物件
        • 在Commit之前猶豫不決,會產生出Unreachable狀態的物件
      • $ git cat-file -p <該Unreachable狀態的Blob物件的id>
        • 用git cat-file -p <該Unreachable狀態的Blob物件的id>  來檢視該Unreachable狀態的物件的內容
    • 被刪除的標籤(Tag)物件,也會成為Unreachable狀態的物件
      • 可先參考Tag (標籤) 是什麼?
      • $ git tag -a unreachable_object -m "刪除Tag物件後,會產生Unreachable狀態的物件"
        • 建立一個有附註標籤(Annotated Tag),因為標籤(Tag)物件原本是指向某個Commit物件,但當該有附註標籤被刪除時,這個被指向的Commit物件就會變成Unreachable狀態的物件
      • $ git tag -d unreachable_object
        • 刪除該有附註標籤(Annotated Tag)
      • $ git fsck --unreachable
        • 會顯示這個被刪除的Tag物件(unreachable_object),該檔案會變成為Unreachable狀態的物件
        • 因為它不像Commit物件,還會有Reflog對它念念不忘,已經沒有其他物件或指標指向這個Tag物件,所以這個已被刪除的Tag物件就會立刻變成Unreachable狀態
    • 在做Rebase的時候,其實也會產生出多個Unreachable狀態的物件
      • 可先參考分支(branch)操作的$ git rebase原理的部分
      • 因為在Rebase的過程中,Git會把原本的Commit物件們分別複製一份到新的base分支上並且重新計算一份新的SHA-1值給這些新的Commit物件; 同時,最後分支HEAD也都會遷移過去新的Commit物件上,所以這時候原本舊的那些Commit物件們,就會變成Unreachable狀態的物件們
    • 總結: 在Git的世界中Unreachable狀態的物件其實是還蠻常見到的,以下情況都會產生出Unreachable狀態的物件
      • 利用$ git reset --hard切到別的Commit物件上後,並且將Reflog設定為已到期後; 這時候在Reset之前,Git原本指向的那些Commit物件就會變成Unreachable狀態
      • 新增一個檔案,用$ git add加到暫存區後,又用了$ git rm --cached將該檔案從Git控管中移除,脫離Git控管,變為Untracked file狀態
      • 如果在commit之前,檔案$ git add之後又再修改,然後再執行$ git add指令,也會產生出Unreachable狀態的物件
      • 即便是$ git commit --amend這種單純修改commit message的指令也會產生出Unreachable狀態的物件
      • 被刪除的標籤(Tag)物件,也會成為Unreachable狀態的物件
      • 在做Rebase的時候,其實也會產生出多個Unreachable狀態的物件

觀念釐清

git ls-files - Show information about files in the index and the working tree
git verify-pack - Validate packed Git archive files
Unreachable Object - 沒有任何物件或指標指著它,如同字面上說的"無法到達的"; 但它仍可以指向其他物件 Dangling Object - 跟Unreachable物件一樣,沒有任何物件或指標指著它,它也沒有指著其他物件,如同完全"懸在天邊"的一顆物件

Git無法控管 空目錄/

  • Git在產生物件時,只在乎"檔案內容",所以如果只是新增一個"空的目錄",Git是沒有辦法處理該空目錄的
    • 原因: Git會對檔案的"內容"使用SHA-1演算法計算然後再.git/objects/目錄裡,建立對應的目錄及檔案,如果是一個空目錄的話,就沒有"內容"可以計算,所以Git連感應都感應不到,因此空目錄對Git來說連Untracked file都稱不上喔
    • 空的目錄也不會被commit
    • git不會追蹤空目錄
    • 如果還是想讓空目錄被Git追蹤,只要在空目錄中隨便放一個檔案就行了,慣例上習慣放.keep 或是 .gitkeep 的空檔案,讓Git能"感應"到這個目錄的存在
    • 加入.gitkeep檔案到空目錄讓Git能感應到
    • 利用$ git status 就可以看到Git感應到這個目錄(empty_dir)的存在了,其實是感應到裡⾯那個 .gitkeep 檔案的存在

$ git rm --cached V.S. .gitignore 比較

  • $ git rm --cached V.S. .gitignore 比較
    • $ git rm xxx.html --cached:並不會將檔案真的刪除掉,僅把暫存區(staging area)該檔案從Git控管中移除,脫離Git控管,變為Untracked file狀態;若原本在工作目錄(working directory)中的檔案,不管是否有做過修改(modified)都將留下
    • $ .gitignore:是開發者指定好要Git版控"忽略"掉的檔案和規則,設定好後,Git就不會控管這些檔案了

在專案中的整個.git/ 目錄是Git版控的核心檔案

  • 當檔案被刪除時都還能就回來,因為整個Git紀錄都是放在該專案的根目錄 .git/ 目錄裡面,所以如果真的不小心把 .git/ 刪掉的話,就表示歷史紀錄也被刪掉了,就真的沒救了...

$ git reset 是用來"前往"到指定的Commit物件上的

  • $ git reset的Reset這個英文單字在中文翻譯是"重新設定",但事實上$ git reset指令比較像是"前往"或是"變成",也就是"go to"或是"become"的概念
    • 例如: $ git reset HEAD~2
    • 這個指令應該要解讀成,"我要前往兩個commit之前的狀態" 或是 "我要變成兩個commit之前的狀態",而隨著使用不同的參數模式(mixed or soft or hard),原本的這些檔案就會被丟到不同的區域
    • 實際上$ git reset指令也不是真的刪除或是重新設定commit,只是"前往"到指定的commit物件中,那些看起來好像不見的東西只是暫時看不到,但其實隨時都可以再撿回來

Git四大物件(Blob,Tree,Commit,Tag)彼此之間其實是平行關係

  • Git的四大物件(Blob,Tree,Commit,Tag)彼此之間是"沒有"階層或是目錄,子目錄的關係,大家都是平行的關係,此關係鏈稱為DAG(Directed Acyclic Graph),中文上稱為"有向無循環圖"

Git其實不是在做差異備份,而是在為當時的專案建立快照(snapshot)

  • Git 不是在做差異備份,而是為當時的專案資料建立快照(Snapshot),如果專案內沒有變更的檔案就不會多儲存一份來佔用磁碟空間,而只是增加了一筆這個檔案的對應連結,開發者開啟新版本存取這個檔案時,還是開啟先前的舊版檔案,而不是開啟內容相同的新副本
    • 因此,只要該檔案的內容改了一個字,因為計算出來的SHA-1值不同,Git就會為它做出一顆全新的Blob物件,而"不是只記錄差異"
    • Git在製作新的Blob物件時會先進行壓縮,但為了因應"其實只修改了一點點就要整個檔案重新備份一遍"的作法而有點浪費硬碟的儲存空間,Git有一套自己的資源回收機制
    • Git的資源回收機制,當這個機制"觸發"的時候( 通常會在Git覺得該專案的物件(objects)太多時 ),Git會以非常有效率的方式壓縮物件以及製作索引
      • 情境說明:
      • $ git ls-files -s: 檢視暫存區(staging area)和工作目錄(working directory)中的檔案情況
        • -s (=> --stage): 顯示暫存區(staging area)的檔案資訊
        • git ls-files 顯示暫存區或是工作目錄中的檔案資訊
      • $ git gc: 清除非必要(unnecessary)的檔案和優化本地端儲存庫(local repository)的空間
        • git gc資源回收機制的圖解說明
        • 這個指令會把原本放在 .git/objects/ 目錄下的那些物件全部打包到 .git/objects/pack/ 目錄下
        • git gc後打包的物件會儲存在 .git/objects/pack
      • $ git verify-pack -v <.git/objects/pack/pack-xxxxxx.idx>: 確認Git打包(packed)好的檔案情況
        • -v(=> --verbose): 顯示打包過後的檔案詳細資訊
        • 第一欄: SHA-1值
        • 第二欄: 物件的型態
        • 第三欄: 檔案大小
    • 統整: Git什麼時候會自動觸發資源回收機制呢?
      • 當在 .git/objects/ 目錄下的物件或是打包過的packfile數量過多的時候,Git會自動觸發資源回收指令
      • 當執行 $ git push 指令把內容推至遠端伺服器時

cat分支合併到dog分支&把dog分支合併到cat分支有什麼不同呢?

  • 在Git中,合併分支是哪個分支合併掉哪個分支其實沒有太大差異
    • 情境說明
      • A合併B分支&B合併A分支的圖解說明
        參考圖片出處https://gitbook.tw/
      • cat分支dog分支都是來自master分支
      • 假設這次是cat分支要合併掉dog分支,為了要進行這次的合併,Git會做出一個新的Commit物件,而這個新的Commit物件會分別指向cat分支dog分支,而HEAD會繼續隨著cat分支往前,而dog分支會留在原地
      • 當A合併掉B分支後的圖解說明
        參考圖片出處https://gitbook.tw/
    • 結論: 其實以"結果"來看,誰合併誰其實沒差,因為合併後產生的新的Commit物件的內容都包含了兩個原來的分支(cat & dog)
      • 細節提醒: 這兩個分支都是平等關係,細微的差異是cat合併掉dog分支的話,cat分支會往前移動
      • 細節提醒: 合併分支後而產生的新的Commit物件其實裡面會紀錄誰是誰的老爸,當cat合併dog分支的話,cat分支就會寫在前面

合併分支其實不是真的在合併分支!

ResetRebaseRevert 三個指令有什麼差別?

指令 改變歷史紀錄 說明 適合使用情境
Reset 把目前的狀態設定成某個指定的 Commit 的狀態 通常適用於尚未推出去的 Commit
Rebase 不管是新增、修改、刪除 Commit 都相當方便,用來整理、編輯還沒有推出去的 Commit 相當方便 通常也只適用於尚未推出去的 Commit
Revert 新增一個 Commit 來反轉(或說取消)另一個 Commit 的內容,原本的 Commit 依舊還是會保留在歷史紀錄中。雖然會因此而增加 Commit 數 但通常比較適用於已經推出去的 Commit,或是不允許使用 Reset 或 Rebase 之修改歷史紀錄的指令的場合

標籤(Tag)分支(branch)有什麼不同呢?

  • 標籤(Tag)分支(branch)都是一種指標,都會放在 .git/refs/ 目錄裡面,但放的位置不同
    • 標籤(Tag) 會放在 .git/refs/tags/ 目錄裡面
    • 分支(branch)會放在 .git/refs/heads/ 目錄裡面
  • 可先參考Tag (標籤) 是什麼?
  • 可先參考分支(branch)是什麼?
  • 標籤(Tag)分支(branch)的內容看起來會很像,都是40個字元的SHA-1值
    • 輕量標籤(lightweight tag)指向的是一個Commit物件
      • $ cat .git/refs/tags/<輕量標籤的名稱>
      • 檢視cat .git/refs/tags/<輕量標籤的名稱> 的內容
    • 有附註標籤(annotated tag)指向的是一個Tag物件,而這個Tag物件才會指向一個Commit物件
      • $ cat .git/refs/tags/<有附註標籤的名稱>
      • 檢視cat .git/refs/tags/<有附註標籤的名稱> 的內容
  • 重要: 標籤(Tag)分支(branch)最大的差別在於
    • 分支(branch) "會隨著" commit紀錄移動,也就是說當Git往前推進一個commit紀錄時,它所在的分支會跟著往前移動
    • 標籤(Tag) 不會隨著 commit紀錄移動,也就是說標籤一旦貼上去後,不管commit紀錄怎麼前進,標籤還是會停留在原來貼的那個位置上
  • 結論: 某方面來說,我們可以把分支(branch)視為會移動的標籤(Tag)

Unreachable物件 & Dangling物件 有什麼不同呢?

  • Unreachable Object : 沒有任何物件或指標指著它,如同字面上說的"無法到達的"; 但它仍可以指向其他物件
  • Dangling Object : 跟Unreachable物件一樣,沒有任何物件或指標指著它,它也沒有指著其他物件,如同完全"懸在天邊"的一顆物件
  • Dangling物件可以算是Unreachable物件的子集合,它也是一種Unreachable物件,所以在進行$ git gc時,也會一起被回收走
  • 什麼情況下Git會產生出Dangling物件呢?
    • 情境說明
    • $ echo "test for dangling object" > dangling_object.html
    • $ git add --all
    • $ git commit -m "新增dangling object 內容"
    • 上述三步驟再重複做一遍,已新增出測試要用的2次commit紀錄
    • $ git reset HEAD~2 --hard: 切回到HEAD指標指向的前2次Commit物件
    • $ git fsck --unreachable --no-reflogs: 顯示所有Unreachable狀態的物件和 Dangling狀態的物件
      • Dangling物件的統整筆記
    • 注意! 此時的3d3bce0,4cfc041這兩個Commit物件都已經變成Unreachable狀態的物件;
    • $ git fsck --no-reflogs: 只顯示出Dangling狀態的物件
      • 利用git reflog檢視透過手動建立Dangling物件後,HEAD指標的變化紀錄

$ git log & git reflog 有什麼不同呢?

  • $ git log: 可以顯示所有提交(commit)過的版本資訊

    $ git log - Show commit logs --- by 官方文件

    • git log的畫面
  • $ git reflog: 可以檢視所有分支的所有操作記錄(包括已經被刪除的commit記錄Reset操作)

    $ git reflog - Manage reflog information --- by 官方文件

    • git reflog的畫面

實戰情境題

git stash - Stash the changes in a dirty working directory away
git filter-branch - Rewrite branches
git cherry-pick - Apply the changes introduced by some existing commits
git fsck - Verifies the connectivity and validity of the objects in the database

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

  • 新增了一個檔案叫做abc.txt
  • 執行 $ git add abc.txt 把檔案加到暫存區
  • 又再編輯了一次該檔案
  • 正確做法:必須再將該檔案 $ git add abc.txt 到暫存區一遍
  • git add後又編輯檔案圖解說明

如果不小心使用$ git reset --hard 模式,能救回來嗎?

  • 假如不小心$ git reset HEAD~2 --hard,
  • $ git reflog 是當HEAD有移動的時候(例如切換分支or $ git reset 時,Git都會在 Reflog 上面做記錄),在 Reflog 中,越新的紀錄寫在越上面
    • 官方定義: Reference logs, or "reflogs", record when the tips of branches and other references were updated in the local repository.

  • 可以利用$ git reflog 來檢視HEAD的變化紀錄
    • 當切換分支時,HEAD會變動
    • 當$ git reset 時,HEAD會變動
  • 解決方法:直接回到正確(=>reset錯誤前的那一次)的commit物件上
    • $ git reset c015c1f --hard
    • 就可以把剛剛的東西救回來了
    • git reset --hard後透過git reflog+git reset來挽救的圖解說明
    • 補充: $ git reflog = $ git log -g = $ git log --walk-reflogs

如果這次只想commit一個檔案中的部份內容的話,該怎麼做呢?

  • git add -p xxx.hmtl: 在$ git add將檔案加入暫存區(staging area)的時候,加上 -p 參數,透過互動式介面,選擇要$ git add的範圍
    • -p (=> --patch): 可以互動式地選擇該檔案的哪些部分要加入暫存區(staging area),哪些不用(留在工作目錄就好)
      • e (=> edit): 編輯該檔案中的哪些部分要新增到暫存區(staging area)
    • git add -p選擇要新增到暫存區的範圍

如果修改專案的某些檔案到一半,卻不小心先切換到別的分支(branch),該怎麼辦?

  • $ git checkout -b tiger: 先切換到tiger分支
  • $ echo tiger 123 > tiger_test.html: 新增一個檔案tiger_test.html,並在裡面輸入一串文字
  • 也編輯pizza.html,新增一行文字
  • 此時,切換到master分支,會發現剛剛新增的檔案(tiger_test.html)以及修改的檔案(pizza.html)都還是會留在工作目錄(working directory)不受影響
  • 檔案修改到一半卻git checkout分支的圖解說明-part_總結
  • 總結: Git切換分支( $ git checkout <branch name> )時,並不會影響到已經在工作目錄(working directory)的那些修改喔!

如果不小心把還沒合併的分支刪掉了,該怎麼挽救?

  • 可先參考 分支(branch)是什麼? 的刪除分支篇
  • $ git branch -D <尚未合併的分支名稱>: 強制刪除該分支( 不管該分支是否已經合併(merged)到其上游分支(upstream branch) )
    • 此時會顯示"被刪除的分支"當下是指向哪個Commit物件,可以先記下來這個Commit物件的id
    • 不小心刪掉尚未合併的分支的圖解說明
    • 補充:分支(branch)只是一個指向某個Commit物件的貼紙,刪除這個貼紙"並不會"造成那些Commit物件消失
    • 所以既然刪掉分支後,那些Commit物件都還在的話,只是因為我們當下通常沒有記下那些Commit物件的id,所以比較不容易再拿回來使用,這時我們可以創造一個新的分支(new branch)來把這些Commit物件都接回來!
      • $ git branch <新的分支名稱> <剛剛被刪掉的分支當下指向的Commit物件的id>: 建立一個新的分支(new branch)並讓該分支指向這個Commit物件,相當於再拿一張新的貼紙貼回去的意思
    • 補充: 若當下刪除"尚未合併分支"的時候,沒有記下該分支當下指向的Commit物件的id,可以用git reflog來找找看,因為Git的Reflog會預設保留30

如果想從過去的某次commit紀錄再長一個新的分支(branch)出來,該如何做呢?

  • 第一步驟: 必須先回到過去的那個commit紀錄的狀態
    • $ git checkout <要回到的過去的那個Commit物件的id>
  • 第二步驟: 新增一個分支(branch)在過去的那個Commit物件
    • $ git checkout -b <新的分支名稱>
  • 補充: 在執行第一步驟時
    • 這時候可能會發生 detached HEAD 狀態 (是因為過去的那個Commit物件目前沒有分支指向它)
    • 這時候可以再切換到過去的那個commit紀錄時,同時新增一個分支(branch),以防跳出 detached HEAD 狀態
  • 也可以直接一行指令完成,以下兩種方式都是在表達"請Git幫我在<要回到的過去的那個Commit物件>上貼上一張ocean的分支(branch)貼紙"
    • $ git branch <新的分支名稱> <要回到的過去的那個Commit物件的id>
      • 例如: $ git branch ocean
    • 也可以更方便地,在回到過去的commit紀錄時,新增一個分支,並同時切換到那個分支
    • $ git checkout -b <新的分支名稱> <要回到的過去的那個Commit物件的id>
    • 回到過去的某個commit紀錄並新增分支_統整

當兩個分支都編輯了同一個檔案(both modified 狀態),造成合併分支時發生衝突了,該怎麼解決呢?

  • 假設有兩個分支都編輯過同一個檔案,若檔案內容不一致,會造成both modified 狀態,造成合併分支時會發生衝突
  • 情境說明1
    • Merge方式合併分支時發生衝突
    • 建立一個merge_conflict.html的測試合併分支衝突的檔案
    • master,建立一個sun分支(branch)
    • master,建立一個moon分支(branch)
    • 此時先切換到sun分支,並新增"我是sun分支"到merge_conflict.html的測試合併分支衝突的檔案,加到暫存區後,再commit到本地端repo
      • $ git checkout sun
      • 新增"我是sun分支"到merge_conflict.html的測試合併分支衝突的檔案
      • $ git add --all
      • $ git commit -m "sun分支編輯merge_conflict.html"
      • 合併分支衝突圖解_sun分支
    • 再切換到moon分支,並新增"我是moon分支"到merge_conflict.html的測試合併分支衝突的檔案,加到暫存區後,再commit到本地端repo
      • $ git checkout moon
      • 新增"我是moon分支"到merge_conflict.html的測試合併分支衝突的檔案
      • $ git add --all
      • $ git commit -m "moon分支編輯merge_conflict.html"
      • 合併分支衝突圖解_moon分支
    • $ git merge sun: 將moon分支合併到sun分支的上面
      • 此時Git有能力幫忙檢查簡單的衝突
      • 可以決定要接受哪個分支的修改,也可以都接受雙方的修改
      • git merge遇到both modified狀態_圖解說明`
    • 最後用Sourcetree來檢視一下最後的History
      • "!" : 代表"有衝突"的檔案
      • Sourcetree檢視sun和moon分支同時編輯同一個檔案會造成合併分支衝突的圖解說明
    • 統整圖解說明
      • 合併分支衝突(both modified 狀態)該如何解決_統整
  • 情境說明2
    • Rebase方式合併分支時發生衝突
    • 用git rebase來合併分支卻發生衝突時的console錯誤畫面
      參考圖片出處https://gitbook.tw/chapters/branch/fix-conflict.html
    • 此時HEAD並"沒有指向"任何一個分支,它現在有點像是在修改歷史的時候,卡在某個時空縫隙裡的狀態
    • Sourcetree檢視目前Rebase方式來合併分支卻發生衝突時,HEAD當下不會指向任何一個分支的圖解說明
      參考圖片出處https://gitbook.tw/chapters/branch/fix-conflict.html
    • 檢視一下目前Git的狀態
      • $ git status
      • 當用Rebase方式來合併分支卻發生衝突時,可以先用git status來檢視目前的狀態
        參考圖片出處https://gitbook.tw/chapters/branch/fix-conflict.html
      • 這時候的Git狀態清楚地顯示"rebase in progress"(=> 正在進行中的)
      • 同時,兩個分支同時都編輯到的那個檔案會被標示成both modified 狀態
      • 此時可以直接按照平常的流程,"先將該衝突檔案修正完成後"
      • 再將"已修正的檔案" 加到 "暫存區"(staging area)
        • $ git add <已修正的檔案>
      • 最後,接著繼續完成剛剛中斷的Rebase合併分支的操作
        • $ git rebase --continue
          • --continue: 當已經修改完衝突檔案後,繼續 "重新啟動" Rebase機制
  • 情境說明3
    • Merge方式合併分支時發生衝突(=> "當兩個分支都有一個同名稱的圖片檔案時的情況")
    • 雖然Git有能力幫忙檢查簡單的衝突並標示出"衝突點"在該檔案哪幾行; 但是如果是圖片檔之類的二進位檔的話,就必須用別的方式來解決了
    • 建立一個cutest_animal.jpg的測試合併分支衝突的同名圖片檔案
    • master,建立一個cat分支(branch)
    • master,建立一個dog分支(branch)
    • 先切換到cat分支並新增一個"同名稱的圖片檔案"(cutest_animal.jpg)
      • 新增一個cat分支,且新增一個cutest_animal的圖片檔案(內容為貓)
    • $ git add --all
    • $ git commit -m "cat分支 新增cutest_animal.jpg"
    • 再切換到dog分支並新增一個"同名稱的圖片檔案"(cutest_animal.jpg)
    • $ git add --all
    • $ git commit -m "dog分支 新增cutest_animal.jpg"
    • 此時切換到cat分支,利用指令合併到dog分支上面
      • $ git merge dog
        • 這時會發生合併分支的衝突,並且顯示Git無法自動解決該衝突(Merge conflict in pic/cutest_animal.jpg & Automatic merge failed)`
        • 當兩個分支都有同一個檔名的圖片檔案時,這時候用Merge方式合併時會發生衝突
      • 這時候可以決定要採用哪個分支的同名稱圖片檔案(cutest_animal.jpg)
        • $ git checkout --ours/--theirs <路徑/決定要用的同名稱圖片檔案的檔名>
        • 假如決定要用"目前所在分支的"圖片檔案
          • $ git checkout --ours cutest_animal.jpg
          • --ours: 決定要用"目前所在分支的"圖片檔案
        • 假如`決定要用"另一個分支的"圖片檔案
          • $ git checkout --theirs cutest_animal.jpg
          • --theirs: 決定要用"另一個分支的"圖片檔案
    • 接著記得繼續按照平常的流程,將"已修正的檔案" 加到 "暫存區"(staging area)
    • $ git add <路徑/決定要用的同名稱圖片檔案的檔名>
    • $ git commit -m "git merge圖片檔案發生衝突時,可以用git checkout --ours/--theirs參數 來處理合併分支的衝突"
    • 用Sourcetree來檢視利用git checkout --ours/--theirs <發生衝突圖的檔案名稱> 來解決合併分支衝突時,若衝突的檔案為圖片時的情況_圖解說明

目前的工作做到一半,如果要臨時切換到別的任務,該怎麼做呢?

  • 情境說明
  • 假設現在臨時有個重大Bug要修復,但是當下正在develop分支開發新功能,開發到一半; 這時候可以用以下2種方式來處理
  • commit目前的進度
    • $ git add --all
    • $ git commit -m "尚未完成的新功能開發分支"
    • 接下來就可以切換到重大Bug所在的分支(branch)先進行修復,待完成之後再切換回來原來做一半的develop分支
      • $ git checkout develop
    • 再利用Reset回到HEAD指標目前指向的Commit物件("尚未完成的新功能開發分支")的前一次HEAD紀錄,把剛剛開發到一半的東西拆出來繼續開發
      • $ git reset HEAD^
  • 利用Git的Stash功能
    • $ git stash: 將"暫存區"(staging area)的檔案都加到Stash
      • $ git stash 預設就是相當於 $ git stash push
      • -u (=> --include-untracked): 可以加上這個參數,讓工作目錄(working directory)中的Untracked Files也一起加入到Stash
      • git stash -u 將所有檔案(包含Untracked Files)的當下狀態都儲存起來
    • 也可以輸入 $ git stash save 'xxx description': 將"暫存區"(staging area)的檔案都加到Stash中,並能自定義對該 stash 的描述
      • E.g. 此方法會回傳 stash@{<編號>}: On <分支名稱>: <自定義的 stash 描述> stash@{0}: On ch02: xxx description
    • 這時候檢視Git狀態,會發現當下的狀態跟剛commit完一樣乾淨
    • $ git stash list: 列出所有目前擁有的Stash項目
      • 最前面的stash@{0}就是這個Stash的代名詞
      • 後面的WIP on <該分支的名稱>WIP是"Work In Progress",也就是工作進行中的意思
      • Stash可以建立好多個
      • git stash list 列出目前所有的stash
    • $ git stash pop stash@{<編號>}: 將某個Stash項目拿出來並套用到目前的分支上; 當套用成功後,該Stash就會被刪除
      • 如果使用 $ git stash pop指令,卻沒有指定要套用哪個Stash的話,就會從最小的開始,相當於從stash@{0}開始拿(也就是最後疊上去Stash的那次)
      • git stash pop stash@{0} 可以用來將某個stash拿出來並套用在目前的分支上
    • $ git stash apply stash@{<編號>}: 將某個Stash項目拿出來並套用到目前的分支上; 但是當套用成功後,但"不要"將該Stash刪除,還是會留在Stash List
    • $ git stash drop stash@{<編號>}: 從Stash List中刪除該Stash
    • $ git stash clear: 從Stash List中清除所有的Stash

如果不小心將機敏資料(ex:帳號、密碼)放在Git裡,想把它刪掉,該怎麼做呢?

  • 可先參考如果有特定檔案不想放在Git裡面一起備份或是上傳到Git Server的話,例如:資料庫密碼,雲端伺服器的金鑰...可以加入 .gitignore
  • 首先,如果是已經推送(Push)上去到GitHub上的話,請先不要想要用Git來解決,強烈建議直接先改密碼再說! 改完密碼後,再接著利用Git做後續補救措施
  • 情境說明
  • 假設我想要把config/database.yml這個檔案從每個Commit物件裡把它拿掉,比較辛苦的做法是使用 Rebase指令,然後一個一個進去Commit物件去編輯
  • 那如果不想使用Rabse指令的方式來處理的話,有以下3種方法可以補救
    • 整個砍掉重練(=>不建議這樣做)
      • $ rm -r .git/: 將整個.git/目錄刪掉
      • 把所有含有機敏資料的相關檔案"刪除"或是"修正"
      • 重新commit一次
        • $ git add --all
        • $ git commit -m "initial commit"
    • 利用 $ git filter-branch指令
      • $ git filter-branch --tree-filter <command>
        • $ git filter-branch 這個指令可以讓你根據不同的filter條件,一個一個應用到每個Commit物件上來處理
        • --tree-filter <command>: 會在接下來$ git checkout 的每個Commit物件時,都會執行後面指定的<command>,全部執行完後會再自動重新再commit一次
        • 假設是刪除了一個機敏資料的檔案的話,因為是"刪除"的行為,所以對Git來說在那之後的所有Commit物件都要全部重新計算,也就是相當於產生一份新的commit紀錄
        • 備註: 檔案一旦加入Git版控中,要徹底刪除就沒那麼好刪除了
        • 例如: $ git filter-branch --tree-filter "rm -f config/database.yml" => 將config/database.yml這個檔案從每次的Commit物件中刪除
      • 如果又後悔了,想要回復剛剛$ git filter-branch造成的結果以前的狀態,該怎麼辦呢?
        • 原理: 當執行$ git filter-branch指令的時候,Git會幫你把之前的狀態備份一份在 .git/refs/original/refs/heads/ 這個目錄裡面
        • 其實與其說是備份,也只是備份開始進行filter-branch之前的那個HEAD指向的Commit物件SHA-1值而已
        • 接著就可以利用hard Reset指令直接切回去原來的Commit物件,這樣就都回來了
    • 利用 $ git push -f:重新"強制"推送(Push)一份你剛剛已經$ git filter-branch過後的commit上去來覆蓋掉它
      • 提醒: 因為其實已經推送(Push)出去的東西就跟潑出去的水一樣,收不回來的

可以只取用某個分支的其中某幾個commit嗎?

  • 情境說明
  • $ git cherry-pick <要取用的Commit物件的id>: 可以只選取指定的commit來用
  • 假設在red分支新增一個cherry-pick-test.html檔案,並用cherry-pick指令將這個commit拿來使用,加到目前所在的分支上面
    • $ git checkout -b red: 新增一個red分支並切換到該分支上
    • $ echo "test $ git cherry-pick" > cherry-pick-test.html: 新增一段文字到測試檔案中
    • $ git commit -a -m "新增git cherry-pick筆記": 將測試檔案加到暫存區(staging area)並同時提交到儲存庫(repo)
    • $ git checkout master: 切換回master分支
    • $ git cherry-pick d685d6f: 選取red分支上剛剛提交的那個Commit物件,並將它加到目前所在分支的最前面
    • git cherry-pick 可以用來選取某個分支的某個commit來使用_統整
  • $ git cherry-pick <要取用的Commit物件的id 1> <要取用的Commit物件的id 2>: 可以一次選取多個指定的commit來用
  • $ git cherry-pick <要取用的Commit物件的id> --no-commit: 撿過來的commit不會直接合併,而是先放到暫存區(staging area)
    • --no-commit: 撿過來的commit不會直接合併,而是會先放在暫存區(staging area)

如何把一個檔案從Git版控中真正的移除掉呢?

  • 因為Git是一個分散式的版本控制系統,當檔案被Git版控後,就很難真正的移除掉了
  • 想要將已被Git版控的檔案,完全從Git移除掉的話,有以下3種方法可以做到
  • 情境說明
  • 該怎麼完全刪除檔案的commit紀錄_圖解說明
    參考圖片出處https://gitbook.tw/chapters/faq/remove-files-from-git.html
    • 將整個 .git/ 目錄完全移除
    • Rebase方式進行編輯,重整commit紀錄 (=> 適用於當commit紀錄的數量不算多的時候)
    • 利用$ git filter-branch --tree-filter <command>,可以大範圍的對每個Commit物件都執行一次該指定的命令(<command>),並於修改完成後再自動重新commit
      • 例如: $ git filter-branch -f --tree-filter "rm -f config/database.yml"
        • 注意! 多加了一個 "-f"參數,是因為要強制覆寫filter-branch的備份點,這邊使用filter-branch指令把檔案從工作目錄(working directory)移除掉,這時候database.yml確實不見了,但還有好幾個跟資源回收有關的事情需要處理
        • -f (=> --force): 強制覆寫filter-branch的備份點

        git filter-branch -f: git filter-branch refuses to start with an existing temporary directory or when there are already refs starting with refs/original/, unless forced.

      • 可以參考如果不小心將機敏資料(ex:帳號、密碼)放在Git裡,想把它刪掉,該怎麼做呢?
    • 因為執行$ git filter-branch指令,所有Git會自動有備份點,隨時可以透過它再跳回去,所以要先斷了這條線
      • $ rm .git/refs/original/refs/heads/master
    • $ git reflog expire --all --expire=now: 要求Reflog現在立刻過期(因為Reflog預設會自動保存30天內的紀錄)
      • $ git reflog expire: Reflog的子命令expire會修剪掉過去"舊的" Reflog紀錄

      The "expire" subcommand prunes older reflog entries.

      • --all (=> --force): 對所有的Reflog紀錄做處理
      • --expire=<time>: 修剪掉比<time>這個時間還"舊"的Reflog紀錄
    • $ git fsck --unreachable: 驗證所有的Git物件在.git/objects中,並只顯示出"仍然存在"但是為"Unreachable狀態"的物件
    • 最後,啟動Git的資源回收機制,請垃圾車"現在"來把它們載走
    • 可以檢查看看是否真的完全刪除了該檔案,可以利用以下2種方法

遠端共同協作篇---以 GitHub 為例

GitHub 官方文件(Official Docs) - https://docs.github.com/en
GitHub 官方網站(Official Website) - https://github.com/
git remote - Manage set of tracked repositories
git push - Update remote refs along with associated objects
git pull - Fetch from and integrate with another repository or a local branch
git fetch - Download objects and refs from another repository
git clone - Clone a repository into a new directory
Forking Projects - 如果我們想要對一個在GitHub上已經存在的開源專案做出貢獻(修改),但是我們沒有推送(Push)到這個repository的權限的話,我們可以Fork一份這個專案到自己的GitHub上,成為屬於存在於我們自己的GitHub namespace,這樣我們自己就有權限可以推送(Push)到這個repository上面去了
Pull Request - 會發送一個"通知"(PR)給原作者,請求原作者同意將我們之前Fork回來並修改後的內容合併(Merge)到原作者的專案中; 同時原作者也可以不用擔心需要開放編輯權限給所有開發者們而難以管理
git format-patch - Prepare patches for e-mail submission
git am - Apply a series of patches from a mailbox

GitHub 基礎使用操作說明

  • GitHub是目前全球最大的Git Server,可以幫忙貢獻其他人的專案,並且其他人也可以回饋到你的專案,建立良性循環
  • Git是一個用來做版本控制的軟體,而GitHub是一個網站
    • 其中GitHub的本體是一個Git Server,網站則是用Ruby on Rails開發的
  • GitHub於 2019/01 以後,開始可以免費建立private repository

新建一個GitHub repository,並推送本地端repository到GitHub上

  • 如何建立一個新的GitHub repository呢?
    • 官方建議每當新建一個repository時,需包含以下3種檔案
      • README.md: 該repository的使用說明
      • LICENSE.md: 該repository的授權範圍
      • .gitignore: 該repository指定"不要"追蹤的檔案有哪些?
    • 流程如以下說明
    • 可以選擇要用 HTTPS or SSH 的方式來連線到這個GitHub repository
      • HTTPS:在上傳時需要輸入帳密,如果不需要大多是帳密(Key Chain)已存在電腦內
      • SSH: 需要先在電腦內設定好金鑰(SSH key),並儲存這個金鑰到自己的GitHub上,上傳時就不需要輸入額外帳密
    • 如果是全新開始,可以參考官網上的提示 "create a new repository on the command line" 的指示進行
      • $ echo "# gitLearn" >> README.md
      • $ git init
      • $ git add README.md
      • $ git commit -m "first commit"
      • $ git remote add origin git@github.com:<自己的username>/<要新建的repository的名稱>.git
        • $ git remote add: 代表要設定一個遠端(remote)的節點
        • origin: 是一個慣用的代名詞,作為遠端節點的預設名稱,指的是後面那串GitHub伺服器的位置
        • 如果之後想更改的話,可以直接在輸入一次這個指令更改
          • 例如: $ git remote add superman git@github.com:<自己的username>/<要新建的repository的名稱>.git
      • $ git push -u origin master
        • 設定好遠端節點後,就要將東西推送(Push)上去了
        • 情境說明
        • 這個指令會將master分支的內容推送到遠端的origin節點
          • 如果master分支不存在的話,就會建立一個叫做master的同名分支
          • 但如果本來遠端的origin節點就存在master分支的話,便會移動遠端的origin節點上master分支的位置,使它到目前最新的進度上
          • -u (=> --set-upstream): 設定upstream; 所謂"上游(upstream)"的概念其實就只是另一個分支的名字而已。在Git裡,每個分支可以設定一個上游(upstream),但每個分支最多只能設定一個上游(upstream),它會指向並追蹤(track)某個分支。通常上游(upstream)會是遠端Server上的某個分支,但其實要設定在本地端的其他分支也可以
          • 如果有像這個指令設定好後,未來它就會被當作預設值; 以這個指令來看,就會把 origin/master 設定為本地master分支的 上游(upstream),下次$ git push時,就可以不加任何參數,Git就會預設你是要推送到origin這個遠端節點,並且把master這個分支的內容推送上去
            • 例如: $ git push
        • 補充1: 如果沒有設定好upstream的話,就必須在每次推送出去時,都要跟Git講清楚
          • 例如: $ git push origin master = $ git push origin master:master
        • 補充2: 如果推送上去遠端origin節點時,不想要用跟本地端分支的名稱相同的話,可以採取以下做法
          • 例如: $ git push origin master:dog
            • 這樣代表當把本地端的master分支推送上去遠端origin節點時,就"不會"建立一個origin/master分支,而是建立(or 更新進度)一個叫做origin/dog的遠端分支
    • 如果是要上存本機端(local)現存的專案,則可以參考官網上的提示 "push an existing repository from the command line" 的指示進行
      • $ git remote add origin git@github.com:<自己的username>/<要新建的repository的名稱>.git
      • $ git push -u origin master

從遠端repository利用Pull下載並更新

  • 可以利用Pull指令將遠端儲存庫(remote repository)下載回本地端儲存庫(local repository)並更新
    • 當執行Fetch指令時,Git會檢查一下遠端repository的版本內容後,將目前遠端repository有,但是目前本地端repository沒有的內容下載一份回來,同時移動origin相關的分支
    • 在原理上, $ git pull = $ git fetch + $ git merge FETCH_HEAD
    • Fetch指令說明
      • 適用情境: 只是想確認遠端數據庫的內容卻不是真的想合併
      • 可以取得遠端數據庫的最新歷史記錄; 取得的提交會導入在自動建立的分支中,並可以切換這個名為FETCH_HEAD的分支
      • $ git fetch <遠端origin節點>: 將整個遠端origin節點的全部更新,全部下載回來(預設情況是取回所有分支的更新)
        • 例如: $ git fetch origin
      • $ git fetch <遠端origin節點> <遠端分支的名稱>: 只將遠端origin節點的特定分支(remote branch)的更新下載回來
        • 例如: $ git fetch origin master
      • $ git fetch --all: 會從另一個repository下載分支(branch)和標籤(tag)
        • --all: 下載全部遠端分支(remote branches)的內容
      • 情境說明
        • 因為現在這個專案之前有推送東西到Server上,所以遠端分支也會記錄一份在本機上,一樣也是有HEADmaster分支,但會在前面加註遠端節點origin,變成origin/HEADorigin/master
        • 其實之前在Push指令時有加-u (=> --set-upstream)參數,就是用來設定upstream(上游)用的,所以目前這個origin/master遠端分支其實就是本地master分支的upstream喔
        • 利用 $ git fetch origin master遠端分支(origin/master)下載一份回到本地端的master分支上面
        • 因為這個遠端分支(origin/master)是從master分支分出去的,而且進度還比master分支還要新,可以利用Merge指令合併它們
          • $ git merge origin/master: 將目前所在的分支合併到遠端分支(origin/master); 就能讓本地端分支(master)跟上遠端分支(origin/master)的進度了
          • 可參考分支(branch)操作的$ git merge
        • git fetch + git merge 的完整流程_統整
          參考圖片出處https://gitbook.tw/chapters/github/pull-from-github.html
        • 補充1: 其實可以把origin/master這種遠端分支視為一個從原來本地端master分支分出去的一個分支就好了
        • 補充2: 因為遠端分支(origin/master)與本地端分支(master)是來自同一個源頭,所以在這次的Merge過程,Git會預設使用快轉模式(fast-forward模式)
    • Pull指令說明
      • $ git pull: 其實就是去遠端origin節點抓東西回來(Fetch),並且更新本機的進度(Merge)而已
      • 執行Pull指令時,如果內容沒有衝突,就會自動建立合併提交。如果發生衝突的話,需先解決衝突然後再手動提交
      • 也可以只用Rebase方式來合併分支
        • 可先參考分支(branch)操作的$ git rebase
        • $ git pull --rebase: 指定使用Rebase方式來合併(Merge)下載回來的內容
          • -r (=> --rebase): 當從遠端repository下載(Fetch)回來後,將目前所在的分支(current branch)用Rebase的方式合併到其上游分支(upstream branch)的上面
        • 適用情境: 當不想因為合併分支而多產生一次commit紀錄的話,可以用Rebase方式來合併分支

為什麼有時候會推送(Push)不上去遠端repository呢?

  • 遠端origin節點的repository內容比我們的本地端分支的這份內容還要"新"的話,Git就不會讓我們推送(Push)上去
  • 有時候是因為多人共同協作時,當遠端origin節點的分支已經先被某人更新時,這時候其他人就會發生推送(Push)不上去的情況
  • 這時候會有2種方式可以無法推送(Push)的問題
    • 方法一: 先更新本地端儲存庫(local repository)的內容,再推送上去遠端儲存庫(remote repository)一次
      • $ git pull --rebase: 將遠端origin節點的repository下載回來後,指定要用Rebase方式來合併
        • --rebase: 將目前所在的分支rebase到遠端origin節點上游分支(upstream branch)的"上面"
      • 此時如果合併沒有發生衝突的話,就可以順利的往上推送(Push)了
      • git push --rebase 指定要用Rebase的方式更新本地端repository的內容後,再推送上去遠端儲存庫一遍
        參考圖片出處https://gitbook.tw/chapters/github/fail-to-push.html
    • 方法二: 無論規則,強制推送(Push)上去
      • $ git push -f: 強制將目前本地端repository的內容推送到遠端origin節點的repository上面,同時會將原本遠端repository的內容"覆蓋掉"

從伺服器上複製遠端repository下來

  • 可以利用$ git clone <HTTPS or SSH> 指令來將遠端的repository複製一份到本地端來檢視
  • 此處的Clone指令的用途,不單純只有複製所有實體檔案,同時也包含複製了整個專案的以下內容
    • 歷史紀錄(log & Reflog)
    • 分支(Branch)
    • 標籤(Tag)
  • 如果想要複製下來後,重新命名這個repository的話,可以利用以下指令
    • $ git clone git@github.com:Hans-Tsai/gitLearn.git <新命名的分支名稱>
  • 在Git Version 1.7.10之後,如果想要只單純地從遠端repository複製特定的分支下來到本機端的話,可以利用以下指令
    • $ git clone -b <指定要複製的分支名稱> --single-branch git@github.com:Hans-Tsai/gitLearn.git
    • -b <指定的分支名稱> (=> --branch <指定的分支名稱>): Instead of pointing the newly created HEAD to the branch pointed to by the cloned repository’s HEAD, point to branch instead. In a non-bare repository, this is the branch that will be checked out. --branch can also take tags and detaches the HEAD at that commit in the resulting repository.
    • --single-branch: 只複製該指定要複製的分支的"最新"歷史commit紀錄,可以搭配--branch參數來使用

Clone(複製) 和 Pull(拉下來更新) 有什麼不同呢?

  • Clone(複製)
    • 使用情境
    • 當這個專案是第一次看到,我們想要"複製"一份到本地端來看看的時候可以用Clone(複製)指令
    • $ git clone <remote repo by HTTPS or SSH>: 從遠端repository複製一份這個專案到本地端的電腦
  • Pull(拉下來更新)
    • 使用情境
    • 當這個專案我已經下載回來了(本地端已經有一份這個專案了)的時候,只想要"更新"到該專案最新版的遠端repository的內容的話,這時候可以使用Pull(拉下來更新)指令
    • $ git pull <遠端節點名稱(ex: origin)> <要合併到目前所在分支上面的遠端分支名稱>: 從遠端repository拉下來(Fetch)一份到本地端且合併(Merge)到目前所在的分支上面

利用Pull Request來跟其它開發者的開源專案做互動

  • 因為我們沒有權限來推送(Push)更新到GitHub上面別人的專案上,Fork可以用來將GitHub上別的開發者寫的開源專案,"複製"一份到我們自己的GitHub namespace上,這樣我們就有權限可以推送(Push)更新到這個專案上了
  • Fork的流程介紹
    • 先複製(Fork)一份原作的專案到你自己的GitHub帳號底下
      • GitHub Fork別人的專案的按鈕
    • 因為這個複製回來的專案已經在你自己的GitHub帳號下,所以你就有完整的權限,想怎麼改就怎麼改
      • $ git clone <自己GitHub上剛剛Fork回來的那個repository>
    • 改完後,先推回(Push)"你自己帳號"的專案
      • $ git push
    • 然後發個"通知"(Pull Request),讓原作者知道你有幫忙做了一些事情,請他看一下
      • 這就是Pull Request,Pull Requset其實就是發一個請求請原作者拉回去(Pull)的請求(Request),可以簡稱為"PR"
      • GitHub 檢視Pull Request的頁面
      • 回到自己的專案頁面,點選"New pull request"的按鈕,再點選"Create pull request"的按鈕後,可以選擇要PR到原作的哪個分支
      • 當按下Create pull request按鈕後就可以新建一個PR
    • 原作者看完後說"我覺得可以",然後就決定把你做的這些修改合併(Merge)到他的專案裡
      • 當原作者覺得這次的修改沒問題的話,就可以收下這次的PR,點選"Merge pull request"的按鈕後,就會合併(Merge)這次的commit了
  • 可參考官方文件提供的Fork流程 https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo
  • 可參考官方文件提供的Pull Request流程 https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests
  • 在多人共同協作的實戰情境中,通常會先挑選固定一個分支作為可以上線的正式版本分支,慣例上常會挑選master分支或是production分支作為正式分支; 每位開發者都會先從公司的專案Fork一份到自己的GitHub帳號裡面,再下載到本機端進行開發,待開發的功能完成後再發PR回公司的專案,此時負責管理這個專案的人收到PR後,可以開始進行Code Review,並確認無誤後便可進行合併,這樣做的優點是可以保持讓這個產品的分支處於隨時都可以上線的狀態

如何跟上當初Fork的專案的進度呢?

  • 當原作者先收下別人的Pull Request時,該專案的進度就會比在我們自己帳號底下的進度還要前面了; 這時如果想要讓自己帳號底下Fork過來的這個專案"跟上"原作專案的目前的進度的話,可以利用以下2種方法
    • 也可以先參考為什麼有時候會推送(Push)不上去遠端repository呢?
    • 方法一: 砍掉重新Fork一遍
    • 方法二: 跟上游同步
      • 先設定原作的遠端節點
        • $ git remote -v: 可以列出遠端節點的url
        • -v (=> --verbose): 詳細列出遠端節點的名稱與url
        • 利用git remote -v 來列出遠端節點的url
        • $ git remote add <遠端節點的名稱> <遠端節點repository的url>: 新增另一個遠端節點名稱與url,而這個遠端節點指的位置就是"原作的專案"
        • 可參考新建一個GitHub repository,並推送本地端repository到GitHub上的$ git remote add
      • 抓取原作專案的內容
        • $ git fetch <剛剛設定好的新的指向原作專案的遠端節點的名稱>: 利用Fetch指令來取得原作專案最新版的內容
      • $ git merge <要合併到的分支名稱>: 利用Merge指令合併到遠端節點(原作專案)的遠端分支上面
        • 例如: $ git merge test_origin/master
        • 這樣一來我們本機的進度就跟原作的是一樣了
      • 如果想要自己在GitHub上那個Fork過來的專案進度也更新到最新版的話,只要將現在本機端的專案推送(Push)上去就好了
        • $ git push origin master
        • 這樣一來,在我們電腦裡的專案,以及在GitHub上從原作那邊Fork過來的專案都會是最新的進度了

如何刪除遠端分支呢?

  • 其實要刪除遠端分支可以透過GitHub上各個Repository最上面的branches圖案點進去後,就可以刪除遠端分支了(垃圾桶圖案)
    • 透過GitHub上的圖形化介面來刪除遠端分支
  • 也可以透過Git指令來刪除遠端分支
    • $ git push <遠端節點的名稱> <要記得在這邊空一格>:<要刪除的遠端分支的名稱>: 其實可以透過推送(Push)時重新命名遠端分支的名稱時,來達到刪除遠端分支的效果
    • 可參考新建一個GitHub repository,並推送本地端repository到GitHub上 的補充2(如何在推送時重新命名一個遠端分支的名稱呢?) 篇

什麼時機適合用$ git push -f 來強制推送(Push)上去到遠端repository呢?

  • 當想要整理Git歷史紀錄or只會影響到自己一人時,可以使用強制推送( $ git push -f )的指令
    • 整理歷史紀錄時,有時候會遇到該專案的commit紀錄非常混亂,於是我們使用了Rebase的方式來修改commit的歷史紀錄,但是因為Rebase相當於修改已經發生的事實,所以正常來說會推送(Push)不上去,這時候就可以使用強制推送( $ git push -f )的指令
    • 當自己要開發一個新功能並從遠端的repository "開了一個新的分支" 出來開發,但是開發完後同樣發現commit紀錄太過瑣碎了,想利用Rebase的方式來修改commit的歷史紀錄,但是因為Rebase相當於修改已經發生的事實,所以正常來說會推送(Push)不上去,"但因為這個歷史紀錄影響的範圍只有我一個人,所以並不影響其他人正常使用",所以這時候就可以使用強制推送( $ git push -f )的指令
      • 例如: $ git push -f origin features/my_branch
      • 這樣只會強制更新features/my_branch這個分支的內容,並不會影響其它分支。
  • 也可以使用GitHub提供的保護機制,可以避免某個分支被強制推送(Force Push)來更新
  • 萬一利用$ git push -f 來強制推送(Push)上去到遠端repository後又後悔了的話,可以換你或是其它有之前進度的隊友,再次進行$ git push -f 指令一次,把正確的內容強迫推上去,蓋掉前一次的$ git push -f 指令所造成的災難
  • 可參考為什麼有時候會推送(Push)不上去遠端repository呢?的 方法二: 無論規則,強制推送(Push)上去 篇

如何透過更新檔(Patch)來更新本機端的repository的內容呢?

  • 在Git和許多版本控制的軟體還沒有誕生以前,開發者們必須透過Email來寄送更新檔案
  • 但是有了Git之後,也可以利用$ git format-patch 指令來產生更新檔(Patch)
    • $ git format-patch <某個Commit物件的id> <某個Commit物件的id>: 產生一份更新檔,範圍是從前者的Commit物件到後者的Commit物件為止,但"不包含"前者的Commit物件本身
    • 也可以指定相對的$ git logcommit紀錄的範圍
      • $ git format-patch -2: 這樣Git會幫忙產生最新的3次commit紀錄更新檔(Patch)
    • 也可以指定產生的更新檔(Patch)的位置
      • $ git format-patch -2 -o <更新檔要存放的檔案位置>: 這樣Git會幫忙產生最新的3次commit紀錄更新檔(Patch),並指定將更新檔(Patch)存放在指定的位置
      • 如同以下圖片示範
      • 利用git format-patch 來產生更新檔並指定更新檔要儲存的位置
        參考圖片出處https://gitbook.tw/chapters/github/patch.html
  • 可以利用$ git am 指令來應用(Apply)更新檔(Patch)
    • $ git am < 更新檔存放的檔案位置路徑/* >: 表示要將該路徑下的所有更新檔(Patches)都應用到現在的專案上
    • 可以一次應用(Apply)一個更新檔,也可以一次應用(Apply)所有更新檔
    • Git會依據檔案的名字"依序"一個一個套用在現有的專案上面

Git 常見的團隊工作流程 & 規範

GitHub Guides(Official Guides) - https://guides.github.com/

Git Flow介紹

  • Git Flow最早是在西元2010年被提出的,Git Flow是一套團隊共同協作的工作流程,以讓團隊內的成員可以有一個能共同遵循的 "開發" & "維運" 流程
  • 特色: Git Flow流程會有masterdevelophotfixreleasefeature這五種分支,各種分支負責不同的功能,其中masterdevelop這兩個分支又被稱作長期分支,因為他們會一直存活在整個Git Flow裡,而其它的分支大多會因任務結束而被刪除
  • 以下是Git Flow流程圖
  • 以下介紹Git Flow流程中會碰到的五大分支
    • 長期分支
      • Master 分支(= 主分支): 主要是用來放穩定、隨時可上線的版本。這個分支的來源只能從別的分支合併過來,開發者不會直接commit 到這個分支。因為是穩定版本,所以通常也會在這個分支上的commit上打上版本號標籤
      • Develop 分支(= 開發分支): 主要是所有開發的基礎分支。當有新的功能需要開發的時候,所有的Feature分支都是從這邊切出去。而當Feature分支的新功能完成後,也都會合併回來這個分支
    • 短期分支
      • Hotfix 分支(= 修復分支): 當線上產品發生緊急問題的時候,會從Master分支開一個Hotfix分支出來進行修復,當Hotfix分支修復完成之後,會合併回Master分支,也同時會合併一份到Develop分支
        • 說明1: 之所以要從Master分支切出來修復是因為Develop分支可能尚在開發中,這時候如果從Develop分支切出去再修復的話,要併回Master分支時,很可能會遇到合併衝突的問題
        • 說明2: 之所以要當修復完時,要合併回Develop分支是因為假設之後Develop分支的新功能開發完成後,並且也合併回到Master分支的時候,這時候Hotfix遇到的問題又會再度浮現
      • Release 分支(= 預發分支): 當認為Develop分支夠成熟了,就可以Develop分支合併到Release分支,在這邊進行算是上線前的最後測試測試完成後,Release分支將會同時合併到Master以及Develop這兩個分支上
        • 說明1: 因為Master分支是上線版本,所以要合併到Master分支
        • 說明2: 而合併回Develop分支的目的,是因為可能在Release分支上還會測到並修正一些問題,所以需要跟Develop分支同步,免得之後的版本又再度出現同樣的問題
      • Feature 分支(= 功能分支): 當要開始新增功能的時候,就是使用Feature分支的時候了Feature分支都是從Develop分支來的,完成之後會再併回Develop分支
  • 關於Git Flow 流程的介紹可以參考以下的文章

GitHub Flow介紹

  • Github Flow是一個適合15–20人左右團隊,在部署上自動化且一天之內會需要多次部署的開發(持續發布)

  • 特色: GitHub Flow是一個輕量型、以分支為基礎的工作流程,可以讓我們的團隊與專案更有規律地部署

    GitHub flow is a lightweight, branch-based workflow that supports teams and projects where deployments are made regularly. --- by 官方文件

    There's only one rule: anything in the main branch is always deployable. --- --- by 官方文件

  • GitHub Flow官方文件介紹

    • 前置流程:
      • 要先將遠端的repository利用Fork的方式複製到自己的GitHub repository
      • 將該Fork過來的專案Clone(複製)到自己本機端(local)的電腦上
    • CREATE A BRANCH: 在自己本機端(local)的Fork過來的專案上,建立一個新的分支,以讓我們安全的做開發和調整
      • 包含$ git add
      • 包含$ git commit -m
    • OPEN A PULL REQUEST: 利用Pull Request機制來 發出/收到 全世界的其他開發者的回饋(feedback)
      • 包含 Conversation
      • 包含 Code Review
    • MERGE AND DEPLOY: 合併(Merge)這次Pull Request的修改內容到原作者的遠端repository上,並且部署(Deploy)這次的修改變化到原作者的遠端repository上
    • 以下是GitHub Flow官方的流程圖說明
    • 關於GitHub Flow 流程的介紹可以參考以下的文章
  • GitHub Flow民間介紹

GitLab Flow介紹

  • GitLab flow分成兩種情況來應付不同的開發流程
    • 持續發布(Environment Branches & Upstream First)
    • 版本發布(Release Branches & Upstream First)
  • 特色: GitLab Flow有一個最主要的原則就是"上游優先"(upstream first),所謂上游優先原則的意思是只存在一個主分支master,此分支為所有其它的分支的上游,也就是說所有的分支都是由主分支(master分支)建立; 所以分支合併的順序很重要,要一次和並且確保"通過測試"才可以往下游合併,除非是緊急情況,才可以允許跳過上游直接在下游操作合併
  • 接下來要介紹GitLab Flow的兩種情境的工作流程(workflow)
    • 持續發布(Environment Branches & Upstream First)
      • 持續發布的專案,會建議再多出一個分支,為預發分支(pre-production)
      • 每個環境(如開發環境、預發環境、測試環境…等)都會有對應的分支。如下圖,開發環境為master分支,預發環境為pre-production分支,生產環境為production分支
      • 由下圖來舉例,如果生產環境(production)發生錯誤,則要建一個新分支修改完後合併到最上游的開發分支(master),且經過測試,再繼續往預發分支合併(pre-production),都要經過測試沒問題後才能夠再往下合併(Merge)到生產環境
      • GitLab Flow流程_Environment branches with GitLab flow_官方圖解說明
        參考圖片出處https://docs.gitlab.com/ee/topics/gitlab_flow.html#git-flow-and-its-problems
    • 版本發布(Release Branches & Upstream First)
      • GitLab Flow中,對於版本發布的項目,建議每一個穩定的版本都要從master分支拉出來創建一個新分支,比如下圖的2–3-stable2–4-stable(release分支)
      • 根據對應的release分支再創建一個修復分支,修補bug後,一樣要照著上游優先規則,先合併(Merge)到master分支,確認沒問題過後,才能夠合併(Merge)到release分支,並且要更新版本號
        • Semantic Versioning(語易化版本規範)
          • 總共會是三位數搭配兩個小數點的版本表示方式, 例如: 2.0.0
          • 三個位數代表的分別是: MAJOR.MINOR.PATCH
          • MAJOR version: 當我們發布與之前版本不相容(Incompatibal) 的API調整
          • MINOR version: 當我們新增一個可以向後兼容規範(backwards compatible manner) 的功能
          • PATCH version: 當我們處理了可以向後兼容(backwards compatible) 的故障處理(bug fixes)
        • 可參考(Semantic Versioning)語易化版本規範
      • 當如果有多個release version的話,會建立不同版號的release branch,當如果有需要版本的更新檔(patch),會藉由$ git cherry-pick挑選需要的Commit物件到對應的版本分支(release branch)上。
      • GitLab Flow流程_Release branches with GitLab flow_官方圖解說明
        參考圖片出處https://docs.gitlab.com/ee/topics/gitlab_flow.html#git-flow-and-its-problems
  • GitLab Flow 的 Merge Request(MR)介紹
    • 在GitLab Flow中,當需要將兩個分支合併(Merge)的時候就可以使用Merge Request
    • Merge Request可以被當作code review的工具,而且不用另外安裝額外的軟體或是套件
      • 當我們新建一個Merge Request後,可以用 "/cc @mark" 的方式來標記其他的團隊成員,此時代表該MR尚未完成,但歡迎大家對這次的MR做出回饋(feedback)
      • 這時所有的團隊成員都可以針對這次的MR所做的修改做出全部(general)或是局部對某幾行(specific line)的評論(line comments)
      • 當被指派收到MR的成員(通常是最了解這個專案程式碼的人 or 該專案的管理者),覺得此次的MR是可以被接受的,就會接受此次的MR,並合併(Merge)到主分支(master分支)
      • 但如果被指派收到MR的成員(通常是最了解這個專案程式碼的人 or 該專案的管理者),覺得此次的MR不能被接受的,也可以拒絕接受此次的MR,並且不合併(Merge)到主分支(master分支)
      • GitLab Flow流程_Merge/pull requests with GitLab flow_官方圖解說明
        參考圖片出處https://docs.gitlab.com/ee/topics/gitlab_flow.html#git-flow-and-its-problems
  • 在GitLab Flow中,慣例上都會將主分支(master)設定為被保護的分支(protected branch)
    • 所以如果我們想要將這次的修改合併(Merge)到主分支(master)的話,就要將MR請求指派(assign)給擁有維護者權限的人(maintainer permissions)
    • 關於GitLab 的權限設定可以參考GitLab Permissions

Git Flow & GitHub Flow & GitLab Flow 三者的比較

  • Git Flow
    • 優點
      • 是一套完整明確且清晰的團隊共同協作流程
      • 短期分支(hotfix & release & feature分支)是依據"任務"建立的,當任務結束時,就會合併到長期分支(master & develop分支)
    • 缺點
      • 相對另外兩種工作流程(workflow)來說會比較複雜
      • 需頻繁切換分支
      • 在現今版本快速更迭的時代,Hotfix & Release分支是幾乎用不到的。因為合併到master分支後如果有bug就直接修復且發布下個版本了,如果還使用這兩個分支,需要合併回develop和master分支,但實際上開發者很常忘記合併回這兩個主分支
    • 可參考Git Flow介紹
  • GitHub Flow
    • 優點
      • 適合屬於需要持續發布的專案
      • 方便結合CI/CD (Continuous integration/Continuous Delivery & Continuous Deployment)
      • 相對Git Flow簡單許多
    • 缺點
      • master分支會變得不穩定
        • 情境說明
        • 當遇到iOS官方版本更新,而如果審核期間又有新版本要發布的話,這個時候只有一個master主分支就會不夠用,所以還得另外再建一個新分支來維護
      • master分支合併後如果不發布,會造成線上(production)和master分支的內容不一致
    • 可參考GitHub Flow介紹
  • GitLab Flow
    • 優點
      • GitLab Flow 結合了 Git Flow & GitHub Flow兩者的優點
        • 有開發環境上的彈性又有單一主分支的方便
        • master分支的分支不夠,於是增加了一個production分支,專門用來發布版本(release)
      • 方便結合CI/CD (Continuous integration/Continuous Delivery & Continuous Deployment)
    • 缺點
      • 相較於GitHub Flow來說會比較複雜
      • prodcution版本需要多個版本(pre-production分支)來維護時,可能會變得如同Git Flow一樣複雜
    • 可參考GitLab Flow介紹
  • 三種團隊共同協作的工作流程(workflow)的共同點
    • 都是功能驅動式開發(Feature-driven development)---以需求為開發的起點,先有需求才有以上那些分支,且開發完後該分支就會被合併到主分支,然後刪除
    • Git Flow & GitHub Flow & GitLab Flow 三種工作流程的比較可參考4 branching workflows for Git

觀念補充

git hash-object - Compute object ID and optionally creates a blob from a file

終端機(Terminal)是什麼?

  • 終端機本身通常不是一部電腦,它本身沒有運算能力,僅用來顯示資料及輸入資料,所有的計算都是在主機上處理的
  • 其實終端機就是可以讓使用者輸入指令,來跟電腦進行互動

Vim 是Git的預設編輯器,Vim主要常用的兩種模式

  • Normal模式:無法輸入文字,僅能複製,貼上,存檔,離開 動作
  • Insert模式:需要先按下 I or A or O ,才能開始輸入文字
  • 在Insert模式下,按esc可退回到Normal模式
  • 在Normal模式下,按下:wq! ,代表強制存檔完成後直接關閉這個檔案
  • Vim圖解操作說明
    參考圖片出處https://gitbook.tw/

SHA-1(安全散列演算法) 介紹

  • SHA-1 (Secure Hash Algorithm 1)是一種雜湊演算法,計算之後的結果通常會以4016進位的數字方式來呈現
  • SHA-1: 該演算法的特性之一就是,當輸入的值相同,就會有相同的輸出值
  • 在Git中,所有物件的"編號"的計算,主要都是透過這個演算法算出來的
  • 碰撞(collision): 代表輸入兩個不同的值(不同的檔案),卻得到相同的結果(相同的SHA-1值),這種情況稱為碰撞(collision)
  • 在Git裡,不同種類的Git物件,SHA-1值的計算方式會有些不同
  • 可以利用Git內建的$ git hash-object 指令來幫忙算SHA-1值,也可以得到相同的結果
    • $ git hash-object: 計算出物件的ID(SHA-1值),並選擇性地從一個指定的檔案,來建立一個Blob物件
      • --stdin: 讀取物件來自標準輸入,而不是來自一個檔案
    • 例如: Blob物件的SHA-1值計算公式為
      • blob字樣 (若為commit物件,則為commit字樣;若為Tree物件,則改為Tree字樣)
      • 一個空白字元
      • 輸入內容的長度
      • Null結束符號
      • 輸入內容
    • 從上列公式可以看出,第1~5項都沒有跟時間或是亂碼有關的內容,只跟要計算的內容有關
    • 但是Commit物件Tag物件則除外,因為這兩個物件本身就有包括時間
    • 所以,以Blob物件來說,不管是在什麼時間或不同的電腦上,一樣的輸入值,就會有一樣的內容
    • 例如: $ printf "Hello, Hans Tsai" | git hash-object --stdin
    • git hash-object --stdin可以計算SHA-1的值

參考資料來源

書籍

Medium

iT邦幫忙

練習場

免費線上學習資源

About

It's my own note for git. This repository is just for learning and making me more convenient to find out git CLI commands as well as git basic core concepts. Use Git v2.29.0 (for macOS Catalina 10.15.6).

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages