【VCS】Git之無盡探索

本文是關於Git探索的一篇文章,闡述了Git的大部分命令和使用方式,並列舉了幾個典型的使用場景以供參考和體會。

多host環境
clone,remote,fetch,pull,push
gitignore
add
stash
commit
status,log,diff,blame,grep
branch,checkout,merge
tag
cherry-pick
checkout,reset,revert,rm
rebase
mv,repack,bisect
Other Case

對於Git這個分佈式的VCS,從鏈表的角度來看待是最容易理解的:
一次commit相當於添加一個節點,節點由hash標識,內容就是所做修改的索引;每個分支都是一條鏈,有一個指向頭結點的指針HEAD

Git配置

下載地址:點我跳轉下載

添加環境變量:

以Windows爲例:

%GIT_HOME%\bin;%GIT_HOME%\usr\bin;

生成SSH密鑰:

ssh-keygen -t rsa -C "[email protected]"

會提示輸入路徑,建議:
/c/User/用戶名/.ssh/id_rsa_abc

添加多Host環境

添加多host環境,對於在公司使用自己的機器是很有必要的,公私分明嘛~
文件位置/C/User/用戶名/.ssh/config,沒有就新建一個:內容如下

# 配置git.oschina.net 
Host git.oschina.net 
    HostName git.oschina.net
    IdentityFile C:\\Users\\用戶名\\.ssh\\id_rsa_oschina
    PreferredAuthentications publickey
    User oschinaUserName
    
# 配置github.com
Host github.com                 
    HostName github.com
    IdentityFile C:\\Users\\用戶名\\.ssh\\id_rsa_github
    PreferredAuthentications publickey
    User githubUserName
    

測試連接:
連通後會生成 /C/User/用戶名/.ssh/known_hosts 文件:

ssh -T [email protected]
ssh -T git.oschina.net

這樣就可以把公鑰 id_rsa_xxx 添加到git服務器上的 SSH Keys 中去了。

最後可按需要,順帶配置用戶信息:
全局配置:

git config --global user.name 姓名
git config --global user.email 郵箱

爲當前倉庫設置用戶信息:

git config --local user.name 姓名
git config --local user.email 郵箱

一般倉庫可以採用SSH方式關聯,但必須以HTTP/HTTPS方式關聯遠程倉庫時,可以考慮記住密碼:在用戶目錄下生成文件.git-credential記錄用戶名密碼的信息。

# 默認會話時長(據說是15min):
git config --local credential.helper cache		# linux/mac
git config --local credential.helper wincred	# windows

# 自己定義會話時間:
git config credential.helper 'cache --timeout=3600'		# linux/mac
git config credential.helper 'wincred --timeout=3600'	# windows
# 不過期會話時間:
git config --local credential.helper store

至此,Git配置結束。

Git操作

Git相關的命令如果記不太清楚可以使用提示:

git --help
git 具體某條命令 --help

倉庫(clone,remote,fetch,pull,push)

在當前目錄初始化一個倉庫:

git init

克隆一個遠程倉庫:

git clone <URL> [--branch <branch> --depth=1] [./dir]

# 準備後續免密push的操作(字符需要Escape方式編碼,git remote -v可看到密碼)
git clone http://賬號:密碼@git.ops.test.com.cn/root/puppet.git

添加子module,這樣子文件夾會做文件處理:

git submodule add <URL> <./submodule>

遠程倉庫關聯:

git remote [-v]
git remote add <name> <URL>
git remote set-url <name> <URL>
git remote remove <name>

拉取本地倉庫沒有的內容(新增分支,遠程分支修改等):

git fetch [origin <branch>]

拉取本分支的更新併合並:pull = fetch + merge,默認拉去所有合併到對應分支,當指定遠程分支時默認合併到當前分支:

git pull origin <remote_branch>[:<local_branch>]

準備推送本分支的修改到遠程倉庫前可以先查看本地分支和遠程分支的關聯情況:

