Git科普文,Git基本原理&各種騷操作

iisheng發表於2020-08-03

Git簡單介紹

Git是一個分散式版本控制軟體,最初由Linus Torvalds創作,於2005年以GPL釋出。最初目的是為更好地管理Linux核心開發而設計。

Git工作流程以及各個區域

  • Workspace:工作區
  • Staging/Index:暫存區
  • Local Repository:本地倉庫(可修改)
  • /refs/remotes:遠端倉庫的引用(不可修改)
  • Remote:遠端倉庫

Git檔案狀態變化

Git各種命令

Git簡單命令


# 在當前目錄新建一個git倉庫
git init

# 開啟git倉庫圖形介面
gitk

# 顯示所有變更資訊
git status

# 刪除所有Untracked files
git clean -fd

# 下載遠端倉庫的所有更新
git fetch remote

# 下載遠端倉庫的所有更新,並且Merge
git pull romote branch-name


# 檢視上次commit id
git rev-parse HEAD 

# 將指定分支合併到當前分支
git merge branch-name

# 將最近的一次commit打包到patch檔案中
git format-patch HEAD^ 

# 將patch檔案 新增到本地倉庫
git am  patch-file

# 檢視指定檔案修改歷史
git blame file-name

Git常用命令

git clone

# 將遠端git倉庫克隆到本地
git clone url

# 將遠端git倉庫克隆到本地
git clone -b branch url 

git stash

# 將修改過,未add到Staging區的檔案,暫時儲存起來
git stash

# 恢復之前stash儲存的內容
git stash apply

# 儲存stash 並寫message
git stash save "stash test"

# 檢視stash了哪些儲存
git stash list

# 將stash@{1}儲存的內容還原到工作區
git stash apply stash@{1}

# 刪除stash@{1}儲存的內容
git stash drop stash@{1}

# 刪除所有快取的stash
git stash clear

git config

# 配置git圖形介面編碼為utf-8
git config --global gui.encoding=utf-8 

# 設定全域性提交程式碼的使用者名稱 
git config --global user.name name  
# 設定全域性提交程式碼時的郵箱
git config --global user.email email
# 設定當前專案提交程式碼的使用者名稱 
git config user.name name  

git remote

# 顯示所有遠端倉庫
git remote -v  

#  增加一個新的遠端倉庫
git remote add name url 

#  刪除指定遠端倉庫
git remote remove name

# 獲取指定遠端倉庫的詳細資訊
git remote show origin

git add

# 新增所有的修改到Staging區
git add .
git add --all  

# 新增指定檔案到Staging區
git add file   

# 新增多個修改的檔案到Staging區
git add file1 file2   

# 新增修改的目錄到Staging區
git add dir

# 新增所有src目錄下main開頭的所有檔案到Staging區    
git add src/main*  

git commit

# 提交Staging區的程式碼到本地倉庫區
git commit -m "message"  

# 提交Staging中在指定檔案到本地倉庫區
git commit file1 file2 -m "message"  

# 使用新的一次commit,來覆蓋上一次commit
git commit --amend -m "message" 

# 修改上次提交的使用者名稱和郵箱
git commit --amend --author="name <email>" --no-edit

git branch

# 列出本地所有分支
git branch   

# 列出本地所有分支 並顯示最後一次提交的雜湊值
git branch -v

# 在-v 的基礎上 並且顯示上游分支的名字
git branch -vv

# 列出上游所有分支
git branch -r  

# 新建一個分支,但依然停留在當前分支
git branch branch-name  

# 刪除分支
git branch -d branch-name   

# 設定分支上游
git branch --set-upstream-to origin/master

# 本地分支重新命名
git branch -m old-branch new-branch

git checkout

# 建立本地分支並關聯遠端分支
git checkout -b local-branch origin/remote-branch

# 新建一個分支,且切換到新分支
git checkout -b branch-name

# 切換到另一個分支
git checkout branch-name  

# 撤銷工作區檔案的修改,跟上次Commit一樣
git checkout commit-file  

git tag

# 建立帶有說明的標籤
git tag -a v1.4 -m 'my version 1.4'

#  打標籤
git tag tag-name

# 檢視所有標籤
git tag 

# 給指定commit打標籤
git tag tag-name commit-id

# 刪除標籤
git tag -d tag-name   

