【问题标题】:go-git: Correct way to create a local branch, emulating behavior of "git branch <branchname>"?go-git:创建本地分支的正确方法,模拟“git branch <branchname>”的行为?
【发布时间】:2021-07-13 02:08:27
【问题描述】:

正如标题所暗示的,我试图弄清楚如何使用 go-git 创建本地分支,其结果与 Git CLI 命令 git branch &lt;branchname&gt; 相同。

据我所知,git branch &lt;branchname&gt;(没有明确的&lt;start-point&gt; 参数)做了两件事:

  1. 创建.git/refs/heads/&lt;branchname&gt; 以指向当前HEAD 提交
  2. 创建.git/logs/refs/heads/&lt;branchname&gt;,其中一行记录了分支的创建。

它可能会做得更多,但我知道这两件事是肯定的。 (如果您知道它的更多功能,请分享!)

接下来的大部分内容都记录了我在研究我的选择时的发现之旅,我想我现在可能已经掌握了上面的#1。不过,对于#2,我开始认为我可能是 SOL,至少使用 go-git

第一个想法:Repository.CreateBranch

我最初的幼稚想法是只打电话给Repository.CreateBranch,并且有一个answer 用于类似的SO 问题(“如何使用go-git 签出新的本地分支?”),这似乎可以证明这一点主意。但是,一旦我开始研究细节,事情就变得非常令人困惑。

首先,Repository.CreateBranchconfig.Config 作为输入(为什么?),并且似乎还修改了存储库的.git/config 文件(再次,为什么?)。我已经验证了 git branch &lt;branchname&gt; 命令不会触及 repo 的配置,当我调用该命令时我当然不需要提及任何有关配置的内容。

其次,我在上面链接的 SO 答案引用了 go-gitrepository_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" 不是参考规范(因为参考规范有一个: 将其srcdst 组件分开)。

经过大量的头疼和代码阅读后,我得出了一个(非常)初步的结论,即评论中的“refspec”这个词一定是错误的,而应该只是“ref”。但我对此完全不确定:如果我是对的,那为什么这个字段命名为 Merge 而不是 Ref

另一个初步结论是Repository.CreateBranch 并不是真正用于创建纯粹的本地分支,而是用于创建与远程分支有某种关系的本地分支——例如,如果我正在从远程拉别人的分支。

实际上,在重新阅读 Repository.CreateBranch 方法时,我完全不相信它真的创建了一个分支(也就是说,它创建了 .git/refs/heads/&lt;branchname&gt;) .除非我遗漏了某些东西(完全有可能),否则它似乎所做的只是在.git/config 中创建一个[branch "&lt;name&gt;"] 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 &lt;branchname&gt; 没有执行的新分支这一事实之外,它也无法创建.git/logs/refs/heads/&lt;branchname&gt;

另外——作为一个潜在的非常令人讨厌的惊喜——它会清除工作树中所有未跟踪的文件。默认情况下,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.&lt;name&gt;.merge 的信息。所以,是的,Merge 作为字段名称现在对我来说很有意义。但是为什么这个方法被命名为CreateBranch,为什么它是Repository类型的方法,而不是Config类型的方法,我想不通。

标签: git go go-git


【解决方案1】:

首先,我没有足够的声誉来评论 Pedro 的回答,但是他的方法在 Checkout 阶段失败了,因为实际上没有在存储上创建分支(从未调用 repo 的 Storer)。

其次,这是我第一次听说.git/log 目录,所以不,git branch 不会为该目录中的分支创建记录。

这将我引向实际的解决方案,它是作为分支示例提供的解决方案the go-git repo

  • 创建一个分支(离开 HEAD):
Info("git branch test")
branchName := plumbing.NewBranchReferenceName("test")
headRef, err := r.Head()
CheckIfError(err)
ref := plumbing.NewHashReference(branchName, headRef.Hash())
err = r.Storer.SetReference(ref)
CheckIfError(err)
  • 结帐分店
Info("git checkout test")
w, err := r.Worktree()
CheckIfError(err)
err = w.Checkout(&git.CheckoutOptions{Branch: ref.Name()})
CheckIfError(err)

然而,这样一来,.git/config 处的这个分支就没有配置,所以应该调用repo.Branch 函数,但这真的很不直观。

【讨论】:

【解决方案2】:

我是怎么做的:

创建对新分支的本地引用

branchName := "new-branch"
localRef := plumbing.NewBranchReferenceName(branchName)

创建分支

opts := &gitConfig.Branch{
    Name:   branchName,
    Remote: "origin",
    Merge:  localRef,
}

if err := repo.CreateBranch(opts); err != nil {
    return err
}

如果您确实需要更改到该分支...只需进行结帐(不记得它是否实际更改为使用创建创建的分支)

获取工作树

w, err := repo.Worktree()
if err != nil {
    return rest.InternalServerError(err.Error())
}

结帐

if err := w.Checkout(&git.CheckoutOptions{Branch: plumbing.ReferenceName(localRef.String())}); err != nil {
    return nil
}

如果你想跟踪远程分支

创建远程引用

remoteRef := plumbing.NewRemoteReferenceName("origin", branchName)

远程跟踪

newReference := plumbing.NewSymbolicReference(localRef, remoteRef)

if err := repo.Storer.SetReference(newReference); err != nil {
   return err
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-10-01
    • 1970-01-01
    • 2021-09-16
    • 1970-01-01
    • 2021-08-17
    • 2018-12-25
    • 2019-09-18
    • 2011-06-03
    相关资源
    最近更新 更多