Git Guide

 參考文獻:

  1. Git 菜鳥教程
  2. Git 簡明指南
  3. Git 命令手冊
  4. Git 命令手冊(pdf版)
  5. Git 教程:廖雪峯

# Git 概述

1、Git

Git是一個開源的 分佈式版本控制系統,用於敏捷高效地處理任何或小或大的項目。

2、Git 誕生

Linus在1991年創建了開源的Linux,從此,Linux系統不斷髮展,已經成爲最大的服務器系統軟件了。

Linus雖然創建了Linux,但Linux的壯大是靠全世界熱心的志願者參與的,這麼多人在世界各地爲Linux編寫代碼,那Linux的代碼是如何管理的呢?

事實是,在2002年以前,世界各地的志願者把源代碼文件通過diff的方式發給Linus,然後由Linus本人通過手工方式合併代碼!

你也許會想,爲什麼Linus不把Linux代碼放到版本控制系統裏呢?不是有CVS、SVN這些免費的版本控制系統嗎?因爲Linus堅定地反對CVS和SVN,這些集中式的版本控制系統不但速度慢,而且必須聯網才能使用。有一些商用的版本控制系統,雖然比CVS、SVN好用,但那是付費的,和Linux的開源精神不符。

不過,到了2002年,Linux系統已經發展了十年了,代碼庫之大讓Linus很難繼續通過手工方式管理了,社區的弟兄們也對這種方式表達了強烈不滿,於是Linus選擇了一個商業的版本控制系統BitKeeper,BitKeeper的東家BitMover公司出於人道主義精神,授權Linux社區免費使用這個版本控制系統。

安定團結的大好局面在2005年就被打破了,原因是Linux社區牛人聚集,不免沾染了一些梁山好漢的江湖習氣。開發Samba的Andrew試圖破解BitKeeper的協議(這麼幹的其實也不只他一個),被BitMover公司發現了(監控工作做得不錯!),於是BitMover公司怒了,要收回Linux社區的免費使用權。

Linus可以向BitMover公司道個歉,保證以後嚴格管教弟兄們,嗯,這是不可能的。實際情況是這樣的:

Linus花了兩週時間自己用C寫了一個分佈式版本控制系統,這就是Git!一個月之內,Linux系統的源碼已經由Git管理了!牛是怎麼定義的呢?大家可以體會一下。

Git迅速成爲最流行的分佈式版本控制系統,尤其是2008年,GitHub網站上線了,它爲開源項目免費提供Git存儲,無數開源項目開始遷移至GitHub,包括jQuery,PHP,Ruby等等。

歷史就是這麼偶然,如果不是當年BitMover公司威脅Linux社區,可能現在我們就沒有免費而超級好用的Git了。

3、分佈式 VS 集中式

# Git 安裝

在使用Git前我們需要先安裝 Git。Git 目前支持 Linux/Unix、Solaris、Mac和 Windows 平臺上運行。

Git 各平臺安裝包下載地址爲:http://git-scm.com/downloads

下載安裝即可,安裝好之後,輸入指令,查看 git 安裝位置及其版本。

$ which git
/usr/local/bin/git
$ git --version
git version 2.15.0

提示:windows 下需將 which 替換成 where.

1、配置用戶信息

配置個人的用戶名稱和電子郵件地址:

$ git config --global user.name "Your Name"
$ git config --global user.email "[email protected]"

如果用了 --global 選項,那麼更改的配置文件就是位於你用戶主目錄下的那個,以後你所有的項目都會默認使用這裏配置的用戶信息。

如果要在某個特定的項目中使用其他名字或者電郵,只要去掉 --global 選項重新配置即可,新的設定保存在當前項目的 .git/config 文件裏。

2、查看配置信息

輸入指令

$ git config --list

# Git 工作流程

一般工作流程如下:

  • 克隆 Git 資源作爲工作目錄。
  • 在克隆的資源上添加或修改文件。
  • 如果其他人修改了,你可以更新資源。
  • 在提交前查看修改。
  • 提交修改。
  • 在修改完成後,如果發現錯誤,可以撤回提交併再次修改並提交。

下圖展示了git的工作流程:

git

# Git 創建版本庫

版本庫又稱爲倉庫,英文名 repository,你可以簡單理解成一個目錄,這個目錄裏面的所有文件都可以被Git管理起來,每個文件的修改、刪除,Git都能跟蹤,以便任何時刻都可以追蹤歷史,或者在將來某個時刻可以“還原”。

所以,創建一個版本庫非常簡單。

首先,選擇一個合適的地方,創建一個空目錄,然後在終端中打開:

$ cd /Users/LiHongyao/Desktop/git_repository
$ pwd
/Users/LiHongyao/Desktop/git_repository

我在桌面上創建了一個 git_repository 的空目錄。pwd 命令用於顯示當前目錄。

然後,通過 git init 命令把這個目錄變成Git可以管理的倉庫:

$ git init
Initialized empty Git repository in /Users/LiHongyao/Desktop/git_repository/.git/

初始化後,會在 git_repository 目錄下會出現一個名爲 .git 的目錄,這個目錄是Git來跟蹤管理版本庫的,所有 Git 需要的數據和資源都存放在這個目錄中。

# Git 基本操作

1、添加文件至版本庫

再此之前,可先安裝一個 Notepad++ 編輯工具。

我們先創建一個 README.md 文件放置在 git_repositorie 目錄下,文件內容爲:

Git is a version control system.
Git is free software.

提示:文件內容不限。

$ touch README.md
$ vim README.md

macOS 在終端可輸入以上指令創建/編輯 README.md 文件,在 vim 中,輸入 i 進行編輯,編輯完成之後 按 ESC 退出編輯狀態輸入 :wq 保存退出即可。macOS下還可通過以下指令查看 README.md 文件的內容:

$ cat README.md

將文件添加至版本庫只需要兩個步驟,如下所示:

第1步:用命令 git add 告訴Git,把文件添加到倉庫:

$ git add README.md

提示:執行上面的命令,沒有任何顯示,這就對了,Unix的哲學是“沒有消息就是好消息”,說明添加成功。

我們可通過該指令添加通過文件,如下所示:

$ git add file1.md
$ git add file2.md
$ git add file3.md

第2步:用命令 git commit 告訴Git,把文件提交到倉庫:

$ git commit -m "wrote a readme file"
[master (root-commit) 0ba5814] wrote a readme file
 1 file changed, 2 insertions(+)
 create mode 100644 README.md

提示:

-m :該參數用於設置提交說明,可以輸入任意內容,便於我們在歷史記錄查找。

2、提交修改

  • git status:查看工作區狀態
  • git diff:查看修改

我們已經成功添加了 README.md 文件,接下來修改 README.md 文件內容如下:

Git is a distributed version control system.
Git is free software.

運行 git status 命令查看結果:

$ git status
On branch master
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.md

no changes added to commit (use "git add" and/or "git commit -a")

git status 命令可以讓我們時刻掌握倉庫當前的狀態,上面的命令告訴我們,README.md 被修改過了,但還沒有準備提交的修改。

