detached ece7682

migration.go RawHistoryFolder

1package persist
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"io"
8	"os"
9	"path/filepath"
10	"sort"
11
12	"github.com/bnema/vev/internal/domain"
13)
14
15const legacyCatalogueBackupSuffix = ".pre-v6.bak"
16
17// Migration describes an automatic, lossless catalogue format migration.
18type Migration struct {
19	Performed     bool
20	SourceFormats []uint16
21	TargetFormat  uint16
22	RecordCount   int
23	BackupPath    string
24}
25
26func (p *Persister) migrateLegacyRecords(path string, records []domain.CatalogueRecord) (Migration, error) {
27	if p == nil || p.store == nil {
28		return Migration{}, errPersistenceUnavailable
29	}
30	formats, err := p.recordFormats()
31	if err != nil {
32		return Migration{}, err
33	}
34	legacy := false
35	for _, format := range formats {
36		if format != catalogueRecordVersion {
37			legacy = true
38			break
39		}
40	}
41	if !legacy {
42		return Migration{}, nil
43	}
44
45	raw, err := os.ReadFile(path)
46	if err != nil {
47		return Migration{}, fmt.Errorf("read catalogue backup source: %w", err)
48	}
49	backupPath := path + legacyCatalogueBackupSuffix
50	if err := createMatchingBackup(backupPath, raw); err != nil {
51		return Migration{}, err
52	}
53
54	updates := make(map[string]*domain.CatalogueRecord, len(records))
55	for i := range records {
56		updates[records[i].Name] = &records[i]
57	}
58	if err := p.Apply(updates); err != nil {
59		return Migration{}, fmt.Errorf("write migrated catalogue: %w", err)
60	}
61	return Migration{
62		Performed:     true,
63		SourceFormats: formats,
64		TargetFormat:  catalogueRecordVersion,
65		RecordCount:   len(records),
66		BackupPath:    backupPath,
67	}, nil
68}
69
70func (p *Persister) recordFormats() ([]uint16, error) {
71	formats := make(map[uint16]struct{})
72	var decodeErr error
73	p.store.Range(func(key, value []byte) bool {
74		stored, err := decodeStoredRecordValue(string(key), value)
75		if err != nil {
76			decodeErr = err
77			return false
78		}
79		formats[stored.formatVersion] = struct{}{}
80		return true
81	})
82	if decodeErr != nil {
83		return nil, decodeErr
84	}
85	out := make([]uint16, 0, len(formats))
86	for format := range formats {
87		out = append(out, format)
88	}
89	sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
90	return out, nil
91}
92
93func createMatchingBackup(path string, data []byte) error {
94	matched, err := existingBackupMatches(path, data)
95	if err != nil || matched {
96		return err
97	}
98
99	file, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".tmp-*")
100	if err != nil {
101		return fmt.Errorf("create catalogue backup temporary file: %w", err)
102	}
103	tmp := file.Name()
104	cleanup := func(err error) error {
105		return errors.Join(err, file.Close(), os.Remove(tmp))
106	}
107	if err := file.Chmod(0o600); err != nil {
108		return cleanup(fmt.Errorf("set catalogue backup permissions: %w", err))
109	}
110	if err := writeBackup(file, data); err != nil {
111		return cleanup(fmt.Errorf("write catalogue backup: %w", err))
112	}
113	if err := file.Sync(); err != nil {
114		return cleanup(fmt.Errorf("sync catalogue backup: %w", err))
115	}
116	if err := file.Close(); err != nil {
117		_ = os.Remove(tmp)
118		return fmt.Errorf("close catalogue backup: %w", err)
119	}
120	if err := os.Link(tmp, path); err != nil {
121		_ = os.Remove(tmp)
122		if errors.Is(err, os.ErrExist) {
123			matched, matchErr := existingBackupMatches(path, data)
124			if matchErr != nil {
125				return matchErr
126			}
127			if !matched {
128				return fmt.Errorf("persist: catalogue backup %q disappeared during migration", path)
129			}
130		} else {
131			return fmt.Errorf("publish catalogue backup: %w", err)
132		}
133	} else if err := os.Remove(tmp); err != nil {
134		return fmt.Errorf("remove catalogue backup temporary file: %w", err)
135	}
136	dir, err := os.Open(filepath.Dir(path))
137	if err != nil {
138		return fmt.Errorf("open catalogue backup directory: %w", err)
139	}
140	defer dir.Close()
141	if err := dir.Sync(); err != nil {
142		return fmt.Errorf("sync catalogue backup directory: %w", err)
143	}
144	return nil
145}
146
147func existingBackupMatches(path string, data []byte) (bool, error) {
148	info, err := os.Lstat(path)
149	if errors.Is(err, os.ErrNotExist) {
150		return false, nil
151	}
152	if err != nil {
153		return false, fmt.Errorf("stat existing catalogue backup: %w", err)
154	}
155	if !info.Mode().IsRegular() || info.Mode().Perm() != 0o600 {
156		return false, fmt.Errorf("persist: catalogue backup %q is not a private regular file", path)
157	}
158	existing, err := os.ReadFile(path)
159	if err != nil {
160		return false, fmt.Errorf("read existing catalogue backup: %w", err)
161	}
162	if !bytes.Equal(existing, data) {
163		return false, fmt.Errorf("persist: catalogue backup %q does not match migration source", path)
164	}
165	return true, nil
166}
167
168func writeBackup(file *os.File, data []byte) error {
169	for len(data) > 0 {
170		n, err := file.Write(data)
171		if err != nil {
172			return err
173		}
174		if n == 0 {
175			return io.ErrShortWrite
176		}
177		data = data[n:]
178	}
179	return nil
180}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close