Git讓你從入門到精通,看這一篇就夠了!

李紅歐巴發表於2019-04-19

簡介

Git 是什麼?

Git 是一個開源的分散式版本控制系統。

什麼是版本控制?

版本控制是一種記錄一個或若干檔案內容變化,以便將來查閱特定版本修訂情況的系統。

什麼是分散式版本控制系統?

介紹分散式版本控制系統前,有必要先了解一下傳統的集中式版本控制系統。

集中化的版本控制系統,諸如 CVS,Subversion 等,都有一個單一的集中管理的伺服器,儲存所有檔案的修訂版本,而協同工作的人們都通過客戶端連到這臺伺服器,取出最新的檔案或者提交更新。

這麼做最顯而易見的缺點是中央伺服器的單點故障。如果當機一小時,那麼在這一小時內,誰都無法提交更新,也就無法協同工作。要是中央伺服器的磁碟發生故障,碰巧沒做備份,或者備份不夠及時,就會有丟失資料的風險。最壞的情況是徹底丟失整個專案的所有歷史更改記錄。

Git讓你從入門到精通,看這一篇就夠了!

分散式版本控制系統的客戶端並不只提取最新版本的檔案快照,而是把程式碼倉庫完整地映象下來。這麼一來,任何一處協同工作用的伺服器發生故障,事後都可以用任何一個映象出來的本地倉庫恢復。因為每一次的提取操作,實際上都是一次對程式碼倉庫的完整備份。

Git讓你從入門到精通,看這一篇就夠了!

為什麼使用 Git?

Git 是分散式的。這是 Git 和其它非分散式的版本控制系統,例如 svn,cvs 等,最核心的區別。分散式帶來以下好處:

工作時不需要聯網

首先,分散式版本控制系統根本沒有“中央伺服器”,每個人的電腦上都是一個完整的版本庫,這樣,你工作的時候,就不需要聯網了,因為版本庫就在你自己的電腦上。既然每個人電腦上都有一個完整的版本庫,那多個人如何協作呢?比方說你在自己電腦上改了檔案 A,你的同事也在他的電腦上改了檔案 A,這時,你們倆之間只需把各自的修改推送給對方,就可以互相看到對方的修改了。

更加安全

集中式版本控制系統,一旦中央伺服器出了問題,所有人都無法工作。

分散式版本控制系統,每個人電腦中都有完整的版本庫,所以某人的機器掛了,並不影響其它人。


本文的重點是你有沒有收穫與成長,其餘的都不重要,希望讀者們能謹記這一點。同時我經過多年的收藏目前也算收集到了一套完整的學習資料,希望對想成為架構師的朋友有一定的參考和幫助 需要更詳細思維導圖和以下資料的可以加一下技術交流分享群:“708 701 457”免費獲取

Git讓你從入門到精通,看這一篇就夠了!


安裝

Debian/Ubuntu 環境安裝

如果你使用的系統是 Debian/Ubuntu , 安裝命令為:

$ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \
> libz-dev libssl-dev
$ apt-get install git-core
$ git --version
git version 1.8.1.2
複製程式碼

Centos/RedHat 環境安裝

如果你使用的系統是 Centos/RedHat ,安裝命令為:

$ yum install curl-devel expat-devel gettext-devel \
> openssl-devel zlib-devel
$ yum -y install git-core
$ git --version
git version 1.7.1
複製程式碼

Windows 環境安裝

Git 官方下載地址下載 exe 安裝包。按照安裝嚮導安裝即可。

建議安裝 Git Bash 這個 git 的命令列工具。

Mac 環境安裝

Git 官方下載地址下載 mac 安裝包。按照安裝嚮導安裝即可。

配置

Git 自帶一個 git config 的工具來幫助設定控制 Git 外觀和行為的配置變數。 這些變數儲存在三個不同的位置:

  1. /etc/gitconfig 檔案: 包含系統上每一個使用者及他們倉庫的通用配置。 如果使用帶有 --system 選項的 git config時,它會從此檔案讀寫配置變數。
  2. \~/.gitconfig 或 \~/.config/git/config 檔案:只針對當前使用者。 可以傳遞 --global 選項讓 Git 讀寫此檔案。
  3. 當前使用倉庫的 Git 目錄中的 config 檔案(就是 .git/config):針對該倉庫。

每一個級別覆蓋上一級別的配置,所以 .git/config 的配置變數會覆蓋 /etc/gitconfig 中的配置變數。

在 Windows 系統中,Git 會查詢 $HOME 目錄下(一般情況下是 C:\Users\$USER)的 .gitconfig 檔案。 Git 同樣也會尋找 /etc/gitconfig 檔案,但只限於 MSys 的根目錄下,即安裝 Git 時所選的目標位置。

使用者資訊

當安裝完 Git 應該做的第一件事就是設定你的使用者名稱稱與郵件地址。 這樣做很重要,因為每一個 Git 的提交都會使用這些資訊,並且它會寫入到你的每一次提交中,不可更改:

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
複製程式碼

再次強調,如果使用了 --global 選項,那麼該命令只需要執行一次,因為之後無論你在該系統上做任何事情, Git 都會使用那些資訊。 當你想針對特定專案使用不同的使用者名稱稱與郵件地址時,可以在那個專案目錄下執行沒有 --global 選項的命令來配置。

很多 GUI 工具都會在第一次執行時幫助你配置這些資訊。

.gitignore

.gitignore 檔案可能從字面含義也不難猜出:這個檔案裡配置的檔案或目錄,會自動被 git 所忽略,不納入版本控制。

在日常開發中,我們的專案經常會產生一些臨時檔案,如編譯 Java 產生的 *.class 檔案,又或是 IDE 自動生成的隱藏目錄(Intellij 的 .idea 目錄、Eclipse 的 .settings 目錄等)等等。這些檔案或目錄實在沒必要納入版本管理。在這種場景下,你就需要用到 .gitignore 配置來過濾這些檔案或目錄。

