如何在Go中使用`go-git`模块模拟`git –work-tree …`的功能?

huangapple go评论145阅读模式
英文:

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函数,希望能够提供一个工作树作为选项。然而,看着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
}

如何使用go-git模拟git --work-tree ...的功能?


编辑1:解释了".git"的实际命名不是".git"。

英文:

I have a bare repository in which I need to add and commit a set of files. As far as I understand it, adding files to the index requires a worktree. Using git on the command line, I would set the git-dir option to point to the bare directory along with setting the work-tree option to point to a worktree in which the files to be added to the index live. Like so:

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

It's worth mentioning that the ".git" directory is not, and can not, be named simply ".git". It is in fact a "custom" ".git" dir. Like git --git-dir /path/to/.notgit ....

I tried setting the core.worktree config option. However, with core.bare set to true this results in a fatal error. Both from the command line:

$ 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

and using 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)

One thought I had was to lean on the git.PlainOpenWithOptions function to hopefully allow me to provide a worktree as an option. However, looking at the git.PlainOpenOptions struct type, this fell apart quickly.

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
}

How do I mimic git --work-tree ... with go-git?


Edit 1: Explained that ".git" is not exactly named ".git".

答案1

得分: 1

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

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中使用`go-git`模块模拟`git –work-tree …`的功能?

在执行Go代码之后
如何在Go中使用`go-git`模块模拟`git –work-tree …`的功能?

英文:

When you use git.Open(), it basically sets worktree field in repository struct as nil, since it uses PlainOpenWithOptions internally with the default value of DetectDotGit as false. If you use the following constructor, you will see, the untracked files will be added successfully.

    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)

Before go code execution
如何在Go中使用`go-git`模块模拟`git –work-tree …`的功能?

After go code execution
如何在Go中使用`go-git`模块模拟`git –work-tree …`的功能?

答案2

得分: 1

我不是Git的专家,但我一直在使用go-git进行尝试,并且已经能够使用Git的底层命令创建一个裸仓库并向其中添加文件。虽然代码有点冗长,但一旦你掌握了要领,就会变得很简单。要注意的主要是Git有许多不同的对象类型用于执行其工作,我们只需要创建每个对象,这是下面代码的主要部分。

以下代码将在/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)

	// 创建一个新的仓库
	r, err := git.PlainInit(dir, true)
	panicIf(err)

	// 将其更改为使用"main"而不是"master"
	h := plumbing.NewSymbolicReference(plumbing.HEAD, "refs/heads/main")
	err = r.Storer.SetReference(h)
	panicIf(err)

	// 在存储中创建一个文件,它由其哈希标识
	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)

	// 创建并存储一个包含存储对象的树
	// 给它一个名字"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)

	// 接下来,创建一个引用树的提交
	// 提交只是关于树的元数据

	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)

	// 现在,将"main"分支指向新创建的提交

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

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

	// 告诉Git默认分支名称是"main"

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

运行此代码后,为了验证其是否正常工作,你可以使用命令行版本的git命令clone生成的裸仓库。假设当前目录是/tmp,操作如下:

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

这将在/tmp/example目录中创建一个工作树,你可以进入该目录:

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

你可以使用类似的技术向裸仓库添加新文件,无需工作目录。以下代码将向仓库添加一个名为"example.md"的文件。(注意,此代码是简单的;如果你运行两次,它将为同一文件创建两个条目,这通常是不应该这样做的;请参阅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"
}

// 向裸仓库添加或替换单个文件
// 这将创建一个包含文件的新提交
// 你可以更改文件或添加新文件
//
func main() {
	dir := getRepo()
	repo, err := git.PlainOpen(dir)
	if err != nil {
		panic(err)
	}

	// 获取对"main"分支头的引用
	mainRef, err := repo.Reference(plumbing.ReferenceName("refs/heads/main"), true)
	panicIf(err)

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

	// 获取提交中引用的树
	tree, err := repo.TreeObject(commit.TreeHash)
	panicIf(err)

	// 将文件复制到仓库中
	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)

	// 向树中添加一个新条目,并将其保存到存储中

	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)

	// 接下来,创建一个引用先前提交以及新树的提交

	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)

	// 现在,将"main"分支指向新创建的提交

	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中所做的操作。

英文:

I'm not an expert in Git, but I've been playing with go-git and I've been able to create a bare repository and add a file to it using the Git plumbing commands. It's a bit verbose, but actually straightforward once you get the gist of it. The main thing to realise is that Git has a number of different object types that it uses to perform its work, and we just need to create each of those objects, which is the bulk of the code below.

The following code will create a new, bare repository in /tmp/example.git, and add a file called "README.md" to it, without the need for any working directory. It does need to create an in-memory representation of the file that we want to store, but that representation is just a byte buffer, not a filesystem. (This code will also change the default branch name from "master" to "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)
}

Once you've run this code, to see that it's working, you can clone the resulting bar repo using the command line version of git. Assuming the current directory is /tmp, this is simple:

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

This will create a working tree in the /tmp/example directory, which you can cd to:

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

You can use a similar technique to add a new file to the bare repo, without the need for a working directory. The following code adds a file called "example.md" to the repo. (Note, this code is naive; if you run it twice, it will create two entries for the same file, which you shouldn't normally do; see the go-git docs for the API to look up a TreeEntry instead of adding one):

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)
}

To run this you will need to create a file called "example.md" in your working directory, maybe like this:

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

After running the add command, you can git pull in the working directory:

/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

and you can see that the file now exists:

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

The way this works is to manually manipulate the data structures used by Git itself. We store the file (blob), create a tree containing the file, and create a commit pointing to the tree. It should be similarly easy to update a file or to delete a file, but every operation that changes the head of a branch will need to create a copy of the tree and commit it, similar to how add has done here.

huangapple
  • 本文由 发表于 2021年11月14日 02:13:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/69956994.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定