我們可通過 git diff 指令查看做了哪些修改:

$ git diff
diff --git a/README.md b/README.md
index 46d49bf..9247db6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
-Git is a version control system.
+Git is a distributed version control system.
 Git is free software.

接下來,我們提交修改,提交修改與提交文件是一樣的。

$ git add "README.md"

在執行 git commit 之前,我們再來查看一下狀態:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README.md

git status 告訴我們,將要被提交的修改包括 README.md ,下一步就可直接提交了:

$ git commit -m "add distributed"
[master 725657e] add distributed
 1 file changed, 1 insertion(+), 1 deletion(-)

提交後,我們再用 git status 命令看看倉庫的當前狀態:

$ git status
On branch master
nothing to commit, working tree clean

Git告訴我們當前沒有需要提交的修改,而且,工作樹是乾淨(working tree clean)的。

3、版本回退

現在,你已經學會了修改文件,然後把修改提交到Git版本庫,那麼我們再練習一次,修改 README.md 文件如下:

Git is a distributed version control system.
Git is free software distributed under the GPL.

然後嘗試提交:

$ git add "README.md"
$ git commit -m "append GPL"
[master 9ed2515] append GPL
 1 file changed, 1 insertion(+), 1 deletion(-)

像這樣,你不斷對文件進行修改,然後不斷提交修改到版本庫裏,那麼版本庫就會記錄提交的多個版本,我們可通過 git log 指令查看歷史版本:

$ git log
commit 9ed2515ca3f3ba0e677ccb13a17756a287e04829 (HEAD -> master)
Author: LiHongyao <lihy_online@163.com>
Date:   Tue Jan 16 19:06:35 2018 +0800

    append GPL

commit 725657efad241b7eaa5e8528797591d2cefa02b3
Author: LiHongyao <lihy_online@163.com>
Date:   Tue Jan 16 18:58:53 2018 +0800

    add distributed

commit 0ba5814bca78a464947b3734b5abb3eb868216fc
Author: LiHongyao <lihy_online@163.com>
Date:   Tue Jan 16 17:50:02 2018 +0800

    wrote a readme file

git log 命令顯示從最近到最遠的提交日誌,我們可以看到3次提交:

最近的一次是 append GPL

上一次是 add distributed

最早的一次是 wrote a readme file

如果嫌輸出信息太多,看得眼花繚亂的,可以試試加上 --pretty=oneline 參數:

$ git log --pretty=oneline
9ed2515ca3f3ba0e677ccb13a17756a287e04829 (HEAD -> master) append GPL
725657efad241b7eaa5e8528797591d2cefa02b3 add distributed
0ba5814bca78a464947b3734b5abb3eb868216fc wrote a readme file

提示:

上述結果中注入 “725657….” 的字符串爲 “commit id”(版本號)。

好了,我們假設,如果你現在需要回退到上一個版本,也就是 “add distributed” 這個版本,我們應該如何實現呢?

首先,Git必須知道當前版本是哪個版本,在Git中,用 HEAD 表示當前版本,也就是最新的提交9ed25...04829(注意我的提交ID和你的肯定不一樣),上一個版本就是 HEAD^,上上一個版本就是HEAD^^,當然往上100個版本寫100個^比較容易數不過來,所以寫成 HEAD~100

現在,我們要把當前版本 “append GPL” 回退到上一個版本 “add distributed”,就可以使用 git reset 命令:

$ git reset --hard HEAD^
HEAD is now at 725657e add distributed

接下來我們查看一下 README.md 文件內容是否爲 ”add distributed“ 版本內容:

$ cat README.md
Git is a distributed version control system.
Git is free software.

果然,內容已經回退到了 ”add distributed“ 版本內容。

接下來,我們再來查看一下版本歷史記錄:

$ git log --pretty=oneline
725657efad241b7eaa5e8528797591d2cefa02b3 (HEAD -> master) add distributed
0ba5814bca78a464947b3734b5abb3eb868216fc wrote a readme file

最新的那個版本 ”append GPL“ 已經看不到了,如果你突然反悔了,又想回到 ”append GPL“ 版本怎麼辦呢?只要你的終端窗口沒有關閉,你就可以往上找,找到 ”append GPL“ 的版本號,使用如下指令即可:

$ git reset --hard 9ed2515ca3f3ba0e677ccb13a17756a287e04829
HEAD is now at 9ed2515 append GPL

這樣,就可以指定回到未來的某個版本:

提示:

版本號沒必要寫全,前幾位就可以了,Git會自動去找。當然也不能只寫前一兩位,因爲Git可能會找到多個版本號,就無法確定是哪一個了

我們在查看一下 README.md 文件的內容:

$ cat README.md
Git is a distributed version control system.
Git is free software distributed under the GPL.

果然,我胡漢三又回來啦~

Git的版本回退速度非常快,因爲Git在內部有個指向當前版本的HEAD指針,當你回退版本的時候,Git僅僅是把HEAD從指向 ”append GPL“ 改爲指向 ”add distributed“,然後順便把工作區的文件更新了。所以你讓HEAD指向哪個版本號,你就把當前版本定位在哪。

現在,你回退到了某個版本,關掉電腦,第二天你又想恢復到新版本怎麼辦呢?

在Git中,總是有後悔藥可以吃的。當你用 $ git reset --hard HEAD^回退到 ”add distributed“ 版本時,再想恢復到 ”append GPL“ ,就必須找到 ”append GPL“ 的 commit id。Git提供了一個命令 git reflog 用來記錄你的每一次命令:

$ git reflog
9ed2515 (HEAD -> master) HEAD@{0}: reset: moving to 9ed2515ca3f3ba0e677ccb13a17756a287e04829
725657e HEAD@{1}: reset: moving to HEAD^
9ed2515 (HEAD -> master) HEAD@{2}: commit: append GPL
725657e HEAD@{3}: commit: add distributed
0ba5814 HEAD@{4}: commit (initial): wrote a readme file

現在我們找到了 ”append GPL“ 版本的id之後,就可以進行回退啦。

本節知識點回顧:

  1. HEAD 指向的版本就是當前版本,因此,Git允許我們在版本的歷史之間穿梭,使用命令 git reset --hard commit_id
  2. 穿梭前,用 git log 可以查看提交歷史,以便確定要回退到哪個版本。
  3. 要重返未來,用 git reflog 查看命令歷史,以便確定要回到未來的哪個版本。

4、工作區和暫存區

Git和其他版本控制系統如SVN的一個不同之處就是有暫存區的概念。

工作區:Working tree

就是你在電腦裏能看到的目錄,比如我的 git_repository 文件夾就是一個工作區:

版本庫:Repository

工作區有一個隱藏目錄 .git,這個不算工作區,而是Git的版本庫。

Git 的版本庫裏存了很多東西,其中最重要的就是稱爲stage(或者叫index)的暫存區,還有Git爲我們自動創建的第一個分支 master,以及指向 master的一個指針叫HEAD

0

