Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some data
16 changes: 16 additions & 0 deletions examples/plugins/git-with-revision/devbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"packages": [],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null"
],
"scripts": {
"run_test": [
"./test.sh"
]
}
},
"include": [
"git+ssh://git@github.com/jetify-com/devbox-plugin-example.git?rev=d9c00334353c9b1294c7bd5dbea128c149b2eb3a"
]
}
4 changes: 4 additions & 0 deletions examples/plugins/git-with-revision/devbox.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"lockfile_version": "1",
"packages": {}
}
9 changes: 9 additions & 0 deletions examples/plugins/git-with-revision/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

expected="I AM SET"
if [ "$MY_ENV_VAR" == "$expected" ]; then
echo "Success! MY_ENV_VAR is set to '$MY_ENV_VAR'"
else
echo "MY_ENV_VAR environment variable is not set to '$expected'"
exit 1
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some data
18 changes: 18 additions & 0 deletions examples/plugins/git/devbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"packages": [],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null"
],
"scripts": {
"run_test": [
"./test.sh"
]
}
},
"include": [
"git+ssh://git@github.com/jetify-com/devbox-plugin-example.git",
"git+https://github.com/jetify-com/devbox-plugin-example.git?dir=custom-dir",
"git+https://github.com/jetify-com/devbox-plugin-example.git?ref=test/branch"
]
}
4 changes: 4 additions & 0 deletions examples/plugins/git/devbox.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"lockfile_version": "1",
"packages": {}
}
14 changes: 14 additions & 0 deletions examples/plugins/git/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

expected="I AM SET (new value)"
custom_expected="I AM SET TO CUSTOM (new value)"
if [ "$MY_ENV_VAR" == "$expected" ] && [ "$MY_ENV_VAR_CUSTOM" == "$custom_expected" ]; then
echo "Success! MY_ENV_VAR is set to '$MY_ENV_VAR'"
echo "Success! MY_ENV_VAR_CUSTOM is set to '$MY_ENV_VAR_CUSTOM'"
else
echo "ERROR: MY_ENV_VAR environment variable is not set to '$expected' OR MY_ENV_VAR_CUSTOM variable is not set to '$custom_expected'"
exit 1
fi

echo BRANCH_ENV_VAR=$BRANCH_ENV_VAR
if [ "$BRANCH_ENV_VAR" != "I AM A BRANCH VAR" ]; then exit 1; fi;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some data
17 changes: 17 additions & 0 deletions examples/plugins/v2-git/devbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"packages": [],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null"
],
"scripts": {
"run_test": [
"./test.sh"
]
}
},
"include": [
"git+ssh://git@github.com/jetify-com/devbox-plugin-example.git",
"git+https://github.com/jetify-com/devbox-plugin-example?dir=custom-dir"
]
}
4 changes: 4 additions & 0 deletions examples/plugins/v2-git/devbox.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"lockfile_version": "1",
"packages": {}
}
11 changes: 11 additions & 0 deletions examples/plugins/v2-git/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

expected="I AM SET (new value)"
custom_expected="I AM SET TO CUSTOM (new value)"
if [ "$MY_ENV_VAR" == "$expected" ] && [ "$MY_ENV_VAR_CUSTOM" == "$custom_expected" ]; then
echo "Success! MY_ENV_VAR is set to '$MY_ENV_VAR'"
echo "Success! MY_ENV_VAR_CUSTOM is set to '$MY_ENV_VAR_CUSTOM'"
else
echo "ERROR: MY_ENV_VAR environment variable is not set to '$expected' OR MY_ENV_VAR_CUSTOM variable is not set to '$custom_expected'"
exit 1
fi
6 changes: 6 additions & 0 deletions internal/plugin/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ func getConfigIfAny(inc Includable, projectDir string) (*Config, error) {
return nil, errors.WithStack(err)
}
return buildConfig(includable, projectDir, string(content))
case *gitPlugin:
content, err := includable.Fetch()
if err != nil {
return nil, errors.WithStack(err)
}
return buildConfig(includable, projectDir, string(content))
case *LocalPlugin:
content, err := os.ReadFile(includable.Path())
if err != nil && !os.IsNotExist(err) {
Expand Down
165 changes: 165 additions & 0 deletions internal/plugin/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2024 Jetify Inc. and contributors. All rights reserved.
// Use of this source code is governed by the license in the LICENSE file.

package plugin

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"go.jetify.com/devbox/nix/flake"
)

