Git 筆記

Git 筆記

簡介

Git 是一種分散式的版本控制系統(Distributed Version Control),優點是免費、開源、速度快、檔案小。

備註

設定

使用者相關

SSH Key 相關

檢查 SSH Key 是否存在,若存在則會看到 id_rsa.pub 檔。

ls -al ~/.ssh

-rw-r--r--   1 Cythilya  staff   744  1 25 21:54 id_rsa.pub

填入 email 並產生 SSH Key

ssh-keygen -t rsa -b 4096 -C "your e-mail"

將 Key 存在 /Users/you/.ssh/id_rsa 並按 Enter 鍵

Enter a file in which to save the key (/Users/you/.ssh/id_rsa): [Press enter]

確認 Agent 是否存在,若存在則會出現 Agent pid XXXXX 的訊息

eval "$(ssh-agent -s)"

將 Key 加到 Agent

ssh-add ~/.ssh/id_rsa

將 SSH Key 貼到 GitHub 個人設定頁上

測試

ssh -T git@XXX.XX.X.XX

出現此訊息代表可使用 SSH Key 連上 GitHub

>  Hi username! You've successfully authenticated, but GitHub does not provide shell access.

指令縮寫

將常用指令設定縮寫,例如:git status 可簡寫為 git st

$ git config --global alias.st status
$ git config --global alias.co checkout
$ git config --global alias.pl 'pull origin master'
$ git config --global alias.ps 'push origin master'

常用命令列指令

Git 檔案的三種狀態與專案的三個區域

在 Git 中檔案分為三種狀態

在 Git 中專案分為三個區域

檔案的三種狀態與專案的三個區域

圖片來源:本地操作

Git 指令

git –version

檢視目前所用 Git 的版本。

$ git --version
git version 2.10.1

git init

建立 .git 的隱藏目錄,它是一個 Git Repository 或稱 Git Directory。目的是初始化這個目錄,讓 Git 對這個目錄做版本控管,之後若不想再控管這個目錄,將 .git 移除即可。.git 裡面放了很多東西,像是版號記錄、目前指到哪個分支、遠端連線位置等。

$ git init
Initialized empty Git repository in /Users/Cythilya/Documents/git/git_test/.git/

訊息提示在 git_test 目錄底下建立了 .git 這個 Git Repository。

git status

檢查 Working Directory 的狀態。例如:未進入版本控管(Untracked)、新增檔案(New File)、刪除檔案(Deleted)、檔案已修改(Modified)等。

git diff

比較差異。

git add

將檔案的變更從 Working Directory 移到 Staging Area。

Unstage Files

將檔案的變更從 Staging Area 移到 Working Directory。

git commit

提交本次修改,將檔案的變更從 Staging Area 移到 Git Repository。

git log

從新到舊列出提交記錄,資料有提交者、提交時間、做了什麼事。

git rm

移除檔案並將變更移到暫存區(工作區域 -> 暫存區)。 另比較 Unstage Files(暫存區 -> 工作區域)

$ git rm <filename>

等同於

$ rm <filename>
$ git add <filename>

git mv

變更檔案名稱並將變更移到 Staging Area。

$ git mv <file1> <file2>

等同於

$ mv <file1> <file2>
$ git add -A

例如,將檔案 a.md 更名為 b.md,會出現 renamed 的提示訊息。

$ git mv a.md b.md

$ git status
# 省略一些提示訊息...

  renamed:    a.md -> b.md

git blame

$ git blame cinderella.html
e7f48a31 (Cythilya Tang 2018-03-29 16:29:57 +0800 1) I am Cinderella!
f26011a2 (Cythilya Tang 2018-03-31 00:34:46 +0800 2) test 0002
c0521d14 (Cythilya Tang 2018-03-31 00:41:02 +0800 3) test 0004

$ git blame -L 1,2 cinderella.html
e7f48a31 (Cythilya Tang 2018-03-29 16:29:57 +0800 1) I am Cinderella!
f26011a2 (Cythilya Tang 2018-03-31 00:34:46 +0800 2) test 0002

git checkout

git reset

這裡的 Reset 並不是字面上的「重新設定」,而是「前往」,主要用來拆掉提交的變更。