分支和 HEAD 的概念我們以後再講。

前面講了我們把文件往Git版本庫裏添加的時候,是分兩步執行的:

第一步是用 git add 把文件添加進去,實際上就是把文件修改添加到暫存區;

第二步是用 git commit 提交更改,實際上就是把暫存區的所有內容提交到當前分支。

因爲我們創建Git版本庫時,Git自動爲我們創建了唯一一個 ”master“ 分支,所以,現在,git commit 就是往 ”master“ 分支上提交更改。

你可以簡單理解爲,需要提交的文件修改放到暫存區,然後,一次性提交暫存區的所有修改。

我們來練習一下,修改 “README.md” 文件,增加一行語句,如下所示:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.

然後,在工作區新增一個 ”LICENSE“ 文本文件(內容隨便寫)。

先用 git status 查看一下狀態:

$ git status
On branch master
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.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    LICENSE

no changes added to commit (use "git add" and/or "git commit -a")

Git非常清楚地告訴我們, “README.md” 被修改了,而 ”LICENSE“ 還從來沒有被添加過,所以它的狀態是 Untracked

現在,使用兩次命令 git add,把 “README.md””LICENSE“ 都添加後,用 git status 再查看一下:

$ git add README.md
$ git add LICENSE
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   LICENSE
    modified:   README.md

現在,暫存區的狀態就變成這樣了:

1

所以,git add 命令實際上就是把要提交的所有修改放到暫存區(Stage),然後,執行git commit就可以一次性把暫存區的所有修改提交到分支。

$ git commit -m "understand how stage works"
[master 707e250] understand how stage works
 2 files changed, 3 insertions(+)
 create mode 100644 LICENSE

一旦提交後,如果你又沒有對工作區做任何修改,那麼工作區就是“乾淨”的:

$ git status
On branch master
nothing to commit, working tree clean

現在版本庫變成了這樣,暫存區就沒有任何內容了:

3

5、管理修改

現在,假定你已經完全掌握了暫存區的概念。下面,我們要討論的就是,爲什麼Git比其他版本控制系統設計得優秀,因爲Git跟蹤並管理的是修改,而非文件。

你會問,什麼是修改?比如你新增了一行,這就是一個修改,刪除了一行,也是一個修改,更改了某些字符,也是一個修改,刪了一些又加了一些,也是一個修改,甚至創建一個新文件,也算一個修改。

爲什麼說Git管理的是修改,而不是文件呢?我們還是做實驗。第一步,對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.

然後添加:

$ git add "README.md"
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README.md

然後,再修改:

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.

提交:

$ git commit -m "git tracks changes"
[master 3adc08e] git tracks changes
 1 file changed, 3 insertions(+), 1 deletion(-)

提交後,再看看狀態:

$ git status
On branch master
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.md

no changes added to commit (use "git add" and/or "git commit -a")

怎麼第二次的修改沒有被提交?我們來回顧一下操作過程:

第一次修改 -> git add -> 第二次修改 -> git commit

你看,我們前面講了,Git管理的是修改,當你用git add命令後,在工作區的第一次修改被放入暫存區,準備提交,但是,在工作區的第二次修改並沒有放入暫存區,所以,git commit只負責把暫存區的修改提交了,也就是第一次的修改被提交了,第二次的修改不會被提交。

提交後,用 git diff HEAD -- README.md命令可以查看工作區和版本庫裏面最新版本的區別:

$ git diff HEAD -- README.md
diff --git a/README.md b/README.md
index 5a35689..b964d55 100644
--- a/README.md
+++ b/README.md
@@ -4,4 +4,4 @@ Git is free software distributed under the GPL.

 Git has a mutable index called stage.

-Git tracks changes.
\ No newline at end of file
+Git tracks changes of files.
\ No newline at end of file

可見,第二次修改確實沒有被提交。

那怎麼提交第二次修改呢?你可以繼續git addgit commit,也可以彆着急提交第一次修改,先git add第二次修改,再git commit,就相當於把兩次修改合併後一塊提交了:

第一次修改 -> git add -> 第二次修改 -> git add -> git commit

好,現在,把第二次修改提交了,然後開始小結。

小結:

現在,你又理解了Git是如何跟蹤修改的,每次修改,如果不add到暫存區,那就不會加入到commit中。

6、撤銷修改

如果你不小心在你的 ”README.md“ 文件中加了一行語句:

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.
My stupid boss still prefers SVN.

在你準備提交前,你猛然發現了“stupid boss”可能會讓你丟掉這個月的獎金!

既然錯誤發現得很及時,就可以很容易地糾正它。你可以刪掉最後一行,手動把文件恢復到上一個版本的狀態。如果用 git status 查看一下:

$ git status
On branch master
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.md

no changes added to commit (use "git add" and/or "git commit -a")

你可以發現,Git會告訴你,git checkout -- file 可以丟棄工作區的修改:

$ git checkout -- "README.md"

命令 git checkout -- readme.txt 意思就是,把 “README.md” 文件在工作區的修改全部撤銷,這裏有兩種情況:

一種是 “README.md” 自修改後還沒有被放到暫存區,現在,撤銷修改就回到和版本庫一模一樣的狀態;

一種是 “README.md” 已經添加到暫存區後,又作了修改,現在,撤銷修改就回到添加到暫存區後的狀態。

總之,就是讓這個文件回到最近一次git commitgit add時的狀態。

現在,看看 “README.md” 的文件內容:

$ cat "README.md"
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.

文件內容果然復原了。

提示:

git checkout -- file命令中的--很重要,沒有--,就變成了“切換到另一個分支”的命令,我們在後面的分支管理中會再次遇到git checkout命令。

還有一種情況,就是你不小心添加了一條語句,還 git add 到暫存區了:

$ cat "README.md"
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.
My stupid boss still prefers SVN.
$ git add "README.md"

慶幸的是,在commit之前,你發現了這個問題。用git status查看一下,修改只是添加到了暫存區,還沒有提交:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README.md

Git同樣告訴我們,用命令git reset HEAD file可以把暫存區的修改撤銷掉(unstage),重新放回工作區:

$ git reset HEAD "README.md"
Unstaged changes after reset:
M   README.md

git reset 命令既可以回退版本,也可以把暫存區的修改回退到工作區。當我們用HEAD時,表示最新的版本。

再用git status查看一下,現在暫存區是乾淨的,工作區有修改:

$ git status
On branch master
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.md

no changes added to commit (use "git add" and/or "git commit -a")

還記得如何丟棄工作區的修改嗎?

git checkout -- "README.md"
lihongyaodeMacBook-Pro:git_repository LiHongyao$ git status
On branch master
nothing to commit, working tree clean

整個世界終於清靜了!

現在,假設你不但改錯了東西,還從暫存區提交到了版本庫,怎麼辦呢?還記得版本回退一節嗎?可以回退到上一個版本。不過,這是有條件的,就是你還沒有把自己的本地版本庫推送到遠程。還記得Git是分佈式版本控制系統嗎?我們後面會講到遠程版本庫,一旦你把“stupid boss”提交推送到遠程版本庫,你就真的慘了……

