Skip to content

Commit 7a85ea6

Browse files
committed
fix: check working tree is clean before upgrading
1 parent d8c17e9 commit 7a85ea6

4 files changed

Lines changed: 85 additions & 3 deletions

File tree

pkg/commands/template/upgrade.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,20 @@ func createUpgradeCommand(
148148
// Create a gitClient for upgrade process
149149
gitClient := template.NewGitClient()
150150

151+
// Check if working tree is clean before upgrading
152+
clean, err := gitClient.GitIsClean()
153+
if err != nil {
154+
return fmt.Errorf("failed to check .git working tree state: %w", err)
155+
}
156+
if !clean {
157+
return fmt.Errorf("uncommitted changes found, please commit or stash them before upgrading")
158+
}
159+
151160
// Ensure .gitignore entry is present for tempExternal
152161
tempExternal := "temp_external"
153162
if err := gitClient.EnsureGitignoreEntry(".gitignore", tempExternal); err != nil {
154163
return fmt.Errorf("failed to add entry to .gitignore %s: %w", tempExternal, err)
155164
}
156-
if err := gitClient.Commit(".gitignore", fmt.Sprintf("chore: ensure %s is in .gitignore", tempExternal)); err != nil {
157-
return fmt.Errorf("failed to commit entry to .gitignore %s: %w", tempExternal, err)
158-
}
159165

160166
// Ensure parent exists
161167
tempParent := filepath.Join(absProjectPath, tempExternal)

pkg/commands/template/upgrade_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ func TestUpgradeCommand(t *testing.T) {
103103
t.Fatalf("Failed to write config file: %v", err)
104104
}
105105

106+
// Create git repo in temp dir
107+
if err := testutils.TestGitInit(testProjectsDir); err != nil {
108+
t.Fatalf("Failed init git repo: %v", err)
109+
}
110+
106111
// Create mock template info getter
107112
mockTemplateInfoGetter := &MockTemplateInfoGetter{
108113
projectName: "template-upgrade-test",
@@ -137,6 +142,11 @@ func TestUpgradeCommand(t *testing.T) {
137142

138143
// Test upgrade command with version flag
139144
t.Run("Upgrade command with version", func(t *testing.T) {
145+
// Discard changes between runs
146+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
147+
t.Fatalf("Failed to clean working tree: %v", err)
148+
}
149+
140150
// Create a flag set and context with no-op logger
141151
set := flag.NewFlagSet("test", 0)
142152
set.String("version", "v0.0.4", "")
@@ -186,6 +196,11 @@ func TestUpgradeCommand(t *testing.T) {
186196

187197
// Test upgrade command without version flag
188198
t.Run("Upgrade command without version", func(t *testing.T) {
199+
// Discard changes between runs
200+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
201+
t.Fatalf("Failed to clean working tree: %v", err)
202+
}
203+
189204
// Create a flag set and context without version flag, with no-op logger
190205
set := flag.NewFlagSet("test", 0)
191206

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

234249
// Test upgrade command with incompatible to devkit version
235250
t.Run("Upgrade command with incompatible version", func(t *testing.T) {
251+
// Discard changes between runs
252+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
253+
t.Fatalf("Failed to clean working tree: %v", err)
254+
}
255+
236256
// Create a flag set and context with no-op logger
237257
set := flag.NewFlagSet("test", 0)
238258
set.String("version", "v0.0.5", "")
@@ -258,6 +278,11 @@ func TestUpgradeCommand(t *testing.T) {
258278

259279
// Test with missing config file
260280
t.Run("No config file", func(t *testing.T) {
281+
// Discard changes between runs
282+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
283+
t.Fatalf("Failed to clean working tree: %v", err)
284+
}
285+
261286
// Create a separate directory without a config file
262287
noConfigDir := filepath.Join(testProjectsDir, "no-config")
263288
err = os.MkdirAll(noConfigDir, 0755)

pkg/template/git_client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package template
22

33
import (
44
"bufio"
5+
"bytes"
56
"context"
67
"fmt"
78
"io"
@@ -240,6 +241,17 @@ func (g *GitClient) ParseCloneOutput(r io.Reader, rep Reporter, dest string, ref
240241
return nil
241242
}
242243

244+
func (g *GitClient) GitIsClean() (bool, error) {
245+
cmd := exec.Command("git", "status", "--porcelain")
246+
var out bytes.Buffer
247+
cmd.Stdout = &out
248+
cmd.Stderr = &out
249+
if err := cmd.Run(); err != nil {
250+
return false, fmt.Errorf("git status failed: %w\n%s", err, out.String())
251+
}
252+
return strings.TrimSpace(out.String()) == "", nil
253+
}
254+
243255
func (g *GitClient) EnsureGitignoreEntry(path, entry string) error {
244256
// Create .gitignore if missing
245257
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)
@@ -263,6 +275,11 @@ func (g *GitClient) EnsureGitignoreEntry(path, entry string) error {
263275
if _, err := f.WriteString("\n" + entry + "\n"); err != nil {
264276
return fmt.Errorf("write %s: %w", path, err)
265277
}
278+
279+
if err := g.Commit(".gitignore", fmt.Sprintf("chore: ensure %s is in .gitignore", entry)); err != nil {
280+
return fmt.Errorf("failed to commit entry to .gitignore %s: %w", entry, err)
281+
}
282+
266283
return nil
267284
}
268285

pkg/testutils/utils.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"os"
8+
"os/exec"
89
"path/filepath"
910
"testing"
1011

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

250251
return stdout, stderr
251252
}
253+
254+
func TestGitCall(repoDir string, cmds [][]string) error {
255+
for _, args := range cmds {
256+
cmd := exec.Command(args[0], args[1:]...)
257+
cmd.Dir = repoDir
258+
out, err := cmd.CombinedOutput()
259+
if err != nil {
260+
return fmt.Errorf("git command %v failed: %v\n%s", args, err, out)
261+
}
262+
}
263+
return nil
264+
}
265+
266+
func TestGitInit(repoDir string) error {
267+
// Init git repo
268+
cmds := [][]string{
269+
{"git", "init"},
270+
{"git", "config", "user.name", "Test User"},
271+
{"git", "config", "user.email", "test@example.com"},
272+
{"git", "add", "."},
273+
{"git", "commit", "--allow-empty", "-m", "initial commit"},
274+
}
275+
return TestGitCall(repoDir, cmds)
276+
}
277+
278+
func TestGitDiscardChanges(repoDir string) error {
279+
// Discard changes in working tree
280+
cmds := [][]string{
281+
{"git", "reset", "--hard"},
282+
{"git", "clean", "-fdx"},
283+
}
284+
return TestGitCall(repoDir, cmds)
285+
}

0 commit comments

Comments
 (0)