git 从旧分支复制创建新分支的最佳方法是什么?

wqlqzqxt  于 2023-01-15  发布在  Git
关注(0)|答案(3)|浏览(311)

假设我想创建一个全新的分支,它是一个现有分支的完全复制(如果这很重要,稍后我会解释我的特殊动机)。
第一次需要这样做时,我是“手工”完成的:

git checkout oldbranch
git checkout -b newbranch

后来我发现了“官”道:

git branch -c oldbranch newbranch

但我刚刚发现了一个相当严重的问题:使用branch -c,如果oldbranch是一个远程跟踪分支,那么 * newbranch并不是一个真正的全新分支 *,因为它继承了oldbranch的上游,这几乎给我带来了一些严重的问题,我花了相当多的时间试图解开它。
那么什么是正确的方法呢?我应该使用我最初的“手工”方法吗?或者我应该记住,每当我使用branch -c时,然后使用git branch --unset-upstream(我认为这是正确的)来删除上游跟踪吗?
这里出现的上下文是我不得不重定一个分支的基,但是我并不真的想重定这个分支的基;相反,我想重定一个分支的基,我想保留旧的,未重定基的分支,部分是基于一般的(即packratty)原则,部分是因为未重定基的分支已经有了一个上游,我显然不想打乱它。
当我把重定基后的副本推送到上游时,我以为git会抱怨没有上游分支,并提醒我去做--set-upstream的事情,但它却抱怨事情不同步,这时我才发现新副本保留了原来分支的上游。

vuktfyat

vuktfyat1#

TL; DR

我 * 怀疑 * 您可能想将branch.autoSetupMerge设置为false,即git config --global branch.autoSetupMerge false。(这里的联系很明显,不是吗?)不过,至少您想停止使用--copy(又名-c)。🙃) At least, though, you want to stop using --copy aka -c .

假设我想创建一个全新的分支,它是一个现有分支的完全复制(如果这很重要,稍后我会解释我的特殊动机)。
动机 * 确实 * 有些重要,但可能没有你想象的那么重要:
使用git branch -c,[当] oldbranch [是]远程跟踪分支时,* newbranch并不是真正的全新分支 ,因为它继承了oldbranch的上游...
这并不完全正确。相反,所有分支创建方法都可以在新分支上"设置上游"。它们是否以及何时"设置"上游取决于许多选项,这使得描述起来很棘手。在这个特殊的例子中,当oldbranch是一个"远程跟踪名称"(我的术语是:见下文),
默认 * 是oldbranch成为上游,也就是说,我们不 * 找到 * oldbranch的上游-它没有上游;只有 * 分支 * 具有上游-但相反它 * 是 * 上游。
为了更好地解释这一点,让我从为什么我讨厌官方的Git名称origin/main开始。Git把这些名称称为 * remotetracking分支名称 *。这个可怜的单词 * branch * 现在有6种不同的含义(早餐前?),其中之一是"远程跟踪名称"。但我们根本不需要它来表示这个意思。如果我们只使用短语"远程跟踪名称",完全省略单词 * branch *,我们有一个名词短语来表示像origin/main这样的名字,它没有歧义,也不会引起错误的联想。
你可能会问:"什么是错误的关联?"这里我们来看看 * branch * name和 * remote-tracking * name之间的本质区别。两者都定位于一个特定的提交。两者都对查找"在某个分支上看到的"多个提交很有用。(在某个Git存储库中,可能以另一种方式使用单词 * branch *),虽然原始提交哈希在大多数情况下都是一样好的(它不是"一样好"的一种情况是,试着整天输入原始提交哈希,它们对人类来说很难正确,我剪切粘贴)但是:

  • 你可以"进入一个分支"。对于git switchgit checkout,如果操作成功,给它们一个分支名称会让你"进入该分支"。在这种attached-HEAD模式下,进行一个 * new * 提交会将 * new * 提交的哈希ID填充到分支名称中。
  • 一个分支可以有一个 * upstream * 集合。upstream实际上是一个由两部分组成的实体,由一个 * remote name *(如origin)和一个 * 在remote上看到的分支名称 *(如refs/heads/main)组成。幸运的是,git branch --set-upstream-to=origin/branch branchgit rev-parse branch@{upstream}让我们忽略了这个由两部分组成的业务,它在很大程度上可以追溯到"remote"最初被发明的时候。
  • 分支可以为git pull设置"rebase mode"。也就是说,* 在此特定分支上时 *,git pull表示git pull --rebase。(这与全局设置不同。)
    • 分支 * 名称位于refs/heads/中:也就是说,main的全名是refs/heads/main。* 远程跟踪名称 * 位于refs/remotes/命名空间中。

所有这些都在不同的时间以不同的频率出现,特别是git switch在与远程跟踪名称一起使用时需要--detach;git checkout * 在与远程跟踪名称一起使用时隐含 * --detach;在这两种情况下,我们都处于"detached HEAD"模式,因此我们根本就处于 * no * 分支。

分支创建选项

在Git中创建一个新分支实际上包含两个步骤(假设我们已经确定名称是有效的并且没有被使用),但是还有第三和第四个可选步骤:
1.首先,我们必须找到某个提交,我们需要它的原始哈希ID,任何有效的哈希ID都可以 * if * 它是一个 * commit * 哈希ID:禁止使用树、标记和blob哈希ID。
1.然后我们只需要创建一个拼写为refs/heads/*name*的新ref。
1.* * 可选:*我们可以请求Git将这个新分支的 * upstream * 设置为某个名称。该名称可以是分支名称或远程跟踪名称。
1.
* 可选:**我们甚至可以复制更多项目。
git branchgit checkout -bgit switch -c--track-t选项告诉Git它 * 一定要执行步骤3 *,这要求我们也提供一个起始点(尽管它不是传递给-t选项的参数);起始点为步骤1 * 提供散列ID,为步骤3 * 提供名称。
(Alas,从Git 2.35版本开始,这就变得更加复杂了。由于我正在研究历史,在添加新内容之前,让我们从更老的历史开始。
这些命令中的--no-track选项告诉Git * 绝对不应该执行第3步 *。我们现在可以提供一个起点,因为知道第3步 * 不会发生
如果我们既不使用--track也不使用--no-track
默认 * 是Git执行第三步 * 当且仅当 *(a)我们提供了一个起始点 * 并且 *(b)我们提供的起始点是一个远程跟踪名称。

但是,使用git config,我们可以修改两个Git设置:branch.autoSetupMerge和/或branch.autoSetupRebase。将branch.autoSetupMerge设置为always时,即使我们使用本地分支名称,步骤3 * 也会 * 发生 。也就是说,只有当我们使用原始哈希ID或其他不合适的东西(当然,也可以使用显式--no-track)时,才可以避免步骤3。或者,我们可以将其设置为false:那么第3步就不会发生,默认值(我们也可以设置)是true,它选择“如果它是一个远程跟踪名称”模式。
设置好branch.autoSetupMerge之后,我们就可以设置branch.autoSetupRebase了,它设置了git pull是否应该表示git pull --rebase,和前面一样,它有多种模式:一个一米五三氮一x、一个一米五四氮一x、一个一米五五氮一x和一个一米五六氮一x;更多细节请参见the git config documentation(我更感兴趣的是,如果pull.ff设置不是默认的never,它将如何与新的pull.ff设置交互)。
理解了所有这些之后,值得一提的是git switch -t还有另一个功能,假设您有一个remote,比如origin,它在存储库中生成了大量的远程跟踪名称,git switchgit checkout命令有一个--guess选项(默认值为on,包括当你的Git足够老而没有这个选项的时候)启用这个选项后,git checkout *name*git switch *name*会默认先检查 * name * 是否存在,如果是,尝试切换到它。但是如果不是,
在抱怨没有这样的分支名称之前 *,该命令将搜索您的远程跟踪名称。如果恰好有一个“明显匹配”-例如,如果你要求切换到不存在的分支dev,而只有一个origin/dev-那么--guess意味着 * 从origin/dev创建dev *。(设置或不设置上游)规则适用于每个branch.autoSetupMerge
但是如果你有两个遥控器,gh1gh2用于两个不同但相关的GitHub存储库-您可能同时拥有gh1/dev * 和 * gh2/dev。然后git switch --guess dev不知道 * 使用哪个存储库 *。使用git switch -t gh1/dev将从您的gh1/dev创建您的dev(你的Git内存是gh1dev)当然,上游设置是强制的;git switch --no-track gh1/dev将执行相同的操作,但强制关闭上游设置。
在我们继续之前,让我们做最后几个观察:

  • git branchgit checkout -bgit switch -c的额外参数(例如git branch newbr startpoint)提供了初始哈希ID以放入新的分支名称中,也就是说,startpoint被解析为哈希ID,就像git rev-parse一样,但它也被解析为查看它是分支还是用于branch.autoSetupMerge目的的远程跟踪 name

如果我们给予Git一个字符串startpoint^{}startpoint^{commit},得到的哈希ID与默认情况下得到的提交ID相同,但是 string 不再匹配分支或远程跟踪 name,因为后缀的原因,所以这个 * 自动地取消了autoSetupMerge设置 *。

  • 除了上游设置之外,分支名称还可以具有变基设置,因此创建新分支实际上有四个步骤,其中两个是可选的(可选地设置上游,可选地设置变基标志)。
  • 除了上游设置,分支名称还有一个 reflog。reflog包含存储在分支名称中的 * hash ID历史 *。(使用git reflog maingit reflog master转储mainmaster分支的reflog,以查看这些。)“第零”项是当前值。

reflog * 可以 * 被禁用(尽管你仍然有一个自动的@{0}),但是在非裸仓库中默认是打开的。所以你可能有你所有分支名称的reflog。HEAD本身也有reflog,你可以有一个针对 every 引用的reflog。core.logAllRefUpdates设置控制是否根据需要创建新的reflog;参见the git config documentation
除了upstream和reflog,每个分支都可以有任意的附加设置,Git now 中没有,但将来可能会有,例如,你可以运行git config branch.main.abc def来设置branch.main.abc = def:它没有任何意义,但是你可以设置它。
git branch-c选项是 copy 标志。它也告诉git branch你正在创建一个新分支,当然,复制东西是没有意义的。但是“create new branch”是git branchdefault 操作。如果没有设置其他操作。添加-c--copy意味着 * 复制reflog和所有其他设置 *(即使是那些Git不知道的!)这将在从本地分支“复制”时复制 upstream 设置,因为它是一个设置。

现在我们也可以描述Git 2.35中新增的--track标志:--track=direct--track=inherit-t选项表示--track=direct。当branch.autoSetupMerge具有默认值时,只有在使用远程跟踪名称创建新分支时,我们才会获得默认的上游设置。远程跟踪名称本身 * 就是 * 新分支的上游。但如果我们将branch.autoSetupMerge设置为always,我们将得到一个带有git branch newbr foo * 的上游集合,以及带有git branch newbr origin/foo的 *。有些人不喜欢 * newbr的上游 * 现在是(本地)分支foo的事实。他们希望git branch读取foo的 * 上游 *,并将newbr的上游设置为foo的上游。
这就是git branch --track=inherit所做的。你必须这样拼写--track=inherit。注意这也是git branch --copy(又名git branch -c)所做的;只是-c在这个过程中做了很多事情(复制reflog加上所有设置)。

重置并保留

这里出现的上下文是我不得不重定一个分支的基,但是我并不真的想重定这个分支的基;相反,我想重定一个分支的基,我想保留旧的,未重定基的分支,部分是基于一般的(即packratty)原则,部分是因为未重定基的分支已经有了一个上游,我显然不想打乱它。
我自己也经常这样做,不过一般来说,我只保留 * 当前版本 * 的上游版本(或者不保留任何上游版本),所有的旧版本都放在我自己的存储库中:

git switch somebranch        # get on it before rebasing
git branch -m somebranch.0   # rename it to somebranch.0
git switch -c somebranch     # make the new one using HEAD, no upstream
git rebase ...

因为我总是使用 * local * 名称(以及git checkout -bgit switch -c),所以即使使用默认设置,我也从来没有使用过上游集,下次我重命名时,我将somebranch重命名为somebranch.1,以此类推。
当我去把重定基的副本推到我的上游时,我期望git会抱怨没有上游分支...
一个很好的副作用是,当我像这样 * 重命名 * 分支时,任何现有的上游设置都会保留旧的(但现在重命名为.0、.1等)分支,这对我来说意味着我不能git push它,因为我将push.default设置为simple:两边的名称不再匹配。因为我从现有分支创建了新分支,它没有上游集,我也不能git push它。
我 * 可以 * 只是依靠刷新日志:如果我根本没有重命名任何东西,那么somebranch的reflog中的值就会变成somebranch.0somebranch.1等等。但是reflog条目反映了一些"自动"的东西,而不是我做出的一些深思熟虑的决定。如果我正在进行实质性的更改,我可能会首先为分支选择一个新的"名称"。

bzzcjhmw

bzzcjhmw2#

那么什么是正确的方法呢?
正确的方法是

git checkout -b newbranch oldbranch

git switch -c newbranch oldbranch

git branch-c选项并不是创建一个新分支的正式方式,该分支以与现有分支相同的“内容状态”开始-它的目的是复制 * 更多 * 关于分支的元数据。
除了git checkout -bgit switch -c,您还可以使用git branch,但 * 不带 * -c选项:

git branch newbranch oldbranch

使用checkout或switch的主要原因是,它们会将您带到新的分支,这通常是您想要的。
(也许离题了,我很好奇:当您提到正式的方式时,您的意思是一直使用git switch -c吗?)

kadbb459

kadbb4593#

假设我想创建一个全新的分支,它是一个现有分支的精确副本。
但你没有!
如果你先解决这个误解,一切都会变得清晰起来。
首先,你要做的并不叫“复制”,而是“分支”。从另一个分支(或任意提交)创建的分支只是指向该提交的指针。新分支开始时与旧分支有着完全相同的历史,这似乎是你想要的,但它们可能会独立地发散。
分支是:一个引用名,一个特定提交的引用,可选的一个上游,可能还有一些其他分支特定的配置。如果你不想复制所有的配置,那么复制一个分支不是你要做的。
为了更清楚起见手册上说:

-c, --copy
    Copy a branch, together with its config and reflog.

其中,config 表示与.git/config中该分支相关的所有内容,例如:

[branch "oldbranch"]
        remote = origin
        merge = refs/heads/oldbranch

那么你到底想要什么呢?显然不是一个字面上的副本,包括上游的精确副本。
快跑

git checkout -b newbranch oldbranch

相关问题