簡體   English   中英

如何編輯和更新不同 git 分支的文件?

[英]how to edit and update files for different git branches?

我的 GitHub 中的存儲庫有兩個分支: mastersolution 首先我git clone

git clone <master url>

然后我cd到該文件夾​​並切換到solution分支

git checkout solution

我發現文件的內容仍然與master的內容相同,例如README.md 如何訪問solution文件?

然后我嘗試git pull來更新solution分支中的文件

git pull origin solution

它可以工作,現在文件的內容用於solution ,但是當我想切換回master ,它失敗並說我需要合並,因為我認為有些文件在兩個分支中具有不同的內容。 如何切換回來?

一般情況下,如何編輯和更新不同分支的文件以及如何輕松來回切換?

另一個例子:

          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2     
              \
               M--N
                   \
                    P

是否需要另一個工作樹?

那些剛接觸 Git 的人通常認為 Git 將更改存儲在分支中 這不是真的。 但是,在您的情況下,我認為您遇到的是這樣一個事實,即當您在 Git 存儲庫中工作時,您是在 Git 所謂的工作樹中這樣做的 你在這里做的任何事情都不在 Git 中(還沒有)。

您可能想使用git worktree add來處理您的特定情況。 在介紹了 Git 如何處理所有這些之后,我們將討論這個問題,因為如果沒有很多基礎知識,它就沒有任何意義。

我喜歡解釋這一點的方式是 Git 根本不存儲更改,並且並不真正關心分支 Git 存儲和關心的是commits 這意味着您需要知道提交是什么以及為您做什么,如何找到提交,如何使用現有提交以及如何進行新提交。

什么是提交

當您使用 Git 工作時,您將使用的基本實體是commit 關於提交,您需要了解三件事。 你只需要記住這些,因為它們是任意的:沒有特別的理由必須這樣做,只是當 Linus Torvalds 編寫 Git 時,這些是他做出的決定。

  1. 每個提交都有編號。

    然而,這些數字並不是簡單的計數:我們沒有提交 #1 后跟提交 2、3、4 等等。 取而代之的是,每個提交都會得到一個獨特的、但又大又丑的十六進制數字,即介於 1 和非常大的數字之間。 1每個存儲庫中的每次提交都會獲得一個唯一的、看起來隨機的數字。

    看起來是隨機的,但不是。 它實際上是內部對象內容的加密校驗和。 這種特殊的編號方案使兩個 Git 能夠通過相互傳遞這些大數字來交換內容。

    這樣做的一個關鍵副作用是,在物理上不可能更改提交中的內容。 (這適用於所有 Git 內部對象。)原因是哈希 ID,即 Git查找對象的方式,內容的校驗和。 取出其中一個,對其內容進行更改,然后將其放回原處,您得到的是一個新的提交(或新的其他內部對象),具有新的不同哈希 ID。 現有的仍然在那里,在現有的 ID 下。 這意味着即使 Git 本身也無法更改存儲提交的內容。

  2. 每個提交存儲每個文件完整快照

    更准確地說,每次提交都存儲了 Git 在您或任何人進行提交時知道的每個文件的完整副本。 當我們研究如何進行新的提交時,我們將稍后進入這個“了解”部分。

    這些副本是只讀的、壓縮的,並以只有 Git 本身可以讀取的格式存儲。 它們也被去重,不僅在每次提交中,而且在每次提交中。 也就是說,如果您的 Git 存儲庫有某個README文件或其他內容的特定副本,存儲在某個提交中,並且您曾經進行過具有該文件的相同副本提交——即使使用其他名稱——Git 只會重新 -使用之前的副本。

  3. 而且,每個提交都會存儲一些元數據

    提交的元數據包括提交者的姓名和電子郵件地址。 Git 從您的user.nameuser.email設置中獲取此信息,並且簡單地相信您就是您所聲稱的那個人。 它們包括,當你(或任何人)做出的承諾的日期和時間標記。 2元數據還包括為什么你(或誰)方面的提交,在的形式提交消息 Git 對消息中的內容並不特別嚴格,但它們通常應該看起來很像電子郵件,具有簡短的一行主題,然后是消息正文。

    但是,此元數據的一部分是嚴格用於 Git 本身的。 每個提交在其元數據中存儲前一次提交的提交編號。 3這種形式提交到簡單的向后看的鏈中:

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

    這里,每個大寫字母代表一些實際的提交哈希 ID。 提交H ,最新的提交,在其中包含較早提交G的實際哈希 ID。 當 Git 從 Git 保留所有提交的任何地方提取較早的提交G ,提交G在其中包含早於G提交F的實際哈希 ID。

    我們說提交H指向提交G ,后者指向提交F Commit F依次指向某個更早的提交,該提交又指向另一個更早的提交,依此類推。 這一路回到有史以來的第一次提交,這是第一次提交 -不能向后指向,所以它只是沒有。

Git 存儲庫的這條向后提交的鏈就是該存儲庫的歷史記錄。 歷史就是承諾; 提交是歷史; 和 Git向后工作 我們從最新的開始,並根據需要向后工作。


1對於 SHA-1,數字介於 1 和 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,975 之間。 這是十六進制的ffffffffffffffffffffffffffffffffffffffff ,或 2 160 -1。 對於 SHA-256,它介於 1 和 2 256 -1 之間。 (使用任何無限精度計算器,例如bcdc來計算 2 256 。它非常大。在這兩種情況下,零都被保留為空哈希。)

2實際上,有兩種用戶-電子郵件-時間三元組,一種稱為“作者”,一種稱為“提交者”。 作者是自己編寫提交的人,而且——早在 Git 被用於開發 Linux 的早期——提交者是通過電子郵件接收補丁並將其放入的人。這就是為什么提交消息的格式為如果它們是電子郵件:通常,它們電子郵件。

3大多數提交只有一個先前的提交。 至少有一次提交——第一次提交——沒有之前的提交; Git 將此稱為根提交 一些提交指向兩個較早的提交,而不僅僅是一個:Git 稱它們為合並提交 (允許合並提交指向兩個以上的較早提交:具有三個或更多父項的提交稱為octopus merge 。它們不會做任何你不能用多個普通合並做的事情,但如果你正在捆綁將多個主題放在一起,他們可以以一種巧妙的方式做到這一點。)


分支名稱是我們查找提交的方式

Git 總是可以通過其丑陋的哈希 ID 找到任何提交。 但是這些哈希 ID 又大又丑。 你能記住你的全部嗎? (我不記得我的了。)幸運的是,我們不需要記住所有這些。 請注意,上面我們是如何從H開始並從那里向后工作的。

所以,如果提交在反向鏈中——而且確實如此——並且我們需要從某個鏈中的最新提交開始,我們如何找到鏈中最后一個提交的哈希 ID? 我們可以把它寫下來:把它寫在紙上,或白板,或其他任何東西上。 然后,每當我們進行新的提交時,我們可以刪除舊的(或將其划掉)並記下新的最新提交。 但是我們為什么要為此煩惱呢? 我們有一台電腦:為什么我們不讓它記住最新的提交?

這正是分支名稱的作用。 它只保存鏈中最后一次提交的哈希 ID:

...--F--G--H   <-- master

名稱master保存最后一次提交H的實際哈希 ID。 和以前一樣,我們說名稱master指向這個提交。

假設我們現在要創建第二個分支。 讓我們起一個新名稱, developfeaturetopic或任何我們喜歡的,也指向提交H

...--F--G--H   <-- master, solution

這兩個名稱標識相同的“最后一次提交”,因此通過H所有提交現在都在兩個分支上

但是,分支名稱的特殊功能是我們可以使用git switch或在 Git 2.23 之前的 Git 中使用git checkout切換到該分支。 我們說git checkout master並且我們得到 commit H並且是“on” master 我們說git switch solution並且我們也得到 commit H ,但這次我們是“on” solution

為了告訴我們使用哪個名稱來查找提交H ,Git 將特殊名稱HEAD附加到一個(並且只有一個)分支名稱:

...--F--G--H   <-- master, solution (HEAD)

如果我們現在進行新的提交——我們稍后會看看我們是如何做到的——Git 會通過將提交H作為其父項寫出來進行新的提交,以便新的提交指向H 我們將新提交稱為I ,盡管它的實際編號只是其他一些看起來隨機的大哈希 ID。 我們無法預測哈希 ID,因為它取決於我們制作它的確切秒數(因為時間戳); 我們只知道它將是獨一無二的。 4

