Skip to content

Commit 685f5ce

Browse files
committed
introduce git plugin
1 parent 3a1050a commit 685f5ce

File tree

4 files changed

+516
-0
lines changed

4 files changed

+516
-0
lines changed

internal/plugin/files.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ func getConfigIfAny(inc Includable, projectDir string) (*Config, error) {
2424
return nil, errors.WithStack(err)
2525
}
2626
return buildConfig(includable, projectDir, string(content))
27+
case *gitPlugin:
28+
content, err := includable.Fetch()
29+
if err != nil {
30+
return nil, errors.WithStack(err)
31+
}
32+
return buildConfig(includable, projectDir, string(content))
2733
case *LocalPlugin:
2834
content, err := os.ReadFile(includable.Path())
2935
if err != nil && !os.IsNotExist(err) {

internal/plugin/git.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright 2024 Jetify Inc. and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package plugin
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strings"
12+
13+
"go.jetify.com/devbox/nix/flake"
14+
)
15+
16+
type gitPlugin struct {
17+
ref *flake.Ref
18+
name string
19+
}
20+
21+
// newGitPlugin creates a Git plugin from a flake reference.
22+
// It uses git clone to fetch the repository.
23+
func newGitPlugin(ref flake.Ref) (*gitPlugin, error) {
24+
if ref.Type != flake.TypeGit {
25+
return nil, fmt.Errorf("expected git flake reference, got %s", ref.Type)
26+
}
27+
28+
name := generateGitPluginName(ref)
29+
30+
return &gitPlugin{
31+
ref: &ref,
32+
name: name,
33+
}, nil
34+
}
35+
36+
func generateGitPluginName(ref flake.Ref) string {
37+
// Extract repository name from URL and append directory if specified
38+
url := ref.URL
39+
if url == "" {
40+
return "unknown.git"
41+
}
42+
43+
// Remove query parameters to get clean URL
44+
if strings.Contains(url, "?") {
45+
url = strings.Split(url, "?")[0]
46+
}
47+
48+
url = strings.TrimSuffix(url, ".git")
49+
50+
parts := strings.Split(url, "/")
51+
if len(parts) < 2 {
52+
return "unknown.git"
53+
}
54+
55+
// Use last two path components (e.g., "owner/repo")
56+
repoParts := parts[len(parts)-2:]
57+
58+
name := strings.Join(repoParts, ".")
59+
name = strings.ReplaceAll(name, "/", ".")
60+
61+
// Append directory to make name unique when multiple plugins
62+
// from same repo are used
63+
if ref.Dir != "" {
64+
dirName := strings.ReplaceAll(ref.Dir, "/", ".")
65+
name = name + "." + dirName
66+
}
67+
68+
return name
69+
}
70+
71+
// getBaseURL extracts the base Git URL without query parameters.
72+
// Query parameters like ?dir=path are used by Nix flakes but not by git clone.
73+
func (p *gitPlugin) getBaseURL() string {
74+
baseURL := p.ref.URL
75+
if strings.Contains(baseURL, "?") {
76+
baseURL = strings.Split(baseURL, "?")[0]
77+
}
78+
return baseURL
79+
}
80+
81+
func (p *gitPlugin) Fetch() ([]byte, error) {
82+
content, err := p.FileContent("plugin.json")
83+
if err != nil {
84+
return nil, err
85+
}
86+
return content, nil
87+
}
88+
89+
func (p *gitPlugin) cloneAndRead(subpath string) ([]byte, error) {
90+
tempDir, err := os.MkdirTemp("", "devbox-git-plugin-*")
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to create temp directory: %w", err)
93+
}
94+
defer os.RemoveAll(tempDir)
95+
96+
// Clone repository using base URL without query parameters
97+
baseURL := p.getBaseURL()
98+
99+
cloneCmd := exec.Command("git", "clone", "--depth", "1", baseURL, tempDir)
100+
if p.ref.Rev != "" {
101+
cloneCmd = exec.Command("git", "clone", "--depth", "1", "--branch", p.ref.Rev, baseURL, tempDir)
102+
}
103+
104+
output, err := cloneCmd.CombinedOutput()
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to clone repository %s: %w\nOutput: %s", p.ref.URL, err, string(output))
107+
}
108+
109+
// Checkout specific commit if revision is a commit hash
110+
if p.ref.Rev != "" && !isBranchName(p.ref.Rev) {
111+
checkoutCmd := exec.Command("git", "checkout", p.ref.Rev)
112+
checkoutCmd.Dir = tempDir
113+
output, err := checkoutCmd.CombinedOutput()
114+
if err != nil {
115+
return nil, fmt.Errorf("failed to checkout revision %s: %w\nOutput: %s", p.ref.Rev, err, string(output))
116+
}
117+
}
118+
119+
// Read file from repository root or specified directory
120+
filePath := filepath.Join(tempDir, subpath)
121+
if p.ref.Dir != "" {
122+
filePath = filepath.Join(tempDir, p.ref.Dir, subpath)
123+
}
124+
125+
content, err := os.ReadFile(filePath)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
128+
}
129+
130+
return content, nil
131+
}
132+
133+
func isBranchName(ref string) bool {
134+
// Full commit hashes are 40 hex characters
135+
if len(ref) == 40 {
136+
for _, c := range ref {
137+
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
138+
return true
139+
}
140+
}
141+
return false
142+
}
143+
return true
144+
}
145+
146+
func (p *gitPlugin) CanonicalName() string {
147+
return p.name
148+
}
149+
150+
// Hash returns a unique hash for this plugin including directory.
151+
// This ensures plugins from the same repo with different dirs are unique.
152+
func (p *gitPlugin) Hash() string {
153+
if p.ref.Dir != "" {
154+
return fmt.Sprintf("%s-%s-%s", p.ref.URL, p.ref.Rev, p.ref.Dir)
155+
}
156+
return fmt.Sprintf("%s-%s", p.ref.URL, p.ref.Rev)
157+
}
158+
159+
func (p *gitPlugin) FileContent(subpath string) ([]byte, error) {
160+
return p.cloneAndRead(subpath)
161+
}
162+
163+
func (p *gitPlugin) LockfileKey() string {
164+
return p.ref.String()
165+
}

0 commit comments

Comments
 (0)