我在工作中是如何使用 Git 的

大家好,我是若川。今天分享一篇關於 git 的好文章。我自己經常用命令行終端和 git 縮寫。具體可以看我以往的文章。使用 ohmyzsh 打造 windows、ubuntu、mac 系統高效終端命令行工具,用過都說好。


前言

最近在網上有個真實發生的案例比較火,說的是一個新入職的員工,不會用 Git 拉代碼,第二天被開除。由此,可見 Git 對我們工作的重要性,無論是前端後端,都是離不開 Git 的,下面就讓我們一探究竟吧。

上面的案例引申出一個問題,入職一家新公司,你的 leader 給你分配了倉庫的權限後,如何配置本地的 Git 環境並拉取代碼?莫慌,按照下面我講的四個步驟走,保證你可以順利使用 Git 進行拉取代碼!

  1. 下載 Git 下載地址 (https://git-scm.com/downloads) ,選擇自己系統對應的版本下載即可。

  2. 在你的電腦上生成 ssh 祕鑰,打開終端,執行 ssh-keygen -t rsa -C "你公司內部郵箱地址",如果執行成功,切換到 ~/.ssh 目錄下,此時目錄應該如下所示。複製 id_rsa.pub 的內容。

  3. 這裏以 Github 爲例,如下圖所示,進入 settings -> SSH and GPG keys 通過 cat 命令查看文件 id_rsa.pub 的內容,然後複製過來,點擊 add ssh key,這一步等於說把你的公鑰放到了 Github 上進行託管。

  4. 全局配置 Git 的用戶名和郵箱

git config --global user.name "xxx"
git config --global user.email "xxx@xx.com"

完成以上四步,你就可以愉快 pull 代碼開發了。和 https 拉取方式不同的是,https 方式需要每次提交前都手動輸入用戶名和密碼,ssh 的方式配置完畢後 Git 都會使用你本地的私鑰和遠程倉庫的公鑰進行驗證是否是一對祕鑰,從而簡化了操作流程。

Git 簡介

在介紹 Git 的相關操作前,我覺得非常有必要了解 Git 的由來,以及 Git 是用來解決什麼問題的。Git(讀音爲 / gɪt/)是一個開源的分佈式版本控制系統,可以有效、高速地處理從很小到非常大的項目版本管理。Linus Torvalds ,這個人我相信大家都知道吧,開源 Linux 系統的發明人。如今,你看到的大部分服務器其實都是運行在 Linux 系統上,令人感到稱歎的是,這位大神級別的程序員不僅創造了 Linux 系統。那 Linux 的代碼是如何管理的呢?2002 年之前,世界各地的志願者把源代碼文件通過 diff 的方式發給 Linus,然後由 Linus 本人通過手工方式合併代碼!要知道,當時的 Linux 的代碼量已經很大了,通過人工管理的方式,一是容易出錯,二是效率低。於是 Linus 選擇了一個商業的版本控制系統 BitKeeper,BitKeeper 的東家 BitMover 公司出於人道主義精神,授權 Linux 社區免費使用這個版本控制系統。最後,出於某種原因,BitMover 公司收回了 Linux 社區的免費使用權,於是 Linus 花了兩週時間自己用 C 語言寫了一個分佈式版本控制系統,這就是 Git 的由來了。

Git 的工作區域和流程

要想弄懂 Git 是怎麼對我們的代碼進行管理的,那首當其衝的是瞭解 Git 的工作區域是如何構成的。因爲,只有徹底弄懂了 Git 工作區域的構成,你纔可以在適當的區域使用合適的命令。如下圖所示,此圖包含了 Git 的 4 個工作區和一些常見的操作。

Workspace:工作區,就是平時進行開發改動的地方,是當前看到最新的內容,在開發的過程也就是對工作區的操作。

Index:暫存區,當執行 git add 的命令後,工作區的文件就會被移入暫存區,暫存區標記了當前工作區中哪些內容是被 Git 管理的,當完成某個需求或者功能後需要提交代碼,第一步就是通過 git add 先提交到暫存區。

Repository:本地倉庫,位於自己的電腦上,通過 git commit 提交暫存區的內容,會進入本地倉庫。

Remote:遠程倉庫,用來託管代碼的服務器,遠程倉庫的內容能夠被分佈在多個地點的處於協作關係的本地倉庫修改,本地倉庫修改完代碼後通過 git push 命令同步代碼到遠程倉庫。

一般來說,Git 的工作流程分爲以下幾步

Git 基本操作

git add

添加文件到暫存區

# 添加某個文件到暫存區,後面可以跟多個文件,以空格區分
git add xxx
# 添加當前更改的所有文件到暫存區。
git add .

git commit

# 提交暫存的更改,會新開編輯器進行編輯
git commit 
# 提交暫存的更改,並記錄下備註
git commit -m "you message"
# 等同於 git add . && git commit -m
git commit -am
# 對最近一次的提交的信息進行修改,此操作會修改 commit 的 hash 值
git commit --amend

git pull

# 從遠程倉庫拉取代碼併合併到本地,可簡寫爲 git pull 等同於 git fetch && git merge 
git pull <遠程主機名> <遠程分支名>:<本地分支名>
# 使用 rebase 的模式進行合併
git pull --rebase <遠程主機名> <遠程分支名>:<本地分支名>

git fetch

與 git pull 不同的是 git fetch 操作僅僅只會拉取遠程的更改,不會自動進行 merge 操作。對你當前的代碼沒有影響

# 獲取遠程倉庫特定分支的更新
git fetch <遠程主機名> <分支名>
# 獲取遠程倉庫所有分支的更新
git fetch --all

git branch

# 新建本地分支,但不切換
git branch <branch-name> 
# 查看本地分支
git branch
# 查看遠程分支
git branch -r
# 查看本地和遠程分支
git branch -a
# 刪除本地分支
git branch -D <branch-nane>
# 重新命名分支
git branch -m <old-branch-name> <new-branch-name>

工作中使用 Git 解決問題的場景

git rebase 讓你的提交記錄更加清晰可讀

git rebase 的使用

rebase 翻譯爲變基,他的作用和 merge 很相似,用於把一個分支的修改合併到當前分支上。

如下圖所示,下圖介紹了經過 rebase 後提交歷史的變化情況。

現在我們來用一個例子來解釋一下上面的過程。

假設我們現在有 2 條分支,一個爲 master,一個爲 feature/1,他們都基於初始的一個提交 add readme 進行檢出分支,之後,master 分支增加了 3.js , 和 4.js 的文件,分別進行了 2 次提交,feature/1 也增加了 1.js 和 2.js 的文件,分別對應以下 2 條提交記錄。

此時,對應分支的提交記錄如下。

master 分支如下圖:

feature/1 分支如下圖

結合起來看是這樣的

此時,切換到 feature/1 分支下,執行 git rebase master,成功之後,通過 git log 查看記錄。

如下圖所示:可以看到先是逐個應用了 mater 分支的更改,然後以 master 分支最後的提交作爲基點,再逐個應用 feature/1 的每個更改。

所以,我們的提交記錄就會非常清晰,沒有分叉,上面演示的是比較順利的情況,但是大部分情況下,rebase 的過程中會產生衝突的,此時,就需要手動解決衝突,然後使用依次 git add 、git rebase --continue 的方式來處理衝突,完成 rebase 的過程,如果不想要某次 rebase 的結果,那麼需要使用 git rebase --skip 來跳過這次 rebase 操作。

git merge 和 git rebase 的區別

不同於 git rebase 的是,git merge 在不是 fast-forward(快速合併)的情況下,會產生一條額外的合併記錄,類似 Merge branch 'xxx' into 'xxx' 的一條提交信息。

另外,在解決衝突的時候,用 merge 只需要解決一次衝突即可,簡單粗暴,而用 rebase 的時候 ,需要依次解決每次的衝突,纔可以提交。

git rebase 交互模式

在開發中,常會遇到在一個分支上產生了很多的無效的提交,這種情況下使用 rebase 的交互式模式可以把已經發生的多次提交壓縮成一次提交,得到了一個乾淨的提交歷史,例如某個分支的提交歷史情況如下:

進入交互式模式的方式是執行:

git rebase -i <base-commit>

參數 base-commit 就是指明操作的基點提交對象,基於這個基點進行 rebase 的操作,對於上述提交歷史的例子,我們要把最後的一個提交對象( ac18084 )之前的提交壓縮成一次提交,我們需要執行的命令格式是:

git rebase -i ac18084

此時會進入一個 vim 的交互式頁面,編輯器列出的信息像下列這樣。

想要合併這一堆更改,我們要使用 Squash 策略進行合併,即把當前的 commit 和它的上一個 commit 內容進行合併, 大概可以表示爲下面這樣,在交互模式的 rebase 下,至少保留一個 pick,,否則命令會執行失敗。

pick  ... ...
s     ... ... 
s     ... ... 
s     ... ...

修改文件後 按下 : 然後 wq 保存退出,此時又會彈出一個編輯頁面,這個頁面是用來編輯提交的信息,修改爲 feat: 更正,最後保存一下,接着使用 git branch 查看提交的 commit 信息,rebase 後的提交記錄如下圖所示,是不是清爽了很多?rebase 操作可以讓我們的提交歷史變得更加清晰。

 

特別注意,只能在自己使用的 feature 分支上進行 rebase 操作,不允許在集成分支上進行 rebase,因爲這種操作會修改集成分支的歷史記錄。

使用 git cherry-pick 獲取指定的 commit

git cherry-pick 可以理解爲” 挑揀” 提交,和 merge 合併一個分支的所有提交不同的是,它會獲取某一個分支的單筆提交,並作爲一個新的提交引入到你當前分支上。當我們需要在本地合入其他分支的提交時,如果我們不想對整個分支進行合併,而是隻想將某一次提交合入到本地當前分支上,那麼就要使用 git cherry-pick 了。

如下場景,以下有三條分支,feature/cherry-pick1 和 feature/cherry-pick2 都是基於 master 檢出的兩條功能性分支,對應的分支 log 記錄如下

master 分支的提交如下

現在 master 只需要 feature/cherry-pick1 和 feature/cherry-pick2 有關 change 的修改,並不關心有關 fix 內容的修改。此時就可以用 cherry-pick 指令了。

語法: git cherry-pick [commit-hash]

commit-hash 表示的是某次 commit 的 hash 值。現在,依次執行以下兩條指令 git cherry-pick e0bb7f3git cherry-pick c9a3101,過程中,如果出現衝突,解決衝突後 進行 git add,接着執行 git cherry-pick --continue,最後,master 上的提交如下

此時,master 分支上應用了需要的提交,就達到了我們想要的效果。如果需要多個 cherry-pick 需要同步到目標分支,可以簡寫爲 git cherry-pick <first-commit-id>...<last-commit-id>,這是一個左開右閉的區間,也就時說 first-commit-id 提交帶來的代碼的改動不會被合併過去,如果需要合併過去,可以使用 git cherry-pick <first-commit-id>^...<last-commit-id>,它表示包含 first-commit-id 到 last-commit-id 在內的提交都會被合併過去。

使用 git revert 回滾某次的提交

想象這麼一個場景,你的項目最近有 2 個版本要上線,這兩個版本還伴隨着之前遺留的 bug 的修復,一開始的時候,你將 bug 修復在了第一個版本的 release 分支上,突然在發版前一天,測試那邊反饋,需要把第一個版本修復 bug 的內容改在第二個版本上,這個時候,第一個版本的集成分支的提交應該包括了第一個版本的功能內容,遺留 bug 修復的提交和其他同事提交的內容,想要通過 reset 的方式粗暴摘除之前的關於 bug 修復的 commit 肯定是不行的,同時,這種做法比較危險,此時,我們既不想破壞之前的提交記錄,又想撤回我們遺留 bug 的 commit 記錄應該怎麼做呢?git revert 就派上了用場。

 

git revert 撤銷某次操作,此操作不會修改原本的提交記錄,而是會新增一條提交記錄來抵消某次操作。

語法: git revert <commit-id> 針對普通 commit

git revert <commit-id> -m 針對 merge 的 commit

下面就用一個案例來理解一下這個命令,如下圖所示,假設被紅框框起來的地方是會引起 bug 的一次提交,在他的提交之後,又進行了 2 次提交,其中包含了其它同事的提交。

此時想把引起提交的 bug 的幹掉,執行 git revert 1121932,執行操作後,再打開查看日誌,如下圖所示,可以看到是新增了一條 commit 記錄,這個 commit 的產生的 msg 是自動生成的,Revert 開頭,後面跟撤回的 commit-msg 信息之前的 commit 記錄並沒有消失,此時也達到了代碼回退的效果

此外 git revert 也可以回滾多次的提交

語法:git revert [commit-id1] [commit-id2] ... 注意這是一個前開後閉區間,即不包括 commit1 ,但包括 commit2 。

回滾我們的提交有二種方式,一種是上文提到的git revert命令外,還可以使用 git reset 命令,那麼它們兩者有什麼區別呢?

git revert 會新建一條 commit 信息,來撤回之前的修改。

git reset 會直接將提交記錄退回到指定的 commit 上。

對於個人的 feature 分支而言,可以使用 git reset 來回退歷史記錄,之後使用 git push --force 進行推送到遠程,但是如果是在多人協作的集成分支上,不推薦直接使用 git reset 命令,而是使用更加安全的 git revert 命令進行撤回提交。這樣,提交的歷史記錄不會被抹去,可以安全的進行撤回。

使用 git stash 來暫存文件

會有這麼一個場景,現在你正在用你的 feature 分支上開發新功能。這時,生產環境上出現了一個 bug 需要緊急修復,但是你這部分代碼還沒開發完,不想提交,怎麼辦?這個時候可以用 git stash 命令先把工作區已經修改的文件暫存起來,然後切換到 hotfix 分支上進行 bug 的修復,修復完成後,切換回 feature 分支,從堆棧中恢復剛剛保存的內容。

基本命令如下

git stash //把本地的改動暫存起來
git stash save "message" 執行存儲時,添加備註,方便查找。
git stash pop // 應用最近一次暫存的修改,並刪除暫存的記錄
git stash apply  // 應用某個存儲,但不會把存儲從存儲列表中刪除,默認使用第一個存儲,即 stash@{0},如果要使用其他個,git stash apply stash@{$num} 。
git stash list // 查看 stash 有哪些存儲
git stash clear // 刪除所有緩存的 stash

下面通過幾幅圖對 stash 的命令做進一步瞭解。

此時,我正在開發一個新功能,修改了 1.js 文件裏的內容

還沒開發完成,這個時候,我想切換到 hotfix 分支上修復 bug,得暫停下開發切換到 hotfix 分支,但是現在工作區還有內容,此時如果切換分支 Git 會報出下面的錯誤

error: Your local changes to the following files would be overwritten by checkout:        1.jsPlease commit your changes or stash them before you switch branches.Aborting

上面那句話的意思就是說工作區有文件修改,不能提交,需要先進行 commit 或者 stash 操作,執行 git stash,結果如下

Saved working directory and index state WIP on stash: 22e561c feat: add 1.js

此時,我們的工作區已經乾淨了,可以切換到 hotfix 分支進行 bug 修復的工作,假設我們現在 bug 修復完成了,繼續切回 feature 分支進行原本功能的開發,此時只需要執行 git stash pop,之前我們暫存的修改就會恢復到工作區,如下圖所示。

當我們想要暫存文件,切換分支做某些事的時候,可以用 git stash 這種機制幫助開發。

推薦在使用 stash 的相關命令時,每一次暫存的時候,不要直接使用 git stash 命令進行暫存下來,而是使用 git stash save "message..." 這種方式,給本次的提交做一個信息的記錄。這樣,想應用更改的時候,先通過 git stash list 查看一下所有的暫存列表。之後,推薦使用 git stash apply stash@${num} 的方式進行應用對應的 stash,這樣不會清空已有的 stash 的列表項,並且能應用到當前的工作區,不需要這個暫存的話,再手動清除就可以了。

不同的工作區域撤銷更改

開發中,我們經常需要回退代碼的操作,在不同的工作區域中,回退代碼的方式也是不相同的。如下圖所示,假設現在要在 feature/revoke 分支上進行開發,

首先通過 git status 查看下現在的狀態。

目前我們的工作區是很乾淨的,沒有任何修改的操作,此時,修改一下代碼再次查看狀態,可以看到,1.js 這個文件被修改了。

現在我們想把 1.js 這個文件恢復到修改前的狀態,即撤回工作區的修改,就可以使用 git checkout -- <filename> 的命令,如果要撤回多個文件的修改,文件之間使用空格隔開,如下圖所示,我們撤回了 1.js 文件的修改,工作區也恢復乾淨了。

如果說現在我們對文件進行了修改,並且已經提交到暫存區了,這部分文件我們不想要的話,那麼就可以通過 git reset <filename> 的命令來對特定的文件進行撤銷,git reset 會撤回所有存在暫存區的文件,如下圖所示,查看前後的狀態可知,文件最後成功撤回到工作區了。

配置 git alias 提升工作效率

一般我們在工作中,接到開發任務後,需要新創建一個分支進行開發 此時需要 用到 git branchgit checkout、 git pull 等命令,在我們一頓操作後,開發完成,到了提交代碼的階段,又要諸如此類 git add 、git commitgit push 等命令,雖然簡單,但是輸入起來也是不夠簡潔,作爲一個程序員,開發程序就是爲了提高我們的效率的,懶是人類進步的源泉,所以我們可以通過配置別名的方式,簡化這些命令。

它的基本用法是 git config --global alias.<簡化的字符> 原始命令

如下面的例子:

$ git config --global alias.co checkout
$ git config --global alias.ci commit
$ git config --global alias.br branch

這裏將 co 表示 checkout,ci 表示 commit,br 表示 branch,以後提交就可以簡寫成

--global 是全局參數,也就是配置一次後,這些命令可以在這臺電腦下的所有倉庫都適用。這些命令其實是更新你全局的 .gitconfig 文件,該文件用來保存全局的 git 配置,vim ~/.gitconfig,執行這段命令後,顯示如下,下圖展示了剛纔通過 git config --global alias 添加的 alias

除了上面那種直接通過命令的方式外,也可以通過修改這個文件的 alias 項來設置別名。

這裏分享一個我自己常用的別名設置,把以下配置替換到 .gitconfig 文件裏的 [alias] 所屬的區域,然後就可以愉快的使用了~

[alias]
st = status -sb
co = checkout
br = branch
mg = merge
ci = commit
ds = diff --staged
dt = difftool
mt = mergetool
last = log -1 HEAD
latest = for-each-ref --sort=-committerdate --format=\"%(committername)@%(refname:short) [%(committerdate:short)] %(contents)\"
ls = log --pretty=format:\"%C(yellow)%h %C(blue)%ad %C(red)%d %C(reset)%s %C(green)[%cn]\" --decorate --date=short
hist = log --pretty=format:\"%C(yellow)%h %C(red)%d %C(reset)%s %C(green)[%an] %C(blue)%ad\" --topo-order --graph --date=short
type = cat-file -t
dump = cat-file -p
lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

這樣,我們每次想查看 Git 的歷史記錄, 就不用輸入那麼一長串命令 直接使用 git lg ,下圖是 axios 源碼裏的提交記錄,使用封裝後的 git lg 查看的效果圖

分支之間的關係一眼就很明瞭,在哪個 commit 上進行的 merge 操作也很清晰,可以幫助我們很好的追溯歷史的提交和解決問題。

總結

本文由淺入深的的講解了 Git 的環境搭建,基本用法,以及工作中使用較爲高頻的 Git 命令的用法,無論你是前端後端還是其它端的開發,日常工作中少不了對 Git 的使用,我們不僅要會用,還要用的漂亮,用的靈活,用的穩健。這樣才能在和同事協作項目的時候更加得心應手,學會了本文這些 Git 的使用技巧後,在日常工作中多多練習,相信會給你帶來很大的收穫!

參考文獻

阮一峯的 git 教程 (https://www.ruanyifeng.com/blog/2014/06/git_remote.html)

Git merge 和 rebase 分支合併命令的區別 (https://juejin.cn/post/6844903603694469134#heading-3)



你好,我是若川,畢業於江西高校。現在是一名前端開發 “工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收穫超百萬閱讀。

從 2014 年起,每年都會寫一篇年度總結,已經寫了 7 篇,點擊查看年度總結

同時,活躍在知乎 @若川,掘金 @若川。致力於分享前端開發經驗,願景:幫助 5 年內前端人走向前列。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qpjLO8DbJvtmTifAuFA94w