配置的規則很簡單,也沒什麼可說的,看幾個例子,自然就明白了。

這裡推薦一下 Github 的開源專案:github.com/github/giti…

在這裡,你可以找到很多常用的模板,如:Java、Nodejs、C++ 的 .gitignore 模板等等。

原理

個人認為,對於 Git 這個版本工具,再不瞭解原理的情況下,直接去學習命令列,可能會一頭霧水。所以,本文特意將原理放在命令使用章節之前講解。

版本庫

當你一個專案到本地或建立一個 git 專案,專案目錄下會有一個隱藏的 .git 子目錄。這個目錄是 git 用來跟蹤管理版本庫的,千萬不要手動修改。

雜湊值

Git 中所有資料在儲存前都計算校驗和,然後以校驗和來引用。 這意味著不可能在 Git 不知情時更改任何檔案內容或目錄內容。 這個功能建構在 Git 底層,是構成 Git 哲學不可或缺的部分。 若你在傳送過程中丟失資訊或損壞檔案,Git 就能發現。

Git 用以計算校驗和的機制叫做 SHA-1 雜湊(hash,雜湊)。 這是一個由 40 個十六進位制字元(0-9 和 a-f)組成字串,基於 Git 中檔案的內容或目錄結構計算出來。 SHA-1 雜湊看起來是這樣:

24b9da6552252987aa493b52f8696cd6d3b00373
複製程式碼

Git 中使用這種雜湊值的情況很多,你將經常看到這種雜湊值。 實際上,Git 資料庫中儲存的資訊都是以檔案內容的雜湊值來索引,而不是檔名。

檔案狀態

在 GIt 中,你的檔案可能會處於三種狀態之一:

  • 已修改(modified) - 已修改表示修改了檔案,但還沒儲存到資料庫中。
  • 已暫存(staged) - 已暫存表示對一個已修改檔案的當前版本做了標記,使之包含在下次提交的快照中。
  • 已提交(committed) - 已提交表示資料已經安全的儲存在本地資料庫中。

工作區域

