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

umputun / stash / 20654643210

02 Jan 2026 09:09AM UTC coverage: 84.052% (-0.08%) from 84.127%
20654643210

push

github

umputun
refactor(git): improve code quality from review findings

- extract writeKeyFile helper to reduce Commit function size
- document ReadAll O(n) performance characteristic
- fix concurrent test to verify success count matches stored keys
- add error context verification in service tests

9 of 14 new or added lines in 1 file covered. (64.29%)

3 existing lines in 2 files now uncovered.

3974 of 4728 relevant lines covered (84.05%)

78.91 hits per line

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

75.06
/app/git/git.go
1
// Package git provides git-based versioning for key-value storage.
2
// It tracks all changes to keys in a local git repository with optional
3
// push to remote.
4
package git
5

6
import (
7
        "errors"
8
        "fmt"
9
        "io"
10
        "os"
11
        "path/filepath"
12
        "strings"
13
        "sync"
14
        "time"
15

16
        log "github.com/go-pkgz/lgr"
17

18
        "github.com/go-git/go-git/v5"
19
        "github.com/go-git/go-git/v5/config"
20
        "github.com/go-git/go-git/v5/plumbing"
21
        "github.com/go-git/go-git/v5/plumbing/object"
22
        "github.com/go-git/go-git/v5/plumbing/transport"
23
        "github.com/go-git/go-git/v5/plumbing/transport/ssh"
24
)
25

26
// Author represents the author of a git commit.
27
type Author struct {
28
        Name  string
29
        Email string
30
}
31

32
// DefaultAuthor returns the default author for git commits.
33
func DefaultAuthor() Author {
52✔
34
        return Author{Name: "stash", Email: "stash@localhost"}
52✔
35
}
52✔
36

37
// KeyValue holds a key's value and format metadata.
38
type KeyValue struct {
39
        Value  []byte
40
        Format string
41
}
42

43
// HistoryEntry represents a single revision of a key.
44
type HistoryEntry struct {
45
        Hash      string    `json:"hash"`
46
        Timestamp time.Time `json:"timestamp"`
47
        Author    string    `json:"author"`
48
        Operation string    `json:"operation"`
49
        Format    string    `json:"format"`
50
        Value     []byte    `json:"value"`
51
}
52

53
// CommitRequest holds parameters for a git commit operation.
54
type CommitRequest struct {
55
        Key       string
56
        Value     []byte
57
        Operation string
58
        Format    string
59
        Author    Author
60
}
61

62
// Config holds git repository configuration
63
type Config struct {
64
        Path   string // local repository path
65
        Branch string // branch name (default: master)
66
        Remote string // remote name (optional, for push/pull)
67
        SSHKey string // path to SSH private key (optional, for push)
68
}
69

70
// Store provides git-backed versioning for key-value storage
71
type Store struct {
72
        cfg  Config
73
        repo *git.Repository
74
        mu   sync.Mutex
75
}
76

77
// New creates a new git store, initializing or opening the repository
78
func New(cfg Config) (*Store, error) {
46✔
79
        if cfg.Path == "" {
47✔
80
                return nil, errors.New("git path is required")
1✔
81
        }
1✔
82
        if cfg.Branch == "" {
82✔
83
                cfg.Branch = "master"
37✔
84
        }
37✔
85

86
        s := &Store{cfg: cfg}
45✔
87
        if err := s.initRepo(); err != nil {
45✔
88
                return nil, fmt.Errorf("failed to init git repo: %w", err)
×
89
        }
×
90
        return s, nil
45✔
91
}
92

93
// initRepo opens existing or creates new git repository
94
func (s *Store) initRepo() error {
45✔
95
        // try to open existing repo
45✔
96
        repo, err := git.PlainOpen(s.cfg.Path)
45✔
97
        if err == nil {
48✔
98
                s.repo = repo
3✔
99
                return s.ensureBranch()
3✔
100
        }
3✔
101

102
        // create new repo if not exists
103
        if errors.Is(err, git.ErrRepositoryNotExists) {
84✔
104
                return s.createNewRepo()
42✔
105
        }
42✔
106

107
        return fmt.Errorf("failed to open repo: %w", err)
×
108
}
109

