
如果大家平常都在使用 Git 作為版本控制工具的話,那么一定每天都能見到 origin,諸如:
$ git push origin main
$ git fetch origin main
$ git pull origin main
# ...
這里的 origin,還有看似相同的 origin/master、origin/main 又是什么呢?
一、遠程名稱(Remote Name)
在 Git 中,其實無論是 origin,還是 upstream 并沒有特殊的含義,但由于被廣泛使用,因此它們有了約定俗成、眾所周知的含義。
就好比如說,在現(xiàn)實世界中小明、小紅是再普通不過的名字,但由于在小學語文課本的對話中常被作為男一女一,用于表示對話的兩個人而已,并沒有特別的意義在里面。而在技術博文中,經??梢钥吹绞褂?foo、bar 作為變量標識符舉例,它們就相當于語文課本中的小明、小紅一樣。那么本文接下來要討論的 origin、upstream 等也是同樣的道理。
先來個餐前菜...
如果我跟你說,以下兩條命令是完全等效的,你是不是就差不多猜得出 origin 表示什么了?
$ git push origin main
$ git push git@github.com:toFrankie/repo-demo.git main
是的,跟你猜的一樣...
1.1 Origin
我們用示例來講...
先在本地隨意創(chuàng)建一個 Git 倉庫 repo-demo,然后新增一個 README.md 文件,接著 Commit 一下(如下圖):

以上都沒問題!接著,我們試著 Push 一下:

可以看到 git push 失敗了,原因很容易理解:我們只是在本地創(chuàng)建一個倉庫,并沒有將本倉庫與遠程倉庫進行關聯(lián),因此 Git 無法理解是將其推送至哪個代碼托管平臺,然后也不知道是平臺上的哪個遠程倉庫,是 GitHub 平臺的,還是 GitLab 平臺的?是平臺上的 React 倉庫,還是 Vue 倉庫,還是別的什么倉庫?Git 統(tǒng)統(tǒng)都不知道,那么自然是無法替你辦事了。
因此,我們需要做的就是把本地的 repo-demo 倉庫與遠程倉庫關聯(lián)一下(請注意,一個本地倉庫是可以關聯(lián)多個遠程倉庫的):
$ git remote add origin <repo_address>
這里用到了 origin,我們先不管為什么用 origin,用其他(比如 foo)行不行的問題?(答案是可以的)
關聯(lián)之后,再進行 Push 就能成功了。