範例 1

拆掉最新的 Commit 回復到前一個 Commit(372fbf8),並將變更放回工作目錄。

查看目前的提交記錄。

$ git log --oneline
4b551f2 commit_1
372fbf8 commit_2
86ea6c6 commit_3

若要拆掉最新的 Commit 回復到前一個 Commit(372fbf8),並將變更放回工作目錄,可使用以下任一方法

由於沒有設定 Reset Mode,所以是 Mixed,也因此變更會放在工作目錄之下。如下,在 Reset 後保留檔案的變更但尚未提交至暫存區。

$ git reset 4b551f2^
Unstaged changes after reset:
M hello.html

檢視工作目錄的狀態,保留變更並放在工作目錄。

$ git status
On branch master
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:   hello.html

no changes added to commit (use "git add" and/or "git commit -a")

範例 2

拆掉最新的 Commit 回復到前一個 Commit(4b551f2),並將變更留至暫存區。

查看目前的提交記錄。

$ git log --oneline
24469d5 commit_1
4b551f2 commit_2
372fbf8 commit_3

拆掉最新的 Commit 回復到前一個 Commit(4b551f2)。

$ git reset 24469d5^ --soft

檢視工作目錄的狀態,保留變更並放在暫存區。

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

  modified:   hello.html

範例 3

拆掉最新的 Commit 回復到前一個 Commit(372fbf8),並將變更完全丟棄。

查看目前的提交記錄。

$ git log --oneline
38c6460 commit_1
4b551f2 commit_2
372fbf8 commit_3

拆掉最新的 Commit 回復到前一個 Commit(372fbf8),並將變更完全丟棄。

$ git reset 38c6460^ --hard
HEAD is now at 4b551f2 commit_2

丟棄變更,沒有看到之前變更的部份出現在工作目錄或暫存區。

$ git status
On branch master
nothing to commit, working tree clean

檢視提交記錄,已拆掉 commit_1。

$ git log --oneline
4b551f2 commit_2
372fbf8 commit_3

範例 4

拆掉多個提交記錄。

回到前三個 Commit 的狀態,也就是回到 70c3f56。

$ git log --oneline
4b551f2 commit_2
372fbf8 commit_3
86ea6c6 commit_4
70c3f56 commit_5

可用以下任一方法

注意!由於一次回復多個提交記錄但打開檔案後看到修改仍被保留可能會覺得怪怪的,要記得預設 Reset Mode 是 Mixed。若希望回復後不要保留修改,改為 Hard 即可,像是這樣 git reset 70c3f56 --hard

範例 5

取回拆掉的提交記錄。

git branch

分支是指向某個提交記錄的指標。

git merge

合併。

合併分支 <branch_name> 到 master

合併時不要使用「快轉模式(Fast Forward)」git merge <branch_name> --no-ff

git rebase

用於重新定義參考基準。

重新定義參考基準

git rebasegit merge 不同之處是不會產生一個額外合併的 Commmit。

$ git rebase <branch_name> # 以分支 <branch_name> 當成新的參考基準

範例:rebase 分支 bugFix 到 master

將 bugFix 的修改接到 master 之後。

備註:Step 1 + Step 2 等同於 git rebase <new_base> <branch>,意即 git rebase master bugFix

如下圖示,比較 git rebasegit merge 的分支圖。

若使用 git merge 會產生一個額外的合併 Commit。

使用 git merge 會產生一個額外的合併 Commit

圖片來源:Learn Git Branching!

但若使用 git rebase 則不會產生一個額外的合併 Commit。這是因為直接將指標指到指定的新位置的關係。

使用 git rebase 則不會產生一個額外的合併 Commit

圖片來源:Learn Git Branching!

這兩種方法各有好處,git merge 保留了合併過程,而 git rebase 讓記錄簡潔。

取消 Rebase

方法一:Reset
$ git reflog
$ git reset <sha-1> --hard
方法二:ORIG_HEAD

ORIG_HEAD 會記錄最近一次「危險操作」之前 HEAD 的位置。

$ git reset ORIG_HEAD --hard

進入互動模式

git rebase -i <sha-1>「-i」表示進入互動模式,可用來標記提交記錄。

標記種類如下

將多個 Commit 合併為一個 Commit

輸入指令 git rebase -i <sha-1> 後使用 squash 標記提交記錄,第一個提交訊息即為合併後的提交訊息。

將一個 Commit 拆解為多個 Commit

在 Commit 之間再加入新的 Commit

調整 Commit 的順序

刪除 Commit

git revert

再做一個新的 Commit,來取消不要的 Commit。作用等同於 git reset HEAD^ --hard,只是 reset 不產生新的提交記錄。

git revert HEAD --no-edit # 取消最新的提交記錄,並且不編輯提交訊息

備註:git checkout <file> 是還原工作區域上特定檔案的變更,不要搞混了。

git tag

設定標籤

標籤是指向特定 Commit 的指標,可當成對某個 Commit 下錨點,在開發時完成特定的里程碑時就很適合使用標籤做標記。若無 <sha-1> 則指向目前的 Commit。

輕量標籤(Lightweight Tag),用來個人使用或暫時標記用。

$ git tag <tag_name> <sha-1>

有附註的標籤(Annotated Tag),用來標註軟體版號。

$ git tag <tag_name> <sha-1> -a -m "這是備註訊息"

刪除標籤

$ git tag -d <tag_name>

git stash

儲存做到一半的工作。

方法一:先提交,之後再拆掉提交

$ git add -A
$ git commit -m "hahaha"
$ git reset HEAD^

方法二:先儲存變更,之後再把變更拿出來

$ git stash [-u] # Untracked Files 無法被 Stash,需要額外使用 -u 參數
$ git stash list # 查看暫存的變更
$ git stash pop stash@{<num>}   # 把變更拿出來
$ git stash drop stash@{<num>}  # 丟棄暫存的變更
$ git stash apply stash@{<num>} # 把 stash@{<num>} 這個 Stash 拿來套用在現在的分支上,但 Stash 不會刪除,還是會留在 Stash 列表上

git cherry-pick <sha-1_1> <sha-1_2> <…>

在此分支之下,揀選並複製這個專案上其他的 Commit,然後放到目前位置(HEAD)之下。

git remote add...

加入遠端連線位置,以備後續將本地端的 Repo 推向遠端 (GitHub) Server。

$ git remote add origin <repository_url> # origin 即指向遠端 Repo 的代表名稱,可更名

git clone

取得現有 Git Repository 的複本。

git fetch

同步遠端伺服器上的資料到本地,可以把 git fetch 想成是在抓取資料(並且是目前本地端所沒有的部份),但不做更新。下載完成後會更新 Remote Branch 所指向的方向,意即 origin/<branch_name> 所指向的方向。

git pull

到遠端抓取更新的資料(Fetch),並且更新本地的進度(Merge),意即 git pull = git fetch + git merge

git push

將本地端的 Repo 推向遠端 Server。

git describe <sha-1>

顯示距離最近的錨點。

<sha-1> 是任何一個可以被 Git 解讀成 Commit 的位置,若沒有指定會以目前所在的位置為準(HEAD)。輸出結果為 <tag>_<num_commits>_g<sha-1>

git grep

搜尋。

git show

顯示資料。

QnA

如何找尋 Git 存放的位置?

$ which git
/usr/bin/git

.git 是什麼?

可參考 git init

origin 是什麼?

origin 是 Git 複製遠端倉庫(Remote Repository)時預設名字,意即當使用 git clone 時,Git 會自動把 remote 命名為 origin,可更名。

HEAD 是什麼?

HEAD 是指向目前所在分支的指標,位於 .git 的 HEAD 檔案。打開來看會是以下這樣…

ref: refs/heads/master

意思是目前指向分支 master。HEAD 的移動記錄可用 git reflog 來查看。Reflog 會保留 30 天。Git 1.8.5 後 HEAD 可縮寫為「@」。

若 HEAD 並非指向目前所在分支,則是「detached HEAD」的狀態。

detached HEAD 是什麼?

沒有指到「本地」某個分支的情況,可能發生這個情況的原因有

