簡介
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 安裝包。按照安裝向導安裝即可。
配置
Git 自帶一個 git config
的工具來幫助設置控制 Git 外觀和行為的配置變量。 這些變量存儲在三個不同的位置:
/etc/gitconfig
文件: 包含系統上每一個用戶及他們倉庫的通用配置。 如果使用帶有--system
選項的git config
時,它會從此文件讀寫配置變量。\~/.gitconfig
或\~/.config/git/config
文件:只針對當前用戶。 可以傳遞--global
選項讓 Git 讀寫此文件。- 當前使用倉庫的 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 的開源項目:https://github.com/github/gitignore
在這里,你可以找到很多常用的模板,如: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 Cheat Sheet,總結很精煉,各位不妨收藏一下。
本節選擇性介紹 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 的用法,確實有不會用的新手同學,可以參考官方教程: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 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 方式即可。
如果覺得我的講解還不夠細致,可以參考:https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/
最佳實踐 Git Flow
詳細內容,可以參考這篇文章:Git 在團隊中的最佳實踐--如何正確使用 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圖片】下載高清圖)