【git】git使用筆記

這一版主要是參考廖海峯老師的博客,本身作了梳理,以後會根據使用經驗持續更新。git

基本概況

  1. git是分佈式版本控制系統(沒有「中央服務器」,相對於集中式);
  2. 用C語言開發的;
  3. 用於替代CVS、SVN;

本地版本庫

版本庫 又名倉庫,英文名repository,你能夠簡單理解成一個目錄,這個目錄裏面的全部文件均可以被Git管理起來,每一個文件的修改、刪除,Git都能跟蹤,以便任什麼時候刻均可以追蹤歷史,或者在未來某個時刻能夠「還原」。github

  1. 全部的版本控制系統只能跟蹤文本文件的改動,好比TXT文件,網頁,全部的程序代碼等等,而圖片、視頻、word文檔屬於二進制文件,因此無法跟蹤文件的變化;
  2. 文本編碼,中文有經常使用的GBK編碼,日文有Shift_JIS編碼,文本編碼強烈建議使用標準的UTF-8編碼,全部語言使用同一種編碼。
# 安裝
$ sudo apt-get install git
 
# 配置: --global表示全局生效,對於這個機器的全部倉庫都生效
# 分佈式版本控制,因此須要用 名字與郵箱 標記機器
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"

# 初始化Git倉庫:
git init

# 添加文件到Git倉庫【版本庫】,分兩步:
git add <file> # 可反覆屢次使用,添加多個文件;
git rm <file>  # 從版本庫中刪除該文件
git commit -m <message>
" 1 file changed, 1 insertion(+), 1 deletion(-)"

# 查看【工做區】的狀態
git status
"On branch master, Changes to be committed: modified: readme.txt"
git diff <file>

# 顯示從最近到最遠的提交日誌,commit的詳細狀況
git log  --pretty=oneline  # 穿梭前,以便肯定要回退到哪一個版本。
git log --graph --pretty=oneline --abbrev-commit
git reflog  # 重返將來,查看命令歷史,以便肯定要回到將來的哪一個版本

# commit id,用HEAD表示當前版本,上一個版本就是HEAD^,上上一個版本就是HEAD^^,HEAD~100
git reset --hard HEAD^/1094a
git reset HEAD <file>
"HEAD is now at e475afc add distributed"


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

遠程倉庫

由於GitHub須要識別出你推送的提交確實是你推送的,而不是別人冒充的,而Git支持SSH協議,因此,GitHub只要知道了你的公鑰,就能夠確認只有你本身才能推送。
GitHub容許你添加多個Key。假定你有若干電腦,你一下子在公司提交,一下子在家裏提交,只要把每臺電腦的Key都添加到GitHub,就能夠在每臺電腦上往GitHub推送了。web

$ ssh-keygen -t rsa -C "youremail@example.com"
"在用戶主目錄裏找到.ssh目錄,裏面有id_rsa和id_rsa.pub兩個文件,這兩個就是SSH Key的祕鑰對,id_rsa是私鑰,不能泄露出去,id_rsa.pub是公鑰,能夠放心地告訴任何人。"

# 添加遠程庫,遠程庫的名字就是origin
# 把本地庫的內容推送到遠程,用git push命令,其實是把當前分支master推送到遠程。
# 因爲遠程庫是空的,第一次推送master分支時,加上了-u參數,把本地的master分支和遠程的master分支關聯起來
# 這是由於Git使用SSH鏈接,而SSH鏈接在第一次驗證GitHub服務器的Key時,須要你確認GitHub的Key的指紋信息是否真的來自GitHub的服務器,輸入yes回車便可。
$ git init
$ git remote add origin git@github.com:michaelliao/learngit.git
$ git push -u origin master

# 從遠程庫克隆
# Git支持多種協議,默認的git://使用ssh,但也可使用https等其餘協議。
# https速度慢,每次推送都必須輸入口令,可是在某些只開放http端口的公司內部就沒法使用ssh協議而只能用https。
$ git clone git@github.com:michaelliao/gitskills.git

分支管理

主分支,即master分支
HEAD嚴格來講不是指向提交,而是指向master,master纔是指向提交的,因此,HEAD指向的就是當前分支shell