git push

# 刪除遠端分支
git push origin :master   

#  刪除遠端標籤
git push origin --delete tag tag-name

# 上傳本地倉庫到遠端分支
git push remote branch-name

# 強行推送當前分支到遠端分支
git push remote branch-name --force

# 推送所有分支到遠端倉庫
git push remote --all  

# 推送所有標籤
git push --tags

# 推送指定標籤
git push origin tag-name

#  刪除遠端標籤(需要先刪除本地標籤)
git push origin :refs/tags/tag-name  

# 將本地dev分支push到遠端master分支
git push origin dev:master

git reset

# 將未commit的檔案移出Staging區
git reset HEAD

# 重置Staging區與上次commit的一樣
git reset --hard  

# 重置Commit程式碼和遠端分支程式碼一樣
git reset --hard origin/master

# 回退到上個commit
git reset --hard HEAD^

# 回退到前3次提交之前,以此類推,回退到n次提交之前
git reset --hard HEAD~3

回退到指定commit
git reset --hard commit-id     

git diff

# 檢視檔案在工作區和暫存區區別
git diff file-name

# 檢視暫存區和本地倉庫區別
git diff --cached  file-name

# 檢視檔案和另一個分支的區別
git diff branch-name file-name

# 檢視兩次提交的區別
git diff commit-id commit-id  

git show

# 檢視指定標籤的提交資訊
git show tag-name

# 檢視具體的某次改動
git show commit-id 

git log

# 指定資料夾 log
git log --pretty=format:"%h %cn %s %cd" --author="iisheng\|勝哥"  --date=short src
# 檢視指定使用者指定format 提交
  git log --pretty=format:"%h %cn %s %cd" --author=iisheng --date=short 

# 檢視該檔案的改動歷史
git log --pretty=oneline file

# 圖形化檢視歷史提交
git log --graph --pretty=oneline --abbrev-commit

# 統計倉庫提交排名前5
git log --pretty='%aN' | sort | uniq -c | sort -k1 -n -r | head -n 5

# 檢視指定使用者新增程式碼行數,和刪除程式碼行數
git log --author="iisheng" --pretty=tformat: --numstat | awk '{ add += $1 ; subs += $2 } END { printf "added lines: %s removed lines : %s \n",add,subs }'

git rebase

# 將指定分支合併到當前分支
git rebase branch-name

# 執行commit id 將rebase 停留在指定commit 處
git rebase -i commit-id

# 執行commit id 將rebase 停留在 專案首次commit處
git rebase -i --root

git restore

# 恢復第一次add 的檔案,同 git rm --cached
git restore --staged file

# 移除staging區的檔案,同 git checkout
git restore file

git revert

# 撤銷前一次commit
git revert HEAD

# 撤銷前前一次commit
git revert HEAD^

# 撤銷指定某次commit
git revert commit-id

Git騷操作

Git命令不能自動補全?(Mac版)

我見過有的人使用Git別名,反正因為有自動補全的存在,我從來沒用過Git別名。不過我的確將我的rm -rf命令替換成了別的指令碼了...

安裝bash-completion

brew install bash-completion

新增 bash-completion 到 ~/.bash_profile:

 if [ -f $(brew --prefix)/etc/bash_completion ]; then
    . $(brew --prefix)/etc/bash_completion
 fi

shell有不同種類,我這裡使用的是bash

程式碼沒寫完,突然要切換到別的分支怎麼辦?

暫存未提交的程式碼

git stash

還原暫存的程式碼

git stash apply

怎麼合併其他分支的指定Commit?

使用cherry-pick命令

git cherry-pick 指定commit-id

本地臨時程式碼不想提交,怎麼一次性清空?

還原未commit的本地更改的程式碼

git reset --hard

還原包含commit的程式碼,到跟遠端分支相同

git reset --hard origin/master

已經提交的程式碼,不需要了,怎麼當做沒提交過?

還原到上次commit

git reset --hard HEAD^

還原到當前之前的幾次commit

git reset --hard HEAD~2

強制推送到遠端分支,確保沒有其他人在push,不然可能會丟失程式碼

git push origin develop --force

歷史commit作者郵箱寫錯了,怎麼一次性改過來?

使用git filter-branch命令。

