Skip to content

Commit 1406ccc

Browse files
committed
improvement: detect previous published release for notes base
1 parent b98df1f commit 1406ccc

File tree

4 files changed

+265
-44
lines changed

4 files changed

+265
-44
lines changed

models/repo/release.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package repo
66

77
import (
88
"context"
9+
"errors"
910
"fmt"
1011
"html/template"
1112
"net/url"
@@ -322,6 +323,40 @@ func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, erro
322323
return rel, nil
323324
}
324325

326+
// GetPreviousPublishedRelease returns the most recent published release created before the provided release.
327+
func GetPreviousPublishedRelease(ctx context.Context, repoID int64, current *Release) (*Release, error) {
328+
if current == nil {
329+
return nil, errors.New("current release must not be nil")
330+
}
331+
332+
cond := builder.NewCond().
333+
And(builder.Eq{"repo_id": repoID}).
334+
And(builder.Eq{"is_draft": false}).
335+
And(builder.Eq{"is_prerelease": false}).
336+
And(builder.Eq{"is_tag": false}).
337+
And(builder.Or(
338+
builder.Lt{"created_unix": current.CreatedUnix},
339+
builder.And(
340+
builder.Eq{"created_unix": current.CreatedUnix},
341+
builder.Lt{"id": current.ID},
342+
),
343+
))
344+
345+
rel := new(Release)
346+
has, err := db.GetEngine(ctx).
347+
Desc("created_unix", "id").
348+
Where(cond).
349+
Get(rel)
350+
if err != nil {
351+
return nil, err
352+
}
353+
if !has {
354+
return nil, ErrReleaseNotExist{0, "previous"}
355+
}
356+
357+
return rel, nil
358+
}
359+
325360
type releaseMetaSearch struct {
326361
ID []int64
327362
Rel []*Release

models/repo/release_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"code.gitea.io/gitea/models/unittest"
10+
"code.gitea.io/gitea/modules/timeutil"
1011

1112
"github.com/stretchr/testify/assert"
1213
)
@@ -37,3 +38,40 @@ func Test_FindTagsByCommitIDs(t *testing.T) {
3738
assert.Equal(t, "delete-tag", rels[1].TagName)
3839
assert.Equal(t, "v1.0", rels[2].TagName)
3940
}
41+
42+
func TestGetPreviousPublishedRelease(t *testing.T) {
43+
assert.NoError(t, unittest.PrepareTestDatabase())
44+
45+
current := unittest.AssertExistsAndLoadBean(t, &Release{ID: 8})
46+
prev, err := GetPreviousPublishedRelease(t.Context(), current.RepoID, current)
47+
assert.NoError(t, err)
48+
assert.EqualValues(t, 7, prev.ID)
49+
}
50+
51+
func TestGetPreviousPublishedRelease_NoPublishedCandidate(t *testing.T) {
52+
assert.NoError(t, unittest.PrepareTestDatabase())
53+
54+
repoID := int64(1)
55+
draft := &Release{
56+
RepoID: repoID,
57+
PublisherID: 1,
58+
TagName: "draft-prev",
59+
LowerTagName: "draft-prev",
60+
IsDraft: true,
61+
CreatedUnix: timeutil.TimeStamp(2),
62+
}
63+
current := &Release{
64+
RepoID: repoID,
65+
PublisherID: 1,
66+
TagName: "published-current",
67+
LowerTagName: "published-current",
68+
CreatedUnix: timeutil.TimeStamp(3),
69+
}
70+
71+
err := InsertReleases(t.Context(), draft, current)
72+
assert.NoError(t, err)
73+
74+
_, err = GetPreviousPublishedRelease(t.Context(), repoID, current)
75+
assert.Error(t, err)
76+
assert.True(t, IsErrReleaseNotExist(err))
77+
}

services/release/notes.go

