QWERTY

Hello World!

Git常用指令

主要參考此書:版本控制:使用Git

建議初學者先看git觀念及此篇的第一段

Git基本觀念

檔案階段 檔案所在位置 說明
已修改 Working Directory(工作目錄) 經過修改的檔案
已暫存 Staging area(暫存區) 要提交的變動清單
已提交 Repository(容器) 已提交的檔案及變動記錄

其他檔案階段

  • 被忽略:在名為.gitignore的檔案中修改想忽略的檔案,如暫存檔、log、或編譯後的object file、筆記
    • 每個目錄都可以有自己的.gitignore檔,以較接近檔案的目錄為準
    • 可以用git add -f強制將被忽略的檔案加入暫存區

觀看檔案的狀態: git status

  • 已修改: Changes not staged for commit
  • 已暫存: Changes to be committed
  • 已提交: 不顯示

基本流程: 修改檔案 → 將修改的檔案加入 Staging area (git add) → 提交變更 (git commit) → 繼續修改檔案(循環)

若要和他人共用,要在線上容器和本機容器間進行同步(git push, git pull)

提交(commit,也稱送交)

  • 版本和容器變動的最小單位
    • 一個commit = 一個版本
    • git 中的 HEAD 等於目前所在的版本
  • 不一定所有檔案都要提交
    • 只有加入Staging area的檔案會被提交
  • 提交時機
    • 程式處於穩定狀態
      • 可以正確執行
      • 測試完成
    • 以時間為單位
      • 結束一天的工作前

辨識碼

  • 所有物件都有一個獨特的辨識碼
    • 包含commit, tag, 文件…都有辨識碼
      • 其值為物件的SHA1 hash
  • 下述指令的[commit]即為識別碼
    • 在沒有重覆的情況下,可取前幾碼代表該物件

設定

先設定自己的名字與信箱,因為git用名字與信箱來分辨作者

# 若不加 --global 則設定只會在該容器生效

# 設定使用者名稱(加上引號)
git config --global user.name "First Last"
# 設定使用者電子郵件(加上引號)
git config --global user.email "user@example.com"
# 開啟顏色,使git內容不會只有單一白色
git config --global color.ui true
# 設定預設編輯器(提交時會用到)
git config --global core.editor "emacs"
# 設定指令的縮寫(將git st設為git status的簡寫)
git config --global alias.st status
# 列出所有設定值
git config -l

設定預設提交格式

# 方法1. 直接設定
git config --global commit.template $HOME/.git-template
# 方法2. 在 .gitconfig 中設定
[commit]
template = /home/frank/.git-template

實作範例

初始化一個專案

git init # 初始化,使當前目錄成為git的容器
touch README.md # 加上專案的說明文件(非必要)
git add README.md # 將說明文件加入暫存區
git commit -m "initial commit" # 提交變更

將本地的容器上傳到遠端(如github)的容器

git remote add origin <URL> # 將遠端容器命名為origin
git push -u origin master # 從本地端的master branch上傳到origin的master branch

本篇:Git常用指令

Git檔案操作

加入暫存區: add

git add:將檔案加入暫存區(staging area)

git add -i # 進入互動模式(推薦)

git add [filename] # 將檔案加入staging area
git add -u # 只把「修改/刪除」的檔案加入staging area,「新增」的檔案不加入
git add -p # 部分暫存:git會詢問哪些檔,然後逐個顯示被選中檔案的每一個差異部分,
# 並詢問你是否希望暫存他們。(= git add --patch)
git add -A # stages All
git add . # stages new and modified, without deleted

參考資料

直接刪除/移動不會影響容器中的檔案,因為這項變動沒有告知git
若要在下一個版本刪除或移動檔案,需要用git rmgit mv

git rm [filename] # 在下一個版本移除,並實際刪除檔案
git rm --cache [filename] # 在下一個版本移除,但不實際刪除檔案
git mv [filename] [new-filename] # 改名、搬移檔案,並將變動加入staging area

checkout

使檔案還原到指定提交時的版本

  • 發現檔案改錯,用git checkout filename把檔案還原到最近的版本
  • git checkout [commit] filename 檔案會被還原到指定的commit時的版本

Git更新版本

提交新版本(commit)