110
// ensureBranch checks out the configured branch, creating it if necessary
111
func (s *Store) ensureBranch() error {
3✔
112
        wt, err := s.repo.Worktree()
3✔
113
        if err != nil {
3✔
114
                return fmt.Errorf("failed to get worktree: %w", err)
×
115
        }
×
116

117
        branchRef := plumbing.NewBranchReferenceName(s.cfg.Branch)
3✔
118

3✔
119
        // try to checkout existing branch
3✔
120
        if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: branchRef}); chkErr == nil {
4✔
121
                return nil
1✔
122
        }
1✔
123

124
        // branch doesn't exist, create it from HEAD
125
        head, headErr := s.repo.Head()
2✔
126
        if headErr != nil {
2✔
127
                return fmt.Errorf("failed to get HEAD: %w", headErr)
×
128
        }
×
129

130
        // create and checkout the branch
131
        if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: branchRef, Hash: head.Hash(), Create: true}); chkErr != nil {
2✔
132
                return fmt.Errorf("failed to checkout branch %s: %w", s.cfg.Branch, chkErr)
×
133
        }
×
134
        return nil
2✔
135
}
136

137
func (s *Store) createNewRepo() error {
42✔
138
        repo, err := git.PlainInit(s.cfg.Path, false)
42✔
139
        if err != nil {
42✔
140
                return fmt.Errorf("failed to init repo: %w", err)
×
141
        }
×
142
        s.repo = repo
42✔
143

42✔
144
        // create initial commit on configured branch
42✔
145
        wt, wtErr := repo.Worktree()
42✔
146
        if wtErr != nil {
42✔
147
                return fmt.Errorf("failed to get worktree: %w", wtErr)
×
148
        }
×
149

150
        // create .gitkeep to have something to commit
151
        gitkeep := filepath.Join(s.cfg.Path, ".gitkeep")
42✔
152
        if writeErr := os.WriteFile(gitkeep, []byte{}, 0o600); writeErr != nil {
42✔
153
                return fmt.Errorf("failed to create .gitkeep: %w", writeErr)
×
154
        }
×
155
        if _, addErr := wt.Add(".gitkeep"); addErr != nil {
42✔
156
                return fmt.Errorf("failed to stage .gitkeep: %w", addErr)
×
157
        }
×
158

159
        _, commitErr := wt.Commit("initial commit", &git.CommitOptions{
42✔
160
                Author: &object.Signature{
42✔
161
                        Name:  "stash",
42✔
162
                        Email: "stash@localhost",
42✔
163
                        When:  time.Now(),
42✔
164
                },
42✔
165
        })
42✔
166
        if commitErr != nil {
42✔
167
                return fmt.Errorf("failed to create initial commit: %w", commitErr)
×
168
        }
×
169

170
        // checkout configured branch (create if not master)
171
        if s.cfg.Branch != "master" {
43✔
172
                head, headErr := repo.Head()
1✔
173
                if headErr != nil {
1✔
174
                        return fmt.Errorf("failed to get HEAD: %w", headErr)
×
175
                }
×
176
                branchRef := plumbing.NewBranchReferenceName(s.cfg.Branch)
1✔
177
                if chkErr := wt.Checkout(&git.CheckoutOptions{
1✔
178
                        Branch: branchRef,
1✔
179
                        Hash:   head.Hash(),
1✔
180
                        Create: true,
1✔
181
                }); chkErr != nil {
1✔
182
                        return fmt.Errorf("failed to checkout branch %s: %w", s.cfg.Branch, chkErr)
×
183
                }
×
184
        }
185

186
        return nil
42✔
187
}
188

189
// writeKeyFile writes key value to file and returns the relative path for staging.
190
func (s *Store) writeKeyFile(key string, value []byte) (string, error) {
55✔
191
        filePath := keyToPath(key)
55✔
192
        fullPath := filepath.Join(s.cfg.Path, filePath)
55✔
193

55✔
194
        if err := os.MkdirAll(filepath.Dir(fullPath), 0o750); err != nil {
55✔
NEW
195
                return "", fmt.Errorf("failed to create directory: %w", err)
×
NEW
196
        }
×
197
        if err := os.WriteFile(fullPath, value, 0o600); err != nil {
55✔
NEW
198
                return "", fmt.Errorf("failed to write file: %w", err)
×
NEW
199
        }
×
200
        return filePath, nil
55✔
201
}
202

