正式開始之前先講個親身經歷經歷的故事,在上一個公司工作有個女同事,晚上和男朋友提前說好了約會看電影,快五點了,就想着趕緊提交代碼,好下班,可是她push
了好幾次,都被遠端拒絕了,這時候她一狠心一跺腳用了-f
,然後就下班去了,她倒是下班了,由於她對之前提交的代碼用了rebase
,而其他同事又基於她之前的代碼開發,直接導致了其他同事的commit id
出現了混亂,而導致當天的上線被擱置了,這是一個真實的案例,而其中的問題就在於這個女同事對rebase
不熟悉,並採用了危險的git
操作。因此,git
的操作雖然簡單,但在開發過程中卻非常重要,如果用不好,自己的工作白費了是輕的,更糟糕的是有可能讓自己成爲同事厭惡的人。因此git
用的好,將會是一個神器,用不好就是噩夢!
前言
首先問一下自己以下幾個問題:
如何合併幾個歷史
commit
?如何修改歷史提交的
message
?如何將某個分支的某次
commit
移到另一個分支尼?
要是你都會,那恭喜你,可以不用閱讀本文了,要是還有疑惑,那就跟着我一起來學習學習吧。
一、基本配置
配置用戶信息
git config --global user.name "FrontDream"
git config --global user.email [email protected]
以上配置過程中用到了--global
,其實還有--local
,--system
,下面對此做一個對比,問題不大,可以不用着重考慮。
git config --local // local只對某個倉庫有效
git config --global // global對當前用戶所有倉庫有效
git config --system // 不常用,對系統所有登陸用戶有效
如果想要顯示 config
的配置,可以加上--list
:
git config --list --local
git config --list --global
git config --list --system
配置文本編輯器
當用戶信息設置完畢,你可以配置默認文本編輯器了,當 Git
需要你輸入信息時會調用它。如果沒有配置,Git
會使用操作系統默認的文本編輯器,通常是 Vim
。如果你想使用不同的文本編輯器,例如 Emacs
,可以這樣做:
git config --global core.editor emacs
配置命令的別名
git config --global alias.ci commit
可以通過 git config
文件來輕鬆地爲每一個命令設置一個別名,如上配置後,當要輸入 git commit
時,只需要輸入 git ci
。
二、建立倉庫
主要有以下幾種場景:
把已有的項目代碼納入到 Git 管理
$cd 項目代碼所在文件夾
$git init
新建的項目(沒有代碼)直接用 Git 管理
$ cd 某個文件夾
$ git init your_project #會在當前路徑下創建和項目名稱同名的文件夾
$ cd your_project
克隆倉庫
git clone https://github.com/libgit2/libgit2
這會在當前目錄下創建一個名爲 “libgit2
” 的目錄,並在這個目錄下初始化一個 .git
文件夾, 從遠程倉庫拉取下所有數據放入 .git
文件夾,然後從中讀取最新版本的文件的拷貝。如果你進入到這個新建的 libgit2
文件夾,你會發現所有的項目文件已經在裏面了,準備就緒等待後續的開發和使用。
如果你想在克隆遠程倉庫的時候,自定義本地倉庫的名字,你可以通過額外的參數指定新的目錄名:
git clone https://github.com/libgit2/libgit2 mylibgit
這會執行與上一條命令相同的操作,但目標目錄名變爲了 mylibgit
。
添加遠程倉庫
當你本地已經有了代碼,在 GitHub
上新建了一個倉庫,這時候需要將遠程的倉庫與本地進行合併與關聯。
首先你需要在倉庫中獲取到自己的 SSH
:
通過命令:
git remote add origin [email protected]:FrontDream/FrontDream.github.io.git
這樣就將本地的項目與遠程的項目進行了關聯,如果這時候遠程是有文件的如 readme
文件,直接git push --all
會失敗,這個時候,需要將遠程的代碼拉下來git fetch
,然後再git merge --allow-unrelated-histories origin/master
,再git push
或者git push -u origin master
就好了。
如果想要在某個的倉庫下配置local
的用戶名和郵箱,可以在當前倉庫的路徑下用以下設置:
git config --local user.name "FrontDream"
git config --local user.email [email protected]
這樣就達到了只對某個倉庫進行配置的目的。如下圖所示
三、深入探索.git 文件
HEAD 文件
如上圖所示,現在 master
分支上,通過cd .git
進行git
文件,然後通過cat
命令將 HEAD
文件的內容輸出,然後切換到 204-
分支, 再次進入.git
文件,並將 HEAD
文件的內容輸入。通過對比,我們可以發現,HEAD
文件存放的是當前所在分支的引用。
Config 文件
如上圖所示,通過在.git
目錄下,通過cat
命令將 congig
文件的內容輸出,我們可以看到,這個文件存儲的就是我們之前配置的用戶名和郵箱,我們可以通過文本編輯器打開這個文件,修改用戶名和郵箱,這個修改的效果和我們用git config
的命令相同。
refs 文件
還記得上面 HEAD
文件中存放的是refs/heads/master
嘛?指的就是 refs
文件夾下的 heads
中的 master
, 如上圖所示,通過在.git
目錄下,通過cd
命令進入 refs
,可以看到有 heads
文件夾和 tags
文件夾,繼續進入 heads
,我們可以看到有很多分支名稱的文件,我們通過cat
命令輸出內容,可以發現,各個分支名的文件存儲的其實是 commit
的哈希。可以通過命令git cat-file -t
跟上輸出內容的前面一部分查看其是什麼類型,輸出是commit
類型。
回到 refs
進入 tags
,發現有很多的版本號的名稱,我們通過cat
輸出其中某個版本號文件的內容,同樣發現,tag
同樣也是某個 commit 的哈希值。但是通過git cat-file -t
跟上輸出內容的前面一部分查看其是什麼類型,輸出是tag
類型。繼續通過git cat-file -p
跟上哈希值(一部分就行),發現其實他是一個object
。不要停,繼續通過git cat-file -t
跟上object
後面的一部分哈希值,輸出了commit
類型。這裏需要注意的是git cat-file
後面跟-t
是查看類型,-p
是查看內容
結論: heads
中存放的是各個分支名,而各個分支名其實是commit
的哈希值,而tags
我們也是在某個 commit
後打的標籤,其是通過一個tag
類型,其實是一個object
對象,包裹着一個commit
哈希值。
objects 文件
首先從.git
目錄下進入objects
目錄,我們發現目錄下面有很多兩位數的文件夾,進入其中一個如6b
,我們發現有很多哈希文件,通過6b
,與哈希值進行拼接,並用git cat-file -t
查看類型,發現爲tree
類型(tree
類型是git
中重要的類型,其他類型還有commit
,blob
等);用git cat-file -p
查看內容,我們發現又是一堆哈希,繼續用-t
查看類型,-p
查看內容,我們發現這是我們的新增文件呀,爲blob
類型。
四、基本用法
我們將在此部分中用提問的方式,引出每一條命令。
Q1: 如何查看工作區、暫存區的狀態?
git status
命令的輸出十分詳細,但其用語有些繁瑣。 Git
有一個選項可以幫你縮短狀態命令的輸出,這樣可以以簡潔的方式查看更改。如果你使用 git status -s
命令或 git status --short
命令,你將得到一種格式更爲緊湊的輸出。
git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt
新添加的未跟蹤文件前面有 ??
標記,新添加到暫存區中的文件前面有A
標記,修改過的文件前面有 M
標記。輸出中有兩欄,左欄指明瞭暫存區的狀態,右欄指明瞭工作區的狀態。例如,上面的狀態報告顯示: README
文件在工作區已修改但尚未暫存,而 lib/simplegit.rb
文件已修改且已暫存。 Rakefile
文件已修,暫存後又作了修改,因此該文件的修改中既有已暫存的部分,又有未暫存的部分。
Q2: 如何查看工作區、暫存區的修改後的差異?
git diff
此比較的是工作目錄中當前文件和暫存區域快照之間的差異。也就是修改之後還沒有暫存起來的變化內容。
git diff --staged
這條命令將比對已暫存文件與最後一次提交的文件差異。
git diff 分支一 分支二 -- 要比較的文件
此命令對兩個分支的某個文件進行比較差異。
git diff #hash1 #hash2
此命令對比 commit1
和 commit2
的差異。
git diff HEAD HEAD~2
對比 HEAD
(其實指向的也是一個commit
的哈希)與 HEAD
的父親的父親(上兩次 commit
)的差異。
Q3: 如何跳過添加到暫存區直接提交?
git commit -a -m 'added new benchmarks'
git commit -am 'added new benchmarks' // 簡化版
此命令將跳過使用暫存區域,儘管使用暫存區域的方式可以精心準備要提交的細節,但有時候這麼做略顯繁瑣。Git 提供了一個跳過使用暫存區域的方式, 只要在提交的時候,給 git commit
加上 -a
選項,Git
就會自動把所有已經跟蹤過的文件暫存起來一併提交,從而跳過 git add
步驟。
Q4: 如何將連續的多個commit
合併成一個?
以下圖爲例,需要將前三個commit
合併成一個,首先複製第三個commit
後面的哈希值。
然後運行命令:
git rebase -i #複製的哈希
出現如下界面:
將第二、第三的pick
修改爲s
(squash 簡寫),如下圖所示,保存按esc
,輸入:wq!
保存退出。
繼續出現如下界面:
添加一條message
,如下圖所示:
保存按esc
輸入:wq!
退出。成功界面如下圖所示:
squash
是指將該commit
合併到相鄰的pick
的commit
中。
Q5: 如何重命名或者移動文件?
重命名文件或者移動文件。如果是移動根目錄下的文件,要有相對路徑。
git mv file_from file_to
Q6: 如何刪除指定文件?
git rm 待刪除文件
Q7: 如何保存文件,但不提交到暫存區?
這個問題可以用一個場景來解釋,如果在開發過程中,工作區已經有了修改內容,這時需要立馬修復一個 bug
,這時是不可能直接跳到某個分支去修改的,我們需要先提交到暫存區,但是我其實不滿意與現在的修改,後面還需要再大改,我不想提交到暫存區。這時就可以通過命令:
git stash
此命令將工作區的內容存放在堆棧中,當我們修改好了 bug
,就重新用:
git stash apply
or
git stash pop
恢復工作區的內容,需要注意的是,apply
會在堆棧中保存原有的stash
信息,而pop
將會把stash
中的內容清空。
Q8: 如何查看歷史提交記錄?
git log
我相信小孩都知道,在不傳入任何參數的默認情況下,git log
會按時間先後順序列出所有的提交,最近的更新排在最上面。
其中一個比較有用的選項是 -p
或 --patch
,它會顯示每次提交所引入的差異(按 補丁 的格式輸出)。你也可以限制顯示的日誌條目數量,例如使用 -2
選項來只顯示最近的兩次提交。
git log -p -2
如果我希望在一行顯示需要的信息,可以進行相應的配置如:
git log --pretty=oneline
git log --oneline //簡化版
oneline
會將每個提交放在一行顯示,在瀏覽大量的提交時非常有用。另外還有 short
,full
和 fuller
選項,它們展示信息的格式基本一致,但是詳盡程度不一。
git log --pretty=format:"%h - %an, %ar : %s"
可以定製記錄的顯示格式:
當 oneline
或 format
與另一個 log
選項 --graph
結合使用時尤其有用。這個選項添加了一些 ASCII
字符串來形象地展示你的分支、合併歷史。
git log --pretty=format:"%h %s" --graph
限制輸出長度
示例如下:
$ git log --pretty="%h - %s" --author='Junio C Hamano' --since="2008-10-01" \
--before="2008-11-01" --no-merges -- t/
5610e3b - Fix testcase failure when extended attributes are in use
acd3b9e - Enhance hold_lock_file_for_{update,append}() API
f563754 - demonstrate breakage of detached checkout with symbolic link HEAD
d1a43f2 - reset --hard/read-tree --reset -u: remove unmerged new paths
51a94af - Fix "checkout --track -b newbranch" on detached HEAD
b0ad11e - pull: allow "git pull origin $something:\$current_branch" into an unborn branch
以上是通過命令行git log
查看版本歷史信息,當然也可以通過gitk
命令調出圖形化界面,查看歷史信息。在命令行中輸入gitk
,調出如下界面:
Q9: 如何修改最近的提交信息?
有時候我們提交完了才發現漏掉了幾個文件沒有添加,或者說上一次的提交信息寫錯了,我們想修改當前分支最近的 commit
的 message
。此時,可以運行帶有 --amend
選項的提交命令來重新提交,第二次的提交會替代掉第一次的提交。
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
兩次提交只有一次提交信息。
Q10: 如何修改歷史的提交信息尼?
這裏需要用到rebase
了。如下圖,我們想修改歷史的提交信息“ant4.0 升級”爲“升級 ant4.0”。
首先需要找到需要修改的提交信息的上一個(父級)的哈希值,記住是需要修改的上一次提交的哈希值,並複製,重要的事情說三遍!
然後通過命令:
git rebase -i #你複製的哈希值
然後將出現如下界面:
如下圖所示,修改你需要修改的提交信息前面的pick
爲r
(reward 的簡寫),保存,然後點擊一下esc
,輸入:wq!
回車退出。
接着出現如下界面,供你修改提交信息。
編輯修改,並保存,在英文輸入法下點擊一下esc
,輸入:wq!
回車退出。出現如下界面,即成功修改了歷史的提交信息。
Q11: 如何取消暫存中的文件?
可以通過如下命令實現取消暫存中的文件:
git reset HEAD <file>
Q12: 如何取消工作區中的文件?
如果我們現在正在工作區中工作,發現現在的寫法還不如已經提交的寫法,想把還沒添加到暫存區的文件取消修改,可以通過如下命令:
git checkout -- <file>
Q13: 如何回退到某個 commit?
有時候我們提交了一些改動,後來又不想要了。有可能是WIP
提交,也可能是某個引入了 bug
的提交。這種情況,我們可以執行git reset
。git reset
會丟棄當前所有暫存的文件,並讓我們決定 HEAD 應該指向哪裏。
git reset --soft #哈希
soft reset
將HEAD
移動到指定的提交(或者相對於HEAD
的位置索引),同時不會丟棄這些提交帶來的改動。執行git status
,你會看到我們依然能夠查看之前提交所做的改動。這很有用,因爲這樣我們就能繼續修改文件內容,後續再次提交了。
git reset --hard #哈希
有時候,我們不想保留某些提交帶來的改動。跟 soft reset
不一樣,我們不再需要訪問這些變動了。Git
應該簡單地重置到指定的提交,並且會重置工作區和暫存區的文件。
Q13: 如何刪除不需要的分支?
git branch -d you-branch
Q14: 如何查看分支?
git branch -v
or
git branch -av
-v
表示的是查看本地有哪些分支,而-av
查看的是本地和遠程的分支。
還有一種情況是爲了查看設置的所有跟蹤分支可以用-vv
。將所有的本地分支列出來並且包含更多的信息,如每一個分支正在跟蹤哪個遠程分支與本地分支是否是領先、落後或是都有。
git branch -vv
Q15: 如何查看哪些分支已經合併到當前分支?
git branch --merged
git branch --no-merged
--merged
查看有哪些分支已經合併到當前分支,而--no-merged
查看所有包含未合併工作的分支,因爲它包含了還未合併的工作,嘗試使用 git branch -d
命令刪除它時會失敗(可以使用 -D
選項強制刪除它)。
Q16: 如何跟遠程分支關聯?
如果遠程已經有了feature-branch
,通過git fetch
拉到本地,並通過以下命令在本地新建了feature-branch
,並同遠程分支關聯。
git checkout -b feature-branch origin/feature-branch
Q17: 如何跟將本地代碼推送到遠程的某個分支?
git push origin local-branch:feature-branch
推送本地的 local-branch
(冒號前面的)分支到遠程 origin
的 feature-branch
(冒號後面的)分支(沒有會自動創建)
下一次其他協作者從服務器上抓取數據時,他們會在本地生成一個遠程分支 origin/feature-branch
,指向服務器的 feature-branch
分支的引用。本地不會自動生成一份可編輯的副本(拷貝)。換一句話說,這種情況下,不會有一個新的 feature-branch
分支,只有一個不可以修改的 origin/feature-branch
指針。
可以運行 git merge origin/feature-branch 將這些工作合併到當前所在的分支。
如果想要在自己的 my_branch
分支上工作,可以將其建立在遠程跟蹤分支之上:
git checkout -b my_branch origin/feature-branch
這樣就新開了一個本地分支 my_branch
用戶跟蹤遠程的 feature-branch
。看起來是挺複雜的,但是可以簡化:
git checkout --track origin/feature-branch
這樣的本地的 feature-branch
分支就可以跟蹤遠程的 feature-branch
分支了。還可以繼續簡化:
git checkout feature-branch
當然簡化的化就無法重命名了,如果需要重命名還是得使用:
git checkout -b my_branch origin/feature-branch
Q18: 如何獲取某個分支的某次提交內容?
當活動分支需要某個分支的某個提交包含的改動時,我們可以用cherry-pick
命令。通過cherry-pick
某個提交,在當前活動分支上會創建一個新提交,包含了前者帶來的改動。
如下圖所示(盜來的),假設 dev
分支上的提交76d12
改動了index.js
文件,我們在master
分支上也需要。我們不需要整個分支上的改動,只要這個提交。
git cherry-pick 76d12
Q19: pull 與 fetch 有什麼區別?
用 git fetch
把這些改動獲取到本地。這不會影響本地分支,fetch
只是下載數據。git pull
實際上是兩個命令合而爲一:git fetch
和git merge
。當我們從 origin
拉取改動時,先是像git fetch
一樣獲取所有數據,然後最新改動會自動合併到本地分支。
Q20: 如何變基?
如上圖所示,我們從C2
拉出了特性分支experiment
並進行了開發到C4
,同時master
繼續開發到C3
,現在我們需要將C3
,與C4
進行合併,通過git merge
是其中一種策略:
git checkout master
git merge experiment
另外一種策略就是通過變基:
git checkout experiment
git rebase master
git checkout master
git merge experiment
這兩種整合方法的最終結果都是將C3
和C4
進行合併到主分支,結果沒有任何區別,主要是歷史記錄的區別,rebase
是一種直線型的,提交歷史非常清晰整潔,而merge
相對來說分支比較複雜。
儘管變基會使得我們的提交歷史變得更加簡潔,但是變基是有風險的,但是當rebase
出現衝突時,處理過程比較麻煩,同時當對已經push
到遠端,同事也基於你的push
進行開發,然後你又對之前的push
執行變基操作,就會出問題,本文開始的故事就是真實的案例。總的原則是,只對尚未推送或分享給別人的本地修改執行變基操作清理歷史, 從不對已推送至別處的提交執行變基操作。
總結
git
操作總的來說並不是很難,難點在於對命令的理解和記住,好好掌握,讓git
成爲我們的利器而不是我們的噩夢!
最後
如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進技術羣,長期交流學習...
關注公衆號「前端下午茶」,持續爲你推送精選好文,也可以加我爲好友,隨時聊騷。
點個在看支持我吧,轉發就更好了