diff --git a/modelmigration/migrations.go b/modelmigration/migrations.go index 42563f729be..429dc8308cc 100644 --- a/modelmigration/migrations.go +++ b/modelmigration/migrations.go @@ -423,6 +423,7 @@ func prepareMigrationTasks() []*migration { newMigration(347, "Add watch options", v28.AddWatchOptions), newMigration(348, "Recreate email_hash table for SHA256 avatar hashes", v28.RecreateEmailHashTable), newMigration(349, "Expand action_schedule content column", v28.ExpandActionScheduleContent), + newMigration(350, "Add published_unix column to release", v28.AddPublishedUnixToRelease), } return preparedMigrations } diff --git a/modelmigration/v28/v350.go b/modelmigration/v28/v350.go new file mode 100644 index 00000000000..7167c9e0a88 --- /dev/null +++ b/modelmigration/v28/v350.go @@ -0,0 +1,28 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v28 + +import ( + "context" + + "gitea.dev/modelmigration/base" + + "xorm.io/xorm" +) + +func AddPublishedUnixToRelease(_ context.Context, x base.EngineMigration) error { + type Release struct { + PublishedUnix int64 `xorm:"NOT NULL DEFAULT 0"` + } + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreDropIndices: true, + }, new(Release)); err != nil { + return err + } + + // existing rows have no recorded publication time, so fall back to their creation time + _, err := x.Exec("UPDATE `release` SET published_unix = created_unix WHERE published_unix = 0 AND is_draft = ?", false) + return err +} diff --git a/modelmigration/v28/v350_test.go b/modelmigration/v28/v350_test.go new file mode 100644 index 00000000000..ad56df1ae53 --- /dev/null +++ b/modelmigration/v28/v350_test.go @@ -0,0 +1,41 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v28 + +import ( + "testing" + + "gitea.dev/modelmigration/migrationtest" + + "github.com/stretchr/testify/require" +) + +func TestAddPublishedUnixToRelease(t *testing.T) { + type Release struct { + ID int64 `xorm:"pk autoincr"` + IsDraft bool `xorm:"NOT NULL DEFAULT false"` + IsTag bool `xorm:"NOT NULL DEFAULT false"` + CreatedUnix int64 `xorm:"INDEX"` + } + + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Release)) + defer deferable() + if x == nil || t.Failed() { + return + } + + _, err := x.Insert( + &Release{CreatedUnix: 1000000}, + &Release{IsDraft: true, CreatedUnix: 2000000}, + &Release{IsTag: true, CreatedUnix: 3000000}, + ) + require.NoError(t, err) + + require.NoError(t, AddPublishedUnixToRelease(t.Context(), x)) + + var got []struct{ PublishedUnix int64 } + require.NoError(t, x.Table("release").OrderBy("id").Find(&got)) + require.Equal(t, []int64{1000000, 0, 3000000}, []int64{got[0].PublishedUnix, got[1].PublishedUnix, got[2].PublishedUnix}, + "everything but drafts is backfilled") +} diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index c6f4bf2997b..2e3af13db52 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -367,7 +367,7 @@ func (stats *ActivityStats) FillReleases(ctx context.Context, repoID int64, from // Published releases list sess := releasesForActivityStatement(ctx, repoID, fromTime) - sess.OrderBy("`release`.created_unix DESC") + sess.OrderBy("`release`.published_unix DESC") stats.PublishedReleases = make([]*repo_model.Release, 0) if err = sess.Find(&stats.PublishedReleases); err != nil { return err @@ -386,5 +386,5 @@ func (stats *ActivityStats) FillReleases(ctx context.Context, repoID int64, from func releasesForActivityStatement(ctx context.Context, repoID int64, fromTime time.Time) db.Session { return db.GetEngine(ctx).Where("`release`.repo_id = ?", repoID). And("`release`.is_draft = ?", false). - And("`release`.created_unix >= ?", fromTime.Unix()) + And("`release`.published_unix >= ?", fromTime.Unix()) } diff --git a/models/fixtures/release.yml b/models/fixtures/release.yml index be44e331ec0..41749a4e26b 100644 --- a/models/fixtures/release.yml +++ b/models/fixtures/release.yml @@ -11,6 +11,7 @@ is_prerelease: false is_tag: false created_unix: 946684800 + published_unix: 946684800 - id: 2 repo_id: 40 @@ -25,6 +26,7 @@ is_prerelease: false is_tag: false created_unix: 946684800 + published_unix: 946684800 - id: 3 repo_id: 1 @@ -39,6 +41,7 @@ is_prerelease: false is_tag: true created_unix: 946684800 + published_unix: 946684800 - id: 4 repo_id: 1 @@ -66,6 +69,7 @@ is_prerelease: true is_tag: false created_unix: 946684800 + published_unix: 946684800 - id: 6 repo_id: 57 @@ -80,6 +84,7 @@ is_prerelease: false is_tag: false created_unix: 946684801 + published_unix: 946684801 - id: 7 repo_id: 57 @@ -94,6 +99,7 @@ is_prerelease: false is_tag: false created_unix: 946684802 + published_unix: 946684802 - id: 8 repo_id: 57 @@ -108,6 +114,7 @@ is_prerelease: false is_tag: false created_unix: 946684803 + published_unix: 946684803 - id: 9 repo_id: 57 @@ -122,6 +129,7 @@ is_prerelease: false is_tag: false created_unix: 946684803 + published_unix: 946684803 - id: 10 repo_id: 57 @@ -136,6 +144,7 @@ is_prerelease: false is_tag: false created_unix: 946684803 + published_unix: 946684803 - id: 11 repo_id: 2 @@ -150,5 +159,6 @@ is_prerelease: false is_tag: false created_unix: 946684803 + published_unix: 946684803 # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/repo/release.go b/models/repo/release.go index f10ecc89409..9dd1ab395d7 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -87,6 +87,7 @@ type Release struct { IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases Attachments []*Attachment `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` + PublishedUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"` } func init() { @@ -473,7 +474,7 @@ func PushUpdateDeleteTags(ctx context.Context, repo *Repository, tags []string) if _, err := db.GetEngine(ctx). Where("repo_id = ? AND is_tag = ?", repo.ID, false). In("lower_tag_name", lowerTags). - Cols("is_draft", "num_commits", "sha1"). + Cols("is_draft", "num_commits", "sha1", "published_unix"). Update(&Release{ IsDraft: true, }); err != nil { diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go index 874b3744c30..cf6afaffbca 100644 --- a/modules/git/foreachref/parser.go +++ b/modules/git/foreachref/parser.go @@ -103,7 +103,7 @@ func (p *Parser) parseRef(refBlock string) (map[string]string, error) { return nil, nil //nolint:nilnil // return nil to signal EOF } - fieldValues := make(map[string]string) + fieldValues := make(map[string]string, len(p.format.fieldNames)) fields := strings.Split(refBlock, p.format.fieldDelimStr) if len(fields) != len(p.format.fieldNames) { diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 871a3db68e4..028764d9d3d 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -7,7 +7,9 @@ package git import ( "context" "fmt" + "strconv" "strings" + "time" "gitea.dev/modules/git/foreachref" "gitea.dev/modules/git/gitcmd" @@ -113,7 +115,7 @@ func (repo *Repository) GetTagWithID(ctx context.Context, idStr, name string) (* func (repo *Repository) GetTagInfos(ctx context.Context, page, pageSize int) ([]*Tag, int, error) { // Generally, refname:short should be equal to refname:lstrip=2 except core.warnAmbiguousRefs is used to select the strict abbreviation mode. // https://git-scm.com/docs/git-for-each-ref#Documentation/git-for-each-ref.txt-refname - forEachRefFmt := foreachref.NewFormat("objecttype", "refname:lstrip=2", "object", "objectname", "creator", "contents", "contents:signature") + forEachRefFmt := foreachref.NewFormat("objecttype", "refname:lstrip=2", "object", "objectname", "creator", "contents", "contents:signature", "committerdate:unix", "*committerdate:unix") var tags []*Tag var tagsTotal int @@ -179,6 +181,15 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) { tag.Tagger = parseSignatureFromCommitLine(ref["creator"]) tag.MessageRaw = ref["contents"] + // a lightweight tag reports the commit date directly, an annotated one only behind the dereferencing "*" + if committerDate := util.IfZero(ref["*committerdate:unix"], ref["committerdate:unix"]); committerDate != "" { + seconds, err := strconv.ParseInt(committerDate, 10, 64) + if err != nil { + return nil, fmt.Errorf("parse committerdate '%s': %w", committerDate, err) + } + tag.CommitDate = time.Unix(seconds, 0) + } + // strip any signature if present in contents field _, tag.MessageRaw, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.MessageRaw), 0) diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index e557f45d297..6c9f8e199ce 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -6,6 +6,7 @@ package git import ( "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -33,6 +34,7 @@ func TestRepository_GetTagInfos(t *testing.T) { assert.Equal(t, "test", tags[1].Name) assert.Equal(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", tags[1].ID.String()) assert.Equal(t, "tag", tags[1].Type) + assert.False(t, tags[0].CommitDate.IsZero(), "for-each-ref resolves the tagged commit's date") } func TestRepository_GetTag(t *testing.T) { @@ -207,7 +209,9 @@ func TestRepository_parseTagRef(t *testing.T) { * add changelog of v1.9.1 * Update CHANGELOG.md `, - "contents:signature": "", + "contents:signature": "", + "committerdate:unix": "1565789218", + "*committerdate:unix": "", }, want: &Tag{ @@ -216,6 +220,7 @@ func TestRepository_parseTagRef(t *testing.T) { Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), Type: "commit", Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), + CommitDate: time.Unix(1565789218, 0), CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"}, Signature: nil, }, @@ -237,6 +242,9 @@ func TestRepository_parseTagRef(t *testing.T) { * Update CHANGELOG.md `, "contents:signature": "", + // an annotated tag can be made long after the commit, so its own date is not the commit date + "committerdate:unix": "", + "*committerdate:unix": "1565700000", }, want: &Tag{ @@ -245,6 +253,7 @@ func TestRepository_parseTagRef(t *testing.T) { Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), Type: "tag", Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), + CommitDate: time.Unix(1565700000, 0), CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"}, Signature: nil, }, diff --git a/modules/git/tag.go b/modules/git/tag.go index f7d44f8f319..496363f2b14 100644 --- a/modules/git/tag.go +++ b/modules/git/tag.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "sort" + "time" "gitea.dev/modules/util" ) @@ -21,6 +22,8 @@ type Tag struct { Type string Tagger *Signature Signature *CommitSignature + + CommitDate time.Time // committer date of Object, only GetTagInfos resolves it } func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) { diff --git a/modules/repository/repo.go b/modules/repository/repo.go index aced7d6031e..02534587614 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -18,6 +18,7 @@ import ( "gitea.dev/modules/log" "gitea.dev/modules/setting" "gitea.dev/modules/timeutil" + "gitea.dev/modules/util" ) /* @@ -245,9 +246,10 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR LowerTagName: strings.ToLower(tag.Name), Sha1: tag.Object.String(), // NOTE: ignored, The NumCommits value is calculated and cached on demand when the UI requires it. - NumCommits: -1, - CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), - IsTag: true, + NumCommits: -1, + CreatedUnix: timeutil.TimeStamp(util.IfZero(tag.CommitDate, tag.Tagger.When).Unix()), + PublishedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), + IsTag: true, } if err := db.Insert(ctx, release); err != nil { return fmt.Errorf("unable insert tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) @@ -265,10 +267,11 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR for _, tag := range updates { if _, err := db.GetEngine(ctx).Where("repo_id = ? AND lower_tag_name = ?", repo.ID, strings.ToLower(tag.Name)). - Cols("sha1", "created_unix"). + Cols("sha1", "created_unix", "published_unix"). Update(&repo_model.Release{ - Sha1: tag.Object.String(), - CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), + Sha1: tag.Object.String(), + CreatedUnix: timeutil.TimeStamp(util.IfZero(tag.CommitDate, tag.Tagger.When).Unix()), + PublishedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), }); err != nil { return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) } diff --git a/modules/structs/release.go b/modules/structs/release.go index 6a3e87ccbc9..b9c6e6a7964 100644 --- a/modules/structs/release.go +++ b/modules/structs/release.go @@ -36,7 +36,7 @@ type Release struct { // swagger:strfmt date-time CreatedAt time.Time `json:"created_at"` // swagger:strfmt date-time - PublishedAt time.Time `json:"published_at"` + PublishedAt *time.Time `json:"published_at"` // The user who published the release Publisher *User `json:"author"` // The files attached to the release diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 8f1dd8ec67f..51babbb0d29 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -301,7 +301,7 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) ( items = append(items, &feeds.Item{ Title: title, Link: link, - Created: rel.CreatedUnix.AsTime(), + Created: rel.PublishedUnix.AsTime(), Author: &feeds.Author{ Name: rel.Publisher.GetDisplayName(), Email: rel.Publisher.GetEmail(), diff --git a/services/convert/release.go b/services/convert/release.go index 74b39fbf531..f3f08ca9b8c 100644 --- a/services/convert/release.go +++ b/services/convert/release.go @@ -8,6 +8,7 @@ import ( repo_model "gitea.dev/models/repo" api "gitea.dev/modules/structs" + "gitea.dev/modules/util" ) // ToAPIRelease convert a repo_model.Release to api.Release @@ -26,7 +27,7 @@ func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_mode IsDraft: r.IsDraft, IsPrerelease: r.IsPrerelease, CreatedAt: r.CreatedUnix.AsTime(), - PublishedAt: r.CreatedUnix.AsTime(), + PublishedAt: util.Iif(r.IsDraft, nil, r.PublishedUnix.AsTimePtr()), Publisher: ToUser(ctx, r.Publisher, nil), Attachments: ToAPIAttachments(repo, r.Attachments), } diff --git a/services/convert/release_test.go b/services/convert/release_test.go index 614e2fb0c12..5b121416c00 100644 --- a/services/convert/release_test.go +++ b/services/convert/release_test.go @@ -24,4 +24,8 @@ func TestRelease_ToRelease(t *testing.T) { assert.EqualValues(t, 1, apiRelease.ID) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1", apiRelease.URL) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1/assets", apiRelease.UploadURL) + assert.Equal(t, release1.PublishedUnix.AsTimePtr(), apiRelease.PublishedAt) + + release1.IsDraft = true + assert.Nil(t, ToAPIRelease(t.Context(), repo1, release1).PublishedAt, "an unpublished release has no publication date") } diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 6c87c89d536..c610443147d 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -278,17 +278,20 @@ func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*ba release.TargetCommitish = "" } + publishedAt := util.Iif(release.Published.IsZero(), release.Created, release.Published) + rel := repo_model.Release{ - RepoID: g.repo.ID, - TagName: release.TagName, - LowerTagName: strings.ToLower(release.TagName), - Target: release.TargetCommitish, - Title: release.Name, - Note: release.Body, - IsDraft: release.Draft, - IsPrerelease: release.Prerelease, - IsTag: false, - CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), + RepoID: g.repo.ID, + TagName: release.TagName, + LowerTagName: strings.ToLower(release.TagName), + Target: release.TargetCommitish, + Title: release.Name, + Note: release.Body, + IsDraft: release.Draft, + IsPrerelease: release.Prerelease, + IsTag: false, + CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), + PublishedUnix: util.Iif(release.Draft, 0, timeutil.TimeStamp(publishedAt.Unix())), } if err := g.remapUser(ctx, release, &rel); err != nil { diff --git a/services/release/release.go b/services/release/release.go index e49fbcc2585..f5a31f2acea 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -140,7 +140,9 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel NewCommitID: commit.ID.String(), }, commits) notify_service.CreateRef(ctx, rel.Publisher, rel.Repo, refFullName, commit.ID.String()) - rel.CreatedUnix = timeutil.TimeStampNow() + } + if rel.PublishedUnix.IsZero() { + rel.PublishedUnix = timeutil.TimeStampNow() } commit, err := gitRepo.GetTagCommit(ctx, rel.TagName) if err != nil { @@ -148,6 +150,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel } rel.Sha1 = commit.ID.String() + rel.CreatedUnix = timeutil.TimeStamp(commit.Committer.When.Unix()) // dated by its commit, so an old commit does not become the latest release rel.NumCommits, err = git.CommitsCountOfCommit(ctx, rel.Repo, commit.ID.String()) if err != nil { return false, fmt.Errorf("CommitsCount: %w", err) @@ -273,6 +276,12 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo return err } isConvertedFromTag := oldRelease.IsTag && !rel.IsTag + // a zero PublishedUnix means "draft", so withdrawing a release has to clear it again + if rel.IsDraft { + rel.PublishedUnix = 0 + } else if isConvertedFromTag || oldRelease.IsDraft { + rel.PublishedUnix = timeutil.TimeStampNow() + } if err := db.WithTx(ctx, func(ctx context.Context) error { if err = repo_model.UpdateRelease(ctx, rel); err != nil { diff --git a/services/release/release_test.go b/services/release/release_test.go index 5dfaed384d8..f45ab2030e5 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -165,12 +165,14 @@ func TestRelease_Update(t *testing.T) { release, err := repo_model.GetRelease(t.Context(), repo.ID, "v1.1.1") assert.NoError(t, err) releaseCreatedUnix := release.CreatedUnix + releasePublishedUnix := release.PublishedUnix advance() release.Note = "Changed note" assert.NoError(t, UpdateRelease(t.Context(), user, gitRepo, release, nil, nil, nil)) release, err = repo_model.GetReleaseByID(t.Context(), release.ID) assert.NoError(t, err) assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix)) + assert.Equal(t, releasePublishedUnix, release.PublishedUnix, "editing does not republish") // Test a changed draft assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ @@ -195,6 +197,19 @@ func TestRelease_Update(t *testing.T) { release, err = repo_model.GetReleaseByID(t.Context(), release.ID) assert.NoError(t, err) assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix)) + assert.Zero(t, release.PublishedUnix, "a draft is unpublished") + + // Test publishing and withdrawing that draft + release.IsDraft = false + assert.NoError(t, UpdateRelease(t.Context(), user, gitRepo, release, nil, nil, nil)) + release, err = repo_model.GetReleaseByID(t.Context(), release.ID) + assert.NoError(t, err) + assert.NotZero(t, release.PublishedUnix, "publishing stamps the publication time") + release.IsDraft = true + assert.NoError(t, UpdateRelease(t.Context(), user, gitRepo, release, nil, nil, nil)) + release, err = repo_model.GetReleaseByID(t.Context(), release.ID) + assert.NoError(t, err) + assert.Zero(t, release.PublishedUnix, "withdrawing unpublishes it again") // Test a changed pre-release assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ @@ -387,3 +402,34 @@ func TestCreateNewTag(t *testing.T) { assert.NoError(t, CreateNewTag(t.Context(), user, repo, "master", "v2.0", "v2.0 is released \n\n BUGFIX: .... \n\n 123")) } + +func TestRelease_DatedByTargetCommit(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := git.OpenRepository(t.Context(), repo) + assert.NoError(t, err) + defer gitRepo.Close() + + newRelease := func(tagName, target string) *repo_model.Release { + rel := &repo_model.Release{ + RepoID: repo.ID, Repo: repo, PublisherID: user.ID, Publisher: user, + TagName: tagName, Target: target, Title: tagName, + } + assert.NoError(t, CreateRelease(t.Context(), gitRepo, rel, nil, "")) + return rel + } + + recent := newRelease("v9.9-recent", "DefaultBranch") + // released afterwards, but from an older commit, so it must not take over as the latest release + old := newRelease("v9.9-old", "master") + + oldCommit, err := gitRepo.GetBranchCommit(t.Context(), "master") + assert.NoError(t, err) + assert.Equal(t, oldCommit.Committer.When.Unix(), int64(old.CreatedUnix), "a release is dated by the commit it points at") + assert.Greater(t, int64(old.PublishedUnix), int64(old.CreatedUnix), "but its publication time is now") + + latest, err := repo_model.GetLatestReleaseByRepoID(t.Context(), repo.ID) + assert.NoError(t, err) + assert.Equal(t, recent.ID, latest.ID) +} diff --git a/services/repository/push.go b/services/repository/push.go index 017a152b6db..a8b8187b633 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -368,47 +368,45 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo return fmt.Errorf("Commit: %w", err) } - sig := tag.Tagger - if sig == nil { - sig = commit.Author - } - if sig == nil { - sig = commit.Committer - } - - createdAt := time.Unix(1, 0) - if sig != nil { - createdAt = sig.When + createdUnix := timeutil.TimeStamp(commit.Committer.When.Unix()) // tagged whenever, but dated by its commit + publishedUnix := createdUnix + if tag.Tagger != nil { + publishedUnix = timeutil.TimeStamp(tag.Tagger.When.Unix()) } rel, has := relMap[lowerTag] title, note := git.SplitCommitTitleBody(tag.MessageUTF8(), 255) if !has { rel = &repo_model.Release{ - RepoID: repo.ID, - Title: title, - TagName: tags[i], - LowerTagName: lowerTag, - Target: "", - Sha1: commit.ID.String(), - NumCommits: -1, // the commits count will be updated when the UI needs it - Note: note, - IsDraft: false, - IsPrerelease: false, - IsTag: true, - PublisherID: pusher.ID, - CreatedUnix: timeutil.TimeStamp(createdAt.Unix()), + RepoID: repo.ID, + Title: title, + TagName: tags[i], + LowerTagName: lowerTag, + Target: "", + Sha1: commit.ID.String(), + NumCommits: -1, // the commits count will be updated when the UI needs it + Note: note, + IsDraft: false, + IsPrerelease: false, + IsTag: true, + PublisherID: pusher.ID, + CreatedUnix: createdUnix, + PublishedUnix: publishedUnix, } newReleases = append(newReleases, rel) } else { rel.Sha1 = commit.ID.String() - rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix()) + rel.CreatedUnix = createdUnix if rel.IsTag { rel.Title = title rel.Note = note + rel.PublishedUnix = publishedUnix } else { rel.IsDraft = false + if rel.PublishedUnix.IsZero() { + rel.PublishedUnix = timeutil.TimeStampNow() + } } rel.PublisherID = pusher.ID if err = repo_model.UpdateRelease(ctx, rel); err != nil { diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl index be3f324e74d..2d1d61f59fa 100644 --- a/templates/repo/home_sidebar_bottom.tmpl +++ b/templates/repo/home_sidebar_bottom.tmpl @@ -15,7 +15,7 @@ {{template "repo/release/label" (dict "Release" .LatestRelease "IsLatest" true)}} -
{{DateUtils.TimeSince .LatestRelease.CreatedUnix}}
+
{{DateUtils.TimeSince .LatestRelease.PublishedUnix}}
{{end}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 1009d224372..90870e54989 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -59,11 +59,11 @@ Ghost {{end}} - - {{ctx.Locale.Tr "repo.released_this"}} - - {{if $release.CreatedUnix}} - {{DateUtils.TimeSince $release.CreatedUnix}} + {{if not $release.IsDraft}} + + {{ctx.Locale.Tr "repo.released_this"}} + + {{DateUtils.TimeSince $release.PublishedUnix}} {{end}} {{if and (gt $release.NumCommits 0) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}} | {{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind}} {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}} diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index 7fd9d1f10b5..4526e37cb03 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -46,18 +46,21 @@ func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title st assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes())) } -func checkLatestReleaseAndCount(t *testing.T, session *TestSession, repoURL, version, label string, count int) { +// returns the first listed title, which is not necessarily the one just created because releases are ordered by commit date +func checkLatestReleaseAndCount(t *testing.T, session *TestSession, repoURL, version, label string, count int) string { req := NewRequest(t, "GET", repoURL+"/releases") resp := session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - labelText := htmlDoc.doc.Find("#release-list > li .detail .label").First().Text() - assert.Equal(t, label, labelText) - titleText := htmlDoc.doc.Find("#release-list > li .detail h4 a").First().Text() - assert.Equal(t, version, titleText) - - releaseList := htmlDoc.doc.Find("#release-list > li") + releaseList := NewHTMLParser(t, resp.Body).doc.Find("#release-list > li") assert.Equal(t, count, releaseList.Length()) + + item := releaseList.FilterFunction(func(_ int, selection *goquery.Selection) bool { + return selection.Find(".detail h4 a").Text() == version + }) + if assert.Equal(t, 1, item.Length(), "release %q is listed exactly once", version) { + assert.Equal(t, label, item.Find(".detail .label").First().Text()) + } + return releaseList.Find(".detail h4 a").First().Text() } func TestViewReleases(t *testing.T) { @@ -113,11 +116,11 @@ func TestCreateReleasePaging(t *testing.T) { } createNewRelease(t, session, "/user2/repo1", "v0.0.12", "v0.0.12", false, true) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").TrString("repo.release.draft"), 10) + assert.Equal(t, "v0.0.12", checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").TrString("repo.release.draft"), 10)) // Check that user4 does not see draft and still see 10 latest releases session2 := loginUser(t, "user4") - checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").TrString("repo.release.stable"), 10) + assert.Equal(t, "v0.0.11", checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").TrString("repo.release.stable"), 10)) } func TestViewReleaseListNoLogin(t *testing.T) {