Files
gitea/services/migrations/github_graphql_timeline_test.go
T
6624d96689 feat(migrations): GraphQL fast path for GitHub migration + rate-limit resilience (#38694)
Adds a batched GraphQL path for GitHub migrations that fetches each
issue or pull request together with its comments, reviews, reactions and
timeline events instead of the REST N+1 pattern, sharply reducing
rate-limit pressure on large repositories.

- GraphQL is used for authenticated migrations with issues or pull
requests and falls back to REST automatically when it fails
- timeline events (close/reopen, lock, rename, label, milestone, merge,
branch deletion, pin, auto-merge) are imported as typed comments showing
their GitHub author
- GitBucket issue migrations no longer stop after the first page,
GitBucket sends no Link header

Fixes: https://github.com/go-gitea/gitea/issues/13243
Fixes: https://github.com/go-gitea/gitea/issues/31856
Signed-off-by: David E. Lovas <david@rexovas.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
2026-10-23 02:23:28 +00:00

81 lines
4.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package migrations
import (
"testing"
"time"
"gitea.dev/modules/json"
base "gitea.dev/modules/migration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvertTimelineItems(t *testing.T) {
for _, testcase := range []struct {
name string
event string
want *base.Comment
}{
{"close by user", `{"__typename":"ClosedEvent","actor":{"login":"closer","databaseId":42}}`, &base.Comment{CommentType: "close", PosterName: "closer", PosterID: 42}},
{"reopen", `{"__typename":"ReopenedEvent"}`, &base.Comment{CommentType: "reopen"}},
{"label added", `{"__typename":"LabeledEvent","label":{"name":"bug"}}`, &base.Comment{CommentType: "label", Content: "1", Meta: map[string]any{"LabelName": "bug"}}},
{"label removed", `{"__typename":"UnlabeledEvent","label":{"name":"bug"}}`, &base.Comment{CommentType: "label", Meta: map[string]any{"LabelName": "bug"}}},
{"deleted label added is skipped", `{"__typename":"LabeledEvent","label":null}`, nil},
{"deleted label removed is skipped", `{"__typename":"UnlabeledEvent","label":null}`, nil},
{"lock reason in content", `{"__typename":"LockedEvent","lockReason":"TOO_HEATED"}`, &base.Comment{CommentType: "lock", Content: "too heated"}},
{"unlock", `{"__typename":"UnlockedEvent"}`, &base.Comment{CommentType: "unlock"}},
{"title changed", `{"__typename":"RenamedTitleEvent","previousTitle":"old","currentTitle":"new"}`, &base.Comment{CommentType: "change_title", Meta: map[string]any{"OldTitle": "old", "NewTitle": "new"}}},
{"milestone added", `{"__typename":"MilestonedEvent","milestoneTitle":"v1"}`, &base.Comment{CommentType: "milestone", Meta: map[string]any{"MilestoneTitle": "v1"}}},
{"milestone removed", `{"__typename":"DemilestonedEvent","milestoneTitle":"v1"}`, &base.Comment{CommentType: "milestone", Meta: map[string]any{"MilestoneTitle": "v1", "Removed": true}}},
{"merge by bot", `{"__typename":"MergedEvent","actor":{"__typename":"Bot","login":"merger","databaseId":88}}`, &base.Comment{CommentType: "merge_pull", PosterName: "merger[bot]", PosterID: 88}},
{"branch deleted", `{"__typename":"HeadRefDeletedEvent","headRefName":"feature"}`, &base.Comment{CommentType: "delete_branch", Content: "feature"}},
{"pin", `{"__typename":"PinnedEvent"}`, &base.Comment{CommentType: "pin"}},
{"null actor becomes ghost", `{"__typename":"UnpinnedEvent","actor":null}`, &base.Comment{CommentType: "unpin", PosterName: "ghost"}},
{"auto merge enabled", `{"__typename":"AutoMergeEnabledEvent"}`, &base.Comment{CommentType: "pull_scheduled_merge"}},
{"auto merge disabled", `{"__typename":"AutoMergeDisabledEvent"}`, &base.Comment{CommentType: "pull_cancel_scheduled_merge"}},
{"unknown event is skipped", `{"__typename":"MentionedEvent"}`, nil},
} {
t.Run(testcase.name, func(t *testing.T) {
var item gqlTimelineItem
require.NoError(t, json.Unmarshal([]byte(testcase.event), &item))
item.CreatedAt = time.Date(2020, 11, 17, 15, 54, 33, 0, time.UTC)
want := []*base.Comment{}
if testcase.want != nil {
testcase.want.Created, testcase.want.Updated = item.CreatedAt, item.CreatedAt
want = append(want, testcase.want)
}
assert.Equal(t, want, convertTimelineItems([]gqlTimelineItem{item}))
})
}
}
func TestDropCloseOnMerge(t *testing.T) {
for _, testcase := range []struct {
name string
events []string
want []string
}{
{"unmerged close stays", []string{"close"}, []string{"close"}},
{"last close after merge drops", []string{"close", "reopen", "merge_pull", "close"}, []string{"close", "reopen", "merge_pull"}},
{"last close before merge drops", []string{"close", "merge_pull"}, []string{"merge_pull"}},
{"close followed by reopen stays", []string{"close", "merge_pull", "reopen"}, []string{"close", "merge_pull", "reopen"}},
{"merge without close stays", []string{"merge_pull"}, []string{"merge_pull"}},
} {
t.Run(testcase.name, func(t *testing.T) {
var events []*base.Comment
for _, event := range testcase.events {
events = append(events, &base.Comment{CommentType: event})
}
var got []string
for _, event := range dropCloseOnMerge(events) {
got = append(got, event.CommentType)
}
assert.Equal(t, testcase.want, got)
})
}
}