小結:

  1. 當你改亂了工作區某個文件的內容,想直接丟棄工作區的修改時,用命令git checkout -- file
  2. 當你不但改亂了工作區某個文件的內容,還添加到了暫存區時,想丟棄修改,分兩步,第一步用命令git reset HEAD file,就回到了1,第二步按1操作。
  3. 已經提交了不合適的修改到版本庫時,想要撤銷本次提交,參考版本回退一節,不過前提是沒有推送到遠程庫。

7、刪除文件

在Git中,刪除也是一個修改操作,我們實戰一下,先添加一個新文件test.md到Git並且提交:

$ touch test.md
$ git add "test.md"
$ git commit -m "add test.md"
[master b6e477c] add test.md
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test.md

一般情況下,你通常直接在文件管理器中把沒用的文件刪了,或者用rm命令刪了:

$ rm test.md

這個時候,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.md

現在你有兩個選擇,一是確實要從版本庫中刪除該文件,那就用命令git rm刪掉,並且git commit

git rm test.md
rm 'test.md'
$ git commit -m "remove test.md"
[master 7b08dab] remove test.md
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 test.md

現在,文件就從版本庫中被刪除了。

另一種情況是刪錯了,因爲版本庫裏還有呢,所以可以很輕鬆地把誤刪的文件恢復到最新版本:

$ git checkout -- test.md

git checkout其實是用版本庫裏的版本替換工作區的版本,無論工作區是修改還是刪除,都可以“一鍵還原”。

小結:

命令git rm用於刪除一個文件。如果一個文件已經被提交到版本庫,那麼你永遠不用擔心誤刪,但是要小心,你只能恢復文件到最新版本,你會丟失最近一次提交後你修改的內容

# 遠程倉庫

到目前爲止,我們已經掌握瞭如何在Git倉庫裏對一個文件進行操作,你再也不用擔心文件備份或者丟失的問題了。

可是有用過集中式版本控制系統SVN的童鞋會站出來說,這些功能在SVN裏早就有了,沒看出Git有什麼特別的地方。

沒錯,如果只是在一個倉庫裏管理文件歷史,Git和SVN真沒啥區別。爲了保證你現在所學的Git物超所值,將來絕對不會後悔,同時爲了打擊已經不幸學了SVN的童鞋,本章開始介紹Git的殺手級功能之一(注意是之一,也就是後面還有之二,之三……):遠程倉庫

Git是分佈式版本控制系統,同一個Git倉庫,可以分佈到不同的機器上。怎麼分佈呢?最早,肯定只有一臺機器有一個原始版本庫,此後,別的機器可以“克隆”這個原始版本庫,而且每臺機器的版本庫其實都是一樣的,並沒有主次之分。

你肯定會想,至少需要兩臺機器才能玩遠程庫不是?但是我只有一臺電腦,怎麼玩?

其實一臺電腦上也是可以克隆多個版本庫的,只要不在同一個目錄下。不過,現實生活中是不會有人這麼傻的在一臺電腦上搞幾個遠程庫玩,因爲一臺電腦上搞幾個遠程庫完全沒有意義,而且硬盤掛了會導致所有庫都掛掉,所以我也不告訴你在一臺電腦上怎麼克隆多個倉庫。

實際情況往往是這樣,找一臺電腦充當服務器的角色,每天24小時開機,其他每個人都從這個“服務器”倉庫克隆一份到自己的電腦上,並且各自把各自的提交推送到服務器倉庫裏,也從服務器倉庫中拉取別人的提交。

完全可以自己搭建一臺運行Git的服務器,不過現階段,爲了學Git先搭個服務器絕對是小題大作。好在這個世界上有個叫GitHub的神奇的網站,從名字就可以看出,這個網站就是提供Git倉庫託管服務的,所以,只要註冊一個GitHub賬號,就可以免費獲得Git遠程倉庫。

在繼續閱讀後續內容前,請自行註冊GitHub賬號。由於你的本地Git倉庫和GitHub倉庫之間的傳輸是通過SSH加密的,所以,需要一點設置:

第1步:創建SSH Key。在用戶主目錄下,看看有沒有.ssh目錄,如果有,再看看這個目錄下有沒有 ”id_rsa“”id_rsa.pub“ 這兩個文件,如果已經有了,可直接跳到下一步。如果沒有,打開Shell(Windows下打開Git Bash),創建SSH Key:

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

你需要把郵件地址換成你自己的郵件地址,然後一路回車,使用默認值即可,由於這個Key也不是用於軍事目的,所以也無需設置密碼。

如果一切順利的話,可以在用戶主目錄裏找到 ”.ssh“ 目錄,裏面有 ”id_rsa“”id_rsa.pub” 兩個文件,這兩個就是SSH Key的祕鑰對, ”id_rsa“ 是私鑰,不能泄露出去, ”id_rsa.pub” 是公鑰,可以放心地告訴任何人。

第2步:

登陸Github -> 點擊右上角個人頭像 -> 點擊Settings進入設置頁面 ->

點擊SSH and GPG keys選項 -> 點擊右上角New SSH key ->

在添加面板中,

Title:設置SSH key 標題,可任意填寫

Key:將 ”id_rsa.pub” 文件內容拷貝至此

然後點擊 Add SSH Key 就可以看到你創建的SSH Key了,如下圖所示:

3

爲什麼GitHub需要SSH Key呢?因爲GitHub需要識別出你推送的提交確實是你推送的,而不是別人冒充的,而Git支持SSH協議,所以,GitHub只要知道了你的公鑰,就可以確認只有你自己才能推送。

當然,GitHub允許你添加多個Key。假定你有若干電腦,你一會兒在公司提交,一會兒在家裏提交,只要把每臺電腦的Key都添加到GitHub,就可以在每臺電腦上往GitHub推送了。

最後友情提示,在GitHub上免費託管的Git倉庫,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放進去。

如果你不想讓別人看到Git庫,有兩個辦法,一個是交點保護費,讓GitHub把公開的倉庫變成私有的,這樣別人就看不見了(不可讀更不可寫)。另一個辦法是自己動手,搭一個Git服務器,因爲是你自己的Git服務器,所以別人也是看不見的。這個方法我們後面會講到的,相當簡單,公司內部開發必備。

確保你擁有一個GitHub賬號後,我們就即將開始遠程倉庫的學習。

小結:

“有了遠程倉庫,媽媽再也不用擔心我的硬盤了。”——Git點讀機

1、添加遠程庫

現在的情景是,你已經在本地創建了一個Git倉庫後,又想在GitHub創建一個Git倉庫,並且讓這兩個倉庫進行遠程同步,這樣,GitHub上的倉庫既可以作爲備份,又可以讓其他人通過該倉庫來協作,真是一舉多得。

首先,登陸GitHub,然後,在右上角找到點擊 + 按鈕,再選擇 “New Repository” 創建一個新的倉庫:

4

