【发布时间】:2021-07-13 02:08:27
【问题描述】:
正如标题所暗示的,我试图弄清楚如何使用 go-git 创建本地分支,其结果与 Git CLI 命令 git branch <branchname> 相同。
据我所知,git branch <branchname>(没有明确的<start-point> 参数)做了两件事:
- 创建
.git/refs/heads/<branchname>以指向当前HEAD提交 - 创建
.git/logs/refs/heads/<branchname>,其中一行记录了分支的创建。
它可能会做得更多,但我知道这两件事是肯定的。 (如果您知道它的更多功能,请分享!)
接下来的大部分内容都记录了我在研究我的选择时的发现之旅,我想我现在可能已经掌握了上面的#1。不过,对于#2,我开始认为我可能是 SOL,至少使用 go-git。
第一个想法:Repository.CreateBranch
我最初的幼稚想法是只打电话给Repository.CreateBranch,并且有一个answer 用于类似的SO 问题(“如何使用go-git 签出新的本地分支?”),这似乎可以证明这一点主意。但是,一旦我开始研究细节,事情就变得非常令人困惑。
首先,Repository.CreateBranch 将config.Config 作为输入(为什么?),并且似乎还修改了存储库的.git/config 文件(再次,为什么?)。我已经验证了 git branch <branchname> 命令不会触及 repo 的配置,当我调用该命令时我当然不需要提及任何有关配置的内容。
其次,我在上面链接的 SO 答案引用了 go-git 的 repository_test.go 中的代码,该代码执行以下操作:
r, _ := Init(memory.NewStorage(), nil) // init repo
testBranch := &config.Branch{
Name: "foo",
Remote: "origin",
Merge: "refs/heads/foo",
}
err := r.CreateBranch(testBranch)
但是config.Branch的定义是:
type Branch struct {
// Name of branch
Name string
// Remote name of remote to track
Remote string
// Merge is the local refspec for the branch <=== ???
Merge plumbing.ReferenceName
...
}
和"refs/heads/foo" 不是参考规范(因为参考规范有一个: 将其src 和dst 组件分开)。
经过大量的头疼和代码阅读后,我得出了一个(非常)初步的结论,即评论中的“refspec”这个词一定是错误的,而应该只是“ref”。但我对此完全不确定:如果我是对的,那为什么这个字段命名为 Merge 而不是 Ref?
另一个初步结论是Repository.CreateBranch 并不是真正用于创建纯粹的本地分支,而是用于创建与远程分支有某种关系的本地分支——例如,如果我正在从远程拉别人的分支。
实际上,在重新阅读 Repository.CreateBranch 方法时,我完全不相信它真的创建了一个分支(也就是说,它创建了 .git/refs/heads/<branchname>) .除非我遗漏了某些东西(完全有可能),否则它似乎所做的只是在.git/config 中创建一个[branch "<name>"] section。但如果这是真的,为什么它是Repository 的方法呢? 为什么它不是config.Config 的方法?
同样,还有一个相关的函数:
func (r *Repository) Branch(name string) (*config.Branch, error)
这只会从配置中返回分支信息。然而,Repository 文档中的下一个函数是:
func (r *Repository) Branches() (storer.ReferenceIter, error)
它确实返回了一个迭代器,遍历.git/refs/heads/ 中的所有条目。
这非常令人困惑,并且文档(例如)无济于事。在任何情况下,除非有人能说服我,否则我很确定 CreateBranch 在实际创建分支方面不会有太大帮助。
Worktree.Checkout ???
一些额外的网络搜索从旧的d-src/go-git repo 中发现了这两个问题:
这两篇文章都提出了创建本地分支的基本方法:
wt, err := repo.Worktree()
if err != nil {
// deal with it
}
err = w.Checkout(&git.CheckoutOptions{
Create: true,
Force: false,
Branch: plumbing.ReferenceName("refs/heads/<branchname>"),
})
除了这个签出git branch <branchname> 没有执行的新分支这一事实之外,它也无法创建.git/logs/refs/heads/<branchname>。
另外——作为一个潜在的非常令人讨厌的惊喜——它会清除工作树中所有未跟踪的文件。默认情况下,git checkout保留对工作树中文件的本地修改,但在 go-git 中,您需要显式指定 Keep: true,即使您已指定 Force: false。 p>
绝对违反了“最小惊讶原则”。值得庆幸的是,在我测试过的本地仓库中,它们都是旧的编辑器备份文件或我早就放弃的旧项目的片段。
storer.ReferenceStorer
碰巧的是,go-git 作者/维护者之一回应了第二个问题,并建议:
为了创建和删除独立于工作树的引用,您应该使用
storer.ReferenceStorer来执行此操作。请看分支示例:https://github.com/src-d/go-git/blob/master/_examples/branch/main.go
这很好,很简单,但它只解决了分支 ref 的创建问题。
我在go-git 源代码中找到的所有“日志”一词似乎都指的是commit 日志,而不是ref 日志。鉴于 reflog 条目看起来不像 .git 树中的其他工件,我想创建/更新它们需要一种不同类型的存储器 - 并且现有存储器看起来都不像(对我来说) 他们这样做。
所以...
关于我应该如何获得正确的 reflog 以配合 ref 的任何建议?
(或者,也许我严重误解了, 有一些方法可以在 go-git 中创建分支,除了我上面列出的那些之外,它可以满足我的需求。)
【问题讨论】:
-
没有查看 go-git 方面的内容来验证任何这一点(因为这对我来说似乎很清楚 :-)):配置参数只是“如何设置分支的上游”。在 Git 中,分支的上游设置由两部分组成:remote 和“合并”。遥控器只是一个字符串:任何现有的遥控器名称都可以,表示特定的遥控器,“。”意思是“这个存储库”。 “合并”部分是奇怪的地方:它是分支的 ref(不是 refspec,你是对的)在远程上看到。
-
这意味着如果“remote”是一个文字“.”字符串,它是分支名称,拼写出来。如果远程是
origin而不是,它是分支名称如在 Git 上看到的origin。如果您有一个映射+refs/heads/*:refs/remotes/origin/*的 refspec,那么它们的main就是您的origin/main,这就是您要传递给git merge的内容,但 merge 行显示的是refs/heads/main,而不是refs/remotes/origin/main。如果您的 refspec 显示+refs/heads/main:refs/remotes/origin/FooledYa,您仍然在此处列出refs/heads/main。 -
Git 在基于
core.logAllRefUpdates设置创建分支时会创建一个reflog,所以go-git 可能也应该遵守这个设置。 -
@torek Aha,您的评论让我查看了
git config的手册页,在那里我找到了有关branch.<name>.merge的信息。所以,是的,Merge作为字段名称现在对我来说很有意义。但是为什么这个方法被命名为CreateBranch,为什么它是Repository类型的方法,而不是Config类型的方法,我想不通。