git branch -vv

已經關聯遠程分支可以直接push:

git push

遠程分支不存在:

git push [-u] origin <local_branch>:<remote_branch>
	# -u 表示添加關聯

只關聯不push:

git branch --set-upstream-to <origin/remote_branch> <local_branch>

忽略追蹤(.gitignore)

編輯倉庫中的 .gitignore 文件,內容格式如下

vim .gitignore
    # 忽略目錄
    build/
    # 忽略yml後綴文件
    *.yml
    # 不忽略a.yml
    !a.yml

gitignore只能忽略untracked文件,對於stracked文件需要刪除索引:

git rm --cached [-r] <file>

Git中的文件狀態

Git倉庫中文件大概有如下幾種狀態,並可通過相應的操作切換:

git-file-status

staging area(add)

  • 保存(new-untracked)和(modified),不包括(remove-untracked):

    git add .
    
  • 保存(remove-untracked)和(modified),不包括(new-untracked):

    git add -u
    
  • 保存所有變化:

    git add -A
    
  • 部分提交,後續會有選擇,捨棄的修改將留在working directory:

    git add -p filename
    	# y - 存儲這個hunk 
    	# n - 不存儲這個hunk 
    	# q - 離開,不存儲這個hunk和其他hunk 
    	# a - 存儲這個hunk和這個文件後面的hunk 
    	# d - 不存儲這個hunk和這個文件後面的hunk 
    	# g - 選擇一個hunk 
    	# / - 通過正則查找hunk 
    	# j - 不確定是否存儲這個hunk,看下一個不確定的hunk 
    	# J - 不確定是否存儲這個hunk,看下一個hunk 
    	# k - 不確定是否存儲這個hunk,看上一個不確定的hunk 
    	# K -不確定是否存儲這個hunk,看上一個hunk 
    	# s - 把當前的hunk分成更小的hunks 
    	# e - 手動編輯當前的hunk 
    	# ? - 輸出幫助信息
    

暫存(stash)

這樣可以保持分支的工作現場乾淨,操作對象是整個staging area

# push
git stash

# pick
git stash apply

# pop
git stash drop

# pop & pick
git stash pop

repository(commit)

提交:

git commit [filename] -m "annotation"

提交working directory,相當於:git add -u + git commit -m "boom~"

git commit -am "boom~"

撤銷HEAD並提交staging area補充到HEAD提交:

git commit -amend -m "make up"

查看(status,log,diff,blame,grep)

查看staging areaworking directory的狀態:

git status

查看提交記錄:

git log --online --graph --all
	# all: 所有分支(默認當前)
	# oneline: 簡易hash, --pretty=oneline
	# graph: 畫出合併切出圖

操作記錄,含reset

git reflog --oneline

文件對比:

  • 對比 (staging area | repository) 和 working directory

    git diff [branch1 branch2] <file>
        # 加branch會對比兩個分支中的文件
    
  • 對比 repositoryworking directory

    git diff HEAD <file>
    
  • 對比 repositorystaging area

    git diff --cached <file>
    

查看修改人:

git blame <file>

查找內容:

git grep '[123]\{1,\}'

分支(branch,checkout,merge)

分支可以理解爲鏈表,鏈表中的元素是commit
查看分支:

git branch [-a]
	# -a 包含遠程分支

切出分支:

git checkout [-b <new_branch>] origin/release|$hash$|tags/v1.0

合併developmaster

  1. 合併前檢查是否存在衝突:

    git diff master develop --stat
    
  2. 開始合併,並解決衝突:

    (master)$ git merge [--squash] develop
    	# --squash 融合提交記錄,參見rebase
    
    # 對於無法自動合併的衝突需要手動處理
    	<<<<<< HEAD 
    	# 當前更改
    	======
    	# 傳入的更改
    	>>>>>> dev 
    
  3. 提交前檢查衝突是否處理完畢:

    git diff --check
    	# 如果沒問題就可以 commit -am 了
    
  4. 如果衝突太多太麻煩,想放棄解決了,可以終止:

    git merge --abort
    