那么 git remote add 內部做了什么默默無聞的工作呢,它其實是往 .git/config 中寫入了一個叫 [remote "origin"] 配置:
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:toFrankie/repo-demo.git
fetch = +refs/heads/*:refs/remotes/origin/*
如果你本地的倉庫是通過 git clone 下來的,Git 會默認將遠程倉庫命名為 origin,自動幫你關聯(lián)上遠端倉庫(可在 .git/config 文件中看到已有 [remote "origin"] 配置項了),因此 Commit 之后就能直接 Push 了。
When a repo is cloned, it has a default remote called
originthat points to your fork on GitHub, not the original repo it was forked from.(引自 Github page)
如果我們在 GitHub 新創(chuàng)建一個 Repository 的話,會看到以下指引:

我們來分析一下,這配置表示什么意思。
[remote "origin"]
url = git@github.com:toFrankie/repo-demo.git
fetch = +refs/heads/*:refs/remotes/origin/*
通過 git remote add 命令,添加了一個叫做 origin 的遠程名稱(Remote Name),
- 其中
url參數,表示該遠程名稱對應的遠程倉庫地址。 - 其中
fetch參數分為兩部分,以冒號:進行分割,冒號左邊表示本地倉庫文件夾,冒號右邊表示遠程倉庫在本地的副本文件夾。里面的加號+表示往里面添加數據的意思。
當使用 git fetch origin 時,Git 將遠程倉庫下的所有分支拉取到本地的 refs/remotes/origin/ 目錄下,然后 git merge 時,它會把 refs/remotes/origin/ 目錄下的對應分支合并到 refs/heads/ 目錄下對應分支上。
那么 origin 究竟是什么呢?
請注意,
origin只是一個名稱(別名),用于指向遠程倉庫。這個別名是可以自行修改的,比如命名為foo、bar等。使用別名好處是「方便」。
比起記住一個遠程倉庫地址,別名實在方便太多了。將 origin 作為遠程倉庫的別名是較為普遍的做法,況且所有代碼托管平臺默認就是 origin。
回到文章開頭的例子:
$ git push origin main
# 相當于(其中 origin 指向了 git@github.com:toFrankie/repo-demo.git 遠程倉庫)
$ git push git@github.com:toFrankie/repo-demo.git main
以上兩種方式是完全等價的,這樣就更能體現(xiàn)別名的優(yōu)勢了,簡潔很多。
既然是別名,自然是可以修改的,主要有以下命令:
# 新增遠程名稱(一個本地倉庫,可以關聯(lián)多個遠程倉庫)
$ git remote add <remote-name> <repo-address>
# 刪除已存在遠程名稱(只會移除本地倉庫與遠程倉庫的管理,不會刪除遠程倉庫的代碼哈)
$ git remote rm <remote-name>
# 更新遠程名稱關聯(lián)的遠程倉庫
$ git remote set-url <remote-name> <repo-address>
# 修改遠程名稱(也可以先刪除再添加)
$ git remote rename <old-remote-name> <new-remote-name>
比如,像這樣:

然后,我們修改下遠程名稱為 foo,也是可以的:

接著,我們隨意修改個文件 Push 一下,是這樣的 git push foo main:

到這里,你應徹底明白 origin 是什么了吧。
前面提到過,一個本地倉庫是可以關聯(lián)多個遠程倉庫的,舉個例子:
$ git remote add bar git@github.com:toFrankie/git-test-demo.git
我們可以查看下 .git/congfig 配置文件,如下(或者通過 git remote -v 查看)

從圖中可以看到,別名 foo 和 bar 分別指向了兩個不同的遠程倉庫,然后使用方法與 origin 是相同的,比如:
# 將本地的 main 分支推送至 foo 對應的遠程倉庫(repo-demo)
$ git push foo main
# 將本地的 main 分支推送至 bar 對應的遠程倉庫(git-test-demo)
$ git push bar main
1.2 upstream(一個特殊的 remote name)
upstream 的譯為“上游”。當你 git clone 一個別人的 Repository 到本地,由于你不是該倉庫的成員,因此你是無法向該倉庫推送代碼的。此時,相較于本地倉庫,別人的這個 Repository 稱為 upstream。
我們可以 Fork 這個 Repository 到自己 GitHub 賬號下,然后通過 git clone 將這個 Fork 出來的倉庫克隆到本地電腦上。(下文將這個別人的倉庫稱為 Upstream-Repo,F(xiàn)ork 出來的倉庫稱為 Origin-Repo)
大致關系如圖所示(源自),其中 Upstream-Repo 對應圖中的 Original,Origin-Repo 對應圖中 Fork:

當我們將 Origin-Repo 克隆到本地,Git 會默認創(chuàng)建一個 origin 的別名指向 Origin-Repo 的倉庫地址。
如果要跟蹤 Upstream-Repo 倉庫的變更,您需要添加另一個名為 upstream 的別名,使其指向 Upstream-Repo 倉庫。
# 1. 添加上游倉庫的別名
$ git remote add upstream <upstream-repo-address>
# 2. 獲取上游倉庫的變更
$ git fetch upstream
# 3. 有需要的話,可以通過 merge 或 rebase 方式合并到本地分支中,比如:
$ git merge upstream/main
盡管添加了 upstream,諸如 git push upstream main 等方式試圖向 Upstream-Repo 提交代碼仍然是不被允許的,因為你不是 Upstream-Repo 倉庫的成員。想給 Upstream-Repo 倉庫貢獻代碼的話,只能通過 Pull Request 的方式。
當然,這一節(jié)提到的 upstream 也是一個約定俗稱的別名,也是可以自定義的。
1.3 小結
除了 origin、upstream 等有眾所周知的含義的遠程名稱之外,我們還可以這樣使用:
由于一個本地倉庫是可以關聯(lián)多個遠程倉庫的,因此,可以設置多個「別名」分別指向不同的遠程倉庫(比如一個 GitHub、一個 GitLab、一個 Gitee),然后通過別名的方式方便、快速地拉取某個遠程倉庫的代碼或者將代碼推送至某個遠程倉庫。
# 添加 github 別名
$ git remote add github git@github.com:toFrankie/repo-demo.git
# 添加 gitlab 別名
$ git remote add gitlab git@gitlab.com:toFrankie/repo-demo.git
# 添加 gitee 別名
$ git remote add gitee git@gitee.com:toFrankie/repo-demo.git
小結一下:
常見的
origin、upstream都只是通過git remote add命令創(chuàng)建的名稱(Remote Name),用于指向某個遠程倉庫(Remote URL)。常用
origin作為遠程倉庫的別名,是一個較為主流的做法。同時,也是各大代碼托管平臺的默認名稱(即git clone一個遠程倉庫, Git 會默認將origin指向該倉庫)。如果你覺得不爽,完全可以自定義(git remote set-url)為“阿貓”、“阿狗”等名稱。查看本地倉庫關聯(lián)的遠程倉庫信息,可以在
.git/config文件或通過git remote -v命令查看。使用別名的最大好處是,無需記住遠程倉庫的 URL,也是唯一的好處吧。不用也是完全 OK 的,完全可以直接使用遠程倉庫 URL,但我想不會有這種朋友吧。
(建議)若無特殊需求,不要為了個性,試圖更改 origin、upstream 等被廣泛使用的別名,其中所表示的約定俗成的、眾所周知的含義。
二、遠程分支(Remote Branch)
常說的「遠程分支」是遠程倉庫對應分支在本地的一個副本。比如常見的 origin/master、origin/main、origin/develop 等都是遠程分支,可以在 .git/refs/remotes/ 目錄下看到。
# 查看所有本地分支
$ git branch
# 查看所有遠程分支(-r 是 --remotes 的簡寫)
$ git branch -r
# 查看所有本地分支和遠程分支(-a 是 --all 的簡寫)
$ git branch -a
可以通過 git branch -r 命令查看所有的遠程分支:
frankie@iMac repo-demo % git branch -r
origin/HEAD -> origin/main
origin/dev
origin/main
如果對 origin/HEAD 不理解的話,先不管下文會介紹。
上一節(jié),我們介紹了遠程名稱只是一個代號、別名,是可以修改的。那么我們將 Remote Name 由 origin 修改為 foo,那么遠程分支,會不會由 origin/main 變?yōu)?foo/main 呢?
修改前:
frankie@iMac repo-demo % git remote -v
origin git@github.com:toFrankie/repo-demo.git (fetch)
origin git@github.com:toFrankie/repo-demo.git (push)
frankie@iMac repo-demo % git branch -r
origin/HEAD -> origin/main
origin/dev
origin/main
修改后:
frankie@iMac repo-demo % git remote rename origin foo
frankie@iMac repo-demo % git remote -v
foo git@github.com:toFrankie/repo-demo.git (fetch)
foo git@github.com:toFrankie/repo-demo.git (push)
frankie@iMac repo-demo % git branch -r
foo/HEAD -> foo/main
foo/dev
foo/main
果然,將遠程名稱修改之后,遠程分支名稱也會跟著改變的。我們通過 tree 命令看下目錄結構,如下:
frankie@iMac repo-demo % tree .git/refs
.git/refs
├── heads
│ ├── dev
│ └── main
├── remotes
│ └── foo
│ ├── HEAD
│ ├── dev
│ └── main
└── tags
那么接下來,若無特殊說明,都將以 origin 作為遠程名稱進行說明或舉例。
通常,拉取最新代碼的過程是這樣的:
通過
git fetch拉取代碼的過程:先讀取.git/config文件里面的配置[remote <remote-name>],將里面的所有(因為fetch并沒有指定其中一個或多個遠程倉庫)遠程名稱對應倉庫的分支下載到本地,并放在.git/refs/remotes/<remote-name>/目錄下。比如
git fetch origin main會創(chuàng)建或更新.git/refs/remotes/origin/main的文件,此時通過git branch -r就能看到一個origin/main的分支。但注意,我們使用的時候還是用origin/main而不是remotes/origin/main哦。有時候,我們可能會通過
git diff命令來對比本地分支與遠程分支的一些信息,才決定要不要合并。比如,git diff main origin/main。通過
git merge或git rebase來進行分支合并。比如git merge origin/main,表示將遠程分支origin/main合并至本地分支main中。
也可以直接使用 git pull 命令,其實包括了 git fetch 和 git merge 兩個過程。請注意 git fetch 并不會修改「本地分支」的代碼。
細心的同學可能會發(fā)現(xiàn),refs/remotes/origin/ 目錄下,相應的分支文件記錄的只是一個 Commit-ID(SHA-1),比較特殊的是 HEAD 文件(即 origin/HEAD 分支)記錄的是 ref: refs/remotes/origin/main 的東西,它始終指向默認遠程分支。
三、HEAD、Detached HEAD、origin/HEAD、FETCH_HEAD、ORIG_HEAD 區(qū)別
這個對于剛接觸的同學,可能看起來有點懵。
其實 Git 中的「分支」是由一個或多個 Commit-ID 組成的集合。通過 git branch 命令創(chuàng)建的分支,只是對某個 Commit-ID 的「引用」。因此,使用 git branch -d 刪除某個本地分支,也只是刪除了這個「引用」而已,并不會刪除任何的 Commit-ID。但是,如果一個 Commit-ID 沒有被任何一個分支引用的話,在一定時間之后,將會被 Git 回收機制刪除。
本節(jié)內容將會講述以下相關內容:
HEAD跟「本地分支」相關。Detached HEAD是一種特殊狀態(tài)的HEAD。origin/DEAD跟「遠程分支」相關。FETCH_HEAD跟git fetch操作相關ORIG_HEAD跟git merge、git reset等「危險操作」相關
3.1 HEAD
我們在哪能看到 HEAD 呢?

接著,我們從 main 分支切換至 dev 分支。

對比發(fā)現(xiàn),HEAD 發(fā)生改變了。
前面提到,分支只是對 Commit-ID 的引用。每當在某個分支上提交代碼,Git 都會產生一個全新的、唯一的 Commit-ID,此時我們的分支名稱也隨之移向最新的一個 Commit-ID。
關于 HEAD 存放于本地倉庫的 .git/HEAD 文件里面,利用 cat 命令可以看到它的內容。
frankie@iMac repo-demo % cat .git/HEAD
ref: refs/heads/dev
frankie@iMac repo-demo % cat .git/refs/heads/dev
866bc9f1d8f4797c0e46e959cb0c9abdd47d8176
所以說到底,此時 HEAD 只是對 Commit-ID 為 866bc9f1d8f4797c0e46e959cb0c9abdd47d8176 的引用。如果切回 main 分支,那么 HEAD 相應的內容就會跟著改變。
而 HEAD 則是比較特殊的一個引用(有些文章稱為「指針」,也問題不大)。除了 git commit 之外,git checkout、git reset 等命令都會影響 HEAD 的指向。
一句話總結:HEAD 是對當前 Commit-ID 的「引用」。
- 當使用
git commit時,HEAD會跟著移動,并指向最新的 Commit-ID。- 當使用
git checkout時,HEAD會移動并指向對應分支的最新一個 Commit-ID。- 當使用
git reset時,HEAD會移動至對應分支的某個 Commit-ID。請注意git reset --hard可以將HEAD和Branch移動至任何地方。
順道提一下,git reset 的本質就是移動 HEAD 來達到撤銷的目的。
觀察以下示例,我使用 git reset --soft 將 HEAD 從 Commit-ID 為 42d46a2 移至 222a33c,變化如下:
frankie@iMac repo-demo % git log
commit 42d46a2356cfdde0ad80bfc042b6cb15eae04759 (HEAD -> main, origin/main, origin/HEAD)
Author: Frankie <1426203851@qq.com>
Date: Sat Feb 26 16:44:30 2022 +0800
docs: update
commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:55:48 2022 +0800
docs: update readme.md
commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:36:51 2022 +0800
docs: update README.md
commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 15:45:15 2022 +0800
docs: add README.md
frankie@iMac repo-demo % git reset --soft 222a33cb3185457a5d726325aa7233e43f0b92d3
frankie@iMac repo-demo % git log
commit 222a33cb3185457a5d726325aa7233e43f0b92d3 (HEAD -> main)
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:36:51 2022 +0800
docs: update README.md
commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 15:45:15 2022 +0800
docs: add README.md
此時,我們再看下 .git/HEAD 的內容:
frankie@iMac repo-demo % cat .git/HEAD
ref: refs/heads/main
frankie@iMac repo-demo % cat .git/refs/heads/main
222a33cb3185457a5d726325aa7233e43f0b92d3
關于
git reset的三個參數--mixed(默認)、--hard、--soft的區(qū)別,推薦看這篇文章,講得很詳細易懂。
3.2 Detached HEAD
detached HEAD 可以稱為「游離 HEAD」,也有稱為「分離 HEAD」的。
一般情況下,我們的 HEAD 會指向某個分支的某個 Commit-ID。但是 HEAD 偶爾會發(fā)生「沒有指向某個本地分支」的情況,這種狀態(tài)的 HEAD 稱為 detached HEAD。
以下情況,就可能會出現(xiàn) detached HEAD:
- 使用
git checkout跳轉至某個 Commit-ID,而這個 Commit-ID 剛好目前沒有分支指向它。- Rebase 的過程其實也是處于不斷的 detached HEAD 狀態(tài)。
- 切換至某個遠程分支的時候。
我們先將 Git 的提示語言切換為英文,看得更加清晰。
frankie@iMac repo-demo % git checkout e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Note: switching to 'e0c619ca3978a38f6eabe79c3dfc67d4296ccc36'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at e0c619c docs: update readme.md
此時 HEAD 指向 e0c619c,這個就是 detached HEAD。
還有,前面提到「沒有指向某個本地分支」,但其實我們使用 git branch 會發(fā)現(xiàn)有以下這樣一個分支:
frankie@iMac repo-demo % git branch
* (HEAD detached at e0c619c)
dev
main
但注意,當我們切換至其他分支時,這個 (HEAD detached at e0c619c) 分支是會被干掉的,因為它只是臨時的。因此人家也提醒你可以使用 git switch -c <new-branch-name> 命令,以創(chuàng)建一個新分支來指向該 Commit-ID。
假設我們有這樣一個場景:想要查看某個歷史版本的源碼,就可以利用此功能來解決。
# -t 即 --track
$ git branch <new-branch-name> <commit-id>
# 或者
$ git checkout -b <new-branch-name> <commit-id>
如果我們使用 git checkout origin/main 切換至 origin/main 遠程分支時,也會產生一個 detached HEAD 的。如果我們想基于某個遠程分支,新建一個同名本地分支,可以這樣:
$ git checkout -t origin/dev
branch 'dev' set up to track 'origin/dev'.
Switched to a new branch 'dev'
# 相當于
$ git checkout -b dev origin/dev
如果要離開 detached HEAD 狀態(tài)很簡單,只要切換至其他分支即可。
3.3 origin/HEAD
從名字可以看出,origin/HEAD 也是一個「遠程分支」,其中 origin 則對應遠程名稱。
一般情況下,origin/HEAD 總是指向遠程倉庫的「默認分支」。假設我們的遠程默認分支為 main。那么在遠程倉庫在本地的副本,origin/HEAD 就是相當于 origin/main。
Git 提供了以下命令讓我們去修改 origin/HEAD 的指向(可通過 git remote set-head -h 查看):
# 將 origin/HEAD 設為遠程倉庫的默認分支(-a 即 --auto)
$ git remote set-head <remote-name> -a
# 將 origin/HEAD 設為某個遠程分支,
# 比如 git remote set-head origin dev,將 origin/HEAD 指向遠程的 dev 分支,相當于 origin/dev
$ git remote set-head <remote-name> <branch-name>
# 刪除 origin/HEAD(-d 即 --delete)
$ git remote set-head <remote-name> -d
以上注釋部分,假定了遠程名稱為 origin。
我們修改下 origin/HEAD 為遠程倉庫的 dev 分支,origin/HEAD 文件里面存儲的內容,同樣表示的也是對某個遠程分支的引用,僅此而已。
frankie@iMac repo-demo % git remote set-head origin dev
frankie@iMac repo-demo % cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/dev
3.4 ORIG_HEAD(拓展內容)
在 .git 目錄下,有一個 ORIG_HEAD 的文件,你有沒有好奇怪,它是什么呢?
frankie@iMac repo-demo % cat .git/ORIG_HEAD
222a33cb3185457a5d726325aa7233e43f0b92d3
還記得,前面使用過 git reset --soft 222a33cb3185457a5d726325aa7233e43f0b92d3 指令來移動 HEAD 的指向嗎?
當我們進行了一些「危險操作」時,比如 git reset、git merge、git rebase 等操作時,Git 會將當前 HEAD 指向的的 Commit-ID 原值保存至 ORIG_HEAD 文件內。需要注意的是,類似 git commit 等操作并不會更新 ORIG_HEAD 的內容。
這樣的話,加入我們執(zhí)行了一些「誤操作」時,可以利用 git reset --hard ORIG_HEAD 回退至上一步。
舉個例子,我們進行一次 git reset 操作:
frankie@iMac repo-demo % git log
commit 42d46a2356cfdde0ad80bfc042b6cb15eae04759 (HEAD -> main, origin/main)
Author: Frankie <1426203851@qq.com>
Date: Sat Feb 26 16:44:30 2022 +0800
docs: update
commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:55:48 2022 +0800
docs: update readme.md
commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:36:51 2022 +0800
docs: update README.md
commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 15:45:15 2022 +0800
docs: add README.md
在 reset 之前,我們可以看到 HEAD 指向的 Commit-ID 為 42d46a2356cfdde0ad80bfc042b6cb15eae04759,接著我們執(zhí)行 git reset 指令:
frankie@iMac repo-demo % git reset --soft e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
frankie@iMac repo-demo % git log [16:39:25]
commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36 (HEAD -> main)
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:55:48 2022 +0800
docs: update readme.md
commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:36:51 2022 +0800
docs: update README.md
commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 15:45:15 2022 +0800
docs: add README.md
在 git reset 完成之后,HEAD 指向了 e0c619ca3978a38f6eabe79c3dfc67d4296ccc36。
既然我們前面提到過,ORIG_HEAD 會記錄高危操作前的 Commit-ID。如果沒錯的話,此時 ORIG_HEAD 記錄的應該是 42d46a2356cfdde0ad80bfc042b6cb15eae04759。
frankie@iMac repo-demo % cat .git/ORIG_HEAD
42d46a2356cfdde0ad80bfc042b6cb15eae04759
如果我們想吃后悔藥了,就可以通過 git reset --hard ORIG_HEAD 進行回退:
frankie@iMac repo-demo % git reset --hard ORIG_HEAD
HEAD is now at 42d46a2 docs: update
frankie@iMac repo-demo % git log
commit 42d46a2356cfdde0ad80bfc042b6cb15eae04759 (HEAD -> main, origin/main)
Author: Frankie <1426203851@qq.com>
Date: Sat Feb 26 16:44:30 2022 +0800
docs: update
commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:55:48 2022 +0800
docs: update readme.md
commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 18:36:51 2022 +0800
docs: update README.md
commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date: Sun Feb 20 15:45:15 2022 +0800
docs: add README.md
當然,不同的場景下版本回退還有其他方式的,視乎實際場景,這里就不展開了。
3.5 FETCH_HEAD
其中 FETCH_HEAD 與 git fetch 有關,也是關鍵部分。
FETCH_HEAD指的是某個分支在遠程倉庫上最新的狀態(tài)。每一個執(zhí)行過git fetch操作的本地倉庫都會存在一個FETCH_HEAD列表,這個列表保存在.git/FETCH_HEAD文件中。FETCH_HEAD文件中的每一行對應著遠程倉庫的一個分支。當前本地分支指向的FETCH_HEAD就是該文件中的「第一行」對應的分支(這段表述源于此處)。
我們知道 git fetch 用以下幾種用法:
# 1
$ git fetch
# 2
$ git fetch <remote-name>
# 3
$ git fetch <remote-name> <remote-branch-name>
# 4
$ git fetch <remote-name> <remote-branch-name>:<local-branch-name>
- git fetch
拉取「所有遠程倉庫」所包含的分支到本地,并在本地創(chuàng)建或更新遠程分支。所有分支最新的 Commit-ID 都會記錄在 .git/FETCH_HEAD 文件中,若有多個分支,FETCH_HEAD 內會多行數據。
- git fetch origin
拉取 origin 對應的遠程倉庫的所包含的分支到本地,FETCH_HEAD 設定同上。
- git fetch origin main
拉取 origin 對應遠程倉庫的 main 分支到本地,且 FETCH_HEAD 只記錄了一條數據,那就是遠程倉庫 main 分支最新的 Commit-ID。
- git fetch origin main:temp
拉取 origin 對應遠程倉庫的 main 分支到本地,其中 FETCH_HEAD 記錄了遠程倉庫 main 分支最新的 Commit-ID,并且基于遠程倉庫的 main 分支創(chuàng)建一個名為 temp 的新本地分支(但不會切換至新分支)。
因此,
FETCH_HEAD記錄的是從遠程倉庫拉取到本地,「對應分支」的最新一個 Commit-ID。當通過git fetch拉取代碼時,
- 若有具體指定了某個遠程倉庫的某個分支,那么
FETCH_HEAD就對應此分支。- 若沒有具體指定遠程倉庫的某個分支,
a.FETCH_HEAD總是指向.git/FETCH_DEAD首行對應的分支。
b. 文件.git/FETCH_DEAD可能會記錄著多個分支,且該文件首行對應的是git fetch時所在分支的同名遠程分支。
接著,與 FETCH_HEAD 相關的是 git pull 操作。
git pull 等價于 git fetch + git merge FETCH_HEAD 兩個步驟的結合。
當 git pull 不添加其他參數時,等價于 git pull <remote-name> <當前分支名>,如果遠程倉庫無與之對應的同名分支,執(zhí)行該命令就會拋出錯誤。舉個例子:
frankie@iMac repo-demo % git branch -a
* temp
remotes/origin/HEAD -> origin/dev
remotes/origin/dev
remotes/origin/main
frankie@iMac repo-demo % 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> temp
好,我們切換到本地的 main 分支,遠程倉庫有與之對應的同名分支。
# 相當于 git pull origin main
$ git pull
拆分為以下步驟:
git fetch origin main
將遠程倉庫的main分支最新 Commit-ID 記錄到.git/FETCH_HEAD中,此時FETCH_HEAD指向該 Commit-ID。git merge FETCH_HEAD
將FETCH_HEAD對應的 Commit-ID 合并至本地main分支中。如果合并過程不存在沖突(即只是 Fast-Forward),那么可以順利完成git pull最后一個步驟,否則的話,需要手動解決沖突。
四、More...
其實上面介紹了很多,細心的同學可能會發(fā)現(xiàn),其實無論是「本地分支」,還是「遠程分支」,它們記錄的只是一個 Commit-ID 或者是對某個分支的引用(形如 ref: refs/heads/main)。
我們觀察本地倉庫的 .git 目錄可以發(fā)現(xiàn),我們的本地分支、遠程分支、標簽都是存在于 .git/refs/ 目錄下:
frankie@iMac repo-demo % tree .git/refs
.git/refs
├── heads
│ ├── dev
│ └── main
├── remotes
│ └── origin
│ ├── HEAD
│ ├── dev
│ └── main
└── tags
前面介紹過,「分支」是由一個或多個 Commit-ID 組成的集合。但我們合并的確是實實在在的代碼啊,那么這些代碼被存放到哪呢?
具體數據都被放在 .git/objects/ 目錄下。
然后,現(xiàn)在回頭再看 .git/config 的配置,看起來是不是很容易理解了。
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:toFrankie/repo-demo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[branch "dev"]
remote = origin
merge = refs/heads/dev
未完待續(xù)...