$ git checkout -b dev 
# 或者
$ git branch dev
$ git checkout dev  # 切換分支:git checkout <name>或者git switch <name>

$ git branch  # 列出全部分支,當前分支前面會標一個*號。
$ git add readme.txt 
$ git checkout master  # 切換回master分支
$ git merge dev  # 把dev分支的工做成果合併到master分支,git merge命令用於合併指定分支到當前分支
$ git branch -d dev

$ git switch -c dev # 建立並切換到新的dev分支,create
$ git switch master # 直接切換到已有的master分支

**注意:**切換分支使用git checkout ,而前面講過的撤銷修改則是git checkout – 服務器

解決衝突

# 「快速合併」僅適用於線性狀況
Git還會自動提示咱們當前master分支比遠程的master分支要超前1個提交。

# 當出現分叉時沒法直接合並,發生衝突,
$ git status
"On branch master Your branch is ahead of 'origin/master' by 2 commits. (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.txt no changes added to commit (use "git add" and/or "git commit -a")"
$ vi <file> # 打開衝突文件,這時候有標註了,修改
$ git add readme.txt # 再提交
$ git commit -m "conflict fixed"
"* cf810e4 (HEAD -> master) conflict fixed |\ | * 14096d0 (feature1) AND simple * | 5dc6824 & simple |/ * b17d20e branch test * d46f35e (origin/master) remove test.txt * b84166e add test.txt * 519219b git tracks changes * e43a48b understand how stage works * 1094adb append GPL * e475afc add distributed * eaadf4e wrote a readme file"

# 一般,合併分支時,若是可能,Git會用Fast forward模式,但這種模式下,刪除分支後,會丟掉分支信息。(對於線性模式)
# 若是要強制禁用Fast forward模式,Git就會在merge時生成一個新的commit,這樣,從分支歷史上就能夠看出分支信息。
# 合併分支時,加上--no-ff參數就能夠用普通模式合併,合併後的歷史有分支,能看出來曾經作過合併,而fast forward合併就看不出來曾經作過合併。
$ git merge --no-ff -m "merge with no-ff" dev  # 由於要建立新的分支,因此要加-m
$ git log --graph --pretty=oneline --abbrev-commit
"* e1e9c68 (HEAD -> master) merge with no-ff |\ | * f52c633 (dev) add merge |/ * cf810e4 conflict fixed"

Bug分支

  • 修復bug時,咱們會經過建立新的bug分支進行修復,而後合併,最後刪除;
  • 當手頭工做沒有完成時,先把工做現場git stash一下,而後去修復bug,修復後,再git stash pop,回到工做現場;
  • 在master分支上修復的bug,想要合併到當前dev分支,能夠用git cherry-pick命令,把bug提交的修改「複製」到當前分支,避免重複勞動。
# 當前正在dev上進行的工做尚未提交
$ git status # 能夠看出本地還有任務沒有commit掉
$ git stash # 能夠把當前工做現場「儲藏」起來,等之後恢復現場後繼續工做
$ git status # 此時工做區變乾淨的(除非有沒有被Git管理的文件),所以能夠放心地建立分支來修復bug。

# 首先肯定要在哪一個分支上修復bug,假定須要在master分支上修復,就從master建立臨時分支:
$ git checkout master
$ git checkout -b issue-101
# 如今修復bug
$ git add readme.txt 
$ git commit -m "fix bug 101"
# 修復完成後,切換到master分支,並完成合並,最後刪除issue-101分支
$ git switch master
$ git merge --no-ff -m "merged bug fix 101" issue-101

# 接着回到dev分支幹活了!
$ git switch dev
$ git status  # 工做區是乾淨的,剛纔的工做現場存到哪去了?用git stash list命令看看:
$ git stash list #工做現場還在,Git把stash內容存在某個地方了
# 須要恢復一下,有兩個辦法:
# 一是用git stash apply恢復,可是恢復後,stash內容並不刪除,你須要用git stash drop來刪除;
$ git stash apply
$ git stash drop
# 另外一種方式是用git stash pop,恢復的同時把stash內容也刪了:
$ git stash pop
# 看不到任何stash內容
$ git stash list 

# 若是是屢次stash,恢復的時候,先用git stash list查看,而後恢復指定的stash,用命令:
$ git stash list
$ git stash apply stash@{0}



# dev分支是早期從master分支分出來的,因此,這個bug其實在當前dev分支上也存在。那怎麼在dev分支上修復一樣的bug?

# 方法1:是重複操做一次,提交

# 方法2:一樣的bug,要在dev上修復,咱們只須要把4c805e2 fix bug 101這個提交所作的修改「複製」到dev分支。
# 注意:咱們只想複製4c805e2 fix bug 101這個提交**所作的修改**,並非把整個master分支merge過來。
# cherry-pick命令,讓咱們能複製一個特定的提交到當前分支:
$ git branch
$ git cherry-pick 4c805e2  # Git自動給dev分支作了一次提交,注意此次提交的commit是1d4b803,它並不一樣於master的4c805e2,由於這兩個commit只是改動相同,但確實是兩個不一樣的commit。用git cherry-pick,咱們就不須要在dev分支上手動再把修bug的過程重複一遍。

# 方法3:既然能夠在master分支上修復bug後,在dev分支上能夠「重放」這個修復過程,那麼直接在dev分支上修復bug,而後在master分支上「重放」行不行?固然能夠,不過你仍然須要git stash命令保存現場,才能從dev分支切換到master分支。

Feature分支

添加一個新功能時,你確定不但願由於一些實驗性質的代碼,把主分支搞亂了,因此,每添加一個新功能,最好新建一個feature分支,在上面開發,完成後,合併,最後,刪除該feature分支。app

  • 開發一個新feature,最好新建一個分支;
  • 若是要丟棄一個沒有被合併過的分支,能夠經過git branch -D強行刪除。
# 準備開發
$ git switch -c feature-vulcan  
# 開發完畢
$ git add vulcan.py 
$ git status
$ git commit -m "add feature vulcan"
# 切回dev,準備合併:
$ git switch dev
# 一切順利的話,feature分支和bug分支是相似的,合併,而後刪除。
$ git merge dev # or --no-ff

# 若是要丟棄一個沒有被合併過的分支
$ git branch -D feature-vulcan

多人協做

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

  • master分支是主分支,所以要時刻與遠程同步;
  • dev分支是開發分支,團隊全部成員都須要在上面工做,因此也須要與遠程同步;
  • bug分支只用於在本地修復bug,就不必推到遠程了,除非老闆要看看你每週到底修復了幾個bug;
  • feature分支是否推到遠程,取決於你是否和你的小夥伴合做在上面開發。
$ git remote   # 查看遠程庫的信息
$ git remote -v # 更詳細

# 推送分支,就是把該分支上的全部本地提交推送到遠程庫。
# 推送時,要指定本地分支,Git就會把該分支推送到遠程庫對應的遠程分支上:
$ git push origin master
# 若是要推送其餘分支,好比dev,就改爲:
$ git push origin dev

共同在dev上進行開發分佈式

$ git clone git@github.com:michaelliao/learngit.git
$ git branch # 此時,只有master
# 要在dev分支上開發,就必須關聯遠程origin的dev分支到本地,因而他用這個命令建立本地dev分支
$ git checkout -b dev origin/dev # 注意:origin/dev的寫法,使用斜杆隔開的

$ git add env.txt
$ git commit -m "add env"
$ git push origin dev

# 碰巧你也對一樣的文件做了修改,則會推送失敗,由於你的小夥伴的最新提交和你試圖推送的提交有衝突
$ git push origin dev
"To github.com:michaelliao/learngit.git ! [rejected] dev -> dev (non-fast-forward) error: failed to push some refs to 'git@github.com:michaelliao/learngit.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."

# 先用git pull把最新的提交從origin/dev抓下來,而後,在本地合併,解決衝突,再推送:
$ git pull
"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分支的連接
# 在本地建立和遠程分支對應的分支
$ git checkout -b branch-name origin/branch-name  # 本地和遠程分支的名稱最好一致
# 創建本地分支和遠程分支的關聯
$ git branch --set-upstream-to=origin/dev dev  # 設置dev和origin/dev的連接
$ git pull
# 若是合併有衝突,須要手動解決,解決的方法和分支管理中的解決衝突徹底同樣
$ git commit -m "fix env conflict"
$ git push origin dev

所以,多人協做的工做模式一般是這樣:svg

  1. 首先,能夠試圖用git push origin推送本身的修改;
  2. 若是推送失敗,則由於遠程分支比你的本地更新,須要先用git pull試圖合併;
  3. 若是合併有衝突,則解決衝突,並在本地提交;
  4. 沒有衝突或者解決掉衝突後,再用git push origin推送就能成功!

若是git pull提示no tracking information,則說明本地分支和遠程分支的連接關係沒有建立,用命令git branch --set-upstream-to origin/fetch

這就是多人協做的工做模式,一旦熟悉了,就很是簡單。

Rebase

後push的童鞋不得不先pull,在本地合併,而後才能push成功,分支會很是地亂

$ git log --graph --pretty=oneline --abbrev-commit

rebase的操做,有人把它翻譯成「變基」,看看怎麼把分叉的提交變成直線。

在這裏插入圖片描述
在這裏插入圖片描述

$ git push origin master
To github.com:michaelliao/learngit.git
 ! [rejected]        master -> master (fetch first)  #這說明有人先於咱們推送了遠程分支
error: failed to push some refs to 'git@github.com:michaelliao/learngit.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 pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:michaelliao/learngit
   d1be385..f005ed4  master     -> origin/master
 * [new tag]         v1.0       -> v1.0
Auto-merging hello.py  # 自動提交了
Merge made by the 'recursive' strategy.
 hello.py | 1 +
 1 file changed, 1 insertion(+)
 
$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.  # 加上剛纔合併的提交,如今咱們本地分支比遠程分支超前3個提交。
  (use "git push" to publish your local commits)
nothing to commit, working tree clean

$ git log --graph --pretty=oneline --abbrev-commit
*   e0ea545 (HEAD -> master) Merge branch 'master' of github.com:michaelliao/learngit # 這個是本地的commit,本題提交3 # 合併分支致使的增多一個commit
|\  
| * f005ed4 (origin/master) set exit=1  #(origin/master)對應的是遠程的,注意左邊graph,這條線是遠程分支
* | 582d922 add author  # 本地提交2 # 這條線是本地分支
* | 8875536 add comment  # 本地提交1
|/  
* d1be385 init hello

# (若是嫌棄太難看了),不然直接push
$ git rebase
First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M	hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M	hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py

$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master) add author # 本地提交2 
* 3611cfe add comment # 本地提交1 # 咱們本地的提交「挪動」了位置,放到了f005ed4 (origin/master) set exit=1以後
* f005ed4 (origin/master) set exit=1 # 能夠認爲rebase的做用是幫咱們在調整了commit順序,原來咱們是新增了2個commit以後,發現和遠程不一樣了,這時候才拉取;可是rebase調整了一下,先幫咱們作了拉取,而後咱們是基於這個最新的拉取再新增2個commit
* d1be385 init hello
# 這就是rebase操做的特色:把分叉的提交歷史「整理」成一條直線,看上去更直觀。缺點是本地的分叉提交已經被修改過了。


# 最後,經過push操做把本地分支推送到遠程:
$ git push origin/master
$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master, origin/master) add author
* 3611cfe add comment
* f005ed4 set exit=1
* d1be385 init hello

總結一下:

若是你和小夥伴同時開發,push後有三種結果

  1. 你改了,小夥伴沒有改:直接push
  2. 你改了,小夥伴也改了,可是沒有衝突:直接pull一下(這裏會發生自動合併),而後再次push便可
  3. 你改了,小夥伴也改了,而且發生衝突:先用git pull把最新的提交從遠程分支上(origin/master或者origin/dev)抓下來(這時候會提示哪些文件出現了衝突),而後,在本地合併解決衝突(解決的方法和分支管理中的解決衝突徹底同樣),再推送push便可

merge 是相對於兩個分支而言的,好比 local/master vs. local/dev, local/master vs. origin/master.