刪除分支:

# -r表示一併刪除origin裏面分支,這只是刪除了一個本地追蹤,下次不會再fetch它:
git branch -d [-r] <branch_name>
# 刪除遠程的分支還需要push過去刪除
git push origin -d <remote_branch>

標籤(tag)

Fetch倉庫TAG:

git fetch origin --tags

查看TAG:

git tag
git tag -l 'v1.0.*'
git show v1.0.0

在當前分支HEAD打TAG:

git tag v0.1
# 在某個commit上打tag,並加附註
git tag -a v0.2 -m "version 0.1 released" $hash$

push tag:

git push origin v1.0
# push 本地所有tag
git push origin --tags

刪除tag

# 刪除本地tag
git tag -d v1.0
# 刪除遠程tag
git push origin --delete tag v1.0

cherry-pick

使用cherry-pick可以把其他分支的一部分commit提交到當前分支:

git log --all --online
# cherry-pick
git cherry-pick [-n] $hash$ $hash$
	#-n,表示先不commit,把commits保留到staging area

撤銷(checkout,reset,revert,rm)

checkout:用staging area覆蓋work directory

git checkout -- <file>

reset:用repository覆蓋staging area,若working directory有修改,則丟棄staging area

git reset HEAD <file>
  • soft, 把repository的($hash$,HEAD]切出到staging area
    git reset --soft HEAD~1|$hash$
    
  • mixed, 把repository的($hash$,HEAD]切出到working directory
    git reset --mixed HEAD~1|$hash$
    
  • hard, 把repository的($hash$,HEAD]拋棄:
    git reset --hard HEAD~1|$hash$
    

案例:找回reset掉的內容:

  1. 查看操作記錄:
    git reflog --oneline
    
  2. 找到某次reset操作(reset記錄會以reset前的HEAD做hash標識),把HEAD指向它:
    git reset --hard $reset_hash$
    # 或者
    git rebase --onto HEAD HEAD $reset_hash$
    

revert:提交逆修改,HEAD繼續前進,而resetHEAD回退:

git revert HEAD~1|$hash$

rm --cached:刪除追蹤索引,不能操作work directory的文件;該操作會把文件從staging area|repository 移到work directory並置爲untracked;此外如果該文件曾commit過,會在staging area生成一個deleted:<file>記錄:

git rm --cached [-r] <file>

rm:刪除repository中的文件,並在staging area添加一個deleted:<file>

git rm <file>

rm -f:刪除untracked文件和文件夾:

git rm <file> -f [-r] [-q] 
	# -r 表示遞歸
	# -q 無刪除回顯

clean -fd:清理工作區,遞歸刪除所有untracked的文件和文件夾:

git clean -fd

好了,重頭戲來了。

衍合(rebase)

這個翻譯其實有些抽象,rebase的作用是對一段線性提交做一些操作,操作後分化出一個新的分支。
這裏我對提交的理解是:若干個commit以hash標識成點,形成一個commit鏈;其中HEAD是指針變量,分支名是指針常量(鏈表頭結點,或者理解成一維數組名)。
下面的語法中,endpoint是可選的,默認爲HEAD。如果發現endpoint常量指針(即分支名),則分化出的分支自動應用到該分支並checkout過去。

語法:

git rebase [-i] [--onto] [base_branch] startpoint [endpoint] 
	# 其中線性提交段是 (startpoint, endpoint],前開後閉

rebase合入分支最新修改(merge按時間排序commit,rebase根據分支排序),reabase把衝突處理交給提MR的人處理,提交記錄比merge更乾淨:

git rebase origin/master [HEAD]
	# solve conflicts
git add <conflict_files>
git rebase --continue

編輯分支上的一段線性提交(reword,squash,reorder,drop):