“Repository name” 填入 “GITTesting”,其他保持默認設置,點擊 “Create repository” 按鈕,就成功地創建了一個新的Git倉庫:

5

目前,在GitHub上的這個 “GITTesting” 倉庫還是空的,GitHub告訴我們,可以從這個倉庫克隆出新的倉庫,也可以把一個已有的本地倉庫與之關聯,然後,把本地倉庫的內容推送到GitHub倉庫。

現在,我們根據GitHub的提示,在本地的 “GITTesting” 倉庫下運行命令:

$ git remote add origin https://github.com/LiHongyao/GITTesting.git

請千萬注意,把上面的 LiHongyao 替換成你自己的GitHub賬戶名,否則,你在本地關聯的就是我的遠程庫,關聯沒有問題,但是你以後推送是推不上去的,因爲你的SSH Key公鑰不在我的賬戶列表中。

添加後,遠程庫的名字就是 origin,這是Git默認的叫法,也可以改成別的,但是origin這個名字一看就知道是遠程庫。

下一步,就可以把本地庫的所有內容推送到遠程庫上:

$ git push -u origin master
Counting objects: 22, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (18/18), done.
Writing objects: 100% (22/22), 1.83 KiB | 939.00 KiB/s, done.
Total 22 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), done.
To https://github.com/LiHongyao/GITTesting.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

把本地庫的內容推送到遠程,用 git push命令,實際上是把當前分支 master 推送到遠程。

由於遠程庫是空的,我們第一次推送 master 分支時,加上了 -u 參數,Git不但會把本地的master 分支內容推送的遠程新的 master 分支,還會把本地的 master 分支和遠程的 master 分支關聯起來,在以後的推送或者拉取時就可以簡化命令。

推送成功後,可以立刻在GitHub頁面中看到遠程庫的內容已經和本地一模一樣:

6

從現在起,只要本地作了提交,就可以通過命令:

$ git push origin master

把本地master分支的最新修改推送至GitHub,現在,你就擁有了真正的分佈式版本庫!

SSH 警告

當你第一次使用Git的clone或者push命令連接GitHub時,會得到一個警告:

The authenticity of host 'github.com (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is xx.xx.xx.xx.xx.
Are you sure you want to continue connecting (yes/no)?

這是因爲Git使用SSH連接,而SSH連接在第一次驗證GitHub服務器的Key時,需要你確認GitHub的Key的指紋信息是否真的來自GitHub的服務器,輸入yes回車即可。

Git會輸出一個警告,告訴你已經把GitHub的Key添加到本機的一個信任列表裏了:

Warning: Permanently added 'github.com' (RSA) to the list of known hosts.

這個警告只會出現一次,後面的操作就不會有任何警告了。

如果你實在擔心有人冒充GitHub服務器,輸入yes前可以對照GitHub的RSA Key的指紋信息是否與SSH連接給出的一致。

小結:

要關聯一個遠程庫,使用命令:

git remote add origin git@server-name:path/repo-name.git

關聯後,使用命令git push -u origin master第一次推送master分支的所有內容;

此後,每次本地提交後,只要有必要,就可以使用命令git push origin master推送最新修改;

分佈式版本系統的最大好處之一是在本地工作完全不需要考慮遠程庫的存在,也就是有沒有聯網都可以正常工作,而SVN在沒有聯網的時候是拒絕幹活的!當有網絡的時候,再把本地提交推送一下就完成了同步,真是太方便了!

2、克隆遠程庫

上一小節中,我們講了先有本地庫,後有遠程庫的時候,如何關聯遠程庫。

現在,假設我們從零開發,那麼最好的方式是先創建遠程庫,然後,從遠程庫克隆。

首先,登陸GitHub,創建一個新的倉庫,名字叫:GITProj

7

注意:創建新的倉庫時,勾選 “Initialize this repository with a README” , 這樣GitHub會自動爲我們創建一個README.md文件。創建完畢後,可以看到README.md文件:

8

現在,遠程庫已經準備好了,下一步是用命令git clone克隆一個本地庫:

$ git clone [email protected]:LiHongyao/GITProj.git
Cloning into 'GITProj'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.

注意把Git庫的地址換成你自己的,然後進入 “GITProj” 目錄看看,已經有 README.md 文件了。

如果有多個人協作開發,那麼每個人各自從遠程克隆一份就可以了。

你也許還注意到,GitHub給出的地址不止一個,可以用 :

這樣的地址。實際上,Git支持多種協議,默認的 git://使用ssh,但也可以使用https等其他協議。使用https除了速度慢以外,還有個最大的麻煩是每次推送都必須輸入口令,但是在某些只開放http端口的公司內部就無法使用ssh協議而只能用https

小結:

  1. 要克隆一個倉庫,首先必須知道倉庫的地址,然後使用git clone命令克隆。
  2. Git支持多種協議,包括https,但通過ssh支持的原生git協議速度最快。

# 分支管理

分支就是科幻電影裏面的平行宇宙,當你正在電腦前努力學習Git的時候,另一個你正在另一個平行宇宙裏努力學習SVN。

如果兩個平行宇宙互不干擾,那對現在的你也沒啥影響。不過,在某個時間點,兩個平行宇宙合併了,結果,你既學會了Git又學會了SVN!

9

分支在實際中有什麼用呢?假設你準備開發一個新功能,但是需要兩週才能完成,第一週你寫了50%的代碼,如果立刻提交,由於代碼還沒寫完,不完整的代碼庫會導致別人不能幹活了。如果等代碼全部寫完再一次提交,又存在丟失每天進度的巨大風險。

現在有了分支,就不用怕了。你創建了一個屬於你自己的分支,別人看不到,還繼續在原來的分支上正常工作,而你在自己的分支上幹活,想提交就提交,直到開發完畢後,再一次性合併到原來的分支上,這樣,既安全,又不影響別人工作。

其他版本控制系統如SVN等都有分支管理,但是用過之後你會發現,這些版本控制系統創建和切換分支比蝸牛還慢,簡直讓人無法忍受,結果分支功能成了擺設,大家都不去用。

但Git的分支是與衆不同的,無論創建、切換和刪除分支,Git在1秒鐘之內就能完成!無論你的版本庫是1個文件還是1萬個文件。

1、創建與合併分支

版本回退 裏,你已經知道,每次提交,Git都把它們串成一條時間線,這條時間線就是一個分支。截止到目前,只有一條時間線,在Git裏,這個分支叫主分支,即 master 分支。HEAD 嚴格來說不是指向提交,而是指向 mastermaster 纔是指向提交的,所以,HEAD 指向的就是當前分支。

一開始的時候,master 分支是一條線,Git用 master指向最新的提交,再用HEAD指向master,就能確定當前分支,以及當前分支的提交點:

10

每次提交,master 分支都會向前移動一步,這樣,隨着你不斷提交,master分支的線也越來越長。

當我們創建新的分支,例如 dev 時,Git 新建了一個指針叫 dev,指向 master 相同的提交,再把 HEAD 指向 dev,就表示當前分支在 dev 上:

11