git commit # 會跳出編輯器,此時可編輯提交訊息
git commit -m 'commit message' # 直接提交並指定提交訊息
git commit -a # 將所有修改過的檔案直接 commit(跳過add步驟), 不包含新增的檔案

存取遠端容器: push, pull

遠端的repository名稱通常取名為origin
push: 上傳, pull: 下載

git remote show # 顯示設定的遠端容器及其名稱、網址
git remote add origin [url] # 設定遠端容器連結並命名為origin
git push origin master # 從本地目前的branch上傳至遠端master
git pull origin master # 從遠端master更新到本地目前的branch
git pull origin/[branchB] [branchA] # 從遠端branchB更新到本地branchA
git clone [url] [localRepositoryPath] # 複製遠端容器至本地
git fetch origin # 下載origin的branch到本地
  • push時,如果和遠端容器有衝突,需要先pull,在本地merge(處理衝突)後再push
  • pull的機制:先 fetch 遠端的 branch,然後與本地端的 branch 做 merge
    • 若有衝突,會多產生一個 merge commit來取代遠端有衝突的所有commit
    • git pull --rebase origin master可以保留雙方的commit

Git分支(branch)

分支(branch): 將特定版本(commit)命名

git branch
git checkout

git branch # 列出本地分支(前方有星號的為目前所在分支)
git branch -r # 列出遠端分支
git branch -a # 列出所有分支(本地+遠端)
git show-branch # 列出本地分支樹狀圖
git branch [branchname] [commit] # 在指定commit上建立分支
git branch -d [branchname] # 刪除指定分支
git branch -m [old-name] [new-name] # 重新命名分支
git branch [new-branch] [old-branch] # 由分支old-branch複製出新分支new-branch

git checkout [branch] # 切換到指定分支
git checkout [commit] # 也可以切換到任何commit
# 若commit不在任何branch上,用 git branch 查看時,會顯示 (no branch)
git checkout -b [new-branch] # 建立並切換到新分支new-branch(從目前所在的分支複製)
  • git checkout -m [branch] # 將工作目錄的變動和分支合併,並切換到該分支
  • git checkout 時,若已修改的檔案和checkout的版本產生衝突,則checkout不會執行
    • 衝突時先git stash(暫時清空已修改的檔案)

Git 標籤(tag)

git tag [tagname] [commit] # 在指定commit新增tag
git tag -d [tagname] # 刪除tag

Git合併(merge)

git merge [branch/commit] # 將指定的版本合併到目前所在的分支

產生衝突時:先用git diff git status查看
衝突的地方為如下格式

<<<<<<<<
...
//version1 code
...
========
...
//version2 code
...
>>>>>>>>

分別是兩個版本的差異。
將其修改後即可提交

  • git diff --ours/--theirs 可以查看原版本和衝突版本的差異(ours→合併的, theirs→被合併的)
  • git diff 用在衝突檔案時只會顯示衝突的部分,而不會顯示只有一邊修改的部分

Git 修改版本

修改版本: rebase

rebase可以重新排序、編輯、移除、合併、拆分提交

  • 優點
    • 可以不產生分支線和額外的merge commit
  • 缺點
    • 等於改變提交記錄,僅適合還沒上傳的commit
    • commit被移動,所以特徵碼會改變(也就是原本的commit會被取代)
    • commit的形狀會被改變(變成一條直線)
      • 可用--preserve-merge參數保留形狀
  • 麻煩
    • 移動的commit若屬於多個分支,則每個分支都要重新指定位置

使用時機:想保留額外commit資訊時,可替代merge

// debug rebase on master
git checkout debug
git rebase master

Original:
master A → B→C→D→E
| ↘
debug F→G→H

After rebase:
master A → B→C→D→E
| ↘
debug F→G→H
git rebase -i [commit] # rebase互動模式(推薦)
# 互動模式指令:pick: commit, squash: 擠壓到前一個commit, edit: 停在此commit提交前的情形以修改(可以拆成多次commit) …
# 在rebase時遇到衝突的話,會提示使用者手動解決衝突(merge)
git rebase --continue # (解決衝突後)繼續rebase
git rebase --skip # 忽略一個原本要rebase的commit
git rebase --abort # 取消rebase

