繁体   English   中英

我如何在 go 中使用 `go-git` 模仿 `git --work-tree...`?

[英]How do I mimic `git --work-tree ...` with `go-git` in go?

我有一个裸存储库,我需要在其中添加和提交一组文件。 据我了解,将文件添加到索引需要一个worktree 在命令行上使用git ,我会将git-dir选项设置为指向裸目录,同时将work-tree选项设置为指向要添加到索引的文件所在的工作树。 像这样:

$ git --git-dir /path/to/.git --work-tree /path/to/worktree add ...

值得一提的是,“.git”目录不是也不可能简单地命名为“.git”。 它实际上是一个“自定义”“.git”目录。 git --git-dir /path/to/.notgit...

我尝试设置core.worktree配置选项。 但是,将core.bare设置为true会导致致命错误。 都来自命令行:

$ git --git-dir /path/to/.notgit config core.worktree /path/to/worktree
$ git --git-dir /path/to/.notgit add ...
warning: core.bare and core.worktree do not make sense
fatal: unable to set up work tree using invalid config

并使用go-git

r, err := git.PlainOpen("/path/to/.notgit")
panicOnError(err)

c, err := r.Config()
panicOnError(err)

fmt.Println(c.Core.IsBare) // true

c.Core.Worktree = "/path/to/worktree"

err = r.SetConfig(c)
panicOnError(err)

_, err = r.Worktree() // panic: worktree not available in a bare repository
panicOnError(err)

我的一个想法是依靠git.PlainOpenWithOptions function 希望允许我提供一个工作树作为一个选项。 然而,看看git.PlainOpenOptions结构类型,它很快就崩溃了。

type PlainOpenOptions struct {
    // DetectDotGit defines whether parent directories should be
    // walked until a .git directory or file is found.
    DetectDotGit bool
    // Enable .git/commondir support (see https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt).
    // NOTE: This option will only work with the filesystem storage.
    EnableDotGitCommonDir bool
}

我如何模仿git --work-tree...go-git


编辑 1:解释说“.git”并不完全命名为“.git”。

当您使用git.Open() ,它基本上将存储库结构中的worktree字段设置为nil ,因为它PlainOpenWithOptions内部使用PlainOpenWithOptions的默认值DetectDotGitfalse 如果使用以下构造函数,您将看到,未跟踪的文件将成功添加。

    r, err := git.PlainOpenWithOptions("/path/to/.git",&git.PlainOpenOptions{DetectDotGit: true})
    panicOnError(err)
    
    c, err := r.Config()
    panicOnError(err)
    
    fmt.Println(c.Core.IsBare) // true
    
    c.Core.Worktree = "/path/to/worktree"
    
    err = r.SetConfig(c)
    panicOnError(err)
    
    _, err = r.Worktree() // panic: worktree not available in a bare repository
    panicOnError(err)

// added this part for test
    workTree, werr := r.Worktree()
    panicOnError(werr)

    hash, hashErr := workTree.Add("a.txt")
    if hashErr != nil {
        log.Fatal(hashErr)
    }
fmt.Println(hash)

go代码执行前在此处输入图片说明

go代码执行后在此处输入图片说明

我不是 Git 方面的专家,但我一直在玩 go-git,我已经能够创建一个裸存储库并使用 Git 管道命令向其中添加一个文件。 它有点冗长,但一旦你理解了它的要点,实际上就很简单了。 要意识到的主要事情是 Git 有许多不同的 object 类型用于执行其工作,我们只需要创建这些对象中的每一个,这是下面代码的主体。

以下代码将在/tmp/example.git中创建一个新的裸存储库,并向其中添加一个名为“README.md”的文件,而无需任何工作目录。 它确实需要创建我们要存储的文件的内存表示,但该表示只是一个字节缓冲区,而不是文件系统。 (此代码还将默认分支名称从“master”更改为“main”):

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
    "github.com/go-git/go-git/v5/plumbing/filemode"
    "github.com/go-git/go-git/v5/plumbing/object"
    "os"
    "time"
)

func panicIf(err error) {
    if err != nil {
        panic(err)
    }
}

func getRepo() string {
    return "/tmp/example.git"
}

func main() {

    dir := getRepo()
    err := os.Mkdir(dir, 0700)
    panicIf(err)

    // Create a new repo
    r, err := git.PlainInit(dir, true)
    panicIf(err)

    // Change it to use "main" instead of "master"
    h := plumbing.NewSymbolicReference(plumbing.HEAD, "refs/heads/main")
    err = r.Storer.SetReference(h)
    panicIf(err)

    // Create a file in storage. It's identified by its hash.
    fileObject := plumbing.MemoryObject{}
    fileObject.SetType(plumbing.BlobObject)
    w, err := fileObject.Writer()
    panicIf(err)

    _, err = w.Write([]byte("# My Story\n"))
    panicIf(err)

    err = w.Close()
    panicIf(err)

    fileHash, err := r.Storer.SetEncodedObject(&fileObject)
    panicIf(err)

    // Create and store a Tree that contains the stored object.
    // Give it the name "README.md".

    treeEntry := object.TreeEntry{
        Name: "README.md",
        Mode: filemode.Regular,
        Hash: fileHash,
    }

    tree := object.Tree{
        Entries: []object.TreeEntry{treeEntry},
    }

    treeObject := plumbing.MemoryObject{}
    err = tree.Encode(&treeObject)
    panicIf(err)

    treeHash, err := r.Storer.SetEncodedObject(&treeObject)
    panicIf(err)

    // Next, create a commit that references the tree
    // A commit is just metadata about a tree.

    commit := object.Commit{
        Author:    object.Signature{"Bob", "bob@example.com", time.Now()},
        Committer: object.Signature{"Bob", "bob@example.com", time.Now()},
        Message:   "first commit",
        TreeHash:  treeHash,
    }

    commitObject := plumbing.MemoryObject{}
    err = commit.Encode(&commitObject)
    panicIf(err)

    commitHash, err := r.Storer.SetEncodedObject(&commitObject)
    panicIf(err)

    // Now, point the "main" branch to the newly-created commit

    ref := plumbing.NewHashReference("refs/heads/main", commitHash)
    err = r.Storer.SetReference(ref)

    cfg, err := r.Config()
    panicIf(err)

    // Tell Git that the default branch name is "main".

    cfg.Init.DefaultBranch = "main"
    err = r.SetConfig(cfg)
    panicIf(err)
}

