summaryrefslogtreecommitdiff
path: root/internal/showcase
diff options
context:
space:
mode:
Diffstat (limited to 'internal/showcase')
-rw-r--r--internal/showcase/code_extractor.go14
-rw-r--r--internal/showcase/images.go17
-rw-r--r--internal/showcase/images_test.go55
-rw-r--r--internal/showcase/language_detector.go4
-rw-r--r--internal/showcase/showcase.go6
5 files changed, 87 insertions, 9 deletions
diff --git a/internal/showcase/code_extractor.go b/internal/showcase/code_extractor.go
index 92ee1e4..7c40573 100644
--- a/internal/showcase/code_extractor.go
+++ b/internal/showcase/code_extractor.go
@@ -9,6 +9,16 @@ import (
"strings"
)
+// closeFile closes a file opened for reading. These reads are best-effort
+// (shebang sniffing, line counting, snippet extraction) and the file is never
+// written to, so a close failure here cannot affect correctness - the error
+// is intentionally discarded rather than treated as actionable. Shared by
+// the other showcase package files that open read-only files (images.go,
+// language_detector.go).
+func closeFile(file *os.File) {
+ _ = file.Close()
+}
+
// extractCodeSnippet extracts a random code snippet from the repository
func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, string, error) {
if len(languages) == 0 {
@@ -113,7 +123,7 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
matched = true
}
}
- file.Close()
+ closeFile(file)
}
}
@@ -185,7 +195,7 @@ func extractSnippetFromFile(filePath string, minLines, maxLines int) (string, er
if err != nil {
return "", err
}
- defer file.Close()
+ defer closeFile(file)
// Read all lines
var lines []string
diff --git a/internal/showcase/images.go b/internal/showcase/images.go
index b6fe6d7..2c09a8e 100644
--- a/internal/showcase/images.go
+++ b/internal/showcase/images.go
@@ -226,19 +226,28 @@ func isGitHostedImage(url string) bool {
strings.Contains(url, "codeberg.page")
}
-// copyFile copies a file from src to dst
-func copyFile(src, dst string) error {
+// copyFile copies a file from src to dst. The destination is a write, so
+// unlike the read-only closeFile helper used elsewhere in this package, a
+// failure to close it is reported via the named return: Sync() already
+// covers the common flush-failure case, but a close error on a lagging
+// network filesystem is still real data-loss information worth surfacing,
+// and it must not mask an earlier, more specific error.
+func copyFile(src, dst string) (err error) {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
- defer sourceFile.Close()
+ defer closeFile(sourceFile)
destFile, err := os.Create(dst)
if err != nil {
return err
}
- defer destFile.Close()
+ defer func() {
+ if cerr := destFile.Close(); cerr != nil && err == nil {
+ err = fmt.Errorf("failed to close destination file %s: %w", dst, cerr)
+ }
+ }()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
diff --git a/internal/showcase/images_test.go b/internal/showcase/images_test.go
new file mode 100644
index 0000000..c88da00
--- /dev/null
+++ b/internal/showcase/images_test.go
@@ -0,0 +1,55 @@
+package showcase
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// TestCopyFile_CopiesContent verifies the basic success path still works
+// after copyFile was changed to use a named return so that a destination
+// Close() error can be surfaced without changing the happy-path result.
+func TestCopyFile_CopiesContent(t *testing.T) {
+ t.Parallel()
+
+ dir := t.TempDir()
+ src := filepath.Join(dir, "source.txt")
+ dst := filepath.Join(dir, "dest.txt")
+ want := "hello showcase\n"
+
+ if err := os.WriteFile(src, []byte(want), 0644); err != nil {
+ t.Fatalf("failed to write source file: %v", err)
+ }
+
+ if err := copyFile(src, dst); err != nil {
+ t.Fatalf("copyFile() returned error: %v", err)
+ }
+
+ got, err := os.ReadFile(dst)
+ if err != nil {
+ t.Fatalf("failed to read destination file: %v", err)
+ }
+ if string(got) != want {
+ t.Fatalf("copyFile() wrote %q, want %q", string(got), want)
+ }
+}
+
+// TestCopyFile_MissingSourceReturnsError verifies that a missing source file
+// still produces an error (and never a nil error masked by the deferred
+// destination-close handling) and does not leave a destination file behind.
+func TestCopyFile_MissingSourceReturnsError(t *testing.T) {
+ t.Parallel()
+
+ dir := t.TempDir()
+ src := filepath.Join(dir, "does-not-exist.txt")
+ dst := filepath.Join(dir, "dest.txt")
+
+ err := copyFile(src, dst)
+ if err == nil {
+ t.Fatal("copyFile() with missing source returned nil error, want non-nil")
+ }
+
+ if _, statErr := os.Stat(dst); statErr == nil {
+ t.Fatal("copyFile() with missing source created a destination file, want none")
+ }
+}
diff --git a/internal/showcase/language_detector.go b/internal/showcase/language_detector.go
index 692f048..9a42be1 100644
--- a/internal/showcase/language_detector.go
+++ b/internal/showcase/language_detector.go
@@ -201,7 +201,7 @@ func detectLanguages(repoPath string) (languages []LanguageStats, documentation
}
}
}
- file.Close()
+ closeFile(file)
}
}
@@ -281,7 +281,7 @@ func countFileLines(path string) (int, error) {
if err != nil {
return 0, err
}
- defer file.Close()
+ defer closeFile(file)
scanner := bufio.NewScanner(file)
lines := 0
diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go
index ae938cb..9582c1f 100644
--- a/internal/showcase/showcase.go
+++ b/internal/showcase/showcase.go
@@ -350,7 +350,11 @@ func (g *Generator) prepareStatsRepoPath(repoName, repoPath string) (string, fun
}
cleanup := func() error {
- defer os.RemoveAll(tempRoot)
+ // Best-effort cleanup of the temporary worktree root, matching the
+ // discard above for the same call on the worktree-add failure path:
+ // this is scratch space, and by the time we get here the actual
+ // worktree removal below is the operation whose error matters.
+ defer func() { _ = os.RemoveAll(tempRoot) }()
if _, err := runCommandWithCustomTimeout(45*time.Second, "git", "-C", repoPath, "worktree", "remove", "--force", worktreePath); err != nil {
return fmt.Errorf("failed to remove temporary worktree for %s: %w", repoName, err)