1. 程式人生 > >Git常用操作指南

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_rsaid_rsa.pub這兩個檔案,如果已經有了,可直接跳到下一步。

如果沒有,則執行:

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

一路回車即可。執行命令後,我們再進入到"~/.ssh"目錄下,執行"ls"命令,可以看到裡面有id_rsaid_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]
  1. –hard:重設“暫存區”和“工作區”,從<commit>以來在工作區中的任何改變都被丟棄,並把HEAD指向<commit>。(徹底回退到某個版本,本地的原始碼也會變為上一個版本的內容。)
  2. –soft:“工作區”中的內容不作任何改變,HEAD指向<commit>,自從<commit>以來的所有改變都會回退到“暫存區”中,顯示在git status“Changes to be committed”中。(回退到某個版本,只回退了commit的資訊。如果還要提交,直接commit即可。)
  3. –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 常用示例

  1. 回退add操作

    $ git add test
    $ git reset HEAD test  
    # HEAD指的是當前指向的版本號,可以將HEAD還成任意想回退的版本號

    可以將test從“已暫存”狀態(Index區)回滾到指定Commit時暫存區的狀態。

  2. 回退最後一次提交

    $ git add test
    $ git commit -m "Add test"
    $ git reset --soft HEAD^

    可以將test從“已提交”狀態變為“已暫存”狀態。

  3. 回退最近幾次提交,並把這幾次提交放到新分支上

    $ git branch topic # 已當前分支為基礎,新建分支topic
    $ git reset --hard HEAD~2 # 在當前分支上回滾提交
    $ git checkout topic

    通過臨時分支來保留提交,然後在當前分支上做硬回滾。

  4. 將本地的狀態回退到和遠端一樣

    $ git reset --hard origin/devlop
  5. 回退到某個版本提交

    $ git reset 497e350

    當前HEAD會指向“497e350”,暫存區中的狀態會恢復到提交“497e350”時暫存區的狀態。

撤銷修改

當我們因為一些原因想要丟棄工作區某些檔案修改時,可以使用“git checkout -- <file>”命令,該命令僅會恢復工作區檔案狀態,不會對版本庫有任何改動。

命令git checkout -- file1.txt意思就是,把file1.txt檔案在工作區的修改全部撤銷,這裡有兩種情況:

  • 一種是file1.txt自修改後還沒有被放到暫存區,現在,撤銷修改就回到和版本庫一模一樣的狀態;
  • 一種是file1.txt已經新增到暫存區後,又作了修改,現在,撤銷修改就回到新增到暫存區後的狀態。

總之,就是讓這個檔案回到最近一次git commitgit 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嚴格來說不是指向提交,而是指向mastermaster才是指向提交的,所以,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中,分支完全可以在本地自己藏著玩,是否推送,視你的心情而定!

抓取分支

多人協作時,大家都會往masterdev分支上推送各自的修改。

現在,模擬一個你的同事,可以在另一臺電腦(注意要把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分支上開發,就必須建立遠端origindev分支到本地,於是他用這個命令建立本地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分支的連結,根據提示,設定devorigin/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

因此,多人協作的工作模式通常是這樣:

  1. 首先,可以試圖用git push origin <branch-name>推送自己的修改;
  2. 如果推送失敗,則因為遠端分支比你的本地更新,需要先用git pull試圖合併;
  3. 如果合併有衝突,則解決衝突,並在本地提交;
  4. 沒有衝突或者解決掉衝突後,再用git push origin <branch-name>推送就能成功!

如果git pull提示no tracking information,則說明本地分支和遠端分支的連結關係沒有建立,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>

這就是多人協作的工作模式,一旦熟悉了,就非常簡單。

Rebase

git rebasegit 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

忽略檔案的原則是:

  1. 忽略作業系統自動生成的檔案,比如縮圖等;
  2. 忽略編譯生成的中間檔案、可執行檔案等,也就是如果一個檔案是通過另一個檔案自動生成的,那自動生成的檔案就沒必要放進版本庫,比如Java編譯產生的.class檔案;
  3. 忽略你自己的帶有敏感資訊的配置檔案,比如存放口令的配置檔案。

舉個例子:

假設你在Windows下進行Python開發,Windows會自動在有圖片的目錄下生成隱藏的縮圖檔案,如果有自定義目錄,目錄下就會有Desktop.ini檔案,因此你需要忽略Windows自動生成的垃圾檔案:

# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini

然後,繼續忽略Python編譯產生的.pyc.pyodist等檔案或目錄:

# 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 statusstatus這個單詞真心不好記。

如果敲git st就表示git status那就簡單多了,當然這種偷懶的辦法我們是極力贊成的。

我們只需要敲一行命令,告訴Git,以後st就表示status

$ git config --global alias.st status

好了,現在敲git st看看效果。

當然還有別的命令可以簡寫,很多人都用co表示checkoutci表示commitbr表示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]

配置別名也可以直接修改這個檔案,如果改錯了,可以刪掉檔案重新通過命令配置。

總結

  1. Git記錄的是檔案的修改狀態,而不是檔案本身。
  2. 初始化一個Git倉庫,使用git init命令。
  3. 新增檔案到Git倉庫,分兩步:
    • 使用命令git add <file>,注意,可反覆多次使用,新增多個檔案;
    • 使用命令git commit -m <message>,完成。
  4. 每次修改,如果不用git add到暫存區,那就不會加入到commit中。
  5. 提交後,可用git diff HEAD -- <file_name>命令可以檢視工作區和版本庫裡面最新版本的區別。
  6. 要關聯一個遠端庫,使用命令git remote add origin git@server-name:path/repo-name.git,使用命令git push -u origin master第一次推送master分支的所有