Skip to content
Merged
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
3 changes: 3 additions & 0 deletions config/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Thumbs.db
# Release outputs
temp_external/

# Internal upgrade directory
temp_internal/

# DevKit context configurations (only devnet.yaml is indexed)
config/contexts/**/*
!config/contexts/devnet.yaml
Expand Down
50 changes: 42 additions & 8 deletions pkg/commands/template/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,52 @@ func createUpgradeCommand(
return fmt.Errorf("failed to get current working directory: %w", err)
}

// Create temporary directory for cloning the template
tempDir, err := os.MkdirTemp("", "devkit-template-upgrade-*")
// Create a gitClient for upgrade process
gitClient := template.NewGitClient()

// Check if working tree is clean before upgrading
clean, err := gitClient.GitIsClean()
if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
return fmt.Errorf("failed to check .git working tree state: %w", err)
}
if !clean {
return fmt.Errorf("uncommitted changes found, please commit or stash them before upgrading")
}

// Ensure .gitignore entry is present for tempInternal
tempInternal := "temp_internal"
if err := gitClient.EnsureGitignoreEntry(".gitignore", fmt.Sprintf("%s/", tempInternal)); err != nil {
return fmt.Errorf("failed to add entry to .gitignore %s: %w", tempInternal, err)
}

// Ensure parent exists
tempParent := filepath.Join(absProjectPath, tempInternal)
if err := os.MkdirAll(tempParent, 0o755); err != nil {
return fmt.Errorf("failed to create %s: %w", tempParent, err)
}
defer os.RemoveAll(tempDir) // Clean up on exit

tempCacheDir, err := os.MkdirTemp("", "devkit-template-cache-*")
// Create run-specific temp dir inside tempParent
tempDir, err := os.MkdirTemp(tempParent, ".tmp-devkit-template-upgrade-*")
if err != nil {
return fmt.Errorf("failed to create temporary cache directory: %w", err)
return fmt.Errorf("failed to create temporary directory: %w", err)
}
defer os.RemoveAll(tempCacheDir) // Clean up on exit

// Remove tempParent if it is empty after tempDir cleanup
defer func() {
err = os.RemoveAll(tempDir)
if err != nil {
logger.Warn("failed to remove %s: %v\n", tempDir, err)
}
entries, err := os.ReadDir(tempParent)
if err != nil {
logger.Warn("could not read %s: %v\n", tempParent, err)
}
if len(entries) == 0 {
if err := os.Remove(tempParent); err != nil {
logger.Warn("failed to remove %s: %v\n", tempParent, err)
}
}
}()

logger.Info("Upgrading project template:")
logger.Info(" Project: %s", projectName)
Expand All @@ -171,7 +205,7 @@ func createUpgradeCommand(

// Fetch main template
fetcher := &template.GitFetcher{
Client: template.NewGitClient(),
Client: gitClient,
Logger: *progresslogger.NewProgressLogger(
logger,
tracker,
Expand Down
25 changes: 25 additions & 0 deletions pkg/commands/template/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func TestUpgradeCommand(t *testing.T) {
t.Fatalf("Failed to write config file: %v", err)
}

// Create git repo in temp dir
if err := testutils.TestGitInit(testProjectsDir); err != nil {
t.Fatalf("Failed init git repo: %v", err)
}

// Create mock template info getter
mockTemplateInfoGetter := &MockTemplateInfoGetter{
projectName: "template-upgrade-test",
Expand Down Expand Up @@ -137,6 +142,11 @@ func TestUpgradeCommand(t *testing.T) {

// Test upgrade command with version flag
t.Run("Upgrade command with version", func(t *testing.T) {
// Discard changes between runs
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
t.Fatalf("Failed to clean working tree: %v", err)
}

// Create a flag set and context with no-op logger
set := flag.NewFlagSet("test", 0)
set.String("version", "v0.0.4", "")
Expand Down Expand Up @@ -186,6 +196,11 @@ func TestUpgradeCommand(t *testing.T) {

// Test upgrade command without version flag
t.Run("Upgrade command without version", func(t *testing.T) {
// Discard changes between runs
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
t.Fatalf("Failed to clean working tree: %v", err)
}

// Create a flag set and context without version flag, with no-op logger
set := flag.NewFlagSet("test", 0)

Expand Down Expand Up @@ -233,6 +248,11 @@ func TestUpgradeCommand(t *testing.T) {

// Test upgrade command with incompatible to devkit version
t.Run("Upgrade command with incompatible version", func(t *testing.T) {
// Discard changes between runs
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
t.Fatalf("Failed to clean working tree: %v", err)
}

// Create a flag set and context with no-op logger
set := flag.NewFlagSet("test", 0)
set.String("version", "v0.0.5", "")
Expand All @@ -258,6 +278,11 @@ func TestUpgradeCommand(t *testing.T) {

// Test with missing config file
t.Run("No config file", func(t *testing.T) {
// Discard changes between runs
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
t.Fatalf("Failed to clean working tree: %v", err)
}

// Create a separate directory without a config file
noConfigDir := filepath.Join(testProjectsDir, "no-config")
err = os.MkdirAll(noConfigDir, 0755)
Expand Down
59 changes: 59 additions & 0 deletions pkg/template/git_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package template

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
Expand Down Expand Up @@ -239,3 +240,61 @@ func (g *GitClient) ParseCloneOutput(r io.Reader, rep Reporter, dest string, ref
}
return nil
}

func (g *GitClient) GitIsClean() (bool, error) {
cmd := exec.Command("git", "status", "--porcelain")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Run(); err != nil {
return false, fmt.Errorf("git status failed: %w\n%s", err, out.String())
}
return strings.TrimSpace(out.String()) == "", nil
}

func (g *GitClient) EnsureGitignoreEntry(path, entry string) error {
// Create .gitignore if missing
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()

// Check if entry exists
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if strings.TrimSpace(scanner.Text()) == entry {
return nil // already present
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("scan %s: %w", path, err)
}

// Append entry
if _, err := f.WriteString("\n" + entry + "\n"); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}

if err := g.Commit(".gitignore", fmt.Sprintf("chore: ensure %s is in .gitignore", entry)); err != nil {
return fmt.Errorf("failed to commit entry to .gitignore %s: %w", entry, err)
}

return nil
}

func (g *GitClient) Commit(file, msg string) error {
cmds := [][]string{
{"git", "add", file},
{"git", "commit", "-m", msg},
}

for _, args := range cmds {
cmd := exec.Command(args[0], args[1:]...)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s failed: %v\n%s", strings.Join(args, " "), err, string(out))
}
}
return nil
}
34 changes: 34 additions & 0 deletions pkg/testutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"

Expand Down Expand Up @@ -249,3 +250,36 @@ func CaptureOutput(fn func()) (stdout string, stderr string) {

return stdout, stderr
}

func TestGitCall(repoDir string, cmds [][]string) error {
for _, args := range cmds {
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = repoDir
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git command %v failed: %v\n%s", args, err, out)
}
}
return nil
}

func TestGitInit(repoDir string) error {
// Init git repo
cmds := [][]string{
{"git", "init"},
{"git", "config", "user.name", "Test User"},
{"git", "config", "user.email", "[email protected]"},
{"git", "add", "."},
{"git", "commit", "--allow-empty", "-m", "initial commit"},
}
return TestGitCall(repoDir, cmds)
}

func TestGitDiscardChanges(repoDir string) error {
// Discard changes in working tree
cmds := [][]string{
{"git", "reset", "--hard"},
{"git", "clean", "-fdx"},
}
return TestGitCall(repoDir, cmds)
}
Loading