git rebase --onto master [commitA] [commitB] # 使用onto可以將從A到B的提交移植到完全不同的分支(master)上

參考資料:修改一個分支的歷史–使用rebase

merge還是rebase?

需要保留樹狀記錄就用merge,反之用rebase
個人較愛用rebase,理由是保留樹狀記錄只會造成日後版本維護的麻煩

修正版本: amend

  • 修改最近一次的commit
    • staging area的修改會合併到最近的提交
  • 使用時機
    • 提交訊息的修改
    • 發現一些小錯誤
      • 忘記add/多add檔案/找到小bug 的時候
git add [filename] # 修改錯誤後加入暫存區
git commit --amend # 將此修改加入最近的提交
#==========
git commit --amend [file1] [file2] ... # 直接將指定檔案的變更加入最近的commit

回到指定版本: reset

git reset [commit] [--soft/--mixed/--hard] # 還原到指定提交
git reset HEAD [filepath] # 將檔案從staging area移除 (= git checkout filepath)
--mixed 暫存區也被改變,但工作目錄不變 (預設值,暫存區清空)
--hard 暫存區和工作目錄都被改變(目前的目錄中,所有檔案的修改會消失)
  • --soft
    • 只有版本改變,檔案不改變
    • 版本改變造成的所有變更都會放在staging area
      • 若你reset --soft到五天前的版本,那這五天內的commit變更都會在staging area
      • reset --soft後直接git commit,則最新的版本會和reset前的版本一模一樣
  • --mixed
    • 只有版本改變,檔案不改變
    • 變更不會放在staging area, 要自己手動add
  • --hard
    • 檔案會回到指定commit的狀態
    • 通常是出了嚴重問題後的解決方法
    • 若有檔案不在版本中(沒有被add,commit過),reset --hard後檔案依舊存在

Whats the difference between git reset –mixed, –soft, and –hard?

提交: revert

git revert [commit]

  • 新增一筆commit,此commit會抵消之前的變更,使檔案像是回到指定版本
    • 若最近的變更是寫一頁作文,revert會加上一個塗一頁立可白的變更
      使用時機: 你的commit已經上傳,別人已下載,所以不適合用git rebase, git reset, git commit --amend等方法隨便修改版本記錄

Git比較

git diff # 比較實際檔案與目前版本的不同
git diff --cached # 比較staging area與目前版本的不同
--stat # 只顯示檔名和不同的行數
git diff [commit] [commit] # 比較兩個版本的不同
git diff -S[string] # 只列出變更中,有包含string字串的差異
git diff -w # 不將空白視為變更
git diff [commit1]:[file1] [commit2]:[file2] # 比較兩個版本的指定檔案

Git記錄: log, show

git log 用來查詢提交記錄,有很多參數可以用
建議可以直接用GUI(gitk, git gui, git instaweb)來看

git log [commit] # 列出指定版本的記錄(也就是找其之前的記錄)
git log [commitA]..[commitB] # 列出從A到B的提交(不含A, 含B)
git log ^[commitA] # 顯示指定版本之後的提交
git log -[num] # 最近[num]筆的提交紀錄
git log --no-merges # 不顯示合併的提交
git log --graph # 顯示樹狀結構圖
git log --stat # 顯示修改行數
git log --author [username] # 指定使用者的提交
git log --since="2 weeks ago" # 近2週內的提交
git log --follow [filename] # 列出包含該檔案變動的提交

git show [commit] # 列出指定版本的最新提交的詳細修改內容
git show [commit]:[filename] # 顯示該提交的指定檔案

可以將版本記錄視為一顆樹,前一個版本為其父節點

  • 列出指定版本的記錄 = 找其祖先
  • git diff foo..bar = git diff foo bar
  • git diff foo...bar: foo和bar的”merge base”(最近的共同祖先)和bar的差異
  • git log foo bar: 顯示所有foo和bar的提交(其中一方有即可)
  • git log foo..bar: 不屬於foo,屬於bar的提交
  • git log foo...bar: 只有其中一方才有的提交

參考資料:What are the differences between double-dot 「..」 and triple-dot 「…」 in Git diff commit ranges?

調整記錄的格式

