• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyberDuck79 / duckfile / 18097442731

29 Sep 2025 12:46PM UTC coverage: 79.573% (+0.04%) from 79.529%
18097442731

Pull #70

github

CyberDuck79
refactor: extract writeMetadataFile helper to reduce metadata writing duplication

- Add writeMetadataFile() helper function for consistent JSON metadata writing
- Replace duplicate json.Marshal + os.WriteFile patterns in remote.go
- Reduces code duplication in template and remote metadata handling
Pull Request #70: Phase 2: Implement separated targets and remotes configurations

592 of 686 new or added lines in 9 files covered. (86.3%)

24 existing lines in 5 files now uncovered.

2758 of 3466 relevant lines covered (79.57%)

11.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.2
/internal/run/remote.go
1
package run
2

3
import (
4
        "encoding/json"
5
        "fmt"
6
        "os"
7
        "path/filepath"
8

9
        "github.com/CyberDuck79/duckfile/internal/config"
10
        "github.com/CyberDuck79/duckfile/internal/log"
11
)
12

13
// writeMetadataFile writes metadata to a JSON file with error logging
14
func writeMetadataFile(filePath string, metadata map[string]string, description string) {
97✔
15
        metadataBytes, _ := json.Marshal(metadata)
97✔
16
        if err := os.WriteFile(filePath, metadataBytes, 0o644); err != nil {
97✔
NEW
17
                log.Warnf("failed to write %s: %v", description, err)
×
NEW
18
        }
×
19
}
20

21
// fetchRemote clones (or reclones into) the remote cache directory at the repository level.
22
// This is now separated from template extraction to enable sharing across multiple templates.
23
func fetchRemote(force bool, resolved config.ResolvedTemplate, paths *templatePaths) error {
51✔
24
        if force {
53✔
25
                log.Infof("🔄 force fetching remote: %s@%s", resolved.Repo, resolved.Ref)
2✔
26
        } else {
51✔
27
                log.Infof("🔄 fetch remote: %s@%s", resolved.Repo, resolved.Ref)
49✔
28
        }
49✔
29
        if err := os.MkdirAll(paths.remoteDir, 0o755); err != nil {
51✔
30
                return err
×
31
        }
×
32

33
        // Clone the entire repository into remote cache (shared across templates)
34
        repoDir, err := cloneFunc(resolved.Repo, resolved.Ref, paths.remoteDir, resolved.Submodules)
51✔
35
        if err != nil {
52✔
36
                return err
1✔
37
        }
1✔
38

39
        // Store remote metadata for future reference
40
        metadata := map[string]string{
50✔
41
                "repo": resolved.Repo,
50✔
42
                "ref":  resolved.Ref,
50✔
43
        }
50✔
44
        writeMetadataFile(filepath.Join(paths.remoteDir, "metadata.json"), metadata, "remote metadata")
50✔
45

50✔
46
        // capture commit hash (best-effort) at remote level
50✔
47
        if commitHash, err := getCurrentCommitFunc(repoDir); err != nil {
83✔
48
                log.Debugf("skip commit hash capture (unavailable): %v", err)
33✔
49
        } else if err := writeCommitHashMetadata(paths.remoteDir, commitHash); err != nil {
50✔
NEW
50
                log.Warnf("failed to write commit hash metadata: %v", err)
×
NEW
51
        }
×
52
        return nil
50✔
53
}
54

55
// extractTemplate extracts a specific template file from the remote cache to the template cache.
56
// This allows multiple templates to share the same remote repository while caching individual files.
57
func extractTemplate(resolved config.ResolvedTemplate, paths *templatePaths) error {
51✔
58
        if err := os.MkdirAll(paths.templateDir, 0o755); err != nil {
51✔
UNCOV
59
                return err
×
UNCOV
60
        }
×
61

62
        // Find the repository directory within remote cache
63
        repoDir := filepath.Join(paths.remoteDir, "repo")
51✔
64
        src := filepath.Join(repoDir, resolved.Path)
51✔
65

51✔
66
        // Validate checksum if provided
51✔
67
        if resolved.Checksum != "" {
61✔
68
                if err := validateResolvedTemplateChecksum(src, resolved.Checksum); err != nil {
13✔
69
                        return err
3✔
70
                }
3✔
71
        }
72

73
        // Read and cache the template file
74
        raw, err := os.ReadFile(src)
48✔
75
        if err != nil {
49✔
76
                return fmt.Errorf("read template file %s: %w", resolved.Path, err)
1✔
77
        }
1✔
78

79
        if err := os.WriteFile(paths.remoteTemplateFile, raw, 0o644); err != nil {
47✔
NEW
80
                return fmt.Errorf("cache template file: %w", err)
×
81
        }
×
82

83
        // Store template metadata
84
        metadata := map[string]string{
47✔
85
                "remote": paths.remoteKey,
47✔
86
                "path":   resolved.Path,
47✔
87
        }
47✔
88
        if resolved.Checksum != "" {
54✔
89
                metadata["checksum"] = resolved.Checksum
7✔
90
        }
7✔
91
        writeMetadataFile(filepath.Join(paths.templateDir, "metadata.json"), metadata, "template metadata")
47✔
92

47✔
93
        return nil
47✔
94
}
95

96
// decideTemplateFetch determines whether the template file needs to be extracted from remote cache
97
func decideTemplateFetch(force, needRemote bool, resolved config.ResolvedTemplate, paths *templatePaths) (bool, error) {
55✔
98
        // If we just fetched the remote, we need to extract the template
55✔
99
        if needRemote {
97✔
100
                return true, nil
42✔
101
        }
42✔
102

103
        // Check if template cache exists
104
        if _, err := os.Stat(paths.remoteTemplateFile); os.IsNotExist(err) {
15✔
105
                return true, nil
2✔
106
        }
2✔
107

108
        return force, nil
11✔
109
}
110

111
// decideRemoteFetchResolved determines whether the remote repository needs to be fetched
112
// based on the resolved template configuration
113
// handleCommitHashValidation handles validation and auto-update logic for resolved templates
114
func handleCommitHashValidation(resolved config.ResolvedTemplate, autoUpdateOnChangeFlag *bool, paths *templatePaths) (bool, error) {
7✔
115
        log.Infof("🔍 checking remote updates: %s@%s", resolved.Repo, resolved.Ref)
7✔
116
        valid, err := validateCachedCommitHash(resolved.Repo, resolved.Ref, paths.remoteDir)
7✔
117
        if err != nil {
7✔
NEW
118
                return false, fmt.Errorf("commit hash validation failed: %w", err)
×
NEW
119
        }
×
120

121
        if valid {
9✔
122
                return false, nil // No update needed
2✔
123
        }
2✔
124

125
        // Template has changed - determine auto-update behavior
126
        autoUpdate := resolved.AutoUpdateOnChange
5✔
127
        if autoUpdateOnChangeFlag != nil {
5✔
NEW
128
                autoUpdate = *autoUpdateOnChangeFlag
×
NEW
129
        }
×
130

131
        if autoUpdate {
8✔
132
                log.Infof("📦 updating remote cache (commit changed)")
3✔
133
                if err := invalidateCache(paths.remoteDir); err != nil {
3✔
NEW
134
                        return false, err
×
NEW
135
                }
×
136
                return true, nil // Need to fetch
3✔
137
        }
138

139
        // Auto-update disabled - return informative error
140
        storedHash, _ := readCommitHashMetadata(paths.remoteDir)
2✔
141
        remoteHash, _ := getRemoteCommitFunc(resolved.Repo, resolved.Ref)
2✔
142
        return false, fmt.Errorf("template has been updated remotely, but automatic updates are disabled.\n\nTemplate: %s@%s\nCached commit:  %s\nRemote commit:  %s\n\nEnable autoUpdateOnChange or re-run with --force", resolved.Repo, resolved.Ref, truncateHash(storedHash), truncateHash(remoteHash))
2✔
143
}
144

145
func decideRemoteFetchResolved(force, trackCommitHash bool, resolved config.ResolvedTemplate, cfg *config.DuckConf, autoUpdateOnChangeFlag *bool, paths *templatePaths) (bool, error) {
57✔
146
        needRemote := force
57✔
147

57✔
148
        // Check if remote repository cache exists
57✔
149
        repoDir := filepath.Join(paths.remoteDir, "repo")
57✔
150
        if _, err := os.Stat(repoDir); os.IsNotExist(err) {
95✔
151
                needRemote = true
38✔
152
        }
38✔
153

154
        // Handle commit hash validation if enabled and cache exists
155
        if !needRemote && trackCommitHash {
64✔
156
                fetchNeeded, err := handleCommitHashValidation(resolved, autoUpdateOnChangeFlag, paths)
7✔
157
                if err != nil {
9✔
158
                        return false, err
2✔
159
                }
2✔
160
                if fetchNeeded {
8✔
161
                        needRemote = true
3✔
162
                }
3✔
163
        }
164

165
        return needRemote, nil
55✔
166
}
167

168
// decideRemoteFetch determines whether the remote template needs to be fetched
169
// again based on force flag, presence of cache, and (optionally) commit hash
170
// tracking with auto-update behavior.
171
func decideRemoteFetch(force, trackCommitHash bool, target config.Target, cfg *config.DuckConf, autoUpdateOnChangeFlag *bool, paths *templatePaths) (bool, error) {
5✔
172
        needRemote := force
5✔
173
        if _, err := os.Stat(paths.remoteTemplateFile); os.IsNotExist(err) {
6✔
174
                needRemote = true
1✔
175
        }
1✔
176
        if !needRemote && trackCommitHash { // commit hash validation path
7✔
177
                log.Infof("🔍 checking remote updates: %s@%s", target.Template.Repo, target.Template.Ref)
2✔
178
                valid, err := validateCachedCommitHash(target.Template.Repo, target.Template.Ref, paths.remoteDir)
2✔
179
                if err != nil {
2✔
180
                        return false, fmt.Errorf("commit hash validation failed: %w", err)
×
181
                }
×
182
                if !valid {
3✔
183
                        autoUpdate := config.ResolveAutoUpdateOnChange(autoUpdateOnChangeFlag, &target.Template, cfg)
1✔
184
                        if autoUpdate {
2✔
185
                                log.Infof("📦 updating remote cache (commit changed)")
1✔
186
                                if err := invalidateCache(paths.remoteDir); err != nil {
1✔
187
                                        return false, err
×
188
                                }
×
189
                                needRemote = true
1✔
UNCOV
190
                        } else {
×
UNCOV
191
                                storedHash, _ := readCommitHashMetadata(paths.remoteDir)
×
UNCOV
192
                                remoteHash, _ := getRemoteCommitFunc(target.Template.Repo, target.Template.Ref)
×
UNCOV
193
                                return false, fmt.Errorf("template has been updated remotely, but automatic updates are disabled.\n\nTemplate: %s@%s\nCached commit:  %s\nRemote commit:  %s\n\nEnable autoUpdateOnChange or re-run with --force", target.Template.Repo, target.Template.Ref, truncateHash(storedHash), truncateHash(remoteHash))
×
UNCOV
194
                        }
×
195
                }
196
        }
197
        return needRemote, nil
5✔
198
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc