關於Git,你真的學會了嗎?

「鋒哥,Git有什麼可說的,不就是git add添加,git commit提交嘛」 據說我要寫一篇Git教程,小明不屑一顧地說。 「..."。java

小明是個人一個學生。目前,是一名Android開發工程師。git

過了幾天,我又再次見到了小明。程序員

「鋒哥,今天,我在Github新建了一個版本庫,本地提交後推送遠程的時候,卻被拒絕了,是怎麼回事?」github

如下是小明的操做記錄:數據庫

git init
git add .
git commit -m "Init commit"
git remote add origin git@github.com:xiaoming/xxx.git
git pull origin master
複製代碼

以上操做觸發了下面的錯誤:編程

From git@github.com:xiaoming/xxx.git
 * branch            master     -> FETCH_HEAD
 * [new branch]      master     -> origin/master
fatal: refusing to merge unrelated histories
複製代碼

「小明,注意看最後一句提示。翻譯成中文的意思是 ‘拒絕合併不相關的歷史’,這個問題有兩個方案能夠處理。"安全

  • git pull命令實際上是觸發了拉取git fetch和合並git merge兩個操做。而本地的版本庫和遠程版本庫在第一次拉取或推送完成以前是絕不相關的,Git爲了不沒必要要的合併,默認不容許進行這樣的操做。但你能夠手動添加--allow-unrelated-histories強制進行合併,這是方案一。
git pull origin master --allow-unrelated-histories
複製代碼
  • 再來看方案二,從你上面的操做來看,你只是在本地初始化了一個版本庫,並完成了基礎的提交。接下來,你但願和遠程版本庫創建關聯,將提交推送到遠程。這種狀況下,其實你可能並不須要遠程的默認數據(一般是一個空的README文件)。因此,你能夠添加-f參數,將提交強制提交併覆蓋遠程版本庫。
git push -f origin master
複製代碼

小明如有所思地點點頭,這是小明第一次遇到Git問題。我想,接下來他應該會比較順利了。bash

沒想到,過了幾天,我又收到了小明的消息。這一次,他發來的是對Git的抱怨。服務器

「鋒哥,Git好討厭,提交日誌出現了錯誤,也不能修改。你知道搜狗輸入法有時候不夠智能,輸入太快不當心就輸錯了...😓」app

「🙂,你這孩子,別輕易下結論哈。其實,Git是容許修改提交記錄的。使用Git最舒服的一點就是:Git永遠都會給你反悔的機會。這一點,其它的版本控制工具是作不到的!」

「哦,原來是這樣啊!那快說說看,要怎麼作?」 小明已經一副火燒眉毛的表情了。

git commit命令中有一個參數叫--amend就是爲解決這個問題而生的。所以,若是是最近的提交,你只須要按照下面的命令操做便可。」

git commit --amend -m "這是新的提交日誌"
複製代碼

看完個人消息,小明給我發來一個微笑的表情。小明的抱怨讓我想起一句好氣又可笑的農村俗語 「屙屎不出怪茅坑」,哈哈。

本覺得一切能夠風平浪靜了。沒想到,過了一個月左右,忽然接到了小明的緊急電話。電話那頭,小明彷佛心情很急躁。

「鋒哥,我不當心進行了還原操做,我寫的代碼全丟了。幾千行的代碼啊,明天晚上就要發版本了,有辦法找回來嗎?」

聽到這個消息,我內心盤算,大約有50%的機率應該是找不回來了。這孩子比較粗心,可能根本就沒提交到版本庫。但若是他正好提交到了版本庫,興許還有救。所以,我安慰他說 「小明,別急!你打開TeamViewer,我遠程幫你看看」

連上機器後,我使用history命令看到小明在提交以後使用了git reset --hard xxx命令進行重置。--hardgit reset命令中惟一一個不安全的操做,它會真正地銷燬數據,以致於你在git log中徹底看不到操做日誌。但是,Git真的很聰明,它還保存了另一份日誌叫reflog,這個日誌記錄了你每次修改HEAD的操做。所以,你能夠經過下面的命令對數據進行還原:

git reflog

// 使用這個命令,你看到的日誌大概是這樣
c8278f9 (HEAD -> master) HEAD@{0}: reset: moving to c8278f9914a91e3aca6ab0993b48073ba1e41b2b
3e59423 HEAD@{1}: commit: a
c8278f9 (HEAD -> master) HEAD@{2}: commit (amend): v2 update
2dc167b HEAD@{3}: commit: v2
2e342e9 HEAD@{4}: commit (initial): Init commit
複製代碼

