Git 從入門到精通,這篇包教包會!

作者:靜默虛空

juejin.im/post/5c8296f85188257e3941b2d4

簡介

Git 是什麼

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

什麼是版本控制

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

什麼是分佈式版本控制系統

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

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

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

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

爲什麼使用 Git

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

工作時不需要聯網

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

更加安全

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

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

安裝

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 安裝包。按照安裝嚮導安裝即可。

https://git-scm.com/downloads

配置

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 [email protected]

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

很多 GUI 工具都會在第一次運行時幫助你配置這些信息。

.gitignore

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

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

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

這裏推薦一下 Github 的開源項目:https://github.com/github/gitignore

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

原理

個人認爲,對於 Git 這個版本工具,再不瞭解原理的情況下,直接去學習命令行,可能會一頭霧水。所以,本文特意將原理放在命令使用章節之前講解。可以參考: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 Cheat Sheet,總結很精煉,各位不妨收藏一下。

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

創建倉庫

克隆一個已創建的倉庫:

# 通過 SSH
$ git clone ssh://[email protected]/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 作爲最著名的代碼開源協作社區,在程序員圈想必無人不知,無人不曉。關於Git和Github你不知道的十件事

這裏不贅述 Github 的用法,確實有不會用的新手同學,可以參考官方教程:

https://guides.github.com/

clone 方式

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

而 Github 上支持 HTTPS 和 SSH。

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

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

以下介紹一下,如何生成證書,以及在 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 [email protected]

首先 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== [email protected]

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

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

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

如果覺得我的講解還不夠細緻,可以參考:https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/

最佳實踐 Git Flow

詳細內容,可以參考這篇文章:Git 在團隊中的最佳實踐--如何正確使用 Git Flow

https://www.cnblogs.com/cnblogsfans/p/5075073.html

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 <[email protected]>"

如果你需要修改所有歷史, 參考 '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 p

https://juejin.im/post/5c8296f85188257e3941b2d4

刪除任意提交(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 <[email protected]>
Date:   Tue Jul 22 15:39:27 2014 -0400

    Bug #21 - Added CSRF protection

commit 5ea51731d150f7ddc4a365437931cd8be3bf3131
Author: Alex Lee <[email protected]>
Date:   Tue Jul 22 15:39:12 2014 -0400

    Bug #14 - Fixed spacing on title

commit a13b85e984171c6e2a1729bb061994525f626d14
Author: Aki Rose <[email protected]>
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 <[email protected]>
Date:   Wed Jul 30 00:34:10 2014 +0200

    foo.txt added

commit 69204cdf0acbab201619d95ad8295928e7f411d5
Author: Kate Hudson <[email protected]>
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.

http://stackoverflow.com/questions/11058312/how-can-i-use-git-rebase-without-requiring-a-forced-push

我需要組合(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 官網:https://git-scm.com/

    • Git Github:https://github.com/git/git

  • 模板

    • gitignore 模板 - .gitignore 文件模板

    • gitattributes 模板 - .gitattributes 文件模板

    • github-cheat-sheet - git 命令簡略圖表

  • Git 書

  • Git 官方推薦教程 - Scott Chacon 的 Git 書。

  • Git 教程

    • Git 中文教程:https://github.com/geeeeeeeeek/git-recipes

    • 廖雪峯的 Git 教程:https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000

    • 有關 git 的學習資源:https://github.com/xirong/my-git

  • 文章

    • Git Cookbook:https://github.com/k88hudson/git-flight-rules/blob/master/README_zh-CN.md

    • Git 奇技淫巧:https://github.com/521xueweihan/git-tips

    • Git 風格指南:https://github.com/aseaday/git-style-guide

    • Git 在團隊中的最佳實踐--如何正確使用 Git Flow:http://www.cnblogs.com/cnblogsfans/p/5075073.html

  • Git 工具

    • guis - Git 官網展示的客戶端工具列表。

    • gogs - 極易搭建的自助 Git 服務。

    • gitflow - 應用 fit-flow 模型的工具。

    • firstaidgit.io 一個可搜索的最常被問到的 Git 的問題

    • git-extra-commands - 一堆有用的額外的 Git 腳本

    • git-extras - GIT 工具集 -- repo summary, repl, changelog population, author commit percentages and more

    • git-fire - git-fire 是一個 Git 插件,用於幫助在緊急情況下添加所有當前文件, 做提交(committing), 和推(push)到一個新分支(阻止合併衝突)。

    • git-tips - Git 小提示

    • git-town - 通用,高級 Git 工作流支持!http://www.git-town.com

  • GUI 客戶端(GUI Clients)

    • GitKraken - 豪華的 Git 客戶端 Windows, Mac & Linux

    • git-cola - 另外一個 Git 客戶端 Windows & OS X

    • GitUp - 一個新的 Git 客戶端,在處理 Git 的複雜性上有自己的特點

    • gitx-dev - 圖形化的 Git 客戶端 OS X

    • Source Tree - 免費的圖形化 Git 客戶端 Windows & OS X

    • Tower - 圖形化 Git 客戶端 OS X(付費)

  • git cheat sheet

  • github-git-cheat-sheet:https://services.github.com/on-demand/downloads/github-git-cheat-sheet.pdf

END

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章