我知道有很多问题在问同样的问题,但是我没有找到针对我的特定问题的准确答案。
我只是在服务器上设置了一个新的远程git repo并使用了:
git push --all origin
但是,并不是所有的东西都在远程服务器上运行,因此我想再次推送它。无论我尝试了什么,我总是会收到相同的消息:
Everything up-to-date
编辑:
我的意思是git status
:HEAD detached at f6b6299 nothing to commit, working tree clean
。
如上所述,我在遥控器(hint: The 'hooks/post-receive' hook was ignored because it's not set as executable.
)上弄乱了东西。
第一次推送成功: Enumerating objects: 121, done. Counting objects: 100% (121/121)...
但是,在修复上述错误并尝试再次推送后,我收到了Everything up-to-date
消息。我想再次推送它,但似乎不可能。
我在这里想念的是什么以及如何将其再次推到遥控器上?
啊哈,这很重要:
我得到这个git状态:
HEAD detached at f6b6299 nothing to commit, working tree clean.
的方式git push
工作很简单,但也有很多的细节管理。你很可能想在自己的存储库中创建一个新的分支名称,或更新一些现有的分支名称,以标识commit f6b6299
。如果不了解你的具体情况,很难给出更具体的答案。
如上所述,我在遥控器(
hint: The 'hooks/post-receive' hook was ignored because it's not set as executable.
)上弄乱了东西
这是一个单独的问题,必须单独解决。无法直接在Git本身中解决此问题。
git push
我们首先要了解Git本身,这是最基本的知识:Git就是有关commits的一切。那些刚接触Git的人经常认为它与文件有关。不是:它是关于提交的。提交包含文件,这是文件输入图片的位置,但重点是提交。或者,他们可能认为Git与分支有关。不是:它仍然是关于提交的。分支允许我们查找提交,这就是分支名称输入图片的地方。当然,能够找到提交非常重要!但这实际上仍然是提交本身的全部。
每个Git提交都有编号。每个人都有一个唯一的号码,但号码不是连续的。我们不能仅仅对它们进行计数:我们不能将它们列出为提交#1,提交#2等。相反,我们得到了这些大的丑陋的哈希ID。它们看起来是随机的,但实际上完全是确定性的。一串数字和字母,例如f6b6299
(但更长:这个缩写为完整唯一编号的唯一前缀)实际上是一个十六进制数字,给出了该特定提交的原始编号。
这个数字在每个Git存储库中都是相同的,因此我可以通过查看我是否有一个以数字开头的提交f6b6299
(它必须与整个事物完全匹配,来确定我是否有提交)。需要更长的版本)。任何Git存储库都可以通过检入其数据库来判断是否具有此提交。Git存储库中的主数据库中充满了由这些数字索引的内容:这就是Git存储提交(以及随之而来的支持对象)的方式。
有时,你会将Git连接到其他Git。这两个Git具有一种Git-sex,可以在其中交换提交(尽管是单向的:一个Git是发送方,另一个是接收方)。这些是git push
与之相对的地方git fetch
,但是我们稍后再讨论。
无论如何,回到提交:每个都存储两件事:
提交存储你或其他人提交提交时Git知道的每个文件的完整快照。这些文件以特殊的只读,仅Git,压缩和重复数据删除格式存储。重复数据删除解决了人们的反对意见,即当你只更改数千个文件中的一个文件时进行新提交会浪费大量空间。它将这样做,除了如果有5000个文件并且你更改了一个文件之外,新提交仅重用4999个文件。
这是完全安全的,因为提交中的所有文件永远都是只读的。 任何部分的任何承诺都不能更改。这是因为提交的唯一哈希ID包含了提交数据和元数据的每一位。如果你确实确实提取了提交,进行了一些修改然后放回去,那么你得到的是带有新的唯一哈希ID的新提交。现有提交保持不变。
同时,每个提交还存储一些元数据,即有关提交本身的信息。元数据包括你的姓名(或进行提交的人的姓名),电子邮件地址以及一些日期和时间标记等。它包括你使用看到的日志消息git log
,这是你解释为什么进行提交的机会。
不过,对于Git本身而言至关重要的是,Git向此元数据添加了自己的内容。每个提交都存储一些较早提交的原始哈希ID 。大多数提交仅存储其直接父级的一个哈希ID。(在此,我们将不讨论此规则的例外情况。)
这是什么对我们来说意味着此处是很简单的:它意味着每次提交,实际上,点向后到紧前提交。让我们用单个大写字母代表实际的,随机外观的哈希ID来绘制此代码:
... <-F <-G <-H
此处H
是最新提交的哈希ID 。例如,可能是f6b6299
。无论是什么,我们都可以将其记在纸上,白板上或其他东西上,以便我们知道。然后,我们可以在各种Git命令中键入该丑陋的哈希ID,例如git log
。Git将使用该哈希ID在其“包含所有提交的所有Git对象”的大型数据库中进行查找,以查看其是否具有该哈希ID(并检查它是否也是提交,因为还有其他一些对象类型)。
当然,你的Git仓库实在没有这个承诺,所以Git的发现它。因此,Git可以提取该提交附带的所有文件:它们是提交中的数据。同时,Git还可以提取所有元数据,因此它可以显示你进行了提交,以及何时进行。它可以显示你的日志消息。但是对于Git而言,最重要的是,它可以找到该提交之前即将发生的提交的实际哈希ID(我们称为)。G
使用的哈希ID G
,Git可以提取提交G
。这样就可以获取所有文件的完整快照H
,例如,Git可以将其与其中的快照进行比较,以查看更改了什么。G
不过,这也会获取Git的元数据,其中包括尚早提交的哈希ID F
。
使用的哈希ID F
,Git可以读取该提交。Git可以对链中的每个提交继续这样做,从最新到历史的最开始。这些提交中的每一个,当它遍历这个向后看的链时,都是存储在Git存储库中的历史记录。所有这一切都通过提交哈希ID来实现:我们只需要通过将最新的哈希ID保存在某个地方就可以开始整个工作。
这就是分支名称的来源。考虑到我们只要写下最后一个的哈希ID就能找到每个提交,那么,为什么我们不让计算机写下它呢?让我们将最后一次提交的数字(哈希ID)保存在文件或其他内容中。实际上,让我们建立另一个小数据库,其中包含诸如和的名称。在此数据库中,让Git存储某个链中最后一次提交的哈希ID 。develop
feature/tall
我们可以这样画:
...--F--G--H <-- main
\
I--J <-- develop
也就是说,我们的名称数据库包含分支名称(还包括标记名称和其他名称)main
,其中包含分支名称,该分支名称包含commit的哈希ID H
。根据定义,这是该分支上的最后一次提交。它还具有develop
保存哈希ID的名称J
,根据定义,哈希ID是分支上的最后一次提交develop
。
我们说名称指向提交,提交指向父母。因此main
指向H
,它指向返回G
,依此类推。同样,develop
指向,指向J
,指向I
,指向G
。提交通过G
在两个分支上,而提交H
仅在上main
。如果要强调这一点,我们可以像这样绘制同一组提交:
H <-- main
/
...--F--G
\
I--J <-- develop
这表示相同的Git存储库,具有相同的提交和名称。我们只是略有不同地绘制了它,以弄清楚分支的分支位置以及两个分支上的提交。
(请注意,我们在这里相当宽松地使用了分支一词。另请参见“分支”到底是什么意思?)
HEAD
,或者,我们如何知道我们正在使用哪个分支?假设我们确实有这个:
H <-- main
/
...--F--G
\
I--J <-- develop
如果运行git checkout main
或git switch main
,则将使用名称 main
和哈希ID为的提交H
。如果运行git checkout develop
或git switch develop
,则将使用名称 develop
和哈希ID为的提交J
。我们想要一种记住使用哪个名称的好方法。我们开工吧:
H <-- main (HEAD)
/
...--F--G
\
I--J <-- develop
在这里,特殊名称HEAD
告诉我们我们正在使用哪个分支名称。我们只需将其附加到某个分支名称即可。那就是我们使用的名字!如果我们有此设置,并进行新的提交,则Git将通过写出新的提交,为其赋予新的唯一哈希ID并将该哈希ID填充到name中 来处理该过程main
。新的提交将指向commit H
,因为这是我们为了进行新提交而开始的提交,现在,我们将拥有:
H--K <-- main (HEAD)
/
...--F--G
\
I--J <-- develop
在这里,HEAD
仍附加到main
,但现在的名称 main
表示commit K
。
这里的想法是:
HEAD
找到分支名称;和这就是在分支上的含义:特殊名称HEAD
附加到该分支名称上。
HEAD
如果出于某种原因,我们想看看某个历史性的提交怎么办?鉴于:
H--K <-- main
/
...--F--G
\
I--J <-- develop
我们可能要检查提交G
。我们可以做到的一种方法是附加一个新的分支名称来提交G
:
H--K <-- main
/
...--F--G <-- inspect
\
I--J <-- develop
然后我们可以使用git checkout inspect
或git switch inspect
,然后进行提交G
并查看它,甚至进行新的提交:
H--K <-- main
/
...--F--G <-- inspect (HEAD)
\
I--J <-- develop
一旦我们进行了新的提交,它将获得一个新的哈希ID:
H--K <-- main
/
...--F--G--L <-- inspect (HEAD)
\
I--J <-- develop
分支以通常的方式成长。
很好,在Git中很容易做到,但是Git并不需要我们实际使用这个新名称。我们可以代替,只要HEAD
点直接提交一些。也就是说,我们可以这样做:
H--K <-- main
/
...--F--G <-- HEAD
\
I--J <-- develop
如果这样做,并且现在进行一次新提交,则会得到以下信息:
H--K <-- main
/
...--F--G--L <-- HEAD
\
I--J <-- develop
在Git中,这种操作模式称为分离式HEAD。你可以通过这种方式工作,但是它会令人烦恼和痛苦,因为分支名称是我们通常存储最后提交哈希ID的位置。
假设我们已经提交L
并没有在纸上或其他任何东西上写下哈希ID,并决定我们要查看develop
并运行git checkout develop
或git switch develop
。我们会得到这个:
H--K <-- main
/
...--F--G--L ???
\
I--J <-- develop (HEAD)
特殊名称HEAD
不再包含原始哈希ID。它不再分离。那么...我们将如何找到承诺L
?1个
1如果不深入讨论“这里是解决错误的方法”部分,我们将不会。该提交将从视图中消失,我们将再也看不到它。最终,Git会将其丢弃。这就是为什么我们通常不以“分离式HEAD”模式工作的原因-除非是在进行中的重新设置基准期间。但这是针对其他StackOverflow答案的。
比方说,我们是在这种模式下:
H--K <-- main
/
...--F--G--L <-- HEAD
\
I--J <-- develop
并且我们希望Git记住commit的哈希ID L
。显而易见的事情是创建一个指向commit的分支名称L
。我们可以使用任何数量的命令来做到这一点:
git branch save-me
或者:
git checkout -b save-me
或者:
git switch -c save-me
例如。这三个都创建一个新的分支名称,在这种情况下save-me
,该分支名称指向commit L
。该git branch
命令会执行此操作,但是会保持HEAD
分离:
H--K <-- main
/
...--F--G--L <-- HEAD, save-me
\
I--J <-- develop
而checkout和switch命令执行此操作,然后将其附加HEAD
到该名称:
H--K <-- main
/
...--F--G--L <-- save-me (HEAD)
\
I--J <-- develop
因此,它们或多或少都一样好,但是,如果你希望“出现在”新分支上,则可以使用执行该操作的分支。
假设由于某种原因我们处于这种情况下-我们不必担心为什么,只是我们是:
H <-- main
/
...--F--G
\
I--J <-- develop, HEAD
我们可以返回“上”分支develop
,即附加HEAD
到该分支,而无需更改提交。名称develop
选择提交J
; 名字也是HEAD
。所以:
git checkout develop # or git switch develop
将给我们:
H <-- main
/
...--F--G
\
I--J <-- develop (HEAD)
我们HEAD
现在再次安装,我们很分支develop
,使用承诺J
。唯一发生变化的是,我们从分离HEAD模式转到了附着HEAD模式,因此新提交K
现在将develop
通过将新提交的哈希ID写入分支名称来扩展分支。
git push
既然你知道分支名称的工作原理,我们就可以看看git push
真正的作用。我们让Git调用了其他一些Git,并且两个Git具有我前面提到的Git-sex。假设在两个存储库中,我们都是从相同的提交集和一些相似的分支名称开始的。 他们有:
H <-- main (HEAD)
/
...--F--G
\
I--J <-- develop
当我们运行git clone
以复制整个存储库时,我们得到了所有提交,而没有任何分支:
H <-- origin/main
/
...--F--G
\
I--J <-- origin/develop
这些有趣的origin/*
名称是远程跟踪名称,而不是分支名称。他们让我们的Git查找提交,但是他们不让我们“接受”它们。如果我们尝试使用git checkout
以下名称:
git checkout origin/develop
Git将使我们进入“分离头”模式。
现在,在git clone
完成之前,继续在我们的新存储库中创建一个分支名称。-b
当我们使用时,它使用的名称就是我们通过选项告诉它的名称git clone -b develop
:
H <-- origin/main
/
...--F--G
\
I--J <-- develop (HEAD), origin/develop
如果我们不使用告诉它-b
,那么我们的Git会使用一个分支名称,该名称基于其他Git推荐的名称。通常是master
(截至去年的当前Git和GitHub)或main
(截至今天的GitHub,也许还有将来的Git),但是如果你使用的是GitHub,则可以使用此处的Web界面进行设置。所以也许我们得到:
H <-- main (HEAD), origin/main
/
...--F--G
\
I--J <-- origin/develop
现在,我们可以创建自己的分支名称,例如inspect
指向G
或其他名称:我们唯一的限制是,每当要求Git创建新的分支名称时,我们都必须选择一些现有的提交。2 我们使用分支名称(找到最后的提交)或origin/develop
从其他Git分支名称复制而来的远程跟踪名称(例如)找到那些现有的提交;或者我们只运行git log
orgit log --all --decorate --graph --oneline
或其他方法,然后剪切并粘贴原始提交哈希ID。我们根据需要创建和销毁分支名称,但有一个约束条件:我们必须选择一些现有的提交。
当然,我们可以创建新的提交。一旦创建它们,它们就会存在于我们的Git存储库中。现在,我们可以创建新名称来挑选这些新提交。我们可以通过创建一个指向某些现有提交的新分支名称来做到这一点:
git checkout -b newbranch origin/develop
H <-- main, origin/main
/
...--F--G
\
I--J <-- newbranch (HEAD), origin/develop
然后进行新的提交:
H <-- main, origin/main
/
...--F--G
\
I--J <-- origin/develop
\
K--L <-- newbranch (HEAD)
需要注意的是没有origin/newbranch
的,因为他们的Git,在GitHub上或什么的,不具有分支的名称newbranch
。他们也没有提交K-L
。
因此,现在我们运行git push origin newbranch
。这指示我们的Git调用他们的Git。简称origin
,Git称为远程,它存储了我们的Git用于将消息发送到其Git的URL,以便使它们的存储库发生事情。我们的Git调用他们的Git;他们查看他们的存储库;我们和他们看到他们没有承诺K-L
; 并且我们已经告知Git发送这些新提交,因此我们的Git就是这样做的。我们的Git打包了commitK
和L
,以及我们没有的所有文件。
Git使用哈希ID自行解决所有这些问题。由于哈希ID在所有Git存储库中都是通用的,并且它们确实具有提交这一事实意味着它们也具有J
导致所有提交的所有内容J
,因此它们必须具有与所有这些提交一起的所有文件。我们的Git可以把他们的东西尽可能的小,让他们建立的提交K
和L
该店内所有相同的文件和元数据作为我们的提交K
和L
,并因此将具有相同的哈希标识。
发送完这些提交后,我们的Git会要求其Git创建或更新其名称newbranch
,以使其指向commit L
。假设他们服从此请求(默认情况下,我们的Git将其作为有礼貌的请求而非强制命令发送),他们现在将具有:
H <-- main (HEAD)
/
...--F--G
\
I--J <-- develop
\
K--L <-- newbranch
在他们的资料库中。他们有我们的新承诺,而且有个名字newbranch
。我们的Gitgit push
看到他们同意创建newbranch
,从而结束了我们的行动,因此我们的Git创造了我们自己origin/newbranch
来记住他们的newbranch
,现在我们有了:
H <-- main, origin/main
/
...--F--G
\
I--J <-- origin/develop
\
K--L <-- newbranch (HEAD), origin/newbranch
2这里有一个特殊的模式git checkout --orphan
,我们不会进入这里,在这里我们不会选择现有的提交。
git push
HEAD分离的效果不佳假设无论出于何种原因,我们都有这样的分离HEAD情况:
K--L <-- HEAD
/
H <-- main, origin/main
/
...--F--G
\
I--J <-- origin/develop
运行时git push origin
,我们将要求其Git进行新的提交并设置分支名称。 但是我们没有任何分支名称来标识commit L
。 因此,除非我们专门指示这样做,否则我们甚至不会发送承诺。这样的规范采用如下形式:K-L
git push
git push origin HEAD:refs/heads/newbranch # don't do this
使用这种构造很少是一个好主意。它不工作! 只是这会在其Git存储库中创建新分支,但不会在我们的存储库中创建任何分支名称。换句话说,这是造成混乱的秘诀。我们保持在HEAD分离模式,如果我们进行更多的提交,则它们不在任何分支上。
在这种情况下,创建新的分支名称或更新一些现有的分支名称更为明智。例如,我们可以立即创建新名称newbranch
并继续使用它:
K--L <-- newbranch (HEAD)
/
H <-- main, origin/main
/
...--F--G
\
I--J <-- origin/develop
现在我们可以git push origin newbranch
像往常一样,并且我们和他们将使用相同的分支名称newbranch
来标识commit L
。从明年开始,或者与这些存储库一起工作的其他人,将从今天开始对自己感到更加快乐。
请注意,如果我们希望其他Git使用新的提交,则可能需要将它们放置在它们实际使用的任何分支名称上。3 例如,也许我们需要更新它们main
。如果是这样,我们可能应该先更新我们的 main
:
git branch temp
将创建一个临时名称来保存commitL
的哈希ID:
K--L <-- temp, HEAD
/
H <-- main, origin/main
/
...--F--G
\
I--J <-- origin/develop
之后我们可以像git checkout main
这样进行快进合并4:
git switch main
git merge --ff-only temp
既然一切顺利,我们现在就可以将其全部删除5temp
并画出:
K--L <-- main (HEAD)
/
H <-- origin/main
/
...--F--G
\
I--J <-- origin/develop
现在,我们可以安全地运行git push origin main
,向K-L
他们发送提交,并礼貌地要求他们更新其分支名称main
以指向commit L
。如果一切顺利,我们将:
H--K--L <-- main (HEAD), origin/main
/
...--F--G
\
I--J <-- origin/develop
并且,由于他们正在部署main
分支(假设这就是他们正在部署的分支),因此他们现在正在使用与我们正在使用的相同的提交,因此也使用了相同的文件。
3询问你自己使用的分支名称,方式以及原因。每个Git存储库都可以出于任何目的使用其喜欢的名称。GitHub,GitLab,Bitbucket等不同的托管系统为使用分支机构提供了不同的自动化系统。有无数种可能性。Git提供工具和机制,而不是预先打包的解决方案。
4快速合并根本不是合并,但在这里通常是正确的选择。
5如果操作不顺利,则说明我们处于良好状态,因为所有内容都永久保存在提交中,并且我们有可以查找所有提交的名称。很显然,我们必须弄清楚什么地方出了错,并修复是,但因为提交是永久的,只要我们能找到他们反正和安全这样,我们就可以继续进行。