讓我們繪制新的提交鏈,包括 Git 使用的偷偷摸摸的技巧:

...--F--G--H   <-- master
            \
             I   <-- solution (HEAD)

完成新提交I ,Git 將新提交的哈希 ID 寫入當前分支名稱solution 所以現在名稱solution標識了提交I

如果我們切換回名稱master ,我們將看到提交H所有文件,當我們再次切換回solution時,我們將看到提交I的文件。 或者,也就是說,我們可能會這樣看待它們。 但我們可能不會!


4鴿巢原理告訴我們,這最終會失敗。 哈希ID的大尺寸告訴我們,失敗的幾率分鍾,並在實踐中,它永遠不會發生。 生日問題要求哈希非常大,蓄意攻擊已經從 SHA-1 的純理論問題轉變為至少理論上可行的問題,這就是 Git 轉向更大和更安全的哈希的原因。


進行新的提交

它的時間,現在更仔細地看看我們如何真正做出新的承諾I之上。 請記住,我們提到提交中的數據(構成快照的文件)是完全只讀的。 提交以一種特殊的、壓縮的、只讀的、僅限 Git 的格式存儲文件,只有 Git 本身可以讀取。 這對於做任何實際工作都毫無用處。

出於這個原因,Git必須將提交中的文件提取到某種工作區中。 Git 將此工作區稱為您的工作樹工作樹 這個概念非常簡單明了。 Git 只是從提交中獲取“冷凍干燥”的文件,對它們進行再水化或重組,現在您有了可用的文件。 這些文件的可用工作樹副本當然是副本 你可以對他們做任何你想做的事情。 這些都不會觸及提交中的任何原件。

正如我在本文開頭提到的,這些文件的工作樹副本不在 Git 中 他們在你的工作區。 它們是你的文件,而不是 Git 的。 你可以做任何你想做的事情,也可以和他們一起做。 Git 只是從一些現有的提交中填充它們,當你告訴 Git 這樣做時。 在那之后,它們都是你的。

但是,在某些時候,您可能希望 Git 進行新的提交,當它這樣做時,您希望它從您的文件更新文件。 如果 Git 只是重新保存它自己的所有文件不變,那將毫無用處。

在其他非 Git 版本控制系統中,這通常非常容易。 您只需運行,例如,在 Mercurial 中執行hg commit ,Mercurial 就會讀回您的工作樹文件,將它們壓縮成它自己的內部形式5並進行提交。 這當然需要已知文件的列表(並且,例如, hg add更新列表)。 但是 Git 不會這樣做:這太容易了,而且/或者可能太慢了。

相反,Git 所做的是將每個文件的額外“副本”與提交工作樹分開保存。 該文件采用“冷凍干燥”(壓縮和重復數據刪除)格式,但實際上不像提交中的文件那樣被凍結 實際上,每個文件的第三“復制”坐在之間的承諾和你的工作樹。 6

每個文件的這個額外副本存在於 Git 所稱的不同地方,即indexstaging area ,或者——現在很少見—— cache 這三個名字都描述了同樣的事情。 (它主要實現為一個名為.git/index的文件,除了這個文件可以包含將 Git 重定向到其他文件的指令,並且您可以讓 Git 與其他索引文件一起操作。)

因此,當您切換到某個特定提交時,Git 所做的是:

  • 從該提交中提取每個文件;
  • 將原始數據(和文件名)放入 Git 的索引中;
  • 將 Git 格式(“冷凍干燥”)文件提取到您的工作樹中,您可以在其中查看和處理它。

當你運行git commit ,Git 所做的是:

  • 將索引的內容打包,作為保存的快照;
  • 組裝和打包所有適當的元數據以制作提交對象——這包括通過使用當前提交的哈希 ID 作為新提交的父項,使新提交點回到當前提交;
  • 把所有這些寫成一個新的提交;
  • 將新提交的哈希 ID 填充到當前分支名稱中

因此,在您運行git commit時索引(也稱為暫存區)中的任何內容都是git commit的內容。 這意味着,如果您更改了工作樹中的內容——無論是修改某個文件、添加新文件、完全刪除文件,還是其他任何內容——您需要將更新后的文件復制回 Git 的索引(或從Git 的索引完全,如果想法是刪除文件)。 通常,您用來執行此操作的命令是git add 此命令采用一些文件名並使用該文件或這些文件的工作樹副本來替換該文件或這些文件的索引副本。 如果文件從你的工作樹中丟失了(因為你刪除了它), git add也會通過從那里刪除文件來更新 Git 的索引。

換句話說, git add意味着使這個文件的索引副本/這些文件與工作樹副本匹配 只有當文件是全新的——在你運行git add時不存在於索引中——文件才真正添加到索引中。 7對於大多數文件,它實際上只是替換現有副本

文件的索引副本是 Git 中的排序:它存儲在所有內部對象的大數據庫中。 但是,如果文件的索引副本以前從未提交過,則它處於不穩定狀態。 直到您運行git commit ,並且 Git 將索引中的所有內容打包並將其轉換為新的提交,它才會安全地提交給 Git 並且無法刪除或銷毀。 8


5 Mercurial 使用非常不同的存儲方案,它經常存儲差異,但偶爾存儲快照。 這幾乎無關緊要,但 Git 提供並記錄了可以直接訪問其內部存儲格式的工具,因此有時了解 Git 的內部存儲格式可能很重要。

6因為它總是重復數據刪除,這個文件的“副本”最初不占用空間。 更准確地說,它的內容不需要空間。 它在 Git 的索引文件中占用了一些空間,但相對較小:通常每個文件只有幾十或幾百個字節。 索引只包含文件名、一些模式和其他緩存信息,以及一個內部 Git 對象哈希 ID。 實際內容作為內部blob 對象存儲在 Git 對象數據庫中,這就是 Git 執行重復數據刪除的方式。

7也許git add應該被稱為git update-indexgit update-staging-area ,但已經有一個git update-index update-index 命令需要知道 Git 如何將文件存儲為內部 blob 對象:它不是很用戶友好,實際上並不是為了成為你自己會使用的東西。

8提交的文件在 Git 中作為一個大部分永久性且完全只讀的實體存在——但它的permanence ,即這里以 most 為前綴的文件,取決於提交的永久性。 完全刪除提交可能的。 如果您從未向任何其他 Git 發送過某些特定提交,那么從您自己的 Git 存儲庫中刪除該提交將使它真正消失(盡管不是立即消失)。 完全刪除提交的一個大問題是,如果你已經將它發送到其他 Git,那么其他 Git 稍后可能會再次將其返回給你:提交在某種程度上是病毒式的。 當兩個 Git 彼此有 Git-sex 時,其中一個很可能會捕獲提交。


概括

所以,現在我們知道什么是提交:帶有兩個部分的編號對象,數據(快照)和元數據(信息),它們通過它們的元數據向后串在一起。 現在我們也知道分支名稱是什么:它們存儲我們應該在某個鏈中調用最后一個提交的哈希 ID(即使它之后有更多提交)。 我們知道任何提交中的任何內容都不能更改,但我們總是可以添加新的提交。 要添加新的提交,我們:

  • 讓 Git 提取現有的提交,通常是通過分支名稱;
  • 處理現在在我們工作樹中的文件;
  • 使用git add更新我們想要更新的任何文件:這git add更新后的內容從我們的工作樹復制回 Git 的索引;
  • 使用git commit進行新的提交,更新分支名稱。

如果我們像這樣進行一系列提交:

...--G--H   <-- main, br1, br2

並將HEAD附加到br1並進行兩個新的提交,我們將得到:

          I--J   <-- br1 (HEAD)
         /
...--G--H   <-- main, br2

如果我們現在將HEAD附加到br2並進行兩次新提交,我們將得到:

          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2 (HEAD)

請注意,在每個步驟中,我們只是存儲庫中所有提交的集合添加了一個提交 名稱br1現在標識鏈上的最后一次提交; 名稱br2標識其鏈上的最后一次提交; 名稱main標識該鏈上的最后一次提交。 提交H和更早的在所有三個分支上 9