运行此代码后,要查看它是否正常工作,您可以使用git的命令行版本clone生成的 bar 存储库。 假设当前目录是/tmp ,这很简单:

/tmp $ git clone example.git
Cloning into 'example'...
done.

这将在/tmp/example目录中创建一个工作树,您可以将其 cd 到:

/tmp $ cd example
/tmp/example $ ls
README.md

您可以使用类似的技术将新文件添加到裸存储库,而无需工作目录。 以下代码将名为“example.md”的文件添加到存储库。 请注意,此代码是幼稚的;如果您运行它两次,它将为同一个文件创建两个条目,您通常不应该这样做;请参阅 API 的 go-git 文档以查找 TreeEntry 而不是添加一个):

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
    "github.com/go-git/go-git/v5/plumbing/filemode"
    "github.com/go-git/go-git/v5/plumbing/object"
    "io"
    "os"
    "time"
)

func panicIf(err error) {
    if err != nil {
        panic(err)
    }
}

func getRepo() string {
    return "/tmp/example.git"
}

// Add or replace a single file in a bare repository.
// This creates a new commit, containing the file.
// You can change the file or add a new file.
//
func main() {
    dir := getRepo()
    repo, err := git.PlainOpen(dir)
    if err != nil {
        panic(err)
    }

    // Get a reference to head of the "main" branch.
    mainRef, err := repo.Reference(plumbing.ReferenceName("refs/heads/main"), true)
    panicIf(err)

    commit, err := repo.CommitObject(mainRef.Hash())
    panicIf(err)

    // Get the tree referred to in the commit.
    tree, err := repo.TreeObject(commit.TreeHash)
    panicIf(err)

    // Copy the file into the repository
    fileObject := plumbing.MemoryObject{}
    fileObject.SetType(plumbing.BlobObject)
    w, err := fileObject.Writer()
    panicIf(err)

    file, err := os.Open("example.md")
    panicIf(err)

    _, err = io.Copy(w, file)
    panicIf(err)

    err = w.Close()
    panicIf(err)

    fileHash, err := repo.Storer.SetEncodedObject(&fileObject)
    panicIf(err)

    // Add a new entry to the tree, and save it into storage.

    newTreeEntry := object.TreeEntry{
        Name: "example.md",
        Mode: filemode.Regular,
        Hash: fileHash,
    }

    tree.Entries = append(tree.Entries, newTreeEntry)

    treeObject := plumbing.MemoryObject{}
    err = tree.Encode(&treeObject)
    panicIf(err)

    treeHash, err := repo.Storer.SetEncodedObject(&treeObject)
    panicIf(err)

    // Next, create a commit that references the previous commit, as well as the new tree

    newCommit := object.Commit{
        Author:       object.Signature{"Alice", "alice@example.com", time.Now()},
        Committer:    object.Signature{"Alice", "alice@example.com", time.Now()},
        Message:      "second commit",
        TreeHash:     treeHash,
        ParentHashes: []plumbing.Hash{commit.Hash},
    }

    commitObject := plumbing.MemoryObject{}
    err = newCommit.Encode(&commitObject)
    panicIf(err)

    commitHash, err := repo.Storer.SetEncodedObject(&commitObject)
    panicIf(err)

    // Now, point the "main" branch to the newly-created commit

    ref := plumbing.NewHashReference("refs/heads/main", commitHash)
    err = repo.Storer.SetReference(ref)
    panicIf(err)
}

要运行它,您需要在您的工作目录中创建一个名为“example.md”的文件,可能如下所示:

$ echo "# An example file" > example.md
$ go build
$ ./add

运行add命令后,可以git pull入工作目录:

/tmp/example $ git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 266 bytes | 266.00 KiB/s, done.
From /tmp/example
   6f234cc..c248a9d  main       -> origin/main
Updating 6f234cc..c248a9d
Fast-forward
 example.md | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 example.md

您可以看到该文件现在存在:

/tmp/example $ ls
README.md   example.md
/tmp/example $ cat example.md
# An example file
/tmp/example $

它的工作方式是手动操作 Git 本身使用的数据结构。 我们存储文件 (blob),创建包含该文件的树,并创建指向树的提交。 更新文件或删除文件应该同样容易,但是每个更改分支头的操作都需要创建树的副本并提交它,类似于此处add的方式。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM