chapter 1: 如何創建版本庫
- 初始化一個倉庫
$ git init
- 添加文件到Git倉庫的過程:
$ git add readme.txt
(沒有消息提示, Unix哲學: 沒有消息就是好消息)
$git commit -m 'write a readme file'
[master (root-commit) cb926e7] wrote a readme file
1 file changed, 2 insertions(+)
create mode 100644 readme.txt
chapter 2: 時光穿梭機
- 修改完文件后, 查看結果:
$ 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.txt
no changes added to commit (use "git add" and/or "git commit -a")
git status命令可以讓我們時刻掌握倉庫當前的狀態,上面的命令告訴我們,readme.txt被修改過了,但還沒有準備提交的修改
"Changes not staged for commit": 表示修改沒有add到暫存區中等待提交, 也就是修改還只存在工作區
經過'$ git add readme.txt'后, 執行'$ git status', 輸出了如下日志:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: readme.txt
"Changes to be committed": 表示修改已經add到了staged(暫存區), 等待被commit到HEAD master.
- 執行完'$ git commit - m 'add something'', 再執行"$ git status", 輸出如下日志:
$ git status
On branch master
nothing to commit (working directory clean)
Git告訴我們當前沒有需要提交的修改,而且,工作目錄是干凈(working directory clean)的
- 查看修改了什么內容
$ git diff readme.txt
section 1: 版本回退
- 查看歷史記錄
$ git log
$ git log
commit 3628164fb26d48395383f8f31179f24e0882e1e0
Author: Michael Liao askxuefeng@gmail.com
Date: Tue Aug 20 15:11:49 2013 +0800
append GPL
commit ea34578d5496d7dd233c827ed32a8cd576c5ee85
Author: Michael Liao askxuefeng@gmail.com
Date: Tue Aug 20 14:53:12 2013 +0800
add distributed
commit cb926e7ea50ad11b8f9e909c05226233bf755030
Author: Michael Liao askxuefeng@gmail.com
Date: Mon Aug 19 17:51:55 2013 +0800
wrote a readme file
git log命令顯示從最新到最久的提交日志
如果想輸出信息更見簡潔:
$ git log --pretty=oneline
$ git log --pretty=oneline
3628164fb26d48395383f8f31179f24e0882e1e0 append GPL
ea34578d5496d7dd233c827ed32a8cd576c5ee85 add distributed
cb926e7ea50ad11b8f9e909c05226233bf755030 wrote a readme file
3628164fb26d48395383f8f31179f24e0882e1e0 是commit id(版本號).
每提交一個新版本, 實際上Git就會把它們自動串成一條時間線
- 如何將文件回退到上一個版本
Git中使用HEAD表示當前版本, 也就是最新提交的commit id 所在的版本. 上一個版本就是"HEAD^", 上上個版本就是"HEAD^^".
上100個版本就是"HEAD~100"
$ git reset --hard HEAD^
$ git reset --hard HEAD^
HEAD is now at ea34578 add distributed
此時, 你如果想回到最新的commit去. 只要找到那個版本的commit id(一般取前7位):
$ git reset --hard 3628164
就可以指定回到未來的某個版本!
Git的版本回退速度非常快,因為Git在內部有個指向當前版本的HEAD指針,當你回退版本的時候,Git僅僅是把HEAD從指向“append GPL”:
git-head
改為指向“add distributed”:
git-head-move
然后順便把工作區的文件更新了。所以你讓HEAD指向哪個版本號,你就把當前版本定位在哪。
如果, 你在當前的命令行中找不到最新版本的commit id. 可以通過如下命令:
$ git reflog
$ git reflog
ea34578 HEAD@{0}: reset: moving to HEAD^
3628164 HEAD@{1}: commit: append GPL
ea34578 HEAD@{2}: commit: add distributed
cb926e7 HEAD@{3}: commit (initial): wrote a readme file
3628164就是你要找的那個版本commit id.
section 2: 工作區和暫存區
工作區: working directory
版本庫: Reposity, 工作區中的那個'.git'目錄, 版本庫中存了許多東西, 最重要的就是被稱為'stage(or index)'的暫存區, 還有Git為我們自動創建的第一個分支master, 以及指向master的一個指針叫HEAD
git-repo
暫存區: stage(或index), 作為版本庫的最重要的部分存在與'.git'目錄中.
把文件往Git版本庫里添加的時候,是分兩步執行的:
第一步是用“git add”把文件添加進去,實際上就是把文件修改添加到暫存區;
第二步是用“git commit”提交更改,實際上就是把暫存區的所有內容提交到當前分支;
因為我們創建Git版本庫時,Git自動為我們創建了唯一一個master分支,所以,現在,commit就是往master分支上提交更改
從來沒有add過的文件, 使用'git status'查看, 顯示的是'Untracked files'
執行了git add命令后, 暫存區的狀態如圖:
git-stage
所以,git add命令實際上就是把要提交的所有修改放到暫存區(Stage),然后,執行git commit就可以一次性把暫存區的所有修改提交到分支
執行了git commit命令后, 暫存區就沒有內容了, 修改的文件已經被commit到了master上:
現在版本庫變成了這樣,暫存區就沒有任何內容了:
git-stage-after-commit
section 3: 管理修改
為什么Git比其他版本控制系統設計得優秀,因為Git跟蹤并管理的是修改,而非文件。
你會問,什么是修改?比如你新增了一行,這就是一個修改,刪除了一行,也是一個修改,更改了某些字符,也是一個修改,刪了一些又加了一些,也是一個修改,甚至創建一個新文件,也算一個修改
如果你進行了如下操作:
第一次修改 -> git add -> 第二次修改 -> git commit
則只有第一次修改的內容被提交到了reposity中, 因為只有第一次修改被add到stage中
每次修改,如果不add到暫存區,那就不會加入到commit中
section 4: 撤銷修改
- 丟棄工作區的修改:
$ git checkout -- readme.txt
命令git checkout -- readme.txt意思就是,把readme.txt文件在工作區的修改全部撤銷,這里有兩種情況:
一種是readme.txt自修改后還沒有被放到暫存區,現在,撤銷修改就回到和版本庫一模一樣的狀態;
一種是readme.txt已經添加到暫存區后,又作了修改,現在,撤銷修改就回到添加到暫存區后的狀態。
總之,就是讓這個文件回到最近一次git commit或git add時的狀態。
注意: '--'很重要, 如果沒有, 將變成了"創建一個新分支"的命令
- 撤銷掉暫存區的修改(unstage) , 重新放回到工作區
$ git reset HEAD readme.txt
Unstaged changes after reset:
M readme.txt
git reset命令既可以回退版本, 也可以把暫存區中的修改回退到工作區
還記得如何回退版本嗎?
section 5: 刪除文件
在本地刪除了文件"rm text.txt", 立刻可以通過"git status"看到變化
- 如果你要從版本庫中刪除文件
$ git rm test.txt
rm 'test.txt'
$ git commit -m "remove test.txt"
[master d17efd8] remove test.txt
1 file changed, 1 deletion(-)
delete mode 100644 test.txt
- 如果你錯刪了文件, 把誤刪的文件恢復到最新版本:
$ git checkout -- test.txt
git checkout其實是用版本庫里的版本替換工作區的版本,無論工作區是修改還是刪除,都可以“一鍵還原”。
chapter 3: 遠程倉庫
- 配置github
第1步:創建SSH Key。在用戶主目錄下,看看有沒有.ssh目錄,如果有,再看看這個目錄下有沒有id_rsa和id_rsa.pub這兩個文件,如果已經有了,可直接跳到下一步。如果沒有,打開Shell(Windows下打開Git Bash),創建SSH Key:
$ ssh-keygen -t rsa -C "youremail@example.com"
如果一切順利的話,可以在用戶主目錄里找到.ssh目錄,里面有id_rsa和id_rsa.pub兩個文件,這兩個就是SSH Key的秘鑰對,id_rsa是私鑰,不能泄露出去,id_rsa.pub是公鑰,可以放心地告訴任何人.
第2步:登陸GitHub,打開“Account settings”,“SSH Keys”頁面:
然后,點“Add SSH Key”,填上任意Title,在Key文本框里粘貼id_rsa.pub文件的內容
chapter: 創建和合并分支
git中默認有一個主分支, 叫做master. HEAD嚴格來說不是指向提交, 而是指向master分支, 而master才是指向提交的. 所以HEAD就是指向當前的分支.
每一次提交, master分支都會向前移動一步
當你創建新的分支如dev時, Git新建了一個指針叫dev, 指向master相同的提交, 再把HEAD指向dev, 就表示當前分支在dev上:
git-br-create
從現在開始, 對工作區的修改和提交都市針對dev分支了, 比如新提交一次后, dev分支就向前移動一步, 而master指針不變:
git-br-dev-fd
假如我們在dev中的工作完成了, 就可以把dev合并到master上.
創建分支, 并切換到分支:
$ git checkout -b 15_learn_git_flow -t master
git checkout命令加上-b參數表示創建并切換,相當于以下兩條命令:
$ git branch dev
$ git checkout dev
Switched to branch 'dev'
然后,用git branch命令查看當前分支:
$ git branch
- dev
master
git branch命令會列出所有分支,當前分支前面會標一個*號。
然后,我們就可以在dev分支上正常提交,比如對readme.txt做個修改,加上一行:
Creating a new branch is quick.
然后提交:
$ git add readme.txt
$ git commit -m "branch test"
[dev fec145a] branch test
1 file changed, 1 insertion(+)
現在,dev分支的工作完成,我們就可以切換回master分支:
$ git checkout master
Switched to branch 'master'
切換回master分支后,再查看一個readme.txt文件,剛才添加的內容不見了!因為那個提交是在dev分支上,而master分支此刻的提交點并沒有變:
git-br-on-master
現在,我們把dev分支的工作成果合并到master分支上:
$ git merge dev
Updating d17efd8..fec145a
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)
git merge命令用于合并指定分支到當前分支。合并后,再查看readme.txt的內容,就可以看到,和dev分支的最新提交是完全一樣的。
注意到上面的Fast-forward信息,Git告訴我們,這次合并是“快進模式”,也就是直接把master指向dev的當前提交,所以合并速度非常快。
當然,也不是每次合并都能Fast-forward,我們后面會將其他方式的合并。
合并完成后,就可以放心地刪除dev分支了:
$ git branch -d dev
Deleted branch dev (was fec145a).
刪除后,查看branch,就只剩下master分支了:
$ git branch
- master
因為創建、合并和刪除分支非常快,所以Git鼓勵你使用分支完成某個任務,合并后再刪掉分支,這和直接在master分支上工作效果是一樣的,但過程更安全。
小結
Git鼓勵大量使用分支:
查看分支:git branch
創建分支:git branch name
切換分支:git checkout name
創建+切換分支:git checkout -b name
合并某分支到當前分支:git merge name
刪除分支:git branch -d name
問題: 如何創建遠程的分支?
- 先在本地創建分支
$ git checkout -b 18_learn_git_flow -t master
- 將本地的分支push到遠程
$ git push origin 18_learn_git_flow
section 4: Bug分支
修復bug時,我們會通過創建新的bug分支進行修復,然后合并,最后刪除;
當手頭工作沒有完成時,先把工作現場git stash一下,然后去修復bug,修復后,再git stash pop,回到工作現場。
查看全部的工作現場 :
$ git stash list
恢復工作現場:
方式一: 通過git stash apply stash@{0}, 但恢復后, stash內容并不刪除, 需要使用git stash drop來刪除
方式二: 通過 git stash pop, 恢復的同時把stash內容也刪掉.
section 5: Feature分支
刪除分支:
$ git branch -d feature1 --- 刪除本地分支1, 如果該分支沒有被merge request, 則不能刪除
$ git branch -D feature1 --- 刪除本地分支, 不管該branch有沒有merge request
刪除遠程分支:
$ git push origin --delete 18_learn_git_flow
或者, 通過推送一個空的branch到遠程達到刪除的目的:
$ git branch -d branchName
$ git push origin :branchName
刪除不存在對應遠程分支的本地分支:
假設這樣一種情況:
1. 我創建了本地分支b1并pull到遠程分支 origin/b1;
2. 其他人在本地使用fetch或pull創建了本地的b1分支;
3. 我刪除了 origin/b1 遠程分支;
4. 其他人再次執行fetch或者pull并不會刪除這個他們本地的 b1分支,運行 git branch -a 也不能看出這個branch被刪除了,如何處理
使用$ git remote show origin 查看b1的狀態
如果顯示b1是stable, 使用 $ git remote prune origin 可以將其從本地版本庫中刪除
或者, 更簡單的方式是, 使用 $ git fetch -p , 它在fetch之后刪除掉沒有與遠程分支對應的本地分支
重命名本地分支:
$ git branch -m oldName newName
重命名遠程分支, 其實也就是先刪除遠程分支, 然后將重命名后的本地分支推送上去:
$ git push --delete origin oldName
$ git branch -m oldName newName
$ git push origin newName
chapter: 多人協作
因此,多人協作的工作模式通常是這樣:
首先,可以試圖用git push origin branch-name推送自己的修改;
如果推送失敗,則因為遠程分支比你的本地更新,需要先用git pull試圖合并;
如果合并有沖突,則解決沖突,并在本地提交;
沒有沖突或者解決掉沖突后,再用git push origin branch-name推送就能成功!
如果git pull提示“no tracking information”,則說明本地分支和遠程分支的鏈接關系沒有創建,用命令git branch --track branch-name origin/branch-name。
這就是多人協作的工作模式,一旦熟悉了,就非常簡單。
小結
查看遠程庫信息,使用git remote -v;
本地新建的分支如果不推送到遠程,對其他人就是不可見的;
從本地推送分支,使用git push origin branch-name,如果推送失敗,先用git pull抓取遠程的新提交;
在本地創建和遠程分支對應的分支,使用git checkout -b branch-name origin/branch-name,本地和遠程分支的名稱最好一致;
建立本地分支和遠程分支的關聯,使用git branch --track branch-name origin/branch-name;
從遠程抓取分支,使用git pull,如果有沖突,要先處理沖突。
chapter: 標簽管理
發布一個版本時,我們通常先在版本庫中打一個標簽,這樣,就唯一確定了打標簽時刻的版本。將來無論什么時候,取某個標簽的版本,就是把那個打標簽的時刻的歷史版本取出來。所以,標簽也是版本庫的一個快照。
Git的標簽雖然是版本庫的快照,但其實它就是指向某個commit的指針(跟分支很像對不對?但是分支可以移動,標簽不能移動),所以,創建和刪除標簽都是瞬間完成的
命令git tag name用于新建一個標簽,默認為HEAD,也可以指定一個commit id;
-a tagname -m "blablabla..."可以指定標簽信息;
-s tagname -m "blablabla..."可以用PGP簽名標簽;
命令git tag可以查看所有標簽;
$ git checkout master
$ git tag v1.0
$ git tag v1.0 36564232
$ git tag -a v1.0 -m "version 1.0 released" 36564232
查看tag的詳情內容:
$ git show tagname
刪除標簽:
$ git tag -d v1.0
因為創建的標簽都只存儲在本地, 不會自動推送到遠程, 所以, 打錯了標簽可以在本地安全刪除.
如果要推送某個標簽到遠程, 使用命令:
$ git push origin tagname
或者, 一次性推送全部尚未推送到遠程的本地標簽:
$ git push origin --tags
刪除遠程標簽(其實是推送一個空的tag到遠程tag達到刪除目的):
1) 先刪除本地的標簽: $ git tag -d v.0
2) 然后從遠程刪除: $ git push origin :refs/tags/v1.0
或者, 1.7以后可以這么做:
$ git push origin --delete tag v1.0
獲取遠程的tag:
$ git fetch origin tag <tagname>
小結
命令git push origin tagname可以推送一個本地標簽;
命令git push origin --tags可以推送全部未推送過的本地標簽;
命令git tag -d tagname可以刪除一個本地標簽;
命令git push origin :refs/tags/tagname可以刪除一個遠程標簽。