Git常用操作指南
目錄
- 前言
- Git簡介
- 安裝之後第一步
- 建立版本庫
- 本地倉庫
- 遠端倉庫
- 版本控制
- 工作區和暫存區
- 版本回退
- 撤銷修改
- 刪除檔案
- 分支管理
- 建立與合併分支
- 解決衝突
- 分支管理策略
- 狀態儲存
- 多人協作
- Rebase
- 標籤管理
- 建立標籤
- 操作標籤
- 自定義Git
- 忽略特殊檔案
- 配置別名
- 配置檔案
- 總結
前言
因為工作需求,最近又重新溫習了一下Git操作,遂總結了一篇Git常用操作指南,方便日後學習查閱,本部落格精簡提煉了在開發過程中Git經常用到的核心命令,主要參考了《廖雪峰老師的Git教程》,希望對大家學習使用Git能帶來幫助。
Git簡介
Git是Linux之父Linus的第二個偉大的作品,它最早是在Linux上開發的,被用來管理Linux核心的原始碼。後來慢慢地有人將其移植到了Unix、Windows、Max OS等作業系統中。
Git是一個分散式的版本控制系統,與集中式的版本控制系統不同的是,每個人都工作在通過克隆建立的本地版本庫中。也就是說每個人都擁有一個完整的版本庫,檢視提交日誌、提交、建立里程碑和分支、合併分支、回退等所有操作都直接在本地完成而不需要網路連線。
對於Git倉庫來說,每個人都有一個獨立完整的倉庫,所謂的遠端倉庫或是伺服器倉庫其實也是一個倉庫,只不過這臺主機24小時執行,它是一個穩定的倉庫,供他人克隆、推送,也從伺服器倉庫中拉取別人的提交。
Git是目前世界上最先進的分散式版本控制系統。
安裝之後第一步
安裝完成後,還需要最後一步設定,在命令列輸入:
$ git config --global user.name "Your Name" $ git config --global user.email "[email protected]"
因為Git是分散式版本控制系統,所以,每個機器都必須配置使用者資訊:你的名字和Email地址。
注意git config
命令的--global
引數,用了這個引數,表示你這臺機器上所有的Git倉庫都會使用這個配置,當然也可以對某個倉庫指定不同的使用者名稱和Email地址。
建立版本庫
本地倉庫
版本庫又名倉庫,英文名repository,你可以簡單理解成一個目錄,這個目錄裡面的所有檔案都可以被Git管理起來,每個檔案的修改、刪除,Git都能跟蹤,以便任何時刻都可以追蹤歷史,或者在將來某個時刻可以“還原”。
所以,建立一個版本庫非常簡單,首先,選擇一個合適的地方,建立一個空目錄:
$ mkdir learngit
$ cd learngit
$ pwd
Path
----
D:\Blog\tmp\learngit
第二步,通過git init
命令把這個目錄變成Git可以管理的倉庫:
$ git init
Initialized empty Git repository in D:/Blog/tmp/learngit/.git/
遠端倉庫
建立SSH Key
Git支援多種協議,包括https
,但通過ssh
支援的原生git
協議速度最快。由於本地Git倉庫和GitHub倉庫之間的傳輸是通過SSH加密的,所以,需要在關聯遠端倉庫前需要配置SSH Key
至Github設定中,這樣遠端倉庫才允許本機對遠端倉庫的拉去/推送操作。
開啟Shell
,進入到"~/.ssh
"目錄下,執行"ls
"命令看看這個目錄下有沒有id_rsa
和id_rsa.pub
這兩個檔案,如果已經有了,可直接跳到下一步。
如果沒有,則執行:
$ ssh-keygen -t rsa -C "[email protected]"
一路回車即可。執行命令後,我們再進入到"~/.ssh
"目錄下,執行"ls
"命令,可以看到裡面有id_rsa
和id_rsa.pub
兩個檔案,這兩個就是SSH Key的祕鑰對,id_rsa
是私鑰,不能洩露出去,id_rsa.pub
是公鑰,可以放心地告訴任何人。
開啟“Account settings”,“SSH Keys”頁面,然後,點“New SSH Key”,填上任意Title,在Key文字框裡貼上id_rsa.pub
檔案的內容(Win 10 下可使用"type ~/.ssh/id_rsa.pub
"命令檢視公鑰檔案內容):
點選“Add SSH Key”之後,就可以看到你的公鑰已經加入到了你的Github倉庫配置中。
新增遠端庫
首先,登陸GitHub,然後,在右上角找到“Create a new repo”按鈕,建立一個新的倉庫:
在Repository name填入learngit
,其他保持預設設定,點選“Create repository”按鈕,就成功地建立了一個新的Git倉庫:
這樣就成功建立了一個空白的遠端倉庫,那麼如何將這個遠端倉庫與本地倉庫進行關聯呢?
我們根據Git所給出的提示可知,可以在本地建立一個新倉庫對遠端倉庫進行關聯,也可以對本地已有倉庫進行關聯。
關聯新倉庫
echo "# learngit" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin [email protected]:guoyaohua/learngit.git
git push -u origin master
關聯已有倉庫
git remote add origin [email protected]:guoyaohua/learngit.git
git push -u origin master
我們可以使用上文在本地初始化的“learngit”倉庫。(注意:本地倉庫和遠端倉庫可以不同名,本文只是為了寫教程設定為相同名稱。)
我們再重新整理下Github Code
介面,發現新加入的README.md
檔案已經推送到了遠端倉庫中。
版本控制
工作區和暫存區
工作區(Working Directory)
就是你在電腦裡能看到的目錄,比如我們剛剛建立的learngit
資料夾就是一個工作區:
版本庫(Repository)
工作區有一個隱藏目錄.git
,這個不算工作區,而是Git的版本庫。
Git的版本庫裡存了很多東西,其中最重要的就是稱為Stage(或者叫Index)的暫存區,還有Git為我們自動建立的第一個分支master
,以及指向master
的一個指標叫HEAD
。
分支和HEAD
的概念本文後面再詳細說明。
我們把檔案往Git版本庫裡新增的時候,是分兩步執行的:
第一步是用git add
把檔案新增進去,實際上就是把檔案修改新增到暫存區;
第二步是用git commit
提交更改,實際上就是把暫存區的所有內容提交到當前分支。
因為我們建立Git版本庫時,Git自動為我們建立了唯一一個master
分支,所以現在,git commit
就是往master
分支上提交更改。
你可以簡單理解為,需要提交的檔案修改通通放到暫存區,然後,一次性提交暫存區的所有修改。
使用git status
命令可以檢視當前倉庫的狀態。
版本回退
Git版本控制可以理解為,我們再編寫程式碼的過程中,會對code進行多次修改,每當你覺得檔案修改到一定程度的時候,就可以“儲存一個快照”,這個快照在Git中被稱為commit
。一旦你把檔案改亂了,或者誤刪了檔案,還可以從最近的一個commit
恢復,然後繼續工作,而不是把幾個月的工作成果全部丟失。
在實際工作中,我們用git log
命令檢視我們提交的歷史記錄:
$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master)
Author: Yaohua Guo <[email protected]>
Date: Fri May 18 21:06:15 2018 +0800
append GPL
commit e475afc93c209a690c39c13a46716e8fa000c366
Author: Yaohua Guo <[email protected]>
Date: Fri May 18 21:03:36 2018 +0800
add distributed
commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Yaohua Guo <[email protected]>
Date: Fri May 18 20:59:18 2018 +0800
wrote a readme file
Git中,commit id是一個使用SHA1計算出來的一個非常大的數字,用十六進位制表示,commit後面的那一串十六進位制數字就是每一次提交的版本號,我們可以通過git log
命令看到每次提交的版本號、使用者名稱、日期以及版本描述等資訊。
我們可以使用git reset
命令進行版本回退操作。
$ git reset --hard HEAD^
在Git中,用HEAD表示當前版本,上一個版本就是HEAD^ ,上上一個版本就是HEAD^^ ,以此類推,如果需要回退幾十個版本,寫幾十個^容易數不過來,所以可以寫,例如回退30個版本為:HEAD~30。
如果回退完版本又後悔了,想恢復,也是可以的,使用如下即可:
$ git reset --hard commit_id
不過當我們執行git reset
進行版本回退之後,之前最新的版本號無法通過git log
查詢到,此時需要使用git reflog
命令查詢Git的操作記錄,我們可以從該記錄中找到之前的commit id資訊。
$ git reflog
e475afc HEAD@{1}: reset: moving to HEAD^
1094adb (HEAD -> master) HEAD@{2}: commit: append GPL
e475afc HEAD@{3}: commit: add distributed
eaadf4e HEAD@{4}: commit (initial): wrote a readme file
在Git中,版本回退速度非常快,因為Git在內部有個指向當前版本的HEAD指標,當你回退版本的時候,Git僅僅是把HEAD從指向回退的版本,然後順便重新整理工作區檔案。
重置命令
重置命令的作用是將當前的分支重設(reset)到指定的<commit>
或者HEAD
(預設是HEAD,即最新的一次提交),並且根據[mode]有可能更新Index和Working directory(預設是mixed)。
$ git reset [--hard|soft|mixed|merge|keep] [commit|HEAD]
- –hard:重設“暫存區”和“工作區”,從
<commit>
以來在工作區中的任何改變都被丟棄,並把HEAD指向<commit>
。(徹底回退到某個版本,本地的原始碼也會變為上一個版本的內容。) - –soft:“工作區”中的內容不作任何改變,HEAD指向
<commit>
,自從<commit>
以來的所有改變都會回退到“暫存區”中,顯示在git status
的“Changes to be committed”中。(回退到某個版本,只回退了commit的資訊。如果還要提交,直接commit即可。) - –mixed:僅重設“暫存區”,並把HEAD指向
<commit>
,但是不重設“工作區”,本地檔案修改不受影響。這個模式是預設模式,即當不顯示告知git reset
模式時,會使用mixed模式。這個模式的效果是,工作區中檔案的修改都會被保留,不會丟棄,但是也不會被標記成“Changes to be committed”,但是會提示檔案未被更新。(回退到某個版本,只保留原始碼,回退commit和index資訊)
檔案粒度操作
需要注意的是在mixed
模式下進行reset
操作時可以是全域性性重置,也可以是檔案粒度重置,區別在於二者作用域不同,檔案粒度只會使對應檔案的暫存區狀態變為指定commit時該檔案的暫存區狀態,並且不會改變版本庫狀態,即HEAD指標不會改變,我們看一下效果。
首先我們新建兩個檔案進行兩次提交,可以看到目前HEAD指向最新一次提交“text2”。
我們對“file1.txt”進行reset操作,令其重置為“text1”狀態。
並且我們通過git log命令可發現,此時HEAD指標並沒有改變,還是指向最新一次提交“Text 2”,可知檔案粒度的reset --mixed
不改變版本庫HEAD指標狀態。
對於soft和hard模式則無法進行檔案粒度操作。
Reset 常用示例
回退add操作
$ git add test $ git reset HEAD test # HEAD指的是當前指向的版本號,可以將HEAD還成任意想回退的版本號
可以將test從“已暫存”狀態(Index區)回滾到指定Commit時暫存區的狀態。
回退最後一次提交
$ git add test $ git commit -m "Add test" $ git reset --soft HEAD^
可以將test從“已提交”狀態變為“已暫存”狀態。
回退最近幾次提交,並把這幾次提交放到新分支上
$ git branch topic # 已當前分支為基礎,新建分支topic $ git reset --hard HEAD~2 # 在當前分支上回滾提交 $ git checkout topic
通過臨時分支來保留提交,然後在當前分支上做硬回滾。
將本地的狀態回退到和遠端一樣
$ git reset --hard origin/devlop
回退到某個版本提交
$ git reset 497e350
當前HEAD會指向“497e350”,暫存區中的狀態會恢復到提交“497e350”時暫存區的狀態。
撤銷修改
當我們因為一些原因想要丟棄工作區某些檔案修改時,可以使用“git checkout -- <file>
”命令,該命令僅會恢復工作區檔案狀態,不會對版本庫有任何改動。
命令git checkout -- file1.txt
意思就是,把file1.txt
檔案在工作區的修改全部撤銷,這裡有兩種情況:
- 一種是
file1.txt
自修改後還沒有被放到暫存區,現在,撤銷修改就回到和版本庫一模一樣的狀態; - 一種是
file1.txt
已經新增到暫存區後,又作了修改,現在,撤銷修改就回到新增到暫存區後的狀態。
總之,就是讓這個檔案回到最近一次git commit
或git add
時的狀態。
刪除檔案
在Git中,刪除也是一個修改操作,我們實戰一下,先新增一個新檔案test.txt
到Git並且提交:
一般情況下,你通常直接在檔案管理器中把沒用的檔案刪了,或者用rm
命令刪了:
$ rm test.txt
這個時候,Git知道你刪除了檔案,因此,工作區和版本庫就不一致了,git status
命令會立刻告訴你哪些檔案被刪除了:
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: test.txt
no changes added to commit (use "git add" and/or "git commit -a")
現在你有兩個選擇,一是確實要從版本庫中刪除該檔案,那就用命令git rm
刪掉,並且git commit
:
$ git rm test.txt
rm 'test.txt'
$ git commit -m "remove test.txt"
[master d46f35e] remove test.txt
1 file changed, 1 deletion(-)
delete mode 100644 test.txt
現在,檔案就從版本庫中被刪除了。
提示:先手動刪除檔案,然後使用
git rm <file>
和git add <file>
效果是一樣的。
另一種情況是刪錯了,因為版本庫裡還有呢,所以可以很輕鬆地把誤刪的檔案恢復到最新版本:
$ git checkout -- test.txt
git checkout
其實是用版本庫裡的版本替換工作區的版本,無論工作區是修改還是刪除,都可以“一鍵還原”。
注意:從來沒有被新增到版本庫就被刪除的檔案,是無法恢復的!
分支管理
建立與合併分支
在上文“版本回退”裡,我們已經知道,每次提交,Git都把它們串成一條時間線,這條時間線就是一個分支。截止到目前,只有一條時間線,在Git裡,這個分支叫主分支,即master
分支。HEAD
嚴格來說不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是當前分支。
一開始的時候,master
分支是一條線,Git用master
指向最新的提交,再用HEAD
指向master
,就能確定當前分支,以及當前分支的提交點:
每次提交,master
分支都會向前移動一步,這樣,隨著你不斷提交,master
分支的線也越來越長。
當我們建立新的分支,例如dev
時,Git新建了一個指標叫dev
,指向master
相同的提交,再把HEAD
指向dev
,就表示當前分支在dev
上:
Git建立一個分支很快,因為除了增加一個dev
指標,改改HEAD
的指向,工作區的檔案都沒有任何變化。
不過,從現在開始,對工作區的修改和提交就是針對dev
分支了,比如新提交一次後,dev
指標往前移動一步,而master
指標不變:
假如我們在dev
上的工作完成了,就可以把dev
合併到master
上。Git怎麼合併呢?最簡單的方法,就是直接把master
指向dev
的當前提交,就完成了合併:
所以Git合併分支也很快!就改改指標,工作區內容也不變!
合併完分支後,甚至可以刪除dev
分支。刪除dev
分支就是把dev
指標給刪掉,刪掉後,我們就剩下了一條master
分支:
下面開始實戰。
首先,我們建立dev
分支,然後切換到dev
分支:
$ git checkout -b dev
Switched to a new branch 'dev'
git checkout
命令加上-b
引數表示建立並切換,相當於以下兩條命令:
$ git branch dev # 建立dev分支
$ git checkout dev # 切換到dev分支
Switched to branch 'dev'
然後,用git branch
命令檢視當前分支:
$ git branch
* dev
master
git branch
命令會列出所有分支,當前分支前面會標一個*
號。
然後,我們就可以在dev
分支上正常提交,比如對readme.txt
做個修改,加上一行:
Creating a new branch is quick.
然後提交:
$ git add readme.txt
$ git commit -m "branch test"
[dev b17d20e] branch test
1 file changed, 1 insertion(+)
現在,dev
分支的工作完成,我們就可以切換回master
分支:
$ git checkout master
Switched to branch 'master'
切換回master
分支後,再檢視一個readme.txt
檔案,剛才新增的內容不見了!因為那個提交是在dev
分支上,而master
分支此刻的提交點並沒有變:
現在,我們把dev
分支的工作成果合併到master
分支上:
$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)
git merge
命令用於合併指定分支到當前分支。合併後,再檢視readme.txt
的內容,就可以看到,和dev
分支的最新提交是完全一樣的。
注意到上面的Fast-forward
資訊,Git告訴我們,這次合併是“快進模式”,也就是直接把master
指向dev
的當前提交,所以合併速度非常快。
當然,也不是每次合併都能Fast-forward
,我們後面會講其他方式的合併。
合併完成後,就可以放心地刪除dev
分支了:
$ git branch -d dev
Deleted branch dev (was b17d20e).
刪除後,檢視branch
,就只剩下master
分支了:
$ git branch
* master
因為建立、合併和刪除分支非常快,所以Git鼓勵你使用分支完成某個任務,合併後再刪掉分支,這和直接在master
分支上工作效果是一樣的,但過程更安全。
解決衝突
在真正開發過程中,合併分支經常會遇到分支衝突的情況,無法直接合並,我們來模擬一下這個場景。
準備新的feature1
分支,繼續我們的新分支開發:
$ git checkout -b feature1
Switched to a new branch 'feature1'
修改readme.txt
最後一行,改為:
Creating a new branch is quick AND simple.
在feature1
分支上提交:
$ git add readme.txt
$ git commit -m "AND simple"
[feature1 14096d0] AND simple
1 file changed, 1 insertion(+), 1 deletion(-)
切換到master
分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Git還會自動提示我們當前master
分支比遠端的master
分支要超前1個提交。
在master
分支上把readme.txt
檔案的最後一行改為:
Creating a new branch is quick & simple.
提交:
$ git add readme.txt
$ git commit -m "& simple"
[master 5dc6824] & simple
1 file changed, 1 insertion(+), 1 deletion(-)
現在,master
分支和feature1
分支各自都分別有新的提交,變成了這樣:
這種情況下,Git無法執行“快速合併(Fast-forward)”,只能試圖把各自的修改合併起來,但這種合併就可能會有衝突,我們試試看:
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
Git告訴我們,readme.txt
檔案存在衝突,必須手動解決衝突後再提交。git status
也可以告訴我們衝突的檔案:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
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: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")
我們可以直接檢視readme.txt的內容:
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
Git用<<<<<<<
,=======
,>>>>>>>
標記出不同分支的內容,我們修改如下後儲存:
Creating a new branch is quick and simple.
再提交:
$ git add readme.txt
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed
現在,master
分支和feature1
分支變成了下圖所示:
用帶引數的git log
也可以看到分支的合併情況:
$ git log --graph --pretty=oneline --abbrev-commit
* cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file
最後,刪除feature1
分支:
$ git branch -d feature1
Deleted branch feature1 (was 14096d0).
工作完成。
分支管理策略
通常,合併分支時,如果可能,Git會用Fast forward
模式,但這種模式下,刪除分支後,會丟掉分支資訊。
如果要強制禁用Fast forward
模式,Git就會在merge時生成一個新的commit,這樣,從分支歷史上就可以看出分支資訊。
下面我們實戰一下--no-ff
方式的git merge
:
首先,仍然建立並切換dev
分支:
$ git checkout -b dev
Switched to a new branch 'dev'
修改readme.txt檔案,並提交一個新的commit:
$ git add readme.txt
$ git commit -m "add merge"
[dev f52c633] add merge
1 file changed, 1 insertion(+)
現在,我們切換回master
:
$ git checkout master
Switched to branch 'master'
準備合併dev
分支,請注意--no-ff
引數,表示禁用Fast forward
:
$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
readme.txt | 1 +
1 file changed, 1 insertion(+)
因為本次合併要建立一個新的commit,所以加上-m
引數,把commit描述寫進去。
合併後,我們用git log
看看分支歷史:
$ git log --graph --pretty=oneline --abbrev-commit
* e1e9c68 (HEAD -> master) merge with no-ff
|\
| * f52c633 (dev) add merge
|/
* cf810e4 conflict fixed
...
可以看到,不使用Fast forward
模式,merge後就像這樣:
分支策略
在實際開發中,我們應該按照幾個基本原則進行分支管理:
首先,master
分支應該是非常穩定的,也就是僅用來發布新版本,平時不能在上面幹活;
那在哪幹活呢?幹活都在dev
分支上,也就是說,dev
分支是不穩定的,到某個時候,比如1.0版本釋出時,再把dev
分支合併到master
上,在master
分支釋出1.0版本;
你和團隊同事每個人都在dev
分支上幹活,每個人都有自己的分支,時不時地往dev
分支上合併就可以了。
所以,團隊合作的分支看起來就像這樣:
狀態儲存
當我們在開發過程中,經常遇到這樣的情況,我們需要暫時放下手中的工作,切換到其他分支進行開發,例如當我們在dev分支進行程式2.0版本開發時,發現1.0版本的程式出現了bug,必須立刻進行修復,但是在目前的dev分支我們可能已經做了很多修改,暫存區可能有了暫存狀態,甚至可能在開發過程中在dev分支進行了多次commit,這時如果我們想切換回master分支,進行bug修復,這時就需要使用到git stash
命令儲存原分支當前的狀態。
在講解git stash
之前,我們先考慮兩種場景:
第一種就是我們未在dev分支進行任何提交,此時HEAD指標指向dev,dev和master指向同一次commit,如下圖:
我們可能在dev的工作區做了很多修改,也將部分修改狀態加入了暫存區(即進行了git add
操作),這時我們嘗試一下直接使用git checkout
命令切換分支。
此時,Git狀態如下:
我們修改“file1.txt”和“file2.txt”的內容,並將“file1.txt”的改動加入暫存區。
此時可看出工作區和暫存區就都有改變,但HEAD指標指向的dev與master為同一個commit節點。
這時我們執行git checkout master
命令嘗試切換分支。
可以看出,成功切換到了master分支上,而且工作區和暫存區的狀態依舊保留。
我們再考慮一個場景,在dev分支開發時,進行了一次提交,此時HEAD指向dev分支,dev分支超前master分支一次commit,具體見下圖:
如果此時我們工作區或暫存區有未提交更改時,就無法進行分支切換操作(如果沒有未提交修改的話當然可以進行分支切換操作)。
我想這時大家就會有一個疑問,為什麼兩種狀態下我們都修改了暫存區和工作區的狀態,但是一個可以切換分支並且保留工作區、暫存區狀態,而另一種狀態就無法切換分支呢?
我起初在遇到這個問題的時候也是很詫異,在網上搜索了好多資料,依舊沒有查到有價值的資訊。
這時我們就應該從Git的原理來進行分析了,Git在進行版本控制時,記錄的並不是檔案本身的資訊,而是檔案的修改狀態,例如我們再一個10000行程式碼的檔案中,新加入了一行程式碼進行,Git並不是將最新的10001行程式碼作為備份,而是僅僅記錄了新舊檔案之間的差異,即在哪個位置修改了什麼內容(修改包括:增加、刪除、修改等)。
我們來分析一下上問題到的第一種場景:我們未在dev分支進行任何提交,此時HEAD指標指向dev,dev和master指向同一次commit。
雖然我們再dev分支的工作區和暫存區做了修改,這些修改都是基於dev指向的commit而言的,而且此時dev和master指向同一個commit,所以,該場景下,dev分支工作區和暫存區的修改依舊適用於master分支,所以可以成功切換分支。
而第二種場景:在dev分支開發時,進行了一次提交,此時HEAD指向dev分支,dev分支超前master分支一次commit。
這時,dev工作區和暫存區的狀態是基於最新的dev指向的commit而言的,已經不能應用於master指向的commit了,所以在進行切換分支時,提示報錯。
應用例項
軟體開發中,bug就像家常便飯一樣。有了bug就需要修復,在Git中,由於分支是如此的強大,所以,每個bug都可以通過一個新的臨時分支來修復,修復後,合併分支,然後將臨時分支刪除。
當你接到一個修復一個代號101的bug的任務時,很自然地,你想建立一個分支issue-101
來修復它,但是,當前正在dev
上進行的工作還沒有提交:
$ git status
On branch dev
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: hello.py
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: readme.txt
並不是你不想提交,而是工作只進行到一半,還沒法提交,預計完成還需1天時間。但是,必須在兩個小時內修復該bug,怎麼辦?
幸好,Git還提供了一個stash
功能,可以把當前工作現場“儲藏”起來,等以後恢復現場後繼續工作:
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge
現在,用git status
檢視工作區,就是乾淨的(除非有沒有被Git管理的檔案),因此可以放心地建立分支來修復bug。
首先確定要在哪個分支上修復bug,假定需要在master
分支上修復,就從master
建立臨時分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
$ git checkout -b issue-101
Switched to a new branch 'issue-101'
現在修復bug,需要把“Git is free software ...”改為“Git is a free software ...”,然後提交:
$ git add readme.txt
$ git commit -m "fix bug 101"
[issue-101 4c805e2] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
修復完成後,切換到master
分支,並完成合並,最後刪除issue-101
分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
readme.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
修復好BUG之後,就可以返回原分支繼續之前的工作了。
$ git checkout dev
Switched to branch 'dev'
$ git status
On branch dev
nothing to commit, working tree clean
工作區是乾淨的,剛才的工作現場存到哪去了?用git stash list
命令看看:
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge
工作現場還在,Git把stash內容存在某個地方了,但是需要恢復一下,有兩個辦法:
一是用git stash apply
恢復,但是恢復後,stash內容並不刪除,你需要用git stash drop
來刪除;
另一種方式是用git stash pop
,恢復的同時把stash內容也刪了:
$ git stash pop
On branch dev
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: hello.py
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: readme.txt
Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)
再用git stash list
檢視,就看不到任何stash內容了:
$ git stash list
你可以多次stash,恢復的時候,先用git stash list
檢視,然後恢復指定的stash,用命令:
$ git stash apply stash@{0}
多人協作
當你從遠端倉庫克隆時,實際上Git自動把本地的master
分支和遠端的master
分支對應起來了,並且,遠端倉庫的預設名稱是origin
。
用git remote -v
檢視遠端庫的詳細資訊:
$ git remote -v
origin [email protected]:guoyaohua/learngit.git (fetch)
origin [email protected]:guoyaohua/learngit.git (push)
上面顯示了可以抓取和推送的origin
的地址。如果沒有推送許可權,就看不到push的地址。
推送分支
推送分支,就是把該分支上的所有本地提交推送到遠端庫。推送時,要指定本地分支,這樣,Git就會把該分支推送到遠端庫對應的遠端分支上:
$ git push origin master
如果要推送其他分支,比如dev
,就改成:
$ git push origin dev
但是,並不是一定要把本地分支往遠端推送,那麼,哪些分支需要推送,哪些不需要呢?
master
分支是主分支,因此要時刻與遠端同步;dev
分支是開發分支,團隊所有成員都需要在上面工作,所以也需要與遠端同步;- bug分支只用於在本地修復bug,就沒必要推到遠端了,除非老闆要看看你每週到底修復了幾個bug;
- feature分支是否推到遠端,取決於你是否和你的小夥伴合作在上面開發。
總之,就是在Git中,分支完全可以在本地自己藏著玩,是否推送,視你的心情而定!
抓取分支
多人協作時,大家都會往master
和dev
分支上推送各自的修改。
現在,模擬一個你的同事,可以在另一臺電腦(注意要把SSH Key新增到GitHub)或者同一臺電腦的另一個目錄下克隆:
$ git clone [email protected]:guoyaohua/learngit.git
Cloning into 'learngit'...
remote: Counting objects: 40, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 40 (delta 14), reused 40 (delta 14), pack-reused 0
Receiving objects: 100% (40/40), done.
Resolving deltas: 100% (14/14), done.
當你的同事從遠端庫clone時,預設情況下,你的同事只能看到本地的master
分支。不信可以用git branch
命令看看:
$ git branch
* master
現在,你的同事要在dev
分支上開發,就必須建立遠端origin
的dev
分支到本地,於是他用這個命令建立本地dev
分支:
$ git checkout -b dev origin/dev
現在,他就可以在dev
上繼續修改,然後,時不時地把dev
分支push
到遠端:
$ git add env.txt
$ git commit -m "add env"
[dev 7a5e5dd] add env
1 file changed, 1 insertion(+)
create mode 100644 env.txt
$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 308 bytes | 308.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
f52c633..7a5e5dd dev -> dev
你的同事已經向origin/dev
分支推送了他的提交,而碰巧你也對同樣的檔案作了修改,並試圖推送:
$ type env.txt
env
$ git add env.txt
$ git commit -m "add new env"
[dev 7bd91f1] add new env
1 file changed, 1 insertion(+)
create mode 100644 env.txt
$ git push origin dev
To github.com:michaelliao/learngit.git
! [rejected] dev -> dev (non-fast-forward)
error: failed to push some refs to '[email protected]:guoyaohua/learngit.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
推送失敗,因為你的同事的最新提交和你試圖推送的提交有衝突,解決辦法也很簡單,Git已經提示我們,先用git pull
把最新的提交從origin/dev
抓下來,然後,在本地合併,解決衝突,再推送:
$ git pull
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.
git pull <remote> <branch>
If you wish to set tracking information for this branch you can do so with:
git branch --set-upstream-to=origin/<branch> dev
git pull
也失敗了,原因是沒有指定本地dev
分支與遠端origin/dev
分支的連結,根據提示,設定dev
和origin/dev
的連結:
$ git branch --set-upstream-to=origin/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
再pull:
$ git pull
Auto-merging env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.
這回git pull
成功,但是合併有衝突,需要手動解決,解決的方法和分支管理中的解決衝突完全一樣。解決後,提交,再push:
$ git commit -m "fix env conflict"
[dev 57c53ab] fix env conflict
$ git push origin dev
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 621 bytes | 621.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To [email protected]:guoyaohua/learngit.git
7a5e5dd..57c53ab dev -> dev
因此,多人協作的工作模式通常是這樣:
- 首先,可以試圖用
git push origin <branch-name>
推送自己的修改; - 如果推送失敗,則因為遠端分支比你的本地更新,需要先用
git pull
試圖合併; - 如果合併有衝突,則解決衝突,並在本地提交;
- 沒有衝突或者解決掉衝突後,再用
git push origin <branch-name>
推送就能成功!
如果git pull
提示no tracking information
,則說明本地分支和遠端分支的連結關係沒有建立,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>
。
這就是多人協作的工作模式,一旦熟悉了,就非常簡單。
Rebase
git rebase
和git merge
做的事其實是一樣的。它們都被設計來將一個分支的更改併入另一個分支,只不過方式有些不同。
git rebase
用於把一個分支的修改合併到當前分支。
假設你現在基於遠端分支"origin",建立一個叫"mywork"的分支。
$ git checkout -b mywork origin
假設遠端分支"origin"已經有了2個提交,如圖:
現在我們在這個分支做一些修改,然後生成兩個提交(commit)。
但是與此同時,有些人也在"origin"分支上做了一些修改並且做了提交了. 這就意味著"origin"和"mywork"這兩個分支各自"前進"了,它們之間"分叉"了。
在這裡,你可以用“pull"命令把“origin”分支上的修改拉下來並且和你的修改合併; 結果看起來就像一個新的"合併的提交"(merge commit):
但是,如果你想讓“mywork”分支歷史看起來像沒有經過任何合併一樣,你也許可以用 git rebase
:
$ git checkout mywork
$ git rebase origin
這些命令會把你的"mywork"分支裡的每個提交(commit)取消掉,並且把它們臨時儲存為補丁(patch)(這些補丁放到".git/rebase"目錄中),然後把"mywork"分支更新為最新的"origin"分支,最後把儲存的這些補丁應用到"mywork"分支上。
當"mywork"分支更新之後,它會指向這些新建立的提交(commit),而那些老的提交會被丟棄。 如果執行垃圾收集命令(pruning garbage collection),這些被丟棄的提交就會刪除。
現在我們可以看一下用merge和用rebase所產生的歷史的區別:
當我們使用git log
來參看commit時,其commit的順序也有所不同。
假設C3提交於9:00AM
,C5提交於10:00AM
,C4提交於11:00AM
,C6提交於12:00AM
,
對於使用git merge
來合併所看到的commit的順序(從新到舊)是:
C7,C6,C4,C5,C3,C2,C1
對於使用git rebase來合併所看到的commit的順序(從新到舊)是:
C7,C6',C5',C4,C3,C2,C1
因為C6'提交只是C6提交的克隆,C5'提交只是C5提交的克隆,
從使用者的角度看使用git rebase
來合併後所看到的commit的順序(從新到舊)是:
C7,C6,C5,C4,C3,C2,C1
另外,我們在使用git pull
命令的時候,可以使用--rebase
引數,即git pull --rebase
,這裡Git會把你的本地當前分支裡的每個提交(commit)取消掉,並且把它們臨時儲存為補丁(patch)(這些補丁放到".git/rebase"目錄中),然後把分支更新 為最新的"origin"分支,最後把儲存的這些補丁應用到分支上。
解決衝突
在rebase的過程中,也許會出現衝突(conflict)。在這種情況,Git會停止rebase並會讓你去解決衝突。rebase和merge的另一個區別是rebase的衝突是一個一個解決,如果有十個衝突,在解決完第一個衝突後,用"git add
"命令去更新這些內容的索引(index),然後,你無需執行 git-commit,只要執行:
$ git add -u
$ git rebase --continue
繼續後才會出現第二個衝突,直到所有衝突解決完,而merge是所有的衝突都會顯示出來。
在任何時候,你可以用--abort
引數來終止rebase的行動,並且"mywork" 分支會回到rebase開始前的狀態。
$ git rebase --abort
所以rebase的工作流就是
git rebase
while(存在衝突) {
git status
# 找到當前衝突檔案,編輯解決衝突
git add -u
git rebase --continue
if( git rebase --abort )
break;
}
最後衝突全部解決,rebase成功。
標籤管理
釋出一個版本時,我們通常先在版本庫中打一個標籤(tag),這樣,就唯一確定了打標籤時刻的版本。將來無論什麼時候,取某個標籤的版本,就是把那個打標籤的時刻的歷史版本取出來。所以,標籤也是版本庫的一個快照。
Git的標籤雖然是版本庫的快照,但其實它就是指向某個commit的指標(跟分支很像,但是分支可以移動,標籤不能移動),所以,建立和刪除標籤都是瞬間完成的。
Git有commit,為什麼還要引入tag?
“請把上週一的那個版本打包釋出,commit號是6a5819e...”
“一串亂七八糟的數字不好找!”
如果換一個辦法:
“請把上週一的那個版本打包釋出,版本號是v1.2”
“好的,按照tag v1.2查詢commit就行!”
所以,tag就是一個讓人容易記住的有意義的名字,它跟某個commit綁在一起。
建立標籤
在Git中打標籤非常簡單,首先,切換到需要打標籤的分支上:
$ git branch
* dev
master
$ git checkout master
Switched to branch 'master'
然後,敲命令git tag <name>
就可以打一個新標籤:
$ git tag v1.0
可以用命令git tag
檢視所有標籤:
$ git tag
v1.0
預設標籤是打在最新提交的commit上的。有時候,如果忘了打標籤,比如,現在已經是週五了,但應該在週一打的標籤沒有打,怎麼辦?
方法是找到歷史提交的commit id,然後打上就可以了:
$ git log --pretty=oneline --abbrev-commit
12a631b (HEAD -> master, tag: v1.0, origin/master) merged bug fix 101
4c805e2 fix bug 101
e1e9c68 merge with no-ff
f52c633 add merge
cf810e4 conflict fixed
5dc6824 & simple
14096d0 AND simple
b17d20e branch test
d46f35e remove test.txt
b84166e add test.txt
519219b git tracks changes
e43a48b understand how stage works
1094adb append GPL
e475afc add distributed
eaadf4e wrote a readme file
比方說要對add merge
這次提交打標籤,它對應的commit id是f52c633
,敲入命令:
$ git tag v0.9 f52c633
再用命令git tag
檢視標籤:
$ git tag
v0.9
v1.0
注意,標籤不是按時間順序列出,而是按字母排序的。可以用git show <tagname>
檢視標籤資訊:
$ git show v0.9
commit f52c63349bc3c1593499807e5c8e972b82c8f286 (tag: v0.9)
Author: Yaohua Guo <[email protected]>
Date: Fri May 18 21:56:54 2018 +0800
add merge
diff --git a/readme.txt b/readme.txt
...
可以看到,v0.9
確實打在add merge
這次提交上。
還可以建立帶有說明的標籤,用-a
指定標籤名,-m
指定說明文字:
$ git tag -a v0.1 -m "version 0.1 released" 1094adb
用命令git show <tagname>
可以看到說明文字:
$ git show v0.1
tag v0.1
Tagger: Yaohua Guo <[email protected]>
Date: Fri May 18 22:48:43 2018 +0800
version 0.1 released
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
Author: Yaohua Guo <[email protected]>
Date: Fri May 18 21:06:15 2018 +0800
append GPL
diff --git a/readme.txt b/readme.txt
...
操作標籤
如果標籤打錯了,也可以刪除:
$ git tag -d v0.1
Deleted tag 'v0.1' (was f15b0dd)
因為建立的標籤都只儲存在本地,不會自動推送到遠端。所以,打錯的標籤可以在本地安全刪除。
如果要推送某個標籤到遠端,使用命令git push origin <tagname>
:
$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To [email protected]:guoyaohua/learngit.git
* [new tag] v1.0 -> v1.0
或者,一次性推送全部尚未推送到遠端的本地標籤:
$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To [email protected]:guoyaohua/learngit.git
* [new tag] v0.9 -> v0.9
如果標籤已經推送到遠端,要刪除遠端標籤就麻煩一點,先從本地刪除:
$ git tag -d v0.9
Deleted tag 'v0.9' (was f52c633)
然後,從遠端刪除。刪除命令也是push,但是格式如下:
$ git push origin :refs/tags/v0.9
To [email protected]:guoyaohua/learngit.git
- [deleted] v0.9
要看看是否真的從遠端庫刪除了標籤,可以登陸GitHub檢視。
自定義Git
忽略特殊檔案
有些時候,你必須把某些檔案放到Git工作目錄中,但又不能提交它們,比如儲存了資料庫密碼的配置檔案啦,等等,每次git status
都會顯示Untracked files ...
,有強迫症的朋友心裡肯定不爽。
好在Git考慮到了大家的感受,這個問題解決起來也很簡單,在Git工作區的根目錄下建立一個特殊的.gitignore
檔案,然後把要忽略的檔名填進去,Git就會自動忽略這些檔案。
不需要從頭寫.gitignore
檔案,GitHub已經為我們準備了各種配置檔案,只需要組合一下就可以使用了。所有配置檔案可以直接線上瀏覽:https://github.com/github/gitignore
忽略檔案的原則是:
- 忽略作業系統自動生成的檔案,比如縮圖等;
- 忽略編譯生成的中間檔案、可執行檔案等,也就是如果一個檔案是通過另一個檔案自動生成的,那自動生成的檔案就沒必要放進版本庫,比如Java編譯產生的
.class
檔案; - 忽略你自己的帶有敏感資訊的配置檔案,比如存放口令的配置檔案。
舉個例子:
假設你在Windows下進行Python開發,Windows會自動在有圖片的目錄下生成隱藏的縮圖檔案,如果有自定義目錄,目錄下就會有Desktop.ini
檔案,因此你需要忽略Windows自動生成的垃圾檔案:
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini
然後,繼續忽略Python編譯產生的.pyc
、.pyo
、dist
等檔案或目錄:
# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build
加上你自己定義的檔案,最終得到一個完整的.gitignore
檔案,內容如下:
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini
# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build
# My configurations:
db.ini
deploy_key_rsa
最後一步就是把.gitignore
也提交到Git,就完成了!當然檢驗.gitignore
的標準是git status
命令是不是說working directory clean
。
使用Windows的朋友注意了,如果你在資源管理器裡新建一個.gitignore
檔案,它會非常弱智地提示你必須輸入檔名,但是在文字編輯器裡“儲存”或者“另存為”就可以把檔案儲存為.gitignore
了。
有些時候,你想新增一個檔案到Git,但發現新增不了,原因是這個檔案被.gitignore
忽略了:
$ git add App.class
The following paths are ignored by one of your .gitignore files:
App.class
Use -f if you really want to add them.
如果你確實想新增該檔案,可以用-f
強制新增到Git:
$ git add -f App.class
或者你發現,可能是.gitignore
寫得有問題,需要找出來到底哪個規則寫錯了,可以用git check-ignore
命令檢查:
$ git check-ignore -v App.class
.gitignore:3:*.class App.class
Git會告訴我們,.gitignore
的第3行規則忽略了該檔案,於是我們就可以知道應該修訂哪個規則。
配置別名
有沒有經常敲錯命令?比如git status
?status
這個單詞真心不好記。
如果敲git st
就表示git status
那就簡單多了,當然這種偷懶的辦法我們是極力贊成的。
我們只需要敲一行命令,告訴Git,以後st
就表示status
:
$ git config --global alias.st status
好了,現在敲git st
看看效果。
當然還有別的命令可以簡寫,很多人都用co
表示checkout
,ci
表示commit
,br
表示branch
:
$ git config --global alias.co checkout
$ git config --global alias.ci commit
$ git config --global alias.br branch
以後提交就可以簡寫成:
$ git ci -m "bala bala bala..."
--global
引數是全域性引數,也就是這些命令在這臺電腦的所有Git倉庫下都有用。
在撤銷修改一節中,我們知道,命令git reset HEAD file
可以把暫存區的修改撤銷掉(unstage),重新放回工作區。既然是一個unstage操作,就可以配置一個unstage
別名:
$ git config --global alias.unstage 'reset HEAD'
當你敲入命令:
$ git unstage test.py
實際上Git執行的是:
$ git reset HEAD test.py
配置一個git last
,讓其顯示最後一次提交資訊:
$ git config --global alias.last 'log -1'
這樣,用git last
就能顯示最近一次的提交:
$ git last
commit adca45d317e6d8a4b23f9811c3d7b7f0f180bfe2
Merge: bd6ae48 291bea8
Author: Yaohua Guo <[email protected]>
Date: Thu Aug 22 22:49:22 2013 +0800
merge & fix hello.py
甚至可以進一步美化把lg
配置成:
$ git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
來看看git lg
的效果:
配置檔案
配置Git的時候,加上--global
是針對當前使用者起作用的,如果不加,那隻針對當前的倉庫起作用。
配置檔案放哪了?每個倉庫的Git配置檔案都放在.git/config
檔案中:
$ type .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = [email protected]:michaelliao/learngit.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[alias]
last = log -1
別名就在[alias]
後面,要刪除別名,直接把對應的行刪掉即可。
而當前使用者的Git配置檔案放在使用者主目錄下的一個隱藏檔案.gitconfig
中:
$ type .gitconfig
[alias]
co = checkout
ci = commit
br = branch
st = status
[user]
name = Your Name
email = [email protected]
配置別名也可以直接修改這個檔案,如果改錯了,可以刪掉檔案重新通過命令配置。
總結
- Git記錄的是檔案的修改狀態,而不是檔案本身。
- 初始化一個Git倉庫,使用
git init
命令。 - 新增檔案到Git倉庫,分兩步:
- 使用命令
git add <file>
,注意,可反覆多次使用,新增多個檔案; - 使用命令
git commit -m <message>
,完成。
- 使用命令
- 每次修改,如果不用
git add
到暫存區,那就不會加入到commit
中。 - 提交後,可用
git diff HEAD -- <file_name>
命令可以檢視工作區和版本庫裡面最新版本的區別。 - 要關聯一個遠端庫,使用命令
git remote add origin git@server-name:path/repo-name.git
,使用命令git push -u origin master
第一次推送master分支的所有