Lines changed: 100 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -141,61 +141,26 @@ func resolveBaseTag(ctx context.Context, repo *repo_model.Repository, gitRepo *g
141141
requestedBase = strings.TrimSpace(requestedBase)
142142
if requestedBase != "" {
143143
if gitRepo.IsTagExist(requestedBase) {
144-
baseCommit, err := gitRepo.GetCommit(requestedBase)
145-
if err != nil {
146-
return nil, newErrReleaseNotesTagNotFound(requestedBase)
147-
}
148-
return &baseSelection{
149-
CompareBase: requestedBase,
150-
PreviousTag: requestedBase,
151-
Commit: baseCommit,
152-
}, nil
144+
return buildBaseSelectionForTag(gitRepo, requestedBase)
153145
}
154146
return nil, newErrReleaseNotesTagNotFound(requestedBase)
155147
}
156148

157-
rel, err := repo_model.GetLatestReleaseByRepoID(ctx, repo.ID)
158-
switch {
159-
case err == nil:
160-
candidate := strings.TrimSpace(rel.TagName)
161-
if !strings.EqualFold(candidate, tagName) {
162-
if gitRepo.IsTagExist(candidate) {
163-
baseCommit, err := gitRepo.GetCommit(candidate)
164-
if err != nil {
165-
return nil, newErrReleaseNotesTagNotFound(candidate)
166-
}
167-
return &baseSelection{
168-
CompareBase: candidate,
169-
PreviousTag: candidate,
170-
Commit: baseCommit,
171-
}, nil
172-
}
173-
return nil, newErrReleaseNotesTagNotFound(candidate)
174-
}
175-
case repo_model.IsErrReleaseNotExist(err):
176-
// fall back to tags below
177-
default:
178-
return nil, fmt.Errorf("GetLatestReleaseByRepoID: %w", err)
149+
candidate, err := autoPreviousReleaseTag(ctx, repo, tagName)
150+
if err != nil {
151+
return nil, err
152+
}
153+
if candidate != "" {
154+
return buildBaseSelectionForTag(gitRepo, candidate)
179155
}
180156

181157
tagInfos, _, err := gitRepo.GetTagInfos(0, 0)
182158
if err != nil {
183159
return nil, fmt.Errorf("GetTagInfos: %w", err)
184160
}
185161

186-
for _, tag := range tagInfos {
187-
if strings.EqualFold(tag.Name, tagName) {
188-
continue
189-
}
190-
baseCommit, err := gitRepo.GetCommit(tag.Name)
191-
if err != nil {
192-
return nil, newErrReleaseNotesTagNotFound(tag.Name)
193-
}
194-
return &baseSelection{
195-
CompareBase: tag.Name,
196-
PreviousTag: tag.Name,
197-
Commit: baseCommit,
198-
}, nil
162+
if previousTag, ok := findPreviousTagName(tagInfos, tagName); ok {
163+
return buildBaseSelectionForTag(gitRepo, previousTag)
199164
}
200165

201166
initialCommit, err := findInitialCommit(headCommit)
@@ -209,6 +174,97 @@ func resolveBaseTag(ctx context.Context, repo *repo_model.Repository, gitRepo *g
209174
}, nil
210175
}
211176

177+
func buildBaseSelectionForTag(gitRepo *git.Repository, tagName string) (*baseSelection, error) {
178+
tagName = strings.TrimSpace(tagName)
179+
if tagName == "" {
180+
return nil, ErrReleaseNotesNoBaseTag{}
181+
}
182+
183+
baseCommit, err := gitRepo.GetCommit(tagName)
184+
if err != nil {
185+
return nil, newErrReleaseNotesTagNotFound(tagName)
186+
}
187+
return &baseSelection{
188+
CompareBase: tagName,
189+
PreviousTag: tagName,
190+
Commit: baseCommit,
191+
}, nil
192+
}
193+
194+
func autoPreviousReleaseTag(ctx context.Context, repo *repo_model.Repository, tagName string) (string, error) {
195+
tagName = strings.TrimSpace(tagName)
196+
if tagName == "" {
197+
return "", nil
198+
}
199+
200+
currentRelease, err := repo_model.GetRelease(ctx, repo.ID, tagName)
201+
switch {
202+
case err == nil:
203+
return findPreviousPublishedReleaseTag(ctx, repo, currentRelease)
204+
case repo_model.IsErrReleaseNotExist(err):
205+
// this tag has no stored release, fall back to latest release below
206+
default:
207+
return "", fmt.Errorf("GetRelease: %w", err)
208+
}
209+
210+
rel, err := repo_model.GetLatestReleaseByRepoID(ctx, repo.ID)
211+
switch {
212+
case err == nil:
213+
candidate := strings.TrimSpace(rel.TagName)
214+
if candidate == "" || strings.EqualFold(candidate, tagName) {
215+
return "", nil
216+
}
217+
return candidate, nil
218+
case repo_model.IsErrReleaseNotExist(err):
219+
return "", nil
220+
default:
221+
return "", fmt.Errorf("GetLatestReleaseByRepoID: %w", err)
222+
}
223+
}
224+
225+
func findPreviousPublishedReleaseTag(ctx context.Context, repo *repo_model.Repository, current *repo_model.Release) (string, error) {
226+
target := strings.TrimSpace(current.TagName)
227+
prev, err := repo_model.GetPreviousPublishedRelease(ctx, repo.ID, current)
228+
switch {
229+
case err == nil:
230+
case repo_model.IsErrReleaseNotExist(err):
231+
return "", nil
232+
default:
233+
return "", fmt.Errorf("GetPreviousPublishedRelease: %w", err)
234+
}
235+
236+
candidate := strings.TrimSpace(prev.TagName)
237+
if candidate == "" || strings.EqualFold(candidate, target) {
238+
return "", nil
239+
}
240+
return candidate, nil
241+
}
242+
243+
func findPreviousTagName(tags []*git.Tag, target string) (string, bool) {
244+
target = strings.TrimSpace(target)
245+
foundTarget := false
246+
for _, tag := range tags {
247+
name := strings.TrimSpace(tag.Name)
248+
if name == "" {
249+
continue
250+
}
251+
if strings.EqualFold(name, target) {
252+
foundTarget = true
253+
continue
254+
}
255+
if foundTarget {
256+
return name, true
257+
}
258+
}
259+
if !foundTarget && len(tags) > 0 {
260+
name := strings.TrimSpace(tags[0].Name)
261+
if name != "" {
262+
return name, true
263+
}
264+
}
265+
return "", false
266+
}
267+
212268
func findInitialCommit(commit *git.Commit) (*git.Commit, error) {
213269
current := commit
214270
for current.ParentCount() > 0 {

services/release/notes_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
package release
55

66
import (
7+
"context"
78
"fmt"
9+
"strings"
810
"testing"
11+
"time"
912

1013
"code.gitea.io/gitea/models/db"
1114
issues_model "code.gitea.io/gitea/models/issues"
1215
repo_model "code.gitea.io/gitea/models/repo"
1316
"code.gitea.io/gitea/models/unittest"
1417
user_model "code.gitea.io/gitea/models/user"
18+
"code.gitea.io/gitea/modules/git"
1519
"code.gitea.io/gitea/modules/gitrepo"
1620
"code.gitea.io/gitea/modules/timeutil"
1721

@@ -69,6 +73,14 @@ func TestGenerateReleaseNotes_NoReleaseFallsBackToTags(t *testing.T) {
6973
Where("repo_id=?", repo.ID).
7074
Delete(new(repo_model.Release))
7175
require.NoError(t, err)
76+
t.Cleanup(func() {
77+
if len(releases) == 0 {
78+
return
79+
}
80+
ctx := context.Background()
81+
_, err := db.GetEngine(ctx).Insert(&releases)
82+
require.NoError(t, err)
83+
})
7284

7385
result, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
7486
TagName: "v1.2.0",
@@ -79,6 +91,52 @@ func TestGenerateReleaseNotes_NoReleaseFallsBackToTags(t *testing.T) {
7991
assert.Contains(t, result.Content, "@user5")
8092
}
8193

94+
func TestAutoPreviousReleaseTag_UsesPrevPublishedRelease(t *testing.T) {
95+
unittest.PrepareTestEnv(t)
96+
ctx := t.Context()
97+
98+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
99+
prev := insertTestRelease(ctx, t, repo, "auto-prev", timeutil.TimeStamp(100), releaseInsertOptions{})
100+
insertTestRelease(ctx, t, repo, "auto-draft", timeutil.TimeStamp(150), releaseInsertOptions{IsDraft: true})
101+
insertTestRelease(ctx, t, repo, "auto-pre", timeutil.TimeStamp(175), releaseInsertOptions{IsPrerelease: true})
102+
current := insertTestRelease(ctx, t, repo, "auto-current", timeutil.TimeStamp(200), releaseInsertOptions{})
103+
104+
candidate, err := autoPreviousReleaseTag(ctx, repo, current.TagName)
105+
require.NoError(t, err)
106+
assert.Equal(t, prev.TagName, candidate)
107+
}
108+
109+
func TestAutoPreviousReleaseTag_LatestReleaseFallback(t *testing.T) {
110+
unittest.PrepareTestEnv(t)
111+
ctx := t.Context()
112+
113+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
114+
latest := insertTestRelease(ctx, t, repo, "auto-latest", timeutil.TimeStampNow(), releaseInsertOptions{})
115+
116+
candidate, err := autoPreviousReleaseTag(ctx, repo, "missing-tag")
117+
require.NoError(t, err)
118+
assert.Equal(t, latest.TagName, candidate)
119+
}
120+
121+
func TestFindPreviousTagName(t *testing.T) {
122+
tags := []*git.Tag{
123+
{Name: "v2.0.0"},
124+
{Name: "v1.1.0"},
125+
{Name: "v1.0.0"},
126+
}
127+
128+
prev, ok := findPreviousTagName(tags, "v1.1.0")
129+
require.True(t, ok)
130+
assert.Equal(t, "v1.0.0", prev)
131+
132+
prev, ok = findPreviousTagName(tags, "v9.9.9")
133+
require.True(t, ok)
134+
assert.Equal(t, "v2.0.0", prev)
135+
136+
_, ok = findPreviousTagName([]*git.Tag{{Name: ""}}, "v1.0.0")
137+
assert.False(t, ok)
138+
}
139+
82140
func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64) *issues_model.PullRequest {
83141
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: posterID})
84142

@@ -115,3 +173,37 @@ func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCom
115173
require.NoError(t, pr.Issue.LoadAttributes(t.Context()))
116174
return pr
117175
}
176+
177+
type releaseInsertOptions struct {
178+
IsDraft bool
179+
IsPrerelease bool
180+
IsTag bool
181+
}
182+
183+
func insertTestRelease(ctx context.Context, t *testing.T, repo *repo_model.Repository, tag string, created timeutil.TimeStamp, opts releaseInsertOptions) *repo_model.Release {
184+
t.Helper()
185+
lower := strings.ToLower(tag)
186+
187+
release := &repo_model.Release{
188+
RepoID: repo.ID,
189+
PublisherID: repo.OwnerID,
190+
TagName: tag,
191+
LowerTagName: lower,
192+
Target: repo.DefaultBranch,
193+
Title: tag,
194+
Sha1: fmt.Sprintf("%040d", int64(created)+time.Now().UnixNano()),
195+
IsDraft: opts.IsDraft,
196+
IsPrerelease: opts.IsPrerelease,
197+
IsTag: opts.IsTag,
198+
CreatedUnix: created,
199+
}
200+
201+
_, err := db.GetEngine(ctx).Insert(release)
202+
require.NoError(t, err)
203+
t.Cleanup(func() {
204+
_, err := db.GetEngine(context.Background()).ID(release.ID).Delete(new(repo_model.Release))
205+
require.NoError(t, err)
206+
})
207+
208+
return release
209+
}

0 commit comments

Comments
 (0)