前言
發現 git
這塊所知甚淺,打算利用空閒時間好好學習一下;
git 整理
(DVCS Distributed version controll system 分佈式版本控制系統)
git簡介
- 直接記錄快照,而非差異比較; 文件變化,保存一個指向這次快照的索引,文件沒有變化,對上次保存的快照做一鏈接;
- 本地操作;
- SHA-1哈希值作爲索引,校驗保持數據完整性;
- 文件三種狀態;
committed,modified,staged
;- committed: 已提交該文件已被安全的保存在本地數據庫中;
- modified: 已修改修改了某個文件,還沒有提交保存;
- staged: 已暫存已修改的文件放在下次提交時要保存的清單中;
git流轉過程:
git的工作區域(modified),暫存區域(staged),本地倉庫(committed)
git目錄:保存元數據和對象數據庫的地方
git clone 就是.git目錄;
git clone --bare 新建的目錄本身就是git目錄;
基本工作流程:
工作目錄中修改文件(modified)->對修改的文件進行快照,保存到暫存區域(staged)->提交更新,將保存在暫存區的文件快照永久轉存到git目錄中(commited);
git配置 .git/config
--global 表示更改的配置文件就是用戶主目錄下,以後所有的項目默認使用這裏配置的用戶信息,可去除表示特定的項目中運用;
git config --global user.name "John Doe"
git config --global user.email [email protected]
git config --list 查看配置信息;
git help 查看幫助信息;
git config可用於設置別名;
git config --global alias.br branch 設置branch的快捷名稱;
即`git br 查看所有分支;`
git 基礎
工作目錄中初始化新倉庫
git init : 對現有的某個項目開始用git管理,只需到此項目所有的目錄執行;
git clone [url] newfolder:克隆url的文件;保存下載下來的所有版本記錄,並從中取出最新版本的文件拷貝;
可自定義新建的項目目錄;
自動使用默認的`master`和`origin`名字;
git add *.c 納入版本控制,多功能命令;
記錄每次更新到倉庫
工作目錄下的所有文件不外乎兩種狀態: 已跟蹤
或未跟蹤
;已跟蹤指本來就被納入控制的文件,狀態可能爲未更新,已修改,已暫存(unmodified,modified,staged)
;其他的所有文件屬於未跟蹤(untracked)
;
查看文件狀態
git status
-
clean: 現在的工作目錄是乾淨的,已跟蹤文件沒有修改過,沒有出現未跟蹤的新文件;
-
創建文件後:
untracked files
; 出現未跟蹤文件; -
跟蹤新文件: git add 文件 後,
Changes to be committed
new file
已被跟蹤,暫存區,處於已暫存狀態;提交後被留在歷史記錄中; -
暫存已修改文件: 修改已經跟蹤的文件,會出現
Changes not staged for commit
modified
已跟蹤文件的內容產生變化,但還沒有暫存,需要運行git add 後出現:Changes to be committed
newfile or modified
表明已經暫存;此時再次修改後還需要git add;
添加至暫存區
git add
多功能命令,根據目標文件的狀態不同,命令效果不同;
- 未跟蹤過的文件標記爲需要跟蹤;
- 將已跟蹤目標文件快照放入暫存區,git會暫存運行命令時的版本;
- 用於合併時把有衝突的文件標記爲已解決狀態;
忽略文件格式 .gitignore
- 所有空行或者以註釋符號 # 開頭的行都會被 Git 忽略。
- 可以使用標準的 glob 模式匹配 (shell 所使用的簡化了的正則表達式)。
- *: 0個或多個任意字符; [abc]:匹配任意一個列在方括號中的字符,a,b,c; ?:只匹配一個任意字符;[0-9]兩個字符範圍內的都可以匹配;
- 匹配模式最後跟反斜槓(/)說明要忽略的是目錄。
- 要忽略指定模式以外的文件或目錄,可以在模式前加上驚歎號(!)取反。
# 此爲註釋 – 將被 Git 忽略
# 忽略所有 .a 結尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 僅僅忽略項目根目錄下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目錄下的所有文件
build/
# 會忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目錄下所有擴展名爲 txt 的文件
doc/**/*.txt
git diff
無參數比較的是修改之後還沒有暫存起來的變化;
git diff --cached 查看已經暫存起來的文件和上次提交時的快照之間的差異;
git commit -m “msg”
提交時記錄的是放在暫存區的快照,沒有暫存的仍然保持已修改狀態,可以下次提交時納入版本管理; 每一次提交都是對項目做個快照,可以回到這個狀態或者進行比較;
跳過使用暫存區域 git commit -a -m "msg"
自動將素有已經跟蹤過的文件暫存起來一併提交;
git commit --amend 修改最後一次提交,將使用當前的暫存區域快照提交,可以用於添加額外的文件或者提交信息寫錯了,較少commit節點;
git rm 移出文件;
必須從暫存區移除,然後提交; 如果直接刪除文件 會出現changes not staged for commit
deleted
;
刪除git倉庫,但保留在當前工作目錄中,以便加入到gitignore中;使用 git rm --cached 文件
git rm log/*.log
git rm *.~ (刪除所有以~結尾的文件;)
git mv
git mv oldfile newfile 相當於
- mv oldname newname
- git rm oldname
- git add newname
git log && gitk
顯示歷史記錄和圖形化記錄
git reset HEAD 取消已經暫存的文件;
- 取消暫存區域的文件;(一個是add 的撤銷操作)
git reset HEAD 文件名
將暫存區的文件回到已修改未暫存的狀態;
- 取消工作目錄中已修改的文件;(一個是commit的撤銷操作)
git checkout -- 文件名
取消對文件的修改,需要回到已修改未暫存狀態,會吧之前對改文件的修改取消了;
git remote 遠程項目的管理
git remote
列出每個遠程庫的簡短名字(origin);
git remote -v
顯示對應的克隆地址;
git remote add [shortname] [url]
添加一個新的遠程倉庫,指定一個簡單名稱;使用shortname指代對應的倉庫地址;
git remote show [remote-name]
查看遠程倉庫信息;
git remote rename oldname newname
遠程倉庫的重命名;
git remote rm [remote-name]
遠程倉庫的刪除
git fetch 從遠程倉庫中抓取數據
git fetch [remote-name]
此命令會到遠程倉庫中拉取所有你本地倉庫中還沒有的數據; 只是將遠端的數據拉取到本地倉庫,不會自動合併到當前工作分支;
git pull
自動抓取數據,git fetch + git merge
git push 推送數據到遠程倉庫
git push [remote-name] [branch-name]
將本地的branchname分支推送到遠程的origin服務器上;
git tag 打標籤
git tag
顯示現有的所有標籤; 搜索加 (-l v1.4.2.*
)
- -l lightweight 輕量級的,指向特定提交對象的引用;
- -a annotated 含附註的,存儲在倉庫中的一個獨立對象;
git tag v1.4
輕量級標記;
git tag -a v1.4 -m "my version 1.4"
打附註標籤;
git tag -a v1.2 序列號
對某次提交打tag;
git push origin [tagname]
分享某個標籤到遠程;
git push origin --tags
推送所有的標籤到遠程;
git show
查看相關標籤信息;
git tag -s v1.2 -m "msg"
簽署標籤,使用GPG來簽署標籤;
git tag -v [tag-name]
驗證標籤;
git分支的原理簡介
因爲git保存的是一系列文件快照;
git commit
新建一個提交對象前,git會對每一個文件計算校驗和
(SHA1)將當前版本的文件快照保存在git倉庫中(blob對象);對每一個子目錄計算校驗和
,然後在git倉庫中將這些目錄保存爲tree對象;之後創建的提交對象,除了包含相關提交信息以外,還包含着指向這個tree對象的指針;
-
blob對象: 表示文件快照內容;
-
tree對象: 記錄着目錄樹內容及其中各個文件對應blob對象索引;
-
commit對象: 包含指向tree對象(根目錄)的索引和其他提交信息元數據的commit對象;
修改後再次提交,提交對象會包含一個指向上次提交對象的指針(parent對象)
分支 本質上是一個指向commit對象的可變指針;
git 使用master作爲分支的默認名稱;在若干次提交後,其實已經有一個指向最後一次提交對象的master分支,它在每次提交的時候都會自動向前移動;
git branch newbranchname
在當前commit對象上新建一個分支指針,但不切換至新分支;
HEAD 判斷當前在哪個分支上工作
git保存了一個名叫HEAD
的特別指針; git中HEAD指向你正在工作中的本地分支的指針(當前分支的別名)
git checkout newbranchname
切換到其他分支上;
每次提交head都會隨着分支一起向前移動,原master分支仍然指向原先git checkout時所在的commit對象;
若此時運行git checkout master
,head指向另一個分支;
將HEAD指針移回到master分支;
並把工作目錄中的文件換成了master分支所指向的快照內容;
git commit 新建一個新的提交對象,向不同方向開發;
分支的新建和合並
git checkout -b newbranch
git checkout -d branchname
- -b : 新建一個分支,並切換head指向新建分支;(branch + checkout)
- -d : 刪除一個分支;
clean 後的切換分支;
如果你的暫存區或者工作目錄中,有沒有提交的修改,它會和你即將檢出的分支產生衝突從而阻止git爲你切換分支;保持clean後,可切換分支;
hotfix 分支從master分支所在點分化出來的;master是線上版本,iss53分支是當前正在開發的feature功能;
git checkout -b hotfix
,git checkout master
,git merge hotfix
Fast-forward
: 向前合併,由於master分支所在的提交對象是要併入hotfix分支的直接上游,git只需把master分支指針直接右移,這種單線的歷史分支不存在任何需要解決的分歧,這種合併方式可以稱爲Fast forward;
合併之後,master分支和hotfix分支指向同一位置;刪除不需要的hotfix分支(指針);
切換回正在開發的分支iss53,不受影響;如果想要納入hotfix的修改,需要git merge master
or 等iss53完成後將iss53分支中的更新併入master;
分支的合併
git checkout master
,git merge iss53
Auto-merging
: 這次合併並不同於hotfix的合併;因爲開發歷史從更早的地方開始分叉的;由於master分支所指向的提交對象並不是iss53分支的直接祖先,git需要用兩個分支的末端(C4,C5)以及他們公共的祖先(C2)進行一次簡單的三方合併,紅框是需要合併的提交對象;
不同方向的分支合併,需要對三方合併後的結果重新做一個新的快照,自動創建一個指向它的提交對象C6,此提交對象有兩個祖先(C4,C5); 刪除feature功能分支即可完成開發;
遇到衝突後的分支合併;
git status
查看合併衝突;
Automatic merge failed
,unmerged paths
- 衝突標記
===========
隔開衝突;<<<<<<<<HEAD
開始衝突,>>>>>>>iss53
與分支結束衝突地方;- 上方是
HEAD
(即master分支,當前分支,運行merge命令時所切換的分支)的內容; - 下方是iss53分支中的內容;
解決衝突之後,git add
再來一次快照保存到暫存區域,一旦暫存表示衝突已解決;
git mergetool
運行可視化的合併工具並引導你解決所有衝突;
總結merge
合併分爲兩種合併:fast-forward
和非共同祖先的三方合併;
fast-forward
: 單線的提交歷史,如果低節點的master分支指針的想去高節點的dev分支指針,使用merge
命令 fast-forward 合併,指向同一個commit 提交對象節點;- 非共同祖先的兩分支的合併,使用兩分支的末節點和共同祖先的節點進行三方合併,產生一個新的虛擬節點; 可使用的命令爲三個,
merge
,rebase
😭cherry pick
遴選節點再重演)
分支的管理
git branch
查看所有分支-v
查看各個分支最後一次的提交信息;--no-merged
查看尚未合併的分支;刪除未合併的分支使用get branch -D name
強制刪除;--merged
查看哪些分支已被併入當前分支(哪些分支是當前分支的直接上游,父提交對象),此時可將沒有*
的分支刪除掉;
利用分支進行開發的工作流程;(git flow 雛形)
- 長期分支
隨着提交對象不斷的右移指針,穩定分支的指針總是在提交歷史中落後於前沿特性開發的指針;展現不同層次的穩定新,當前沿指針分支進入到穩定階段,將之合併到更高層的分支中去;
- 特性分支
特性(Topic)分支,短期的用來實現單一特性,通常創建特性分支,提交若干提交對象更新後,合併到主幹分支,然後刪除特性分支;
新建多個特性分支,針對於不同提交對象的節點進行新建分支的功能嘗試;
具體情景: 決定使用iss91v2
特性分支嘗試的解決方案,並拋棄原來的iss91
分支的內容(拋棄C5,C6提交對象),直接在主幹中併入另外兩個分支,這些分支都是本地分支,完全不涉及與服務器的交互;
遠程分支
對遠程倉庫中的分支的索引; 使用(遠程倉庫名/(分支名))
形式表示遠程分支,如origin/master
;一次克隆建立本地分支和遠程分支,都指向origin上的master分支;
多人開發中,遠程倉庫master向前推進,本地master也朝不同方向推進;
此時運行git fetch origin
同步遠程服務器上數據到本地;找到origin是哪個服務器,獲取未擁有的數據,更新本地的數據庫,將origin/master指針移到它最新的位置上;
多個遠程倉庫,添加另一個遠程倉庫和拉取另一個遠程倉庫,在本地提交中新建一個指針;
推動本地分支 (分支全面: refs/head/分支名)
git push (遠程倉庫名) (分支名)
: 取出我的本地分支,推送到遠程倉庫分支,git分支名擴展爲refs/head/localbranchname:refs/head/remotebranchname
;
git push origin branchname
== git push origin branchname:branchname
git push [遠程名] [本地分支名]:[遠程分支名]
git push origin branchname:awebranchname
推送本地branchname分支到遠程倉庫創建awebranchname分支;
git merge origin/master
合併遠程分支到當前分支
git checkout -b branchname origin/branchname
在遠程分支的基礎上,分化出一個新的分支並切換; git checkout -b [本地分支名] [遠程名/分支名]
== git checkout --track [遠程名/分支名]
跟蹤遠程分支
從遠程分支checkout 出來的分支被稱爲跟蹤分支,是一種和某個遠程分支有直接聯繫的本地分支;在跟蹤分支中使用git push
or git pull
,git會判斷應該向哪個服務器的哪個分支推送or拉取數據;
克隆倉庫clone時,git自動創建一個名爲master 的分支來跟蹤origin/master,可使用--track
簡化;
刪除遠程分支
git push [遠程名] : [遠程分支名]
刪除遠程服務器上的某個分支;
分支的變基 (NICE)
將一個分支的修改整合到另一個分支上有兩種方法: rebase
,merge
基本的變基操作
git rebase --abort
變基衝突,回退所有的變基操作;
git rebase --continue
合併衝突後的繼續變基;
如果使用merge
,三方合併(C2基,C3快照,C4快照),會產生一個新的基於兩個祖先的提交對象節點(C5);
rebase
可以將C3裏產生的變化補丁 在C4的基礎上重新打一遍,即一個分支裏提交的變化移到另一個分支裏重新放一遍,git中操作叫變基
;
git checkout experment
,git rebase master
首先回到兩個分支最近的共同祖先,根據當前分支(也就是要進行變基的分支experiment)後續的歷次提交對象(C3,)生成一系列文件補丁,然後以基底分支(也就是主幹分支master)最後一個提交對象(C4)爲新的出發點,逐個應用之前準備好的補丁文件,最後會生成一個新的合併提交對象(C3`),從而改變experiment的提交歷史,使它成爲master的直接下游;
回到master分支,進行一次Fast-Forward
合併;
變基的好處
變基可以產生一個更爲整潔的提交歷史;一般使用變基的目的,是想得到一個能在遠程分支上乾淨應用的補丁,按照每行的修改次序重演一遍修改;(實際上是把解決分支補丁
同最新主幹代碼
之間衝突的責任,劃轉爲由提交補丁的人來解決;這樣維護者不需要做任何整合工作,只需要根據你提供的倉庫地址做一次快進合併,或者直接採納你提交的代碼)
情景分析
決定將client分支中的修改(C8,C9)併到master中,跳過server分支直接放到master分支中重演一遍,--onto
指定新的基底分支;
git rebase --onto master server client
: 取出client分支,找出client分支和server分支的共同祖先之後的變化,然後把它們在master上重演一遍;
快進合併master分支,包含client分支的變化;git checkout master
,git merge client
將server分支的變化也包含過來,將server變基到master git rebase master server
git rebase [主幹分支] [特性分支]
不需要切換分支的情況下,取出特性分支server,在master分支上重演;
再次快進合併,刪除特性分支 git branch -d client
,git branch -d server
變基的風險
準則:
一旦分支中的提交對象發佈到公共倉庫,就千萬不要對該分支進行變基操作
在進行變基的時候,實際上拋棄了一些現存的提交對象而創造了一些類似但不同的新的提交對象;如果你將原來分支中的提交對象發佈出去,並且其他人更新下載後再其基礎上開展工作,而稍後你又用git rebase
拋棄這些提交對象,把新的重演後的提交對象發佈出去的話,合作者們就必須重新合併他們的工作,當你再次從他們那獲取代碼時,就會一團亂;
如:獲取遠程倉庫的代碼,並提交幾個提交對象;
合作者提交變化C2,合併他自己的分支提交對象C5得到新的提交對象C6,推送到遠程;當你抓取併合併到本地的開發分支,顯示:
接下來,推送C6的人決定用變基取代之前的合併操作;繼而又使用git push --force
覆蓋了服務器上的歷史,得到C4`,而之後你再從服務器上下載最新提交後,得到:
你獲取遠程數據合併後,此時C4`和之前的C4的sha1值完全不同,git會當做新的提交對象處理,而C7中已經包含了C4的修改內容,於是合併操作會將C7和C4`合併成C8;
所以把變基當成一種在推送之前清理提交歷史的手段,而且僅僅變基那些尚未公開的提交對象,就沒問題;
繞過 clean 的切換分支;
- stashing commit amending;
git stash
git工作流
集中式工作流
一箇中心庫,開發者pull,push;
集成管理員工作流
github網站的開發流,每個開發者有自己的公共倉庫,pull request由維護者處理你提交的更新;
司令官和副官工作流
大型項目使用,dictator(司令官)合併lieutenant(副官)的分支,副官合併零散的特性分支,司令將集成後的分支推送到共享倉庫中,其他開發者以共享倉庫爲基礎衍合;
衍合與挑揀(cherry-pick)的流程(對應於sourcetree的遴選)
查看不同分支diff命令
git diff master...contrib
...
語法,加在原始分支(擁有共同祖先)和當前分支之間;
cherry-pick
也可以保持線性的提交歷史;類似於針對某次特定提交的衍合,首先提取某次提交的補丁,然後試着應用在當前分支上;如果某個特性分支上有多個commit,你只想引入其中之一就可以使用這種方法;
master中git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf
引入另一個分支挑揀一些提交對象進行應用;
交互式暫存
交互式命令,幫助你方便的構建至包含特定組合和部分文件的提交對象,確保你的提交在邏輯上劃分爲相應的變更集;
git add -i
or git add -interactive
git進入一個交互式的shell模式;
顯示暫存區,類似與sourcetree中的挑選某些提交對象暫存;
儲藏(Stashing)
-
git stash
不想提交你正在進行中的工作,所以儲藏這些變更,往堆棧中推送一個新的儲藏;執行後工作目錄就clean了,可以方便的切換到其他分支上;你的變更都保存在棧上; -
git stash list
查看現有的儲藏; -
git stash apply
默認應用最近儲藏,git stash apply stash@{2}
應用指定的儲藏;git stash apply --index
對文件的變更被重新應用,但是被暫存的文件沒有重新被暫存,帶–index 來重新應用被暫存的變更;
-
git stash drop
移除儲藏; -
git stash pop
移除儲藏,並應用儲藏;
獲取你工作目錄的中間狀態,也就是你修改過的被追蹤的文件和暫存的變更(modified類型),並將它保存到一個未完結變更的堆棧中,隨時可以重新應用;
取消儲藏
git stash show -p stash@{0} | git apply -R
取消某個stash的應用;
git stash show -p | git apply -R
沒有指定具體的儲藏,會選擇最近的儲藏;
從儲藏中創建分支
git stash branch branchname
創建一個新的分支,檢出你儲藏工作時的所處的提交, 重新應用你的工作,如果成功,將會丟棄儲藏;可以恢復儲藏的工作然後在新的分支上繼續當前的工作;
重寫歷史
改變最近一次提交
- 改變提交說明
- 改變快照內容;
git commit --amend
修改最後一次提交說明
修改多個提交說明
可以使用交互式rebase
來衍合一系列的提交到它們原來所在的HEAD上而不是移到新的;
git rebase -i HEAD~3
指明你想要的修改的提交的父提交,衍合命令(HEAD~3…HEAD範圍內的每一個提交都會被重寫,不要涵蓋已經推送到遠程的提交,因爲其他開發者可能基於之前提交作爲基礎開發;);
rebase順序爲 1,2,3,從上到下,最底部爲最後一次提交對象;不同於log的記錄順序,最近的位於最上方;
-
p
pick: 使用當前commit; -
e
edit: 使用當前commit,停止修改; -
s
squash(壓扁): 使用當前commit,和先前的commit合併;
將某次commit 前方的 pick 改爲 edit ,可多次修正提交內容;之後多次
git commit --amend
, git rebase --continue
;完成對多個提交的修改;
重排提交和刪除提交
修改commit的順序和刪除某些commit對象;直接使用vim 將多個文件交換位置,和刪除某些節點;
squash壓扁提交
交互式的衍合可以將一系列的提交壓爲單一提交; 指定某一提交前爲squash
,git會同時應用那個變更和它之前的變更並將提交說明歸併;
拆分提交
撤銷一次提交,多次部分的暫存或提交直到結束;rebase -i
腳本中對commit對象使用edit
;
那裏你可以用git reset HEAD^
對那次提交進行一次混合的重置,這將撤銷那次提交併且將修改的文件撤回。此時可暫存並提交文件,直到你擁有多次提交,結束後,git rebase --continue
;
原歷史記錄
pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added `cat-file`
退出命令後,先應用第一次提交f7f3f6d,後進入edit;
git操作;
$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue
應用後最後一次提交a5f4a0d,提交歷史:
$ git log -4 --pretty=format:"%h %s"
1c002dd added `cat-file`
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit
與rebase一樣,確保不包含已經推送到共享倉庫的提交;
核彈級選項filter-branch
使用腳本的方式修改大量的提交;
從所有提交中刪除一個文件
git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
--tree-filter
每次檢出項目時先執行指定的命令然後重新提交結果;
將一個子目錄設置爲新的根目錄
git filter-branch --subdirectory-filter 子目錄名 HEAD
全局性地更換電子郵件地址
$ git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="[email protected]";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
最後
項目還是推薦使用sourceTree
等圖形化工具吧;
兩個重點:
- git要常用
分支
; - 使用
rebase
-i
交互性的變基 可產生線性的提交歷史,更直觀;