feat(remote): support include git remote (#1652)

This commit is contained in:
Valentin Maerten
2024-09-24 13:44:54 -04:00
committed by GitHub
parent d1dc271b9a
commit e6ea0647d7
9 changed files with 407 additions and 13 deletions

View File

@@ -7,6 +7,8 @@ import (
"strings"
"time"
giturls "github.com/whilp/git-urls"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/logger"
@@ -48,24 +50,39 @@ func NewNode(
) (Node, error) {
var node Node
var err error
switch getScheme(entrypoint) {
scheme, err := getScheme(entrypoint)
if err != nil {
return nil, err
}
switch scheme {
case "git":
node, err = NewGitNode(entrypoint, dir, insecure, opts...)
case "http", "https":
node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...)
default:
// If no other scheme matches, we assume it's a file
node, err = NewFileNode(l, entrypoint, dir, opts...)
}
if node.Remote() && !experiments.RemoteTaskfiles.Enabled {
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")
}
return node, err
}
func getScheme(uri string) string {
if i := strings.Index(uri, "://"); i != -1 {
return uri[:i]
func getScheme(uri string) (string, error) {
u, err := giturls.Parse(uri)
if u == nil {
return "", err
}
return ""
if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") {
return "git", nil
}
if i := strings.Index(uri, "://"); i != -1 {
return uri[:i], nil
}
return "", nil
}
func getDefaultDir(entrypoint, dir string) string {

View File

@@ -81,6 +81,9 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
if strings.Contains(entrypoint, "://") {
return entrypoint, nil
}
if strings.HasPrefix(entrypoint, "git") {
return entrypoint, nil
}
path, err := execext.Expand(entrypoint)
if err != nil {

126
taskfile/node_git.go Normal file
View File

@@ -0,0 +1,126 @@
package taskfile
import (
"context"
"fmt"
"io"
"net/url"
"path/filepath"
"strings"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
giturls "github.com/whilp/git-urls"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
)
// An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct {
*BaseNode
URL *url.URL
rawUrl string
ref string
path string
}
func NewGitNode(
entrypoint string,
dir string,
insecure bool,
opts ...NodeOption,
) (*GitNode, error) {
base := NewBaseNode(dir, opts...)
u, err := giturls.Parse(entrypoint)
if err != nil {
return nil, err
}
basePath, path := func() (string, string) {
x := strings.Split(u.Path, "//")
return x[0], x[1]
}()
ref := u.Query().Get("ref")
rawUrl := u.String()
u.RawQuery = ""
u.Path = basePath
if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
}
return &GitNode{
BaseNode: base,
URL: u,
rawUrl: rawUrl,
ref: ref,
path: path,
}, nil
}
func (node *GitNode) Location() string {
return node.rawUrl
}
func (node *GitNode) Remote() bool {
return true
}
func (node *GitNode) Read(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.URL.String(),
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
})
if err != nil {
return nil, err
}
file, err := fs.Open(node.path)
if err != nil {
return nil, err
}
// Read the entire response body
b, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return b, nil
}
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
}
return resolvedEntrypoint, nil
}
func (node *GitNode) ResolveDir(dir string) (string, error) {
path, err := execext.Expand(dir)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil
}
func (node *GitNode) FilenameAndLastDir() (string, string) {
return filepath.Base(node.path), filepath.Base(filepath.Dir(node.path))
}

75
taskfile/node_git_test.go Normal file
View File

@@ -0,0 +1,75 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGitNode_ssh(t *testing.T) {
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
}
func TestGitNode_sshWithDir(t *testing.T) {
node, err := NewGitNode("git@github.com:foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
}
func TestGitNode_https(t *testing.T) {
node, err := NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint)
}
func TestGitNode_httpsWithDir(t *testing.T) {
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
}
func TestGitNode_FilenameAndDir(t *testing.T) {
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir := node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, ".", dir)
node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
}

View File

@@ -74,7 +74,6 @@ func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.TaskfileFetchFailedError{
URI: node.URL.String(),

22
taskfile/node_test.go Normal file
View File

@@ -0,0 +1,22 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestScheme(t *testing.T) {
scheme, err := getScheme("https://github.com/foo/bar.git")
assert.NoError(t, err)
assert.Equal(t, "git", scheme)
scheme, err = getScheme("https://github.com/foo/bar.git?ref=v1//taskfile/common.yml")
assert.NoError(t, err)
assert.Equal(t, "git", scheme)
scheme, err = getScheme("git@github.com:foo/bar.git?ref=main//Taskfile.yml")
assert.NoError(t, err)
assert.Equal(t, "git", scheme)
scheme, err = getScheme("https://github.com/foo/common.yml")
assert.NoError(t, err)
assert.Equal(t, "https", scheme)
}