git rebase -i startpoint [endpoint]
	# p, pick = 使用提交
	# r, reword = 使用提交,但修改提交說明
	# e, edit = 使用提交,但停止以便進行提交修補
	# s, squash = 使用提交,但和前一個版本融合
	# f, fixup = 類似於 "squash",但丟棄提交說明日誌
	# x, exec = 使用 shell 運行命令(此行剩餘部分)
	# d, drop = 刪除提交

下面分析4個rebase典型案例,假設當前分支狀態如下圖所示:
[外鏈圖片轉存失敗(img-odPtFdEq-1562834637827)(https://i.loli.net/2019/03/14/5c8a3fbc6f7d9.png)]

案例1-並集:next基於master;此時得知master有新修改,next需要同步master的最新修改。

git rebase master next

# 命令完成後自動切到next分支
# 此時next = [master + next#HEAD]

案例2-差集:topic基於next,next基於master;此時得知next有重大bug,要求topic中屬於next部分的提交都得去掉。

# 差集計算:topic = master + (topic - next)
git rebase --onto master next topic

# 多段差集:topic = master + (topic#HEAD~4, topic]
git rebase --onto master HEAD~4 topic

案例3-修改commit:編輯(reword,reorder,edit,drop)一段Commit:

git rebase -i HEAD~4 HEAD

案例4-融合commit:融合(squash)操作也屬於修改,但這裏單獨拿出來示例,因爲它可以多人合作完成,也可以個人獨立完成:

  1. 方式一:由A同學解衝突。 A同學利用squash合併commit,B同學直接合並。
    # A同學
    git rebase -i next topic
    
    # B同學
    (next)$ git merge topic
    
  2. 方式二:由B同學解衝突。 B同學自己融合A同學提交的MR。
(next)$ git merge --squash topic
git commit -m "merged topic to next"

其他命令(mv,repack,bisect)

# 重命名/移動staged的文件後自動處於staged
git mv oldfile newfile
git mv file folder

# repack 把鬆散對象打包以提高git運行效率
git repack -d

# 二分查找有問題的提交
git bisect start master f608824
git bisect run make test
	# test腳本爲判斷是否有問題的依據
	line = gets
	exit 1 if line != "15\n"

一些技巧

  1. 淺克隆:對於比較大的倉庫,本地不想要那麼多無關的分支,無關的歷史記錄,可以考慮淺克隆。

    # 以淺克隆的方式獲取庫
    git clone <URL> --branch <master> --depth=1
    
    # 添加origin中的分支
    git remote set-branches origin <branch_new1> <branch_new2>
    
    # 以淺克隆的方式fetch origin中的分支
    git fetch --depth 1 origin [<branch_new1>]
    
    # 深化origin 中的克隆深度爲 N
    git fetch origin --deepen N
    
    # 刪除分支
    git branch -rd <branch_name>
    git push origin -d <branch_name>
    
    
  2. 拓撲序:對於提交日誌,默認是按commit時間排序,拓撲序可以把相關聯的多個提交放在一起顯示。

    # 默認按時間排序
    git log --date-order --oneline
    
    # 指定拓撲排序
    git log --topo-order --oneline
    
  3. 二分查找:有時候無法從final版本代碼定位問題,可能需要回滾代碼定位。可以考慮log(N)複雜度的二分查找。

    # 開始二分查找(reset的時候會回到這個HEAD)
    git bisect start
    # 告訴git,當前的HEAD提交是有問題的
    git bisect bad
    # 告訴git,某個提交是OK的
    git bisect good $hash$
    # 這時git會reset到中間的commit,此時編譯驗證,並告知結果
    git bisect <bad/good>
    # 這樣git會不斷劃分區間查找,最終會告訴你哪個commit有問題。找到後退出查找模式
    git bisect reset
    

以上。

此外,如題,筆者也在不停探索,如果有錯誤歡迎指出斧正~

最後感謝這個遊戲:Githug

以及這個通關攻略:「Githug」Git 遊戲通關流程

還有這個在線練習平臺:Git在線練習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章