git log --pretty=oneline # 一個commit一行
git log --pretty=short # 簡短
git log --pretty=format:'%h was %an, %ar, message: %s'
# %s: 提交訊息, %h: 辨識碼(hash), %an: 作者, %ar: 時間

Git尋找提交: bisect, blame

git bisect 用二分搜尋法找尋有bug的提交

git bisect start         # git會依照你提供的資訊改變目前的工作目錄,以方便測試
git bisect good [commitA] # 告知到此commit之前是好的
git bisect bad [commitB] # 告知此commit仍有錯誤(通常用HEAD)
# git 會自動切換版本,若切換的版本是好的,則輸入git bisect good,反之則輸入git bisect bad
# 最後會找出第一個壞掉的提交
git bisect log # 顯示之前提供的good, bad記錄
git bisect visualize # 顯示在搜尋範圍的commit
git bisect reset # 找到錯誤的提交後,還原至bisect前的版本

用檔案尋找提交

git blame [filename] # 顯示檔案的每一行最後是被哪個commit修改的
git log -S [string] [filename] # 顯示變動包含string字串的commit
git reflog [Reference] # 參照(Reference, 預設為HEAD)改變的歷史
  • checkout, commit, reset, rebase … 都會修改HEAD
  • 可做為reset的參考

Git藏匿變更: stash

git stash: 把目前工作目錄(working directory)的變更丟到一個stack中,之後再回來拿

保持工作目錄的乾淨

使用時機

  1. 你不得不修改一個緊急bug,你可以先把目前工作目錄的變更丟到stash,這時候你的工作目錄和上次剛提交內容的狀況一樣,等到修完bug後再把stash中剛剛做到一半的東西還原以繼續
  2. 今天的工作時間結束後,還有未完成的部分,可以先stash後執行測試,確認最新版本是正確的
git stash                # 把目前工作區的修改丟到stash裡
git stash save [stashName] # 把目前工作區的修改丟到stash裡,並加上命名
git stash pop # 從stash取出最新放入的一筆修改,並從stash中移除(若發生衝突時會保留)
git stash apply # 將最新放入的一筆修改套用在工作目錄上
git stash drop # 移除最新放入的stash
git stash show [stashName] # 列出某一筆stash的修改內容
# 命名:最新放入的為 stash@{0}, 再來是 stash@{1} 以此類推
git stash show -p [stashName] # 列出某一筆stash的修改統計
git stash list # 列出所有在stash的變更
git stash [file] # 將檔案丟進stash
git stash clear # 清空stash
git stash branch [branchName] # 用最新放入的stash做為新的branch
git stash -p # 挑選要暫存的變更
git stash --includev # 同時暫存尚未在版本控制中的檔案(即從來沒被add過的檔案)

不常用的指令

Git遠端容器管理: remote

git remote # 列出預設遠端容器的名稱
git remote show # 顯示預設遠端容器的詳細資料
git remote add [remote-name] [remote-url] # 加入一個遠端容器,並命名為[remote-name]
git remote update # 更新Repository的所有branch
git remote rm [branch] # 刪除遠端分支

git push --tags # 上傳標籤資訊(git push不會上傳標籤)
git push [remote] [branch] # 上傳分支
git push [remote] [branch]:[uploadbranch] # 上傳分支並在遠端容器中重新命名
git push [remote] --delete [branch] # 刪除遠端分支

git branch --set-upstream [branch] [remote] # 連結一個本地分支和遠端分支

Git clean

清除不包含在版本控制中的檔案(被.gitignore忽略的檔案不會被清除)

git clean -n #  列出會被清掉的檔案
git clean -f # 執行清除
git clean -d # 清除資料夾
git clean -x # 連 .gitignore 裡列的檔案也清掉

Git垃圾回收: gc

git儲存的記錄很多: stash, reflog, log …

gc(garbage collection):刪除所有沒有被指到(reference)且已存在一段時間的物件

# 整理前和整理後的差異, 可由: git count-objects 看出
git count-objects
git gc
git count-objects

參考資料

  • 版本控制:使用Git
  • Git scm
  • Git情境劇系列
  • Git初學筆記
  • ihower: 還沒push前可以做的(壞)事
  • git: get ready to use it

歡迎關注我的其它發布管道