我从 github 克隆了一个存储库,我做了一些更改。我在 github 上的存储库有 2 个分支,main
和dev
个分支。我想先将我的更改推送到 dev,然后再推送到 main。
我跑了:
git branch
在克隆(本地)存储库中,给我输出:
*main
我没有看到我在 github 上制作的开发分支。
如何将更改推送到开发分支?
正如 knittl 所说,git branch
显示您的分支名称。 其他一些 Git 存储库,例如 GitHub 上的存储库,将有自己的分支名称。
在 Git 中,重要的实际上不是分支名称。 这些是给你的,不是给 Git 的。 更准确地说,它们的存在是为了 Git 可以帮助您找到所需的提交。 这是因为 Git 实际上都是关于提交的,而不是关于分支的——尽管我们将提交组织成分支——而不是关于文件,尽管每个提交都包含文件的快照。
这意味着当你使用 Git 时,你必须首先考虑提交。 你需要在某种直觉层面上确切地知道提交是什么以及为你做什么。 否则,Git 所做的所有疯狂的事情都将令人沮丧和不可能,你将生活在这个 xkcd 漫画中。
Git 是关于提交的,那么什么是提交? 它对你有什么作用?
每个 Git 提交:
-
已编号。 每个提交都有一个唯一的编号,以十六进制表示为哈希ID(或对象 ID)。 这是提交的真实名称,没有它,Git 就找不到提交。 (为了好玩,另请参阅电视比喻。 哈希 ID 又大又丑,人类不可能记住(或者在大多数情况下,发音),所以 Git 有时让我们使用缩短的版本。
-
完全,完全,只读:永远冻结。 这是因为数字一旦分配,就意味着提交,并且在每个存储库中都意味着提交。 当你的 Git 存储库将此提交发送到其他某个 Git 存储库时,该其他 Git 将使用相同的编号。 它永远不会将该数字用于任何其他提交,甚至在您进行该提交之前也不会。 (这就是 Git 中的魔力所在,如果你知道哈希理论、密码学等,你就会知道这实际上是行不通的。 总有一天,Git 会失败。 哈希ID的庞大大小将这个时间推到了我们需要的未来,或者至少,我们希望它能做到。
-
包含两件事:Git 在您(或任何人)提交时知道的所有文件的快照,以及一些元数据或有关提交本身的信息,例如提交者的姓名。
每个提交的元数据中都有一堆信息,但 Git 本身的关键项是每个提交都包含一个列表(通常只有一个条目长)的上一个提交或提交的原始哈希 ID。 Git 称这些为提交的父级。
每次提交中的快照会永久冻结每个文件,以便拥有存储库的任何人都可以取回每个文件的任何版本。 这些文件以只有 Git 可以读取的特殊格式存储,实际上没有任何内容可以写入,并且它们都在提交内部和提交之间进行了重复数据删除,因此大多数提交大多重用早期提交中的文件这一事实意味着大多数提交占用的实际空间很少。 如果您进行完全重用旧文件的新提交(这可以通过多种方式实现),那么新提交实际上根本不占用文件空间,只有一点空间来保存自己的元数据。
快照意味着提交使您能够完全按照您(或任何人)提交时的状态查看每个文件。 但是,您不能将提交的文件用作文件,因为它采用特殊的仅限 Git 格式,并且您无法按照计算机想要的方式写入它。 因此,这意味着要使用快照,您必须让 Git将其提取。 我们不会在这里讨论任何细节,但当你切换到特定提交时,这就是git checkout
或git switch
正在做的事情:Git提取提交的文件,就像从存档中提取一样(因为它们在存档中)。 然后,使用或处理提取的文件,而不是存储在 Git 中的文件。这意味着当您处理文件时,这些文件不在 Git 中。您最终必须进行新的提交,将新快照存储到 Git 中。
同时,元数据为您提供了几件事:
-
它让你知道谁做了提交。
git log
命令将打印用户的姓名和电子邮件地址,以及日期和时间戳。 (有了--pretty=fuller
你会看到每个提交实际上有两个这样的;这部分是 Git 早期"每个人都通过电子邮件发送补丁"用法遗留下来的。 -
它告诉您他们想告诉您他们为什么要提交的内容:这是他们的日志消息。 日志消息的重点不是说他们做了什么——Git 可以通过将此提交中的快照与此提交父级中的快照进行比较来表明这一点——而是为什么他们用
i += 2
替换i++
。 它修复了错误#123吗? 是功能增强吗? 这种事情可以出现在日志消息中。 -
使用该父元数据,Git 可以将提交向后串在一起。 这是历史:随着时间的推移,这就是这个项目中发生的事情。 通过读出最新的提交,我们找到当前的源快照,通过使用其元数据,我们找到它早期的父提交。 使用存储的哈希 ID,Git 现在可以向你显示父提交。 该提交包含更早的祖父级提交的哈希 ID,因此 Git 现在可以向您显示祖父级;该提交包含更早的提交哈希 ID,依此类推。
这意味着存储库中的提交是存储库中的历史记录。要访问所有历史记录,Git 需要最新的提交。
分支名称可帮助您(和 Git)找到最新的提交
让我们绘制一些提交。 我们假设我们有一个很小的存储库,里面只有三个提交。 它们将有三个随机的、又大又丑的哈希ID,没有人能记住或发音,所以我们称它们为提交A
、B
和C
。 提交C
将是最新的,因此在其元数据中将具有早期提交B
的实际哈希 ID。 我们说提交C
点来提交B
,我们这样画:
B <-C
但是B
是一个提交,所以它有一个父哈希ID列表 - 只有一个长提交 - 这意味着B
指向其父A
:
A <-B <-C
A
也是一个提交,但是,作为有史以来的第一个提交,它有一个没有父项的列表(一个空的父项列表),并且不会指向后方。 这就是git log
知道停止倒退的方式:什么都没有了。
但是:Git 如何找到正确的哈希 ID 来提取提交C
以便您可以首先使用它? 请记住,哈希 ID 看起来是随机的。 它们是不可预测的(因为,除其他外,它们完全取决于您进行提交的确切秒数)。 只有一种方法可以知道哈希ID,那就是保存它:写在某个地方。 我们可以自己把它们写下来,然后必须一遍又一遍地输入,但这一点也不好玩。 所以 Git 为我们存储了它们。这就是分支名称。分支名称仅存储一个哈希 ID,即最新提交的ID。
如果我们有一个名为main
的分支,那么,并且C
是main
的最新提交,那么这个名字main
包含提交C
的哈希 ID。 和以前一样,我们说这指向提交C
,并用箭头绘制它:
A <-B <-C <--main
在这一点上,我喜欢变得懒惰,停止将箭头从向后提交绘制到以前的提交作为箭头,因为绘图即将发生什么,并且因为我没有一个好的"箭头字体"可用。 这是可以的,因为一旦我们进行提交,从该提交到其父级的向后箭头将永远冻结,就像任何提交的所有部分一样。 它必须向后指向,因为我们不知道任何未来的哈希 ID 可能是什么,所以我们只知道它们指向后:
A--B--C <-- main
但是,来自分支名称的箭头会随着时间的推移而变化。 让我们在main
上进行一个新的提交,通过使用git switch main
或git checkout main
选择 commitC
作为我们将处理/处理的提交,然后做一些工作并运行git add
和git commit
进行新的提交D
,如下所示:
A--B--C <-- main
D
新提交D
向后指向以前的提交C
。 但是现在D
——无论它的真实哈希 ID 是什么——都是最新的提交,所以名称main
需要指向D
。 所以git commit
的最后一步是 Git 将D
的哈希 ID 写入名称main
中:
A--B--C
D <-- main
(现在我们可以再次将整个事情绘制在一条线上)。
如果在我们制作D
之前,我们创建一个新的分支名称,比如dev
,会发生什么? 让我们画一个,看看会发生什么:
A--B--C <-- dev, main
我们做出新的承诺D
:
A--B--C
D
哪个名称会更新?Git 的答案很简单:Git 更新我们签出的任何分支名称。所以我们需要知道,我们现在使用的是main
这个名字,还是我们现在使用这个名字dev
?
为了记住我们使用的名称,我们将特殊名称HEAD
添加到一个分支名称中,如下所示:
A--B--C <-- dev, main (HEAD)
这意味着我们正在使用 commitC
,但这样做是因为/通过名称main
. 如果我们git switch dev
或git checkout dev
,我们会得到:
A--B--C <-- dev (HEAD), main
我们仍在使用提交C
但现在我们通过名称dev
使用它。 如果我们现在D
提交,我们会得到:
A--B--C <-- main
D <-- dev (HEAD)
现在有两个最新的提交:C
是最新的main
提交,D
是最新的dev
提交。 请注意,从C
开始的提交都在两个分支上,并且D
是最新的dev
提交这一事实不会干扰C
是最新的main
提交的事实。
假设我们切换回main
(如果我们愿意,可以绘制dev
"上方"而不是"下方"):
D <-- dev
/
A--B--C <-- main (HEAD)
我们已经回到使用提交C
,所以我们看到的是来自提交C
的文件,而不是来自提交D
的文件。 如果我们现在创建并切换到一个名为br2
的新分支,我们得到这个:
D <-- dev
/
A--B--C <-- br2 (HEAD), main
我们仍在使用提交C
但现在我们通过名称br2
来实现。 如果我们现在进行新的提交,我们会得到:
D <-- dev
/
A--B--C <-- main
E <-- br2 (HEAD)
这就是 Git 分支的全部内容。该名称找到最新的提交,然后从那里我们/Git向后工作。我们在向后工作时发现的提交集是"在"分支上的提交集,这是该分支的历史记录。
根据定义,这些名称查找最新的提交。 这允许我们通过强制其中一个分支名称备份一个或两个或三个提交来"让时间倒流"。 当我们使用git reset --hard HEAD~1
来"擦除"提交时,我们会这样做:它实际上并没有消失,我们只是通过确保我们找不到带有分支名称的它来假装我们从未成功过它。例如,如果我们在br2
上并做出错误的提交F
:
D <-- dev
/
A--B--C <-- main
E--F <-- br2 (HEAD)
我们可以使用git reset --hard HEAD~1
(或git reset --hardhash-of-E
)来获得这个:
D <-- dev
/
A--B--C <-- main
E <-- br2 (HEAD)
F ??? [abandoned]
然后我们可以做一个更正的提交G
:
D <-- dev
/
A--B--C <-- main
E--G <-- br2 (HEAD)
F
既然没有办法找到F
,我们就再也见不到它了,好像它已经消失了。 (Git最终可能会在 30 天或更长时间后决定我们真的不想要它,并完全放弃它,但提交很难摆脱。 如果您确实将哈希 ID 保存在某个地方,例如在纸上或文件中,并将其提供给git show
,您可能会发现提交F
仍然存在。 何时甚至是否真的取消了它,故意保持有点神秘。
多个存储库
Git 存储库主要由两个数据库组成:
有一个(通常更大)保存提交对象和其他支持对象。 Git 将其称为其对象数据库或对象存储,Git 需要哈希 ID 才能在此数据库中查找对象。
另外,还有第二个(通常小得多)名称数据库(分支名称、标签名称和许多其他名称),Git 使用它来查找提交和其他对象。 每个名称只包含一个哈希 ID。
我们可以将一个 Git 数据库连接到另一个数据库。 当我们这样做时,两个 Git 软件包使用哈希 ID 来交换对象。 两个存储库将对相同的对象使用相同的哈希 ID,因此它们只需比较哈希ID 即可判断另一个存储库具有哪些对象。 然后一个 Git(无论哪个发送东西)只发送另一个需要的对象,使用它已经拥有的对象来避免发送它不需要的东西。
通过这种方式,两个存储库最终共享提交。 它们实际上具有具有相同哈希 ID 的相同对象,因此它们彼此共享提交。 两者都有所有内容的完整副本。
但是,此名称数据库中的分支名称特定于此特定存储库。 我们可能会让我们的 Git将它们展示给其他一些 Git,并且其他 Git 可以获取这些名称和哈希 ID 并用它们做一些事情,但它们是我们的分支名称。这个数据库中的标签名称,我们尝试共享:如果其他一些 Git 仓库也有标签名称,我们尝试按原样使用它们的名称,并按原样与他们共享我们的标签名称,这样v1.2
意味着两个仓库中的哈希 ID 相同。 但是分支名称不是以这种方式共享的! 每个存储库都有自己的存储库。
而不是共享分支名称,然后,当你运行git fetch
或git fetch origin
,你告诉你的 Git:调用他们的 Git 软件,让它连接到他们的仓库,并通过他们的分支分支名称找到他们所有最新的提交。 然后带上所有提交。 取下他们所有的分支名称,并将其更改为我的远程跟踪名称。他们的main
成为你的origin/main
;他们的dev
成为你的origin/dev
;等等。 这样,无论他们是否向其分支添加了提交,您的分支都不会受到干扰。 您会收到他们拥有的任何新提交,并且您的远程跟踪名称会记住他们最新的提交哈希 ID。
但这对git push
来说并非如此.
来自git push
的"非快进"错误
当你运行git push origin
或git push origin dev
时,你让你的 Git 调用他们的 Git 软件和存储库,就像你对git fetch
所做的那样,但这次你选择让你的Git 将你的新提交发送给他们。 与其让你的 Git 读取他们的分支名称并找到他们的新提交,不如让你的 Git 发送哈希 ID 并找到你的新提交。然后你的 Git 将这些新的提交对象交给他们......然后要求或命令他们设置其分支名称之一。 他们没有您的远程跟踪名称! 您只需要求他们直接设置其分支名称即可。
假设您有:
D <-- dev (HEAD)
/
A--B--C <-- main
在您的仓库中,但他们在仓库中获取了一些提交E
,如下所示:
A--B--C <-- main [their main, in their repository]
E <-- dev [their dev, in their repository]
您的 Git 会向他们发送您的新提交D
,并要求他们设置dev
以记住提交D
。
如果他们这样做了——他们不会这样做——他们将如何找到他们的承诺E
? 请记住,他们的Git 将使用他们的分支名称来查找最新的提交。 如果他们的dev
移动到定位提交D
,并且D
不会导致E
- 并且它不会导致 - 他们将"丢失">他们的提交E
。
如果发生这种事情,他们会说:不,我不会将dev
设置为记住D
作为最新提交,因为这会丢失其他一些最新提交。这在您的git push
显示为:
! [rejected] dev -> dev (non-fast-forward)
error: failed to push some refs to ...
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes
hint: (e.g. 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
发生这种情况时,您需要:
- 运行
git fetch
以获取他们缺少的任何新提交; - 检查你和他们的提交以及这些提交可能相互之间的关系(父、子、兄弟等):使用带有各种选项的
git log
;请参阅 Pretty Git 分支图 - 如有必要,请重新编写您的提交,例如,使用
git rebase
; - 现在您已经纠正了内容,请重复您的
git push
,或者如果您确定应该告诉他们的 Git 存储库是的,请使用git push --force-with-lease
,丢失这些提交!
这是很多需要知道的东西。 但是,如果您要使用 Git 和分布式资料库,则需要了解它。 它至少应该有点熟悉,提交的概念以及它们为你做什么应该非常熟悉。
你可以试试这个:
git fetch
git checkout dev
git add .
git commit -m "your commit message here"
git push
git fetch
将获取从远程存储库到本地的所有现有分支的更新。然后,您可以执行git checkout dev
切换到dev
分支。然后终于commit
,push
dev
然后推送到main
,可以做一个pull request
,批准它,合并到main
。
git branch
仅显示本地分支,要显示远程分支,请使用git branch -r
(要显示本地和远程分支,请使用git branch -a
)。
您可以通过指定源(本地)和目标(远程)分支将任何本地分支推送到任何远程分支:
git push origin main:dev
将本地主分支上的提交推送到存储库"origin"中的远程开发分支。
显然,
git branch dev
git checkout dev
git pull origin dev
git push origin dev:dev
工作。