Skip to content

Commit 049742d

Browse files
fgksgfclaude
andauthored
Fix null pointer panic in listFiles when repository has no commits or invalid HEAD (#202)
Fixes a panic that occurs when calling repo.Head().Hash() in empty git repositories, git worktrees, or repositories with invalid HEAD references. The issue happened in pkg/header/check.go where head could be nil, causing a null pointer dereference when calling head.Hash(). This commonly occurs in the following scenarios: - Empty git repositories with no initial commits - Git worktrees with detached HEAD or invalid branch references - Corrupted repositories with invalid HEAD pointers - New worktrees created from non-existent commits or branches Changes: - Add proper null check for head reference before calling head.Hash() - Add error handling for repo.Head() to gracefully handle edge cases - Skip git-based file discovery when repository has no commits or invalid HEAD - Add test cases for empty repositories and worktree scenarios in check_test.go - Ensure graceful fallback to glob-based file discovery in problematic git states Resolves panic at pkg/header/check.go:99 when working with empty git repositories or git worktree environments with invalid HEAD references. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <[email protected]>
1 parent a62f574 commit 049742d

File tree

2 files changed

+195
-13
lines changed

2 files changed

+195
-13
lines changed

pkg/header/check.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,29 @@ func listFiles(config *ConfigHeader) ([]string, error) {
9292
candidates = append(candidates, file)
9393
}
9494

95-
head, _ := repo.Head()
96-
commit, _ := repo.CommitObject(head.Hash())
97-
tree, err := commit.Tree()
98-
if err != nil {
99-
return nil, err
100-
}
101-
if err := tree.Files().ForEach(func(file *object.File) error {
102-
if file == nil {
103-
return errors.New("file pointer is nil")
95+
head, err := repo.Head()
96+
if err != nil || head == nil {
97+
// Repository has no commits or invalid HEAD, skip git-based file discovery
98+
logger.Log.Debugf("Repository has no commits or invalid HEAD (head: %v), skipping git-based file discovery. Error: %v", head, err)
99+
} else {
100+
commit, err := repo.CommitObject(head.Hash())
101+
if err != nil {
102+
logger.Log.Debugln("Failed to get commit object:", err)
103+
} else {
104+
tree, err := commit.Tree()
105+
if err != nil {
106+
return nil, err
107+
}
108+
if err := tree.Files().ForEach(func(file *object.File) error {
109+
if file == nil {
110+
return errors.New("file pointer is nil")
111+
}
112+
candidates = append(candidates, file.Name)
113+
return nil
114+
}); err != nil {
115+
return nil, err
116+
}
104117
}
105-
candidates = append(candidates, file.Name)
106-
return nil
107-
}); err != nil {
108-
return nil, err
109118
}
110119

111120
seen := make(map[string]bool)

pkg/header/check_test.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
"strings"
2424
"testing"
2525

26+
"github.com/go-git/go-git/v5"
27+
"github.com/go-git/go-git/v5/plumbing"
28+
"github.com/go-git/go-git/v5/plumbing/object"
2629
"github.com/stretchr/testify/require"
2730
"gopkg.in/yaml.v3"
2831
)
@@ -96,3 +99,173 @@ func TestCheckFile(t *testing.T) {
9699
}
97100
})
98101
}
102+
103+
func TestListFilesWithEmptyRepo(t *testing.T) {
104+
// Create a temporary directory for testing
105+
tempDir, err := os.MkdirTemp("", "skywalking-eyes-test")
106+
if err != nil {
107+
t.Fatal(err)
108+
}
109+
defer os.RemoveAll(tempDir)
110+
111+
// Change to temp directory
112+
originalDir, err := os.Getwd()
113+
if err != nil {
114+
t.Fatal(err)
115+
}
116+
defer os.Chdir(originalDir)
117+
118+
if err := os.Chdir(tempDir); err != nil {
119+
t.Fatal(err)
120+
}
121+
122+
// Initialize an empty git repository
123+
_, err = git.PlainInit(".", false)
124+
if err != nil {
125+
t.Fatal(err)
126+
}
127+
128+
// Create a test file
129+
testFile := filepath.Join(tempDir, "test.go")
130+
err = os.WriteFile(testFile, []byte("package main"), 0644)
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
135+
// Create a basic config
136+
config := &ConfigHeader{
137+
Paths: []string{"**/*.go"},
138+
}
139+
140+
// This should not panic even with empty repository
141+
fileList, err := listFiles(config)
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
146+
// Should still find files using glob fallback
147+
if len(fileList) == 0 {
148+
t.Error("Expected to find at least one file")
149+
}
150+
}
151+
152+
func TestListFilesWithWorktreeDetachedHEAD(t *testing.T) {
153+
// Create a temporary directory for testing
154+
tempDir, err := os.MkdirTemp("", "skywalking-eyes-worktree-test")
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
defer os.RemoveAll(tempDir)
159+
160+
// Change to temp directory
161+
originalDir, err := os.Getwd()
162+
if err != nil {
163+
t.Fatal(err)
164+
}
165+
defer os.Chdir(originalDir)
166+
167+
if err := os.Chdir(tempDir); err != nil {
168+
t.Fatal(err)
169+
}
170+
171+
// Initialize a git repository with a commit
172+
repo, err := git.PlainInit(".", false)
173+
if err != nil {
174+
t.Fatal(err)
175+
}
176+
177+
// Create and commit a file
178+
testFile := "test.go"
179+
err = os.WriteFile(testFile, []byte("package main"), 0644)
180+
if err != nil {
181+
t.Fatal(err)
182+
}
183+
184+
worktree, err := repo.Worktree()
185+
if err != nil {
186+
t.Fatal(err)
187+
}
188+
189+
_, err = worktree.Add(testFile)
190+
if err != nil {
191+
t.Fatal(err)
192+
}
193+
194+
commit, err := worktree.Commit("Initial commit", &git.CommitOptions{
195+
Author: &object.Signature{
196+
Name: "Test User",
197+
198+
},
199+
})
200+
if err != nil {
201+
t.Fatal(err)
202+
}
203+
204+
// First, verify normal case works
205+
config := &ConfigHeader{
206+
Paths: []string{"**/*.go"},
207+
}
208+
209+
fileList, err := listFiles(config)
210+
if err != nil {
211+
t.Fatal(err)
212+
}
213+
214+
if len(fileList) == 0 {
215+
t.Error("Expected to find files with valid commit")
216+
}
217+
218+
// Now simulate detached HEAD by checking out to a non-existent commit hash
219+
// This will create an invalid HEAD state that our fix should handle
220+
invalidHash := plumbing.NewHash("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
221+
err = worktree.Checkout(&git.CheckoutOptions{
222+
Hash: invalidHash,
223+
})
224+
// We expect this to fail, creating an invalid state
225+
if err == nil {
226+
t.Fatal("Expected checkout to invalid hash to fail")
227+
}
228+
229+
// This should not panic even with problematic git state
230+
fileList2, err := listFiles(config)
231+
if err != nil {
232+
// It's okay if there's an error, we just don't want a panic
233+
t.Logf("Got expected error: %v", err)
234+
}
235+
236+
// Should still find files using glob fallback
237+
if len(fileList2) == 0 {
238+
t.Error("Expected to find at least one file via fallback")
239+
}
240+
241+
t.Logf("Found %d files: %v", len(fileList2), fileList2)
242+
243+
// Verify we can find our test file
244+
found := false
245+
for _, file := range fileList2 {
246+
if filepath.Base(file) == testFile {
247+
found = true
248+
break
249+
}
250+
}
251+
if !found {
252+
t.Error("Expected to find test.go in file list")
253+
}
254+
255+
// Test with valid commit to ensure normal case still works
256+
err = worktree.Checkout(&git.CheckoutOptions{
257+
Hash: commit,
258+
})
259+
if err != nil {
260+
t.Fatal(err)
261+
}
262+
263+
fileList3, err := listFiles(config)
264+
if err != nil {
265+
t.Fatal(err)
266+
}
267+
268+
if len(fileList3) == 0 {
269+
t.Error("Expected to find files with valid commit")
270+
}
271+
}

0 commit comments

Comments
 (0)