能夠看到,咱們在版本3e59423進行了git reset操做,最新版本是3e59423。所以,咱們能夠再次經過git reset命令回到這個版本:

git reset --hard 3e59423
複製代碼

以上操做完成後,你會驚喜地發現,丟失的數據竟然神奇般地回來了。

「謝謝鋒哥!!!🌺 🌺 🌺」

「下次別這樣操做了哈。另外,你怎麼一次性丟失這麼多代碼。必定要記得勤提交。」 小明出現這樣的問題,與平時的不規範操做也是分不開的。所以,最後我還不忘囑咐了他一句。

「好的,我知道了。對了,我一個還有比較疑惑的問題。git checkoutgit reset到底有啥區別?我之前用SVN的時候git checkout是用來檢出代碼的,在Git中能夠用它切換分支或者指定版本,但git reset一樣能夠作到。難道二者是徹底同樣的嗎?」 小明在QQ中給我發來了回覆消息。

「這是一個比較有深度的問題,解釋這個問題須要一點時間。接下來,你仔細聽」

理解Git工做空間

理解這個問題以前,先來簡單學習一些Git基礎知識。Git有三種狀態:

  • 已提交(commited):數據已徹底保存到本地數據庫中
  • 已修改(modified):修改了文件,但尚未保存到數據庫中
  • 已暫存(staged):對一個已修改的文件作了標記,將包含在下一次提交的版本快照中

這三種狀態對應Git三個工做區域:Git版本庫、暫存區和工做區

Git版本庫是Git用來保存項目的元數據和對象數據庫的地方,使用git clone命令時拷貝的就是這裏的數據。

工做目錄是對某個版本獨立檢出的內容,這些數據能夠供你使用和修改。

暫存區在Git內部對應一個名爲index的文件,它保存了下次將要提交的文件列表信息。所以,暫存區有時候也被叫做 「索引」。

一個基礎的Git工做流程以下: 1)在工做區修改文件 2)使用git add將文件添加到暫存區,也就是記錄到index文件中 3)使用git commit將暫存區中記錄的文件列表,使用快照永久地保存到Git版本庫中

理解HEAD

解釋這個問題,你還須要簡單理解HEAD是什麼。簡單來講,HEAD是當前分支引用的指針,它永遠指向該分支上最後一次提交。爲了讓你更容易理解HEAD,你能夠將HEAD看做上一次提交數據的快照。

若是你感興趣,你可使用一個底層命令來查看當前HEAD的快照信息:

git ls-tree -r HEAD

100644 blob aca4b576b7d4534266cb818ab1191d91887508b9	demo/src/main/java/com/youngfeng/snake/demo/Constant.java
100644 blob b8691ec87867b180e6ffc8dd5a7e85747698630d	demo/src/main/java/com/youngfeng/snake/demo/SnakeApplication.java
100644 blob 9a70557b761171ca196196a7c94a26ebbec89bb1	demo/src/main/java/com/youngfeng/snake/demo/activities/FirstActivity.java
100644 blob fab8d2f5cb65129df09185c5bd210d20484154ce	demo/src/main/java/com/youngfeng/snake/demo/activities/SecondActivity.java
100644 blob a7509233ecd8fe6c646f8585f756c74842ef0216	demo/src/main/java/com/youngfeng/snake/demo/activities/SplashActivity.java
複製代碼

這裏簡單解釋一下每一個字段的意思:100644表示文件模式,其對應一個普通文件。blob表示Git內部存儲對象數據類型,另外還有一種數據類型tree,對應一個樹對象,中間較長的字符串對應當前文件的SHA-1值,這部分不須要記住,簡單瞭解便可。

因此,簡單來講,HEAD對應一個樹形結構,存儲了當前分支全部的Git對象快照:

咱們用一個表格簡單來總結一下以上知識點:

HEAD Index(暫存區) 工做區
上一次提交的快照,下一次提交的父節點 預期的下一次提交快照 當前正在操做的沙盒目錄

理解git resetgit checkout區別主要是理解Git內部是怎麼操做以上三棵樹的。

接下來,咱們用一個簡單的例子來看一下使用git reset到底發生了什麼。先建立一個Git版本庫並觸發三次提交:

git init repo
touch file.txt
git add file.txt
git commit -m "v1"

echo v2 > file.txt
git add file.txt
git commit -m "v2"

echo v3 > file.txt
git add file.txt
git commit -m "v3"
複製代碼