203
// Commit writes key-value to file and commits to git.
204
func (s *Store) Commit(req CommitRequest) error {
61✔
205
        if err := s.validateKey(req.Key); err != nil {
67✔
206
                return err
6✔
207
        }
6✔
208

209
        s.mu.Lock()
55✔
210
        defer s.mu.Unlock()
55✔
211

55✔
212
        now := time.Now()
55✔
213

55✔
214
        format := req.Format
55✔
215
        if format == "" {
99✔
216
                format = "text"
44✔
217
        }
44✔
218

219
        filePath, err := s.writeKeyFile(req.Key, req.Value)
55✔
220
        if err != nil {
55✔
NEW
221
                return err
×
UNCOV
222
        }
×
223

224
        wt, err := s.repo.Worktree()
55✔
225
        if err != nil {
55✔
226
                return fmt.Errorf("failed to get worktree: %w", err)
×
227
        }
×
228

229
        if _, addErr := wt.Add(filePath); addErr != nil {
55✔
230
                return fmt.Errorf("failed to stage file: %w", addErr)
×
231
        }
×
232

233
        // commit with metadata including format
234
        msg := fmt.Sprintf("%s %s\n\ntimestamp: %s\noperation: %s\nkey: %s\nformat: %s",
55✔
235
                req.Operation, req.Key, now.Format(time.RFC3339), req.Operation, req.Key, format)
55✔
236

55✔
237
        _, commitErr := wt.Commit(msg, &git.CommitOptions{
55✔
238
                Author: &object.Signature{
55✔
239
                        Name:  req.Author.Name,
55✔
240
                        Email: req.Author.Email,
55✔
241
                        When:  now,
55✔
242
                },
55✔
243
        })
55✔
244
        if commitErr != nil {
55✔
245
                return fmt.Errorf("failed to commit: %w", commitErr)
×
246
        }
×
247

248
        return nil
55✔
249
}
250

251
// Delete removes key file and commits the deletion.
252
// The author parameter specifies who made the change.
253
func (s *Store) Delete(key string, author Author) error {
6✔
254
        // validate key before any file operations
6✔
255
        if err := s.validateKey(key); err != nil {
10✔
256
                return err
4✔
257
        }
4✔
258

259
        s.mu.Lock()
2✔
260
        defer s.mu.Unlock()
2✔
261

2✔
262
        now := time.Now()
2✔
263
        filePath := keyToPath(key)
2✔
264
        fullPath := filepath.Join(s.cfg.Path, filePath)
2✔
265

2✔
266
        // check if file exists
2✔
267
        if _, err := os.Stat(fullPath); os.IsNotExist(err) {
3✔
268
                return nil // nothing to delete
1✔
269
        }
1✔
270

271
        // remove file
272
        if err := os.Remove(fullPath); err != nil {
1✔
273
                return fmt.Errorf("failed to remove file: %w", err)
×
274
        }
×
275

276
        // stage deletion
277
        wt, err := s.repo.Worktree()
1✔
278
        if err != nil {
1✔
279
                return fmt.Errorf("failed to get worktree: %w", err)
×
280
        }
×
281

282
        if _, rmErr := wt.Remove(filePath); rmErr != nil {
1✔
283
                return fmt.Errorf("failed to stage deletion: %w", rmErr)
×
284
        }
×
285

286
        // commit deletion
287
        msg := fmt.Sprintf("delete %s\n\ntimestamp: %s\noperation: delete\nkey: %s", key, now.Format(time.RFC3339), key)
1✔
288
        _, commitErr := wt.Commit(msg, &git.CommitOptions{
1✔
289
                Author: &object.Signature{
1✔
290
                        Name:  author.Name,
1✔
291
                        Email: author.Email,
1✔
292
                        When:  now,
1✔
293
                },
1✔
294
        })
1✔
295
        if commitErr != nil {
1✔
296
                return fmt.Errorf("failed to commit deletion: %w", commitErr)
×
297
        }
×
298

299
        return nil
1✔
300
}
301

