生活随笔
收集整理的這篇文章主要介紹了
Git之深入解析工作流程、常用命令与Reset模式分析
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、Git 工作流程
① 工作區域
四個區: Index / Stage:暫存區(和 git stash 命令暫存的地方不一樣) 五種狀態:
工作區: 程序員進行開發改動的地方,是我們當前看到的,也是最新的。 平常我們開發就是拷貝遠程倉庫中的一個分支,基于該分支進行開發。在開發過程中就是對工作區的操作。 暫存區: .git 目錄下的 index 文件,暫存區會記錄 git add 添加文件的相關信息(文件名、大小、timestamp,…),不保存文件實體,通過 id 指向每個文件實體。可以使用 git status 查看暫存區的狀態,暫存區標記了當前工作區中,哪些內容是被 git 管理的。 當完成某個需求或功能后需要提交到遠程倉庫,那么第一步就是通過 git add 先提交到暫存區,被 git 管理。 本地倉庫: 保存了對象被提交過的各個版本,比起工作區和暫存區的內容,它要更舊一些。 git commit 后同步 index 的目錄樹到本地倉庫,方便從下一步通過 git push 同步本地倉庫與遠程倉庫的同步。 遠程倉庫: 遠程倉庫的內容可能被分布在多個地點的處于協作關系的本地倉庫修改,因此它可能與本地倉庫同步,也可能不同步,但是它的內容是最舊的。
$ git init
$ git add README
. md
$ git commit
- m
"first commit"
關聯遠程倉庫:當使用上述命令來關聯遠程服務器倉庫的時候,本地 .git 目錄也是會發生改變的,通過命令查看 .git/config 文件的話,可以看到配置文件中出現了 [remote] 字段:
$ git remote add origin git@github
. com
: escapelife
/ git
- demo
. git
? cat
. git
/ config
[ core
] repositoryformatversion
= 0 filemode
= truebare
= falselogallrefupdates
= trueignorecase
= trueprecomposeunicode
= true
[ remote
"origin" ] url
= git@github
. com
: escapelife
/ git
- demo
. gitfetch
= + refs
/ heads
/ * : refs
/ remotes
/ origin
/ *
當執行如下命令,將本地 master 分支推送到遠程 origin 倉庫的 master 分支之后,登錄 GitHub 就可以看到推送的文件及目錄內容。 推送分支內容的時候,會列舉推送的 objects 數量,并將其內容進行壓縮,之后推送到遠程的 GitHub 倉庫,并且創建一個遠程的 master 分支(origin 倉庫)。
$ git push
- u origin master
推送之后,可以發現,本地的 .git 生成一些文件和目錄,它們都是什么呢?如下所示,會新增四個目錄和兩個文件,皆為遠程倉庫的信息,當通過命令查看 master 這個文件的內容時,會發現其也是一個 commit 對象。此時與本地 master 分支所指向的一致,而其用于表示遠程倉庫的當前版本,用于和本地進行區別和校對的:
? tree
. git
├── logs
│ ├── HEAD
│ └── refs
│ ├── heads
│ │ ├── dev
│ │ ├── master
│ │ └── tmp
│ └── remotes
│ └── origin
│ └── master
└── refs├── heads│ ├── dev│ ├── master│ └── tmp├── remotes │ └── origin │ └── master └── tags
小結 任何修改都是從進入 index 區才開始被版本控制; 只有把修改提交到本地倉庫,該修改才能在倉庫中留下痕跡; 與協作者分享本地的修改,可以把它們 push 到遠程倉庫來共享。
② 分支管理
團隊開發中,遵循一個合理、清晰的 Git 使用流程,是非常重要的,否則每個人都提交一堆雜亂無章的 commit,項目很快就會變得難以協調和維護。 develop 分支 - 開發分支: develop 分支作為常駐開發分支,用來匯總下個版本的功能提交,在下個版本的 release 分支創建之前不允許提交非下個版本的功能到 develop 分支; develop 分支內容會自動發布到內網開發環境,確保 develop 分支隨時可編譯、可運行,上面的功能模塊是相對穩定、干凈的,隨時可以在 develop 上拉 feature 分支進行開發。 feature 分支 - 功能分支: 對于新功能開發,應從 develop 上切出一個 feature 分支進行開發,命名格式為 feature/project,其中功能名使用小寫單詞結合中劃線連綴方式,如 feature/update-web-style; feature 分支進行編譯通過并自測通過后,再合并到主干 develop 分支上。 補丁分支 - hotfix branch: 所謂補丁分支,就是當代碼已經部署到了正式環境之后發現沒有預測到底 Bug 時,通過補丁分支進行修改之后,生成補丁進行修復; 當然也可以說不使用布丁分支,重新發布新版本也是可以的,但是這樣還是不太合理。 master 分支 - 主分支: 當 develop 分支開發完成一個版本的時候,測試沒有問題之后就可以將其提交之后合并到 master 分支,master 分支內容會自動發布到內網正式環境; 需要注意的是,一般情況從開發分支合入到主干分支不會有代碼沖突的,如果有的話那就是沒有按照上述流程嚴格執行的結果。 release 分支 - 預發布分支: 最后就到了發包的最后階段,將已經在 master 內網正式環境上測試沒有問題的版本合入 release 分支,打包給客戶部署或者更新線上環境,完成最后的更新操作。
③ 代碼規范
提交代碼其實是有很多講究的,如果都按照自己的想法隨意的提交代碼,到最后自己都不知道當時這次提交到底是為了解決什么問題,良好的代碼提交習慣即有利于自己之后的審查,也有助于其他人觀看,同時利用腳本來提取有價值的信息,如查看人個人的工作量,每日的工作任務等。 提交模板 - commit model,我們更多使用的是如下簡化版 commit 模板:
< 提交類型
> - < 項目名稱
> : < 本次代碼提交的動機
>
feat
- Pandas
: update runtime to V1
. 3.0
提交類型類型說明使用頻率 feat 增加新功能 高 fix 修復 Bug 高 perf 提高性能的代碼 高 style 編碼規范或風格上的修改,不影響功能 高 docs 僅改變項目文檔 高 build 改變項目構建流程或包依賴 低 ci 改變 CI 配置或執行腳本 低 test 添加缺失測試或更正現有測試 低 refactor 代碼更改既不修復錯誤也不添加功能 低
④ 使用技巧
代碼合并 - pull request: 功能分支合并進 master 分支,必須通過 Pull Request 操作,在 Gitlab 里面叫做 Merge Request。 Pull Request 本質其實就是一種對話機制,你可以在提交的時候附加上有用的信息,并 @ 相關的核心開發者或團隊,引起他們的注意,讓他們為了的代碼把好最后一道關卡,保證你的代碼質量。
分支保護 - protected branch: master 分支應該受到保護,不是每個人都可以修改這個分支,以及擁有審批 Pull Request 的權力; Github 和 Gitlab 都提供“保護分支”的功能。
新建問題 - issue: Issue 用于Bug 追蹤和需求管理,建議先新建 Issue,再新建對應的功能分支; 功能分支總是為了解決一個或多個 Issue,功能分支的名稱,可以與 issue 的名字保持一致,并且以 issue 的編號起首,比如 15-change-password; 開發完成后,在提交說明里面,可以寫上"fixes #15" 或者 “closes #67”; Github 和 gitlab 規定,只要 commit message 里面有下面這些動詞+編號,就會關閉對應的 issue。
close
closes
closed
fix
fixes
fixed
resolve
resolves
resolved
這種方式還可以一次關閉多個 issue,或者關閉其他代碼庫的 issue,格式是 username/repository#issue_number。Pull Request 被接受以后,issue 關閉,原始分支就應該刪除,如果以后該 issue 重新打開,新分支可以復用原來的名字。 沖突解決 - merge: Git 有兩種合并:一種是 “直進式合并”(fast forward)不生成單獨的合并節點;另一種是 “非直進式合并” (none fast-forword)會生成單獨節點; 前者不利于保持 commit 信息的清晰,也不利于以后的回滾,建議總是采用后者(即使用 --no-ff 參數)只要發生合并,就要有一個單獨的合并節點。 合并提交 - squash: 為了便于他人閱讀你的提交,也便于 cherry-pick 或撤銷代碼變化,在發起 Pull Request 之前,應該把多個 commit 合并成一個。
前提是,該分支只有你一個人開發,且沒有跟 master 合并過,這可以采用 rebase 命令附帶的 squash 操作:
$ git checkout master
$ git pull
$ git checkout
- b myfeature
$ git add
.
$ git status
$ git commit
- m
"this is a test."
$ git fetch origin
$ git rebase origin
/ master
$ git rebase
- i origin
/ master
pick
07c5abd Introduce OpenPGP
and teach basic usage
s de9b1eb Fix PostChecker
: : Post
s
3e7ee36 Hey kids
, stop
all the highlighting
pick fa20af3 git interactive rebase
, squash
, amend
$ git push
- - force origin myfeature
編號參數類型使用說明 1 p/pick 正常選中 2 r/reword 正常選中,并且修改提交信息 3 e/edit 正常選中,rebase 時會暫停,允許你修改這個 commit 4 s/squash 正常選中,會將當前 commit 與上一個 commit 合并 5 f/fixup 與 squash 相同,但不會保存當前 commit 的提交信息 6 x/exec 執行其他 shell 命令
二、常用的 Git 命令
① HEAD
HEAD 始終指向當前所處分支的最新的提交點,所處的分支變化了,或者產生了新的提交點,HEAD 就會跟著改變。
② init
執行 git init,完成如下命令之后,可以得到下圖所示的內容,右側的就是 git 創建的代碼倉庫,其中包含了用于版本管理所需要的內容:
$ mkdir git
- demo
$ cd git
- demo
& & git init
$ rm
- rf
. git
/ hooks
/ * . sample
$ watch
- n
1 - d find
.
? tree
. git
. git
├── HEAD
├── config
├── description
├── hooks
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs├── heads└── tags
.git/config - 當前代碼倉庫本地的配置文件: 本地配置文件(.git/config)和全局配置文件(~/.gitconfig); 通過執行如下命令,可以將用戶配置記錄到本地代碼倉庫的配置文件中去; git config user.name “demo”; git config user.email “demo@demo.com”。
? cat
. git
/ config
[ core
] repositoryformatversion
= 0 filemode
= truebare
= falselogallrefupdates
= trueignorecase
= trueprecomposeunicode
= true
[ user
] name
= demoemail
= demo@demo
. com
.git/objects - 當前代碼倉庫代碼的存儲位置:
? ll
. git
/ objects
total
0
drwxr
- xr
- x
2 escape staff
64B Nov
23 20 : 39 info
drwxr
- xr
- x
2 escape staff
64B Nov
23 20 : 39 pack? ll
. git
/ objects
/ info
? ll
. git
/ objects
/ pack
? cat
. / . git
/ info
/ exclude
.git/hooks - 當前代碼倉庫默認鉤子腳本:
. / . git
/ hooks
/ commit
- msg
. sample
. / . git
/ hooks
/ pre
- rebase
. sample
. / . git
/ hooks
/ pre
- commit
. sample
. / . git
/ hooks
/ applypatch
- msg
. sample
. / . git
/ hooks
/ fsmonitor
- watchman
. sample
. / . git
/ hooks
/ pre
- receive
. sample
. / . git
/ hooks
/ prepare
- commit
- msg
. sample
. / . git
/ hooks
/ post
- update
. sample
. / . git
/ hooks
/ pre
- merge
- commit
. sample
. / . git
/ hooks
/ pre
- applypatch
. sample
. / . git
/ hooks
/ pre
- push
. sample
. / . git
/ hooks
/ update
. sample
? cat
. git
/ HEAD
ref
: refs
/ heads
/ master
? ll
. git
/ refs
total
0
drwxr
- xr
- x
2 escape staff
64B Nov
23 20 : 39 heads
drwxr
- xr
- x
2 escape staff
64B Nov
23 20 : 39 tags? ll
. git
/ refs
/ heads
? ll
. git
/ refs
/ tags
.git/description - 當前代碼倉庫的描述信息:
? cat
. git
/ description
Unnamed repository
; edit this
file 'description' to name the repository
.
③ add
add 主要實現將工作區修改的內容提交到暫存區,交由 git 管理:
命令描述 git add 添加當前目錄的所有文件到暫存區 git add < dir> 添加指定目錄到暫存區,包括子目錄 git add < file1> 添加指定文件到暫存區
執行完成如下命令之后,可以得到下圖所示的內容,可以發現右側新增了一個文件,但是 git 目錄里面的內容絲毫沒有變化,這是因為,現在執行的修改默認是放在工作區的,而工作區里面的修改不歸 git 目錄去管理:
$ echo
"hello git" > helle
. txt
$ git status
$ git add hello
. txt
$ watch
- n
1 - d find
.
生成的 8d 目錄以及下面的文件,而其名稱的由來是因為 git 對其進行了一個叫做 SHA1 的 Hash 算法,用于將文件內容或者字符串變成這么一串加密的字符:
$ git cat
- file - t
8d0e41
blob
$ git cat
- file - p
8d0e41
hello git
$ git cat
- file - s
8d0e41
10
blob
10 \0hello git
執行 git add 命令將文件從工作區添加到暫存區里面,git 會把幫助我們生成一些 git 的對象,它存儲的是文件的內容和文件類型并不存儲文件名稱。 為了驗證上述的說法,可以添加同樣的內容到另一個文件,然后進行提交,來觀察 .git 目錄的變化。可以看到,右側的 objects 目錄并沒有新增目錄和文件,這就可以證明,blob 類型的 object 只存儲的是文件的內容,如果兩個文件的內容一致的話,則只需要存儲一個 object 即可。 那么 object 為什么沒有存儲文件名稱呢?這里因為 SHA1 的 Hash 算法計算哈希的時候,本身就不包括文件名稱,所以取什么名稱都是無所謂的。
$ echo
"hello git" > tmp
. txt
$ git add tmp
. txt
$ watch
- n
1 - d find
.
④ commit
commit 主要實現將暫存區的內容提交到本地倉庫,并使得當前分支的 HEAD 向后移動一個提交點:
命令描述 git commit -m 提交暫存區到本地倉庫,message 代表說明信息 git commit -m 提交暫存區的指定文件到本地倉庫 git commit --amend -m 使用一次新的 commit,替代上一次提交
Git 倉庫中的提交記錄保存的是我們的目錄下所有文件的快照,就像是把整個目錄復制,然后再粘貼一樣,但比復制粘貼優雅許多。Git 希望提交記錄盡可能地輕量,因此在每次進行提交時,它并不會盲目地復制整個目錄,條件允許的情況下,它會將當前版本與倉庫中的上一個版本進行對比,并把所有的差異打包到一起作為一個提交記錄,Git 還保存提交的歷史記錄,這也是為什么大多數提交記錄的上面都有父節點的原因。 當使用 add 命令將工作區提交到暫存區,而暫存區其實保存的是當前文件的一個狀態,其中包括有哪些目錄和文件,以及其對應的大小和內容等信息,但是最終是需要將其提交到代碼倉庫(本地)的,而其命令就是 git commit:
而當執行 git commit 命令的時候,究竟都發生了什么呢?可以看到當提交之后,.git 目錄中生成了兩個信息的 object 對象,其中 logs 和 refs 目錄都有新的文件生成。通過如下操作,可以查看到其提交的類型和對應內容:
$ git commit
- m
"1st commit" $ git cat
- file - t
6e4a700
$ git cat
- file - p
6e4a700 $ git cat
- file - t
64d6ef5
$ git cat
- file - p
64d6ef5
$ watch
- n
1 - d tree
. git
當執行 git commit 命令之后,會生成一個 commit 對象和一個 tree 對象,commit 對象內容里面包含一個 tree 對象和相關提交信息,而 tree 對象里面則包含這次提交版本里面的文件狀態(文件名稱和 blob 對象),這樣就知道了這次提交的變動:
這次提交之后,處理 objects 目錄發生變動之外,還有一些其他的變化,比如 logs 和 refs 的目錄有所變化。再查看 refs 目錄里面的內容,發現其指向 6e4a70 這個 commit 對象,即當前 master 分支上面最新的提交就是這個 6e4a70。 而這個 6e4a70 這個 commit 對象,有一個 HEAD 的指向,就是 .git 目錄下的 HEAD 文件,其實質就是一個指針,其永遠指向我們當前工作的分支,即這里我們工作在 master 分支上。當切換分支的時候,這個文件的指向也會隨機改變的。
$ cat
. git
/ refs
/ heads
/ master
$ cat
. git
/ HEAD
$ watch
- n
1 - d tree
. git
當再次對 file2.txt 文件的內容進行變更、添加以及提交之后,發現在提交的時候,查看的 commit 對象的內容時,其包含有父節點的 commit 信息:
$ echo
"file2.txt" > file2
. txt
$ git status
$ git add file2
. txt
$ git ls
- files
- s
$ git cat
- file - p
0ac9638
$ git commit
- m
"2nd commit"
$ git cat
- file - p bab53ff
$ git cat
- file - p
2f07720
$ watch
- n
1 - d tree
. git
在 Git 中空文件夾是不算在追蹤范圍內的,而且添加文件夾并不會增加 object 對象,當查看 index 內容的時候,會發現文件名稱是包含相對路徑的。 而當通過 commit 命令提交之后,會發現生成了三個 object 對象,因為 commit 操作不會生成 blob 對象,所以分別是一個 commit 對象和兩個 tree 對象。可以發現,tree 對象里面有包含一個目錄的 tree,其里面包含對象文件內容。 下圖所示的文件狀態,可以體會到 git 中版本的概念,即 commit 對象指向一個該版本中的文件目錄樹的根(tree),然后 tree 在指向 blob 對象(文件)和 tree 對象(目錄),這樣就可以無限的往復下去形成一個完整的版本:
$ mkdir floder1
$ echo
"file3" > floder1
/ file3
. txt
$ git add floder1
$ git ls
- files
- s
$ git commit
- m
"3rd commit"
$ git cat
- file - p
1711e01
$ git cat
- file - p
9ab67f8 $ watch
- n
1 - d tree
. git
⑤ checkout
當執行 checkout 命令的時候,其不光可以切換分支,而且可以切換到指定的 commit 上面,即 HEAD 文件會指向某個 commit 對象。在 Git 里面,將 HEAD 文件沒有指向 master 的這個現象稱之為 detached HEAD。這里不管 HEAD 文件指向的是分支名稱也好,是 commit 對象也罷,其實本質都是一樣的,因為分支名稱也是指向某個 commit 對象的。
$ git checkout
6e4a700
$ git log
$ glo
= git log
當切換到指定的 commit 的時候,如果需要在對應的 commit 上繼續修改代碼提交的話,可以使用上文提及的 swtich 命令創建新分支,再進行提交。但是,通常我們都不會這么玩,都會使用 checkout 命令來創建新分支的:
$ git checkout
- b tmp
$ git log
即使可以這樣操作,我們也很少使用,創建的 dev 分支的時候,創建該分支并有了一個新的提交,但是沒有合并到 master 分支就直接刪除,就無法再“看到”。其實,在 Git 里面任何的操作,比如分支的刪除,它只是刪除了指向某個特定 commit 的指針引用而已,而那個 commit 本身并不會被刪除,即 dev 分支的那個 commit 提交還是在的。 那怎么找到這個 commit 呢? 第一種方法:在 objects 目錄下面,自己一個一個看,然后切換過去; 第二種方法:使用 Git 提供的 git reflog 專用命令來查找,該命令的作用就是用于將之前的所有操作都記錄下來。
$ git reflog
$ git checkout
9fb7a14
$ git checkout
- b dev
$ glo
= git log
⑥ branch
涉及到協作,自然會涉及到分支。關于分支,大概有四種操作: 分支是用來標記特定代碼的提交,每一個分支通過 SHA1sum 值來標識,所以對分支的操作是輕量級的,改變的僅僅是 SHA1sum 值:
命令描述 git branch 列出所有本地分支 git branch -r 列出所有遠程分支 git branch -a 列出所有本地分支和遠程分支 git branch < branch-name> 新建一個分支,但依然停留在當前分支 git checkout -b < branch-name> 新建一個分支,并切換到該分支 git branch --track < branch> 新建一個分支,與指定的遠程分支建立追蹤關系 git checkout < branch-name> 切換到指定分支,并更新工作區 git branch -d < branch-name> 刪除分支 git push origin --delete < branch-name> 刪除遠程分支
分支就是一個有名字的(master/dev)指向 commit 對象的一個指針,在初始化倉庫的時候,會默認分配一個叫做 master 的分支(在最新的版本默認倉庫已經變更為 main),而 master 分支就是指向最新的一次提交。為什么需要給分支起名字呢?就是為了方便使用和記憶,可以簡單理解為 alias 命令的意義一致:
要實現一個分支,最基本需要解決兩個問題,第一個就是需要存儲每一個分支指向的 commit,第二個問題就是在切換分支的時候幫助我們標識當前分支。在 git 中,它有一個非常特殊的 HEAD 文件,而 HEAD 文件是一個指針,其有一個特性就是總會指向當前分支的最新的一個 commit 對象。而這個 HEAD 文件正好,解決了這兩個問題。 當從 master 切換分支到 dev 的時候,HEAD 文件也會隨即切換,即指向 dev 這個指針:
$ cat
. git
/ HEAD
$ cat
. git
/ refs
/ heads
/ master
$ git cat
- file - t
1711e01
$ glo
= git log
$ git branch
$ git branch dev
$ ll
. git
/ refs
/ heads
$ cat
. git
/ refs
/ heads
/ master
$ cat
. git
/ refs
/ heads
/ dev
$ cat
. git
/ HEAD
$ git checkout dev
$ cat
. git
/ HEAD
$ glo
= git log
即使刪除分支,但是該分支上一些特有的對象并不會被刪除的,這些對象其實就是俗稱的垃圾對象,還有多次使用 add 命令所產生的也有垃圾對象:
$ echo
"dev" > dev
. txt
$ git add dev
. txt
$ git commit
- m
"1st commit from dev branch"
$ git checkout master
$ git branch
- d dev
$ git branch
- D dev
$ git cat
- file - t
861832c
$ git cat
- file - p
861832c
$ git cat
- file - p
680f6e9
$ git cat
- file - p
38f8e88
$ glo
= git log
⑦ fetch
命令描述 git fetch <遠程主機名> 將某個遠程主機的更新全部取回本地 git fetch <遠程主機名> <分支名> 取回特定分支的更新 git fetch origin master 取回 origin 主機的 master 分支
一旦遠程主機的版本庫有了更新(Git 術語叫做 commit),需要將這些更新取回本地,這時就要用到 git fetch 命令:
$ git fetch
< 遠程主機名
>
上面命令將某個遠程主機的更新,全部取回本地。git fetch 命令通常用來查看其他人的進程,因為它取回的代碼對你本地的開發代碼沒有影響。默認情況下,git fetch 取回所有分支(branch)的更新。如果只想取回特定分支的更新,可以指定分支名。 取回更新后,會返回一個 FETCH_HEAD,指的是某個 branch 在服務器上的最新狀態,可以在本地通過它查看剛取回的更新信息:
$ git log
- p FETCH_HEAD
$ git fetch origin master
所取回的更新,在本地主機上要用“遠程主機名/分支名”的形式讀取,比如 origin 主機的 master,就要用 origin/master 讀取。git branch 命令的 -r 選項,可以用來查看遠程分支,-a 選項查看所有分支:
$ git branch
- r
origin
/ master$ git branch
- a
* masterremotes
/ origin
/ master
上面命令表示,本地主機的當前分支是 master,遠程分支是 origin/master。取回遠程主機的更新以后,可以在它的基礎上,使用 git checkout 命令創建一個新的分支。如下所示,在 origin/master 的基礎上,創建一個新分支:
$ git checkout
- b newBrach origin
/ master
此外,也可以使用 git merge 命令或者 git rebase 命令,在本地分支上合并遠程分支:
$ git merge origin
/ master
# 或者
$ git rebase origin
/ master
上面命令表示在當前分支上,合并 origin/master。
⑧ merge
merge 命令把不同的分支合并起來,如上圖,在實際開放中,我們可能從 master 分支中切出一個分支,然后進行開發完成需求,中間經過 R3、R4、R5 的 commit 記錄,最后開發完成需要合入 master 中,這便用到了 merge。
命令描述 git fetch < remote> merge 之前先拉一下遠程倉庫最新代碼 git merge < branch> 合并指定分支到當前分支
一般在 merge 之后,會出現 conflict,需要針對沖突情況,手動解除沖突,主要是因為兩個用戶修改了同一文件的同一塊區域。
git merge
-- abort
撤銷合并:撤銷合并時采用 git reset/revert 操作。
⑨ rebase
rebase 又稱為衍合,是合并的另外一種選擇。在開始階段,我們處于 new 分支上,執行 git rebase dev,那么 new 分支上新的 commit 都在 master 分支上重演一遍,最后 checkout 切換回到 new 分支。這一點與 merge 是一樣的,合并前后所處的分支并沒有改變。 git rebase dev,通俗的解釋就是 new 分支想站在 dev 的肩膀上繼續下去,rebase 也需要手動解決沖突。 rebase 與 merge 的區別: 現在有這樣的兩個分支:test 和 master,提交如下:
D
-- - E test
/ A
-- - B
-- - C
-- - F master
在 master 執行 git merge test,然后會得到如下結果:
D
-- -- -- -- E
/ \A
-- - B
-- - C
-- - F
-- -- G test
, master
在 master 執行 git rebase test,然后得到如下結果:
A
-- - B
-- - D
-- - E
-- - C
'---F' test
, master
可以看到,merge 操作會生成一個新的節點,之前的提交分開顯示,而 rebase 操作不會生成新的節點,是將兩個分支融合成一個線性的提交。 如果想要一個干凈的,沒有 merge commit 的線性歷史樹,那么應該選擇 git rebase;如果想保留完整的歷史記錄,并且想要避免重寫 commit history 的風險,應該選擇使用 git merge。
⑩ reset
reset 命令把當前分支指向另一個位置,并且相應的變動工作區和暫存區:
命令描述 git reset —soft < commit> 只改變提交點,暫存區和工作目錄的內容都不改變 git reset —mixed < commit> 改變提交點,同時改變暫存區的內容 git reset —hard < commit> 暫存區、工作區的內容都會被修改到與提交點完全一致的狀態 git reset --hard HEAD 讓工作區回到上次提交時的狀態
? revert
git revert 用一個新提交來消除一個歷史提交所做的任何修改:
說明: git revert 是用一次新的 commit 來回滾之前的 commit,git reset 是直接刪除指定的commit; 在回滾這一操作上看,效果差不多,但是在日后繼續 merge 以前的老版本時有區別。因為 git revert 是用一次逆向的 commit “中和”之前的提交,因此日后合并老的 branch 時,導致這部分改變不會再次出現,減少沖突。但是 git reset 是之間把某些 commit 在某個 branch 上刪除,因而和老的branch 再次 merge 時,這些被回滾的 commit 應該還會被引入,產生很多沖突。關于這一點,請參考:git reset revert 回退回滾取消提交返回上一版本; git reset 是把 HEAD 向后移動了一下,而 git revert 是 HEAD 繼續前進,只是新的 commit 的內容和要 revert 的內容正好相反,能夠抵消要被 revert 的內容。
? push
命令描述 git push < remote> 上傳本地指定分支到遠程倉庫 git push < remote> --force 強行推送當前分支到遠程倉庫,即使有沖突 git push < remote> --all 推送所有分支到遠程倉庫
git push 命令用于將本地分支的更新,推送到遠程主機,它的格式與 git pull 命令相仿:
$ git push
< 遠程主機名
> < 本地分支名
> : < 遠程分支名
>
注意,分支推送順序的寫法是<來源地>:<目的地>,所以 git pull 是<遠程分支>:<本地分支>,而 git push 是<本地分支>:<遠程分支>。如果省略遠程分支名,則表示將本地分支推送與之存在“追蹤關系”的遠程分支(通常兩者同名),如果該遠程分支不存在,則會被新建。 將本地的 master 分支推送到 origin 主機的 master 分支,如果后者不存在,則會被新建:
$ git push origin master
如果省略本地分支名,則表示刪除指定的遠程分支,因為這等同于推送一個空的本地分支到遠程分支。刪除 origin 主機的 master 分支,如下所示:
$ git push origin
: master
# 等同于
$ git push origin
-- delete master
如果當前分支與遠程分支之間存在追蹤關系,則本地分支和遠程分支都可以省略。將當前分支推送到 origin 主機的對應分支:
$ git push origin
如果當前分支只有一個追蹤分支,那么主機名都可以省略:
$ git push
如果當前分支與多個主機存在追蹤關系,則可以使用 -u 選項指定一個默認主機,這樣后面就可以不加任何參數使用 git push。將本地的 master 分支推送到 origin 主機,同時指定 origin 為默認主機,后面就可以不加任何參數使用 git push:
$ git push
- u origin master
不帶任何參數的 git push,默認只推送當前分支,這叫做 simple 方式。此外,還有一種 matching 方式,會推送所有有對應的遠程分支的本地分支。Git 2.0 版本之前,默認采用 matching 方法,現在改為默認采用 simple 方式,如果要修改這個設置,可以采用 git config 命令:
$ git config
-- global push
. default matching
# 或者
$ git config
-- global push
. default simple
還有一種情況,就是不管是否存在對應的遠程分支,將本地的所有分支都推送到遠程主機,這時需要使用 --all 選項,將所有本地分支都推送到 origin 主機:
$ git push
-- all origin
如果遠程主機的版本比本地版本更新,推送時 Git 會報錯,要求先在本地做 git pull 合并差異,然后再推送到遠程主機。這時,如果一定要推送,可以使用 --force 選項。如下所示,使用 --force 選項,結果導致遠程主機上更新的版本被覆蓋,除非很確定要這樣做,否則應該盡量避免使用 --force 選項:
$ git push
-- force origin
最后,git push 不會推送標簽(tag),除非使用 --tags 選項:
$ git push origin
-- tags
? pull
git pull 命令的作用是,取回遠程主機某個分支的更新,再與本地的指定分支合并。它的完整格式稍稍有點復雜:
$ git pull
< 遠程主機名
> < 遠程分支名
> : < 本地分支名
>
比如,取回 origin 主機的 next 分支,與本地的 master 分支合并,需要寫成下面這樣:
$ git pull origin next
: master
如果遠程分支是與當前分支合并,則冒號后面的部分可以省略:
$ git pull origin next
上面命令表示,取回 origin/next 分支,再與當前分支合并。實質上,這等同于先做 git fetch,再做 git merge:
$ git fetch origin
$ git merge origin
/ next
在某些場合,Git 會自動在本地分支與遠程分支之間,建立一種追蹤關系(tracking)。比如,在 git clone 的時候,所有本地分支默認與遠程主機的同名分支,建立追蹤關系,也就是說,本地的 master 分支自動“追蹤” origin/master 分支。 Git 也允許手動建立追蹤關系,如下,指定 master 分支追蹤 origin/next 分支:
git branch
-- set
- upstream master origin
/ next
如果當前分支與遠程分支存在追蹤關系,git pull 就可以省略遠程分支名。如下,本地的當前分支自動與對應的 origin 主機"追蹤分支"(remote-tracking branch)進行合并:
$ git pull origin
如果當前分支只有一個追蹤分支,連遠程主機名都可以省略。如下,當前分支自動與唯一一個追蹤分支進行合并:
$ git pull
如果合并需要采用 rebase 模式,可以使用 --rebase 選項。
$ git pull
-- rebase
< 遠程主機名
> < 遠程分支名
> : < 本地分支名
>
如果遠程主機刪除了某個分支,默認情況下,git pull 不會在拉取遠程分支的時候,刪除對應的本地分支。這是為了防止,由于其他人操作了遠程主機,導致 git pull 不知不覺刪除了本地分支。 但是,我們可以改變這個行為,加上參數 -p 就會在本地刪除遠程已經刪除的分支:
$ git pull
- p
# 等同于下面的命令
$ git fetch
-- prune origin
$ git fetch
- p
? stash
命令描述 git stash save “xx” 執行存儲,并添加備注,只執行 git stash 也是可以的,但查找時不方便識別 git stash list 查看 stash 了哪些存儲 git stash show 顯示做了哪些修改,默認 show 第一個存儲,如果要顯示其他存儲,后面加stash@{$num},比如第二個 git stash show stash@{1} git stash show -p 顯示第一個存儲的改動,如果想顯示其他存儲,加上 stash@{$num},比如第二個:git stash show stash@{1} -p git stash apply 應用某個存儲,但不會把存儲從存儲列表中刪除,默認使用第一個存儲,即 stash@{0},如果要使用其他的,git stash apply stash@{$num} , 比如第二個:git stash apply stash@{1} git stash pop 命令恢復之前緩存的工作目錄,將緩存堆棧中的對應 stash 刪除,并將對應修改應用到當前的工作目錄下,默認為第一個 stash,即 stash@{0},如果要應用并刪除其他 stash,命令:git stash pop stash@{$num},比如應用并刪除第二個:git stash pop stash@{1} git stash drop stash@{$num} 丟棄 stash@{$num} 存儲,從列表中刪除這個存儲 git stash clear 刪除所有緩存的stash
注意:沒有在 git 版本控制中的文件,是不能被 git stash 存起來的。如果新增了一個文件,直接執行 git stash 是不會存起來的,可以先執行 git add,再執行 git stash。這個時候,想切分支就再也不會報錯有改動未提交了。 如果要應用這些 stash,直接使用 git stash apply 或者 git stash pop 就可以再次導出來了。 總結: git add 只是把文件加到 git 版本控制里,并不等于就被 stash 起來了,git add 和 git stash 沒有必然的關系,但是執行 git stash 能正確存儲的前提是文件必須在 git 版本控制中才行。 常規 git stash 的一個限制是它會一下暫存所有的文件。有時,只備份某些文件更為方便,讓另外一些與代碼庫保持一致。一個非常有用的技巧,用來備份部分文件: 調用 git stash –keep-index,只會備份那些沒有被 add 的文件; 調用 git reset 取消已經 add 的文件的備份,繼續自己的工作。
? clone
遠程操作的第一步,通常是從遠程主機克隆一個版本庫,這時就要用到 git clone 命令:
$ git clone
< 版本庫的網址
>
$ git clone https
:
命令會在本地主機生成一個目錄,與遠程主機的版本庫同名。如果要指定不同的目錄名,可以將目錄名作為 git clone 命令的第二個參數:
$ git clone
< 版本庫的網址
> < 本地目錄名
>
git clone 支持多種協議,除了 HTTP(s) 以外,還支持 SSH、Git、本地文件協議等,如下:
$ git clone http
[ s
] :
$ git clone ssh
:
$ git clone git
:
$ git clone
/ opt
/ git
/ project
. git
$ git clone file
:
$ git clone ftp
[ s
] :
$ git clone rsync
:
$ git clone
[ user@
] example
. com
: path
/ to
/ repo
. git
/
通常來說,Git 協議下載速度最快,SSH 協議用于需要用戶認證的場合。
? remote
為了便于管理,Git 要求每個遠程主機都必須指定一個主機名,git remote 命令就用于管理主機名。不帶選項的時候,git remote 命令列出所有遠程主機:
$ git remote
origin
$ git remote
- v
origin git@github
. com
: jquery
/ jquery
. git ( fetch
)
origin git@github
. com
: jquery
/ jquery
. git ( push
)
上面命令表示,當前只有一臺遠程主機,叫做 origin,以及它的網址。克隆版本庫的時候,所使用的遠程主機自動被 Git 命名為 origin,如果想用其他的主機名,需要用 git clone 命令的 -o 選項指定:
$ git clone
- o jQuery https
:
$ git remote
jQuery
上面命令表示,克隆的時候,指定遠程主機叫做 jQuery。git remote show 命令加上主機名,可以查看該主機的詳細信息:
$ git remote show
< 主機名
>
git remote add 命令用于添加遠程主機:
$ git remote add
< 主機名
> < 網址
>
git remote rm 命令用于刪除遠程主機:
$ git remote rm
< 主機名
>
git remote rename 命令用于遠程主機的改名:
$ git remote rename
< 原主機名
> < 新主機名
>
? 其他命令
命令描述 git status 顯示有變更的文件 git log 顯示當前分支的版本歷史 git diff 顯示暫存區和工作區的差異 git diff HEAD 顯示工作區與當前分支最新 commit 之間的差異 git diff --cached 查看到暫存區和本地倉庫之間的差異 git diff master 查看到master和本地倉庫之間的差異 git cherry-pick <commit> 選擇一個 commit,合并進當前分支
三、Git Reset 三種模式
使用 Git 時有可能 commit 提交代碼后,發現這一次 commit 的內容是有錯誤的,那么有兩種處理方法: 使用 git reset 命令撤銷這一次錯誤的 commit; 第一種方法多一條 commit 記錄;第二種方法,錯誤的 commit 不會被保留下來。 一句話概括 git reset:
git reset:Reset current HEAD to the specified state
例如,有一次 commit 不是很滿意,需要回到上一次的 Commit 里面,那么這個時候就需要通過 reset,把 HEAD 指針指向上一次的 commit 的點,它有三種模式:soft、mixed、hard,如下所示:
理解了這三個模式,對于使用這個命令很有幫助。在理解這三個模式之前,需要略微知道一點 Git 的基本流程,如下所示: Index/Stage 暫存區域,和 git stash 命令暫存的地方不一樣,使用 git add xx,就可以將 xx 添加近 Stage 里面; Repository 提交的歷史,即使用 git commit 提交后的結果。
簡單敘述一下把文件存入 Repository 流程: 剛開始 working tree、index 與 repository(HEAD) 里面的內容都是一致的:
當 git 管理的文件夾里面的內容出現改動后,此時 working tree 的內容就會跟 index 及 repository(HEAD) 的不一致,而 Git 知道是哪些文件(Tracked File)被改動過,直接將文件狀態設置為 modified (Unstaged files): 當執行 git add 后,會將這些改變的文件內容加入 index 中 (Staged files),所以此時 working tree 跟 index 的內容是一致的,但與 repository(HEAD) 內容不一致:
接著執行 git commit 后,將 Git 索引中所有改變的文件內容提交至 Repository 中,建立出新的 commit 節點(HEAD)后,working tree、index 與 repository(HEAD) 區域的內容又會保持一致:
① reset --hard:重置 stage 區和工作目錄
reset --hard 會在重置 HEAD 和 branch 的同時,重置 stage 區和工作目錄里的內容。當在 reset 后面加了 --hard 參數時,stage 區和工作目錄里的內容會被完全重置為和 HEAD 的新位置相同的內容,換句話說,就是沒有 commit 的修改會被全部擦掉:
然后,執行 reset 并附上了 --hard 參數:
git reset
-- hard
HEAD 和當前 branch 切到上一條 commit 的同時,工作目錄里的新改動和已經 add 到 stage 區的新改動也一起全都消失:
可以看到,在 reset --hard 后,所有的改動都被擦掉。
② reset --soft:保留工作目錄,并把重置 HEAD 所帶來的新的差異放進暫存區
reset --soft 會在重置 HEAD 和 branch 時,保留工作目錄和暫存區中的內容,并把重置 HEAD 所帶來的新的差異放進暫存區。 那么,什么是“重置 HEAD 所帶來的新的差異”?如下所示:
由于 HEAD 從 4 移動到了 3,而且在 reset 的過程中工作目錄和暫存區的內容沒有被清理掉,所以 4 中的改動在 reset 后就也成了工作目錄新增的“工作目錄和 HEAD 的差異”,這就是上面一段中所說的“重置 HEAD 所帶來的差異”。 此模式下會保留 working tree 工作目錄的內容,不會改變到目前所有的 git 管理的文件夾的內容;也會保留 index 暫存區的內容,讓 index 暫存區與 working tree 工作目錄的內容是一致的。就只有 repository 中的內容的更變需要與 reset 目標節點一致,因此原始節點與 reset 節點之間的差異變更集合會存在與 index 暫存區中(Staged files),所以可以直接執行 git commit 將 index 暫存區中的內容提交至 repository 中。 當我們想合并“當前節點”與“reset 目標節點”之間不具有太大意義的 commit 記錄(可能是階段性地頻繁提交)時,可以考慮使用 Soft Reset 來讓 commit 演進線圖較為清晰點:
同樣的情況下,把修改后的 AppDelegate.h 文件 add 到 stage 區,修改后的 AppDelegate.m 保留在工作目錄:
git show
-- stat
這時,執行 git reset --soft HEAD^:
那么除了 HEAD 和它所指向的 branch1 被移動到 HEAD^ 之外,原先 HEAD 處 commit 的改動(README.md 文件)也會被放進暫存區。 這就是 --soft 和 --hard 的區別:–hard 會清空工作目錄和暫存區的改動,而 --soft 則會保留工作目錄的內容,并把因為保留工作目錄內容所帶來的新的文件差異放進暫存區。
③ reset --mixed:保留工作目錄,并清空暫存區
reset 如果不加參數,那么默認使用 --mixed 參數,它的行為是:保留工作目錄,并且清空暫存區。也就是說,工作目錄的修改、暫存區的內容以及由 reset 所導致的新的文件差異,都會被放進工作目錄。簡而言之,就是把所有差異都混合(mixed)放在工作目錄中。 以上面的情況為例:
工作目錄的內容和 --soft 一樣會被保留,但和 --soft 的區別在于,它會把暫存區清空,并把原節點和 reset 節點的差異的文件放在工作目錄。總而言之,工作目錄的修改、暫存區的內容以及由 reset 所導致的新的文件差異,都會被放進工作目錄。
④ Reset 三種模式總結
reset 的本質:移動 HEAD 以及它所指向的 branch。實質上,reset 這個指令雖然可以用來撤銷 commit,但它的實質行為并不是撤銷,而是移動 HEAD ,并且捎帶上 HEAD 所指向的 branch(如果有的話)。也就是說,reset 這個指令的行為其實和它的字面意思“重置”十分相符:它是用來重置 HEAD 以及它所指向的 branch 的位置的。 而 reset --hard HEAD^ 之所以起到了撤銷 commit 的效果,是因為它把 HEAD 和它所指向的 branch 一起移動到了當前 commit 的父 commit 上,從而起到了“撤銷”的效果:
Git 的歷史只能往回看,不能向未來看,所以把 HEAD 和 branch 往回移動,就能起到撤回 commit 的效果。所以同理,reset --hard 不僅可以撤銷提交,還可以用來把 HEAD 和 branch 移動到其他的任何地方:
git reset
-- hard branch2
⑤ reset 三種模式區別和使用場景
區別: –hard:重置位置的同時,直接將 working Tree 工作目錄、index 暫存區及 repository 都重置成目標 Reset 節點的內容,所以效果看起來等同于清空暫存區和工作區; –soft:重置位置的同時,保留 working Tree 工作目錄和 index 暫存區的內容,只讓 repository 中的內容和 reset 目標節點保持一致,因此原節點和 reset 節點之間的【差異變更集】會放入 index 暫存區中(Staged files)。所以效果看起來就是工作目錄的內容不變,暫存區原有的內容也不變,只是原節點和 Reset 節點之間的所有差異都會放到暫存區中。 –mixed(默認):重置位置的同時,只保留 Working Tree 工作目錄的內容,但會將 Index 暫存區和 Repository 中的內容更改和 reset 目標節點一致,因此原節點和 Reset 節點之間的【差異變更集】會放入 Working Tree 工作目錄中。所以效果看起來就是原節點和 Reset 節點之間的所有差異都會放到工作目錄中。 使用場景: 要放棄目前本地的所有改變時,即去掉所有 add 到暫存區的文件和工作區的文件,可以執行 git reset -hard HEAD 來強制恢復 git 管理的文件夾的內容及狀態; 真的想拋棄目標節點后的所有 commit(可能覺得目標節點到原節點之間的 commit 提交都是錯了,之前所有的 commit 有問題)。 原節點和 reset 節點之間的【差異變更集】會放入 index 暫存區中(Staged files),所以假如之前工作目錄沒有改過任何文件,也沒 add 到暫存區,那么使用 reset --soft 后,可以直接執行 git commit 將 index 暫存區中的內容提交至 repository 中。為什么要這樣呢? 這樣做的使用場景是:假如想合并「當前節點」與「reset 目標節點」之間不具太大意義的 commit 記錄(可能是階段性地頻繁提交,就是開發一個功能的時候,改或者增加一個文件的時候就commit,這樣做導致一個完整的功能可能會好多個 commit 點,這時假如需要把這些 commit 整合成一個 commit 的時候)時,可以考慮使用 reset --soft 來讓 commit 演進線圖較為清晰。總而言之,可以使用 --soft 合并 commit 節點。 使用完 reset --mixed 后,可以直接執行 git add 將這些改變果的文件內容加入 index 暫存區中,再執行 git commit,將 Index 暫存區 中的內容提交至 Repository 中,這樣一樣可以達到合并 commit 節點的效果(與上面 --soft 合并 commit 節點差不多,只是多了 git add 添加到暫存區的操作); 移除所有 Index 暫存區中準備要提交的文件(Staged files),可以執行 git reset HEAD 來 Unstage 所有已列入 Index 暫存區的待提交的文件(有時候發現 add 錯文件到暫存區,就可以使用命令); commit 提交某些錯誤代碼,或者沒有必要的文件也被 commit 上去,不想再修改錯誤再 commit (因為會留下一個錯誤 commit 點),可以回退到正確的 commit 點上,然后所有原節點和 reset 節點之間差異會返回工作目錄,假如有個沒必要的文件的話就可以直接刪除了,再 commit 上去就 OK 了。
四、撤銷修改
① 已修改,未暫存
如果只是在編輯器里修改了文件,但還沒有執行 git add .,這時候文件還在工作區,并沒有進入暫存區,可以用:
git checkout
.
git reset
-- hard
可以看到,在執行完 git checkout . 之后,修改已被撤銷,git diff 沒有任何內容了。git add . 的反義詞是 git checkout .,做完修改之后,如果想向前走一步,讓修改進入暫存區,就執行git add .,如果想向后退一步,撤銷剛才的修改,就執行 git checkout .。
② 已暫存,未提交
已經執行 git add .,但還沒有執行 git commit -m “comment”:
git reset
git checkout
.
git reset
-- hard
可以發現兩種情況都可以用同一個命令 git reset --hard 來完成,這個強大的命令,可以一步到位地把你的修改完全恢復到未修改的狀態。
③ 已提交,未推送
執行 git commit 后,代碼已經進入了本地倉庫:
git reset
-- hard origin
/ master
還是這個 git reset --hard 命令,只不過這次多了一個參數 origin/master,正如上文中的,origin/master 代表遠程倉庫,既然已經污染了本地倉庫,那么就從遠程倉庫把代碼取回來。
④ 已推送
如果執行了 git add -> git commit -> git push 了,這時代碼已經進入遠程倉庫。如果想恢復的話,只需要先撤銷本地修改,再強制 push 到遠程倉庫:
git reset
-- hard HEAD
^
git push
- f
五、同時 push 到多個遠程倉庫
進入項目目錄,打開 .git/config 文件(.git 是隱藏目錄,需要打開顯示隱藏文件):
只需要在 [remote “origin”] 下增加一條 url 地址,就可以在 push 時推送到多個遠程倉庫:
如果使用 sourceTree 軟件進行 git 操作,那么需要注意在 settings -> 高級下面,多個遠程地址對應的驗證信息是否正確:
總結
以上是生活随笔 為你收集整理的Git之深入解析工作流程、常用命令与Reset模式分析 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。