以上操做完成後,版本庫如今看起來是這樣的:

接下來執行命令git reset 14ad152看看會發生什麼。如下是命令執行完成後看到的結果:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
 M file.txt

cat file.txt
### This is output ###
v3
複製代碼

能夠看到版本庫中文件版本回退到了V2,工做區文件內容同以前的版本V3一致;爲了確認暫存區發生了什麼變化,咱們再使用一個底層命令對比一下暫存區數據和版本庫數據是否一致:

# 查看暫存區信息
git ls-files -s
### This is output ###
100644 8c1384d825dbbe41309b7dc18ee7991a9085c46e 0	file.txt

# 查看版本庫快照信息
git ls-tree -r HEAD
### This is output ###
100644 blob 8c1384d825dbbe41309b7dc18ee7991a9085c46e	file.txt
複製代碼

能夠看到當前版本庫和暫存區信息是徹底一致的,HEAD指向了v2提交,用一個圖形來表示整個過程,應該是這樣:

看一眼上圖,理解一下剛剛發生的事情:首先,HEAD指針發生了移動,指向了V2,並撤銷了上一次提交。目前,版本庫和暫存區都保存的是第二次提交的記錄,工做區卻保存了最近一次修改。稍微聯想一下,你就會發現,此次的git reset命令剛好是最近一次提交的逆向操做。讓數據徹底回到了上一次提交前的狀態。因此,若是你想撤銷最近一次提交,能夠這麼作。

增長--soft參數測試

以上是咱們對git reset命令的第一次嘗試,在下一輪嘗試前,先執行git help reset看看reset命令的用法:

git reset [-q] [<tree-ish>] [--] <paths>...
git reset (--patch | -p) [<tree-ish>] [--] [<paths>...]
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]
複製代碼

看最後一句發現,reset命令後面還能夠接5個不一樣的參數: --soft--mixed--hard--merge--keep。這裏咱們主要關注前面三個,其中--mixed其實剛剛已經嘗試過,它和不帶參數的git reset命令是一樣的效果。換而言之,--mixedgit reset命令的默認行爲。接下來執行git reset --soft 14ad152看看會發生什麼。命令執行完成後,按照慣例,咱們一樣使用基礎命令看看發生了什麼變化:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
M  file.txt

cat file.txt
### This is output ###
v3
複製代碼

奇怪了?爲何會和上次不帶任何參數的執行結果徹底一致?難道Git出現了設計錯誤。相信你看到結果必定會有這樣的疑問,其實否則!由於,這裏我用文本粘貼了輸出結果,忽略了命令的字體顏色,其實這裏第二條命令輸出結果中的M顏色與上一次執行結果是不同的。爲了讓你看到不一樣,看下面的截圖:

這個顏色表示:file.txt文件已經被添加到了暫存區,使用 git commit命令就能夠完成提交。爲了嚴謹,咱們依然使用上面的底層命令看看版本庫和暫存區信息是否一致。注意:這裏的結果應該是不一致纔對,由於版本庫記錄的文件版本是v2,而暫存區記錄的文件版本實際上是v3。

git ls-tree -r HEAD
### This is output ###
100644 blob 8c1384d825dbbe41309b7dc18ee7991a9085c46e	file.txt

git ls-files -s
### This is output ###
100644 29ef827e8a45b1039d908884aae4490157bcb2b4 0	file.txt
複製代碼

能夠看到,兩個命令執行輸出的SHA-1並不一致,驗證了咱們的猜測。

這裏咱們能夠得出一個結論:--soft和默認行爲(--mixed)不同的地方是:--soft會將工做區的最新文件版本再作一步操做,添加到暫存區。使用這個命令能夠用來合併提交。即:若是你在某一次提交中有未完成的工做,而你反悔了,你可使用這個命令撤銷提交,等工做作完後繼續一次性完成提交。

增長--hard參數測試

接下來咱們對最後一個參數進行測試,這也是小明在使用過程出現問題的一個參數。執行命令git reset --hard 14ad152,看看發生了什麼:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
>>> No output <<<

cat file.txt
v2
複製代碼

注意看,此次使用git status -s徹底看不到輸出,這就證實:當前工做區,暫存區,版本庫數據是徹底一致的。查看文件內容,發現文件回到了v2版本。一般狀況下,若是你看到這種狀況,必定會嚇一跳,你最近一次提交的數據竟然徹底丟失了。的確,這是Git命令中少有的幾個真正銷燬數據的命令之一。除非你很是清楚地知道本身在作什麼,不然,請儘可能不要使用這個命令!

咱們依然用一張圖,完整地描述這個命令到底發什麼了什麼:

能夠看到,相對於默認行爲,--hard將工做區的數據也還原到了V2版本,以致於V3版本的提交已經徹底丟失。

git checkout

接下來看git checkout, 按照慣例,先執行git checkout 14ad152看看會發生什麼:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
>>> No output <<<

cat file.txt
v2
複製代碼

能夠看到,又出現了神奇的一幕,這一次git checkout命令的執行結果的確和git reset --hard徹底一致。這是否意味着二者就沒有任何區別了呢?固然也不是。嚴格來講,二者有兩個「本質」的區別:

  • 相對而言,git checkout對工做目錄是安全的,它不會將工做區已經修改的文件還原,git reset則無論三七二十一一股腦所有還原。
  • 另一個比較重要的區別是,git checkout並不移動HEAD分支的指向,它是經過直接修改HEAD引用來完成指針的指向。

第二個不一樣點相對比較難理解,咱們用一張圖來更直觀地展現兩者的區別:

簡單來講,git reset會經過移動指針來完成HEAD的指向,而git checkout則經過直接修改HEAD自己來完成指向的移動。

命令做用於部分文件

git resetgit checkout還能夠做用於一個文件,或者部分文件,即帶文件路徑執行。這種狀況下,兩個命令的表現不太同樣。咱們來試試看,先執行git reset 14ad15 -- file.txt命令嘗試將文件恢復到V2版本。命令執行完成,按照慣例用一些基礎命令來看看發生了什麼:

git log --abbrev-commit --pretty=oneline
### This is output ###
4521405 (HEAD -> master) v3
14ad152 v2
bcc49f4 v1

git status -v
### This is output ###
diff --git a/file.txt b/file.txt
index 29ef827..8c1384d 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-v3
+v2

cat file.txt
v3
複製代碼

能夠看到,版本庫和工做區的數據都沒有發生變化。惟一發生變化的是暫存區,暫存區記錄下一次提交的改動將致使數據從V3恢復到V2版本!

這裏咱們能夠這樣理解:執行這條命令後,Git先將暫存區和工做區的文件版本恢復到V2,再將工做區的文件版本恢復到V3。與--hard不同的地方是:這個命令並不會覆蓋工做區已經修改的文件,是安全操做。

執行帶路徑的git checkout命令和git reset命令有一些細微的差異,相對於git resetgit checkout帶路徑執行會覆蓋工做區已經修改的內容,致使數據丟失,是一個非安全操做。

針對上面的全部實驗,咱們用一個簡單的表格來總結他們的區別,以及操做是否安全:

不帶路徑執行

命令行 HEAD 暫存區 工做區 目錄安全
git reset [--mixed] YES YES NO YES
git reset --soft YES YES NO YES
git reset --hard YES YES YES NO
git checkout Modify YES YES YES

帶路徑執行

命令行 HEAD 暫存區 工做區 目錄安全
git reset -- NO YES NO YES
git checkout NO YES YES NO

「小明,你明白了嗎?」 消息發送過去以後,等了好久卻一直沒有響應。 「哎,這孩子!估計聽睡着了... 😆」

自從此次問到Git的問題後,已經兩年過去了,小明再沒有問到關於Git的問題。而就在昨天,忽然又收到了小明的消息。

「鋒哥,我如今已是Android Leader了。如今安卓團隊一共6我的,咱們如今在作一個社交類應用,在Git管理方面我仍是發現了一些問題。其中一個問題就是,如今版本庫有好多分支,其中開發主要在develop分支。主幹分支是master主要用於版本發佈。可還有一些分支卻顯得很是混亂,有什麼辦法改善這種狀況嗎?」

「關於Git的分支設計,目前有一個公認比較好的設計叫 Git Flow模型。關於Git Flow模型,你能夠查看這篇文章 nvie.com/posts/a-suc… 瞭解一下"

"好的!還有一個困擾了我好久的問題是,你們的提交日誌寫的比較籠統。在查找問題的時候很是不便,並且大部分同窗一次性提交好多文件,致使解決問題的時候不能準肯定位到具體是哪一次提交致使的。我告訴你們,一次提交改動要儘量小。但當別人問到具體的提交規則的時候我又不知道從何提及..."

