假设我想创建一个全新的分支,它是一个现有分支的完全复制(如果这很重要,稍后我会解释我的特殊动机)。
第一次需要这样做时,我是“手工”完成的:
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
的事情,但它却抱怨事情不同步,这时我才发现新副本保留了原来分支的上游。
3条答案
按热度按时间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 switch
或git checkout
,如果操作成功,给它们一个分支名称会让你"进入该分支"。在这种attached-HEAD模式下,进行一个 * new * 提交会将 * new * 提交的哈希ID填充到分支名称中。origin
)和一个 * 在remote上看到的分支名称 *(如refs/heads/main
)组成。幸运的是,git branch --set-upstream-to=origin/branch branch
和git 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 branch
、git checkout -b
或git 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;更多细节请参见thegit config
documentation(我更感兴趣的是,如果pull.ff
设置不是默认的never
,它将如何与新的pull.ff
设置交互)。理解了所有这些之后,值得一提的是
git switch -t
还有另一个功能,假设您有一个remote,比如origin
,它在存储库中生成了大量的远程跟踪名称,git switch
和git checkout
命令有一个--guess
选项(默认值为on,包括当你的Git足够老而没有这个选项的时候)启用这个选项后,git checkout *name*
或git switch *name*
会默认先检查 *name
* 是否存在,如果是,尝试切换到它。但是如果不是, 在抱怨没有这样的分支名称之前 *,该命令将搜索您的远程跟踪名称。如果恰好有一个“明显匹配”-例如,如果你要求切换到不存在的分支dev
,而只有一个origin/dev
-那么--guess
意味着 * 从origin/dev
创建dev
*。(设置或不设置上游)规则适用于每个branch.autoSetupMerge
。但是如果你有两个遥控器,
gh1
和gh2
用于两个不同但相关的GitHub存储库-您可能同时拥有gh1/dev
* 和 *gh2/dev
。然后git switch --guess dev
不知道 * 使用哪个存储库 *。使用git switch -t gh1/dev
将从您的gh1/dev
创建您的dev
(你的Git内存是gh1
的dev
)当然,上游设置是强制的;git switch --no-track gh1/dev
将执行相同的操作,但强制关闭上游设置。在我们继续之前,让我们做最后几个观察:
git branch
或git checkout -b
或git switch -c
的额外参数(例如git branch newbr startpoint
)提供了初始哈希ID以放入新的分支名称中,也就是说,startpoint
被解析为哈希ID,就像git rev-parse
一样,但它也被解析为查看它是分支还是用于branch.autoSetupMerge
目的的远程跟踪 name。如果我们给予Git一个字符串
startpoint^{}
或startpoint^{commit}
,得到的哈希ID与默认情况下得到的提交ID相同,但是 string 不再匹配分支或远程跟踪 name,因为后缀的原因,所以这个 * 自动地取消了autoSetupMerge
设置 *。git reflog main
或git reflog master
转储main
或master
分支的reflog,以查看这些。)“第零”项是当前值。reflog * 可以 * 被禁用(尽管你仍然有一个自动的
@{0}
),但是在非裸仓库中默认是打开的。所以你可能有你所有分支名称的reflog。HEAD
本身也有reflog,你可以有一个针对 every 引用的reflog。core.logAllRefUpdates
设置控制是否根据需要创建新的reflog;参见thegit 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 branch
的 default 操作。如果没有设置其他操作。添加-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)原则,部分是因为未重定基的分支已经有了一个上游,我显然不想打乱它。
我自己也经常这样做,不过一般来说,我只保留 * 当前版本 * 的上游版本(或者不保留任何上游版本),所有的旧版本都放在我自己的存储库中:
因为我总是使用 * local * 名称(以及
git checkout -b
或git switch -c
),所以即使使用默认设置,我也从来没有使用过上游集,下次我重命名时,我将somebranch
重命名为somebranch.1
,以此类推。当我去把重定基的副本推到我的上游时,我期望git会抱怨没有上游分支...
一个很好的副作用是,当我像这样 * 重命名 * 分支时,任何现有的上游设置都会保留旧的(但现在重命名为.0、.1等)分支,这对我来说意味着我不能
git push
它,因为我将push.default
设置为simple
:两边的名称不再匹配。因为我从现有分支创建了新分支,它没有上游集,我也不能git push
它。我 * 可以 * 只是依靠刷新日志:如果我根本没有重命名任何东西,那么
somebranch
的reflog中的值就会变成somebranch.0
、somebranch.1
等等。但是reflog条目反映了一些"自动"的东西,而不是我做出的一些深思熟虑的决定。如果我正在进行实质性的更改,我可能会首先为分支选择一个新的"名称"。bzzcjhmw2#
那么什么是正确的方法呢?
正确的方法是
或
git branch
的-c
选项并不是创建一个新分支的正式方式,该分支以与现有分支相同的“内容状态”开始-它的目的是复制 * 更多 * 关于分支的元数据。除了
git checkout -b
或git switch -c
,您还可以使用git branch
,但 * 不带 *-c
选项:使用checkout或switch的主要原因是,它们会将您带到新的分支,这通常是您想要的。
(也许离题了,我很好奇:当您提到正式的方式时,您的意思是一直使用
git switch -c
吗?)kadbb4593#
假设我想创建一个全新的分支,它是一个现有分支的精确副本。
但你没有!
如果你先解决这个误解,一切都会变得清晰起来。
首先,你要做的并不叫“复制”,而是“分支”。从另一个分支(或任意提交)创建的分支只是指向该提交的指针。新分支开始时与旧分支有着完全相同的历史,这似乎是你想要的,但它们可能会独立地发散。
分支是:一个引用名,一个特定提交的引用,可选的一个上游,可能还有一些其他分支特定的配置。如果你不想复制所有的配置,那么复制一个分支不是你要做的。
为了更清楚起见手册上说:
其中,config 表示与
.git/config
中该分支相关的所有内容,例如:那么你到底想要什么呢?显然不是一个字面上的副本,包括上游的精确副本。
快跑