簡體   English   中英

為什么 git pull --rebase origin master 需要“master”,但 git rebase -i origin 不需要?

[英]Why is "master" required in git pull --rebase origin master but not git rebase -i origin?

當我想針對遠程主機重新設置基准時,我使用

git pull --rebase origin master

如果我使用

git pull --rebase origin

我收到錯誤

You asked to pull from the remote 'origin', but did not specify
a branch. Because this is not the default configured remote
for your current branch, you must specify a branch on the command line.

但是為什么會這樣

git rebase -i origin

作品?

在這種情況下

git rebase -i origin master

實際上導致

fatal: fatal: no such branch/commit 'master'

我沒有名為 master 的本地分支,但是為什么在這種情況下它不去遠程分支呢?

git pull命令與大多數其他 Git 命令完全不同。 I'd say that in many ways, the closest other Git command is git gc , which—like git pull —is a convenience wrapper to avoid needing to type in multiple separate Git commands. 1

git pull作用是:

  1. 運行git fetch 然后
  2. 運行第二個 Git 命令。

第一個命令git fetch需要遠程的名稱。 名稱origin是第一個遠程的標准名稱,並且由於大多數 Git 存儲庫只有一個遠程,所以origin是該存儲庫中第一個、最后一個和唯一遠程的名稱。

你可以把它關掉——你可以運行git pull而不需要額外的參數——Git 會找到一些合適的遙控器。 但是如果你要提供額外的 arguments,第一個非選項參數遠程名稱,所以git pull frabjous使用frabjous作為遠程名稱。

第二個命令是git mergegit rebase 2第二個命令需要一個提交 hash ID ,例如4c53a8c20f8984adb226293a3ffd7b88c3f4ac1a ,或者可以代替提交 Z0800FC577294C34E0B28AD283943594Z ID 的東西。 3但是,通常情況下,我們在這里使用一個名稱——一個分支名稱,如mastermaindev或其他名稱——作為“可行的東西”。 總的想法git pull的思考方式——是:從其他人那里獲取東西,然后合並它 這里的“其他人”是遙控器,“要獲取的東西”是“他在某個分支上的任何新提交”。 所以你在這里輸入的名字,當你輸入一個名字時,就是另一個人的分支名稱

請注意,與git fetch一樣,您可以忽略所有這些,然后運行:

git pull

pull 命令將根據您為當前分支設置的上游計算出要使用的遠程- 可能是origin - 和要使用的名稱,這一切都是獨立的。 “上游”只是你可以設置的東西:對於名為xyzzy的分支,上游可能已經設置origin/xyzzy

請注意,此處的上游名稱origin/xyzzy中有一個斜杠:它由遠程名稱origin ,然后是斜杠,然后是遠程分支名稱xyzzy 因此,如果在遙控器上看到的分支名稱是frab/jous ,那么您將在此處使用origin/frab/jous ,並帶有兩個斜杠:一個用於將origin與另一個人的分支名稱分開,另一個另一個人的分支名稱中。

如果你要輸入一個名字,在你的git pull命令上,你必須把它放在 remote 之后 完成此操作后,Git 假設您只需輸入遠程上看到的分支名稱。 所以你輸入:

git pull origin frab/jous

或者這里的任何東西,意思是:

  1. 運行git fetch origin 然后
  2. origin/frab/jous為 hash ID 並酌情運行git mergegit rebase

請注意,這兩個步驟中的任何一個都可能完全失敗,而第二個步驟可能會在中間停止 如果一個步驟失敗,則任何剩余步驟都不會發生,並且您應該從失敗點重新開始,無論是什么,如果您想從中斷的地方繼續 - 所以您需要知道哪一步失敗,如果有一個失敗。 幸運的是,對於我們大多數人來說, git fetch運行額外的時間是非常安全的,所以我們幾乎可以忽略它的失敗與成功。 但是您仍然需要知道是完成中間停止的合並還是變基。 由於這個和其他原因,我總是鼓勵 Git 新手學習單獨的命令。 認識到他們何時工作、何時完全失敗以及何時停止是很重要的。

不幸的是,這意味着你需要了解這個奇怪之處,其中git pull另一個人的名字作為分支(省略了origin/ ),而git mergegit rebase了你的名字(包括origin/ Z rebase)。 但是無論如何你都必須學習這個。 記下它! 他們的分支名稱是他們的; 您的Git 存儲庫從中讀取它們的名稱和哈希 ID 值(在git fetch步驟期間),並將它們存儲在您的Git 存儲庫中這些origin/下。

這還是漏掉了很多 Git 的設置學習曲線非常陡峭。 我現在休息一下腳注,然后討論另一件事。


