Git系列-工作流
為了更好的使用Git進行項目管理, 一些牛人發明了不同的工作流. 本文是發表的Atlassian網站上的 文章 , 比較了4種常見的Git工作流方式. 可以通過本文了解目前常見的Git工作流.
常見的4種Git工作流:
- 中心化工作流
- Feature分支工作流
- Gitflow工作流
- Forking工作流
中心化工作流
這種方式與SVN工作方式相同. 但由于Git的特性, 每個開發者都可以將開發任務保存在本地倉庫中, 避免考慮遠程服務器的順序. 并且可以利用Git分支的特性
工作方式
和SVN類似, 中心化工作流使用一個中心倉庫保存項目的所有變更. 默認開發分支叫做 master , 所有的變更都提交到這個分支上. 該工作流只需要 master 分支, 不需要額外分支.
開發人員首先從中心倉庫克隆代碼. 然后在本地的項目中, 編輯代碼并提交變更. 這些變更當前保存在本地倉庫中, 與中心倉庫是隔離的. 這可以讓開發人員只在方便的時候同步代碼.
開發人員通過push將本地變更推送到中心倉庫的master分支.
處理沖突
在開發人員推送代碼前, 必須先拉取中心倉庫的最新代碼, 并在此基礎上rebase. 也就是將自己的代碼增加在別人的代碼上. 這樣可以產生一個線性的歷史.
如果此時產生沖突, Git會暫停rebase并需要由開發人員手動處理沖突.
示例
某人初始化了中心倉庫
首先某人需要在服務器上創建一個中心倉庫:
ssh user@host git init --bare /path/to/repo.git
所有人從中心倉庫克隆代碼
每個開發人員都將項目克隆到本地:
git clone ssh://user@host/path/to/repo.git
Git會自動使用 origin 作為倉庫的別名
John開始開發某個功能
John可以在他的本地倉庫開發新功能, 使用標準的Git提交流程:
- 編輯代碼
- stage
用于暫存工作目錄的部分代碼變更, 但不會提交到倉庫 - commit
由于這些變更是發生在本地的, 所以John可以重復該過程, 而不用擔心中心倉庫的變化
git status # 查看倉庫狀況 git add <some-file> # 暫存一個文件 git commit # 提交一個文件</some-file>
Mary也開始開發另一個功能
在John開發的同時, Mary開始開發另一個功能, 也使用標準的Git提交流程
和John一樣, Mary也不用考慮中心倉庫的變化
John將他的代碼推送到中心倉庫
John完成了功能, 他應該講本地的提交推送到中心倉庫, 這樣其他成員就可以訪問新的代碼
git push origin master
origin 是中心倉庫的別名, master 是分支名
由于當前遠程分支并沒有別人提交代碼, 所以不會產生任何沖突
Mary提交了她的代碼
現在Mary也完成了代碼, 她也將代碼推送到中心服務器
git push origin master
但是當前中心倉庫已經有了John的新代碼, 而Mary本地的代碼還是舊的, 所以Git會拒絕推送請求, 并提示以下信息來阻止Mary覆蓋最新的代碼:
error: failed to push some refs to '/path/to/repo.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Merge the remote changes (e.g. 'git pull') hint: before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
現在Mary需要先拉取中心倉庫的最新代碼到本地, 將代碼整合后, 再推送到中心倉庫
Mary在最新代碼基礎上rebase
Mary使用 git pull 來從中心倉庫中拉取最新代碼
git pull --rebase origin master
--rebase 選項會使Git將Mary所有的提交移動到master分支的最新部分, 從而組成一條線性的提交歷史, 如圖:
- 如果忘記該選項, 也是可以的. 但會在其他人同步中心倉庫時產生一個 merge commit
- 在中心工作流下, 推薦使用rebase美化提交歷史, 而不要生成merge commit
Mary解決了沖突
Rebase會將每個本地commit一個接一個地傳遞給最新的master分支. 也就是說你會在每次commit都處理合并沖突, 而不是在一次大的合并沖突中一次性處理. 這可以保持你的commit盡可能地專注, 也可以保持一個干凈的項目歷史. 最終使你更容易找到bug出現的位置, 如果需要回滾的話, 也能把對項目的影響降到最低.
如果Mary和John工作在不相關的功能上, Rebase一般不會產生沖突. 但如果真的產生了沖突, Git會暫停當前commit的rebase, 并輸出如下信息, 同時攜帶一些相關說明:
CONFLICT (content): Merge conflict in <some-file>
Git最偉大的事情是任何人都可以解決自己的合并沖突. 在本例中, Mary只需要簡單的執行 git status 來查看問題所在. 沖突文件會出現在Unmerged paths部分:
# Unmerged paths: # (use "git reset HEAD <some-file>..." to unstage) # (use "git add/rm <some-file>..." as appropriate to mark resolution) # # both modified: <some-file>
然后, Mary編輯該文件. 如果結果合適, 她可以暫存這些文件, 然后執行 git rebase 來完成其他事情:
git add <some-file> git rebase --continue
大功告成. Git將會移動到下一個commit, 并對其他沖突執行重復的操作.
如果你到這一步后不知道該如何繼續, 不要慌張. 只需要執行以下命令, 你就可以回退到執行 git pull --rebase 的時候:
git rebase --abort
Mary成功發布了功能
在Mary與中央倉庫同步后, 他就可以發布她的變更:
git push origin master
接下來做什么
如你所見, 使用Git命令便可方便地替換傳統SVN開發環境. 這對于傳統SVN團隊是好的, 但這并不是Git的本質.
如果你的團隊喜歡使用中心化工作流, 但希望追蹤合作的詳細內容, 那么 Feature分支工作流 值得一試. 通過為每一個功能聲明一個單獨的分支, 你便可以在新功能合并到項目之前進行深入的討論.
Feature分支工作流
一旦你掌握了 中心化工作流 , 那么在開發過程中增加feature分支則是一件非常簡單的事, 這樣做可以鼓勵開發者合作, 并追蹤討論.
Feature分支工作流的核心理念是所以的功能開發應當發生在一個單獨的分支上, 而不能在master分支. 這種方式使得多個開發者可以工作在某個功能上, 而不會干擾主要代碼. 同時也意味著master分支永遠不會包含有問題的代碼, 這對持續集成環境具有極大的優勢.
將功能開發包裹起來還能實現 pull request , 它是一種對分支進行討論的方式, 能夠在某個功能合并到項目之前讓其他開發者有機會看到. 或者是你在某個功能中間遇到困難, 你可以創建一個pull request來詢問同事有什么好的建議. 關鍵點在于, pull request讓團隊評論每個人的工作變得極其簡單.
如何工作
Feature分支工作流仍然使用中央倉庫, master仍然代表主要項目歷史. 但是開發者在每次開始工作時, 都要創建一個新的分支. 功能分支應當有一個具有描述性的名稱, 例如 animated-menu-items 或 issue-#1061 . 這是為了讓每個分支有一個清晰, 專注的目標.
Git對待master分支和feature分支沒有任何技術上的區別, 所以開發者可以像在中心化工作流一樣編輯, 暫存, 提交變更到一個feature分支.
此外, feature分支也可以被推送到中央倉庫. 這樣可以在不影響主要代碼的情況下, 與其他開發者分享某個feature. 由于 master 分支是唯一的”特殊”分支, 在中央倉庫保存多個功能分支不會產生任何影響. 當然, 這也便于每個人保存各自的本地提交.
Pull Request
除了隔離功能開發之外, 分支的變更還可以通過 pull request 進行討論. 當某人完成了一個功能, 他們不需要立即合并到master中, 而是將功能分支推送到中央倉庫, 然后發起一個pull request來請求合并到master中. 這種方式可以讓其他開發者有機會在合并代碼之前審查變更.
代碼審查是pull request的主要優勢, 但是他們實際上是被設計為討論代碼的. 你可以把pull request看作是某個分支的討論. 例如, 如果一個開發者的某個功能需要幫助, 那就可以發起一個pull request. 與此相關的人會收到通知, 他們可以很快地查看問題.
當pull request被接受時, 所進行的操作同中心化工作流類似. 首先, 你需要確保本地的master已經同步為最新. 然后, 將功能分支合并到master分支, 再將更新后的master分支推送到中央倉庫.
示例
本例演示使用pull request進行代碼審查.
Mary開始了一個新功能
在Mary開始開發新功能之前, 她需要工作在一個單獨的分支. 她可以使用如下命令創建一個新的分支:
git checkout -b marys-feature master
上面命令會從 master 分支創建一個新的 marys-feature 分支, -b 標志告訴Git如果分支不存在的話則創建一個. 在這個功能分支上, Mary像之前一樣編輯, 暫存, 提交代碼:
git status git add <some-file> git commit
Mary去吃午飯
Mary在她的功能分支上增加了一些commit. 在她去吃午飯前, 最好將她的功能分支推送到中央倉庫. 這相當于一個備份, 如果Mary同其他開發者共同協作的話, 這樣做也可以讓其它開發者訪問她的commit.
git push -u origin marys-feature
該命令將 marys-feature 推送到中央倉庫, -u 標志將其添加為遠端追蹤分支. 在設置追蹤分支后, Mary可以不使用任何參數調用 git push 來推送他的功能
Mary完成了功能
當Mary吃完午飯回來后, 她完成了她的功能. 在合并到 master 之前, 他需要發起一個pull request來讓其他人知道她做了什么. 但首先, 她應該確保中央倉庫有她最新的提交:
git push
然后, 她在Git圖形化界面中發起一個pull request, 請求將 marys-feature 合并入 master , 其他成員會自動收到通知. pull request最偉大的事是它可以在相關commit中顯式評論, 所以可以很方便地對某段變更提出問題.
Bill收到了pull request
Bill收到了pull request, 并查看了 marys-feature . 他認為需要在合并之前做一些改動, 之后他和Mary通過pull request進行了一系列的磋商.
Mary修改代碼
Mary使用相同的方式來修改代碼. 她編輯, 暫存, 提交, 推送變更到中央倉庫. 他的所有動作都會在pull request中記錄并顯示, 而Bill也可以繼續評論.
如果Bill愿意的話, 他可以將 marys-feature 拉取到他的本地倉庫中, 自己來操作代碼. 他的所有commit也會顯示在pull request中.
Mary發布了功能
當Bill接受了pull request后, 需要有人來將功能分支合并到主分支中(這可以由Bill或Mary來做):
git checkout master git pull git pull origin marys-feature git push
首先, 無論誰來執行合并, 都需要切換到他的 master 分支, 并拉取最新代碼. 然后使用 git pull origin marys-feature 將 marys-feature 合并到中央倉庫的代碼副本中. 你也可以使用簡單的 git merge marys-feature , 但之前的命令可以保證你能夠拉取到最新的功能分支代碼. 最后, 更新后的 master 分支要被推送回 origin .
這一流程通常會產生一個merge commit. 有些開發者喜歡這樣, 因為這可以表示功能的合并. 但如果你喜歡線性的歷史, 你可以在合并之前, 在 master 上將功能分支rebase進去.
有些圖形界面會在點擊 接受 按鈕后自動處理pull request的合并.
與此同時, John也做了相同的事
當Mary和Bill在 marys-feature 分支討論pull request時, John也在他的功能分支上做著相同的事. 通過使用不同的分支來隔離功能, 每個人都可以獨立工作.
接下來做什么
feature分支工作流對于開發項目來說非常的彈性. 但問題是有時過于彈性. 對于大一些的團隊, 通常會為不同的分支指派更多的角色. Gitflow工作流則是管理功能開發, 發布準備, 維護的常用模式.
Gitflow工作流
Gitflow工作流是圍繞項目發布定義的一種嚴格的分支模型. 要比Feature分支工作流更為復雜, 為大型項目管理提供了全新的框架.
該工作流沒有比Feature工作流增加任何新的概念或命令. 而是為不同的分支指派了更為具體的角色, 并定義了這些分支如何以及何時進行交互. 它為準備, 維護, 發布都使用了獨立的分支. 同時你也可以使用Feature分支工作流的所有特性: pull request, 隔離實驗, 更有效率的協作.
如何工作
Gitflow工作流仍然使用中央倉庫作為所有開發者的交流中心. 唯一的區別是分支結構的不同.
歷史分支
除了單獨的 master 分支, 該工作流使用兩個分支來記錄項目的歷史. master 分支存儲主要發布歷史, develop 分支用于feature分支的整合. 同時使用tag位 master 分支來標記版本號.
功能分支
每個新功能應當在單獨的分支上開發, 同時能夠推送到中央倉庫用于備份或協作. 但在這里功能分支是從 develop 分離出來的. 當一個功能完成時, 該功能分支也會合并到 develop 分支中. 功能分支不能直接同 master 分支交互.
發布分支
當 develop 分支擁有足夠多的功能, 適合發布的時候, 你需要從 develop 分支分離一個 release 分支. 創建該分支意味著進入了下一個發布周期, 所以在這一點之后不能增加任何新的功能, 而只能在 release 分支上增加bug fix, 文檔生成, 或是其他與發布相關的任務. 當可以發布的時候, release 分支會合并到 master 分支中, 并對master分支使用版本號打上標簽. 然后將 release 合并回 develop 分支.
使用單獨的分支用作發布可以讓一個團隊在完善當前發布版本的同時, 由另一個團隊繼續為下一次發布功能進行開發. 同時也創建了良好的開發環境(例如, 現在可以很容易的說”本周我們開發4.0版本”, 在倉庫中也可以看到該版本).
常用慣例:
- 從 develop 分支分離
- 合并入 master 分支
- 命名慣例: release-* 或 release/*
維護分支
維護分支, 或叫做 hotfix 分支, 通常用于對發布版本進行快速修復. 這是唯一可以直接從 master 分支中分離出來的分支. 當修復完畢后, 該分支應該同時合并到 master 和 develop 分支(或是當前的release分支), 此外 master 分支應該使用新的版本號打標簽.
使用單獨的分支進行bug修復可以讓團隊在不影響其他工作或發布周期的情況下進行工作.
示例
本例演示了如何使用該工作流完成一個發布周期
創建一個開發分支
第一步是從 master 分支創建一個 develop 分支. 最簡單的做法是在本地創建一個空的 develop 分支, 將其推送到服務器:
git branch develop git push -u origin develop
該分支會包含項目的完整歷史, 而 master 只包含版本片段. 其他開發者現在應該從中央倉庫克隆并檢出開發分支:
git clone ssh://user@host/path/to/repo.git git checkout -b develop origin/develop
現在每個人都有一個本地分支副本.
Mary和John開發新功能
John和Mary開始在一個單獨的分支上工作. 他們都需要為相應的功能創建單獨的分支. 他們應該從 develop 上創建分支:
git checkout -b some-feature develop
他們都使用相同的方式在功能分支上提交代碼: 編輯, 暫存, 提交:
git status git add <some-file> git commit
Mary完成了功能
Mary增加了幾個commit之后, 她感覺功能已經開發完畢. 如果她的團隊使用pull request, 那么現在便可以創建一個pull request來請求將他的功能合并入 develop . 如果沒有使用pull request的話, 她可以將功能分支合并入本地的 develop 分支, 然后將 develop 分支推送到中央倉庫:
git pull origin develop git checkout develop git merge some-feature git push git branch -d some-feature
第一條命令保證 develop 分支是最新的. 注意功能分支不能直接合并到 master 分支中. 沖突可以像中心化工作流相同的方式解決.
Mary準備發布
當John仍然在他的功能上工作時, Mary開始準備發布項目. 同功能開發一樣, 她使用單獨的分支來包裹發布的準備. 這一步也是發布版本號建立的地方:
git checkout -b release-0.1 develop
這一分支是為了理清發布代碼, 進行測試, 更新文檔, 以及其他與發布相關的事.
當Mary創建該分支并將其推送到中央倉庫后, 發布功能便已凍結. 任何不在 develop 分支的功能都必須推遲到下一個發布周期.
Mary完成發布
當 release 分支準備就緒時, Mary將 release 分支合并到 master 和 develop 分支中, 然后刪除 release 分支. 注意, 將代碼合并會 develop 分支非常重要, 因為一些關鍵的更新可能是在 release 分支更新的. 如果Mary的團隊強調代碼審查, 那么此時是提交pull request的最好時機:
git checkout master git merge release-0.1 git push git checkout develop git merge release-0.1 git push git branch -d release-0.1
發布分支作為功能開發(develop)和發布(master)的緩沖. 當你合并到 master 中時, 你應該為commit打標簽用于參考
git tag -a 0.1 -m "Initial public release" master git push --tags
Git有一些hook, 當某個特定行為在倉庫內發生時, 會執行相應的腳本. 可以配置一個hook, 當推送 master 分支或推送標簽到中央倉庫時, 自動創建一個發布版本
用戶發現一個bug
發布版本后, Mary與John繼續開發后續的功能. 此時有一個用戶開啟了一個工單, 投訴當前發行版中的一個bug. 為了處理這個bug, Mary(或是John)從 master 分支創建了一個維護分支, 修復了問題, 然后直接合并回 master :
git checkout -b issue-#001 master # Fix the bug git checkout master git merge issue-#001 git push
與發布分支類似, 維護分支包含重要的更新, 也需要被合并到 develop 中, 所以Mary也需要進行這項合并. 然后便可刪除維護分支:
git checkout develop git merge issue-#001 git push git branch -d issue-#001
接下來做什么
現在你應該了解了中心化工作流, feature分支工作流, Gitflow工作流.
要記住, 工作流只是演示可以做什么, 并不是死板的規定. 所以不用擔心你會不贊同某些方式. 最終的目標是讓Git為你工作.
Forking工作流
Forking工作流與上面討論的工作流完全不同. 這種工作流為 每一個 開發者提供一個單獨的服務端倉庫, 而沒有中央倉庫. 這意味著每個開發者都有兩個倉庫: 本地私有倉庫, 公開的服務端倉庫.
Forking工作流的優勢在于不需要每個人都把代碼推送到同一個中央倉庫, 便可集成代碼. 開發者只需要推送到他們各自的服務端倉庫, 唯一的項目管理員推送到官方倉庫. 管理員可以在不提供其他開發者訪問官方倉庫權限的情況下接受commit
這樣可以為大型, 組織化的團隊(包括不可信的第三方)提供一個彈性的方式來安全的協作. 也是開源項目的理想工作流.
如何工作
同其他工作流一樣, Forking工作流從官方倉庫開始. 但是當新的開發者開始工作時, 他們并不直接從官方倉庫克隆代碼.
他們會 fork 官方倉庫并在服務器上創建一個官方倉庫代碼的副本. 這份副本作為他們個人的公開倉庫 - 其它開發者不可以對其進行push, 但是其它開發者可以從該倉庫拉取變更(一會兒說為什么這樣做). 當他們在服務端創建了副本后, 開發者執行 git clone 將副本拷貝到本地機器, 作為本地私有的開發環境.
當他們準備發布本地提交時, 他們要將commit推送到自己的公開倉庫中, 而不是官方倉庫. 然后, 他們向官方倉庫發起一個pull request, 讓項目管理員知道有變更. pull request也可以用于討論問題.
管理員拉取開發者的變更到本地倉庫, 檢查是否有問題, 然后將其合并到本地的 master 分支, 然后推送到服務器官方倉庫的 master 分支. 現在代碼已經是項目的一部分. 其他開發者可以從官方倉庫同步到本地倉庫.
官方倉庫
理解”官方”倉庫非常重要. 從技術角度將, Git不會把任何倉庫看做官方倉庫. 實際上項目管理員的倉庫就是官方倉庫.
Forking工作流的分支
所有的私人公開倉庫只是便于同其他開發者分享代碼. 每個人仍然需要使用分支來隔離獨立的功能, 就像Feature分支工作流和Gitflow工作流一樣. 唯一的區別是這些分支的分享方式. 在Forking工作流中, 他們會被拉取到其他開發者的本地倉庫, 而在Feature工作流和Gitflow工作流中他們會被推送到官方倉庫.
示例
項目管理員初始化了官方倉庫
任何基于Git的項目, 第一步是在服務端創建一個團隊成員都可以訪問的官方倉庫. 一般來說, 該倉庫也會作為管理員的公開倉庫.
公開倉庫應該是空的, 無論他們是否代表官方代碼. 所以項目管理員應該執行以下代碼來建立官方倉庫:
ssh user@host git init --bare /path/to/repo.git
開發者fork官方倉庫
下一步, 所以其他開發者需要fork這個官方倉庫. 可以使用 git clone 來操作, 或是使用網站提供的fork按鈕.
之后, 每個開發者自己的服務端倉庫. 同官方倉庫一樣, 它們都是空的倉庫.
開發者克隆它們fork的倉庫
然后每個開發者需要克隆它們自己的公開倉庫. 可以使用 git clone 命令.
本例假設使用Bitbucket托管倉庫.
git clone https://user@bitbitcket.org/user/repo.git
與其他工作流使用單一的 origin 遠端不同, Forking工作流使用兩個遠端 - 一個用于官方倉庫, 另一個用于開發者自己的服務端倉庫. 你可以給這些遠端取任意名稱, 通常使用 origin 作為你fork的倉庫的遠端名稱(在你git clone時會自動創建), 使用 upstream 作為官方倉庫的遠端名稱.
git remote add upstream https://bitbucket.org/maintainer/repo
你需要使用上面的命令創建upstream遠端. 這可以讓你保持本地倉庫與官方項目同步. 注意如果你的upstream倉庫開啟了驗證(比如它不是開源的), 逆序要提供一個用戶名:
git remote add upstream https://user@bitbucket.org/maintainer/repo.git
然后會要求用戶在克隆或拉取官方代碼之前提供一個有效的密碼
開發者在各自功能上工作
在他們克隆的本地倉庫中, 開發者像其他工作流一樣編輯代碼, 提交變更, 創建分支:
git checkout -b some-feature # Edit some code git commit -a -m "Add first draft of some feature"
他們的所有變更都是完全私有的, 直到他們推送到各自的公開倉庫. 如果官方倉庫有任何更新, 他們可以使用 git pull 拉取最新commit:
git pull upstream master
由于開發者都工作在單獨的功能分支上, 這通常會產生fast-forward合并
開發者發布他們的功能
當開發者準備好分享他們的新功能時, 他們需要做兩件事. 首先, 他們需要推送代碼到公開倉庫, 以便其他開發者能夠訪問. 他們的 origin 遠端應該已經建立, 所以只需要做以下:
git push origin feature-branch
這里的 origin 是開發者自己的服務端倉庫, 而不是官方代碼.
第二, 他們需要通知項目管理員合并代碼到官方倉庫. Bitbucket提供了 Pull Request 按鈕來使用表單實現該功能. 一般情況下, 你會希望將你的功能分支合并到upstream遠端的 master 分支
項目管理員合并功能
當項目管理員接收到pull request時, 他們的工作是決定是否將它們合并到官方代碼中. 他們可以做以下兩件事之一:
- 在pull request中直接檢查代碼
- 將代碼拉取到他們本地倉庫, 手動合并
第一種方式更為簡單, 可以讓管理員查看變更的diff, 進行評論, 并使用圖形化界面進行合并. 然而, 當pull request產生沖突時, 第二個選項也是必須的. 當產生沖突時, 管理員需要從開發者的服務端倉庫fetch功能分支, 將他們合并到本地 master 分支, 然后解決沖突:
git fetch https://bitbucket.org/user/repo feature-branch # Inspect the changes git checkout master git merge FETCH_HEAD
當變更集成到本地 master 后, 管理員要將其推送到服務端的官方倉庫, 這樣其他開發者便能夠訪問:
git push origin master
記住, 管理員的origin指的是他的公開倉庫, 也就是官方代碼倉庫.
開發者同步官方倉庫代碼
由于主倉庫代碼更新了, 其他開發者應該同步官方倉庫:
git pull upstream master
接下來做什么
如果你之前使用SVN, 那么Forking工作流是一個顛覆性的改變. 但不必擔心 - 它所做的一切只是基于Feature分支工作流的高層次抽象.
來自:http://blog.lixplor.com/2017/01/26/git-workflow/