在任何時候,只有一個當前提交 它由HEAD標識: HEAD附加到您的分支名稱之一。 當前提交的文件通過 Git 的索引被復制到您的工作樹中,並且也只有一個工作樹和一個索引。 如果您想切換到某個其他分支名稱,並且該其他分支名稱反映了某個其他提交,則您還必須切換 Git 的索引和您的工作樹。 10


9其他版本控制系統采取其他立場。 例如,在 Mercurial 中,提交只發生在一個分支上。 這需要不同的內部結構。

10這並不完全正確,但細節變得復雜。 請參閱在當前分支上有未提交的更改時簽出另一個分支


git worktree add

現在我們知道如何使用我們的一個工作樹、Git 的一個索引和一個單一的HEAD ,我們可以看到從一個分支切換到另一個分支是多么痛苦:我們所有的工作樹文件每次我們都會更新switch(無論如何,腳注10中提到的復雜情況除外)。

如果您需要在兩個不同的分支中工作,有一個簡單的解決方案:制作兩個單獨的克隆。 每個克隆都有自己的分支、自己的索引和自己的工作樹。 但這有一個很大的缺點:這意味着您有兩個完整的存儲庫。 它們可能會占用大量額外空間。 11而且,您可能不喜歡處理多個克隆和涉及的額外分支名稱。 如果相反,您可以共享底層克隆,但有另一個工作樹怎么辦?

為了使第二個工作樹有用,這個新的工作樹必須有自己的索引自己的HEAD 這就是git worktree add所做的:它在當前工作樹之外的某處創建一個新的工作樹12並為該新工作樹提供自己的索引和HEAD 添加的工作樹必須位於未在主工作樹中檢出的某個分支上,並且未在任何其他添加的工作樹中檢出。

因為添加的工作樹有自己獨立的東西,所以你可以在那里工作而不會干擾你在主工作樹中所做的工作。 因為兩個工作樹共享一個底層存儲庫,所以每當您在一個工作樹中進行新提交時,它都會立即在另一個工作樹中可見。 由於進行提交會更改存儲在分支名稱中的哈希 ID,因此添加的工作樹不得使用與任何其他工作樹相同的分支名稱(否則分支名稱、當前提交哈希 ID、工作樹內容之間的鏈接、和索引內容變得混亂)——但是添加的工作樹總是可以使用分離的 HEAD模式(我們沒有在這里描述)。

總的來說, git worktree add是處理您的情況的一種非常好的方法。 如果你打算用它做很多工作,請確保你的 Git 版本至少是 2.15。 git worktree命令是 Git 2.5 版中的新命令,但有一個令人討厭的錯誤,如果您有一個分離的 HEAD 或在其中工作緩慢,並且您還在主工作樹中執行任何工作,則該錯誤可能會咬您; 這個錯誤直到 Git 2.15 版本才被修復。


11如果您使用路徑名進行本地克隆,Git 將嘗試硬鏈接內部文件以節省大量空間。 主要解決了這個問題,但有些人仍然不喜歡有兩個單獨的存儲庫,隨着時間的推移,空間使用量也會增加。 使用 Git 的替代機制也有一些技巧可以解決這個問題。 例如,我相信 GitHub 使用它可以讓分叉更好地為他們工作。 但總的來說, git worktree填補了一個感知空白; 也許你會喜歡它。

12從技術上講,添加的工作樹不必位於主工作樹之外。 但是把它放在里面是個壞主意:它只會讓人困惑。 把它放在別的地方。 通常,“就在隔壁”是一個不錯的計划:如果您的主要工作樹在$HOME/projects/proj123/ ,您可以使用$HOME/projects/proj123-alt$HOME/projects/proj123-branchX或其他任何.

如果你想在分支之間切換(這里是 Master & Solution),你可以通過兩種方式來實現。 例如,如果您在“解決方案”分支中有更改並且想要切換到“主”分支。

  1. 如果您對“解決方案”分支中的更改感到滿意,則可以在切換到“主”分支之前提交更改。

  2. 如果您不想提交更改,您可以隱藏更改。 這將使您將所做的所有更改存儲在文件中,並將您的分支(“解決方案”)返回到您進行這些更改之前的狀態。

我發現在分支上工作的最佳工具是SourceTree

暫無
暫無

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

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