與檔案狀態對應的,不同狀態的檔案在 Git 中處於不同的工作區域。

  • 工作區(working) - 當你 git clone 一個專案到本地,相當於在本地克隆了專案的一個副本。工作區是對專案的某個版本獨立提取出來的內容。 這些從 Git 倉庫的壓縮資料庫中提取出來的檔案,放在磁碟上供你使用或修改。
  • 暫存區(staging) - 暫存區是一個檔案,儲存了下次將提交的檔案列表資訊,一般在 Git 倉庫目錄中。 有時候也被稱作`‘索引’',不過一般說法還是叫暫存區。
  • 本地倉庫(local) - 提交更新,找到暫存區域的檔案,將快照永久性儲存到 Git 本地倉庫。
  • 遠端倉庫(remote) - 以上幾個工作區都是在本地。為了讓別人可以看到你的修改,你需要將你的更新推送到遠端倉庫。同理,如果你想同步別人的修改,你需要從遠端倉庫拉取更新。

Git讓你從入門到精通,看這一篇就夠了!

命令

國外網友製作了一張 Git Cheat Sheet,總結很精煉,各位不妨收藏一下。

本節選擇性介紹 git 中比較常用的命令列場景。

Git讓你從入門到精通,看這一篇就夠了!

建立倉庫

克隆一個已建立的倉庫:

# 通過 SSH
$ git clone ssh://user@domain.com/repo.git

#通過 HTTP
$ git clone http://domain.com/user/repo.git
複製程式碼

建立一個新的本地倉庫:

$ git init
複製程式碼

新增修改

新增修改到暫存區:

# 把指定檔案新增到暫存區
$ git add xxx

# 把當前所有修改新增到暫存區
$ git add .

# 把所有修改新增到暫存區
$ git add -A
複製程式碼

提交修改到本地倉庫:

# 提交本地的所有修改
$ git commit -a

# 提交之前已標記的變化
$ git commit

# 附加訊息提交
$ git commit -m 'commit message'
複製程式碼

儲藏

有時,我們需要在同一個專案的不同分支上工作。當需要切換分支時,偏偏本地的工作還沒有完成,此時,提交修改顯得不嚴謹,但是不提交程式碼又無法切換分支。這時,你可以使用 git stash 將本地的修改內容作為草稿儲藏起來。

官方稱之為儲藏,但我個人更喜歡稱之為存草稿。

# 1\. 將修改作為當前分支的草稿儲存
$ git stash

# 2\. 檢視草稿列表
$ git stash list
stash@{0}: WIP on master: 6fae349 :memo: Writing docs.

# 3.1 刪除草稿
$ git stash drop stash@{0}

# 3.2 讀取草稿
$ git stash apply stash@{0}
複製程式碼

撤銷修改

撤銷本地修改:

# 移除快取區的所有檔案(i.e. 撤銷上次git add)
$ git reset HEAD

# 將HEAD重置到上一次提交的版本,並將之後的修改標記為未新增到快取區的修改
$ git reset <commit>

# 將HEAD重置到上一次提交的版本,並保留未提交的本地修改
$ git reset --keep <commit>

# 放棄工作目錄下的所有修改
$ git reset --hard HEAD

# 將HEAD重置到指定的版本,並拋棄該版本之後的所有修改
$ git reset --hard <commit-hash>

# 用遠端分支強制覆蓋本地分支
$ git reset --hard <remote/branch> e.g., upstream/master, origin/my-feature

# 放棄某個檔案的所有本地修改
$ git checkout HEAD <file>
複製程式碼

刪除新增.gitignore檔案前錯誤提交的檔案:

$ git rm -r --cached .
$ git add .
$ git commit -m "remove xyz file"
複製程式碼

撤銷遠端修改(建立一個新的提交,並回滾到指定版本):

$ git revert <commit-hash>
複製程式碼

徹底刪除指定版本:

# 執行下面命令後,commit-hash 提交後的記錄都會被徹底刪除,使用需謹慎
$ git reset --hard <commit-hash>
$ git push -f
複製程式碼

更新與推送

更新:

# 下載遠端端版本,但不合併到HEAD中
$ git fetch <remote>

# 將遠端端版本合併到本地版本中
$ git pull origin master

# 以rebase方式將遠端分支與本地合併
$ git pull --rebase <remote> <branch>
複製程式碼

推送:

# 將本地版本推送到遠端端
$ git push remote <remote> <branch>

# 刪除遠端端分支
$ git push <remote> :<branch> (since Git v1.5.0)
$ git push <remote> --delete <branch> (since Git v1.7.0)

# 釋出標籤
$ git push --tags
複製程式碼

檢視資訊

顯示工作路徑下已修改的檔案:

$ git status
複製程式碼

顯示與上次提交版本檔案的不同:

$ git diff
複製程式碼

顯示提交歷史:

# 從最新提交開始,顯示所有的提交記錄(顯示hash, 作者資訊,提交的標題和時間)
$ git log

# 顯示某個使用者的所有提交
$ git log --author="username"

# 顯示某個檔案的所有修改
$ git log -p <file>
複製程式碼

顯示搜尋內容:

# 從當前目錄的所有檔案中查詢文字內容
$ git grep "Hello"

# 在某一版本中搜尋文字
$ git grep "Hello" v2.5
複製程式碼

分支

增刪查分支:

# 列出所有的分支
$ git branch

# 列出所有的遠端分支
$ git branch -r

# 基於當前分支建立新分支
$ git branch <new-branch>

# 基於遠端分支建立新的可追溯的分支
$ git branch --track <new-branch> <remote-branch>

# 刪除本地分支
$ git branch -d <branch>

# 強制刪除本地分支,將會丟失未合併的修改
$ git branch -D <branch>
複製程式碼

切換分支:

# 切換分支
$ git checkout <branch>

# 建立並切換到新分支
$ git checkout -b <branch>
複製程式碼

標籤

# 給當前版本打標籤
$ git tag <tag-name>

# 給當前版本打標籤並附加訊息
$ git tag -a <tag-name>
複製程式碼

合併與重置

merge 與 rebase 雖然是 git 常用功能,但是強烈建議不要使用 git 命令來完成這項工作。

因為如果出現程式碼衝突,在沒有程式碼比對工具的情況下,實在太艱難了。

你可以考慮使用各種 Git GUI 工具。

合併:

# 將分支合併到當前HEAD中
$ git merge <branch>
複製程式碼

重置:

# 將當前HEAD版本重置到分支中,請勿重置已釋出的提交
$ git rebase <branch>
複製程式碼

Github

Github 作為最著名的程式碼開源協作社群,在程式設計師圈想必無人不知,無人不曉。

這裡不贅述 Github 的用法,確實有不會用的新手同學,可以參考官方教程:guides.github.com/

clone 方式

Git 支援三種協議:HTTPS / SSH / GIT

而 Github 上支援 HTTPS 和 SSH。

HTTPS 這種方式要求你每次 push 時都要輸入使用者名稱、密碼,有些繁瑣。

而 SSH 要求你本地生成證照,然後在你的 Github 賬戶中註冊。第一次配置麻煩是麻煩了點,但是以後就免去了每次 push 需要輸入使用者名稱、密碼的繁瑣。

Git讓你從入門到精通,看這一篇就夠了!

以下介紹以下,如何生成證照,以及在 Github 中註冊。

生成 SSH 公鑰

如前所述,許多 Git 伺服器都使用 SSH 公鑰進行認證。 為了向 Git 伺服器提供 SSH 公鑰,如果某系統使用者尚未擁有金鑰,必須事先為其生成一份。 這個過程在所有作業系統上都是相似的。 首先,你需要確認自己是否已經擁有金鑰。 預設情況下,使用者的 SSH 金鑰儲存在其 \~/.ssh 目錄下。 進入該目錄並列出其中內容,你便可以快速確認自己是否已擁有金鑰:

$ cd ~/.ssh
$ ls
authorized_keys2  id_dsa       known_hosts
config            id_dsa.pub
複製程式碼

我們需要尋找一對以 id_dsa 或 id_rsa 命名的檔案,其中一個帶有 .pub 副檔名。 .pub 檔案是你的公鑰,另一個則是私鑰。 如果找不到這樣的檔案(或者根本沒有 .ssh 目錄),你可以通過執行 ssh-keygen 程式來建立它們。在 Linux/Mac 系統中,ssh-keygen 隨 SSH 軟體包提供;在 Windows 上,該程式包含於 MSysGit 軟體包中。

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/schacon/.ssh/id_rsa):
Created directory '/home/schacon/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/schacon/.ssh/id_rsa.
Your public key has been saved in /home/schacon/.ssh/id_rsa.pub.
The key fingerprint is:
d0:82:24:8e:d7:f1:bb:9b:33:53:96:93:49:da:9b:e3 schacon@mylaptop.local
複製程式碼

首先 ssh-keygen 會確認金鑰的儲存位置(預設是 .ssh/id_rsa),然後它會要求你輸入兩次金鑰口令。如果你不想在使用金鑰時輸入口令,將其留空即可。

現在,進行了上述操作的使用者需要將各自的公鑰傳送給任意一個 Git 伺服器管理員(假設伺服器正在使用基於公鑰的 SSH 驗證設定)。 他們所要做的就是複製各自的 .pub 檔案內容,並將其通過郵件傳送。 公鑰看起來是這樣的:

$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
NrRFi9wrf+M7Q== schacon@mylaptop.local
複製程式碼

在你的 Github 賬戶中,依次點選 Settings > SSH and GPG keys > New SSH key

然後,將上面生成的公鑰內容貼上到 Key 編輯框並儲存。至此大功告成。

後面,你在克隆你的 Github 專案時使用 SSH 方式即可。

如果覺得我的講解還不夠細緻,可以參考:help.github.com/articles/ad…

最佳實踐 Git Flow

Git 在實際開發中的最佳實踐策略 Git Flow 可以歸納為以下:

  • master 分支 - 也就是我們經常使用的主線分支,這個分支是最近釋出到生產環境的程式碼,這個分支只能從其他分支合併,不能在這個分支直接修改。
  • develop 分支 - 這個分支是我們的主開發分支,包含所有要釋出到下一個 release 的程式碼,這個分支主要是從其他分支合併程式碼過來,比如 feature 分支。
  • feature 分支 - 這個分支主要是用來開發一個新的功能,一旦開發完成,我們合併回 develop 分支進入下一個 release。
  • release 分支 - 當你需要一個釋出一個新 release 的時候,我們基於 Develop 分支建立一個 release 分支,完成 release 後,我們合併到 master 和 develop 分支。
  • hotfix 分支 - 當我們在 master 發現新的 Bug 時候,我們需要建立一個 hotfix, 完成 hotfix 後,我們合併回 master 和 develop 分支,所以 hotfix 的改動會進入下一個 release。

常見問題

編輯提交(editting commits)

我剛才提交了什麼

如果你用 git commit -a 提交了一次變化(changes),而你又不確定到底這次提交了哪些內容。 你就可以用下面的命令顯示當前HEAD上的最近一次的提交(commit):

(master)$ git show
複製程式碼

或者

$ git log -n1 -p
複製程式碼

我的提交資訊(commit message)寫錯了

如果你的提交資訊(commit message)寫錯了且這次提交(commit)還沒有推(push), 你可以通過下面的方法來修改提交資訊(commit message):

$ git commit --amend
複製程式碼

這會開啟你的預設編輯器, 在這裡你可以編輯資訊. 另一方面, 你也可以用一條命令一次完成:

$ git commit --amend -m 'xxxxxxx'
複製程式碼

如果你已經推(push)了這次提交(commit), 你可以修改這次提交(commit)然後強推(force push), 但是不推薦這麼做。

我提交(commit)裡的使用者名稱和郵箱不對

如果這只是單個提交(commit),修改它:

$ git commit --amend --author "New Authorname <authoremail@mydomain.com>"
複製程式碼

如果你需要修改所有歷史, 參考 'git filter-branch'的指南頁.

我想從一個提交(commit)裡移除一個檔案

通過下面的方法,從一個提交(commit)裡移除一個檔案:

$ git checkout HEAD^ myfile
$ git add -A
$ git commit --amend
複製程式碼

這將非常有用,當你有一個開放的補丁(open patch),你往上面提交了一個不必要的檔案,你需要強推(force push)去更新這個遠端補丁。

我想刪除我的的最後一次提交(commit)

如果你需要刪除推了的提交(pushed commits),你可以使用下面的方法。可是,這會不可逆的改變你的歷史,也會搞亂那些已經從該倉庫拉取(pulled)了的人的歷史。簡而言之,如果你不是很確定,千萬不要這麼做。

$ git reset HEAD^ --hard
$ git push -f [remote] [branch]
複製程式碼

如果你還沒有推到遠端, 把 Git 重置(reset)到你最後一次提交前的狀態就可以了(同時儲存暫存的變化):

(my-branch*)$ git reset --soft HEAD@{1}
複製程式碼

這隻能在沒有推送之前有用. 如果你已經推了, 唯一安全能做的是 git revert SHAofBadCommit, 那會建立一個新的提交(commit)用於撤消前一個提交的所有變化(changes); 或者, 如果你推的這個分支是 rebase-safe 的 (例如: 其它開發者不會從這個分支拉), 只需要使用 git push -f; 更多, 請參考 the above section

刪除任意提交(commit)

同樣的警告:不到萬不得已的時候不要這麼做.

$ git rebase --onto SHA1_OF_BAD_COMMIT^ SHA1_OF_BAD_COMMIT
$ git push -f [remote] [branch]
複製程式碼

或者做一個 互動式 rebase 刪除那些你想要刪除的提交(commit)裡所對應的行。

我嘗試推一個修正後的提交(amended commit)到遠端,但是報錯:

To https://github.com/yourusername/repo.git
! [rejected]        mybranch -> mybranch (non-fast-forward)
error: failed to push some refs to 'https://github.com/tanay1337/webmaker.org.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.
複製程式碼

注意, rebasing(見下面)和修正(amending)會用一個新的提交(commit)代替舊的, 所以如果之前你已經往遠端倉庫上推過一次修正前的提交(commit),那你現在就必須強推(force push) (-f)。 注意 – 總是 確保你指明一個分支!

(my-branch)$ git push origin mybranch -f
複製程式碼

一般來說, 要避免強推. 最好是建立和推(push)一個新的提交(commit),而不是強推一個修正後的提交。後者會使那些與該分支或該分支的子分支工作的開發者,在源歷史中產生衝突。

我意外的做了一次硬重置(hard reset),我想找回我的內容

如果你意外的做了 git reset --hard, 你通常能找回你的提交(commit), 因為 Git 對每件事都會有日誌,且都會儲存幾天。

(master)$ git reflog
複製程式碼

你將會看到一個你過去提交(commit)的列表, 和一個重置的提交。 選擇你想要回到的提交(commit)的 SHA,再重置一次:

(master)$ git reset --hard SHA1234
複製程式碼

這樣就完成了。

暫存(Staging)

我需要把暫存的內容新增到上一次的提交(commit)

(my-branch*)$ git commit --amend
複製程式碼

我想要暫存一個新檔案的一部分,而不是這個檔案的全部

一般來說, 如果你想暫存一個檔案的一部分, 你可這樣做:

$ git add --patch filename.x
複製程式碼

-p 簡寫。這會開啟互動模式, 你將能夠用 s 選項來分隔提交(commit); 然而, 如果這個檔案是新的, 會沒有這個選擇, 新增一個新檔案時, 這樣做:

$ git add -N filename.x
複製程式碼

然後, 你需要用 e 選項來手動選擇需要新增的行,執行 git diff --cached 將會顯示哪些行暫存了哪些行只是儲存在本地了。

我想把在一個檔案裡的變化(changes)加到兩個提交(commit)裡

git add 會把整個檔案加入到一個提交. git add -p 允許互動式的選擇你想要提交的部分.

我想把暫存的內容變成未暫存,把未暫存的內容暫存起來

這個有點困難, 我能想到的最好的方法是先 stash 未暫存的內容, 然後重置(reset),再 pop 第一步 stashed 的內容, 最後再 add 它們。

$ git stash -k
$ git reset --hard
$ git stash pop
$ git add -A
複製程式碼

未暫存(Unstaged)的內容

我想把未暫存的內容移動到一個新分支

$ git checkout -b my-branch
複製程式碼

我想把未暫存的內容移動到另一個已存在的分支

$ git stash
$ git checkout my-branch
$ git stash pop
複製程式碼

我想丟棄本地未提交的變化(uncommitted changes)

如果你只是想重置源(origin)和你本地(local)之間的一些提交(commit),你可以:

## one commit
(my-branch)$ git reset --hard HEAD^
## two commits
(my-branch)$ git reset --hard HEAD^^
## four commits
(my-branch)$ git reset --hard HEAD~4
## or
(master)$ git checkout -f
複製程式碼

重置某個特殊的檔案, 你可以用檔名做為引數:

$ git reset filename
複製程式碼

我想丟棄某些未暫存的內容

如果你想丟棄工作拷貝中的一部分內容,而不是全部。

簽出(checkout)不需要的內容,保留需要的。

$ git checkout -p
## Answer y to all of the snippets you want to drop
複製程式碼

另外一個方法是使用 stash, Stash 所有要保留下的內容, 重置工作拷貝, 重新應用保留的部分。

$ git stash -p
## Select all of the snippets you want to save
$ git reset --hard
$ git stash pop
複製程式碼

或者, stash 你不需要的部分, 然後 stash drop。

$ git stash -p
## Select all of the snippets you don't want to save
$ git stash drop
複製程式碼

分支(Branches)

我從錯誤的分支拉取了內容,或把內容拉取到了錯誤的分支

這是另外一種使用 git reflog 情況,找到在這次錯誤拉(pull) 之前 HEAD 的指向。

(master)$ git reflog
ab7555f HEAD@{0}: pull origin wrong-branch: Fast-forward
c5bc55a HEAD@{1}: checkout: checkout message goes here
複製程式碼

重置分支到你所需的提交(desired commit):

$ git reset --hard c5bc55a
複製程式碼

完成。

我想扔掉本地的提交(commit),以便我的分支與遠端的保持一致

先確認你沒有推(push)你的內容到遠端。

git status 會顯示你領先(ahead)源(origin)多少個提交:

(my-branch)$ git status
## On branch my-branch
## Your branch is ahead of 'origin/my-branch' by 2 commits.
##   (use "git push" to publish your local commits)
#
複製程式碼

一種方法是:

(master)$ git reset --hard origin/my-branch
複製程式碼

我需要提交到一個新分支,但錯誤的提交到了 master

在 master 下建立一個新分支,不切換到新分支,仍在 master 下:

(master)$ git branch my-branch
複製程式碼

把 master 分支重置到前一個提交:

(master)$ git reset --hard HEAD^
複製程式碼

HEAD^ 是 HEAD^1 的簡寫,你可以通過指定要設定的HEAD來進一步重置。

或者, 如果你不想使用 HEAD^, 找到你想重置到的提交(commit)的 hash(git log 能夠完成), 然後重置到這個 hash。 使用git push 同步內容到遠端。

例如, master 分支想重置到的提交的 hash 為a13b85e:

(master)$ git reset --hard a13b85e
HEAD is now at a13b85e
複製程式碼

簽出(checkout)剛才新建的分支繼續工作:

(master)$ git checkout my-branch
複製程式碼

我想保留來自另外一個 ref-ish 的整個檔案

假設你正在做一個原型方案(原文為 working spike (see note)), 有成百的內容,每個都工作得很好。現在, 你提交到了一個分支,儲存工作內容:

(solution)$ git add -A && git commit -m "Adding all changes from this spike into one big commit."
複製程式碼

當你想要把它放到一個分支裡 (可能是feature, 或者 develop), 你關心是保持整個檔案的完整,你想要一個大的提交分隔成比較小。

假設你有:

  • 分支 solution, 擁有原型方案, 領先 develop 分支。
  • 分支 develop, 在這裡你應用原型方案的一些內容。

我去可以通過把內容拿到你的分支裡,來解決這個問題:

(develop)$ git checkout solution -- file1.txt
複製程式碼

這會把這個檔案內容從分支 solution 拿到分支 develop 裡來:

## On branch develop
## Your branch is up-to-date with 'origin/develop'.
## Changes to be committed:
##  (use "git reset HEAD <file>..." to unstage)
#
##        modified:   file1.txt
複製程式碼

然後, 正常提交。

Note: Spike solutions are made to analyze or solve the problem. These solutions are used for estimation and discarded once everyone gets clear visualization of the problem. ~ Wikipedia.

我把幾個提交(commit)提交到了同一個分支,而這些提交應該分佈在不同的分支裡

假設你有一個master分支, 執行git log, 你看到你做過兩次提交:

(master)$ git log

commit e3851e817c451cc36f2e6f3049db528415e3c114
Author: Alex Lee <alexlee@example.com>
Date:   Tue Jul 22 15:39:27 2014 -0400

    Bug #21 - Added CSRF protection

commit 5ea51731d150f7ddc4a365437931cd8be3bf3131
Author: Alex Lee <alexlee@example.com>
Date:   Tue Jul 22 15:39:12 2014 -0400

    Bug #14 - Fixed spacing on title

commit a13b85e984171c6e2a1729bb061994525f626d14
Author: Aki Rose <akirose@example.com>
Date:   Tue Jul 21 01:12:48 2014 -0400

    First commit
複製程式碼

讓我們用提交 hash(commit hash)標記 bug (e3851e8 for #21, 5ea5173 for #14).

首先, 我們把master分支重置到正確的提交(a13b85e):

(master)$ git reset --hard a13b85e
HEAD is now at a13b85e
複製程式碼

現在, 我們對 bug #21 建立一個新的分支:

(master)$ git checkout -b 21
(21)$
複製程式碼

接著, 我們用 cherry-pick 把對 bug #21 的提交放入當前分支。 這意味著我們將應用(apply)這個提交(commit),僅僅這一個提交(commit),直接在 HEAD 上面。

(21)$ git cherry-pick e3851e8
複製程式碼

這時候, 這裡可能會產生衝突, 參見互動式 rebasing 章 衝突節 解決衝突.

再者, 我們為 bug #14 建立一個新的分支, 也基於master分支

(21)$ git checkout master
(master)$ git checkout -b 14
(14)$
複製程式碼

最後, 為 bug #14 執行 cherry-pick:

(14)$ git cherry-pick 5ea5173
複製程式碼

我想刪除上游(upstream)分支被刪除了的本地分支

一旦你在 github 上面合併(merge)了一個 pull request, 你就可以刪除你 fork 裡被合併的分支。 如果你不準備繼續在這個分支裡工作, 刪除這個分支的本地拷貝會更乾淨,使你不會陷入工作分支和一堆陳舊分支的混亂之中。

$ git fetch -p
複製程式碼

我不小心刪除了我的分支

如果你定期推送到遠端, 多數情況下應該是安全的,但有些時候還是可能刪除了還沒有推到遠端的分支。 讓我們先建立一個分支和一個新的檔案:

(master)$ git checkout -b my-branch
(my-branch)$ git branch
(my-branch)$ touch foo.txt
(my-branch)$ ls
README.md foo.txt
複製程式碼

新增檔案並做一次提交

(my-branch)$ git add .
(my-branch)$ git commit -m 'foo.txt added'
(my-branch)$ foo.txt added
 1 files changed, 1 insertions(+)
 create mode 100644 foo.txt
(my-branch)$ git log

commit 4e3cd85a670ced7cc17a2b5d8d3d809ac88d5012
Author: siemiatj <siemiatj@example.com>
Date:   Wed Jul 30 00:34:10 2014 +0200

    foo.txt added

commit 69204cdf0acbab201619d95ad8295928e7f411d5
Author: Kate Hudson <katehudson@example.com>
Date:   Tue Jul 29 13:14:46 2014 -0400

    Fixes #6: Force pushing after amending commits
複製程式碼

現在我們切回到主(master)分支,‘不小心的’刪除my-branch分支

(my-branch)$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
(master)$ git branch -D my-branch
Deleted branch my-branch (was 4e3cd85).
(master)$ echo oh noes, deleted my branch!
oh noes, deleted my branch!
複製程式碼

在這時候你應該想起了reflog, 一個升級版的日誌,它儲存了倉庫(repo)裡面所有動作的歷史。

(master)$ git reflog
69204cd HEAD@{0}: checkout: moving from my-branch to master
4e3cd85 HEAD@{1}: commit: foo.txt added
69204cd HEAD@{2}: checkout: moving from master to my-branch
複製程式碼

正如你所見,我們有一個來自刪除分支的提交 hash(commit hash),接下來看看是否能恢復刪除了的分支。

(master)$ git checkout -b my-branch-help
Switched to a new branch 'my-branch-help'
(my-branch-help)$ git reset --hard 4e3cd85
HEAD is now at 4e3cd85 foo.txt added
(my-branch-help)$ ls
README.md foo.txt
複製程式碼

看! 我們把刪除的檔案找回來了。 Git 的 reflog 在 rebasing 出錯的時候也是同樣有用的。

我想刪除一個分支

刪除一個遠端分支:

(master)$ git push origin --delete my-branch
複製程式碼

你也可以:

(master)$ git push origin :my-branch
複製程式碼

刪除一個本地分支:

(master)$ git branch -D my-branch
複製程式碼

我想從別人正在工作的遠端分支簽出(checkout)一個分支

首先, 從遠端拉取(fetch) 所有分支:

(master)$ git fetch --all
複製程式碼

假設你想要從遠端的daves分支簽出到本地的daves

(master)$ git checkout --track origin/daves
Branch daves set up to track remote branch daves from origin.
Switched to a new branch 'daves'
複製程式碼

(--track 是 git checkout -b [branch] [remotename]/[branch] 的簡寫)

這樣就得到了一個daves分支的本地拷貝, 任何推過(pushed)的更新,遠端都能看到.

Rebasing 和合並(Merging)

我想撤銷 rebase/merge

你可以合併(merge)或 rebase 了一個錯誤的分支, 或者完成不了一個進行中的 rebase/merge。 Git 在進行危險操作的時候會把原始的 HEAD 儲存在一個叫 ORIG_HEAD 的變數裡, 所以要把分支恢復到 rebase/merge 前的狀態是很容易的。

(my-branch)$ git reset --hard ORIG_HEAD
複製程式碼

我已經 rebase 過, 但是我不想強推(force push)

不幸的是,如果你想把這些變化(changes)反應到遠端分支上,你就必須得強推(force push)。 是因你快進(Fast forward)了提交,改變了 Git 歷史, 遠端分支不會接受變化(changes),除非強推(force push)。這就是許多人使用 merge 工作流, 而不是 rebasing 工作流的主要原因之一, 開發者的強推(force push)會使大的團隊陷入麻煩。使用時需要注意,一種安全使用 rebase 的方法是,不要把你的變化(changes)反映到遠端分支上, 而是按下面的做:

(master)$ git checkout my-branch
(my-branch)$ git rebase -i master
(my-branch)$ git checkout master
(master)$ git merge --ff-only my-branch
複製程式碼

更多, 參見 this SO thread.

我需要組合(combine)幾個提交(commit)

假設你的工作分支將會做對於 master 的 pull-request。 一般情況下你不關心提交(commit)的時間戳,只想組合 所有 提交(commit) 到一個單獨的裡面, 然後重置(reset)重提交(recommit)。 確保主(master)分支是最新的和你的變化都已經提交了, 然後:

(my-branch)$ git reset --soft master
(my-branch)$ git commit -am "New awesome feature"
複製程式碼

如果你想要更多的控制, 想要保留時間戳, 你需要做互動式 rebase (interactive rebase):

(my-branch)$ git rebase -i master
複製程式碼

如果沒有相對的其它分支, 你將不得不相對自己的HEAD 進行 rebase。 例如:你想組合最近的兩次提交(commit), 你將相對於HEAD\~2 進行 rebase, 組合最近 3 次提交(commit), 相對於HEAD\~3, 等等。

(master)$ git rebase -i HEAD~2
複製程式碼

在你執行了互動式 rebase 的命令(interactive rebase command)後, 你將在你的編輯器裡看到類似下面的內容:

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
pick b729ad5 fixup
pick e3851e8 another fix

## Rebase 8074d12..b729ad5 onto 8074d12
#
## Commands:
##  p, pick = use commit
##  r, reword = use commit, but edit the commit message
##  e, edit = use commit, but stop for amending
##  s, squash = use commit, but meld into previous commit
##  f, fixup = like "squash", but discard this commit's log message
##  x, exec = run command (the rest of the line) using shell
#
## These lines can be re-ordered; they are executed from top to bottom.
#
## If you remove a line here THAT COMMIT WILL BE LOST.
#
## However, if you remove everything, the rebase will be aborted.
#
## Note that empty commits are commented out
複製程式碼

所有以 # 開頭的行都是註釋, 不會影響 rebase.

然後,你可以用任何上面命令列表的命令替換 pick, 你也可以通過刪除對應的行來刪除一個提交(commit)。

例如, 如果你想 單獨保留最舊(first)的提交(commit),組合所有剩下的到第二個裡面, 你就應該編輯第二個提交(commit)後面的每個提交(commit) 前的單詞為 f:

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
f b729ad5 fixup
f e3851e8 another fix
複製程式碼

如果你想組合這些提交(commit) 並重新命名這個提交(commit), 你應該在第二個提交(commit)旁邊新增一個r,或者更簡單的用s 替代 f:

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
s b729ad5 fixup
s e3851e8 another fix
複製程式碼

你可以在接下來彈出的文字提示框裡重新命名提交(commit)。

Newer, awesomer features

## Please enter the commit message for your changes. Lines starting
## with '#' will be ignored, and an empty message aborts the commit.
## rebase in progress; onto 8074d12
## You are currently editing a commit while rebasing branch 'master' on '8074d12'.
#
## Changes to be committed:
#	modified:   README.md
#

複製程式碼

如果成功了, 你應該看到類似下面的內容:

(master)$ Successfully rebased and updated refs/heads/master.
複製程式碼
安全合併(merging)策略

--no-commit 執行合併(merge)但不自動提交, 給使用者在做提交前檢查和修改的機會。 no-ff 會為特性分支(feature branch)的存在過留下證據, 保持專案歷史一致。

(master)$ git merge --no-ff --no-commit my-branch
複製程式碼
我需要將一個分支合併成一個提交(commit)
(master)$ git merge --squash my-branch
複製程式碼
我只想組合(combine)未推的提交(unpushed commit)

有時候,在將資料推向上游之前,你有幾個正在進行的工作提交(commit)。這時候不希望把已經推(push)過的組合進來,因為其他人可能已經有提交(commit)引用它們了。

(master)$ git rebase -i @{u}
複製程式碼

這會產生一次互動式的 rebase(interactive rebase), 只會列出沒有推(push)的提交(commit), 在這個列表時進行 reorder/fix/squash 都是安全的。

檢查是否分支上的所有提交(commit)都合併(merge)過了

檢查一個分支上的所有提交(commit)是否都已經合併(merge)到了其它分支, 你應該在這些分支的 head(或任何 commits)之間做一次 diff:

(master)$ git log --graph --left-right --cherry-pick --oneline HEAD...feature/120-on-scroll
複製程式碼

這會告訴你在一個分支裡有而另一個分支沒有的所有提交(commit), 和分支之間不共享的提交(commit)的列表。 另一個做法可以是:

(master)$ git log master ^feature/120-on-scroll --no-merges
複製程式碼

互動式 rebase(interactive rebase)可能出現的問題

這個 rebase 編輯螢幕出現'noop'

如果你看到的是這樣:

noop

複製程式碼

這意味著你 rebase 的分支和當前分支在同一個提交(commit)上, 或者 領先(ahead) 當前分支。 你可以嘗試:

  • 檢查確保主(master)分支沒有問題
  • rebase HEAD\~2 或者更早
有衝突的情況

如果你不能成功的完成 rebase, 你可能必須要解決衝突。

首先執行 git status 找出哪些檔案有衝突:

(my-branch)$ git status
On branch my-branch
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
複製程式碼

在這個例子裡面, README.md 有衝突。 開啟這個檔案找到類似下面的內容:

   <<<<<<< HEAD
   some code
   =========
   some code
   >>>>>>> new-commit
複製程式碼

你需要解決新提交的程式碼(示例裡, 從中間==線到new-commit的地方)與HEAD 之間不一樣的地方.

有時候這些合併非常複雜,你應該使用視覺化的差異編輯器(visual diff editor):

(master*)$ git mergetool -t opendiff
複製程式碼

在你解決完所有衝突和測試過後, git add 變化了的(changed)檔案, 然後用git rebase --continue 繼續 rebase。

(my-branch)$ git add README.md
(my-branch)$ git rebase --continue
複製程式碼

如果在解決完所有的衝突過後,得到了與提交前一樣的結果, 可以執行git rebase --skip

任何時候你想結束整個 rebase 過程,回來 rebase 前的分支狀態, 你可以做:

(my-branch)$ git rebase --abort
複製程式碼

雜項(Miscellaneous Objects)

克隆所有子模組

$ git clone --recursive git://github.com/foo/bar.git
複製程式碼

如果已經克隆了:

$ git submodule update --init --recursive
複製程式碼

刪除標籤(tag)

$ git tag -d <tag_name>
$ git push <remote> :refs/tags/<tag_name>
複製程式碼

恢復已刪除標籤(tag)

如果你想恢復一個已刪除標籤(tag), 可以按照下面的步驟: 首先, 需要找到無法訪問的標籤(unreachable tag):

$ git fsck --unreachable | grep tag
複製程式碼

記下這個標籤(tag)的 hash,然後用 Git 的 update-ref:

$ git update-ref refs/tags/<tag_name> <hash>
複製程式碼

這時你的標籤(tag)應該已經恢復了。

已刪除補丁(patch)

如果某人在 GitHub 上給你發了一個 pull request, 但是然後他刪除了他自己的原始 fork, 你將沒法克隆他們的提交(commit)或使用 git am。在這種情況下, 最好手動的檢視他們的提交(commit),並把它們拷貝到一個本地新分支,然後做提交。

做完提交後, 再修改作者,參見變更作者。 然後, 應用變化, 再發起一個新的 pull request。

跟蹤檔案(Tracking Files)

我只想改變一個檔名字的大小寫,而不修改內容

(master)$ git mv --force myfile MyFile
複製程式碼

我想從 Git 刪除一個檔案,但保留該檔案

(master)$ git rm --cached log.txt
複製程式碼

配置(Configuration)

我想給一些 Git 命令新增別名(alias)

在 OS X 和 Linux 下, 你的 Git 的配置檔案儲存在 \~/.gitconfig。我在[alias] 部分新增了一些快捷別名(和一些我容易拼寫錯誤的),如下:

[alias]
    a = add
    amend = commit --amend
    c = commit
    ca = commit --amend
    ci = commit -a
    co = checkout
    d = diff
    dc = diff --changed
    ds = diff --staged
    f = fetch
    loll = log --graph --decorate --pretty=oneline --abbrev-commit
    m = merge
    one = log --pretty=oneline
    outstanding = rebase -i @{u}
    s = status
    unpushed = log @{u}
    wc = whatchanged
    wip = rebase -i @{u}
    zap = fetch -p
複製程式碼

我想快取一個倉庫(repository)的使用者名稱和密碼

你可能有一個倉庫需要授權,這時你可以快取使用者名稱和密碼,而不用每次推/拉(push/pull)的時候都輸入,Credential helper 能幫你。

$ git config --global credential.helper cache
## Set git to use the credential memory cache
複製程式碼
$ git config --global credential.helper 'cache --timeout=3600'
## Set the cache to timeout after 1 hour (setting is in seconds)
複製程式碼

我不知道我做錯了些什麼

你把事情搞砸了:你 重置(reset) 了一些東西, 或者你合併了錯誤的分支, 亦或你強推了後找不到你自己的提交(commit)了。有些時候, 你一直都做得很好, 但你想回到以前的某個狀態。

這就是 git reflog 的目的, reflog 記錄對分支頂端(the tip of a branch)的任何改變, 即使那個頂端沒有被任何分支或標籤引用。基本上, 每次 HEAD 的改變, 一條新的記錄就會增加到reflog。遺憾的是,這隻對本地分支起作用,且它只跟蹤動作 (例如,不會跟蹤一個沒有被記錄的檔案的任何改變)。

(master)$ git reflog
0a2e358 HEAD@{0}: reset: moving to HEAD\~2
0254ea7 HEAD@{1}: checkout: moving from 2.2 to master
c10f740 HEAD@{2}: checkout: moving from master to 2.2
複製程式碼

上面的 reflog 展示了從 master 分支簽出(checkout)到 2.2 分支,然後再籤回。 那裡,還有一個硬重置(hard reset)到一個較舊的提交。最新的動作出現在最上面以 HEAD@{0}標識.

如果事實證明你不小心回移(move back)了提交(commit), reflog 會包含你不小心回移前 master 上指向的提交(0254ea7)。

$ git reset --hard 0254ea7
複製程式碼

然後使用 git reset 就可以把 master 改回到之前的 commit,這提供了一個在歷史被意外更改情況下的安全網。

小結

最後,放一張我總結的腦圖總結一下以上的知識點。

Git讓你從入門到精通,看這一篇就夠了!

相關文章