如何離開 detached HEAD 狀態?讓 HEAD 有任何分支可以指向它就可以了,例如,讓它回到 master 分支

$ git checkout master
Switched to branch 'master'

新增與刪除分支

git reset vs git rebase vs git revert

可參考 Reset、Revert 跟 Rebase 指令有什麼差別?

git rebase vs git merge

兩者皆可作為合併多個 Commit 的解法,而不同的是 git rebase 使用複製 Commit 的方式,git merge 會額外產生一個合併的 Commit。

點此看詳細內容。

git rebase vs git cherry-pick

git rebasegit cherry-pick 皆可將 Commit 複製後接在某個 Commit 之後,但差異是什麼?使用的時機點是什麼?

git fetch vs git pull

git fetch 不會更新本地資料,但 git pull 會更新本地資料。

如何還原變更

修改提交訊息

追加檔案到最近一次的提交

方法一

使用 git reset 把最後一次的 Commit 拆掉,加入新檔案後再重新 Commit。

方法二

使用 --amend 進行 Commit,--no-edit 表示不要編輯 Commit 訊息。

$ git add <new_file>           # 將新加入的檔案放到暫存區
$ git commit --amend --no-edit # 將新加入的檔案併入這個提交裡面

忽略已存在的檔案

由於忽略清單(.gitignore)的生效,必須是在檔案尚未進入版控之前,因此若要忽略已存在的檔案,就必須先將檔案移出版本控管,意即 Unstage。接著在將移出這個變更和更新後的忽略清單加入暫存區和倉儲區,最後可移除忽略檔案。

$ vim .gitignore                        # 設定忽略已存在的檔案
$ git rm --cached <file_name>           # 將特定檔案移出版本控管
$ git add <file_name>                   # 將移出版本控管檔案的變更加入暫存區
$ git add .gitignore                    # 將修改後的忽略清單加入暫存區
$ git commit -m "ignore existing file"  # 將移出版本控管檔案的變更加入倉儲區
$ git clean -fx                         # 清除忽略的檔案,若要刪除忽略的檔案可做這一步

如何解決非文字的衝突?

可參考這裡

如何處理 Diverged History (分岔)?

同步遠端進度,並將本地更新推上遠端。

方法一:Rebase

$ git fetch
$ git rebase origin/master
$ git push

等同於

$ git pull —-rebase; git push

方法二:Merge

$ git fetch
$ git merge origin/master
$ git push

等同於

$ git pull
$ git push

如何 Fork 自己在 GitHub 上的 Repository?

答案是「無法」,但可以先複製一份舊的 Repository 的程式碼,然後推到另一個新的 Repository 上。

複製一份舊的 Repository 的程式碼,並放到新的資料夾中。

$ git clone git@github.com/<user_name>/<repo_name> <new_repo_name>
$ cd <new_repo_name>

接著來看遠端連線狀況。

$ git remote -v
origin git@github.com:<user_name>/<repo_name>.git (fetch)
origin git@github.com:<user_name>/<repo_name>.git (push)

目前都是指向舊的 Repository,現在要改成指向新的 Repository。

$ git remote set-url origin git@github.com/<user_name>/<new_repo_name>

確認是指向新的 Repository。

$ git remote -v
origin git@github.com:<user_name>/<new_repo_name>.git (fetch)
origin git@github.com:<user_name>/<new_repo_name>.git (push)

如果之後要不定時抓舊的 Repository 的進度做更新,或把新的 Repository 的進度推到舊的 Repository,可設定 fetch 和 push 到特定遠端節點 upstream 時,會抓取或推到舊的 Repository 上。

$ git remote add upstream git@github.com/<user_name>/<repo_name>

最後,將程式碼推到遠端新的 Repository 上。

$ git push origin master

注意!這個新的 Repository 要夠乾淨,上面不可以有預設的 README.md、Licence 等系統預先準備好的檔案,否則會推失敗,就算使用 git pull 也會因「fatal: refusing to merge unrelated histories」而無法合併。

如何設定遠端追蹤(Remote Tracking)?

方法一:使用 git checkout -b

方法二:使用 git branch -u

參考資料 / 推薦閱讀


comments powered by Disqus