302
// Push pushes commits to remote repository
303
func (s *Store) Push() error {
3✔
304
        if s.cfg.Remote == "" {
5✔
305
                return nil // no remote configured
2✔
306
        }
2✔
307

308
        s.mu.Lock()
1✔
309
        defer s.mu.Unlock()
1✔
310

1✔
311
        var auth transport.AuthMethod
1✔
312
        if s.cfg.SSHKey != "" {
2✔
313
                var err error
1✔
314
                auth, err = ssh.NewPublicKeysFromFile("git", s.cfg.SSHKey, "")
1✔
315
                if err != nil {
2✔
316
                        return fmt.Errorf("failed to load SSH key: %w", err)
1✔
317
                }
1✔
318
        }
319

320
        err := s.repo.Push(&git.PushOptions{
×
321
                RemoteName: s.cfg.Remote,
×
322
                Auth:       auth,
×
323
                RefSpecs: []config.RefSpec{
×
324
                        config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", s.cfg.Branch, s.cfg.Branch)),
×
325
                },
×
326
        })
×
327
        if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
×
328
                return fmt.Errorf("failed to push: %w", err)
×
329
        }
×
330
        return nil
×
331
}
332

333
// Head returns the current HEAD commit hash as a short string
334
func (s *Store) Head() (string, error) {
3✔
335
        s.mu.Lock()
3✔
336
        defer s.mu.Unlock()
3✔
337

3✔
338
        ref, err := s.repo.Head()
3✔
339
        if err != nil {
3✔
340
                return "", fmt.Errorf("failed to get HEAD: %w", err)
×
341
        }
×
342
        return ref.Hash().String()[:7], nil
3✔
343
}
344

345
// Pull fetches and merges from remote repository
346
func (s *Store) Pull() error {
3✔
347
        if s.cfg.Remote == "" {
5✔
348
                return nil // no remote configured
2✔
349
        }
2✔
350

351
        s.mu.Lock()
1✔
352
        defer s.mu.Unlock()
1✔
353

1✔
354
        var auth transport.AuthMethod
1✔
355
        if s.cfg.SSHKey != "" {
2✔
356
                var err error
1✔
357
                auth, err = ssh.NewPublicKeysFromFile("git", s.cfg.SSHKey, "")
1✔
358
                if err != nil {
2✔
359
                        return fmt.Errorf("failed to load SSH key: %w", err)
1✔
360
                }
1✔
361
        }
362

363
        wt, err := s.repo.Worktree()
×
364
        if err != nil {
×
365
                return fmt.Errorf("failed to get worktree: %w", err)
×
366
        }
×
367

368
        pullErr := wt.Pull(&git.PullOptions{
×
369
                RemoteName:    s.cfg.Remote,
×
370
                Auth:          auth,
×
371
                ReferenceName: plumbing.NewBranchReferenceName(s.cfg.Branch),
×
372
        })
×
373
        if pullErr != nil && !errors.Is(pullErr, git.NoErrAlreadyUpToDate) {
×
374
                return fmt.Errorf("failed to pull: %w", pullErr)
×
375
        }
×
376
        return nil
×
377
}
378

379
// Checkout switches to specified revision (commit, tag, or branch)
380
func (s *Store) Checkout(rev string) error {
4✔
381
        s.mu.Lock()
4✔
382
        defer s.mu.Unlock()
4✔
383

4✔
384
        wt, err := s.repo.Worktree()
4✔
385
        if err != nil {
4✔
386
                return fmt.Errorf("failed to get worktree: %w", err)
×
387
        }
×
388

389
        // try to resolve as branch first
390
        branchRef := plumbing.NewBranchReferenceName(rev)
4✔
391
        if _, refErr := s.repo.Reference(branchRef, true); refErr == nil {
5✔
392
                if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: branchRef}); chkErr != nil {
1✔
393
                        return fmt.Errorf("failed to checkout branch %s: %w", rev, chkErr)
×
394
                }
