1

我的本地系统上有一个 git 存储库/original/.git。现在,我已将这个存储库克隆到/cloned_one/.git. 一切看起来都很好。在这里,我有相同的文件,/original其中非常好。我创建一个新文件并提交它。现在,我的克隆存储库是一个提前提交。我想推动改变,但不知道如何!

它变得更加棘手,因为我对以下命令的多样性和确切的用例有点困惑。我知道其中一些,并且实际上以某种方式与它们合作,但我看到很多用户在错误的地方使用它们,这让我感到困惑。不确定何时使用以下命令。

  • git fetch
  • git rebase
  • git push
  • git merge
  • git --force push

谢谢。

4

3 回答 3

2

Git 的远程仓库可以来自 ssh、https 和目录。

在你的例子中,

  • /original/.git具有指向 github 的远程源(通过 ssh 或 https,例如https://github.com/user/example.git:)
  • /cloned_one/.git具有指向目录的远程源(例如:/original/.git

这看起来像某种链表:

[/cloned_one/.git] --origin--> [/original/.git] --origin--> [github]

这是重现此类设置的示例命令:

$ cd /tmp
$ git clone https://github.com/schacon/example.git original
Cloning into 'original'...
remote: Enumerating objects: 4, done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 4
Receiving objects: 100% (4/4), 18.52 KiB | 3.70 MiB/s, done.
$ mkdir cloned_one
$ git clone /tmp/original cloned_one
Cloning into 'cloned_one'...
done.
$ cd cloned_one/
$ echo newfile > newfile.txt
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        newfile.txt

nothing added to commit but untracked files present (use "git add" to track)
$ git add newfile.txt 
$ git commit -m 'add new file'
[master e45e780] add new file
 1 file changed, 1 insertion(+)
 create mode 100644 newfile.txt
$ git remote -v
origin  /tmp/original (fetch)
origin  /tmp/original (push)
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

现在,cloned_one提前 1 次提交original

您可以将更改从cloned_oneto推送到originalwith (记住 to cd /tmp/cloned_onefirst):

  • git push
  • git push origin master
  • git push /tmp/original master

这里推送的语法是指定你要推送的位置(例如:到origin,或者到/tmp/original目录),以及你要推送的分支

$ cd /tmp/cloned_one/
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
$ git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 333 bytes | 333.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: is denied, because it will make the index and work tree inconsistent
remote: with what you pushed, and will require 'git reset --hard' to match
remote: the work tree to HEAD.
remote: 
remote: You can set the 'receive.denyCurrentBranch' configuration variable
remote: to 'ignore' or 'warn' in the remote repository to allow pushing into
remote: its current branch; however, this is not recommended unless you
remote: arranged to update its work tree to match what you pushed in some
remote: other way.
remote: 
remote: To squelch this message and still keep the default behaviour, set
remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
To /tmp/original
 ! [remote rejected] master -> master (branch is currently checked out)
error: failed to push some refs to '/tmp/original'

现在它说您无法推送到 /tmp/original 因为它不是一个裸仓库。您可以通过将 /tmp/original 更改为裸 repo 来解决此问题,或者只需将其配置为在推送到如下所示时更新其工作树:

$ cd /tmp/original/
$ git config receive.denyCurrentBranch updateInstead
$ cd /tmp/cloned_one/
$ git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 333 bytes | 333.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To /tmp/original
   c3d5e92..e45e780  master -> master

现在,如果您想将您的更改推送回 github(的来源/tmp/original),您可以从/tmp/original或从推送它/tmp/cloned_one

  • cd /tmp/original ; git push origin master
  • cd /tmp/cloned_one ; git push https://github.com/username/example.git

请注意,当从 推送时/tmp/original,您可以将目标指定为origin(因为 /tmp/original 的来源来自 github)。从 推送时/tmp/cloned_one,您必须将目标指定为完整 URL。

也可以将cloned_one's remote 更改为指向 github(搜索 git remote 手册)

进一步阅读:

于 2021-11-05T02:12:56.230 回答
2

关于 Git 命令

TL;博士

要将更改上传到克隆的存储库,请使用git push. 即使您克隆了本地计算机上的存储库,这也适用。

本地分支机构和远程分支机构

为了理解事物,存储库通常有两组分支:

  • 您当地的分支机构
  • 远程分支

通常,当您使用 列出分支时git branch,它只会列出本地分支。但是您可以列出所有这些git branch -a在此处输入图像描述

在这里,这显示了我的本地分支以及远程分支(红色,表示它们是远程的)

当你运行时git pull,这实际上做了两个步骤:

  • 首先,它运行git fetch。这将根据您克隆的存储库中的任何内容更新所有远程分支。请注意,此 repo 实际上可能不在远程服务器上;它可能在您的本地机器上,就像您的情况一样。
  • 其次,它运行git mergegit rebase取决于您的配置选项。这会将您的远程分支合并到您的本地分支中。

这就是您的存储库的更新方式。

但是你会在不合并的情况下获取吗?

在不合并的情况下获取可能很有用,因为它允许您在将远程更改合并到本地分支之前检查它们。这可以这样做:

git fetch
git diff <your branch> origin/<your branch>

在我的一个存储库中运行它,我看到我克隆的原始存储库发生了以下更改。(这使用diff-so-fancy以稍微漂亮的方式显示更改,但在普通 git diff 上看起来不错)。

在此处输入图像描述

TL;DR:您通常不需要获取,但提前查看更改很不错

合并与变基

合并是指您获取两个单独的历史记录,并将它们与合并提交连接在一起。

大多数提交都有一个父级,但合并提交有两个父级(或三个或更多,在章鱼合并的情况下)。初始提交的父节点为零,但它们很特别,因为它们之前没有任何内容。

我们可以看一下我正在处理的一个项目的提交图,以查看一个示例。在这里,我确实在几个功能分支中工作,然后创建拉取请求以将它们合并回主分支。

在此处输入图像描述

我认为这样的图表很漂亮,但有些人更喜欢线性历史,这就是重新定位的用武之地。

它不是将两个分支连接在一起,而是将一个分支折断,然后将其粘在另一个分支上。这是一篇很好的文章,详细介绍了它。

rebase 有什么用?

如果您在本地分支上有提交(例如,main),并且在远程分支上有新的提交(例如,origin/main),那么通常,作为拉取的最后一步,git 将合并两者,以便您的本地分支现在将拥有来自origin/main.

这是我更喜欢变基而不是合并的一次:将本地提交放在远程提交之上会更好,因此历史上没有多个分支。

我使用以下命令将变基设置为默认选项:

git config --global pull.rebase merges

这意味着“将我的本地提交重新设置在远程提交之上,但保留我有意进行的任何合并(例如合并到功能分支中)”。这是一个很好的默认设置,它让我很开心。

git push 和 git push --force

这是您将本地更改获取到克隆存储库的方式。

git push将上传您的更改,但如果远程存储库有您没有的更改(例如,因为同事在您之前推送到它),它将拒绝上传您的更改,因为这会导致冲突。解决这个问题很简单:

git pull
git push
  • 拉取通过合并或变基将您同事的更改与您的更改集成,以便可以上传您的更改
  • 推送然后上传您的更改

那么 git push --force 呢?

使用这个命令要非常小心。它的主要目的是重写历史,如果秘密被提交到 git 存储库,或者如果您出于其他原因需要永久删除文件,这有时是必要的。

如果您的同事对远程存储库进行了更改,则强制推送将覆盖这些更改

如果你想覆盖你所做的提交,一个更安全的选项是--force-with-lease,这将确保唯一被覆盖的更改是你的。

于 2021-11-05T02:36:11.487 回答
2

这是我喜欢的想法:存储库共享提交,但它们不共享分支名称。

Git 存储库的核心——边缘变得凌乱——实际上是一对数据库。两个数据库都是简单的键值存储

  • 其中一个——通常是迄今为止最大的——被大而丑陋的哈希 ID 索引/访问。这些是提交哈希 ID,以及其他内部 Git 对象哈希 ID。所以这是对象数据库(尽管 Git 通常不会以这种方式引用它)。

  • 另一个将名称(分支名称、标签名称和所有其他类型的名称)映射到那些丑陋的大哈希 ID。这让我们这些可怜的人使用Git;哈希 ID 不可用。这是名称数据库,每个名称都映射到一个哈希 ID。多个名称可以映射到一个特定的哈希 ID,并且许多哈希 ID 没有映射到它们的名称,所以如果您习惯于从数学集合的角度思考,这是一种非内射、非满射的映射(见满射函数)。

对象数据库中的每个提交对象都有一个唯一的哈希 ID任何对象的哈希 ID,无论是否提交,都是对构成对象的数据运行加密哈希函数的结果(这就是 Git 进行一致性检查和文件内容去重的方式)。但由于提交的几个属性,每个提交本身都是唯一的,因此每个提交都有一个唯一的哈希 ID。1 使用这些提交哈希 ID,任何两个不同的Git 存储库,使用兼容的 Git 软件——我称之为“两个 Git”——这两个 Git 可以通过比较提交哈希 ID立即判断它们是否具有相同的提交。2

这一切归结为,当我们将两个 Git 相互挂钩时,一个——发送者——说我有 commit之类的a123456...话,你愿意吗?其他人回答是,请不,谢谢,我已经有了,这允许发送者将发送者所拥有的接收者缺少的提交发送给接收者。

这个过程——例如,发送我有但你没有的提交——是一个git fetchgit push,取决于谁开始对话。如果您开始对话以获取我的提交,那么您正在使用git fetch. 如果我开始对话以向您发送提交,我正在使用git push.


1任何熟悉鸽笼原理的人都会立即反对哈希函数,即使是具有 2 160范围 (SHA-1) 或 2 256范围 (SHA-256) 的哈希函数,最终也必须产生冲突。这是真的,当这种情况发生时,Git 停止运行,因此哈希必须足够大,以至于“最终”足够几十年或几个世纪后不用担心。(由于生日问题,这比起初看起来要难。)

2由于提交形成的 DAG,我们可以做得比仅列出所有原始哈希 ID 更好,但如果我们有两个相当小的存储库(例如每个存储数千个提交),那么仅列出所有原始哈希 ID 就很容易了。哈希 ID,查看谁有哪些提交。


这就是使用多个 Git 存储库的大部分内容

说真的,这种传输是我们所需要的,也是原始 Git 中的全部,用于将两个 Git 相互连接。通过比较哈希 ID 并发送我有的提交,而你没有,你最终会分享我的提交。如果我然后转身从你那里得到任何没有的提交,我们现在就我们拥有的提交集而言是平等的。但这不是使用Git 的便捷方式,因此我们现在添加一些附加功能。

分支名称、标签名称、远程跟踪名称等

人类不擅长哈希 ID e9e5ba39a78c8f5057262d49e261b42a8660d5b9:. 说什么?可能有几个人可以向您重复 Git-repository-for-Git 标签哈希 ID 列表,如果他们已经阅读过它们,但我有时会转置字符,并且不费心尝试记住它们。这就是计算机的用途。我让计算机为我记住哈希 ID:现在master可能会保留e9e5ba39a78c8f5057262d49e261b42a8660d5b9。稍后,master将持有一些其他的哈希 ID。无论它持有什么哈希 ID,也就是说,根据定义,在这个 Git 存储库中的分支上的最新提交。 master

当我们使用 时git fetch,Git 会以一种很好的方式为我们维护所有这些东西。我们创建一个远程——一个类似的短名称——origin我们在它下面存储一个传统的 URL,例如ssh://git@github.com/path/to/repo.git,或类似的路径名/absolute/path/to/dir/.git,或任何适当的名称。3

现在我们有了这个短名称,origin或者fred我们选择的任何名称,我们的Git 可以调用他们的Git——也就是说,我们的 Git 软件可以使用 ssh 或 https 或其他任何方式来访问他们的 Git 软件;我们的 Git 将在我们的存储库中运行,他们的 Git 将在他们的存储库中运行——一旦我们调用了他们的 Git,我们就可以git fetch从他们那里获取提交。我们用一个简单的方法来做到这一点:

git fetch origin

例如。

他们的 Git 列出了他们的分支名称和相应的提交哈希 ID。我们的 Git 检查这些哈希 ID 并使用它来确定它们是否有我们没有的提交。如果我们不限制我们的 Git,4我们的 Git 现在会带来我们还没有的所有提交,所以现在我们共享他们的提交。

但是:我们的分支名称是我们用来在存储库中查找特定提交的东西,我们出于某种特定原因喜欢它。我们git fetch 不碰我们的分支名称。相反,我们的 Git 采用它们的每个分支名称并将它们更改为远程跟踪名称5 Git 通过将远程名称(在本例中为)粘贴在origin分支名称前面加上一个斜杠来做到这一点。所以这是从哪里来origin/mainorigin/master:那是我们的他们的分支名称的副本。这是我们的 Git 记住他们的 Git或(无论他们拥有哪个)的方式。mainmaster

所以,git fetch让我们获得他们的提交,但对我们自己的分支不做任何事情。由于 Git 使用散列技巧,所有对象(包括所有提交)都是完全只读的,因此此更新仅将新提交添加到我们的存储库。我们分享他们的提交,但不分享他们的分支名称。


3一个非常短的相对路径 like../d/.git并不比 长得多或难于键入origin,因此对于这种情况,您可能会想省略远程名称。但不要这样做!Git 需要该远程名称来形成远程跟踪名称。如果您跳过使用远程名称,那么您使用的是 Git 仍然支持的原始模式,这只是 <插入正文部分> 的痛苦。

4您可以故意限制您git fetch减少网络流量或其他任何事情。大多数时候这几乎是不必要的,因为 Git 很聪明,只带入任何需要的对象。不过,在极少数情况下,它可能很有用。

5 Git 调用这些远程跟踪分支名称。虽然它们不是分支名称,但无论如何不在我们的存储库中。它们是我们复制别人的分支名称。因此,从我们的角度来看,这些是非分支名称,只是碰巧“跟踪”了其他人的(分支)名称。Git 过度使用了动词track和形容词branch,但我们可以在这里减少这种过度使用。


从此时起,您在本地工作,至少直到git push

你现在拥有的是:

  • 所有提交;
  • 您自己的分支名称;和
  • 您的分支名称的副本/记忆。

您可以通过将特定提交 ( ) 提取到工作树和 Git 的索引暂存区域(同一事物的两个术语)中来使用您的存储库。您最终会做出新的提交,这些提交会添加到现有的提交中。当您进行新提交时,Git 会自动更新您当前的分支名称,以便您的分支名称自动包含您的新提交。git checkout

您已经询问了合并和变基,这些实际上都是相当大的主题,但它们在其他地方已经很好地涵盖了。请记住,这git merge主要意味着结合工作。尽管git merge对通常的工作组合方式充满了例外,但这主要是通过进行新的提交来发生的。同时,git rebase这意味着我对我的存储库中的某些提交感到满意,但是我不喜欢这些提交。我想将它们复制到一组新的和改进的提交中,并可以选择在此过程中使用特殊技巧。

因为你实际上不能更改任何现有的提交,所以复制意味着你得到重复——或者更确切地说,接近重复,“接近”部分取决于你正在改变的内容,假设你正在改变一些东西或者你' d 只使用原件。rebase 的目标是停止使用原始文件并开始使用近似重复文件。由于我们通常使用分支名称来查找我们的提交,Git 通过移动名称来实现这一点:

             K--L   <-- br2 (originals)
            /
...--F--G--H--I--J   <-- br1
                  \
                   K'-L'  <-- proposed new br2

<-- older ... newer -->

我们有 Git 复制KK',通过使用J作为基本提交来改进副本,然后我们有 Git 复制L到,通过使用作为基础来L'改进副本。这是变基的主要部分。但是现在我们必须停止使用,并开始使用。由于我们通过分支名称找到提交,Git 只需要从提交中“剥离标签”并将其粘贴到提交上。然后 Git 向后工作——这是 Git 中的一个通用主题,它从末尾开始并向后工作——从 开始,所以现在你再也看不到了。K'L' K-LK'-L'LL'L' K-L

因为提交是共享的,而分支名称不是,如果你从其他 Git 存储库获得提交,你现在有一个问题:他们的分支名称仍然指的是 commit ,而不是你的 new 。K-LLL'

git push不只是一个颠倒的git fetch

当你运行时git push,你:

  • 让你的 Git 调用其他 Git;
  • 让你的 Git 给出 Git特定的提交:不仅仅是我拥有的所有你没有的,而是我拥有的某个分支上的每个提交,而你没有(提交我有你没有的,在某个特定的分支上我的选择);
  • 最后,一旦这些提交完成,您礼貌地请求(常规git push)或命令(git push --force他们的Git 应该更新他们的分支名称

当我们使用 时git fetch,我们有这些友好的远程跟踪名称,我们的 Git 可以通过这些名称记住它们的分支名称。该push命令不打扰友好。我们只是建议用一些新的哈希 ID 完全覆盖他们的分支名称。

如果我们发送给他们的提交添加到他们当时看到的分支中,那么这个礼貌的请求(例如,请将您的分支名称develop设置a123456...为)可能是可以的。他们有:

...--G--H   <-- develop

whereH代表9876543...或任何哈希 ID。我们向他们发送了提交I-J

...--G--H   <-- develop
         \
          I--J   <-- polite request to set "develop" to point to J

如果他们遵守这个礼貌的请求,他们的提交H仍然可以找到,因为 Git 从分支名称的提交向后工作。 J导致返回I导致返回H,嘿,我们没有丢失任何提交!

但是,在 a 之后git rebase,我们接受了一些现有的提交并将它们丢弃,以支持新的和改进的替代品。假设他们有:

             I--J   <-- br1
            /
...--F--G--H   <-- develop
            \
             K--L   <-- br2

他们的存储库中,我们现在向他们发送一对K'-L'添加到 commit 的提交J

                  K'-L'   <-- please set br2 here
                 /
             I--J   <-- br1
            /
...--F--G--H   <-- develop
            \
             K--L   <-- br2

这个礼貌的请求,他们将其设置br2为指向L'而不是L,不会很好地接受。他们会说:不!如果我这样做,我会失去提交! 当然,我们希望他们输K-L,赞成K'-L'。但除非我们告诉他们,否则他们不知道这一点。

这是git push --force为了什么。但是,如果他们的 Git 存储库非常活跃,也许我们更早 K-L,但现在他们有:

                  K'-L'   <-- please set br2 here
                 /
             I--J   <-- br1
            /
...--F--G--H   <-- develop
            \
             K--L--N   <-- br2

我们提议他们放弃整个K-L-N链条,转而支持我们的K'-L'.

--force-with-lease选项是试图改善这种情况。它在当前的 Git 中运行良好,但可能会出现一些不太正确的更新。(我认为它应该保持“正确”,也有建议这样做,所以我们拭目以待。)

裸仓库

当我们使用 Git 时,我们有一些提交已签出。这个提交来自我们当前的分支,是我们当前的提交。我们可能正在处理一个新的提交。在这种状态下,Git 确实无法接收到我们当前分支的更新:这会搞砸事情。6 因此,Git 将拒绝推送到当前签出的分支。

为了回避这个问题,用于接收请求的 Git 存储库通常设置git push存储库。裸存储库是没有工作树的存储库。没有工作树,没有人可以在裸存储库中做任何工作。这意味着没有什么可以搞砸的,裸存储库总是可以收到推送。

还有其他方法可以解决这个问题(配置设置receive.denyCurrentBranch),但另一个回避问题的好方法是避免 git pushgit fetch在使用两个不同的存储库时使用,您可以同时控制它们。当你在 A 工作时总是从 B 取,而当你在 B 工作时总是从 A 取,你不能踩到自己。


6特别是,Git 的索引包含当前提交的图像,由我们编辑的任何建议的替换文件git add、我们添加的新文件和/或我们编辑的文件修改git rm。Git 将从索引中的任何内容构建新提交,新提交的父级将是当前提交,其哈希 ID 存储在分支名称中。因此,更新分支名称也需要更新索引和工作树。如果我们正在做某事,那是个坏主意。即使我们不是,更新分支名称也意味着我们的新提交可能会到达我们没有预料到的地方。整个形势严峻。

底线

在 Git 中,您使用commits在本地工作。这些在您的存储库中:只有一个,您现在在其中,具有一棵工作树和 Git 的索引。7 要从其他 Git 获取提交,请使用git fetch远程短名称,如origin. 添加更多遥控器,使用git remote add,以添加更多从 获取位置。用于修改git remote set-url一个特定远程的 URL,git remote remove删除一个远程,git remote update并使 Git 从所有远程获取。8 考虑设置fetch.prunetrue解决我认为fetch默认值中的历史错误。9

在 you 之后git fetch,您拥有所有提交。对他们做任何你喜欢的事。将它们留在您的存储库中,重新设置它们,无论如何。然后,如果另一个 Git 是设置为接收请求的Git,请使用发送您的新提交并要求他们设置其分支名称之一以记住您的提交:git pushgit push

  • 如果这在他们的 Git 中创建了一个新名称,他们可能会允许它。
  • 如果这更新了一个名称,使得旧的提交仍然存在,他们可能会允许它。
  • 如果这更新了名称以停止在他们身边找到旧的提交,除非您使用其中一个--force选项,否则他们将拒绝它;为安全起见--force-with-lease

如果另一个 Git不是裸露的,和/或您想直接在其中工作,请更改您正在工作的位置,以便您现在位于另一个存储库中,并用于git fetch获取您在您所在的存储库所做的提交工作,刚才。由于fetch它本身总是安全的,因此10这将是安全的。

永远,永远记住,如果你还没有提交某些东西,那么它不在Git 中(还)。如果你已经提交了它,它就在一个提交中,即使在小灾难之后,你通常也可以取回它。11


7git worktree add使这张图片复杂化了,所以我们在这里忽略它。

8或者,使用git fetch --all:这是什么--all意思,所有遥控器。该git remote update命令更漂亮,但可以让你在这里做更多的事情。

9也就是说,运行:

git config --global fetch.prune true

一次,在您的全局配置中,在这台特定的机器上设置它。当git fetch从某个远程 R 获取数据时,它会为它在 R 上看到的每个分支创建或更新。但是如果 R 有一个名为昨天的分支,而今天该分支消失了,那么您的 Git 不会做任何事情:它是昨天在您运行时创建的,并且现在没有创建或更新,所以它不会创建或更新它。这会给你留下一个“陈旧的” 。R/branchfancy42fancy42git fetchfancy42fancy42

启用修剪git fetch后,git remote update请注意它们现在fancy42已经消失了,因此您也不应该拥有它们R/fancy42。你的 Git 会删除它。这不是默认值,因为它最初不是默认值,现在更改它为时已晚。可能有人喜欢甚至依赖它。如果您自己喜欢它,请fetch.prune设置为false并且不要运行git remote prune

10您可以配置或强制git fetch设置为“不安全”,但除非您故意这样做或使用git clone --mirror这样做,否则您不会遇到这种情况。

11这条规则的最大例外是如果 Git 数据库本身被损坏。这往往发生在以下情况:

  • 存储库位于共享驱动器(Google Drive、iCloud、OneDrive 等)上;
  • 计算机崩溃或未正确关闭,导致操作系统无法将内容写入磁盘;
  • 计算机本身着火(这曾经是笔记本电脑的常见问题);或者
  • 您使用操作系统强制删除.git目录内的 Git 存储库数据库。

否则这些东西真的很可靠。

于 2021-11-05T04:57:12.823 回答