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

umputun / stash / 19720393988

26 Nov 2025 11:33PM UTC coverage: 82.976% (+0.8%) from 82.193%
19720393988

push

github

umputun
refactor(server): extract identity detection and cleanup

- extract getIdentity() helper to deduplicate auth code in handlers
- remove unused return value from setupLogs()
- add tests for git.Head() method

19 of 23 new or added lines in 2 files covered. (82.61%)

93 existing lines in 2 files now uncovered.

1857 of 2238 relevant lines covered (82.98%)

21.56 hits per line

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

70.25
/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
        "os"
10
        "path/filepath"
11
        "strings"
12
        "time"
13

14
        "github.com/go-git/go-git/v5"
15
        "github.com/go-git/go-git/v5/config"
16
        "github.com/go-git/go-git/v5/plumbing"
17
        "github.com/go-git/go-git/v5/plumbing/object"
18
        "github.com/go-git/go-git/v5/plumbing/transport"
19
        "github.com/go-git/go-git/v5/plumbing/transport/ssh"
20
)
21

22
// Author represents the author of a git commit.
23
type Author struct {
24
        Name  string
25
        Email string
26
}
27

28
// DefaultAuthor returns the default author for git commits.
29
func DefaultAuthor() Author {
35✔
30
        return Author{Name: "stash", Email: "stash@localhost"}
35✔
31
}
35✔
32

33
// KeyValue holds a key's value and format metadata.
34
type KeyValue struct {
35
        Value  []byte
36
        Format string
37
}
38

39
// CommitRequest holds parameters for a git commit operation.
40
type CommitRequest struct {
41
        Key       string
42
        Value     []byte
43
        Operation string
44
        Format    string
45
        Author    Author
46
}
47

48
// Config holds git repository configuration
49
type Config struct {
50
        Path   string // local repository path
51
        Branch string // branch name (default: master)
52
        Remote string // remote name (optional, for push/pull)
53
        SSHKey string // path to SSH private key (optional, for push)
54
}
55

56
// Store provides git-backed versioning for key-value storage
57
type Store struct {
58
        cfg  Config
59
        repo *git.Repository
60
}
61

62
// New creates a new git store, initializing or opening the repository
63
func New(cfg Config) (*Store, error) {
32✔
64
        if cfg.Path == "" {
33✔
65
                return nil, fmt.Errorf("git path is required")
1✔
66
        }
1✔
67
        if cfg.Branch == "" {
56✔
68
                cfg.Branch = "master"
25✔
69
        }
25✔
70

71
        s := &Store{cfg: cfg}
31✔
72
        if err := s.initRepo(); err != nil {
31✔
UNCOV
73
                return nil, fmt.Errorf("failed to init git repo: %w", err)
×
UNCOV
74
        }
×
75
        return s, nil
31✔
76
}
77

78
// initRepo opens existing or creates new git repository
79
func (s *Store) initRepo() error {
31✔
80
        // try to open existing repo
31✔
81
        repo, err := git.PlainOpen(s.cfg.Path)
31✔
82
        if err == nil {
33✔
83
                s.repo = repo
2✔
84
                return s.ensureBranch()
2✔
85
        }
2✔
86

87
        // create new repo if not exists
88
        if errors.Is(err, git.ErrRepositoryNotExists) {
58✔
89
                return s.createNewRepo()
29✔
90
        }
29✔
91

UNCOV
92
        return fmt.Errorf("failed to open repo: %w", err)
×
93
}
94

95
// ensureBranch checks out the configured branch, creating it if necessary
96
func (s *Store) ensureBranch() error {
2✔
97
        wt, err := s.repo.Worktree()
2✔
98
        if err != nil {
2✔
UNCOV
99
                return fmt.Errorf("failed to get worktree: %w", err)
×
UNCOV
100
        }
×
101

102
        branchRef := plumbing.NewBranchReferenceName(s.cfg.Branch)
2✔
103

2✔
104
        // try to checkout existing branch
2✔
105
        if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: branchRef}); chkErr == nil {
3✔
106
                return nil
1✔
107
        }
1✔
108

109
        // branch doesn't exist, create it from HEAD
110
        head, headErr := s.repo.Head()
1✔
111
        if headErr != nil {
1✔
UNCOV
112
                return fmt.Errorf("failed to get HEAD: %w", headErr)
×
UNCOV
113
        }
×
114

115
        // create and checkout the branch
116
        if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: branchRef, Hash: head.Hash(), Create: true}); chkErr != nil {
1✔
117
                return fmt.Errorf("failed to checkout branch %s: %w", s.cfg.Branch, chkErr)
×
118
        }
×
119
        return nil
1✔
120
}
121

122
func (s *Store) createNewRepo() error {
29✔
123
        repo, err := git.PlainInit(s.cfg.Path, false)
29✔
124
        if err != nil {
29✔
UNCOV
125
                return fmt.Errorf("failed to init repo: %w", err)
×
126
        }
×
127
        s.repo = repo
29✔
128

29✔
129
        // create initial commit on configured branch
29✔
130
        wt, wtErr := repo.Worktree()
29✔
131
        if wtErr != nil {
29✔
UNCOV
132
                return fmt.Errorf("failed to get worktree: %w", wtErr)
×
UNCOV
133
        }
×
134

135
        // create .gitkeep to have something to commit
136
        gitkeep := filepath.Join(s.cfg.Path, ".gitkeep")
29✔
137
        if writeErr := os.WriteFile(gitkeep, []byte{}, 0o600); writeErr != nil {
29✔
138
                return fmt.Errorf("failed to create .gitkeep: %w", writeErr)
×
UNCOV
139
        }
×
140
        if _, addErr := wt.Add(".gitkeep"); addErr != nil {
29✔
UNCOV
141
                return fmt.Errorf("failed to stage .gitkeep: %w", addErr)
×
UNCOV
142
        }
×
143

144
        _, commitErr := wt.Commit("initial commit", &git.CommitOptions{
29✔
145
                Author: &object.Signature{
29✔
146
                        Name:  "stash",
29✔
147
                        Email: "stash@localhost",
29✔
148
                        When:  time.Now(),
29✔
149
                },
29✔
150
        })
29✔
151
        if commitErr != nil {
29✔
152
                return fmt.Errorf("failed to create initial commit: %w", commitErr)
×
153
        }
×
154

155
        // checkout configured branch (create if not master)
156
        if s.cfg.Branch != "master" {
30✔
157
                head, headErr := repo.Head()
1✔
158
                if headErr != nil {
1✔
UNCOV
159
                        return fmt.Errorf("failed to get HEAD: %w", headErr)
×
UNCOV
160
                }
×
161
                branchRef := plumbing.NewBranchReferenceName(s.cfg.Branch)
1✔
162
                if chkErr := wt.Checkout(&git.CheckoutOptions{
1✔
163
                        Branch: branchRef,
1✔
164
                        Hash:   head.Hash(),
1✔
165
                        Create: true,
1✔
166
                }); chkErr != nil {
1✔
UNCOV
167
                        return fmt.Errorf("failed to checkout branch %s: %w", s.cfg.Branch, chkErr)
×
UNCOV
168
                }
×
169
        }
170

171
        return nil
29✔
172
}
173

174
// Commit writes key-value to file and commits to git.
175
func (s *Store) Commit(req CommitRequest) error {
29✔
176
        // validate key before any file operations
29✔
177
        if err := s.validateKey(req.Key); err != nil {
35✔
178
                return err
6✔
179
        }
6✔
180

181
        // default format to text
182
        format := req.Format
23✔
183
        if format == "" {
42✔
184
                format = "text"
19✔
185
        }
19✔
186

187
        // convert key to file path with .val suffix
188
        filePath := keyToPath(req.Key)
23✔
189
        fullPath := filepath.Join(s.cfg.Path, filePath)
23✔
190

23✔
191
        // ensure parent directory exists
23✔
192
        if err := os.MkdirAll(filepath.Dir(fullPath), 0o750); err != nil {
23✔
UNCOV
193
                return fmt.Errorf("failed to create directory: %w", err)
×
UNCOV
194
        }
×
195

196
        // write file
197
        if err := os.WriteFile(fullPath, req.Value, 0o600); err != nil {
23✔
UNCOV
198
                return fmt.Errorf("failed to write file: %w", err)
×
UNCOV
199
        }
×
200

201
        // stage file
202
        wt, err := s.repo.Worktree()
23✔
203
        if err != nil {
23✔
204
                return fmt.Errorf("failed to get worktree: %w", err)
×
UNCOV
205
        }
×
206

207
        if _, addErr := wt.Add(filePath); addErr != nil {
23✔
UNCOV
208
                return fmt.Errorf("failed to stage file: %w", addErr)
×
UNCOV
209
        }
×
210

211
        // commit with metadata including format
212
        msg := fmt.Sprintf("%s %s\n\ntimestamp: %s\noperation: %s\nkey: %s\nformat: %s",
23✔
213
                req.Operation, req.Key, time.Now().Format(time.RFC3339), req.Operation, req.Key, format)
23✔
214

23✔
215
        _, commitErr := wt.Commit(msg, &git.CommitOptions{
23✔
216
                Author: &object.Signature{
23✔
217
                        Name:  req.Author.Name,
23✔
218
                        Email: req.Author.Email,
23✔
219
                        When:  time.Now(),
23✔
220
                },
23✔
221
        })
23✔
222
        if commitErr != nil {
23✔
UNCOV
223
                return fmt.Errorf("failed to commit: %w", commitErr)
×
UNCOV
224
        }
×
225

226
        return nil
23✔
227
}
228

229
// Delete removes key file and commits the deletion.
230
// The author parameter specifies who made the change.
231
func (s *Store) Delete(key string, author Author) error {
6✔
232
        // validate key before any file operations
6✔
233
        if err := s.validateKey(key); err != nil {
10✔
234
                return err
4✔
235
        }
4✔
236

237
        filePath := keyToPath(key)
2✔
238
        fullPath := filepath.Join(s.cfg.Path, filePath)
2✔
239

2✔
240
        // check if file exists
2✔
241
        if _, err := os.Stat(fullPath); os.IsNotExist(err) {
3✔
242
                return nil // nothing to delete
1✔
243
        }
1✔
244

245
        // remove file
246
        if err := os.Remove(fullPath); err != nil {
1✔
UNCOV
247
                return fmt.Errorf("failed to remove file: %w", err)
×
UNCOV
248
        }
×
249

250
        // stage deletion
251
        wt, err := s.repo.Worktree()
1✔
252
        if err != nil {
1✔
UNCOV
253
                return fmt.Errorf("failed to get worktree: %w", err)
×
UNCOV
254
        }
×
255

256
        if _, rmErr := wt.Remove(filePath); rmErr != nil {
1✔
UNCOV
257
                return fmt.Errorf("failed to stage deletion: %w", rmErr)
×
UNCOV
258
        }
×
259

260
        // commit deletion
261
        msg := fmt.Sprintf("delete %s\n\ntimestamp: %s\noperation: delete\nkey: %s", key, time.Now().Format(time.RFC3339), key)
1✔
262
        _, commitErr := wt.Commit(msg, &git.CommitOptions{
1✔
263
                Author: &object.Signature{
1✔
264
                        Name:  author.Name,
1✔
265
                        Email: author.Email,
1✔
266
                        When:  time.Now(),
1✔
267
                },
1✔
268
        })
1✔
269
        if commitErr != nil {
1✔
UNCOV
270
                return fmt.Errorf("failed to commit deletion: %w", commitErr)
×
271
        }
×
272

273
        return nil
1✔
274
}
275

276
// Push pushes commits to remote repository
277
func (s *Store) Push() error {
3✔
278
        if s.cfg.Remote == "" {
5✔
279
                return nil // no remote configured
2✔
280
        }
2✔
281

282
        var auth transport.AuthMethod
1✔
283
        if s.cfg.SSHKey != "" {
2✔
284
                var err error
1✔
285
                auth, err = ssh.NewPublicKeysFromFile("git", s.cfg.SSHKey, "")
1✔
286
                if err != nil {
2✔
287
                        return fmt.Errorf("failed to load SSH key: %w", err)
1✔
288
                }
1✔
289
        }
290

UNCOV
291
        err := s.repo.Push(&git.PushOptions{
×
UNCOV
292
                RemoteName: s.cfg.Remote,
×
UNCOV
293
                Auth:       auth,
×
UNCOV
294
                RefSpecs: []config.RefSpec{
×
UNCOV
295
                        config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", s.cfg.Branch, s.cfg.Branch)),
×
UNCOV
296
                },
×
UNCOV
297
        })
×
UNCOV
298
        if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
×
UNCOV
299
                return fmt.Errorf("failed to push: %w", err)
×
UNCOV
300
        }
×
UNCOV
301
        return nil
×
302
}
303

304
// Head returns the current HEAD commit hash as a short string
305
func (s *Store) Head() (string, error) {
3✔
306
        ref, err := s.repo.Head()
3✔
307
        if err != nil {
3✔
308
                return "", fmt.Errorf("failed to get HEAD: %w", err)
×
309
        }
×
310
        return ref.Hash().String()[:7], nil
3✔
311
}
312

313
// Pull fetches and merges from remote repository
314
func (s *Store) Pull() error {
3✔
315
        if s.cfg.Remote == "" {
5✔
316
                return nil // no remote configured
2✔
317
        }
2✔
318

319
        var auth transport.AuthMethod
1✔
320
        if s.cfg.SSHKey != "" {
2✔
321
                var err error
1✔
322
                auth, err = ssh.NewPublicKeysFromFile("git", s.cfg.SSHKey, "")
1✔
323
                if err != nil {
2✔
324
                        return fmt.Errorf("failed to load SSH key: %w", err)
1✔
325
                }
1✔
326
        }
327

328
        wt, err := s.repo.Worktree()
×
329
        if err != nil {
×
UNCOV
330
                return fmt.Errorf("failed to get worktree: %w", err)
×
UNCOV
331
        }
×
332

UNCOV
333
        pullErr := wt.Pull(&git.PullOptions{
×
334
                RemoteName:    s.cfg.Remote,
×
335
                Auth:          auth,
×
336
                ReferenceName: plumbing.NewBranchReferenceName(s.cfg.Branch),
×
337
        })
×
UNCOV
338
        if pullErr != nil && !errors.Is(pullErr, git.NoErrAlreadyUpToDate) {
×
UNCOV
339
                return fmt.Errorf("failed to pull: %w", pullErr)
×
UNCOV
340
        }
×
UNCOV
341
        return nil
×
342
}
343

344
// Checkout switches to specified revision (commit, tag, or branch)
345
func (s *Store) Checkout(rev string) error {
2✔
346
        wt, err := s.repo.Worktree()
2✔
347
        if err != nil {
2✔
UNCOV
348
                return fmt.Errorf("failed to get worktree: %w", err)
×
UNCOV
349
        }
×
350

351
        // try to resolve as branch first
352
        branchRef := plumbing.NewBranchReferenceName(rev)
2✔
353
        if _, refErr := s.repo.Reference(branchRef, true); refErr == nil {
2✔
UNCOV
354
                if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: branchRef}); chkErr != nil {
×
UNCOV
355
                        return fmt.Errorf("failed to checkout branch %s: %w", rev, chkErr)
×
356
                }
×
357
                return nil
×
358
        }
359

360
        // try to resolve as tag
361
        tagRef := plumbing.NewTagReferenceName(rev)
2✔
362
        if _, refErr := s.repo.Reference(tagRef, true); refErr == nil {
2✔
UNCOV
363
                if chkErr := wt.Checkout(&git.CheckoutOptions{Branch: tagRef}); chkErr != nil {
×
UNCOV
364
                        return fmt.Errorf("failed to checkout tag %s: %w", rev, chkErr)
×
UNCOV
365
                }
×
UNCOV
366
                return nil
×
367
        }
368

369
        // try to resolve as commit hash
370
        hash, resolveErr := s.repo.ResolveRevision(plumbing.Revision(rev))
2✔
371
        if resolveErr != nil {
3✔
372
                return fmt.Errorf("failed to resolve revision %s: %w", rev, resolveErr)
1✔
373
        }
1✔
374

375
        if chkErr := wt.Checkout(&git.CheckoutOptions{Hash: *hash}); chkErr != nil {
1✔
UNCOV
376
                return fmt.Errorf("failed to checkout commit %s: %w", rev, chkErr)
×
UNCOV
377
        }
×
378
        return nil
1✔
379
}
380

381
// ReadAll reads all key-value pairs from the repository with their formats.
382
// Format is extracted from the commit message metadata of the last commit that modified each file.
383
// If no format is found in the commit message, defaults to "text".
384
func (s *Store) ReadAll() (map[string]KeyValue, error) {
5✔
385
        result := make(map[string]KeyValue)
5✔
386

5✔
387
        walkErr := filepath.Walk(s.cfg.Path, func(path string, info os.FileInfo, err error) error {
31✔
388
                if err != nil {
26✔
UNCOV
389
                        return err
×
UNCOV
390
                }
×
391

392
                // skip directories and .git folder
393
                if info.IsDir() {
39✔
394
                        if info.Name() == ".git" {
18✔
395
                                return filepath.SkipDir
5✔
396
                        }
5✔
397
                        return nil
8✔
398
                }
399

400
                // only process .val files
401
                if !strings.HasSuffix(path, ".val") {
18✔
402
                        return nil
5✔
403
                }
5✔
404

405
                // read file content - path is validated by Walk to be within s.cfg.Path
406
                content, readErr := os.ReadFile(path) //nolint:gosec // path is validated by filepath.Walk
8✔
407
                if readErr != nil {
8✔
UNCOV
408
                        return fmt.Errorf("failed to read %s: %w", path, readErr)
×
UNCOV
409
                }
×
410

411
                // convert path back to key
412
                relPath, relErr := filepath.Rel(s.cfg.Path, path)
8✔
413
                if relErr != nil {
8✔
UNCOV
414
                        return fmt.Errorf("failed to get relative path: %w", relErr)
×
UNCOV
415
                }
×
416
                key := pathToKey(relPath)
8✔
417

8✔
418
                // get format from the last commit that modified this file
8✔
419
                format := s.getFileFormat(relPath)
8✔
420

8✔
421
                result[key] = KeyValue{Value: content, Format: format}
8✔
422

8✔
423
                return nil
8✔
424
        })
425

426
        if walkErr != nil {
5✔
UNCOV
427
                return nil, fmt.Errorf("failed to walk repository: %w", walkErr)
×
UNCOV
428
        }
×
429

430
        return result, nil
5✔
431
}
432

433
// getFileFormat finds the last commit that modified a file and extracts format from its message.
434
// returns "text" if no format is found.
435
func (s *Store) getFileFormat(filePath string) string {
8✔
436
        // get log for this specific file
8✔
437
        logIter, err := s.repo.Log(&git.LogOptions{
8✔
438
                FileName: &filePath,
8✔
439
        })
8✔
440
        if err != nil {
8✔
UNCOV
441
                return "text"
×
UNCOV
442
        }
×
443
        defer logIter.Close()
8✔
444

8✔
445
        // get the most recent commit for this file
8✔
446
        commit, err := logIter.Next()
8✔
447
        if err != nil {
8✔
UNCOV
448
                return "text"
×
UNCOV
449
        }
×
450

451
        return parseFormatFromCommit(commit.Message)
8✔
452
}
453

454
// parseFormatFromCommit extracts format value from commit message metadata.
455
// looks for "format: <value>" line in commit message, returns "text" if not found.
456
func parseFormatFromCommit(message string) string {
8✔
457
        for _, line := range strings.Split(message, "\n") {
53✔
458
                if strings.HasPrefix(line, "format: ") {
52✔
459
                        return strings.TrimPrefix(line, "format: ")
7✔
460
                }
7✔
461
        }
462
        return "text"
1✔
463
}
464

465
// keyToPath converts a key to a file path with .val suffix
466
// e.g., "app/config/db" -> "app/config/db.val"
467
func keyToPath(key string) string {
53✔
468
        return key + ".val"
53✔
469
}
53✔
470

471
// validateKey checks if the key is safe (no path traversal).
472
// returns error if key would escape the repository directory.
473
func (s *Store) validateKey(key string) error {
35✔
474
        // reject empty keys
35✔
475
        if key == "" {
37✔
476
                return fmt.Errorf("invalid key: empty key not allowed")
2✔
477
        }
2✔
478

479
        // reject absolute paths
480
        if strings.HasPrefix(key, "/") {
35✔
481
                return fmt.Errorf("invalid key: absolute path not allowed")
2✔
482
        }
2✔
483

484
        // reject path traversal sequences
485
        if strings.Contains(key, "..") {
37✔
486
                return fmt.Errorf("invalid key: path traversal not allowed")
6✔
487
        }
6✔
488

489
        // double-check: resolved path must be within repo
490
        filePath := filepath.Join(s.cfg.Path, keyToPath(key))
25✔
491
        absPath, err := filepath.Abs(filePath)
25✔
492
        if err != nil {
25✔
UNCOV
493
                return fmt.Errorf("invalid key: failed to resolve path")
×
UNCOV
494
        }
×
495
        absBase, err := filepath.Abs(s.cfg.Path)
25✔
496
        if err != nil {
25✔
UNCOV
497
                return fmt.Errorf("invalid key: failed to resolve base path")
×
UNCOV
498
        }
×
499

500
        if !strings.HasPrefix(absPath, absBase+string(filepath.Separator)) {
25✔
UNCOV
501
                return fmt.Errorf("invalid key: path escapes repository")
×
UNCOV
502
        }
×
503

504
        return nil
25✔
505
}
506

507
// pathToKey converts a file path back to a key
508
// e.g., "app/config/db.val" -> "app/config/db"
509
func pathToKey(path string) string {
11✔
510
        return strings.TrimSuffix(path, ".val")
11✔
511
}
11✔
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