複製下面的指令碼,替換相關變數

  • OLD_EMAIL
  • CORRECT_NAME
  • CORRECT_EMAIL

指令碼如下:

#!/bin/sh

git filter-branch --env-filter '

OLD_EMAIL="your-old-email@example.com"
CORRECT_NAME="Your Correct Name"
CORRECT_EMAIL="your-correct-email@example.com"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_COMMITTER_NAME="$CORRECT_NAME"
export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_AUTHOR_NAME="$CORRECT_NAME"
export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

強制推送替換

git push --force --tags origin 'refs/heads/*'

不小心把不該提交的檔案commit了,怎麼永久刪除?

也是使用git filter-branch命令。

git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch FILE-PATH-AND-NAME" \
  --prune-empty --tag-name-filter cat -- --all

強制推送覆蓋遠端分支。

git push origin --force --all

強制推送覆蓋遠端tag

git push origin --force --tags

怎麼保證團隊成員提交的程式碼都是可執行的?

這裡想說的是使用git hooks,一般在專案目錄.git/hooks,客戶端可以使用hooks,控制團隊commit提交規範,或者push之前,自動編譯專案校驗專案可執行。服務端可以使用hooks,控制push之後自動構建專案,merge等自動觸發單元測試等。

git reset --hard命令,執行錯了,能恢復嗎?

檢視當前commit log

誤操作git reset --hard 8529cb7

執行git reflog

還原到之前的樣子

公司使用GitLab,平時還用GitHub,多賬號SSH,如何配置?

編輯 ~/.ssh/config檔案 沒有就建立

# github
Host github.com
Port 22
HostName github.com
PreferredAuthentications publickey
AddKeysToAgent yes
IdentityFile ~/.ssh/github_id_rsa
UseKeychain yes
User iisheng

# gitlab
Host gitlab.iisheng.cn
Port 22
HostName gitlab.iisheng.cn
PreferredAuthentications publickey
AddKeysToAgent yes
IdentityFile ~/.ssh/gitlab_id_rsa
UseKeychain yes
User iisheng

Git commits歷史如何變得清爽起來?

多用git rebase

比如,開發分支是feature,主幹分支是master。我們在進行程式碼合併的時候,可以執行下面的命令。

# 切換當前分支到feature
git checkout feature

# 將當前分支程式碼變基為基於master
git rebase master

然後我們再切換到master分支,執行git merge feature,就可以進行快進式合併了,commmits歷史就不會有交叉了。後文我們會詳細講解。

git rebase會更改commit歷史,請謹慎使用。

下面的圖是Guava專案的commit記錄。

如何修改已經提交的commit資訊?

原始Git提交記錄是這樣的

執行git rebase -i 070943d,對指定commitId之前的提交,進行修改

修改後Git提交記錄變成了這樣

git rebase -i非常實用,還可以將多個commit合併成一個等很多事情,務必要記下。

不小心執行了git stash clear怎麼辦?

git fsck --lost-found

執行之後,可以找到相關丟失的commit-id,然後merge一下即可。

該命令上可以找回git add之後被弄丟的檔案。

啥?你沒執行過git add程式碼就丟了?別怕,一般編譯器有Local History趕緊去試試吧。

詳解git merge

我們執行git merge命令的時候,經常會看到Fast-forward字樣,Fast-forward到底是個什麼東西?

其實,git merge一般有三種場景。

快進式合併

舉個例子,假如初始存在masterhotfix分支是這樣的。

然後我們在hotfix分支加了些程式碼,分支變成這樣了。

這個時候,我們將hotfix分支,mergemaster,即執行git merge hotfix

由於的分支hotfix所指向的提交C3C2的直接後繼, 因此Git會直接將指標向前移動。換句話說,如果順著一個分支走下去能夠到達另一個分支,那麼Git在合併兩者的時候, 只會簡單的將指標向前推進(指標右移),因為這種情況下的合併操作沒有需要解決的分歧——這就叫做 快進(fast-forward)

三方合併

再舉個例子,假如初始存在featuremaster分支情況是這樣的。

然後我們在feature分支加了些程式碼,而master分支也有人加了程式碼,現在分支變成這樣了。

這個時候,我們將feature分支,mergemaster,即執行git merge feature