你看,Git 創建一個分支很快,因爲除了增加一個 dev指針,改改HEAD的指向,工作區的文件都沒有任何變化!

不過,從現在開始,對工作區的修改和提交就是針對 dev 分支了,比如新提交一次後,dev 指針往前移動一步,而 master 指針不變:

12

假如我們在 dev 上的工作完成了,就可以把 dev 合併到 master 上。Git怎麼合併呢?最簡單的方法,就是直接把 master 指向 dev 的當前提交,就完成了合併.

13

所以Git合併分支也很快!就改改指針,工作區內容也不變!

合併完分支後,甚至可以刪除dev分支。刪除dev分支就是把dev指針給刪掉,刪掉後,我們就剩下了一條master分支:

14

下面開始實戰:

首先,我們創建 dev 分支,然後切換到 dev 分支:

$ git checkout -b dev
Switched to a new branch 'dev'

git checkout命令加上 -b 參數表示創建並切換,相當於以下兩條命令:

$ git branch dev
$ git checkout dev
Switched to branch 'dev'

然後,用 git branch 命令查看當前分支:

$ git branch
* dev
  master

git branch 命令會列出所有分支,當前分支前面會標一個*號。

然後,我們就可以在 dev 分支上正常提交,比如對 “README.md” 文件做個修改,加上一行:

Creating a new branch is quick.

然後提交:

$ git add README.md
$ git commit -m "branch test"
[dev e433476] branch test
 1 file changed, 2 insertions(+), 1 deletion(-)

現在,dev 分支的工作完成,我們就可以切換回 master 分支:

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git branch
  dev
* master

切換回 master 分支後,再查看一個 “README.md” 文件,剛纔添加的內容不見了!因爲那個提交是在 dev 分支上,而 master 分支此刻的提交點並沒有變:

15

現在,我們把 dev 分支的工作成果合併到 master 分支上:

$ git merge dev
Updating dcfb8f2..e433476
Fast-forward
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

git merge命令用於合併指定分支到當前分支。合併後,再查看 “README.md” 的內容,就可以看到,和dev分支的最新提交是完全一樣的。

注意:

上面的 “Fast-forward” 信息,Git告訴我們,這次合併是“快進模式”,也就是直接把master指向dev的當前提交,所以合併速度非常快

當然,也不是每次合併都能 “Fast-forward” ,我們後面會講其他方式的合併。

合併完成後,就可以放心地刪除 dev 分支了:

$ git branch -d dev
Deleted branch dev (was e433476).

刪除後,查看 branch,就只剩下master分支了:

$ git branch
* master

因爲創建、合併和刪除分支非常快,所以Git鼓勵你使用分支完成某個任務,合併後再刪掉分支,這和直接在master分支上工作效果是一樣的,但過程更安全。

小結:

  1. Git鼓勵大量使用分支:
  2. 查看分支:git branch
  3. 創建分支:git branch <name>
  4. 切換分支:git checkout <name>
  5. 創建+切換分支:git checkout -b <name>
  6. 合併某分支到當前分支:git merge <name>
  7. 刪除分支:git branch -d <name>

2、解決衝突

合併分支往往也不是一帆風順的,可能會出現衝突。

準備新的 feature 分支,繼續我們的新分支開發:

$ git checkout -b feature
Switched to a new branch 'feature'

修改 “README.md” 文件最後一行爲:

Creating a new branch is quick AND simple.

feature 分支上提交:

$ git add "README.md"
$ git commit -m "And simple"
[feature 3bf80e1] And simple
 1 file changed, 1 insertion(+), 1 deletion(-)

切換到 master 分支:

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

master 分支上把 “README.md” 文件的最後一行改爲:

Creating a new branch is quick & simple.

提交:

$ git add "README.md"
$ git commit -m "& simple"
[master 103a7f9] & simple
 1 file changed, 1 insertion(+), 1 deletion(-)

現在,master 分支和 feature 分支各自都分別有新的提交,變成了這樣:

16

這種情況下,Git無法執行“快速合併”,只能試圖把各自的修改合併起來,但這種合併就可能會有衝突,我們試試看:

$ git merge feature
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

果然衝突了!Git告訴我們,“README.md” 文件存在衝突,必須手動解決衝突後再提交。git status 也可以告訴我們衝突的文件:

$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (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.md

no changes added to commit (use "git add" and/or "git commit -a")

我們可以直接查看 “README.md” 的內容:

$ cat README.md
# GITProj
<<<<<<< HEAD
Creating a new branch & quick.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature

Git用 <<<<<<<=======>>>>>>> 標記出不同分支的內容,我們修改如下後保存:

Creating a new branch is quick and simple.

再提交

$ git add README.md
$ git commit -m "conflict fixed"
[master 8e7e3d8] conflict fixed

現在,master 分支和 feature分支變成了下圖所示:

17

用帶參數的 git log 也可以看到分支的合併情況:

$ git log --graph --pretty=oneline --abbrev-commit
*   8e7e3d8 (HEAD -> master) conflict fixed
|\  
| * 3bf80e1 (feature) And simple
* | 103a7f9 & simple
|/  
* e433476 (origin/master, origin/HEAD) branch test
* dcfb8f2 Initial commit

最後,刪除 feature 分支:

$ git branch -d feature
Deleted branch feature (was 3bf80e1).

工作完成。

小結:

  1. 當Git無法自動合併分支時,就必須首先解決衝突。解決衝突後,再提交,合併完成。
  2. git log --graph 命令可以看到分支合併圖。

3、分支管理策略

通常,合併分支時,如果可能,Git會用 Fast forward 模式,但這種模式下,刪除分支後,會丟掉分支信息。

如果要強制禁用 Fast forward 模式,Git就會在merge時生成一個新的commit,這樣,從分支歷史上就可以看出分支信息。

下面我們實戰一下 --no-ff 方式的 git merge

首先,仍然創建並切換 dev 分支:

$ git checkout -b dev
Switched to a new branch 'dev'

修改 “README.md” 文件,並提交一個新的 commit:

$ git add README.md
$ git commit -m "add hello"
[dev a5191b6] add hello
 1 file changed, 2 insertions(+), 1 deletion(-)

現在,我們切換回 master

git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)

準備合併 dev 分支,請注意 --no-ff 參數,表示禁用 Fast forward

$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

合併後,我們用 git log 看看分支歷史:

$ git log --graph --pretty=oneline --abbrev-commit
*   c2b5888 (HEAD -> master) merge with no-ff
|\  
| * a5191b6 (dev) add hello
|/  
*   8e7e3d8 conflict fixed
...

可以看到,不使用 Fast forward 模式,merge後就像這樣:

18

分支策略

在實際開發中,我們應該按照幾個基本原則進行分支管理:

首先,master 分支應該是非常穩定的,也就是僅用來發布新版本,平時不能在上面幹活;

那在哪幹活呢?幹活都在 dev 分支上,也就是說,dev 分支是不穩定的,到某個時候,比如1.0版本發佈時,再把 dev 分支合併到 master 上,在 master 分支發佈1.0版本;

你和你的小夥伴們每個人都在 dev 分支上幹活,每個人都有自己的分支,時不時地往 dev 分支上合併就可以了。

所以,團隊合作的分支看起來就像這樣:

19

小結:

  1. Git分支十分強大,在團隊開發中應該充分應用。
  2. 合併分支時,加上 --no-ff 參數就可以用普通模式合併,合併後的歷史有分支,能看出來曾經做過合併,而 fast forward 合併就看不出來曾經做過合併。

4、Bug 分支

軟件開發中,bug就像家常便飯一樣。有了bug就需要修復,在Git中,由於分支是如此的強大,所以,每個bug都可以通過一個新的臨時分支來修復,修復後,合併分支,然後將臨時分支刪除。

當你接到修復一個代號爲101的bug的任務時,很自然地,你想創建一個分支 issue-101 來修復它,但是,等等,當前正在 dev 上進行的工作還沒有提交,比如你正在編寫一個 “Test.txt” 文件:

$ git status
On branch dev
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    Test.txt

nothing added to commit but untracked files present (use "git add" to track)

但是你這個文件現在還不能提交,怎麼辦呢?幸好,Git還提供了一個 stash 功能,可以把當前工作現場“儲藏”起來,等以後恢復現場後繼續工作:

$ git stash
Saved working directory and index state WIP on dev: b4537f9 add Test.txt

現在,用 git status 查看工作區,就是乾淨的(除非有沒有被Git管理的文件),因此可以放心地創建分支來修復bug。

首先確定要在哪個分支上修復bug,假定需要在 master 分支上修復,就從 master 創建臨時分支:

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git checkout -b issue-101
Switched to a new branch 'issue-101'

現在修復bug,比如需要將 “README.md” 文件全部替換成如下信息來解決bug:

Hello, git!

提交:

$ git add "README.md"
$ git commit -m "fix bug 101"
[issue-101 341417c] fix bug 101
 1 file changed, 2 deletions(-)

修復完成後,切換到 master 分支,並完成合並,最後刪除 issue-101 分支:

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
 README.md | 2 --
 1 file changed, 2 deletions(-)

$ git branch -d issue-101
Deleted branch issue-101 (was 341417c).

$ git branch
  dev
* master

太棒了,bug已經修復。現在,是時候接着回到dev分支幹活了!

$ 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: b4537f9 add Test.txt

工作現場還在,Git把stash內容存在某個地方了,但是需要恢復一下,有兩個辦法:

  1. git stash apply 恢復,但是恢復後,stash 內容並不刪除,你需要用 git stash drop 來刪除;
  2. git stash pop,恢復的同時把stash內容也刪了:
$ git stash pop
On branch dev
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:   Test.txt

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (3425377dad942439e91506f60b72a3ab20d27e78)

再用 git stash list 查看,就看不到任何stash內容了:

$ git stash list

你可以多次stash,恢復的時候,先用 git stash list 查看,然後恢復指定的stash,用命令:

$ git stash apply stash@{0}

小結:

  1. 修復bug時,我們會通過創建新的bug分支進行修復,然後合併,最後刪除;
  2. 當手頭工作沒有完成時,先把工作現場 git stash 一下,然後去修復bug,修復後,再 git stash pop ,回到工作現場。

5、Feature 分支

軟件開發中,總有無窮無盡的新的功能要不斷添加進來。

添加一個新功能時,你肯定不希望因爲一些實驗性質的代碼,把主分支搞亂了,所以,每添加一個新功能,最好新建一個feature分支,在上面開發,完成後,合併,最後,刪除該feature分支。

現在,你終於接到了一個新任務:開發代號爲 “god” 的新功能。

於是準備開發:

$ git checkout -b feature-god
Switched to a new branch 'feature-god'

5分鐘後,開發完畢(添加一個 “god.md” 文件,內容隨意添加):

$ git add god.md
$ git status
On branch feature-god
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   god.md

$ git commit -m "add feature god"
[feature-god b14b0f1] add feature god
 1 file changed, 2 insertions(+)
 create mode 100644 god.md

切回 master,準備合併:

$ git checkout master

一切順利的話,feature分支和bug分支是類似的,合併,然後刪除。

但是,就在此時,接到上級命令,因經費不足,新功能必須取消!

雖然白乾了,但是這個分支還是必須就地銷燬:

$ git branch -d feature-god
error: The branch 'feature-god' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-god'.

銷燬失敗。Git友情提醒,feature-god 分支還沒有被合併,如果刪除,將丟失掉修改,如果要強行刪除,需要使用命令 git branch -D feature-god

現在我們強行刪除:

git branch -D feature-god
Deleted branch feature-god (was b14b0f1).

終於刪除成功!

小結:

  1. 開發一個新feature,最好新建一個分支;
  2. 如果要丟棄一個沒有被合併過的分支,可以通過git branch -D <name>強行刪除。

6、多人協作

當你從遠程倉庫克隆時,實際上Git自動把本地的master分支和遠程的master分支對應起來了,並且,遠程倉庫的默認名稱是origin

要查看遠程庫的信息,用 git remote

$ git remote
origin

或者,用 git remote -v 顯示更詳細的信息:

$ git remote -v
origin  git@github.com:LiHongyao/GITProj.git (fetch)
origin  git@github.com:LiHongyao/GITProj.git (push)

上面顯示了可以抓取和推送的 origin 的地址。如果沒有推送權限,就看不到push的地址。

6.1、推送分支

推送分支,就是把該分支上的所有本地提交推送到遠程庫。推送時,要指定本地分支,這樣,Git就會把該分支推送到遠程庫對應的遠程分支上:

$ git push origin master

如果要推送其他分支,比如 dev,就改成:

$ git push origin dev

但是,並不是一定要把本地分支往遠程推送,那麼,哪些分支需要推送,哪些不需要呢?

  • master 分支是主分支,因此要時刻與遠程同步;
  • dev 分支是開發分支,團隊所有成員都需要在上面工作,所以也需要與遠程同步;
  • bug 分支只用於在本地修復bug,就沒必要推到遠程了,除非老闆要看看你每週到底修復了幾個bug;
  • feature分支是否推到遠程,取決於你是否和你的小夥伴合作在上面開發。

總之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,視你的心情而定!

6.2、抓取分支

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

現在,模擬一個你的小夥伴,可以在另一臺電腦(注意要把SSH Key添加到GitHub)或者同一臺電腦的另一個目錄下克隆:

$ git clone [email protected]:LiHongyao/GITProj.git
Cloning into 'GITProj'...
remote: Counting objects: 31, done.
remote: Compressing objects: 100% (16/16), done.
remote: Total 31 (delta 6), reused 27 (delta 5), pack-reused 0
Receiving objects: 100% (31/31), done.
Resolving deltas: 100% (6/6), done.

當你的小夥伴從遠程庫clone時,默認情況下,你的小夥伴只能看到本地的 master 分支。不信可以用 git branch 命令看看:

$ git branch
* master

現在,你的小夥伴要在 dev 分支上開發,就必須創建遠程 origindev 分支到本地,於是他用這個命令創建本地 dev 分支:

$ git checkout -b dev origin/dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
Switched to a new branch 'dev'

現在,他就可以在 dev上繼續修改,然後,時不時地把dev分支push到遠程:

$ git add  "README.md"
$ git commit -m "change README.md file"
[dev 4e078ab] change readme.md file
 1 file changed, 2 insertions(+)

$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 371 bytes | 371.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:LiHongyao/GITProj.git
   294f183..4e078ab  dev -> dev

你的小夥伴已經向 origin/dev 分支推送了他的提交,而碰巧你也對同樣的文件作了修改,並試圖推送:

$ git add "README.md"
$ git commit -m "change readme.md with main."
[dev 5ee4d4a] change readme.md with main.
 1 file changed, 1 insertion(+)
$ git push origin dev
To github.com:LiHongyao/GITProj.git
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to '[email protected]:LiHongyao/GITProj.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., '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
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:LiHongyao/GITProj
   294f183..4e078ab  dev        -> origin/dev
   3fa5948..294f183  master     -> origin/master
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 README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

這回 git pull 成功,但是合併有衝突,需要手動解決,解決的方法和分支管理中的 解決衝突完全一樣。解決後,提交,再push:

$ git add "README.md"
$ git commit -m "merge & fix README.md"
[dev e7f096d] merge & fix README.md

$ git push origin dev
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 515 bytes | 515.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To github.com:LiHongyao/GITProj.git
   4e078ab..e7f096d  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 branch-name origin/branch-name

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

小結

  1. 查看遠程庫信息,使用 git remote -v

  2. 本地新建的分支如果不推送到遠程,對其他人就是不可見的;

  3. 從本地推送分支,使用 git push origin branch-name,如果推送失敗,先用 git pull 抓取遠程的新提交;

  4. 在本地創建和遠程分支對應的分支,使用

    shell
    $ git checkout -b branch-name origin/branch-name

    本地和遠程分支的名稱最好一致;

  5. 建立本地分支和遠程分支的關聯,使用:

    $ git branch --set-upstream branch-name origin/branch-name

# 標籤管理

發佈一個版本時,我們通常先在版本庫中打一個標籤(tag),這樣,就唯一確定了打標籤時刻的版本。將來無論什麼時候,取某個標籤的版本,就是把那個打標籤的時刻的歷史版本取出來。所以,標籤也是版本庫的一個快照。

Git的標籤雖然是版本庫的快照,但其實它就是指向某個 commit 的指針(跟分支很像對不對?但是分支可以移動,標籤不能移動),所以,創建和刪除標籤都是瞬間完成的。

Git有commit,爲什麼還要引入tag?

“請把上週一的那個版本打包發佈,commit號是6a5819e…”

“一串亂七八糟的數字不好找!”

如果換一個辦法:

“請把上週一的那個版本打包發佈,版本號是v1.2”

“好的,按照tag v1.2查找commit就行!”

所以,tag就是一個讓人容易記住的有意義的名字,它跟某個 commit 綁在一起。

1、創建標籤

在Git中打標籤非常簡單,首先,切換到需要打標籤的分支上:

$ git branch
* dev
  master
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

然後,敲命令 git tag <name> 就可以打一個新標籤:

git tag v1.0

可以用命令 git tag 查看所有標籤:

$ git tag
v1.0

默認標籤是打在最新提交的 commit 上的。有時候,如果忘了打標籤,比如,現在已經是週五了,但應該在週一打的標籤沒有打,怎麼辦?

方法是找到歷史提交的 “commit id”,然後打上就可以了:

$ git log --pretty=oneline --abbrev-commit
294f183 (HEAD -> master, tag: v1.0, origin/master, origin/HEAD) add info.md file
b57f9b7 add gg.md
3fa5948 change
775e8c8 change Test.txt
0b325f6 merged bug fix 101
341417c fix bug 101
b4537f9 add Test.txt
c2b5888 merge with no-ff
a5191b6 add hello
8e7e3d8 conflict fixed
103a7f9 & simple
3bf80e1 And simple
e433476 branch test

比方說,要爲 “conflict fixed” 這次提交打標籤,它對應的 “commit id”“8e7e3d8” ,敲入命令:

$ git tag v0.8 8e7e3d8
$ git tag
v0.8
v1.0

注意:

標籤不是按時間順序列出,而是按字母排序的。

可以用 git show <tagname> 查看標籤信息:

$ git show v0.8
commit 8e7e3d84ff9a84188d6ef7c5167260f1ee4d8648 (tag: v0.8)
Merge: 103a7f9 3bf80e1
Author: LiHongyao <lihy_online@163.com>
Date:   Wed Jan 17 15:49:42 2018 +0800

    conflict fixed

diff --cc README.md
index e4b9dd8,fb7801c..bd85a0e
--- a/README.md
+++ b/README.md
@@@ -1,2 -1,2 +1,2 @@@
  # GITProj
- Creating a new branch & quick.
 -Creating a new branch is quick AND simple.
++Creating a new branch is quick and simple.

可以看到,v0.8 確實打在 “conflict fixed” 這次提交上。

還可以創建帶有說明的標籤,用 -a 指定標籤名,-m 指定說明文字:

$ git tag -a <tagName> -m <des> <commit id>

小結:

  1. 創建標籤 :git tag <tagName>
  2. 查看標籤:git tag
  3. 查看標籤信息:git show <tagName>

2、操作標籤

如果標籤打錯了,也可以刪除:

$ git tag -d v1.0
Deleted tag 'v1.0' (was 294f183)

因爲創建的標籤都只存儲在本地,不會自動推送到遠程。所以,打錯的標籤可以在本地安全刪除。

如果要推送某個標籤到遠程,使用命令 git push origin <tagname>

$ git push origin v0.8
Total 0 (delta 0), reused 0 (delta 0)
To github.com:LiHongyao/GITProj.git
 * [new tag]         v0.8 -> v0.8

或者,一次性推送全部尚未推送到遠程的本地標籤:

$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To github.com:LiHongyao/GITProj.git
 * [new tag]         v0.1 -> v0.1
 * [new tag]         v0.5 -> v0.5

如果標籤已經推送到遠程,要刪除遠程標籤就麻煩一點,先從本地刪除:

$ git tag -d v0.5
Deleted tag 'v0.5' (was 3bf80e1)

然後,從遠程刪除。刪除命令也是push,但是格式如下:

$ git push origin :refs/tags/v0.5
To github.com:LiHongyao/GITProj.git
 - [deleted]         v0.5

要看看是否真的從遠程庫刪除了標籤,可以登陸GitHub查看。

小結:

  • 命令:git push origin <tagname>可以推送一個本地標籤;
  • 命令:git push origin --tags可以推送全部未推送過的本地標籤;
  • 命令:git tag -d <tagname>可以刪除一個本地標籤;
  • 命令:git push origin :refs/tags/<tagname>可以刪除一個遠程標籤。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章