×
395
                return nil
1✔
396
        }
397

398
        // try to resolve as tag
399
        tagRef := plumbing.NewTagReferenceName(rev)
3✔
400
        if _, refErr := s.repo.Reference(tagRef, true); refErr == nil {
4✔
401
                if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: tagRef}); chkErr != nil {
1✔
402
                        return fmt.Errorf("failed to checkout tag %s: %w", rev, chkErr)
×
403
                }
×
404
                return nil
1✔
405
        }
406

407
        // try to resolve as commit hash
408
        hash, resolveErr := s.repo.ResolveRevision(plumbing.Revision(rev))
2✔
409
        if resolveErr != nil {
3✔
410
                return fmt.Errorf("failed to resolve revision %s: %w", rev, resolveErr)
1✔
411
        }
1✔
412

413
        if chkErr := wt.Checkout(&git.CheckoutOptions{Hash: *hash}); chkErr != nil {
1✔
414
                return fmt.Errorf("failed to checkout commit %s: %w", rev, chkErr)
×
415
        }
×
416
        return nil
1✔
417
}
418

419
// ReadAll reads all key-value pairs from the repository with their formats.
420
// Format is extracted from the commit message metadata of the last commit that modified each file.
421
// If no format is found in the commit message, defaults to "text".
422
//
423
// Note: this function has O(n) git log queries where n is the number of files,
424
// as getFileFormat opens a git log iterator for each file. This is acceptable
425
// for restore operations which are infrequent admin commands.
426
func (s *Store) ReadAll() (map[string]KeyValue, error) {
8✔
427
        s.mu.Lock()
8✔
428
        defer s.mu.Unlock()
8✔
429

8✔
430
        result := make(map[string]KeyValue)
8✔
431

8✔
432
        walkErr := filepath.Walk(s.cfg.Path, func(path string, info os.FileInfo, err error) error {
55✔
433
                if err != nil {
47✔
434
                        return err
×
435
                }
×
436

437
                // skip directories and .git folder
438
                if info.IsDir() {
66✔
439
                        if info.Name() == ".git" {
27✔
440
                                return filepath.SkipDir
8✔
441
                        }
8✔
442
                        return nil
11✔
443
                }
444

445
                // only process .val files
446
                if !strings.HasSuffix(path, ".val") {
36✔
447
                        return nil
8✔
448
                }
8✔
449

450
                // read file content - path is validated by Walk to be within s.cfg.Path
451
                content, readErr := os.ReadFile(path) //nolint:gosec // path is validated by filepath.Walk
20✔
452
                if readErr != nil {
20✔
453
                        return fmt.Errorf("failed to read %s: %w", path, readErr)
×
454
                }
×
455

456
                // convert path back to key
457
                relPath, relErr := filepath.Rel(s.cfg.Path, path)
20✔
458
                if relErr != nil {
20✔
459
                        return fmt.Errorf("failed to get relative path: %w", relErr)
×
460
                }
×
461
                key := pathToKey(relPath)
20✔
462

20✔
463
                // get format from the last commit that modified this file
20✔
464
                format := s.getFileFormat(relPath)
20✔
465

20✔
466
                result[key] = KeyValue{Value: content, Format: format}
20✔
467

20✔
468
                return nil
20✔
469
        })
470

471
        if walkErr != nil {
8✔
472
                return nil, fmt.Errorf("failed to walk repository: %w", walkErr)
×
473
        }
×
474

475
        return result, nil
8✔
476
}
477

478
// History returns commit history for a key (newest first).
479
// limit specifies maximum number of entries to return (0 = unlimited).
480
func (s *Store) History(key string, limit int) ([]HistoryEntry, error) {
4✔
481
        if err := s.validateKey(key); err != nil {
5✔
482
                return nil, err
1✔
483
        }
1✔
484

485
        s.mu.Lock()
3✔
486
        defer s.mu.Unlock()
3✔
487

3✔
488
        filePath := keyToPath(key)
3✔
489

3✔
490
        logIter, err := s.repo.Log(&git.LogOptions{
3✔
491
                FileName: &filePath,
3✔
492
        })
3✔
493
        if err != nil {
3✔
494
                return nil, fmt.Errorf("failed to get log: %w", err)
×
495
        }
×
496
        defer logIter.Close()
3✔
497

3✔
498
        var entries []HistoryEntry
3✔
499
        count := 0
3✔
500

3✔
501
        for limit <= 0 || count < limit {
11✔
502
                commit, iterErr := logIter.Next()
8✔
503
                if iterErr != nil {
10✔
504
                        if errors.Is(iterErr, io.EOF) {
4✔
505
                                break // normal end of history
2✔
506
                        }
507
                        // log unexpected errors but continue with partial results
508
                        log.Printf("[WARN] git history iteration error for key %q: %v", key, iterErr)
×
509
                        break
×
510
                }
511

512
                // extract metadata from commit
513
                entry := HistoryEntry{
6✔
514
                        Hash:      commit.Hash.String()[:7],
6✔
515
                        Timestamp: commit.Author.When,
6✔
516
                        Author:    commit.Author.Name,
6✔
517
                        Operation: parseOperationFromCommit(commit.Message),
6✔
518
                        Format:    parseFormatFromCommit(commit.Message),
6✔
519
                }
6✔
520

6✔
521
                // get file content at this commit (may be missing for delete commits)
6✔
522
                entry.Value = s.getFileContentAtCommit(commit, filePath, key, entry.Hash)
6✔
523

6✔
524
                entries = append(entries, entry)
6✔
525
                count++
6✔
526
        }
527

528
        return entries, nil
3✔
529
}
530

531
// getFileContentAtCommit retrieves file content at a specific commit.
532
// returns nil if file doesn't exist (expected for delete commits).
533
func (s *Store) getFileContentAtCommit(commit *object.Commit, filePath, key, hash string) []byte {
6✔
534
        tree, treeErr := commit.Tree()
6✔
535
        if treeErr != nil {
6✔
536
                log.Printf("[DEBUG] git history: failed to get tree at %s for key %q: %v", hash, key, treeErr)
×
537
                return nil
×
538
        }
×
539

540
        file, fileErr := tree.File(filePath)
6✔
541
        if fileErr != nil {
6✔
542
                return nil // file not found is expected for delete commits
×
543
        }
×
544

545
        content, contentErr := file.Contents()
6✔
546
        if contentErr != nil {
6✔
547
                log.Printf("[DEBUG] git history: failed to read content at %s for key %q: %v", hash, key, contentErr)
×
548
                return nil
×
549
        }
×
550

551
        return []byte(content)
6✔
552
}
553

554
// GetRevision returns value and format at specific revision.
555
func (s *Store) GetRevision(key, rev string) ([]byte, string, error) {
5✔
556
        if err := s.validateKey(key); err != nil {
6✔
557
                return nil, "", err
1✔
558
        }
1✔
559

560
        s.mu.Lock()
4✔
561
        defer s.mu.Unlock()
4✔
562

4✔
563
        filePath := keyToPath(key)
4✔
564

4✔
565
        // resolve revision to commit hash
4✔
566
        hash, err := s.repo.ResolveRevision(plumbing.Revision(rev))
4✔
567
        if err != nil {
5✔
568
                return nil, "", fmt.Errorf("failed to resolve revision %s: %w", rev, err)
1✔
569
        }
1✔
570

571
        // get commit object
572
        commit, err := s.repo.CommitObject(*hash)
3✔
573
        if err != nil {
3✔
574
                return nil, "", fmt.Errorf("failed to get commit: %w", err)
×
575
        }
×
576

577
        // get file tree at commit
578
        tree, err := commit.Tree()
3✔
579
        if err != nil {
3✔
580
                return nil, "", fmt.Errorf("failed to get tree: %w", err)
×
581
        }
×
582

583
        // get file content
584
        file, err := tree.File(filePath)
3✔
585
        if err != nil {
4✔
586
                return nil, "", fmt.Errorf("file not found at revision %s: %w", rev, err)
1✔
587
        }
1✔
588

589
        content, err := file.Contents()
2✔
590
        if err != nil {
2✔
591
                return nil, "", fmt.Errorf("failed to read file: %w", err)
×
592
        }
×
593

594
        // get format from commit message
595
        format := parseFormatFromCommit(commit.Message)
2✔
596

2✔
597
        return []byte(content), format, nil
2✔
598
}
599

