初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package release
import (
"cmp"
"context"
"fmt"
"slices"
"strings"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/util"
)
// GenerateReleaseNotesOptions describes how to build release notes content.
type GenerateReleaseNotesOptions struct {
TagName string
TagTarget string
PreviousTag string
}
// GenerateReleaseNotes builds the markdown snippet for release notes.
func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts GenerateReleaseNotesOptions) (string, error) {
headCommit, err := resolveHeadCommit(gitRepo, opts.TagName, opts.TagTarget)
if err != nil {
return "", err
}
if opts.PreviousTag == "" {
// no previous tag, usually due to there is no tag in the repo, use the same content as GitHub
content := fmt.Sprintf("**Full Changelog**: %s/commits/tag/%s\n", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName))
return content, nil
}
baseCommit, err := gitRepo.GetCommit(opts.PreviousTag)
if err != nil {
return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName)
}
commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String())
if err != nil {
return "", fmt.Errorf("CommitsBetweenIDs: %w", err)
}
prs, err := collectPullRequestsFromCommits(ctx, repo.ID, commits)
if err != nil {
return "", err
}
contributors, newContributors, err := collectContributors(ctx, repo.ID, prs)
if err != nil {
return "", err
}
content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors)
return content, nil
}
func resolveHeadCommit(gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) {
ref := tagName
if !gitRepo.IsTagExist(tagName) {
ref = tagTarget
}
commit, err := gitRepo.GetCommit(ref)
if err != nil {
return nil, util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_target_not_found", ref)
}
return commit, nil
}
func collectPullRequestsFromCommits(ctx context.Context, repoID int64, commits []*git.Commit) ([]*issues_model.PullRequest, error) {
prs := make([]*issues_model.PullRequest, 0, len(commits))
for _, commit := range commits {
pr, err := issues_model.GetPullRequestByMergedCommit(ctx, repoID, commit.ID.String())
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
continue
}
return nil, fmt.Errorf("GetPullRequestByMergedCommit: %w", err)
}
if err = pr.LoadIssue(ctx); err != nil {
return nil, fmt.Errorf("LoadIssue: %w", err)
}
if err = pr.Issue.LoadAttributes(ctx); err != nil {
return nil, fmt.Errorf("LoadIssueAttributes: %w", err)
}
prs = append(prs, pr)
}
slices.SortFunc(prs, func(a, b *issues_model.PullRequest) int {
if cmpRes := cmp.Compare(b.MergedUnix, a.MergedUnix); cmpRes != 0 {
return cmpRes
}
return cmp.Compare(b.Issue.Index, a.Issue.Index)
})
return prs, nil
}
func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest) string {
var builder strings.Builder
builder.WriteString("## What's Changed\n")
for _, pr := range prs {
prURL := pr.Issue.HTMLURL(ctx)
fmt.Fprintf(&builder, "* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL)
}
builder.WriteString("\n")
if len(contributors) > 0 {
builder.WriteString("## Contributors\n")
for _, contributor := range contributors {
fmt.Fprintf(&builder, "* @%s\n", contributor.Name)
}
builder.WriteString("\n")
}
if len(newContributors) > 0 {
builder.WriteString("## New Contributors\n")
for _, contributor := range newContributors {
prURL := contributor.Issue.HTMLURL(ctx)
fmt.Fprintf(&builder, "* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL)
}
builder.WriteString("\n")
}
builder.WriteString("**Full Changelog**: ")
compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName))
fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL)
builder.WriteByte('\n')
return builder.String()
}
func collectContributors(ctx context.Context, repoID int64, prs []*issues_model.PullRequest) ([]*user_model.User, []*issues_model.PullRequest, error) {
contributors := make([]*user_model.User, 0, len(prs))
newContributors := make([]*issues_model.PullRequest, 0, len(prs))
seenContributors := container.Set[int64]{}
seenNew := container.Set[int64]{}
for _, pr := range prs {
poster := pr.Issue.Poster
posterID := poster.ID
if posterID == 0 {
// Migrated PRs may not have a linked local user (PosterID == 0). Skip them for now.
continue
}
if !seenContributors.Contains(posterID) {
contributors = append(contributors, poster)
seenContributors.Add(posterID)
}
if seenNew.Contains(posterID) {
continue
}
isFirst, err := isFirstContribution(ctx, repoID, posterID, pr)
if err != nil {
return nil, nil, err
}
if isFirst {
seenNew.Add(posterID)
newContributors = append(newContributors, pr)
}
}
return contributors, newContributors, nil
}
func isFirstContribution(ctx context.Context, repoID, posterID int64, pr *issues_model.PullRequest) (bool, error) {
hasMergedBefore, err := issues_model.HasMergedPullRequestInRepoBefore(ctx, repoID, posterID, pr.MergedUnix, pr.ID)
if err != nil {
return false, fmt.Errorf("check merged PRs for contributor: %w", err)
}
return !hasMergedBefore, nil
}
+97
View File
@@ -0,0 +1,97 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package release
import (
"testing"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateReleaseNotes(t *testing.T) {
unittest.PrepareTestEnv(t)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
require.NoError(t, err)
t.Run("ChangeLogsWithPRs", func(t *testing.T) {
mergedCommit := "90c1019714259b24fb81711d4416ac0f18667dfa"
createMergedPullRequest(t, repo, mergedCommit, 5)
content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
TagName: "v1.2.0",
TagTarget: "DefaultBranch",
PreviousTag: "v1.1",
})
require.NoError(t, err)
assert.Equal(t, `## What's Changed
* Release notes test pull request in [#6](https://try.gitea.io/user2/repo1/pulls/6)
## Contributors
* @user5
## New Contributors
* @user5 made their first contribution in [#6](https://try.gitea.io/user2/repo1/pulls/6)
**Full Changelog**: [v1.1...v1.2.0](https://try.gitea.io/user2/repo1/compare/v1.1...v1.2.0)
`, content)
})
t.Run("NoPreviousTag", func(t *testing.T) {
content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
TagName: "v1.2.0",
TagTarget: "DefaultBranch",
})
require.NoError(t, err)
assert.Equal(t, "**Full Changelog**: https://try.gitea.io/user2/repo1/commits/tag/v1.2.0\n", content)
})
}
func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64) *issues_model.PullRequest {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: posterID})
issue := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
Poster: user,
PosterID: user.ID,
Title: "Release notes test pull request",
Content: "content",
}
pr := &issues_model.PullRequest{
HeadRepoID: repo.ID,
BaseRepoID: repo.ID,
HeadBranch: repo.DefaultBranch,
BaseBranch: repo.DefaultBranch,
Status: issues_model.PullRequestStatusMergeable,
Flow: issues_model.PullRequestFlowGithub,
}
require.NoError(t, issues_model.NewPullRequest(t.Context(), repo, issue, nil, nil, pr))
pr.HasMerged = true
pr.MergedCommitID = mergeCommit
pr.MergedUnix = timeutil.TimeStampNow()
_, err := db.GetEngine(t.Context()).
ID(pr.ID).
Cols("has_merged", "merged_commit_id", "merged_unix").
Update(pr)
require.NoError(t, err)
require.NoError(t, pr.LoadIssue(t.Context()))
require.NoError(t, pr.Issue.LoadAttributes(t.Context()))
return pr
}
+428
View File
@@ -0,0 +1,428 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package release
import (
"context"
"errors"
"fmt"
"strings"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/repository"
"gitea.dev/modules/storage"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
notify_service "gitea.dev/services/notify"
)
// ErrInvalidTagName represents a "InvalidTagName" kind of error.
type ErrInvalidTagName struct {
TagName string
}
// IsErrInvalidTagName checks if an error is a ErrInvalidTagName.
func IsErrInvalidTagName(err error) bool {
_, ok := err.(ErrInvalidTagName)
return ok
}
func (err ErrInvalidTagName) Error() string {
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
}
func (err ErrInvalidTagName) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrProtectedTagName represents a "ProtectedTagName" kind of error.
type ErrProtectedTagName struct {
TagName string
}
// IsErrProtectedTagName checks if an error is a ErrProtectedTagName.
func IsErrProtectedTagName(err error) bool {
_, ok := err.(ErrProtectedTagName)
return ok
}
func (err ErrProtectedTagName) Error() string {
return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName)
}
func (err ErrProtectedTagName) Unwrap() error {
return util.ErrPermissionDenied
}
func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) {
err := rel.LoadAttributes(ctx)
if err != nil {
return false, err
}
err = rel.Repo.MustNotBeArchived()
if err != nil {
return false, err
}
var created bool
// Only actual create when publish.
if !rel.IsDraft {
if !gitrepo.IsTagExist(ctx, rel.Repo, rel.TagName) {
if err := rel.LoadAttributes(ctx); err != nil {
log.Error("LoadAttributes: %v", err)
return false, err
}
protectedTags, err := git_model.GetProtectedTags(ctx, rel.Repo.ID)
if err != nil {
return false, fmt.Errorf("GetProtectedTags: %w", err)
}
// Trim '--' prefix to prevent command line argument vulnerability.
rel.TagName = strings.TrimPrefix(rel.TagName, "--")
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
if err != nil {
return false, err
}
if !isAllowed {
return false, ErrProtectedTagName{
TagName: rel.TagName,
}
}
commit, err := gitRepo.GetCommit(rel.Target)
if err != nil {
return false, err
}
if len(msg) > 0 {
if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil {
if strings.Contains(err.Error(), "is not a valid tag name") {
return false, ErrInvalidTagName{
TagName: rel.TagName,
}
}
return false, err
}
} else if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil {
if strings.Contains(err.Error(), "is not a valid tag name") {
return false, ErrInvalidTagName{
TagName: rel.TagName,
}
}
return false, err
}
created = true
rel.LowerTagName = strings.ToLower(rel.TagName)
objectFormat := git.ObjectFormatFromName(rel.Repo.ObjectFormatName)
commits := repository.NewPushCommits()
commits.HeadCommit = repository.CommitToPushCommit(commit)
commits.CompareURL = rel.Repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), commit.ID.String())
refFullName := git.RefNameFromTag(rel.TagName)
notify_service.PushCommits(
ctx, rel.Publisher, rel.Repo,
&repository.PushUpdateOptions{
RefFullName: refFullName,
OldCommitID: objectFormat.EmptyObjectID().String(),
NewCommitID: commit.ID.String(),
}, commits)
notify_service.CreateRef(ctx, rel.Publisher, rel.Repo, refFullName, commit.ID.String())
rel.CreatedUnix = timeutil.TimeStampNow()
}
commit, err := gitRepo.GetTagCommit(rel.TagName)
if err != nil {
return false, fmt.Errorf("GetTagCommit: %w", err)
}
rel.Sha1 = commit.ID.String()
rel.NumCommits, err = gitrepo.CommitsCountOfCommit(ctx, rel.Repo, commit.ID.String())
if err != nil {
return false, fmt.Errorf("CommitsCount: %w", err)
}
if rel.PublisherID <= 0 {
u, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
if err == nil {
rel.PublisherID = u.ID
}
}
} else {
rel.CreatedUnix = timeutil.TimeStampNow()
}
return created, nil
}
// CreateRelease creates a new release of repository.
func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentUUIDs []string, msg string) error {
has, err := repo_model.IsReleaseExist(gitRepo.Ctx, rel.RepoID, rel.TagName)
if err != nil {
return err
} else if has {
return repo_model.ErrReleaseAlreadyExist{
TagName: rel.TagName,
}
}
if _, err = createTag(gitRepo.Ctx, gitRepo, rel, msg); err != nil {
return err
}
rel.Title = util.EllipsisDisplayString(rel.Title, 255)
rel.LowerTagName = strings.ToLower(rel.TagName)
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
return err
}
if err = repo_model.AddReleaseAttachments(gitRepo.Ctx, rel.ID, attachmentUUIDs); err != nil {
return err
}
if !rel.IsDraft {
notify_service.NewRelease(gitRepo.Ctx, rel)
}
return nil
}
// ErrTagAlreadyExists represents an error that tag with such name already exists.
type ErrTagAlreadyExists struct {
TagName string
}
// IsErrTagAlreadyExists checks if an error is an ErrTagAlreadyExists.
func IsErrTagAlreadyExists(err error) bool {
_, ok := err.(ErrTagAlreadyExists)
return ok
}
func (err ErrTagAlreadyExists) Error() string {
return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
}
func (err ErrTagAlreadyExists) Unwrap() error {
return util.ErrAlreadyExist
}
// CreateNewTag creates a new repository tag
func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commit, tagName, msg string) error {
has, err := repo_model.IsReleaseExist(ctx, repo.ID, tagName)
if err != nil {
return err
} else if has {
return ErrTagAlreadyExists{
TagName: tagName,
}
}
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
if err != nil {
return err
}
defer closer.Close()
rel := &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: doer.ID,
Publisher: doer,
TagName: tagName,
Target: commit,
IsDraft: false,
IsPrerelease: false,
IsTag: true,
}
if _, err = createTag(ctx, gitRepo, rel, msg); err != nil {
return err
}
return db.Insert(ctx, rel)
}
// UpdateRelease updates information, attachments of a release and will create tag if it's not a draft and tag not exist.
// addAttachmentUUIDs accept a slice of new created attachments' uuids which will be reassigned release_id as the created release
// delAttachmentUUIDs accept a slice of attachments' uuids which will be deleted from the release
// editAttachments accept a map of attachment uuid to new attachment name which will be updated with attachments.
func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, rel *repo_model.Release,
addAttachmentUUIDs, delAttachmentUUIDs []string, editAttachments map[string]string,
) error {
if rel.ID == 0 {
return errors.New("UpdateRelease only accepts an exist release")
}
isTagCreated, err := createTag(ctx, gitRepo, rel, "")
if err != nil {
return err
}
rel.LowerTagName = strings.ToLower(rel.TagName)
oldRelease, err := repo_model.GetReleaseByID(ctx, rel.ID)
if err != nil {
return err
}
isConvertedFromTag := oldRelease.IsTag && !rel.IsTag
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err = repo_model.UpdateRelease(ctx, rel); err != nil {
return err
}
if err = repo_model.AddReleaseAttachments(ctx, rel.ID, addAttachmentUUIDs); err != nil {
return fmt.Errorf("AddReleaseAttachments: %w", err)
}
deletedUUIDs := make(container.Set[string])
if len(delAttachmentUUIDs) > 0 {
// Check attachments
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs)
if err != nil {
return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", delAttachmentUUIDs, err)
}
for _, attach := range attachments {
if attach.ReleaseID != rel.ID {
return util.NewPermissionDeniedErrorf("delete attachment of release permission denied")
}
deletedUUIDs.Add(attach.UUID)
}
if _, err := repo_model.DeleteAttachments(ctx, attachments, true); err != nil {
return fmt.Errorf("DeleteAttachments [uuids: %v]: %w", delAttachmentUUIDs, err)
}
}
if len(editAttachments) > 0 {
updateAttachmentsList := make([]string, 0, len(editAttachments))
for k := range editAttachments {
updateAttachmentsList = append(updateAttachmentsList, k)
}
// Check attachments
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentsList)
if err != nil {
return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", updateAttachmentsList, err)
}
for _, attach := range attachments {
if attach.ReleaseID != rel.ID {
return util.NewPermissionDeniedErrorf("update attachment of release permission denied")
}
}
for uuid, newName := range editAttachments {
if !deletedUUIDs.Contains(uuid) {
if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{
UUID: uuid,
Name: newName,
}, "name"); err != nil {
return err
}
}
}
}
return nil
}); err != nil {
return err
}
for _, uuid := range delAttachmentUUIDs {
if err := storage.Attachments.Delete(repo_model.AttachmentRelativePath(uuid)); err != nil {
// Even delete files failed, but the attachments has been removed from database, so we
// should not return error but only record the error on logs.
// users have to delete this attachments manually or we should have a
// synchronize between database attachment table and attachment storage
log.Error("delete attachment[uuid: %s] failed: %v", uuid, err)
}
}
if !rel.IsDraft {
if !isTagCreated && !isConvertedFromTag {
notify_service.UpdateRelease(gitRepo.Ctx, doer, rel)
return nil
}
notify_service.NewRelease(gitRepo.Ctx, rel)
}
return nil
}
// DeleteReleaseByID deletes a release and corresponding Git tag by given ID.
func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *repo_model.Release, doer *user_model.User, delTag bool) error {
if delTag {
protectedTags, err := git_model.GetProtectedTags(ctx, rel.RepoID)
if err != nil {
return fmt.Errorf("GetProtectedTags: %w", err)
}
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
if err != nil {
return err
}
if !isAllowed {
return ErrProtectedTagName{
TagName: rel.TagName,
}
}
if stdout, _, err := gitrepo.RunCmdString(ctx, repo,
gitcmd.NewCommand("tag", "-d").AddDashesAndList(rel.TagName),
); err != nil && !strings.Contains(err.Error(), "not found") {
log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err)
return fmt.Errorf("git tag -d: %w", err)
}
refName := git.RefNameFromTag(rel.TagName)
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
notify_service.PushCommits(
ctx, doer, repo,
&repository.PushUpdateOptions{
RefFullName: refName,
OldCommitID: rel.Sha1,
NewCommitID: objectFormat.EmptyObjectID().String(),
}, repository.NewPushCommits())
notify_service.DeleteRef(ctx, doer, repo, refName)
if _, err := db.DeleteByID[repo_model.Release](ctx, rel.ID); err != nil {
return fmt.Errorf("DeleteReleaseByID: %w", err)
}
} else {
rel.IsTag = true
if err := repo_model.UpdateRelease(ctx, rel); err != nil {
return fmt.Errorf("Update: %w", err)
}
}
rel.Repo = repo
if err := rel.LoadAttributes(ctx); err != nil {
return fmt.Errorf("LoadAttributes: %w", err)
}
if err := repo_model.DeleteAttachmentsByRelease(ctx, rel.ID); err != nil {
return fmt.Errorf("DeleteAttachments: %w", err)
}
for i := range rel.Attachments {
attachment := rel.Attachments[i]
if err := storage.Attachments.Delete(attachment.RelativePath()); err != nil {
log.Error("Delete attachment %s of release %s failed: %v", attachment.UUID, rel.ID, err)
}
}
if !rel.IsDraft {
notify_service.DeleteRelease(ctx, doer, rel)
}
return nil
}
// Init start release service
func Init() error {
return initTagSyncQueue(graceful.GetManager().ShutdownContext())
}
+362
View File
@@ -0,0 +1,362 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package release
import (
"strings"
"testing"
"time"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
"gitea.dev/services/attachment"
_ "gitea.dev/models/actions"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
func TestRelease_Create(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 := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.1",
Target: "master",
Title: "v0.1 is released",
Note: "v0.1 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: false,
}, nil, ""))
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.1.1",
Target: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
Title: "v0.1.1 is released",
Note: "v0.1.1 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: false,
}, nil, ""))
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.1.2",
Target: "65f1bf2",
Title: "v0.1.2 is released",
Note: "v0.1.2 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: false,
}, nil, ""))
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.1.3",
Target: "65f1bf2",
Title: "v0.1.3 is released",
Note: "v0.1.3 is released",
IsDraft: true,
IsPrerelease: false,
IsTag: false,
}, nil, ""))
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.1.4",
Target: "65f1bf2",
Title: "v0.1.4 is released",
Note: "v0.1.4 is released",
IsDraft: false,
IsPrerelease: true,
IsTag: false,
}, nil, ""))
testPlayload := "testtest"
attach, err := attachment.NewAttachment(t.Context(), &repo_model.Attachment{
RepoID: repo.ID,
UploaderID: user.ID,
Name: "test.txt",
}, strings.NewReader(testPlayload), int64(len([]byte(testPlayload))))
assert.NoError(t, err)
release := repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.1.5",
Target: "65f1bf2",
Title: "v0.1.5 is released",
Note: "v0.1.5 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: true,
}
assert.NoError(t, CreateRelease(gitRepo, &release, []string{attach.UUID}, "test"))
}
func TestRelease_Update(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 := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
// Test a changed release
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v1.1.1",
Target: "master",
Title: "v1.1.1 is released",
Note: "v1.1.1 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: false,
}, nil, ""))
release, err := repo_model.GetRelease(t.Context(), repo.ID, "v1.1.1")
assert.NoError(t, err)
releaseCreatedUnix := release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
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))
// Test a changed draft
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v1.2.1",
Target: "65f1bf2",
Title: "v1.2.1 is draft",
Note: "v1.2.1 is draft",
IsDraft: true,
IsPrerelease: false,
IsTag: false,
}, nil, ""))
release, err = repo_model.GetRelease(t.Context(), repo.ID, "v1.2.1")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
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.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed pre-release
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v1.3.1",
Target: "65f1bf2",
Title: "v1.3.1 is pre-released",
Note: "v1.3.1 is pre-released",
IsDraft: false,
IsPrerelease: true,
IsTag: false,
}, nil, ""))
release, err = repo_model.GetRelease(t.Context(), repo.ID, "v1.3.1")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
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))
// Test create release
release = &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v1.1.2",
Target: "master",
Title: "v1.1.2 is released",
Note: "v1.1.2 is released",
IsDraft: true,
IsPrerelease: false,
IsTag: false,
}
assert.NoError(t, CreateRelease(gitRepo, release, nil, ""))
assert.Positive(t, release.ID)
release.IsDraft = false
tagName := release.TagName
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, tagName, release.TagName)
// Add new attachments
samplePayload := "testtest"
attach, err := attachment.NewAttachment(t.Context(), &repo_model.Attachment{
RepoID: repo.ID,
UploaderID: user.ID,
Name: "test.txt",
}, strings.NewReader(samplePayload), int64(len([]byte(samplePayload))))
assert.NoError(t, err)
assert.NoError(t, UpdateRelease(t.Context(), user, gitRepo, release, []string{attach.UUID}, nil, nil))
assert.NoError(t, repo_model.GetReleaseAttachments(t.Context(), release))
assert.Len(t, release.Attachments, 1)
assert.Equal(t, attach.UUID, release.Attachments[0].UUID)
assert.Equal(t, release.ID, release.Attachments[0].ReleaseID)
assert.Equal(t, attach.Name, release.Attachments[0].Name)
// update the attachment name
assert.NoError(t, UpdateRelease(t.Context(), user, gitRepo, release, nil, nil, map[string]string{
attach.UUID: "test2.txt",
}))
release.Attachments = nil
assert.NoError(t, repo_model.GetReleaseAttachments(t.Context(), release))
assert.Len(t, release.Attachments, 1)
assert.Equal(t, attach.UUID, release.Attachments[0].UUID)
assert.Equal(t, release.ID, release.Attachments[0].ReleaseID)
assert.Equal(t, "test2.txt", release.Attachments[0].Name)
// delete the attachment
assert.NoError(t, UpdateRelease(t.Context(), user, gitRepo, release, nil, []string{attach.UUID}, nil))
release.Attachments = nil
assert.NoError(t, repo_model.GetReleaseAttachments(t.Context(), release))
assert.Empty(t, release.Attachments)
}
func TestRelease_createTag(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 := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
// Test a changed release
release := &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v2.1.1",
Target: "master",
Title: "v2.1.1 is released",
Note: "v2.1.1 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: false,
}
_, err = createTag(t.Context(), gitRepo, release, "")
assert.NoError(t, err)
assert.NotEmpty(t, release.CreatedUnix)
releaseCreatedUnix := release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Note = "Changed note"
_, err = createTag(t.Context(), gitRepo, release, "")
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed draft
release = &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v2.2.1",
Target: "65f1bf2",
Title: "v2.2.1 is draft",
Note: "v2.2.1 is draft",
IsDraft: true,
IsPrerelease: false,
IsTag: false,
}
_, err = createTag(t.Context(), gitRepo, release, "")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
_, err = createTag(t.Context(), gitRepo, release, "")
assert.NoError(t, err)
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed pre-release
release = &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v2.3.1",
Target: "65f1bf2",
Title: "v2.3.1 is pre-released",
Note: "v2.3.1 is pre-released",
IsDraft: false,
IsPrerelease: true,
IsTag: false,
}
_, err = createTag(t.Context(), gitRepo, release, "")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
release.Note = "Changed note"
_, err = createTag(t.Context(), gitRepo, release, "")
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
}
func TestCreateNewTag(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})
assert.NoError(t, CreateNewTag(t.Context(), user, repo, "master", "v2.0",
"v2.0 is released \n\n BUGFIX: .... \n\n 123"))
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package release
import (
"context"
"errors"
"fmt"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/queue"
repo_module "gitea.dev/modules/repository"
"xorm.io/builder"
)
type TagSyncOptions struct {
RepoID int64
}
// tagSyncQueue represents a queue to handle tag sync jobs.
var tagSyncQueue *queue.WorkerPoolQueue[*TagSyncOptions]
func handlerTagSync(items ...*TagSyncOptions) []*TagSyncOptions {
for _, opts := range items {
err := repo_module.SyncRepoTags(graceful.GetManager().ShutdownContext(), opts.RepoID)
if err != nil {
log.Error("syncRepoTags [%d] failed: %v", opts.RepoID, err)
}
}
return nil
}
func addRepoToTagSyncQueue(repoID int64) error {
return tagSyncQueue.Push(&TagSyncOptions{
RepoID: repoID,
})
}
func initTagSyncQueue(ctx context.Context) error {
tagSyncQueue = queue.CreateUniqueQueue(ctx, "tag_sync", handlerTagSync)
if tagSyncQueue == nil {
return errors.New("unable to create tag_sync queue")
}
go graceful.GetManager().RunWithCancel(tagSyncQueue)
return nil
}
func AddAllRepoTagsToSyncQueue(ctx context.Context) error {
if err := db.Iterate(ctx, builder.Eq{"is_empty": false}, func(ctx context.Context, repo *repo_model.Repository) error {
return addRepoToTagSyncQueue(repo.ID)
}); err != nil {
return fmt.Errorf("run sync all tags failed: %v", err)
}
return nil
}