1 git gc runs git repack , git prune-packed , git reflog expire , git worktree prune , git prune , git pack-refs , and/or git rerere gc if/as appropriate. 這並不意味着是一個完全詳盡的列表,因為列表有時會發生變化(例如, git worktree在 Git 2.5 之前不存在)而且我並沒有真正跟蹤。 我通過瀏覽git gc文檔生成了這個列表。 我認為這個特定的手冊頁可能是https://git-man-page-generator.lokaltog.net/的主要靈感來源

2有一些特殊情況例外,包括如果git fetch步驟失敗則什么都不做。

3 This is a bit of an oversimplification, as git merge and git rebase can take more than one hash ID, and for a case that is never used by git pull , git rebase also requires a branch name. 但是,為了git pull運行,它們最終在此處使用 hash ID。


origin既是一個遙遠的地方,又是……嗯……

但是為什么會這樣

git rebase -i origin

作品?

這是陡峭的學習曲線的另一部分讓你大吃一驚的地方。

Git 最終都是關於commits的。 存儲庫中的提交是使用 Git 的原因。 單個提交是有編號的,但數字本身是大的、丑陋的、看起來隨機的東西,完全不適合人類。 這些是 hash ID 或 Object ID,例如從git log中溢出。 它們只有通過剪切和粘貼才能真正使用,所以我們基本上不使用它們:我們使用names

因此,Git 提供的不是一個而是兩個鍵值數據庫 其中之一由 hash ID 索引,這就是 Git 如何訪問其提交和其他內部數據。 Git 放入一個 hash ID,並獲取提交或其他 object ID,其密鑰是特定的 Z0800FC577294C335945Z4。 當 object 是提交 object 時,它代表每個文件的完整快照,以您(或任何人)提交時的形式一直凍結。

但是,為了找到hash ID,Git 保留了第二個數據庫,其中的鍵是名稱:分支名稱、標簽名稱和其他類型的名稱。 分支名稱,如mastermaindevdevelopfrab/jous等,由您決定:您可以選擇任何您喜歡的名稱(盡管在 [0 -9a-f] 設置,因為“名稱” cafebabebadf00ddeadcab可以縮寫為 hash IDs)。 為了防止分支和標簽名稱相互碰撞,Git 實際上將refs/heads/粘貼在每個分支名稱的前面,並將refs/tags/粘貼在每個標簽名稱的前面。

Git 存儲在您的存儲庫中的名稱,以便記住其他一些 Git 存儲庫的分支名稱,是遠程跟蹤名稱(Git 調用這些遠程跟蹤分支名稱)並且實際上以refs/remotes/為前綴,而不是origin/dev ,這些確實是refs/remotes/origin/dev

所有這些名稱,在這些不同的命名空間中,每個都擁有一個 hash ID。 這就是 Git 需要的全部內容,因為提交本身持有其他提交 hash ID。 從一個提交中,Git 可以找到另一個。 從那里,Git 可以找到另一個提交,依此類推。 Git 簡單地將分支名稱定義為“此名稱包含要在此分支上稱為最新的提交的 hash ID”。

因此,如果您在某個分支main上,則該名稱包含一些 hash ID H ,這是某個提交的 hash ID:

            <-H   <-- main

每個提交都包含一個先前提交 hash ID 的列表,通常只有一個條目長,以及所有文件的快照。 這是從H出來的向后箭頭,在這里。 提交H持有一些較早提交的 hash ID。 讓我們稱其為G並將其繪制進去:

        <-G <-H   <-- main

當然, G是一個帶有快照和另一個向后箭頭的提交,所以它必須指向某個更早的提交,它一遍又一遍地重復:

... <-F <-G <-H   <-- main

那是一個 Git 分支。 要將提交添加分支,我們按名稱“簽出”或“切換到”,使名稱成為當前分支名稱,對應的提交H成為當前提交

我們可以有多個名稱指向此提交。 讓我們畫幾個名字: maindev以及origin/main ,這不是一個分支名稱,但仍然指向一個提交。 為了懶惰,我將停止在提交之間使用箭頭,但請記住 Git 只能向后工作,從不向前:

...--F--G--H   <-- dev, main, origin/main

我們選擇一個分支——比如說dev ——切換到。 為了記住我們使用的是名稱dev ,我們將特殊名稱HEAD附加到它:

...--F--G--H   <-- dev (HEAD), main, origin/main

現在我們調整使用 Git 的方式——我不會在這里介紹,但索引或暫存區域(同一事物的兩個術語)至關重要——並最終做出一些新的提交。 我們稱之為I的新提交具有新的唯一 hash ID 並向后指向現有提交H ,如下所示:

...--F--G--H
            \
             I

棘手的一點是 Git 在完成新提交后立即更新當前分支名稱I 其他名稱均未更新,因此它們仍指向H

...--F--G--H   <-- main, origin/main
            \
             I   <-- dev (HEAD)

提交I現在是dev上的最新提交。 通過H進行的提交仍在dev上,並且也繼續在main上。 特殊名稱HEAD仍然附加到dev ,我們當前的提交現在是提交I 提交H仍然存在(並且,對於 Git 的散列方案至關重要的是,它完全沒有受到影響:這就是為什么箭頭都是 go 向后,而不是向前)。

好吧,但是——那又怎樣? 好吧,正如我之前所說,Git 是關於提交的。 當您給 Git 一個分支名稱時,大多數情況下,它會通過找出名稱指向的位置很快將該名稱轉換為 hash ID。 git switchgit checkout命令在這里是不尋常的,因為它們也必須記住名稱,這樣當它們完成時你就可以“打開”該分支。)你,即git rev-parse 如果我們給git rev-parse一些分支名稱,我們可以看到它的實際作用:

$ git rev-parse master
5d01301f2b865aa8dba1654d3f447ce9d21db0b5
$ git rev-parse diff-merge-base
fa1c8acabf0d5649baf87f549d67426d14255e0f

它也可以解析標簽名稱和遠程跟蹤名稱,並且使用--symbolic-full-name它可以告訴我們每個名稱的完整拼寫是什么:

$ git rev-parse --symbolic-full-name v2.35.1
refs/tags/v2.35.1
$ git rev-parse --symbolic-full-name origin/master
refs/remotes/origin/master
$ git rev-parse origin/master
5d01301f2b865aa8dba1654d3f447ce9d21db0b5

如果我們單獨給它origin會發生什么?

$ git rev-parse origin
5d01301f2b865aa8dba1654d3f447ce9d21db0b5
$ git rev-parse --symbolic-full-name origin
refs/remotes/origin/master

嗯,這有點奇怪,不是嗎? 讓我們看一下gitrevisions 文檔,它非常重要,並且巧妙地隱藏在一堆 1000 個基本上不可讀的手冊頁中:

指定修訂
...
<refname> eg, master , heads/master , refs/heads/master
... a <refname>通過采用以下規則中的第一個匹配來消除歧義:

  1. 如果$GIT_DIR/<refname>存在,那就是你的意思(這通常只對HEADFETCH_HEADORIG_HEADMERGE_HEADCHERRY_PICK_HEAD );
  2. 否則, refs/<refname>如果存在;
  3. 否則, refs/tags/<refname>如果存在;
  4. 否則,如果存在,則為refs/heads/<refname>
  5. 否則, refs/remotes/<refname>如果存在;
  6. 否則,如果存在,則為refs/remotes/<refname>/HEAD

正是這個六步規則使名稱縮寫起作用。 我們寫:

git rebase master

和 Git 嘗試將master作為.git中的文件(步驟 1),但這不存在,所以 Git 繼續嘗試refs/master作為名稱。 這也不存在,因此 Git 嘗試將refs/heads/master作為名稱(步驟 3)。 那個確實存在,無論如何在這個存儲庫中,所以它解析為 hash ID 並且修訂指定已完成。

如果我們使用origin/master ,步驟 5 會找到它,因為refs/remotes/origin/master存在(使用git for-each-ref轉儲 ref 表,並查看它確實存在)。 如果我們使用origin它似乎根本不是一個 ref-name——第6步會找到它,因為refs/remotes/origin/HEAD存在。

現在, HEAD以及相應的refs/remotes/origin/HEAD HEAD——是一個特例:它是一個符號引用,在 Git 中類似於 Unix/Linux 文件系統中的符號鏈接。 (In fact, in early Git implementations, it simply was a symbolic link. That does not work well on Windows though, so now it's a file with contents.) The git for-each-ref command expands the link by default, but git branch -r沒有,所以這是看到這一點的一種方式。

底線

這一切的結論是:

  1. origin/HEAD是任何分支的符號引用,它是origin中的HEAD ,通常是mastermain
  2. origin本身是遠程的(由git fetch使用),或可通過 gitrevisions 的第 6 步解析(如大多數其他 Git 命令使用);
  3. git rebase -i origin通過origin/HEAD和步驟 6 解決它;
  4. git pull origin master根本不使用第 6 步:字符串origin只是一個remote ,字符串master通過遠程跟蹤名稱映射成為origin/master (在這種特殊情況下git pull實際上回避了這一切因為它使用的是.git/FETCH_HEAD文件機制,它早於所有這些東西和 go 通過稍微不同的代碼路徑)。

git pull命令將它的大部分標志和 arguments 傳遞給git fetch ,除了它傳遞給第二個命令的一些標志,以及它自己使用的一些標志。 由於歷史......錯誤,它非常復雜? 想法? 概念? 無論如何,Git過去工作方式的歷史,必須在接下來的 3 億年或其他任何時間里保存在琥珀中。 (說真的,Git 的人非常重視兼容性本身,並盡量不破壞現有的使用和工作流程。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM