issue_history_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2package kvstore_test
3
4import (
5	"context"
6	"fmt"
7	"testing"
8	"time"
9
10	storagepb "bnema.dev/ermite/gen/ermite/storage"
11	"bnema.dev/ermite/internal/adapters/kvstore"
12	"bnema.dev/ermite/internal/domain"
13	"bnema.dev/ermite/internal/ports"
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/require"
16)
17
18func TestIssueHistoryAtomicAndCapped(t *testing.T) {
19	ctx := context.Background()
20	s, _, repo := repoWithMRs(t)
21	is := s.Issues()
22	issue, err := is.Create(ctx, newIssue(t, repo, "history"))
23	require.NoError(t, err)
24	stale := issue
25	issue.State = domain.IssueStateResolved
26	ev := domain.IssueEvent{Kind: domain.IssueEventResolved, Actor: "merger", MergeRequest: 9, CreatedAt: created}
27	require.NoError(t, is.Update(ctx, issue, ev, domain.IssueEvent{Kind: domain.IssueEventReopened, CreatedAt: created.Add(time.Second)}))
28	assert.ErrorIs(t, is.Update(ctx, stale, ev), domain.ErrConflict)
29	events, err := is.ListEvents(ctx, repo, issue.Number)
30	require.NoError(t, err)
31	require.Len(t, events, 2)
32	assert.Equal(t, int64(1), events[0].ID)
33	assert.Equal(t, int64(2), events[1].ID)
34	assert.Equal(t, issue.Number, events[0].Issue)
35	assert.Equal(t, "merger", events[0].Actor)
36	open, err := is.ListOpenAll(ctx)
37	require.NoError(t, err)
38	assert.Empty(t, open)
39	page, err := is.List(ctx, repo, ports.ListIssuesParams{State: domain.IssueStateResolved})
40	require.NoError(t, err)
41	require.Len(t, page.Issues, 1)
42	issue = page.Issues[0]
43	batch := make([]domain.IssueEvent, kvstore.MaxIssueEvents)
44	for j := range batch {
45		batch[j] = ev
46	}
47	require.NoError(t, is.Update(ctx, issue, batch...))
48	issue, err = is.Get(ctx, repo, issue.Number)
49	require.NoError(t, err)
50	issue.State = domain.IssueStateOpen
51	require.NoError(t, is.Update(ctx, issue, ev))
52	events, err = is.ListEvents(ctx, repo, issue.Number)
53	require.NoError(t, err)
54	require.Len(t, events, kvstore.MaxIssueEvents)
55	for j, e := range events {
56		assert.Equal(t, int64(j+1), e.ID)
57	}
58	issue, err = is.Get(ctx, repo, issue.Number)
59	require.NoError(t, err)
60	assert.Equal(t, domain.IssueStateOpen, issue.State)
61	require.NoError(t, s.Delete(ctx, repo))
62	for _, prefix := range []string{"is/", "isc/", "isopen/", "isev/", "mrev/"} {
63		assert.Zero(t, countKeys(t, s, prefix+repo.String()+"/"))
64	}
65}
66
67func TestReferenceHistoryDiff(t *testing.T) {
68	ctx := context.Background()
69	s, _, repo := repoWithMRs(t)
70	links := s.IssueLinks()
71	ref := ports.ReferenceEvent{Actor: "writer", At: created}
72	replace := func(issues, closes []int64) {
73		t.Helper()
74		_, _, err := links.ReplaceLinks(ctx, repo, 9, issues, closes, ref)
75		require.NoError(t, err)
76	}
77	replace([]int64{3}, nil)
78	replace([]int64{3}, nil)
79	ref.At = created.Add(time.Second)
80	replace([]int64{3}, []int64{3})
81	replace([]int64{3}, []int64{3})
82	replace([]int64{3}, nil) // downgrade has no event
83	replace(nil, nil)        // removal retains history
84	events, err := s.Issues().ListEvents(ctx, repo, 3)
85	require.NoError(t, err)
86	require.Len(t, events, 2)
87	assert.False(t, events[0].Closes)
88	assert.True(t, events[1].Closes)
89	assert.Equal(t, "writer", events[0].Actor)
90	assert.Equal(t, domain.IssueEventReferenced, events[0].Kind)
91	mrEvents, err := links.EventsOfMergeRequest(ctx, repo, 9)
92	require.NoError(t, err)
93	assert.Equal(t, events, mrEvents)
94	replace([]int64{3}, nil) // reappearance is another event
95	events, err = s.Issues().ListEvents(ctx, repo, 3)
96	require.NoError(t, err)
97	require.Len(t, events, 3)
98	assert.Equal(t, int64(3), events[2].ID)
99	require.NoError(t, s.Delete(ctx, repo))
100	assert.Zero(t, countKeys(t, s, "isev/"+repo.String()+"/"))
101	assert.Zero(t, countKeys(t, s, "mrev/"+repo.String()+"/"))
102}
103
104func TestMRHistoryOrderAndCap(t *testing.T) {
105	ctx := context.Background()
106	s, _, repo := repoWithMRs(t)
107	// Insert in reverse timestamp order; issue and seq break timestamp ties.
108	for n := int64(1); n <= 3; n++ {
109		issue, err := s.Issues().Create(ctx, newIssue(t, repo, "many"))
110		require.NoError(t, err)
111		events := make([]domain.IssueEvent, 201)
112		for j := range events {
113			events[j] = domain.IssueEvent{Kind: domain.IssueEventResolved, MergeRequest: 9, CreatedAt: created.Add(time.Duration(200-j) * time.Second)}
114		}
115		require.NoError(t, s.Issues().Update(ctx, issue, events...))
116	}
117	events, err := s.IssueLinks().EventsOfMergeRequest(ctx, repo, 9)
118	require.NoError(t, err)
119	require.Len(t, events, 500)
120	for j := 1; j < len(events); j++ {
121		a, b := events[j-1], events[j]
122		assert.False(t, a.CreatedAt.After(b.CreatedAt))
123		if a.CreatedAt.Equal(b.CreatedAt) {
124			assert.Less(t, a.Issue, b.Issue)
125		}
126	}
127}
128
129func TestResolvedMigrationAndRestart(t *testing.T) {
130	ctx := context.Background()
131	for _, version := range []uint32{5, 7} {
132		t.Run(fmt.Sprint(version), func(t *testing.T) {
133			dir := t.TempDir()
134			putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: version}))
135			for n := int64(1); n <= 3; n++ {
136				state := storagepb.IssueState_ISSUE_STATE_CLOSED
137				mr := int64(0)
138				if n == 1 {
139					mr = 9
140				}
141				if n == 3 {
142					state = storagepb.IssueState_ISSUE_STATE_OPEN
143				}
144				putRaw(t, dir, fmt.Sprintf("is/proj/%010d", n), mustProto(t, &storagepb.Issue{Number: n, Title: "old", State: state, ClosedByMergeRequest: mr}))
145			}
146			putRaw(t, dir, "isopen/proj/0000000003", []byte{1})
147			repo := name(t, "proj")
148			var migratedVersion uint64
149			for pass := 0; pass < 2; pass++ {
150				s, err := kvstore.Open(ctx, dir)
151				require.NoError(t, err)
152				for n, state := range []domain.IssueState{domain.IssueStateResolved, domain.IssueStateClosed, domain.IssueStateOpen} {
153					issue, err := s.Issues().Get(ctx, repo, int64(n+1))
154					require.NoError(t, err)
155					assert.Equal(t, state, issue.State)
156					assert.True(t, issue.UpdatedAt.IsZero())
157					if n == 0 {
158						assert.Equal(t, int64(9), issue.ClosedByMergeRequest)
159						if pass == 0 {
160							migratedVersion = issue.Version
161						} else {
162							assert.Equal(t, migratedVersion, issue.Version)
163						}
164					}
165				}
166				assert.Equal(t, 1, countKeys(t, s, "isopen/proj/"))
167				assert.Zero(t, countKeys(t, s, "isev/"))
168				assert.Zero(t, countKeys(t, s, "mrev/"))
169				require.NoError(t, s.Close(ctx))
170				assert.Equal(t, uint32(8), readSchema(t, dir))
171			}
172		})
173	}
174}
175
176func TestIssueEventSequenceSurvivesRestart(t *testing.T) {
177	ctx := context.Background()
178	dir := t.TempDir()
179	repo := name(t, "proj")
180	for pass := 0; pass < 2; pass++ {
181		s, err := kvstore.Open(ctx, dir)
182		require.NoError(t, err)
183		if pass == 0 {
184			require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPrivate}))
185			_, err := s.Issues().Create(ctx, newIssue(t, repo, "restart"))
186			require.NoError(t, err)
187		}
188		issue, err := s.Issues().Get(ctx, repo, 1)
189		require.NoError(t, err)
190		require.NoError(t, s.Issues().Update(ctx, issue, domain.IssueEvent{Kind: domain.IssueEventClosed, MergeRequest: 9, CreatedAt: created}))
191		events, err := s.IssueLinks().EventsOfMergeRequest(ctx, repo, 9)
192		require.NoError(t, err)
193		require.Len(t, events, pass+1)
194		assert.Equal(t, int64(pass+1), events[pass].ID)
195		require.NoError(t, s.Close(ctx))
196	}
197}
198
199func TestLinkEventsDropAtCapWithoutChangingIssueVersion(t *testing.T) {
200	ctx := context.Background()
201	s, _, repo := repoWithMRs(t)
202	issue, err := s.Issues().Create(ctx, newIssue(t, repo, "full"))
203	require.NoError(t, err)
204	events := make([]domain.IssueEvent, kvstore.MaxIssueEvents)
205	for j := range events {
206		events[j].Kind = domain.IssueEventClosed
207	}
208	require.NoError(t, s.Issues().Update(ctx, issue, events...))
209	before, err := s.Issues().Get(ctx, repo, issue.Number)
210	require.NoError(t, err)
211	added, _, err := s.IssueLinks().ReplaceLinks(ctx, repo, 9, []int64{issue.Number}, []int64{issue.Number}, ports.ReferenceEvent{Actor: "writer", At: created})
212	require.NoError(t, err)
213	require.Len(t, added, 1)
214	after, err := s.Issues().Get(ctx, repo, issue.Number)
215	require.NoError(t, err)
216	assert.Equal(t, before.Version, after.Version)
217	history, err := s.Issues().ListEvents(ctx, repo, issue.Number)
218	require.NoError(t, err)
219	require.Len(t, history, kvstore.MaxIssueEvents)
220	mrHistory, err := s.IssueLinks().EventsOfMergeRequest(ctx, repo, 9)
221	require.NoError(t, err)
222	assert.Empty(t, mrHistory)
223	links, err := s.IssueLinks().LinksOfMergeRequest(ctx, repo, 9)
224	require.NoError(t, err)
225	assert.Equal(t, added, links)
226}

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 3

Tags 45

↑↓ moveenter openesc close