type gitPlugin struct {
ref *flake.Ref
name string
}

// newGitPlugin creates a Git plugin from a flake reference.
// It uses git clone to fetch the repository.
func newGitPlugin(ref flake.Ref) (*gitPlugin, error) {
if ref.Type != flake.TypeGit {
return nil, fmt.Errorf("expected git flake reference, got %s", ref.Type)
}

name := generateGitPluginName(ref)

return &gitPlugin{
ref: &ref,
name: name,
}, nil
}

func generateGitPluginName(ref flake.Ref) string {
// Extract repository name from URL and append directory if specified
url := ref.URL
if url == "" {
return "unknown.git"
}

// Remove query parameters to get clean URL
if strings.Contains(url, "?") {
url = strings.Split(url, "?")[0]
}

url = strings.TrimSuffix(url, ".git")

parts := strings.Split(url, "/")
if len(parts) < 2 {
return "unknown.git"
}

// Use last two path components (e.g., "owner/repo")
repoParts := parts[len(parts)-2:]

name := strings.Join(repoParts, ".")
name = strings.ReplaceAll(name, "/", ".")

// Append directory to make name unique when multiple plugins
// from same repo are used
if ref.Dir != "" {
dirName := strings.ReplaceAll(ref.Dir, "/", ".")
name = name + "." + dirName
}

return name
}

// getBaseURL extracts the base Git URL without query parameters.
// Query parameters like ?dir=path are used by Nix flakes but not by git clone.
func (p *gitPlugin) getBaseURL() string {
baseURL := p.ref.URL
if strings.Contains(baseURL, "?") {
baseURL = strings.Split(baseURL, "?")[0]
}
return baseURL
}

func (p *gitPlugin) Fetch() ([]byte, error) {
content, err := p.FileContent("plugin.json")
if err != nil {
return nil, err
}
return content, nil
}

func (p *gitPlugin) cloneAndRead(subpath string) ([]byte, error) {
tempDir, err := os.MkdirTemp("", "devbox-git-plugin-*")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)

// Clone repository using base URL without query parameters
baseURL := p.getBaseURL()

cloneCmd := exec.Command("git", "clone", "--depth", "1", baseURL, tempDir)
if p.ref.Rev != "" {
cloneCmd = exec.Command("git", "clone", "--depth", "1", "--branch", p.ref.Rev, baseURL, tempDir)
}

output, err := cloneCmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("failed to clone repository %s: %w\nOutput: %s", p.ref.URL, err, string(output))
}

// Checkout specific commit if revision is a commit hash
if p.ref.Rev != "" && !isBranchName(p.ref.Rev) {
checkoutCmd := exec.Command("git", "checkout", p.ref.Rev)
checkoutCmd.Dir = tempDir
output, err := checkoutCmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("failed to checkout revision %s: %w\nOutput: %s", p.ref.Rev, err, string(output))
}
}

// Read file from repository root or specified directory
filePath := filepath.Join(tempDir, subpath)
if p.ref.Dir != "" {
filePath = filepath.Join(tempDir, p.ref.Dir, subpath)
}

content, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
}

return content, nil
}

func isBranchName(ref string) bool {
// Full commit hashes are 40 hex characters
if len(ref) == 40 {
for _, c := range ref {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return true
}
}
return false
}
return true
}

func (p *gitPlugin) CanonicalName() string {
return p.name
}

// Hash returns a unique hash for this plugin including directory.
// This ensures plugins from the same repo with different dirs are unique.
func (p *gitPlugin) Hash() string {
if p.ref.Dir != "" {
return fmt.Sprintf("%s-%s-%s", p.ref.URL, p.ref.Rev, p.ref.Dir)
}
return fmt.Sprintf("%s-%s", p.ref.URL, p.ref.Rev)
}

func (p *gitPlugin) FileContent(subpath string) ([]byte, error) {
return p.cloneAndRead(subpath)
}

func (p *gitPlugin) LockfileKey() string {
return p.ref.String()
}
Loading