600
// getFileFormat finds the last commit that modified a file and extracts format from its message.
601
// returns "text" if no format is found.
602
func (s *Store) getFileFormat(filePath string) string {
20✔
603
        // get log for this specific file
20✔
604
        logIter, err := s.repo.Log(&git.LogOptions{
20✔
605
                FileName: &filePath,
20✔
606
        })
20✔
607
        if err != nil {
20✔
608
                return "text"
×
609
        }
×
610
        defer logIter.Close()
20✔
611

20✔
612
        // get the most recent commit for this file
20✔
613
        commit, err := logIter.Next()
20✔
614
        if err != nil {
20✔
615
                return "text"
×
616
        }
×
617

618
        return parseFormatFromCommit(commit.Message)
20✔
619
}
620

621
// parseFormatFromCommit extracts format value from commit message metadata.
622
// looks for "format: <value>" line in commit message, returns "text" if not found.
623
func parseFormatFromCommit(message string) string {
34✔
624
        for line := range strings.SplitSeq(message, "\n") {
215✔
625
                if format, found := strings.CutPrefix(line, "format: "); found {
211✔
626
                        return format
30✔
627
                }
30✔
628
        }
629
        return "text"
4✔
630
}
631

632
// parseOperationFromCommit extracts operation from commit message metadata.
633
// looks for "operation: <value>" line, or parses first word of commit message.
634
func parseOperationFromCommit(message string) string {
11✔
635
        for line := range strings.SplitSeq(message, "\n") {
48✔
636
                if op, found := strings.CutPrefix(line, "operation: "); found {
45✔
637
                        return op
8✔
638
                }
8✔
639
        }
640
        // fallback: first word of commit message (e.g., "set", "delete")
641
        if parts := strings.Fields(message); len(parts) > 0 {
5✔
642
                return parts[0]
2✔
643
        }
2✔
644
        return "unknown"
1✔
645
}
646

647
// keyToPath converts a key to a file path with .val suffix
648
// e.g., "app/config/db" -> "app/config/db.val"
649
func keyToPath(key string) string {
131✔
650
        return key + ".val"
131✔
651
}
131✔
652

653
// validateKey checks if the key is safe (no path traversal).
654
// returns error if key would escape the repository directory.
655
func (s *Store) validateKey(key string) error {
76✔
656
        // reject empty keys
76✔
657
        if key == "" {
78✔
658
                return errors.New("invalid key: empty key not allowed")
2✔
659
        }
2✔
660

661
        // reject absolute paths
662
        if strings.HasPrefix(key, "/") {
76✔
663
                return errors.New("invalid key: absolute path not allowed")
2✔
664
        }
2✔
665

666
        // reject path traversal sequences
667
        if strings.Contains(key, "..") {
80✔
668
                return errors.New("invalid key: path traversal not allowed")
8✔
669
        }
8✔
670

671
        // double-check: resolved path must be within repo
672
        filePath := filepath.Join(s.cfg.Path, keyToPath(key))
64✔
673
        absPath, err := filepath.Abs(filePath)
64✔
674
        if err != nil {
64✔
675
                return errors.New("invalid key: failed to resolve path")
×
676
        }
×
677
        absBase, err := filepath.Abs(s.cfg.Path)
64✔
678
        if err != nil {
64✔
679
                return errors.New("invalid key: failed to resolve base path")
×
680
        }
×
681

682
        if !strings.HasPrefix(absPath, absBase+string(filepath.Separator)) {
64✔
683
                return errors.New("invalid key: path escapes repository")
×
684
        }
×
685

686
        return nil
64✔
687
}
688

689
// pathToKey converts a file path back to a key
690
// e.g., "app/config/db.val" -> "app/config/db"
691
func pathToKey(path string) string {
23✔
692
        return strings.TrimSuffix(path, ".val")
23✔
693
}
23✔
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

© 2026 Coveralls, Inc