和之前將分支指標向前推進所不同的是,Git將此次三方合併的結果做了一個新的快照並且自動建立一個新的提交指向它。這個被稱作一次合併提交,它的特別之處在於他有不止一個父提交。

所以我們也知道了,為什麼有的時候merge之後會產生新的commit,而有的時候沒有。

遇到衝突時的合併

如果在兩個分支分別對同一個檔案做了改動,Git就沒法直接合並他們。當遇到衝突的時候,Git會自動停下來,等待我們解決衝突。就像這樣

$ git merge dev 
Auto-merging 111.txt
CONFLICT (content): Merge conflict in 111.txt
Automatic merge failed; fix conflicts and then commit the result.

我們可以在合併衝突後的任意時刻使用git status命令來檢視那些因包含合併衝突而處於未合併unmerged狀態的檔案。

$ git status 
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   111.txt

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

待解決衝突的檔案Git會以未合併的狀態標識出來,出現衝突的檔案會出現一些特殊的區段,看起來像下面的樣子。

<<<<<<< HEAD
111aaa
=======
111b
>>>>>>> dev

<<<<<<< 後面的是當前分支的引用,我們的例子中,就代表master分支。>>>>>>>後面表示的是要合併到當前分支的分支,即dev分支。=======的上半部分,表示當前分支的程式碼。下半部分表示dev分支的程式碼。

我們可以把上面的測試內容改成下面的樣子來解決衝突

111aaa

在解決了所有檔案裡的衝突之後,對每個檔案使用git add命令來將其標記為衝突已解決。

解決衝突的過程中,每一步都可以執行git status檢視當前狀態,Git也會給出相應提示,進行下一步操作。當我們所有的檔案都暫存之後時,執行git status時,Git會給我們看起來像下面的這種提示

$ git status 
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

然後,我們根據提示執行git commit

Merge branch 'dev'
  
# Conflicts:
#       111.txt
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#       .git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# All conflicts fixed but you are still merging.
#

然後,我們儲存這次提交就完成了這次衝突合併。

詳解git rebase

rebase做了什麼

舉個例子。我們同樣用剛才merge的場景。

如果不用rebase,使用merge是下面這樣的,合併分支的時候會產生一個合併提交,而且會有分支交叉的情況。

使用rebase是下面這樣的。

然後,切換到master分支,進行一次快進式合併。

變基實際上就是基於其他分支重塑當前分支。變基之後,當前分支就相當於是基於最新的其他分支新加了一些commit,這樣的話就可以進行快進式合併了。

rebase原理

它的原理是首先找到這兩個分支(即當前分支 dev、變基操作的目標基底分支master)的最近共同祖先 C2,然後對比當前分支相對於該祖先的歷次提交,提取相應的修改並存為臨時檔案, 然後將當前分支指向目標基底C3, 最後以此將之前另存為臨時檔案的修改依序應用,也就是在C3後面新增C4'C5'

Git物件與快照

提到Git,總有人會說快照快照是個什麼鬼?

實際上,Git是一個內容定址檔案系統,其核心部分是一個簡單的鍵值對資料庫。將Git中的物件,儲存在.git/objects目錄下。

Git物件主要分為,資料物件(blob object)樹物件(tree object)提交物件(commit object)標籤物件(tag object)

資料物件

我們新建一個目錄,然後在該目錄下執行git init初始化一個Git專案。

然後,檢視.git/objects目錄下都有什麼。

$ find .git/objects
.git/objects
.git/objects/pack
.git/objects/info

接著,我們寫一個檔案echo '1111' > 111.txt,並執行git add之後,再檢視。

$ find .git/objects
.git/objects
.git/objects/5f
.git/objects/5f/2f16bfff90e6620509c0cf442e7a3586dad8fb
.git/objects/pack
.git/objects/info

我們發現.git/objects目錄下,多了個檔案和目錄。實際上,Git會將我們的檔案資料外加一個頭部資訊header一起做SHA-1校驗運算而得到校驗和。然後,校驗和的前2個字元用於命名子目錄,餘下的38個字元則用作檔名。

我們可以使用下面的命令,顯示在Git物件中儲存的內容。

$ git cat-file -p 5f2f16bfff90e6620509c0cf442e7a3586dad8fb
1111

這就是我們在上文寫入的檔案內容。

上述型別的物件稱之為資料物件(blob object)。資料物件,僅儲存了檔案內容,而檔名字沒有被儲存。

樹物件

資料物件大致對應UNIX中的inodes或檔案內容,樹物件則對應了UNIX中的目錄項。一個樹物件包含了一條或多條樹物件記錄(tree entry),每條記錄含有一個指向資料物件或者子樹物件的SHA-1指標,以及相應的模式、型別、檔名資訊。

通常,Git根據某一時刻暫存區(即index區域)所表示的狀態建立並記錄一個對應的樹物件。

當我們執行過git add之後,暫存區就有內容了,我們可以通過Git底層命令,生成樹物件。

$ git write-tree
b716c7b049ccd9048b0566a57cfd516c17c1e39f

檢視該樹物件的內容。

$ git cat-file -p b716c7b049ccd9048b0566a57cfd516c17c1e39f
100644 blob 5f2f16bfff90e6620509c0cf442e7a3586dad8fb	111.txt

提交物件

資料物件儲存了資料的內容,樹物件可以表示當前目錄的快照。但是,若想重用這些快照,必須記住樹物件的SHA-1雜湊值。而且,我們也不知道是誰儲存了這些快照,在什麼時刻儲存的,以及為什麼儲存這些快照。而以上這些,正是
提交物件(commit object)
能儲存的基本資訊。

我們對當前暫存區進行一次提交,git commit -m "first commit"

然後檢視一下log找到該次提交的commit雜湊值。

$ git log --oneline 
5281f7e (HEAD -> master) first commit

接著,我們檢視一下該提交物件的內容。

$ git cat-file -p 5281f7e
tree b716c7b049ccd9048b0566a57cfd516c17c1e39f
author iisheng <***@gmail.com> 1596073568 +0800
committer iisheng <***@gmail.com> 1596073568 +0800

first commit

提交物件的格式很簡單:它先指定一個頂層樹物件,代表當前專案快照;然後是可能存在的父提交(前面描述的提交物件並不存在任何父提交);之後是作者/提交者資訊(依據你的user.nameuser.email配置來設定,外加一個時間戳);留空一行,最後是提交註釋。

標籤物件

標籤物件(tag object) 非常類似於一個提交物件——它包含一個標籤建立者資訊、一個日期、一段註釋資訊,以及一個指標。主要的區別在於,標籤物件通常指向一個提交物件,而不是一個樹物件。它像是一個永不移動的分支引用——永遠指向同一個提交物件,只不過給這個提交物件加上一個更友好的名字罷了。

實際上Git中的各種物件都是類似的,只不過因為各種物件自身功能不同,儲存結構不同而已。

Git引用-我從遠端拉的程式碼不是最新的?

Git引用相當於是Git中特定雜湊值的別名。一長串的雜湊值不是很友好,但是起個別名,我們就可以像這樣git show mastergit log master的去使用他們。

Git中的引用儲存在.git/refs目錄下。我們可以執行find .git/refs/檢視當前Git專案中都存在哪些引用。

HEAD引用

.git目錄下有一個名字叫做HEAD的檔案,HEAD檔案通常是一個符號引用(symbolic reference)指向目前所在的分支。所謂符號引用,表示它是一個指向其他引用的指標。

如果我們在工作區checkout一個SHA-1值,HEAD引用也會指向這個包含Git物件的SHA-1值。

標籤引用

Git標籤分為,附註標籤和輕量標籤。輕量標籤,使用 git tag v1.0即可建立。附註標籤需要使用-a選項,即git tag -a v1.0 -m "my version 1.0"這種。

輕量標籤就是一個固定的引用。附註標籤需要建立標籤物件,並記錄一個引用來指向該標籤物件。

遠端引用

不熟悉Git的同學,可能會犯這樣一個錯誤。其他同學讓他拉取一下遠端最新的master分支程式碼,他可能直接用IDE找到本地的遠端分支的引用,也就是origin/master,直接checkout一個本地分支。

其實,origin/master只是遠端分支的一個引用,不一定跟遠端分支程式碼同步,我們可以用git fetch或者git pull來讓origin/master和遠端分支同步。

參考文獻:
[1]: https://git-scm.com/

歡迎關注個人微信公眾號【如逆水行舟】,用心輸出基礎、演算法、原始碼系列文章。