「這是一個很好的問題 。中國程序員廣泛存在的一個問題是,巴不得把這輩子能提交的代碼一次性搞定。甚至有人用屢次提交太麻煩的藉口來搪塞問責人。簡單來講,能夠用一句話歸納提交原則:一個idea,一次提交。另外,你說的沒錯,提交必須儘量小,註釋必須儘量表述準確!」

給小明講了這麼多Git,我忍不住半開玩笑地問他,「小明,你如今還以爲Git簡單嗎?」

小明發了一個無奈的表情!說道,「之前是我才疏學淺,略知皮毛,不知道Git原來還有這麼多玩法,忍不住爲Git的發明者點讚了。對了,鋒哥,Git究竟是誰開發的?」

」關於Git的故事,互聯網上其實已經爛大街了。我簡單給你介紹一下吧!Git的誕生實際上是一個偶然,其初始使命是爲Linux內核代碼管理服務的。早年的時候Linux內核源碼是用Bitkeeper版本控制工具管理的。但是,後來由於某些利益關係,Bitkeeper要求Linux社區付費使用。這一舉動激怒了Linus,也就是Linux的創始人,他決定本身開發一個分佈式版本控制系統。幾周時間下來,Git的雛形就誕生了,而且開始在Linux社區中應用開來。雖然Linus是Git的創始人,但是背後的最大功臣倒是一個日本人 Junio C Hamano。Linus在Git開源版本庫的提交只有258次,而Junio C Hamano卻提交了4000屢次。也就是說,在Linus開發後不久項目的管理權就交給了這個日本人。關於 Junio C Hamano,你感興趣的話能夠Google瞭解一下。他如今在Google工做,如同Linus同樣很是低調。「

「這個故事也告訴我:不要用技術去挑戰一個程序員 @_@ 」

這個故事講完,小明與Git的故事就已經告一段落了。其實,還有一些比較常見的問題,小明並無問到過。這裏,我爲你準備了一個附錄,給你介紹一些經常使用的小命令幫你解決平常小問題。它頗有用,必定要拿筆記下來,或者收藏這篇文章備用。

常見問題

問題一:公司的Git服務器是搭建在一個內網服務器上面的,我想把代碼同時提交到OsChina上面,以便在家拉取代碼,遠程辦公,怎麼辦? Git自己是一個分佈式的版本管理系統,實現這個需求很是簡單,使用git remote add命令添加多個遠程版本庫關聯便可。

git remote add company git@xxx
git remote add home git@xxx
複製代碼

問題二:在拉取遠程代碼的時候,若是本地有代碼尚未提交,Git就會提示先提交代碼到版本庫。可暫時我又不想提交,怎麼辦? 針對這個問題,Git提供了一個臨時區域用於保存不想提交的記錄,對應的命令是git stash。一般狀況下,你能夠這樣操做:

# 將暫時還不想提交的數據保存到臨時區域,保存成功後,工做區將和版本庫徹底一致
git stash
# 還原stash數據到工做區
git stash apply
# 以上操做完成後,stash數據依然保存在臨時區域中,爲了刪除這部分數據,使用以下命令便可。
git stash drop
# 若是你想在還原數據的同時從臨時區域刪除數據,能夠這樣操做:
git statsh pop
# 以上兩個命令若是不接任何參數將刪除掉全部的臨時區域數據,若是你只想刪除其中一條記錄,指定對應索引數據便可。
git stash pop/drop stash@{index}
# 查看臨時區域全部數據,使用以下命令:
git stash list
複製代碼

問題三:做爲項目負責人,我但願迅速找出問題代碼的「元兇」,有什麼辦法嗎? 針對這個問題,最好的答案是git blame,使用這個命令並指定具體文件它將顯示文件每一行代碼的最近修改記錄,你能夠清晰地看待最近代碼的修改人。

總結

Git是一個很是優秀的版本控制系統,我極力推薦你在平常開發中使用。這篇文章從小明的角度解釋了幾個常見問題的解決方案,毫無懸念地,你可能還會遇到其它的一些問題。遇到問題,你能夠嘗試Google搜索解決方案。也能夠在文章下方給我留言,我很是樂意爲你解答Git問題。


我是歐陽鋒,我願爲你鞍前馬後,助你平步青雲。若是你喜歡個人文章,請在下方留下你愛的印記。若是你不喜歡個人文章,請先喜歡上個人文章,而後再留下愛的印記。

下次文章再見!拜拜!


學更多編程知識,掃描下方二維碼關注歐陽鋒工做室

歐陽鋒工做室
相關文章
相關標籤/搜索