初始提交: 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
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestArchiveType(t *testing.T) {
name, archiveType := SplitArchiveNameType("test.tar.gz")
assert.Equal(t, "test", name)
assert.Equal(t, "tar.gz", archiveType.String())
name, archiveType = SplitArchiveNameType("a/b/test.zip")
assert.Equal(t, "a/b/test", name)
assert.Equal(t, "zip", archiveType.String())
name, archiveType = SplitArchiveNameType("1234.bundle")
assert.Equal(t, "1234", name)
assert.Equal(t, "bundle", archiveType.String())
name, archiveType = SplitArchiveNameType("test")
assert.Equal(t, "test", name)
assert.Equal(t, "unknown", archiveType.String())
name, archiveType = SplitArchiveNameType("test.xz")
assert.Equal(t, "test.xz", name)
assert.Equal(t, "unknown", archiveType.String())
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ArchiverStatus represents repo archive status
type ArchiverStatus int
// enumerate all repo archive statuses
const (
ArchiverGenerating = iota // the archiver is generating
ArchiverReady // it's ready
)
// ArchiveType archive types
type ArchiveType int
const (
ArchiveUnknown ArchiveType = iota
ArchiveZip // 1
ArchiveTarGz // 2
ArchiveBundle // 3
)
// String converts an ArchiveType to string: the extension of the archive file without prefix dot
func (a ArchiveType) String() string {
switch a {
case ArchiveZip:
return "zip"
case ArchiveTarGz:
return "tar.gz"
case ArchiveBundle:
return "bundle"
}
return "unknown"
}
func SplitArchiveNameType(s string) (string, ArchiveType) {
switch {
case strings.HasSuffix(s, ".zip"):
return strings.TrimSuffix(s, ".zip"), ArchiveZip
case strings.HasSuffix(s, ".tar.gz"):
return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz
case strings.HasSuffix(s, ".bundle"):
return strings.TrimSuffix(s, ".bundle"), ArchiveBundle
}
return s, ArchiveUnknown
}
// RepoArchiver represents all archivers
type RepoArchiver struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"index unique(s)"`
Type ArchiveType `xorm:"unique(s)"`
Status ArchiverStatus
CommitID string `xorm:"VARCHAR(64) unique(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
}
func init() {
db.RegisterModel(new(RepoArchiver))
}
// RelativePath returns the archive path relative to the archive storage root.
func (archiver *RepoArchiver) RelativePath() string {
return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String())
}
// repoArchiverForRelativePath takes a relativePath created from (archiver *RepoArchiver) RelativePath() and creates a shell repoArchiver struct representing it
func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) {
parts := strings.SplitN(relativePath, "/", 3)
if len(parts) != 3 {
return nil, util.NewInvalidArgumentErrorf("invalid storage path: must have 3 parts")
}
repoID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid repo id")
}
commitID, archiveType := SplitArchiveNameType(parts[2])
if archiveType == ArchiveUnknown {
return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid archive type")
}
return &RepoArchiver{RepoID: repoID, CommitID: commitID, Type: archiveType}, nil
}
// GetRepoArchiver get an archiver
func GetRepoArchiver(ctx context.Context, repoID int64, tp ArchiveType, commitID string) (*RepoArchiver, error) {
var archiver RepoArchiver
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver)
if err != nil {
return nil, err
}
if has {
return &archiver, nil
}
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
}
// ExistsRepoArchiverWithStoragePath checks if there is a RepoArchiver for a given storage path
func ExistsRepoArchiverWithStoragePath(ctx context.Context, storagePath string) (bool, error) {
// We need to invert the path provided func (archiver *RepoArchiver) RelativePath() above
archiver, err := repoArchiverForRelativePath(storagePath)
if err != nil {
return false, err
}
return db.GetEngine(ctx).Exist(archiver)
}
// UpdateRepoArchiverStatus updates archiver's status
func UpdateRepoArchiverStatus(ctx context.Context, archiver *RepoArchiver) error {
_, err := db.GetEngine(ctx).ID(archiver.ID).Cols("status").Update(archiver)
return err
}
// DeleteAllRepoArchives deletes all repo archives records
func DeleteAllRepoArchives(ctx context.Context) error {
// 1=1 to enforce delete all data, otherwise it will delete nothing
_, err := db.GetEngine(ctx).Where("1=1").Delete(new(RepoArchiver))
return err
}
// FindRepoArchiversOption represents an archiver options
type FindRepoArchiversOption struct {
db.ListOptions
OlderThan time.Duration
}
func (opts FindRepoArchiversOption) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.OlderThan > 0 {
cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-opts.OlderThan).Unix()})
}
return cond
}
func (opts FindRepoArchiversOption) ToOrders() string {
return "created_unix ASC"
}
// SetArchiveRepoState sets if a repo is archived
func SetArchiveRepoState(ctx context.Context, repo *Repository, isArchived bool) (err error) {
repo.IsArchived = isArchived
if isArchived {
repo.ArchivedUnix = timeutil.TimeStampNow()
} else {
repo.ArchivedUnix = timeutil.TimeStamp(0)
}
_, err = db.GetEngine(ctx).ID(repo.ID).Cols("is_archived", "archived_unix").NoAutoTime().Update(repo)
return err
}
+268
View File
@@ -0,0 +1,268 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
)
// Attachment represent a attachment of issue/comment/release.
type Attachment struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
RepoID int64 `xorm:"INDEX"` // this should not be zero
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
CommentID int64 `xorm:"INDEX"`
Name string
DownloadCount int64 `xorm:"DEFAULT 0"`
Size int64 `xorm:"DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
CustomDownloadURL string `xorm:"-"`
}
func init() {
db.RegisterModel(new(Attachment))
}
// IncreaseDownloadCount is update download count + 1
func (a *Attachment) IncreaseDownloadCount(ctx context.Context) error {
// Update download count.
if _, err := db.GetEngine(ctx).Exec("UPDATE `attachment` SET download_count=download_count+1 WHERE id=?", a.ID); err != nil {
return fmt.Errorf("increase attachment count: %w", err)
}
return nil
}
// AttachmentRelativePath returns the relative path
func AttachmentRelativePath(uuid string) string {
return path.Join(uuid[0:1], uuid[1:2], uuid)
}
// RelativePath returns the relative path of the attachment
func (a *Attachment) RelativePath() string {
return AttachmentRelativePath(a.UUID)
}
// DownloadURL returns the download url of the attached file
func (a *Attachment) DownloadURL() string {
if a.CustomDownloadURL != "" {
return a.CustomDownloadURL
}
return setting.AppURL + "attachments/" + url.PathEscape(a.UUID)
}
// ErrAttachmentNotExist represents a "AttachmentNotExist" kind of error.
type ErrAttachmentNotExist struct {
ID int64
UUID string
}
// IsErrAttachmentNotExist checks if an error is a ErrAttachmentNotExist.
func IsErrAttachmentNotExist(err error) bool {
_, ok := err.(ErrAttachmentNotExist)
return ok
}
func (err ErrAttachmentNotExist) Error() string {
return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
}
func (err ErrAttachmentNotExist) Unwrap() error {
return util.ErrNotExist
}
// GetAttachmentByID returns attachment by given id
func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) {
attach := &Attachment{}
if has, err := db.GetEngine(ctx).ID(id).Get(attach); err != nil {
return nil, err
} else if !has {
return nil, ErrAttachmentNotExist{ID: id, UUID: ""}
}
return attach, nil
}
// GetAttachmentByUUID returns attachment by given UUID.
func GetAttachmentByUUID(ctx context.Context, uuid string) (*Attachment, error) {
attach := &Attachment{}
has, err := db.GetEngine(ctx).Where("uuid=?", uuid).Get(attach)
if err != nil {
return nil, err
} else if !has {
return nil, ErrAttachmentNotExist{0, uuid}
}
return attach, nil
}
// GetAttachmentsByUUIDs returns attachment by given UUID list.
func GetAttachmentsByUUIDs(ctx context.Context, uuids []string) ([]*Attachment, error) {
if len(uuids) == 0 {
return []*Attachment{}, nil
}
// Silently drop invalid uuids.
attachments := make([]*Attachment, 0, len(uuids))
return attachments, db.GetEngine(ctx).In("uuid", uuids).Find(&attachments)
}
// ExistAttachmentsByUUID returns true if attachment exists with the given UUID
func ExistAttachmentsByUUID(ctx context.Context, uuid string) (bool, error) {
return db.GetEngine(ctx).Where("`uuid`=?", uuid).Exist(new(Attachment))
}
// GetAttachmentsByIssueID returns all attachments of an issue.
func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
}
// GetAttachmentsByIssueIDImagesLatest returns the latest image attachments of an issue.
func GetAttachmentsByIssueIDImagesLatest(ctx context.Context, issueID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 5)
return attachments, db.GetEngine(ctx).Where(`issue_id = ? AND (name like '%.apng'
OR name like '%.avif'
OR name like '%.bmp'
OR name like '%.gif'
OR name like '%.jpg'
OR name like '%.jpeg'
OR name like '%.jxl'
OR name like '%.png'
OR name like '%.svg'
OR name like '%.webp')`, issueID).Desc("comment_id").Limit(5).Find(&attachments)
}
// GetAttachmentsByCommentID returns all attachments if comment by given ID.
func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, db.GetEngine(ctx).Where("comment_id=?", commentID).Find(&attachments)
}
// GetAttachmentByReleaseIDFileName returns attachment by given releaseId and fileName.
func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, fileName string) (*Attachment, error) {
attach := &Attachment{ReleaseID: releaseID, Name: fileName}
has, err := db.GetEngine(ctx).Get(attach)
if err != nil {
return nil, err
} else if !has {
return nil, err
}
return attach, nil
}
func GetUnlinkedAttachmentsByUserID(ctx context.Context, userID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, db.GetEngine(ctx).Where("uploader_id = ? AND issue_id = 0 AND release_id = 0 AND comment_id = 0", userID).Find(&attachments)
}
// DeleteAttachment deletes the given attachment and optionally the associated file.
func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error {
_, err := DeleteAttachments(ctx, []*Attachment{a}, remove)
return err
}
// DeleteAttachments deletes the given attachments and optionally the associated files.
func DeleteAttachments(ctx context.Context, attachments []*Attachment, remove bool) (int, error) {
if len(attachments) == 0 {
return 0, nil
}
ids := make([]int64, 0, len(attachments))
for _, a := range attachments {
ids = append(ids, a.ID)
}
cnt, err := db.GetEngine(ctx).In("id", ids).NoAutoCondition().Delete(attachments[0])
if err != nil {
return 0, err
}
if remove {
for i, a := range attachments {
if err := storage.Attachments.Delete(a.RelativePath()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return i, err
}
log.Warn("Attachment file not found when deleting: %s", a.RelativePath())
}
}
}
return int(cnt), nil
}
// DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
func DeleteAttachmentsByIssue(ctx context.Context, issueID int64, remove bool) (int, error) {
attachments, err := GetAttachmentsByIssueID(ctx, issueID)
if err != nil {
return 0, err
}
return DeleteAttachments(ctx, attachments, remove)
}
// DeleteAttachmentsByComment deletes all attachments associated with the given comment.
func DeleteAttachmentsByComment(ctx context.Context, commentID int64, remove bool) (int, error) {
attachments, err := GetAttachmentsByCommentID(ctx, commentID)
if err != nil {
return 0, err
}
return DeleteAttachments(ctx, attachments, remove)
}
// UpdateAttachmentByUUID Updates attachment via uuid
func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...string) error {
if attach.UUID == "" {
return errors.New("attachment uuid should be not blank")
}
_, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
return err
}
// UpdateAttachment updates the given attachment in database
func UpdateAttachment(ctx context.Context, atta *Attachment) error {
sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count")
if atta.ID != 0 && atta.UUID == "" {
sess = sess.ID(atta.ID)
} else {
// Use uuid only if id is not set and uuid is set
sess = sess.Where("uuid = ?", atta.UUID)
}
_, err := sess.Update(atta)
return err
}
// DeleteAttachmentsByRelease deletes all attachments associated with the given release.
func DeleteAttachmentsByRelease(ctx context.Context, releaseID int64) error {
_, err := db.GetEngine(ctx).Where("release_id = ?", releaseID).Delete(&Attachment{})
return err
}
// CountOrphanedAttachments returns the number of bad attachments
func CountOrphanedAttachments(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where("(issue_id > 0 and issue_id not in (select id from issue)) or (release_id > 0 and release_id not in (select id from `release`))").
Count(new(Attachment))
}
// DeleteOrphanedAttachments delete all bad attachments
func DeleteOrphanedAttachments(ctx context.Context) error {
_, err := db.GetEngine(ctx).Where("(issue_id > 0 and issue_id not in (select id from issue)) or (release_id > 0 and release_id not in (select id from `release`))").
Delete(new(Attachment))
return err
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestIncreaseDownloadCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
attachment, err := repo_model.GetAttachmentByUUID(t.Context(), "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
assert.NoError(t, err)
assert.Equal(t, int64(0), attachment.DownloadCount)
// increase download count
err = attachment.IncreaseDownloadCount(t.Context())
assert.NoError(t, err)
attachment, err = repo_model.GetAttachmentByUUID(t.Context(), "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
assert.NoError(t, err)
assert.Equal(t, int64(1), attachment.DownloadCount)
}
func TestGetByCommentOrIssueID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// count of attachments from issue ID
attachments, err := repo_model.GetAttachmentsByIssueID(t.Context(), 1)
assert.NoError(t, err)
assert.Len(t, attachments, 1)
attachments, err = repo_model.GetAttachmentsByCommentID(t.Context(), 1)
assert.NoError(t, err)
assert.Len(t, attachments, 2)
}
func TestDeleteAttachments(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
count, err := repo_model.DeleteAttachmentsByIssue(t.Context(), 4, false)
assert.NoError(t, err)
assert.Equal(t, 2, count)
count, err = repo_model.DeleteAttachmentsByComment(t.Context(), 2, false)
assert.NoError(t, err)
assert.Equal(t, 2, count)
err = repo_model.DeleteAttachment(t.Context(), &repo_model.Attachment{ID: 8}, false)
assert.NoError(t, err)
attachment, err := repo_model.GetAttachmentByUUID(t.Context(), "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18")
assert.Error(t, err)
assert.True(t, repo_model.IsErrAttachmentNotExist(err))
assert.Nil(t, attachment)
}
func TestGetAttachmentByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
attach, err := repo_model.GetAttachmentByID(t.Context(), 1)
assert.NoError(t, err)
assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.UUID)
}
func TestAttachment_DownloadURL(t *testing.T) {
attach := &repo_model.Attachment{
UUID: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
ID: 1,
}
assert.Equal(t, "https://try.gitea.io/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.DownloadURL())
}
func TestUpdateAttachment(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
attach, err := repo_model.GetAttachmentByID(t.Context(), 1)
assert.NoError(t, err)
assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.UUID)
attach.Name = "new_name"
assert.NoError(t, repo_model.UpdateAttachment(t.Context(), attach))
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{Name: "new_name"})
}
func TestGetAttachmentsByUUIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
attachList, err := repo_model.GetAttachmentsByUUIDs(t.Context(), []string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"})
assert.NoError(t, err)
assert.Len(t, attachList, 2)
assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attachList[0].UUID)
assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", attachList[1].UUID)
assert.Equal(t, int64(1), attachList[0].IssueID)
assert.Equal(t, int64(5), attachList[1].IssueID)
}
func TestGetUnlinkedAttachmentsByUserID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
attachments, err := repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 8)
assert.NoError(t, err)
assert.Len(t, attachments, 1)
assert.Equal(t, int64(10), attachments[0].ID)
assert.Zero(t, attachments[0].IssueID)
assert.Zero(t, attachments[0].ReleaseID)
assert.Zero(t, attachments[0].CommentID)
attachments, err = repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 1)
assert.NoError(t, err)
assert.Empty(t, attachments)
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"image/png"
"io"
"net/url"
"strconv"
"gitea.dev/models/db"
"gitea.dev/modules/avatar"
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
)
// CustomAvatarRelativePath returns repository custom avatar file path.
func (repo *Repository) CustomAvatarRelativePath() string {
return repo.Avatar
}
// ExistsWithAvatarAtStoragePath returns true if there is a user with this Avatar
func ExistsWithAvatarAtStoragePath(ctx context.Context, storagePath string) (bool, error) {
// See func (repo *Repository) CustomAvatarRelativePath()
// repo.Avatar is used directly as the storage path - therefore we can check for existence directly using the path
return db.GetEngine(ctx).Where("`avatar`=?", storagePath).Exist(new(Repository))
}
// RelAvatarLink returns a relative link to the repository's avatar.
func (repo *Repository) RelAvatarLink(ctx context.Context) string {
return repo.relAvatarLink(ctx)
}
// generateRandomAvatar generates a random avatar for repository.
func generateRandomAvatar(ctx context.Context, repo *Repository) error {
idToString := strconv.FormatInt(repo.ID, 10)
seed := idToString
img := avatar.RandomImageDefaultSize([]byte(seed))
repo.Avatar = idToString
if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error {
return png.Encode(w, img)
}); err != nil {
return fmt.Errorf("failed to create dir %s: %w", repo.CustomAvatarRelativePath(), err)
}
log.Info("New random avatar created for repository: %d", repo.ID)
if _, err := db.GetEngine(ctx).ID(repo.ID).Cols("avatar").NoAutoTime().Update(repo); err != nil {
return err
}
return nil
}
func (repo *Repository) relAvatarLink(ctx context.Context) string {
// If no avatar - path is empty
avatarPath := repo.CustomAvatarRelativePath()
if len(avatarPath) == 0 {
switch mode := setting.RepoAvatar.Fallback; mode {
case "image":
return setting.RepoAvatar.FallbackImage
case "random":
if err := generateRandomAvatar(ctx, repo); err != nil {
log.Error("generateRandomAvatar: %v", err)
}
default:
// default behaviour: do not display avatar
return ""
}
}
return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
}
// AvatarLink returns the full avatar url with http host or the empty string if the repo doesn't have an avatar.
//
// TODO: refactor it to a relative URL, but it is still used in API response at the moment
func (repo *Repository) AvatarLink(ctx context.Context) string {
relLink := repo.relAvatarLink(ctx)
if relLink != "" {
return httplib.MakeAbsoluteURL(ctx, relLink)
}
return ""
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
func TestRepoAvatarLink(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "https://localhost/")()
defer test.MockVariableValue(&setting.AppSubURL, "")()
repo := &Repository{ID: 1, Avatar: "avatar.png"}
link := repo.AvatarLink(t.Context())
assert.Equal(t, "https://localhost/repo-avatars/avatar.png", link)
setting.AppURL = "https://localhost/sub-path/"
setting.AppSubURL = "/sub-path"
link = repo.AvatarLink(t.Context())
assert.Equal(t, "https://localhost/sub-path/repo-avatars/avatar.png", link)
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// Collaboration represent the relation between an individual and a repository.
type Collaboration struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
Mode perm.AccessMode `xorm:"DEFAULT 2 NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
func init() {
db.RegisterModel(new(Collaboration))
}
// Collaborator represents a user with collaboration details.
type Collaborator struct {
*user_model.User
Collaboration *Collaboration
}
type FindCollaborationOptions struct {
db.ListOptions
RepoID int64
RepoOwnerID int64
CollaboratorID int64
}
func (opts *FindCollaborationOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
}
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
}
if opts.CollaboratorID != 0 {
cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
}
return cond
}
func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
if opts.RepoOwnerID != 0 {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
return nil
},
}
}
return nil
}
// GetCollaborators returns the collaborators for a repository
func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
if err != nil {
return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
}
collaborators := make([]*Collaborator, 0, len(collaborations))
userIDs := make([]int64, 0, len(collaborations))
for _, c := range collaborations {
userIDs = append(userIDs, c.UserID)
}
usersMap := make(map[int64]*user_model.User)
if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
}
for _, c := range collaborations {
u := usersMap[c.UserID]
if u == nil {
u = user_model.NewGhostUser()
}
collaborators = append(collaborators, &Collaborator{
User: u,
Collaboration: c,
})
}
return collaborators, total, nil
}
// GetCollaboration get collaboration for a repository id with a user id
func GetCollaboration(ctx context.Context, repoID, uid int64) (*Collaboration, error) {
collaboration := &Collaboration{
RepoID: repoID,
UserID: uid,
}
has, err := db.GetEngine(ctx).Get(collaboration)
if !has {
collaboration = nil
}
return collaboration, err
}
// IsCollaborator check if a user is a collaborator of a repository
func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
}
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
// Discard invalid input
if mode <= perm.AccessModeNone || mode > perm.AccessModeOwner {
return nil
}
return db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: uid,
}
has, err := e.Get(collaboration)
if err != nil {
return fmt.Errorf("get collaboration: %w", err)
} else if !has {
return nil
}
if collaboration.Mode == mode {
return nil
}
collaboration.Mode = mode
if _, err = e.
ID(collaboration.ID).
Cols("mode").
Update(collaboration); err != nil {
return fmt.Errorf("update collaboration: %w", err)
} else if _, err = e.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
return fmt.Errorf("update access table: %w", err)
}
return nil
})
}
// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int64) (bool, error) {
if repo.OwnerID == userID {
return true, nil
}
teamMember, err := db.GetEngine(ctx).Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id").
Where("team_repo.repo_id = ?", repo.ID).
And("team_unit.`type` = ?", unit.TypeCode).
And("team_user.uid = ?", userID).Table("team_user").Exist()
if err != nil {
return false, err
}
if teamMember {
return true, nil
}
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repo.ID, UserID: userID})
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestRepository_GetCollaborators(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
collaborators, _, err := repo_model.GetCollaborators(t.Context(), &repo_model.FindCollaborationOptions{RepoID: repo.ID})
assert.NoError(t, err)
expectedLen, err := db.GetEngine(t.Context()).Count(&repo_model.Collaboration{RepoID: repoID})
assert.NoError(t, err)
assert.Len(t, collaborators, int(expectedLen))
for _, collaborator := range collaborators {
assert.Equal(t, collaborator.User.ID, collaborator.Collaboration.UserID)
assert.Equal(t, repoID, collaborator.Collaboration.RepoID)
}
}
test(1)
test(2)
test(3)
test(4)
// Test db.ListOptions
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
collaborators1, _, err := repo_model.GetCollaborators(t.Context(), &repo_model.FindCollaborationOptions{
ListOptions: db.ListOptions{PageSize: 1, Page: 1},
RepoID: repo.ID,
})
assert.NoError(t, err)
assert.Len(t, collaborators1, 1)
collaborators2, _, err := repo_model.GetCollaborators(t.Context(), &repo_model.FindCollaborationOptions{
ListOptions: db.ListOptions{PageSize: 1, Page: 2},
RepoID: repo.ID,
})
assert.NoError(t, err)
assert.Len(t, collaborators2, 1)
assert.NotEqual(t, collaborators1[0].ID, collaborators2[0].ID)
}
func TestRepository_IsCollaborator(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID, userID int64, expected bool) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
actual, err := repo_model.IsCollaborator(t.Context(), repo.ID, userID)
assert.NoError(t, err)
assert.Equal(t, expected, actual)
}
test(3, 2, true)
test(3, unittest.NonexistentID, false)
test(4, 2, false)
test(4, 4, true)
}
func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, 4, perm.AccessModeAdmin))
collaboration := unittest.AssertExistsAndLoadBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
assert.Equal(t, perm.AccessModeAdmin, collaboration.Mode)
access := unittest.AssertExistsAndLoadBean(t, &access_model.Access{UserID: 4, RepoID: repo.ID})
assert.Equal(t, perm.AccessModeAdmin, access.Mode)
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, 4, perm.AccessModeAdmin))
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, unittest.NonexistentID, perm.AccessModeAdmin))
// Discard invalid input.
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, 4, perm.AccessMode(-1)))
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
// Organisation owner.
actual, err := repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 2)
assert.NoError(t, err)
assert.True(t, actual)
// Team member.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 4)
assert.NoError(t, err)
assert.True(t, actual)
// Normal user.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 1)
assert.NoError(t, err)
assert.False(t, actual)
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
// Collaborator.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo2, 4)
assert.NoError(t, err)
assert.True(t, actual)
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15})
// Repository owner.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo3, 2)
assert.NoError(t, err)
assert.True(t, actual)
}
func TestRepo_GetCollaboration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
// Existing collaboration.
collab, err := repo_model.GetCollaboration(t.Context(), repo.ID, 4)
assert.NoError(t, err)
assert.NotNil(t, collab)
assert.EqualValues(t, 4, collab.UserID)
assert.EqualValues(t, 4, collab.RepoID)
// Non-existing collaboration.
collab, err = repo_model.GetCollaboration(t.Context(), repo.ID, 1)
assert.NoError(t, err)
assert.Nil(t, collab)
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"xorm.io/builder"
)
// GetRepositoriesByForkID returns all repositories with given fork ID.
func GetRepositoriesByForkID(ctx context.Context, forkID int64) ([]*Repository, error) {
repos := make([]*Repository, 0, 10)
return repos, db.GetEngine(ctx).
Where("fork_id=?", forkID).
Find(&repos)
}
// GetForkedRepo checks if given user has already forked a repository with given ID.
func GetForkedRepo(ctx context.Context, ownerID, repoID int64) *Repository {
repo := new(Repository)
has, _ := db.GetEngine(ctx).
Where("owner_id=? AND fork_id=?", ownerID, repoID).
Get(repo)
if has {
return repo
}
return nil
}
// HasForkedRepo checks if given user has already forked a repository with given ID.
func HasForkedRepo(ctx context.Context, ownerID, repoID int64) bool {
has, _ := db.GetEngine(ctx).
Table("repository").
Where("owner_id=? AND fork_id=?", ownerID, repoID).
Exist()
return has
}
// GetUserFork return user forked repository from this repository, if not forked return nil
func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error) {
var forkedRepo Repository
has, err := db.GetEngine(ctx).Where("fork_id = ?", repoID).And("owner_id = ?", userID).Get(&forkedRepo)
if err != nil {
return nil, err
}
if !has {
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
}
return &forkedRepo, nil
}
// IncrementRepoForkNum increment repository fork number
func IncrementRepoForkNum(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID)
return err
}
// DecrementRepoForkNum decrement repository fork number
func DecrementRepoForkNum(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repoID)
return err
}
// FindUserOrgForks returns the forked repositories for one user from a repository
func FindUserOrgForks(ctx context.Context, repoID, userID int64) ([]*Repository, error) {
cond := builder.And(
builder.Eq{"fork_id": repoID},
builder.In("owner_id",
builder.Select("org_id").
From("org_user").
Where(builder.Eq{"uid": userID}),
),
)
var repos []*Repository
return repos, db.GetEngine(ctx).Table("repository").Where(cond).Find(&repos)
}
// GetForksByUserAndOrgs return forked repos of the user and owned orgs
func GetForksByUserAndOrgs(ctx context.Context, user *user_model.User, repo *Repository) ([]*Repository, error) {
var repoList []*Repository
if user == nil {
return repoList, nil
}
forkedRepo, err := GetUserFork(ctx, repo.ID, user.ID)
if err != nil {
return repoList, err
}
if forkedRepo != nil {
repoList = append(repoList, forkedRepo)
}
orgForks, err := FindUserOrgForks(ctx, repo.ID, user.ID)
if err != nil {
return nil, err
}
repoList = append(repoList, orgForks...)
return repoList, nil
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetUserFork(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// User13 has repo 11 forked from repo10
repo, err := repo_model.GetRepositoryByID(t.Context(), 10)
assert.NoError(t, err)
assert.NotNil(t, repo)
repo, err = repo_model.GetUserFork(t.Context(), repo.ID, 13)
assert.NoError(t, err)
assert.NotNil(t, repo)
repo, err = repo_model.GetRepositoryByID(t.Context(), 9)
assert.NoError(t, err)
assert.NotNil(t, repo)
repo, err = repo_model.GetUserFork(t.Context(), repo.ID, 13)
assert.NoError(t, err)
assert.Nil(t, repo)
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/db"
)
// MergeStyle represents the approach to merge commits into base branch.
type MergeStyle string
const (
// MergeStyleMerge create merge commit
MergeStyleMerge MergeStyle = "merge"
// MergeStyleRebase rebase before merging, and fast-forward
MergeStyleRebase MergeStyle = "rebase"
// MergeStyleRebaseMerge rebase before merging with merge commit (--no-ff)
MergeStyleRebaseMerge MergeStyle = "rebase-merge"
// MergeStyleSquash squash commits into single commit before merging
MergeStyleSquash MergeStyle = "squash"
// MergeStyleFastForwardOnly fast-forward merge if possible, otherwise fail
MergeStyleFastForwardOnly MergeStyle = "fast-forward-only"
// MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
MergeStyleManuallyMerged MergeStyle = "manually-merged"
// MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase
MergeStyleRebaseUpdate MergeStyle = "rebase-update-only"
)
// UpdateStyle is a pull request branch update style
type UpdateStyle string
const (
// UpdateStyleMerge merges the base branch into the pull request branch
UpdateStyleMerge UpdateStyle = "merge"
// UpdateStyleRebase rebases the pull request branch onto the base branch
UpdateStyleRebase UpdateStyle = "rebase"
)
// UpdateDefaultBranch updates the default branch
func UpdateDefaultBranch(ctx context.Context, repo *Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID).Cols("default_branch").Update(repo)
return err
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/unit"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
// ___________.__ ___________ __
// \__ ___/|__| _____ ___\__ ___/___________ ____ | | __ ___________
// | | | |/ \_/ __ \| | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
// | | | | Y Y \ ___/| | | | \// __ \\ \___| <\ ___/| | \/
// |____| |__|__|_| /\___ >____| |__| (____ /\___ >__|_ \\___ >__|
// \/ \/ \/ \/ \/ \/
// CanEnableTimetracker returns true when the server admin enabled time tracking
// This overrules IsTimetrackerEnabled
func (repo *Repository) CanEnableTimetracker() bool {
return setting.Service.EnableTimetracking
}
// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs.
func (repo *Repository) IsTimetrackerEnabled(ctx context.Context) bool {
if !setting.Service.EnableTimetracking {
return false
}
var u *RepoUnit
var err error
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
return setting.Service.DefaultEnableTimetracking
}
return u.IssuesConfig().EnableTimetracker
}
// AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value
func (repo *Repository) AllowOnlyContributorsToTrackTime(ctx context.Context) bool {
var u *RepoUnit
var err error
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
return setting.Service.DefaultAllowOnlyContributorsToTrackTime
}
return u.IssuesConfig().AllowOnlyContributorsToTrackTime
}
// IsDependenciesEnabled returns if dependencies are enabled and returns the default setting if not set.
func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool {
var u *RepoUnit
var err error
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
log.Trace("IsDependenciesEnabled: %v", err)
return setting.Service.DefaultEnableDependencies
}
return u.IssuesConfig().EnableDependencies
}
+230
View File
@@ -0,0 +1,230 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"math"
"sort"
"strings"
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"github.com/go-enry/go-enry/v2"
)
// LanguageStat describes language statistics of a repository
type LanguageStat struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
CommitID string
IsPrimary bool
Language string `xorm:"VARCHAR(50) UNIQUE(s) INDEX NOT NULL"`
Percentage float32 `xorm:"-"`
Size int64 `xorm:"NOT NULL DEFAULT 0"`
Color string `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
}
func init() {
db.RegisterModel(new(LanguageStat))
}
// LanguageStatList defines a list of language statistics
type LanguageStatList []*LanguageStat
// LoadAttributes loads attributes
func (stats LanguageStatList) LoadAttributes() {
for i := range stats {
stats[i].Color = enry.GetColor(stats[i].Language)
}
}
func (stats LanguageStatList) getLanguagePercentages() map[string]float32 {
langPerc := make(map[string]float32)
var otherPerc float32
var total int64
for _, stat := range stats {
total += stat.Size
}
if total > 0 {
for _, stat := range stats {
perc := float32(float64(stat.Size) / float64(total) * 100)
if perc <= 0.1 {
otherPerc += perc
continue
}
langPerc[stat.Language] = perc
}
}
if otherPerc > 0 {
langPerc["other"] = otherPerc
}
roundByLargestRemainder(langPerc, 100)
return langPerc
}
// Rounds to 1 decimal point, target should be the expected sum of percs
func roundByLargestRemainder(percs map[string]float32, target float32) {
leftToDistribute := int(target * 10)
keys := make([]string, 0, len(percs))
for k, v := range percs {
percs[k] = v * 10
floored := math.Floor(float64(percs[k]))
leftToDistribute -= int(floored)
keys = append(keys, k)
}
// Sort the keys by the largest remainder
sort.SliceStable(keys, func(i, j int) bool {
_, remainderI := math.Modf(float64(percs[keys[i]]))
_, remainderJ := math.Modf(float64(percs[keys[j]]))
return remainderI > remainderJ
})
// Increment the values in order of largest remainder
for _, k := range keys {
percs[k] = float32(math.Floor(float64(percs[k])))
if leftToDistribute > 0 {
percs[k]++
leftToDistribute--
}
percs[k] /= 10
}
}
// GetLanguageStats returns the language statistics for a repository
func GetLanguageStats(ctx context.Context, repo *Repository) (LanguageStatList, error) {
stats := make(LanguageStatList, 0, 6)
if err := db.GetEngine(ctx).Where("`repo_id` = ?", repo.ID).Desc("`size`").Find(&stats); err != nil {
return nil, err
}
return stats, nil
}
// GetTopLanguageStats returns the top language statistics for a repository
func GetTopLanguageStats(ctx context.Context, repo *Repository, limit int) (LanguageStatList, error) {
stats, err := GetLanguageStats(ctx, repo)
if err != nil {
return nil, err
}
perc := stats.getLanguagePercentages()
topstats := make(LanguageStatList, 0, limit)
var other float32
for i := range stats {
if _, ok := perc[stats[i].Language]; !ok {
continue
}
if stats[i].Language == "other" || len(topstats) >= limit {
other += perc[stats[i].Language]
continue
}
stats[i].Percentage = perc[stats[i].Language]
topstats = append(topstats, stats[i])
}
if other > 0 {
topstats = append(topstats, &LanguageStat{
RepoID: repo.ID,
Language: "other",
Color: "#cccccc",
Percentage: float32(math.Round(float64(other)*10) / 10),
})
}
topstats.LoadAttributes()
return topstats, nil
}
// UpdateLanguageStats updates the language statistics for repository
func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string, stats map[string]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
oldstats, err := GetLanguageStats(ctx, repo)
if err != nil {
return err
}
var topLang string
var s int64
for lang, size := range stats {
if size > s {
s = size
topLang = lang
}
}
for lang, size := range stats {
upd := false
for _, s := range oldstats {
// Update already existing language
if strings.EqualFold(s.Language, lang) {
s.CommitID = commitID
s.IsPrimary = lang == topLang
s.Size = size
if _, err := sess.ID(s.ID).Cols("`commit_id`", "`size`", "`is_primary`").Update(s); err != nil {
return err
}
upd = true
break
}
}
// Insert new language
if !upd {
if err := db.Insert(ctx, &LanguageStat{
RepoID: repo.ID,
CommitID: commitID,
IsPrimary: lang == topLang,
Language: lang,
Size: size,
}); err != nil {
return err
}
}
}
// Delete old languages
statsToDelete := make([]int64, 0, len(oldstats))
for _, s := range oldstats {
if s.CommitID != commitID {
statsToDelete = append(statsToDelete, s.ID)
}
}
if len(statsToDelete) > 0 {
if _, err := sess.In("`id`", statsToDelete).Delete(&LanguageStat{}); err != nil {
return err
}
}
// Update indexer status
return UpdateIndexerStatus(ctx, repo, RepoIndexerTypeStats, commitID)
})
}
// CopyLanguageStat Copy originalRepo language stat information to destRepo (use for forked repo)
func CopyLanguageStat(ctx context.Context, originalRepo, destRepo *Repository) error {
return db.WithTx(ctx, func(ctx context.Context) error {
RepoLang := make(LanguageStatList, 0, 6)
if err := db.GetEngine(ctx).Where("`repo_id` = ?", originalRepo.ID).Desc("`size`").Find(&RepoLang); err != nil {
return err
}
if len(RepoLang) > 0 {
for i := range RepoLang {
RepoLang[i].ID = 0
RepoLang[i].RepoID = destRepo.ID
RepoLang[i].CreatedUnix = timeutil.TimeStampNow()
}
// update destRepo's indexer status
tmpCommitID := RepoLang[0].CommitID
if err := UpdateIndexerStatus(ctx, destRepo, RepoIndexerTypeStats, tmpCommitID); err != nil {
return err
}
if err := db.Insert(ctx, &RepoLang); err != nil {
return err
}
}
return nil
})
}
+121
View File
@@ -0,0 +1,121 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
)
func init() {
db.RegisterModel(new(RepoLicense))
}
type RepoLicense struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) NOT NULL"`
CommitID string
License string `xorm:"VARCHAR(255) UNIQUE(s) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"`
}
// RepoLicenseList defines a list of repo licenses
type RepoLicenseList []*RepoLicense //revive:disable-line:exported
func (rll RepoLicenseList) StringList() []string {
var licenses []string
for _, rl := range rll {
licenses = append(licenses, rl.License)
}
return licenses
}
// GetRepoLicenses returns the license statistics for a repository
func GetRepoLicenses(ctx context.Context, repo *Repository) (RepoLicenseList, error) {
licenses := make(RepoLicenseList, 0)
if err := db.GetEngine(ctx).Where("`repo_id` = ?", repo.ID).Asc("`license`").Find(&licenses); err != nil {
return nil, err
}
return licenses, nil
}
// UpdateRepoLicenses updates the license statistics for repository
func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string, licenses []string) error {
oldLicenses, err := GetRepoLicenses(ctx, repo)
if err != nil {
return err
}
for _, license := range licenses {
upd := false
for _, o := range oldLicenses {
// Update already existing license
if o.License == license {
o.CommitID = commitID
if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil {
return err
}
upd = true
break
}
}
// Insert new license
if !upd {
if err := db.Insert(ctx, &RepoLicense{
RepoID: repo.ID,
CommitID: commitID,
License: license,
}); err != nil {
return err
}
}
}
// Delete old licenses
licenseToDelete := make([]int64, 0, len(oldLicenses))
for _, o := range oldLicenses {
if o.CommitID != commitID {
licenseToDelete = append(licenseToDelete, o.ID)
}
}
if len(licenseToDelete) > 0 {
if _, err := db.GetEngine(ctx).In("`id`", licenseToDelete).Delete(&RepoLicense{}); err != nil {
return err
}
}
return nil
}
// CopyLicense Copy originalRepo license information to destRepo (use for forked repo)
func CopyLicense(ctx context.Context, originalRepo, destRepo *Repository) error {
repoLicenses, err := GetRepoLicenses(ctx, originalRepo)
if err != nil {
return err
}
if len(repoLicenses) > 0 {
newRepoLicenses := make(RepoLicenseList, 0, len(repoLicenses))
for _, rl := range repoLicenses {
newRepoLicense := &RepoLicense{
RepoID: destRepo.ID,
CommitID: rl.CommitID,
License: rl.License,
}
newRepoLicenses = append(newRepoLicenses, newRepoLicense)
}
if err := db.Insert(ctx, &newRepoLicenses); err != nil {
return err
}
}
return nil
}
// CleanRepoLicenses will remove all license record of the repo
func CleanRepoLicenses(ctx context.Context, repo *Repository) error {
return db.DeleteBeans(ctx, &RepoLicense{
RepoID: repo.ID,
})
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"gitea.dev/models/unittest"
_ "gitea.dev/models" // register table model
_ "gitea.dev/models/actions"
_ "gitea.dev/models/activities"
_ "gitea.dev/models/perm/access" // register table model
_ "gitea.dev/models/repo" // register table model
_ "gitea.dev/models/user" // register table model
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"time"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
)
// ErrMirrorNotExist mirror does not exist error
var ErrMirrorNotExist = util.NewNotExistErrorf("Mirror does not exist")
// Mirror represents mirror information of a repository.
type Mirror struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
Repo *Repository `xorm:"-"`
Interval time.Duration
EnablePrune bool `xorm:"NOT NULL DEFAULT true"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX"`
NextUpdateUnix timeutil.TimeStamp `xorm:"INDEX"`
LastSyncUnix timeutil.TimeStamp `xorm:"INDEX"`
LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"`
LFSEndpoint string `xorm:"lfs_endpoint TEXT"`
RemoteAddress string `xorm:"VARCHAR(2048)"`
}
func init() {
db.RegisterModel(new(Mirror))
}
// BeforeInsert will be invoked by XORM before inserting a record
func (m *Mirror) BeforeInsert() {
if m != nil {
m.UpdatedUnix = timeutil.TimeStampNow()
m.NextUpdateUnix = timeutil.TimeStampNow()
}
}
// GetRepository returns the repository.
func (m *Mirror) GetRepository(ctx context.Context) *Repository {
if m.Repo != nil {
return m.Repo
}
var err error
m.Repo, err = GetRepositoryByID(ctx, m.RepoID)
if err != nil {
log.Error("getRepositoryByID[%d]: %v", m.ID, err)
}
return m.Repo
}
// GetRemoteName returns the name of the remote.
func (m *Mirror) GetRemoteName() string {
return "origin"
}
// ScheduleNextUpdate calculates and sets next update time.
func (m *Mirror) ScheduleNextUpdate() {
if m.Interval != 0 {
m.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(m.Interval)
} else {
m.NextUpdateUnix = 0
}
}
// GetMirrorByRepoID returns mirror information of a repository.
func GetMirrorByRepoID(ctx context.Context, repoID int64) (*Mirror, error) {
m := &Mirror{RepoID: repoID}
has, err := db.GetEngine(ctx).Get(m)
if err != nil {
return nil, err
} else if !has {
return nil, ErrMirrorNotExist
}
return m, nil
}
// UpdateMirror updates the mirror
func UpdateMirror(ctx context.Context, m *Mirror) error {
_, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
return err
}
// TouchMirror updates the mirror updatedUnix
func TouchMirror(ctx context.Context, m *Mirror) error {
m.UpdatedUnix = timeutil.TimeStampNow()
_, err := db.GetEngine(ctx).ID(m.ID).Cols("updated_unix").Update(m)
return err
}
// DeleteMirrorByRepoID deletes a mirror by repoID
func DeleteMirrorByRepoID(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Delete(&Mirror{RepoID: repoID})
return err
}
// MirrorsIterate iterates all mirror repositories.
func MirrorsIterate(ctx context.Context, limit int, f func(idx int, bean any) error) error {
sess := db.GetEngine(ctx).
Where("next_update_unix<=?", time.Now().Unix()).
And("next_update_unix!=0").
OrderBy("updated_unix ASC")
if limit > 0 {
sess = sess.Limit(limit)
}
return sess.Iterate(new(Mirror), f)
}
// InsertMirror inserts a mirror to database
func InsertMirror(ctx context.Context, mirror *Mirror) error {
_, err := db.GetEngine(ctx).Insert(mirror)
return err
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"strings"
"gitea.dev/models/db"
org_model "gitea.dev/models/organization"
user_model "gitea.dev/models/user"
"xorm.io/builder"
)
// GetOrgRepositories get repos belonging to the given organization
func GetOrgRepositories(ctx context.Context, orgID int64) (RepositoryList, error) {
var orgRepos []*Repository
err := db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos)
return orgRepos, err
}
// GetOrgRepositoryIDs get repo IDs belonging to the given organization
func GetOrgRepositoryIDs(ctx context.Context, orgID int64) (repoIDs []int64, _ error) {
err := db.GetEngine(ctx).Table("repository").Where("owner_id = ?", orgID).Cols("id").Find(&repoIDs)
return repoIDs, err
}
type SearchTeamRepoOptions struct {
db.ListOptions
TeamID int64
}
// GetTeamRepositories returns paginated repositories in team of organization.
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) {
sess := db.GetEngine(ctx)
if opts.TeamID > 0 {
sess = sess.In("id",
builder.Select("repo_id").
From("team_repo").
Where(builder.Eq{"team_id": opts.TeamID}),
)
}
if opts.PageSize > 0 {
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
}
var repos []*Repository
return repos, sess.OrderBy("repository.name").
Find(&repos)
}
// AccessibleReposEnvironment operations involving the repositories that are
// accessible to a particular user
type AccessibleReposEnvironment interface {
CountRepos(ctx context.Context) (int64, error)
RepoIDs(ctx context.Context) ([]int64, error)
MirrorRepos(ctx context.Context) (RepositoryList, error)
AddKeyword(keyword string)
SetSort(db.SearchOrderBy)
}
type accessibleReposEnv struct {
org *org_model.Organization
user *user_model.User
team *org_model.Team
teamIDs []int64
keyword string
orderBy db.SearchOrderBy
}
// AccessibleReposEnv builds an AccessibleReposEnvironment for the repositories in `org`
// that are accessible to the specified user.
func AccessibleReposEnv(ctx context.Context, org *org_model.Organization, userID int64) (AccessibleReposEnvironment, error) {
var user *user_model.User
if userID > 0 {
u, err := user_model.GetUserByID(ctx, userID)
if err != nil {
return nil, err
}
user = u
}
teamIDs, err := org.GetUserTeamIDs(ctx, userID)
if err != nil {
return nil, err
}
return &accessibleReposEnv{
org: org,
user: user,
teamIDs: teamIDs,
orderBy: db.SearchOrderByRecentUpdated,
}, nil
}
// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org`
// that are accessible to the specified team.
func AccessibleTeamReposEnv(org *org_model.Organization, team *org_model.Team) AccessibleReposEnvironment {
return &accessibleReposEnv{
org: org,
team: team,
orderBy: db.SearchOrderByRecentUpdated,
}
}
func (env *accessibleReposEnv) cond() builder.Cond {
cond := builder.NewCond()
if env.team != nil {
cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID})
} else {
if env.user == nil || !env.user.IsRestricted {
cond = cond.Or(builder.Eq{
"`repository`.owner_id": env.org.ID,
"`repository`.is_private": false,
})
}
if len(env.teamIDs) > 0 {
cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs))
}
}
if env.keyword != "" {
cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)})
}
return cond
}
func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) {
repoCount, err := db.GetEngine(ctx).
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
Where(env.cond()).
Distinct("`repository`.id").
Count(&Repository{})
if err != nil {
return 0, fmt.Errorf("count user repositories in organization: %w", err)
}
return repoCount, nil
}
func (env *accessibleReposEnv) RepoIDs(ctx context.Context) ([]int64, error) {
var repoIDs []int64
return repoIDs, db.GetEngine(ctx).
Table("repository").
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
Where(env.cond()).
GroupBy("`repository`.id,`repository`." + strings.Fields(string(env.orderBy))[0]).
OrderBy(string(env.orderBy)).
Cols("`repository`.id").
Find(&repoIDs)
}
func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) {
repoIDs := make([]int64, 0, 10)
return repoIDs, db.GetEngine(ctx).
Table("repository").
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id AND `repository`.is_mirror=?", true).
Where(env.cond()).
GroupBy("`repository`.id, `repository`.updated_unix").
OrderBy(string(env.orderBy)).
Cols("`repository`.id").
Find(&repoIDs)
}
func (env *accessibleReposEnv) MirrorRepos(ctx context.Context) (RepositoryList, error) {
repoIDs, err := env.MirrorRepoIDs(ctx)
if err != nil {
return nil, fmt.Errorf("MirrorRepoIDs: %w", err)
}
repos := make([]*Repository, 0, len(repoIDs))
if len(repoIDs) == 0 {
return repos, nil
}
return repos, db.GetEngine(ctx).
In("`repository`.id", repoIDs).
Find(&repos)
}
func (env *accessibleReposEnv) AddKeyword(keyword string) {
env.keyword = keyword
}
func (env *accessibleReposEnv) SetSort(orderBy db.SearchOrderBy) {
env.orderBy = orderBy
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/unit"
"gitea.dev/modules/util"
)
func (repo *Repository) GetPullRequestTargetBranch(ctx context.Context) string {
unitPRConfig := repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
return util.IfZero(unitPRConfig.DefaultTargetBranch, repo.DefaultBranch)
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
)
func TestDefaultTargetBranchSelection(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1})
assert.Equal(t, repo.DefaultBranch, repo.GetPullRequestTargetBranch(ctx))
repo.Units = nil
prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
assert.NoError(t, err)
prConfig := prUnit.PullRequestsConfig()
prConfig.DefaultTargetBranch = "branch2"
prUnit.Config = prConfig
assert.NoError(t, UpdateRepoUnitConfig(ctx, prUnit))
repo.Units = nil
assert.Equal(t, "branch2", repo.GetPullRequestTargetBranch(ctx))
}
func TestPullRequestConfigFromDB(t *testing.T) {
cases := []struct {
// name describes the row shape under test; the comments capture why each row matters.
name string
json string
wantMergeUpdate bool
wantRebaseUpdate bool
wantDefaultStyle UpdateStyle
wantValidatesPass bool
}{
{
// Empty object exercises the all-defaults path (e.g. fresh repos created via low-level paths).
name: "defaults", json: "{}",
wantMergeUpdate: true, wantRebaseUpdate: true,
wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true,
},
{
// Realistic upgrade case: pre-PR JSON lacks the new fields and has AllowRebaseUpdate=false.
// Historical setting must be preserved while new fields take safe defaults.
name: "legacy without new fields",
json: `{"AllowMerge":true,"AllowRebase":true,"AllowRebaseMerge":true,"AllowSquash":true,"AllowRebaseUpdate":false}`,
wantMergeUpdate: true, wantRebaseUpdate: false,
wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true,
},
{
// Partially-migrated row with explicit empty string must normalize so ValidateUpdateSettings passes.
name: "empty default style", json: `{"DefaultUpdateStyle":""}`,
wantMergeUpdate: true, wantRebaseUpdate: true,
wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := new(PullRequestsConfig)
assert.NoError(t, cfg.FromDB([]byte(tc.json)))
assert.Equal(t, tc.wantMergeUpdate, cfg.AllowMergeUpdate)
assert.Equal(t, tc.wantRebaseUpdate, cfg.AllowRebaseUpdate)
assert.Equal(t, tc.wantDefaultStyle, cfg.DefaultUpdateStyle)
if tc.wantValidatesPass {
assert.NoError(t, cfg.ValidateUpdateSettings())
}
})
}
}
func TestPullRequestConfigValidateUpdateSettingsInvalidArgument(t *testing.T) {
cases := []struct {
name string
cfg PullRequestsConfig
}{
{
name: "invalid default style",
cfg: PullRequestsConfig{
AllowMergeUpdate: true,
AllowRebaseUpdate: true,
DefaultUpdateStyle: "invalid",
},
},
{
name: "no update style enabled",
cfg: PullRequestsConfig{
DefaultUpdateStyle: UpdateStyleMerge,
},
},
{
name: "default update style disabled",
cfg: PullRequestsConfig{
AllowRebaseUpdate: true,
DefaultUpdateStyle: UpdateStyleMerge,
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.ErrorIs(t, tc.cfg.ValidateUpdateSettings(), util.ErrInvalidArgument)
})
}
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"time"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// PushMirror represents mirror information of a repository.
type PushMirror struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
Repo *Repository `xorm:"-"`
RemoteName string
RemoteAddress string `xorm:"VARCHAR(2048)"`
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
Interval time.Duration
CreatedUnix timeutil.TimeStamp `xorm:"created"`
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
LastError string `xorm:"text"`
}
type PushMirrorOptions struct {
db.ListOptions
ID int64
RepoID int64
RemoteName string
}
func (opts PushMirrorOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if opts.RemoteName != "" {
cond = cond.And(builder.Eq{"remote_name": opts.RemoteName})
}
if opts.ID > 0 {
cond = cond.And(builder.Eq{"id": opts.ID})
}
return cond
}
func init() {
db.RegisterModel(new(PushMirror))
}
// GetRepository returns the path of the repository.
func (m *PushMirror) GetRepository(ctx context.Context) *Repository {
if m.Repo != nil {
return m.Repo
}
var err error
m.Repo, err = GetRepositoryByID(ctx, m.RepoID)
if err != nil {
log.Error("getRepositoryByID[%d]: %v", m.ID, err)
}
return m.Repo
}
// GetRemoteName returns the name of the remote.
func (m *PushMirror) GetRemoteName() string {
return m.RemoteName
}
// UpdatePushMirror updates the push-mirror
func UpdatePushMirror(ctx context.Context, m *PushMirror) error {
_, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
return err
}
// UpdatePushMirrorInterval updates the push-mirror
func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
_, err := db.GetEngine(ctx).ID(m.ID).Cols("interval").Update(m)
return err
}
func DeletePushMirrors(ctx context.Context, opts PushMirrorOptions) error {
if opts.RepoID > 0 {
_, err := db.Delete[PushMirror](ctx, opts)
return err
}
return util.NewInvalidArgumentErrorf("repoID required and must be set")
}
type findPushMirrorOptions struct {
db.ListOptions
RepoID int64
SyncOnCommit optional.Option[bool]
}
func (opts findPushMirrorOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if opts.SyncOnCommit.Has() {
cond = cond.And(builder.Eq{"sync_on_commit": opts.SyncOnCommit.Value()})
}
return cond
}
// GetPushMirrorsByRepoID returns push-mirror information of a repository.
func GetPushMirrorsByRepoID(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*PushMirror, int64, error) {
return db.FindAndCount[PushMirror](ctx, findPushMirrorOptions{
ListOptions: listOptions,
RepoID: repoID,
})
}
func GetPushMirrorByIDAndRepoID(ctx context.Context, id, repoID int64) (*PushMirror, bool, error) {
var pushMirror PushMirror
has, err := db.GetEngine(ctx).Where("id = ?", id).And("repo_id = ?", repoID).Get(&pushMirror)
if !has || err != nil {
return nil, has, err
}
return &pushMirror, true, nil
}
// GetPushMirrorsSyncedOnCommit returns push-mirrors for this repo that should be updated by new commits
func GetPushMirrorsSyncedOnCommit(ctx context.Context, repoID int64) ([]*PushMirror, error) {
return db.Find[PushMirror](ctx, findPushMirrorOptions{
RepoID: repoID,
SyncOnCommit: optional.Some(true),
})
}
// PushMirrorsIterate iterates all push-mirror repositories.
func PushMirrorsIterate(ctx context.Context, limit int, f func(idx int, bean any) error) error {
sess := db.GetEngine(ctx).
Table("push_mirror").
Join("INNER", "`repository`", "`repository`.id = `push_mirror`.repo_id").
Where("`push_mirror`.last_update + (`push_mirror`.`interval` / ?) <= ?", time.Second, time.Now().Unix()).
And("`push_mirror`.`interval` != 0").
And("`repository`.is_archived = ?", false).
OrderBy("last_update ASC")
if limit > 0 {
sess = sess.Limit(limit)
}
return sess.Iterate(new(PushMirror), f)
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"time"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
)
func TestPushMirrorsIterate(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
now := timeutil.TimeStampNow()
db.Insert(t.Context(), &repo_model.PushMirror{
RemoteName: "test-1",
LastUpdateUnix: now,
Interval: 1,
})
long, _ := time.ParseDuration("24h")
db.Insert(t.Context(), &repo_model.PushMirror{
RemoteName: "test-2",
LastUpdateUnix: now,
Interval: long,
})
db.Insert(t.Context(), &repo_model.PushMirror{
RemoteName: "test-3",
LastUpdateUnix: now,
Interval: 0,
})
repo_model.PushMirrorsIterate(t.Context(), 1, func(idx int, bean any) error {
m, ok := bean.(*repo_model.PushMirror)
assert.True(t, ok)
assert.Equal(t, "test-1", m.RemoteName)
assert.Equal(t, m.RemoteName, m.GetRemoteName())
return nil
})
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"strings"
"gitea.dev/models/db"
"gitea.dev/modules/util"
)
// ErrRedirectNotExist represents a "RedirectNotExist" kind of error.
type ErrRedirectNotExist struct {
OwnerID int64
RepoName string
}
// IsErrRedirectNotExist check if an error is an ErrRepoRedirectNotExist.
func IsErrRedirectNotExist(err error) bool {
_, ok := err.(ErrRedirectNotExist)
return ok
}
func (err ErrRedirectNotExist) Error() string {
return fmt.Sprintf("repository redirect does not exist [uid: %d, name: %s]", err.OwnerID, err.RepoName)
}
func (err ErrRedirectNotExist) Unwrap() error {
return util.ErrNotExist
}
// Redirect represents that a repo name should be redirected to another
type Redirect struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(s)"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
RedirectRepoID int64 // repoID to redirect to
}
// TableName represents real table name in database
func (Redirect) TableName() string {
return "repo_redirect"
}
func init() {
db.RegisterModel(new(Redirect))
}
// LookupRedirect look up if a repository has a redirect name
func LookupRedirect(ctx context.Context, ownerID int64, repoName string) (int64, error) {
repoName = strings.ToLower(repoName)
redirect := &Redirect{OwnerID: ownerID, LowerName: repoName}
if has, err := db.GetEngine(ctx).Get(redirect); err != nil {
return 0, err
} else if !has {
return 0, ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName}
}
return redirect.RedirectRepoID, nil
}
// NewRedirect create a new repo redirect
func NewRedirect(ctx context.Context, ownerID, repoID int64, oldRepoName, newRepoName string) error {
oldRepoName = strings.ToLower(oldRepoName)
newRepoName = strings.ToLower(newRepoName)
if err := DeleteRedirect(ctx, ownerID, newRepoName); err != nil {
return err
}
return db.Insert(ctx, &Redirect{
OwnerID: ownerID,
LowerName: oldRepoName,
RedirectRepoID: repoID,
})
}
// DeleteRedirect delete any redirect from the specified repo name to
// anything else
func DeleteRedirect(ctx context.Context, ownerID int64, repoName string) error {
repoName = strings.ToLower(repoName)
_, err := db.GetEngine(ctx).Delete(&Redirect{OwnerID: ownerID, LowerName: repoName})
return err
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestLookupRedirect(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repoID, err := repo_model.LookupRedirect(t.Context(), 2, "oldrepo1")
assert.NoError(t, err)
assert.EqualValues(t, 1, repoID)
_, err = repo_model.LookupRedirect(t.Context(), unittest.NonexistentID, "doesnotexist")
assert.True(t, repo_model.IsErrRedirectNotExist(err))
}
func TestNewRedirect(t *testing.T) {
// redirect to a completely new name
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.NoError(t, repo_model.NewRedirect(t.Context(), repo.OwnerID, repo.ID, repo.Name, "newreponame"))
unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{
OwnerID: repo.OwnerID,
LowerName: repo.LowerName,
RedirectRepoID: repo.ID,
})
unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{
OwnerID: repo.OwnerID,
LowerName: "oldrepo1",
RedirectRepoID: repo.ID,
})
}
func TestNewRedirect2(t *testing.T) {
// redirect to previously used name
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.NoError(t, repo_model.NewRedirect(t.Context(), repo.OwnerID, repo.ID, repo.Name, "oldrepo1"))
unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{
OwnerID: repo.OwnerID,
LowerName: repo.LowerName,
RedirectRepoID: repo.ID,
})
unittest.AssertNotExistsBean(t, &repo_model.Redirect{
OwnerID: repo.OwnerID,
LowerName: "oldrepo1",
RedirectRepoID: repo.ID,
})
}
func TestNewRedirect3(t *testing.T) {
// redirect for a previously-unredirected repo
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
assert.NoError(t, repo_model.NewRedirect(t.Context(), repo.OwnerID, repo.ID, repo.Name, "newreponame"))
unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{
OwnerID: repo.OwnerID,
LowerName: repo.LowerName,
RedirectRepoID: repo.ID,
})
}
+535
View File
@@ -0,0 +1,535 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"html/template"
"net/url"
"sort"
"strconv"
"strings"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
"gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrReleaseAlreadyExist represents a "ReleaseAlreadyExist" kind of error.
type ErrReleaseAlreadyExist struct {
TagName string
}
// IsErrReleaseAlreadyExist checks if an error is a ErrReleaseAlreadyExist.
func IsErrReleaseAlreadyExist(err error) bool {
_, ok := err.(ErrReleaseAlreadyExist)
return ok
}
func (err ErrReleaseAlreadyExist) Error() string {
return fmt.Sprintf("release tag already exist [tag_name: %s]", err.TagName)
}
func (err ErrReleaseAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrReleaseNotExist represents a "ReleaseNotExist" kind of error.
type ErrReleaseNotExist struct {
ID int64
TagName string
}
// IsErrReleaseNotExist checks if an error is a ErrReleaseNotExist.
func IsErrReleaseNotExist(err error) bool {
_, ok := err.(ErrReleaseNotExist)
return ok
}
func (err ErrReleaseNotExist) Error() string {
return fmt.Sprintf("release tag does not exist [id: %d, tag_name: %s]", err.ID, err.TagName)
}
func (err ErrReleaseNotExist) Unwrap() error {
return util.ErrNotExist
}
// Release represents a release of repository.
type Release struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(n)"`
Repo *Repository `xorm:"-"`
PublisherID int64 `xorm:"INDEX"`
Publisher *user_model.User `xorm:"-"`
TagName string `xorm:"INDEX UNIQUE(n)"`
OriginalAuthor string
OriginalAuthorID int64 `xorm:"index"`
LowerTagName string
Target string
TargetBehind string `xorm:"-"` // to handle non-existing or empty target
Title string
Sha1 string `xorm:"INDEX VARCHAR(64)"`
NumCommits int64
NumCommitsBehind int64 `xorm:"-"`
Note string `xorm:"TEXT"`
RenderedNote template.HTML `xorm:"-"`
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
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"`
}
func init() {
db.RegisterModel(new(Release))
}
// LegacyAttachmentMissingRepoIDCutoff marks the date when repo_id started to be written during uploads
// (2026-01-16T00:00:00Z). Older rows might have repo_id=0 and should be tolerated once.
const LegacyAttachmentMissingRepoIDCutoff timeutil.TimeStamp = 1768521600
func (r *Release) LoadRepo(ctx context.Context) (err error) {
if r.Repo != nil {
return nil
}
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
return err
}
// LoadAttributes load repo and publisher attributes for a release
func (r *Release) LoadAttributes(ctx context.Context) (err error) {
if err := r.LoadRepo(ctx); err != nil {
return err
}
if r.Publisher == nil {
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
} else {
return err
}
}
}
return GetReleaseAttachments(ctx, r)
}
// APIURL the api url for a release. release must have attributes loaded
func (r *Release) APIURL() string {
return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10)
}
// ZipURL the zip url for a release. release must have attributes loaded
func (r *Release) ZipURL() string {
return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".zip"
}
// TarURL the tar.gz url for a release. release must have attributes loaded
func (r *Release) TarURL() string {
return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".tar.gz"
}
// HTMLURL the url for a release on the web UI. release must have attributes loaded
func (r *Release) HTMLURL() string {
return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
}
// APIUploadURL the api url to upload assets to a release. release must have attributes loaded
func (r *Release) APIUploadURL() string {
return r.APIURL() + "/assets"
}
// Link the relative url for a release on the web UI. release must have attributes loaded
func (r *Release) Link() string {
return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
}
// IsReleaseExist returns true if release with given tag name already exists.
func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, error) {
if len(tagName) == 0 {
return false, nil
}
return db.GetEngine(ctx).Exist(&Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)})
}
// UpdateRelease updates all columns of a release
func UpdateRelease(ctx context.Context, rel *Release) error {
rel.Title = util.EllipsisDisplayString(rel.Title, 255)
_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
return err
}
func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
_, err := db.GetEngine(ctx).ID(rel.ID).Cols("num_commits").Update(rel)
return err
}
// AddReleaseAttachments adds a release attachments
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
rel, err := GetReleaseByID(ctx, releaseID)
if err != nil {
return err
}
// Check attachments
attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
if err != nil {
return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", attachmentUUIDs, err)
}
for i := range attachments {
if attachments[i].RepoID == 0 && attachments[i].CreatedUnix < LegacyAttachmentMissingRepoIDCutoff {
attachments[i].RepoID = rel.RepoID
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("repo_id").Update(attachments[i]); err != nil {
return fmt.Errorf("update attachment repo_id [%d]: %w", attachments[i].ID, err)
}
}
if attachments[i].RepoID != rel.RepoID {
return util.NewPermissionDeniedErrorf("attachment belongs to different repository")
}
if attachments[i].ReleaseID != 0 {
return util.NewPermissionDeniedErrorf("release permission denied")
}
attachments[i].ReleaseID = releaseID
// No assign value could be 0, so ignore AllCols().
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("release_id").Update(attachments[i]); err != nil {
return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
}
}
return err
}
// GetRelease returns release by given ID.
func GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) {
rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
has, err := db.GetEngine(ctx).Get(rel)
if err != nil {
return nil, err
} else if !has {
return nil, ErrReleaseNotExist{0, tagName}
}
return rel, nil
}
// GetReleaseByID returns release with given ID.
func GetReleaseByID(ctx context.Context, id int64) (*Release, error) {
rel := new(Release)
has, err := db.GetEngine(ctx).
ID(id).
Get(rel)
if err != nil {
return nil, err
} else if !has {
return nil, ErrReleaseNotExist{id, ""}
}
return rel, nil
}
// GetReleaseForRepoByID returns release with given ID.
func GetReleaseForRepoByID(ctx context.Context, repoID, id int64) (*Release, error) {
rel := new(Release)
has, err := db.GetEngine(ctx).
Where("id=? AND repo_id=?", id, repoID).
Get(rel)
if err != nil {
return nil, err
} else if !has {
return nil, ErrReleaseNotExist{id, ""}
}
return rel, nil
}
// FindReleasesOptions describes the conditions to Find releases
type FindReleasesOptions struct {
db.ListOptions
RepoID int64
IncludeDrafts bool
IncludeTags bool
IsPreRelease optional.Option[bool]
IsDraft optional.Option[bool]
TagNames []string
HasSha1 optional.Option[bool] // useful to find draft releases which are created with existing tags
NamePattern optional.Option[string]
}
func (opts FindReleasesOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID}
if !opts.IncludeDrafts {
cond = cond.And(builder.Eq{"is_draft": false})
}
if !opts.IncludeTags {
cond = cond.And(builder.Eq{"is_tag": false})
}
if len(opts.TagNames) > 0 {
cond = cond.And(builder.In("tag_name", opts.TagNames))
}
if opts.IsPreRelease.Has() {
cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.Value()})
}
if opts.IsDraft.Has() {
cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()})
}
if opts.HasSha1.Has() {
if opts.HasSha1.Value() {
cond = cond.And(builder.Neq{"sha1": ""})
} else {
cond = cond.And(builder.Eq{"sha1": ""})
}
}
if opts.NamePattern.Has() && opts.NamePattern.Value() != "" {
cond = cond.And(builder.Like{"lower_tag_name", strings.ToLower(opts.NamePattern.Value())})
}
return cond
}
func (opts FindReleasesOptions) ToOrders() string {
return "created_unix DESC, id DESC"
}
// GetTagNamesByRepoID returns a list of release tag names of repository.
func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
opts := FindReleasesOptions{
ListOptions: db.ListOptionsAll,
IncludeDrafts: true,
IncludeTags: true,
HasSha1: optional.Some(true),
RepoID: repoID,
}
tags := make([]string, 0)
sess := db.GetEngine(ctx).
Table("release").
Desc("created_unix", "id").
Where(opts.ToConds()).
Cols("tag_name")
return tags, sess.Find(&tags)
}
// GetLatestReleaseByRepoID returns the latest release for a repository
func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
cond := builder.NewCond().
And(builder.Eq{"repo_id": repoID}).
And(builder.Eq{"is_draft": false}).
And(builder.Eq{"is_prerelease": false}).
And(builder.Eq{"is_tag": false})
rel := new(Release)
has, err := db.GetEngine(ctx).
Desc("created_unix", "id").
Where(cond).
Get(rel)
if err != nil {
return nil, err
} else if !has {
return nil, ErrReleaseNotExist{0, "latest"}
}
return rel, nil
}
type releaseMetaSearch struct {
ID []int64
Rel []*Release
}
func (s releaseMetaSearch) Len() int {
return len(s.ID)
}
func (s releaseMetaSearch) Swap(i, j int) {
s.ID[i], s.ID[j] = s.ID[j], s.ID[i]
s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i]
}
func (s releaseMetaSearch) Less(i, j int) bool {
return s.ID[i] < s.ID[j]
}
func hasDuplicateName(attaches []*Attachment) bool {
attachSet := container.Set[string]{}
for _, attachment := range attaches {
if attachSet.Contains(attachment.Name) {
return true
}
attachSet.Add(attachment.Name)
}
return false
}
// GetReleaseAttachments retrieves the attachments for releases
func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
if len(rels) == 0 {
return nil
}
// To keep this efficient as possible sort all releases by id,
// select attachments by release id,
// then merge join them
// Sort
sortedRels := releaseMetaSearch{ID: make([]int64, len(rels)), Rel: make([]*Release, len(rels))}
var attachments []*Attachment
for index, element := range rels {
element.Attachments = []*Attachment{}
sortedRels.ID[index] = element.ID
sortedRels.Rel[index] = element
}
sort.Sort(sortedRels)
// Select attachments
err = db.GetEngine(ctx).
Asc("release_id", "name").
In("release_id", sortedRels.ID).
Find(&attachments)
if err != nil {
return err
}
// merge join
currentIndex := 0
for _, attachment := range attachments {
for sortedRels.ID[currentIndex] < attachment.ReleaseID {
currentIndex++
}
sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment)
}
// Makes URL's predictable
for _, release := range rels {
// If we have no Repo, we don't need to execute this loop
if release.Repo == nil {
continue
}
// If the names unique, use the URL with the Name instead of the UUID
if !hasDuplicateName(release.Attachments) {
for _, attachment := range release.Attachments {
attachment.CustomDownloadURL = release.Repo.HTMLURL() + "/releases/download/" + url.PathEscape(release.TagName) + "/" + url.PathEscape(attachment.Name)
}
}
}
return err
}
// UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID
func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error {
_, err := db.GetEngine(ctx).Table("release").
Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
And("original_author_id = ?", originalAuthorID).
Update(map[string]any{
"publisher_id": posterID,
"original_author": "",
"original_author_id": 0,
})
return err
}
// PushUpdateDeleteTags updates a number of delete tags with context
func PushUpdateDeleteTags(ctx context.Context, repo *Repository, tags []string) error {
if len(tags) == 0 {
return nil
}
lowerTags := make([]string, 0, len(tags))
for _, tag := range tags {
lowerTags = append(lowerTags, strings.ToLower(tag))
}
if _, err := db.GetEngine(ctx).
Where("repo_id = ? AND is_tag = ?", repo.ID, true).
In("lower_tag_name", lowerTags).
Delete(new(Release)); err != nil {
return fmt.Errorf("Delete: %w", err)
}
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").
Update(&Release{
IsDraft: true,
}); err != nil {
return fmt.Errorf("Update: %w", err)
}
return nil
}
// RemapExternalUser ExternalUserRemappable interface
func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error {
r.OriginalAuthor = externalName
r.OriginalAuthorID = externalID
r.PublisherID = userID
return nil
}
// UserID ExternalUserRemappable interface
func (r *Release) GetUserID() int64 { return r.PublisherID }
// ExternalName ExternalUserRemappable interface
func (r *Release) GetExternalName() string { return r.OriginalAuthor }
// ExternalID ExternalUserRemappable interface
func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID }
// InsertReleases migrates release
func InsertReleases(ctx context.Context, rels ...*Release) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, rel := range rels {
if _, err := db.GetEngine(ctx).NoAutoTime().Insert(rel); err != nil {
return err
}
if len(rel.Attachments) > 0 {
for i := range rel.Attachments {
rel.Attachments[i].ReleaseID = rel.ID
}
if _, err := db.GetEngine(ctx).NoAutoTime().Insert(rel.Attachments); err != nil {
return err
}
}
}
return nil
})
}
func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string) (map[string][]*Release, error) {
releases := make([]*Release, 0, len(commitIDs))
if err := db.GetEngine(ctx).Where("repo_id=?", repoID).
In("sha1", commitIDs).
Find(&releases); err != nil {
return nil, err
}
res := make(map[string][]*Release, len(releases))
for _, r := range releases {
res[r.Sha1] = append(res[r.Sha1], r)
}
return res, nil
}
func DeleteRepoReleases(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(Release))
return err
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
)
func TestMigrate_InsertReleases(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
a := &Attachment{
UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12",
}
r := &Release{
Attachments: []*Attachment{a},
}
err := InsertReleases(t.Context(), r)
assert.NoError(t, err)
}
func Test_FindTagsByCommitIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
sha1Rels, err := FindTagsByCommitIDs(t.Context(), 1, "65f1bf27bc3bf70f64657658635e66094edbcb4d")
assert.NoError(t, err)
assert.Len(t, sha1Rels, 1)
rels := sha1Rels["65f1bf27bc3bf70f64657658635e66094edbcb4d"]
assert.Len(t, rels, 3)
assert.Equal(t, "v1.1", rels[0].TagName)
assert.Equal(t, "delete-tag", rels[1].TagName)
assert.Equal(t, "v1.0", rels[2].TagName)
}
func TestAddReleaseAttachmentsRejectsDifferentRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
uuid := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12" // attachment 2 belongs to repo 2
err := AddReleaseAttachments(t.Context(), 1, []string{uuid})
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrPermissionDenied)
attach, err := GetAttachmentByUUID(t.Context(), uuid)
assert.NoError(t, err)
assert.Zero(t, attach.ReleaseID, "attachment should not be linked to release on failure")
}
func TestAddReleaseAttachmentsAllowsLegacyMissingRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
legacyUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20" // attachment 10 has repo_id 0
err := AddReleaseAttachments(t.Context(), 1, []string{legacyUUID})
assert.NoError(t, err)
attach, err := GetAttachmentByUUID(t.Context(), legacyUUID)
assert.NoError(t, err)
assert.EqualValues(t, 1, attach.RepoID)
assert.EqualValues(t, 1, attach.ReleaseID)
}
func TestAddReleaseAttachmentsRejectsRecentZeroRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
recentUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd3800aa"
attachment := &Attachment{
UUID: recentUUID,
RepoID: 0,
IssueID: 0,
ReleaseID: 0,
CommentID: 0,
Name: "recent-zero",
CreatedUnix: LegacyAttachmentMissingRepoIDCutoff + 1,
}
assert.NoError(t, db.Insert(t.Context(), attachment))
err := AddReleaseAttachments(t.Context(), 1, []string{recentUUID})
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrPermissionDenied)
attach, err := GetAttachmentByUUID(t.Context(), recentUUID)
assert.NoError(t, err)
assert.Zero(t, attach.ReleaseID)
assert.Zero(t, attach.RepoID)
}
+967
View File
@@ -0,0 +1,967 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"errors"
"fmt"
"html/template"
"maps"
"net"
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"gitea.dev/models/db"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/git"
giturl "gitea.dev/modules/git/url"
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/markup"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrUserDoesNotHaveAccessToRepo represents an error where the user doesn't has access to a given repo.
type ErrUserDoesNotHaveAccessToRepo struct {
UserID int64
RepoName string
}
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrUserDoesNotHaveAccessToRepo.
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
return ok
}
func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
}
func (err ErrUserDoesNotHaveAccessToRepo) Unwrap() error {
return util.ErrPermissionDenied
}
type ErrRepoIsArchived struct {
Repo *Repository
}
func (err ErrRepoIsArchived) Error() string {
return err.Repo.LogString() + " is archived"
}
type globalVarsStruct struct {
validRepoNamePattern *regexp.Regexp
invalidRepoNamePattern *regexp.Regexp
reservedRepoNames []string
reservedRepoNamePatterns []string
}
var globalVars = sync.OnceValue(func() *globalVarsStruct {
return &globalVarsStruct{
validRepoNamePattern: regexp.MustCompile(`^[-.\w]+$`),
invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`),
reservedRepoNames: []string{".", "..", "-"},
reservedRepoNamePatterns: []string{"*.wiki", "*.git", "*.rss", "*.atom"},
}
})
// IsUsableRepoName returns true when name is usable
func IsUsableRepoName(name string) error {
vars := globalVars()
if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) {
// Note: usually this error is normally caught up earlier in the UI
return db.ErrNameCharsNotAllowed{Name: name}
}
return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns, name)
}
// IsValidSSHAccessRepoName is like IsUsableRepoName, but it allows "*.wiki" because wiki repo needs to be accessed in SSH code
func IsValidSSHAccessRepoName(name string) bool {
vars := globalVars()
if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) {
return false
}
return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns[1:], name) == nil
}
// TrustModelType defines the types of trust model for this repository
type TrustModelType int
// kinds of TrustModel
const (
DefaultTrustModel TrustModelType = iota // default trust model
CommitterTrustModel
CollaboratorTrustModel
CollaboratorCommitterTrustModel
)
// String converts a TrustModelType to a string
func (t TrustModelType) String() string {
switch t {
case DefaultTrustModel:
return "default"
case CommitterTrustModel:
return "committer"
case CollaboratorTrustModel:
return "collaborator"
case CollaboratorCommitterTrustModel:
return "collaboratorcommitter"
}
return "default"
}
// ToTrustModel converts a string to a TrustModelType
func ToTrustModel(model string) TrustModelType {
switch strings.ToLower(strings.TrimSpace(model)) {
case "default":
return DefaultTrustModel
case "collaborator":
return CollaboratorTrustModel
case "committer":
return CommitterTrustModel
case "collaboratorcommitter":
return CollaboratorCommitterTrustModel
}
return DefaultTrustModel
}
// RepositoryStatus defines the status of repository
type RepositoryStatus int
// all kinds of RepositoryStatus
const (
RepositoryReady RepositoryStatus = iota // a normal repository
RepositoryBeingMigrated // repository is migrating
RepositoryPendingTransfer // repository pending in ownership transfer state
RepositoryBroken // repository is in a permanently broken state
)
// Repository represents a git repository.
type Repository struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(s) index"`
OwnerName string
Owner *user_model.User `xorm:"-"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
Name string `xorm:"INDEX NOT NULL"`
Description string `xorm:"TEXT"`
Website string `xorm:"VARCHAR(2048)"`
OriginalServiceType api.GitServiceType `xorm:"index"`
OriginalURL string `xorm:"VARCHAR(2048)"`
DefaultBranch string
DefaultWikiBranch string
NumWatches int
NumStars int
NumForks int
NumIssues int
NumClosedIssues int
NumOpenIssues int `xorm:"-"`
NumPulls int
NumClosedPulls int
NumOpenPulls int `xorm:"-"`
NumMilestones int `xorm:"NOT NULL DEFAULT 0"`
NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"`
NumOpenMilestones int `xorm:"-"`
NumProjects int `xorm:"NOT NULL DEFAULT 0"`
NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"`
NumOpenProjects int `xorm:"-"`
NumActionRuns int `xorm:"NOT NULL DEFAULT 0"`
NumClosedActionRuns int `xorm:"NOT NULL DEFAULT 0"`
NumOpenActionRuns int `xorm:"-"`
IsPrivate bool `xorm:"INDEX"`
IsEmpty bool `xorm:"INDEX"`
IsArchived bool `xorm:"INDEX"`
IsMirror bool `xorm:"INDEX"`
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
commonRenderingMetas map[string]string `xorm:"-"`
Units []*RepoUnit `xorm:"-"`
PrimaryLanguage *LanguageStat `xorm:"-"`
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
ForkID int64 `xorm:"INDEX"`
BaseRepo *Repository `xorm:"-"`
IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"`
TemplateID int64 `xorm:"INDEX"`
Size int64 `xorm:"NOT NULL DEFAULT 0"`
GitSize int64 `xorm:"NOT NULL DEFAULT 0"`
LFSSize int64 `xorm:"NOT NULL DEFAULT 0"`
CodeIndexerStatus *RepoIndexerStatus `xorm:"-"`
StatsIndexerStatus *RepoIndexerStatus `xorm:"-"`
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"`
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
TrustModel TrustModelType
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
Avatar string `xorm:"VARCHAR(64)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
}
func init() {
db.RegisterModel(new(Repository))
}
func RelativePath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git"
}
// RelativePath should be an unix style path like username/reponame.git
func (repo *Repository) RelativePath() string {
return RelativePath(repo.OwnerName, repo.Name)
}
type StorageRepo string
// RelativePath should be an unix style path like username/reponame.git
func (sr StorageRepo) RelativePath() string {
return string(sr)
}
// SanitizedOriginalURL returns a sanitized OriginalURL
func (repo *Repository) SanitizedOriginalURL() string {
if repo.OriginalURL == "" {
return ""
}
u, _ := util.SanitizeURL(repo.OriginalURL)
return u
}
// text representations to be returned in SizeDetail.Name
const (
SizeDetailNameGit = "git"
SizeDetailNameLFS = "lfs"
)
type SizeDetail struct {
Name string
Size int64
}
// SizeDetails forms a struct with various size details about repository
func (repo *Repository) SizeDetails() []SizeDetail {
sizeDetails := []SizeDetail{
{
Name: SizeDetailNameGit,
Size: repo.GitSize,
},
{
Name: SizeDetailNameLFS,
Size: repo.LFSSize,
},
}
return sizeDetails
}
// SizeDetailsString returns a concatenation of all repository size details as a string
func (repo *Repository) SizeDetailsString() string {
var str strings.Builder
sizeDetails := repo.SizeDetails()
for _, detail := range sizeDetails {
fmt.Fprintf(&str, "%s: %s, ", detail.Name, base.FileSize(detail.Size))
}
return strings.TrimSuffix(str.String(), ", ")
}
func (repo *Repository) LogString() string {
if repo == nil {
return "<Repository nil>"
}
return fmt.Sprintf("<Repository %d:%s/%s>", repo.ID, repo.OwnerName, repo.Name)
}
// IsBeingMigrated indicates that repository is being migrated
func (repo *Repository) IsBeingMigrated() bool {
return repo.Status == RepositoryBeingMigrated
}
// IsBeingCreated indicates that repository is being migrated or forked
func (repo *Repository) IsBeingCreated() bool {
return repo.IsBeingMigrated()
}
// IsBroken indicates that repository is broken
func (repo *Repository) IsBroken() bool {
return repo.Status == RepositoryBroken
}
// MarkAsBrokenEmpty marks the repo as broken and empty
// FIXME: the status "broken" and "is_empty" were abused,
// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken"
func (repo *Repository) MarkAsBrokenEmpty() {
repo.Status = RepositoryBroken
repo.IsEmpty = true
}
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (repo *Repository) AfterLoad() {
repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
if repo.DefaultWikiBranch == "" {
repo.DefaultWikiBranch = setting.Repository.DefaultBranch
}
}
// LoadAttributes loads attributes of the repository.
func (repo *Repository) LoadAttributes(ctx context.Context) error {
// Load owner
if err := repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("load owner: %w", err)
}
// Load primary language
stats := make(LanguageStatList, 0, 1)
if err := db.GetEngine(ctx).
Where("`repo_id` = ? AND `is_primary` = ? AND `language` != ?", repo.ID, true, "other").
Find(&stats); err != nil {
return fmt.Errorf("find primary languages: %w", err)
}
stats.LoadAttributes()
for _, st := range stats {
if st.RepoID == repo.ID {
repo.PrimaryLanguage = st
break
}
}
return nil
}
// FullName returns the repository full name
func (repo *Repository) FullName() string {
return repo.OwnerName + "/" + repo.Name
}
// HTMLURL returns the repository HTML URL
func (repo *Repository) HTMLURL(ctxs ...context.Context) string {
// FIXME: this HTMLURL is still used in mail templates, so the "ctx" is not provided.
ctx := util.OptionalArg(ctxs, context.TODO())
return httplib.MakeAbsoluteURL(ctx, repo.Link())
}
// CommitLink make link to by commit full ID
// note: won't check whether it's an right id
func (repo *Repository) CommitLink(commitID string) (result string) {
if git.IsEmptyCommitID(commitID) {
result = ""
} else {
result = repo.Link() + "/commit/" + url.PathEscape(commitID)
}
return result
}
// APIURL returns the repository API URL
func (repo *Repository) APIURL(ctxOpt ...context.Context) string {
ctx := util.OptionalArg(ctxOpt, context.TODO())
return httplib.MakeAbsoluteURL(ctx, setting.AppSubURL+"/api/v1/repos/"+url.PathEscape(repo.OwnerName)+"/"+url.PathEscape(repo.Name))
}
// GetCommitsCountCacheKey returns cache key used for commits count caching.
func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
var prefix string
if isRef {
prefix = "ref"
} else {
prefix = "commit"
}
return fmt.Sprintf("commits-count-%d-%s-%s", repo.ID, prefix, contextName)
}
// LoadUnits loads repo units into repo.Units
func (repo *Repository) LoadUnits(ctx context.Context) (err error) {
if repo.Units != nil {
return nil
}
repo.Units, err = getUnitsByRepoID(ctx, repo.ID)
if log.IsTrace() {
unitTypeStrings := make([]string, len(repo.Units))
for i, unit := range repo.Units {
unitTypeStrings[i] = unit.Type.LogString()
}
log.Trace("repo.Units, ID=%d, Types: [%s]", repo.ID, strings.Join(unitTypeStrings, ", "))
}
return err
}
// UnitEnabled if this repository has the given unit enabled
func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool {
if err := repo.LoadUnits(ctx); err != nil {
log.Warn("Error loading repository (ID: %d) units: %s", repo.ID, err.Error())
}
for _, unit := range repo.Units {
if unit.Type == tp {
return true
}
}
return false
}
// MustGetUnit always returns a RepoUnit object even if the unit doesn't exist (not enabled)
func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit {
ru, err := repo.GetUnit(ctx, tp)
if err == nil {
return ru
}
if !errors.Is(err, util.ErrNotExist) {
setting.PanicInDevOrTesting("Failed to get unit %v for repository %d: %v", tp, repo.ID, err)
}
ru = &RepoUnit{RepoID: repo.ID, Type: tp}
switch tp {
case unit.TypeExternalWiki:
ru.Config = new(ExternalWikiConfig)
case unit.TypeExternalTracker:
ru.Config = new(ExternalTrackerConfig)
case unit.TypePullRequests:
ru.Config = new(PullRequestsConfig)
case unit.TypeIssues:
ru.Config = new(IssuesConfig)
case unit.TypeActions:
ru.Config = new(ActionsConfig)
case unit.TypeProjects:
ru.Config = new(ProjectsConfig)
default: // other units don't have config
}
if ru.Config != nil {
if err = ru.Config.FromDB(nil); err != nil {
setting.PanicInDevOrTesting("Failed to load default config for unit %v of repository %d: %v", tp, repo.ID, err)
}
}
return ru
}
// GetUnit returns a RepoUnit object
func (repo *Repository) GetUnit(ctx context.Context, tp unit.Type) (*RepoUnit, error) {
if err := repo.LoadUnits(ctx); err != nil {
return nil, err
}
for _, unit := range repo.Units {
if unit.Type == tp {
return unit, nil
}
}
return nil, ErrUnitTypeNotExist{tp}
}
// LoadOwner loads owner user
func (repo *Repository) LoadOwner(ctx context.Context) (err error) {
if repo.Owner != nil {
return nil
}
repo.Owner, err = user_model.GetUserByID(ctx, repo.OwnerID)
return err
}
// MustOwner always returns a valid *user_model.User object to avoid
// conceptually impossible error handling.
// It creates a fake object that contains error details
// when error occurs.
func (repo *Repository) MustOwner(ctx context.Context) *user_model.User {
if err := repo.LoadOwner(ctx); err != nil {
return &user_model.User{
Name: "error",
FullName: err.Error(),
}
}
return repo.Owner
}
func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]string {
if len(repo.commonRenderingMetas) == 0 {
metas := map[string]string{
"user": repo.OwnerName,
"repo": repo.Name,
}
unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil {
metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle {
case markup.IssueNameStyleAlphanumeric:
metas["style"] = markup.IssueNameStyleAlphanumeric
case markup.IssueNameStyleRegexp:
metas["style"] = markup.IssueNameStyleRegexp
metas["regexp"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerRegexpPattern
default:
metas["style"] = markup.IssueNameStyleNumeric
}
}
repo.MustOwner(ctx)
if repo.Owner.IsOrganization() {
teams := make([]string, 0, 5)
_ = db.GetEngine(ctx).Table("team_repo").
Join("INNER", "team", "team.id = team_repo.team_id").
Where("team_repo.repo_id = ?", repo.ID).
Select("team.lower_name").
OrderBy("team.lower_name").
Find(&teams)
metas["teams"] = "," + strings.Join(teams, ",") + ","
metas["org"] = strings.ToLower(repo.OwnerName)
}
repo.commonRenderingMetas = metas
}
return repo.commonRenderingMetas
}
// ComposeCommentMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
func (repo *Repository) ComposeCommentMetas(ctx context.Context) map[string]string {
metas := maps.Clone(repo.composeCommonMetas(ctx))
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.NewLineHardBreak)
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.ShortIssuePattern)
return metas
}
// ComposeWikiMetas composes a map of metas for properly rendering wikis
func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string {
// does wiki need the "teams" and "org" from common metas?
metas := maps.Clone(repo.composeCommonMetas(ctx))
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.NewLineHardBreak)
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.ShortIssuePattern)
return metas
}
// ComposeRepoFileMetas composes a map of metas for properly rendering documents (repo files)
func (repo *Repository) ComposeRepoFileMetas(ctx context.Context) map[string]string {
// does document(file) need the "teams" and "org" from common metas?
metas := maps.Clone(repo.composeCommonMetas(ctx))
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.ShortIssuePattern)
return metas
}
// GetBaseRepo populates repo.BaseRepo for a fork repository and
// returns an error on failure (NOTE: no error is returned for
// non-fork repositories, and BaseRepo will be left untouched)
func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) {
if !repo.IsFork {
return nil
}
if repo.BaseRepo != nil {
return nil
}
repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID)
return err
}
// IsGenerated returns whether _this_ repository was generated from a template
func (repo *Repository) IsGenerated() bool {
return repo.TemplateID != 0
}
// RepoPath returns repository path by given user and repository name.
func RepoPath(userName, repoName string) string { //revive:disable-line:exported
return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName)), filepath.Clean(strings.ToLower(repoName)+".git"))
}
// RepoPath returns the repository path
func (repo *Repository) RepoPath() string {
return RepoPath(repo.OwnerName, repo.Name)
}
// Link returns the repository relative url
func (repo *Repository) Link() string {
return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
}
// ComposeCompareURL returns the repository comparison URL
func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID))
}
func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, baseBranch, branchName string) string {
var cmpBranchEscaped string
if repo.ID != baseRepo.ID {
cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
}
cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName))
return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseBranch), cmpBranchEscaped)
}
// IsOwnedBy returns true when user owns this repository
func (repo *Repository) IsOwnedBy(userID int64) bool {
return repo.OwnerID == userID
}
// CanCreateBranch returns true if repository meets the requirements for creating new branches.
func (repo *Repository) CanCreateBranch() bool {
return !repo.IsMirror
}
// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
func (repo *Repository) CanEnablePulls() bool {
return !repo.IsMirror && !repo.IsEmpty
}
// AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled.
func (repo *Repository) AllowsPulls(ctx context.Context) bool {
return repo.CanEnablePulls() && repo.UnitEnabled(ctx, unit.TypePullRequests)
}
// CanEnableEditor returns true if repository meets the requirements of web editor.
// FIXME: most CanEnableEditor calls should be replaced with CanContentChange
// And all other like CanCreateBranch / CanEnablePulls should also be updated
func (repo *Repository) CanEnableEditor() bool {
return repo.CanContentChange()
}
func (repo *Repository) CanContentChange() bool {
return !repo.IsMirror && !repo.IsArchived
}
// DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
if err != nil {
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
return template.HTML(markup.SanitizeDescription(repo.Description))
}
return template.HTML(markup.SanitizeDescription(desc))
}
// CloneLink represents different types of clone URLs of repository.
type CloneLink struct {
SSH string
HTTPS string
Tea string
}
// ComposeHTTPSCloneURL returns HTTPS clone URL based on the given owner and repository name.
func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo))
}
// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name.
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
sshUser := setting.SSH.User
sshDomain := setting.SSH.Domain
if sshUser == "(DOER_USERNAME)" {
// Some users use SSH reverse-proxy and need to use the current signed-in username as the SSH user
// to make the SSH reverse-proxy could prepare the user's public keys ahead.
// For most cases we have the correct "doer", then use it as the SSH user.
// If we can't get the doer, then use the built-in SSH user.
if doer != nil {
sshUser = doer.Name
} else {
sshUser = setting.SSH.BuiltinServerUser
}
}
// non-standard port, it must use full URI
if setting.SSH.Port != 22 {
sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
}
// for standard port, it can use a shorter URI (without the port)
sshHost := sshDomain
if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
}
if setting.Repository.UseCompatSSHURI {
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
}
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
}
// ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name.
func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string {
return fmt.Sprintf("tea clone %s/%s", url.PathEscape(owner), url.PathEscape(repo))
}
func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink {
return &CloneLink{
SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName),
HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName),
Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName),
}
}
// CloneLink returns clone URLs of repository.
func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) {
return repo.cloneLink(ctx, doer, repo.Name)
}
func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) {
return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name)
}
// GetOriginalURLHostname returns the hostname of a URL or the URL
func (repo *Repository) GetOriginalURLHostname() string {
u, err := url.Parse(repo.OriginalURL)
if err != nil {
return repo.OriginalURL
}
return u.Host
}
// GetTrustModel will get the TrustModel for the repo or the default trust model
func (repo *Repository) GetTrustModel() TrustModelType {
trustModel := repo.TrustModel
if trustModel == DefaultTrustModel {
trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel)
if trustModel == DefaultTrustModel {
return CollaboratorTrustModel
}
}
return trustModel
}
// MustNotBeArchived returns ErrRepoIsArchived if the repo is archived
func (repo *Repository) MustNotBeArchived() error {
if repo.IsArchived {
return ErrRepoIsArchived{Repo: repo}
}
return nil
}
// __________ .__ __
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
// \/ \/|__| \/ \/
// ErrRepoNotExist represents a "RepoNotExist" kind of error.
type ErrRepoNotExist struct {
ID int64
UID int64
OwnerName string
Name string
}
// IsErrRepoNotExist checks if an error is a ErrRepoNotExist.
func IsErrRepoNotExist(err error) bool {
_, ok := err.(ErrRepoNotExist)
return ok
}
func (err ErrRepoNotExist) Error() string {
return fmt.Sprintf("repository does not exist [id: %d, uid: %d, owner_name: %s, name: %s]",
err.ID, err.UID, err.OwnerName, err.Name)
}
// Unwrap unwraps this error as a ErrNotExist error
func (err ErrRepoNotExist) Unwrap() error {
return util.ErrNotExist
}
// GetRepositoryByOwnerAndName returns the repository by given owner name and repo name
func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string) (*Repository, error) {
var repo Repository
has, err := db.GetEngine(ctx).Table("repository").Select("repository.*").
Join("INNER", "`user`", "`user`.id = repository.owner_id").
Where("repository.lower_name = ?", strings.ToLower(repoName)).
And("`user`.lower_name = ?", strings.ToLower(ownerName)).
Get(&repo)
if err != nil {
return nil, err
} else if !has {
return nil, ErrRepoNotExist{0, 0, ownerName, repoName}
}
return &repo, nil
}
// GetRepositoryByName returns the repository by given name under user if exists.
func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
var repo Repository
has, err := db.GetEngine(ctx).
Where("`owner_id`=?", ownerID).
And("`lower_name`=?", strings.ToLower(name)).
NoAutoCondition().
Get(&repo)
if err != nil {
return nil, err
} else if !has {
return nil, ErrRepoNotExist{0, ownerID, "", name}
}
return &repo, err
}
// GetRepositoryByURL returns the repository by given url
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
ret, err := giturl.ParseRepositoryURL(ctx, repoURL)
if err != nil || ret.OwnerName == "" {
return nil, errors.New("unknown or malformed repository URL")
}
return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName)
}
// GetRepositoryByURLRelax also accepts an SSH clone URL without user part
func GetRepositoryByURLRelax(ctx context.Context, repoURL string) (*Repository, error) {
if !strings.Contains(repoURL, "://") && !strings.Contains(repoURL, "@") {
// convert "example.com:owner/repo" to "@example.com:owner/repo"
p1, p2, p3 := strings.Index(repoURL, "."), strings.Index(repoURL, ":"), strings.Index(repoURL, "/")
if 0 < p1 && p1 < p2 && p2 < p3 {
repoURL = "@" + repoURL
}
}
return GetRepositoryByURL(ctx, repoURL)
}
// GetRepositoryByID returns the repository by given id if exists.
func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) {
repo := new(Repository)
has, err := db.GetEngine(ctx).ID(id).Get(repo)
if err != nil {
return nil, err
} else if !has {
return nil, ErrRepoNotExist{id, 0, "", ""}
}
return repo, nil
}
// GetRepositoriesMapByIDs returns the repositories by given id slice.
func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repository, error) {
repos := make(map[int64]*Repository, len(ids))
if len(ids) == 0 {
return repos, nil
}
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
}
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
return db.GetEngine(ctx).Get(&Repository{
OwnerID: u.ID,
LowerName: strings.ToLower(repoName),
})
}
// GetTemplateRepo populates repo.TemplateRepo for a generated repository and
// returns an error on failure (NOTE: no error is returned for
// non-generated repositories, and TemplateRepo will be left untouched)
func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) {
if !repo.IsGenerated() {
return nil, nil //nolint:nilnil // return nil for non-generated repositories
}
return GetRepositoryByID(ctx, repo.TemplateID)
}
// TemplateRepo returns the repository, which is template of this repository
func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
repo, err := GetTemplateRepo(ctx, repo)
if err != nil {
log.Error("TemplateRepo: %v", err)
return nil
}
return repo
}
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
type ErrUserOwnRepos struct {
UID int64
}
// IsErrUserOwnRepos checks if an error is a ErrUserOwnRepos.
func IsErrUserOwnRepos(err error) bool {
_, ok := err.(ErrUserOwnRepos)
return ok
}
func (err ErrUserOwnRepos) Error() string {
return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
}
type CountRepositoryOptions struct {
OwnerID int64
Private optional.Option[bool]
}
// CountRepositories returns number of repositories.
// Argument private only takes effect when it is false,
// set it true to count all repositories.
func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, error) {
sess := db.GetEngine(ctx).Where("id > 0")
if opts.OwnerID > 0 {
sess.And("owner_id = ?", opts.OwnerID)
}
if opts.Private.Has() {
sess.And("is_private=?", opts.Private.Value())
}
count, err := sess.Count(new(Repository))
if err != nil {
return 0, fmt.Errorf("countRepositories: %w", err)
}
return count, nil
}
// UpdateRepoIssueNumbers updates one of a repositories amount of (open|closed) (issues|PRs) with the current count
func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed bool) error {
field := "num_"
if isClosed {
field += "closed_"
}
if isPull {
field += "pulls"
} else {
field += "issues"
}
subQuery := builder.Select("count(*)").
From("issue").Where(builder.Eq{
"repo_id": repoID,
"is_pull": isPull,
}.And(builder.If(isClosed, builder.Eq{"is_closed": isClosed})))
// builder.Update(cond) will generate SQL like UPDATE ... SET cond
query := builder.Update(builder.Eq{field: subQuery}).
From("repository").
Where(builder.Eq{"id": repoID})
_, err := db.Exec(ctx, query)
return err
}
// CountNullArchivedRepository counts the number of repositories with is_archived is null
func CountNullArchivedRepository(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository))
}
// FixNullArchivedRepository sets is_archived to false where it is null
func FixNullArchivedRepository(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{
IsArchived: false,
})
}
// UpdateRepositoryOwnerName updates the owner name of all repositories owned by the user
func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName string) error {
if _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil {
return fmt.Errorf("change repo owner name: %w", err)
}
return nil
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"gitea.dev/models/db"
"xorm.io/builder"
)
// RepoIndexerType specifies the repository indexer type
type RepoIndexerType int //revive:disable-line:exported
const (
// RepoIndexerTypeCode code indexer
RepoIndexerTypeCode RepoIndexerType = iota // 0
// RepoIndexerTypeStats repository stats indexer
RepoIndexerTypeStats // 1
)
// RepoIndexerStatus status of a repo's entry in the repo indexer
// For now, implicitly refers to default branch
type RepoIndexerStatus struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX(s)"`
CommitSha string `xorm:"VARCHAR(64)"`
IndexerType RepoIndexerType `xorm:"INDEX(s) NOT NULL DEFAULT 0"`
}
func init() {
db.RegisterModel(new(RepoIndexerStatus))
}
// GetUnindexedRepos returns repos which do not have an indexer status
func GetUnindexedRepos(ctx context.Context, indexerType RepoIndexerType, maxRepoID int64, page, pageSize int) ([]int64, error) {
ids := make([]int64, 0, 50)
cond := builder.Cond(builder.IsNull{
"repo_indexer_status.id",
}).And(builder.Eq{
"repository.is_empty": false,
})
sess := db.GetEngine(ctx).Table("repository").Join("LEFT OUTER", "repo_indexer_status", "repository.id = repo_indexer_status.repo_id AND repo_indexer_status.indexer_type = ?", indexerType)
if maxRepoID > 0 {
cond = builder.And(cond, builder.Lte{
"repository.id": maxRepoID,
})
}
if page >= 0 && pageSize > 0 {
start := 0
if page > 0 {
start = (page - 1) * pageSize
}
sess.Limit(pageSize, start)
}
sess.Where(cond).Cols("repository.id").Desc("repository.id")
err := sess.Find(&ids)
return ids, err
}
// GetIndexerStatus loads repo codes indxer status
func GetIndexerStatus(ctx context.Context, repo *Repository, indexerType RepoIndexerType) (*RepoIndexerStatus, error) {
switch indexerType {
case RepoIndexerTypeCode:
if repo.CodeIndexerStatus != nil {
return repo.CodeIndexerStatus, nil
}
case RepoIndexerTypeStats:
if repo.StatsIndexerStatus != nil {
return repo.StatsIndexerStatus, nil
}
}
status := &RepoIndexerStatus{RepoID: repo.ID}
if has, err := db.GetEngine(ctx).Where("`indexer_type` = ?", indexerType).Get(status); err != nil {
return nil, err
} else if !has {
status.IndexerType = indexerType
status.CommitSha = ""
}
switch indexerType {
case RepoIndexerTypeCode:
repo.CodeIndexerStatus = status
case RepoIndexerTypeStats:
repo.StatsIndexerStatus = status
}
return status, nil
}
// UpdateIndexerStatus updates indexer status
func UpdateIndexerStatus(ctx context.Context, repo *Repository, indexerType RepoIndexerType, sha string) error {
status, err := GetIndexerStatus(ctx, repo, indexerType)
if err != nil {
return fmt.Errorf("UpdateIndexerStatus: Unable to getIndexerStatus for repo: %s Error: %w", repo.FullName(), err)
}
if len(status.CommitSha) == 0 {
status.CommitSha = sha
if err := db.Insert(ctx, status); err != nil {
return fmt.Errorf("UpdateIndexerStatus: Unable to insert repoIndexerStatus for repo: %s Sha: %s Error: %w", repo.FullName(), sha, err)
}
return nil
}
status.CommitSha = sha
_, err = db.GetEngine(ctx).ID(status.ID).Cols("commit_sha").
Update(status)
if err != nil {
return fmt.Errorf("UpdateIndexerStatus: Unable to update repoIndexerStatus for repo: %s Sha: %s Error: %w", repo.FullName(), sha, err)
}
return nil
}
+795
View File
@@ -0,0 +1,795 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// RepositoryListDefaultPageSize is the default number of repositories
// to load in memory when running administrative tasks on all (or almost
// all) of them.
// The number should be low enough to avoid filling up all RAM with
// repository data...
const RepositoryListDefaultPageSize = 64
// RepositoryList contains a list of repositories
type RepositoryList []*Repository
func (repos RepositoryList) Len() int {
return len(repos)
}
func (repos RepositoryList) Less(i, j int) bool {
return repos[i].FullName() < repos[j].FullName()
}
func (repos RepositoryList) Swap(i, j int) {
repos[i], repos[j] = repos[j], repos[i]
}
// ValuesRepository converts a repository map to a list
// FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18
func ValuesRepository(m map[int64]*Repository) []*Repository {
values := make([]*Repository, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}
// RepositoryListOfMap make list from values of map
func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
return RepositoryList(ValuesRepository(repoMap))
}
func (repos RepositoryList) LoadUnits(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
// Load units.
units := make([]*RepoUnit, 0, len(repos)*6)
if err := db.GetEngine(ctx).
In("repo_id", repos.IDs()).
Find(&units); err != nil {
return fmt.Errorf("find units: %w", err)
}
unitsMap := make(map[int64][]*RepoUnit, len(repos))
for _, unit := range units {
if !unit.Type.UnitGlobalDisabled() {
unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit)
}
}
for _, repo := range repos {
repo.Units = unitsMap[repo.ID]
}
return nil
}
func (repos RepositoryList) IDs() []int64 {
repoIDs := make([]int64, len(repos))
for i := range repos {
repoIDs[i] = repos[i].ID
}
return repoIDs
}
func (repos RepositoryList) LoadOwners(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
return repo.OwnerID, true
})
// Load owners.
users := make(map[int64]*user_model.User, len(userIDs))
if err := db.GetEngine(ctx).
Where("id > 0").
In("id", userIDs).
Find(&users); err != nil {
return fmt.Errorf("find users: %w", err)
}
for i := range repos {
repos[i].Owner = users[repos[i].OwnerID]
}
return nil
}
func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
// Load primary language.
stats := make(LanguageStatList, 0, len(repos))
if err := db.GetEngine(ctx).
Where("`is_primary` = ? AND `language` != ?", true, "other").
In("`repo_id`", repos.IDs()).
Find(&stats); err != nil {
return fmt.Errorf("find primary languages: %w", err)
}
stats.LoadAttributes()
for i := range repos {
for _, st := range stats {
if st.RepoID == repos[i].ID {
repos[i].PrimaryLanguage = st
break
}
}
}
return nil
}
// LoadAttributes loads the attributes for the given RepositoryList
func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
if err := repos.LoadOwners(ctx); err != nil {
return err
}
return repos.LoadLanguageStats(ctx)
}
// SearchRepoOptions holds the search options
type SearchRepoOptions struct {
db.ListOptions
Actor *user_model.User
Keyword string
OwnerID int64
PriorityOwnerID int64
TeamID int64
OrderBy db.SearchOrderBy
Private bool // Include private repositories in results
StarredByID int64
WatchedByID int64
AllPublic bool // Include also all public repositories of users and public organisations
AllLimited bool // Include also all public repositories of limited organisations
// None -> include public and private
// True -> include just private
// False -> include just public
IsPrivate optional.Option[bool]
// None -> include collaborative AND non-collaborative
// True -> include just collaborative
// False -> include just non-collaborative
Collaborate optional.Option[bool]
// What type of unit the user can be collaborative in,
// it is ignored if Collaborate is False.
// TypeInvalid means any unit type.
UnitType unit.Type
// None -> include forks AND non-forks
// True -> include just forks
// False -> include just non-forks
Fork optional.Option[bool]
// If Fork option is True, you can use this option to limit the forks of a special repo by repo id.
ForkFrom int64
// None -> include templates AND non-templates
// True -> include just templates
// False -> include just non-templates
Template optional.Option[bool]
// None -> include mirrors AND non-mirrors
// True -> include just mirrors
// False -> include just non-mirrors
Mirror optional.Option[bool]
// None -> include archived AND non-archived
// True -> include just archived
// False -> include just non-archived
Archived optional.Option[bool]
// only search topic name
TopicOnly bool
// only search repositories with specified primary language
Language string
// include description in keyword search
IncludeDescription bool
// None -> include has milestones AND has no milestone
// True -> include just has milestones
// False -> include just has no milestone
HasMilestones optional.Option[bool]
// LowerNames represents valid lower names to restrict to
LowerNames []string
// When specified true, apply some filters over the conditions:
// - Don't show forks, when opts.Fork is OptionalBoolNone.
// - Do not display repositories that don't have a description, an icon and topics.
OnlyShowRelevant bool
}
func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.Private = false
opts.AllLimited = false
}
}
// UserOwnedRepoCond returns user ownered repositories
func UserOwnedRepoCond(userID int64) builder.Cond {
return builder.Eq{
"repository.owner_id": userID,
}
}
// UserAssignedRepoCond return user as assignee repositories list
func UserAssignedRepoCond(id string, userID int64) builder.Cond {
return builder.And(
builder.Eq{
"repository.is_private": false,
},
builder.In(id,
builder.Select("issue.repo_id").From("issue_assignees").
InnerJoin("issue", "issue.id = issue_assignees.issue_id").
Where(builder.Eq{
"issue_assignees.assignee_id": userID,
}),
),
)
}
// UserCreateIssueRepoCond return user created issues repositories list
func UserCreateIssueRepoCond(id string, userID int64, isPull bool) builder.Cond {
return builder.And(
builder.Eq{
"repository.is_private": false,
},
builder.In(id,
builder.Select("issue.repo_id").From("issue").
Where(builder.Eq{
"issue.poster_id": userID,
"issue.is_pull": isPull,
}),
),
)
}
// UserMentionedRepoCond return user metinoed repositories list
func UserMentionedRepoCond(id string, userID int64) builder.Cond {
return builder.And(
builder.Eq{
"repository.is_private": false,
},
builder.In(id,
builder.Select("issue.repo_id").From("issue_user").
InnerJoin("issue", "issue.id = issue_user.issue_id").
Where(builder.Eq{
"issue_user.is_mentioned": true,
"issue_user.uid": userID,
}),
),
)
}
// UserAccessRepoCond returns a condition for selecting all repositories a user has unit independent access to
func UserAccessRepoCond(idStr string, userID int64) builder.Cond {
return builder.In(idStr, builder.Select("repo_id").
From("`access`").
Where(builder.And(
builder.Eq{"`access`.user_id": userID},
builder.Gt{"`access`.mode": int(perm.AccessModeNone)},
)),
)
}
// userCollaborationRepoCond returns a condition for selecting all repositories a user is collaborator in
func UserCollaborationRepoCond(idStr string, userID int64) builder.Cond {
return builder.In(idStr, builder.Select("repo_id").
From("`collaboration`").
Where(builder.And(
builder.Eq{"`collaboration`.user_id": userID},
)),
)
}
// UserOrgTeamRepoCond selects repos that the given user has access to through team membership
func UserOrgTeamRepoCond(idStr string, userID int64) builder.Cond {
return builder.In(idStr, userOrgTeamRepoBuilder(userID))
}
// userOrgTeamRepoBuilder returns repo ids where user's teams can access.
func userOrgTeamRepoBuilder(userID int64) *builder.Builder {
return builder.Select("`team_repo`.repo_id").
From("team_repo").
Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id").
Where(builder.Eq{"`team_user`.uid": userID})
}
// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit.
func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder {
return userOrgTeamRepoBuilder(userID).
Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id").
Where(builder.Eq{"`team_unit`.`type`": unitType}).
And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)})
}
// userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit.
func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) builder.Cond {
return builder.In(idStr, userOrgTeamUnitRepoBuilder(userID, unitType))
}
// UserOrgUnitRepoCond selects repos that the given user has access to through org and the special unit
func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond {
return builder.In(idStr,
userOrgTeamUnitRepoBuilder(userID, unitType).
And(builder.Eq{"`team_unit`.org_id": orgID}),
)
}
// userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations
func userOrgPublicRepoCond(userID int64) builder.Cond {
return builder.And(
builder.Eq{"`repository`.is_private": false},
builder.In("`repository`.owner_id",
builder.Select("`org_user`.org_id").
From("org_user").
Where(builder.Eq{"`org_user`.uid": userID}),
),
)
}
// userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations
func userOrgPublicRepoCondPrivate(userID int64) builder.Cond {
return builder.And(
builder.Eq{"`repository`.is_private": false},
builder.In("`repository`.owner_id",
builder.Select("`org_user`.org_id").
From("org_user").
Join("INNER", "`user`", "`user`.id = `org_user`.org_id").
Where(builder.Eq{
"`org_user`.uid": userID,
"`user`.`type`": user_model.UserTypeOrganization,
"`user`.visibility": structs.VisibleTypePrivate,
}),
),
)
}
// UserOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization
func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond {
return userOrgPublicRepoCond(userID).
And(builder.Eq{"`repository`.owner_id": orgID})
}
// SearchRepositoryCondition creates a query condition according search repository options
func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond {
cond := builder.NewCond()
if opts.Private {
if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID {
// OK we're in the context of a User
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
}
} else {
// Not looking at private organisations and users
// We should be able to see all non-private repositories that
// isn't in a private or limited organisation.
cond = cond.And(
builder.Eq{"is_private": false},
builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(
builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}),
)))
}
if opts.IsPrivate.Has() {
cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()})
}
if opts.Template.Has() {
cond = cond.And(builder.Eq{"is_template": opts.Template.Value()})
}
// Restrict to starred repositories
if opts.StarredByID > 0 {
cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID})))
}
// Restrict to watched repositories
if opts.WatchedByID > 0 {
cond = cond.And(builder.In("id", builder.Select("repo_id").From("watch").Where(builder.Eq{"user_id": opts.WatchedByID})))
}
// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
if opts.OwnerID > 0 {
accessCond := builder.NewCond()
if !opts.Collaborate.Value() {
accessCond = builder.Eq{"owner_id": opts.OwnerID}
}
if opts.Collaborate.ValueOrDefault(true) {
// A Collaboration is:
collaborateCond := builder.NewCond()
// 1. Repository we don't own
collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID})
// 2. But we can see because of:
{
userAccessCond := builder.NewCond()
// A. We have unit independent access
userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID))
// B. We are in a team for
if opts.UnitType == unit.TypeInvalid {
userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID))
} else {
userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType))
}
// C. Public repositories in organizations that we are member of
userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID))
collaborateCond = collaborateCond.And(userAccessCond)
}
if !opts.Private {
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
}
accessCond = accessCond.Or(collaborateCond)
}
if opts.AllPublic {
accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}))))
}
if opts.AllLimited {
accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited}))))
}
cond = cond.And(accessCond)
}
if opts.TeamID > 0 {
cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID})))
}
if opts.Keyword != "" {
// separate keyword
subQueryCond := builder.NewCond()
for v := range strings.SplitSeq(opts.Keyword, ",") {
if opts.TopicOnly {
subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)})
} else {
subQueryCond = subQueryCond.Or(builder.Like{"topic.name", strings.ToLower(v)})
}
}
subQuery := builder.Select("repo_topic.repo_id").From("repo_topic").
Join("INNER", "topic", "topic.id = repo_topic.topic_id").
Where(subQueryCond).
GroupBy("repo_topic.repo_id")
keywordCond := builder.In("id", subQuery)
if !opts.TopicOnly {
likes := builder.NewCond()
for v := range strings.SplitSeq(opts.Keyword, ",") {
likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)})
// If the string looks like "org/repo", match against that pattern too
if opts.TeamID == 0 && strings.Count(opts.Keyword, "/") == 1 {
pieces := strings.Split(opts.Keyword, "/")
ownerName := pieces[0]
repoName := pieces[1]
likes = likes.Or(builder.And(builder.Like{"owner_name", strings.ToLower(ownerName)}, builder.Like{"lower_name", strings.ToLower(repoName)}))
}
if opts.IncludeDescription {
likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)})
}
}
keywordCond = keywordCond.Or(likes)
}
cond = cond.And(keywordCond)
}
if opts.Language != "" {
cond = cond.And(builder.In("id", builder.
Select("repo_id").
From("language_stat").
Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true})))
}
if opts.Fork.Has() || opts.OnlyShowRelevant {
if opts.OnlyShowRelevant && !opts.Fork.Has() {
cond = cond.And(builder.Eq{"is_fork": false})
} else {
cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
if opts.ForkFrom > 0 && opts.Fork.Value() {
cond = cond.And(builder.Eq{"fork_id": opts.ForkFrom})
}
}
}
if opts.Mirror.Has() {
cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()})
}
if opts.Actor != nil && opts.Actor.IsRestricted {
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
}
if opts.Archived.Has() {
cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()})
}
if opts.HasMilestones.Has() {
if opts.HasMilestones.Value() {
cond = cond.And(builder.Gt{"num_milestones": 0})
} else {
cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
}
}
if opts.OnlyShowRelevant {
// Only show a repo that has at least a topic, an icon, or a description
subQueryCond := builder.NewCond()
// Topic checking. Topics are present.
if setting.Database.Type.IsPostgreSQL() { // postgres stores the topics as json and not as text
subQueryCond = subQueryCond.Or(builder.And(builder.NotNull{"topics"}, builder.Neq{"(topics)::text": "[]"}))
} else {
subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"}))
}
// Description checking. Description not empty
subQueryCond = subQueryCond.Or(builder.Neq{"description": ""})
// Repo has a avatar
subQueryCond = subQueryCond.Or(builder.Neq{"avatar": ""})
// Always hide repo's that are empty
subQueryCond = subQueryCond.And(builder.Eq{"is_empty": false})
cond = cond.And(subQueryCond)
}
return cond
}
// SearchRepository returns repositories based on search options,
// it returns results in given range and number of total results.
func SearchRepository(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
cond := SearchRepositoryCondition(opts)
return SearchRepositoryByCondition(ctx, opts, cond, true)
}
// CountRepository counts repositories based on search options,
func CountRepository(ctx context.Context, opts SearchRepoOptions) (int64, error) {
return db.GetEngine(ctx).Where(SearchRepositoryCondition(opts)).Count(new(Repository))
}
// SearchRepositoryByCondition search repositories by condition
func SearchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) {
sess, count, err := searchRepositoryByCondition(ctx, opts, cond)
if err != nil {
return nil, 0, err
}
defaultSize := 50
if opts.PageSize > 0 {
defaultSize = opts.PageSize
}
repos := make(RepositoryList, 0, defaultSize)
if err := sess.Find(&repos); err != nil {
return nil, 0, fmt.Errorf("Repo: %w", err)
}
if opts.PageSize <= 0 {
count = int64(len(repos))
}
if loadAttributes {
if err := repos.LoadAttributes(ctx); err != nil {
return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
}
}
return repos, count, nil
}
func searchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) {
page := opts.Page
if page <= 0 {
page = 1
}
orderBy := opts.OrderBy
if len(orderBy) == 0 {
orderBy = db.SearchOrderByAlphabetically
}
args := make([]any, 0)
if opts.PriorityOwnerID > 0 {
orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", orderBy))
args = append(args, opts.PriorityOwnerID)
} else if strings.Count(opts.Keyword, "/") == 1 {
// With "owner/repo" search times, prioritise results which match the owner field
orgName := strings.Split(opts.Keyword, "/")[0]
orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", orderBy))
args = append(args, orgName)
}
sess := db.GetEngine(ctx)
var count int64
if opts.PageSize > 0 {
var err error
count, err = sess.
Where(cond).
Count(new(Repository))
if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err)
}
}
sess = sess.Where(cond).OrderBy(orderBy.String(), args...)
if opts.PageSize > 0 {
sess = sess.Limit(opts.PageSize, (page-1)*opts.PageSize)
}
return sess, count, nil
}
// SearchRepositoryIDsByCondition search repository IDs by given condition.
func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]int64, error) {
repoIDs := make([]int64, 0, 10)
return repoIDs, db.GetEngine(ctx).
Table("repository").
Cols("id").
Where(cond).
Find(&repoIDs)
}
func userAllPublicRepoCond(cond builder.Cond, orgVisibilityLimit []structs.VisibleType) builder.Cond {
return cond.Or(builder.And(
builder.Eq{"`repository`.is_private": false},
// Aren't in a private organisation or limited organisation if we're not logged in
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
builder.And(
builder.Eq{"type": user_model.UserTypeOrganization},
builder.In("visibility", orgVisibilityLimit)),
))))
}
// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
cond := builder.NewCond()
if user == nil || !user.IsRestricted || user.ID <= 0 {
orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate}
if user == nil || user.ID <= 0 {
orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited)
}
// 1. Be able to see all non-private repositories
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
}
if user != nil {
// 2. Be able to see all repositories that we have unit independent access to
// 3. Be able to see all repositories through team membership(s)
if unitType == unit.TypeInvalid {
// Regardless of UnitType
cond = cond.Or(
UserAccessRepoCond("`repository`.id", user.ID),
UserOrgTeamRepoCond("`repository`.id", user.ID),
)
} else {
// For a specific UnitType
cond = cond.Or(
UserCollaborationRepoCond("`repository`.id", user.ID),
userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
)
}
// 4. Repositories that we directly own
cond = cond.Or(builder.Eq{"`repository`.owner_id": user.ID})
if !user.IsRestricted {
// 5. Be able to see all public repos in private organizations that we are an org_user of
cond = cond.Or(userOrgPublicRepoCond(user.ID))
} else if !setting.Service.RequireSignInViewStrict {
orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate, structs.VisibleTypeLimited}
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
}
}
return cond
}
// SearchRepositoryByName takes keyword and part of repository name to search,
// it returns results in given range and number of total results.
func SearchRepositoryByName(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
opts.IncludeDescription = false
return SearchRepository(ctx, opts)
}
// SearchRepositoryIDs takes keyword and part of repository name to search,
// it returns results in given range and number of total results.
func SearchRepositoryIDs(ctx context.Context, opts SearchRepoOptions) ([]int64, int64, error) {
opts.IncludeDescription = false
cond := SearchRepositoryCondition(opts)
sess, count, err := searchRepositoryByCondition(ctx, opts, cond)
if err != nil {
return nil, 0, err
}
defaultSize := 50
if opts.PageSize > 0 {
defaultSize = opts.PageSize
}
ids := make([]int64, 0, defaultSize)
err = sess.Select("id").Table("repository").Find(&ids)
if opts.PageSize <= 0 {
count = int64(len(ids))
}
return ids, count, err
}
// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered.
func AccessibleRepoIDsQuery(user *user_model.User) *builder.Builder {
// NB: Please note this code needs to still work if user is nil
return builder.Select("id").From("repository").Where(AccessibleRepositoryCondition(user, unit.TypeInvalid))
}
// FindUserCodeAccessibleRepoIDs finds all at Code level accessible repositories' ID by the user's id
func FindUserCodeAccessibleRepoIDs(ctx context.Context, user *user_model.User) ([]int64, error) {
return SearchRepositoryIDsByCondition(ctx, AccessibleRepositoryCondition(user, unit.TypeCode))
}
// FindUserCodeAccessibleOwnerRepoIDs finds all repository IDs for the given owner whose code the user can see.
func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user *user_model.User) ([]int64, error) {
return SearchRepositoryIDsByCondition(ctx, builder.NewCond().And(
builder.Eq{"owner_id": ownerID},
AccessibleRepositoryCondition(user, unit.TypeCode),
))
}
// GetUserRepositories returns a list of repositories of given user.
func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
if len(opts.OrderBy) == 0 {
opts.OrderBy = "updated_unix DESC"
}
cond := builder.NewCond()
if opts.Actor == nil {
return nil, 0, util.NewInvalidArgumentErrorf("GetUserRepositories: Actor is needed but not given")
}
cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID})
if !opts.Private {
cond = cond.And(builder.Eq{"is_private": false})
}
if len(opts.LowerNames) > 0 {
cond = cond.And(builder.In("lower_name", opts.LowerNames))
}
sess := db.GetEngine(ctx)
count, err := sess.Where(cond).Count(new(Repository))
if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err)
}
sess = sess.Where(cond).OrderBy(opts.OrderBy.String())
repos := make(RepositoryList, 0, opts.PageSize)
return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos)
}
func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int64) (RepositoryList, error) {
if len(repoIDs) == 0 {
return RepositoryList{}, nil
}
repos := make(RepositoryList, 0, len(repoIDs))
return repos, db.GetEngine(ctx).Where(builder.Eq{"owner_id": ownerID}).In("id", repoIDs).Find(&repos)
}
+468
View File
@@ -0,0 +1,468 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"strings"
"testing"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func getTestCases() []struct {
name string
opts repo_model.SearchRepoOptions
count int
} {
testCases := []struct {
name string
opts repo_model.SearchRepoOptions
count int
}{
{
name: "PublicRepositoriesByName",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)},
count: 7,
},
{
name: "PublicAndPrivateRepositoriesByName",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)},
count: 14,
},
{
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
count: 14,
},
{
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
count: 14,
},
{
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
count: 14,
},
{
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
count: 14,
},
{
name: "PublicRepositoriesOfUser",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)},
count: 2,
},
{
name: "PublicRepositoriesOfUser2",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)},
count: 0,
},
{
name: "PublicRepositoriesOfOrg3",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)},
count: 2,
},
{
name: "PublicAndPrivateRepositoriesOfUser",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)},
count: 4,
},
{
name: "PublicAndPrivateRepositoriesOfUser2",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)},
count: 0,
},
{
name: "PublicAndPrivateRepositoriesOfOrg3",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)},
count: 4,
},
{
name: "PublicRepositoriesOfUserIncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15},
count: 5,
},
{
name: "PublicRepositoriesOfUser2IncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18},
count: 1,
},
{
name: "PublicRepositoriesOfOrg3IncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20},
count: 3,
},
{
name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true},
count: 9,
},
{
name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true},
count: 4,
},
{
name: "PublicAndPrivateRepositoriesOfOrg3IncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true},
count: 7,
},
{
name: "PublicRepositoriesOfOrganization",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)},
count: 1,
},
{
name: "PublicAndPrivateRepositoriesOfOrganization",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)},
count: 2,
},
{
name: "AllPublic/PublicRepositoriesByName",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)},
count: 7,
},
{
name: "AllPublic/PublicAndPrivateRepositoriesByName",
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)},
count: 14,
},
{
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)},
count: 34,
},
{
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)},
count: 39,
},
{
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true},
count: 15,
},
{
name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName",
opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true},
count: 13,
},
{
name: "AllPublic/PublicRepositoriesOfOrganization",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)},
count: 34,
},
{
name: "AllTemplates",
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)},
count: 2,
},
{
name: "OwnerSlashRepoSearch",
opts: repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0},
count: 2,
},
{
name: "OwnerSlashSearch",
opts: repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0},
count: 4,
},
}
return testCases
}
func TestSearchRepository(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("SearchRepositoryPublic", testSearchRepositoryPublic)
t.Run("SearchRepositoryPublicRestricted", testSearchRepositoryRestricted)
t.Run("SearchRepositoryPrivate", testSearchRepositoryPrivate)
t.Run("SearchRepositoryNonExistingOwner", testSearchRepositoryNonExistingOwner)
t.Run("SearchRepositoryWithInDescription", testSearchRepositoryWithInDescription)
t.Run("SearchRepositoryNotInDescription", testSearchRepositoryNotInDescription)
t.Run("SearchRepositoryCases", testSearchRepositoryCases)
}
func testSearchRepositoryPublic(t *testing.T) {
// test search public repository on explore page
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 10,
},
Keyword: "repo_12",
Collaborate: optional.Some(false),
})
assert.NoError(t, err)
if assert.Len(t, repos, 1) {
assert.Equal(t, "test_repo_12", repos[0].Name)
}
assert.Equal(t, int64(1), count)
repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 10,
},
Keyword: "test_repo",
Collaborate: optional.Some(false),
})
assert.NoError(t, err)
assert.Equal(t, int64(2), count)
assert.Len(t, repos, 2)
}
func testSearchRepositoryRestricted(t *testing.T) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true})
performSearch := func(t *testing.T, user *user_model.User) (publicRepoIDs []int64) {
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 10000},
Actor: user,
})
require.NoError(t, err)
assert.Len(t, repos, int(count))
for _, repo := range repos {
require.NoError(t, repo.LoadOwner(t.Context()))
if repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate {
publicRepoIDs = append(publicRepoIDs, repo.ID)
}
}
return publicRepoIDs
}
normalPublicRepoIDs := performSearch(t, user2)
require.Greater(t, len(normalPublicRepoIDs), 10) // quite a lot
t.Run("RestrictedUser-NoSignInRequirement", func(t *testing.T) {
// restricted user can also see public repositories if no "required sign-in"
repoIDs := performSearch(t, restrictedUser)
assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs)
})
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
t.Run("NormalUser-RequiredSignIn", func(t *testing.T) {
// normal user can still see all public repos, not affected by "required sign-in"
repoIDs := performSearch(t, user2)
assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs)
})
t.Run("RestrictedUser-RequiredSignIn", func(t *testing.T) {
// restricted user can see only their own repo
repoIDs := performSearch(t, restrictedUser)
assert.Equal(t, []int64{4}, repoIDs)
})
}
func testSearchRepositoryPrivate(t *testing.T) {
// test search private repository on explore page
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 10,
},
Keyword: "repo_13",
Private: true,
Collaborate: optional.Some(false),
})
assert.NoError(t, err)
if assert.Len(t, repos, 1) {
assert.Equal(t, "test_repo_13", repos[0].Name)
}
assert.Equal(t, int64(1), count)
repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 10,
},
Keyword: "test_repo",
Private: true,
Collaborate: optional.Some(false),
})
assert.NoError(t, err)
assert.Equal(t, int64(3), count)
assert.Len(t, repos, 3)
}
func testSearchRepositoryNonExistingOwner(t *testing.T) {
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID})
assert.NoError(t, err)
assert.Empty(t, repos)
assert.Equal(t, int64(0), count)
}
func testSearchRepositoryWithInDescription(t *testing.T) {
repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 10,
},
Keyword: "description_14",
Collaborate: optional.Some(false),
IncludeDescription: true,
})
assert.NoError(t, err)
if assert.Len(t, repos, 1) {
assert.Equal(t, "test_repo_14", repos[0].Name)
}
assert.Equal(t, int64(1), count)
}
func testSearchRepositoryNotInDescription(t *testing.T) {
repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 10,
},
Keyword: "description_14",
Collaborate: optional.Some(false),
IncludeDescription: false,
})
assert.NoError(t, err)
assert.Empty(t, repos)
assert.Equal(t, int64(0), count)
}
func testSearchRepositoryCases(t *testing.T) {
testCases := getTestCases()
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), testCase.opts)
assert.NoError(t, err)
assert.Equal(t, int64(testCase.count), count)
page := testCase.opts.Page
if page <= 0 {
page = 1
}
expectedLen := testCase.opts.PageSize
if testCase.opts.PageSize*page > testCase.count+testCase.opts.PageSize {
expectedLen = 0
} else if testCase.opts.PageSize*page > testCase.count {
expectedLen = testCase.count % testCase.opts.PageSize
}
if assert.Len(t, repos, expectedLen) {
for _, repo := range repos {
assert.NotEmpty(t, repo.Name)
if len(testCase.opts.Keyword) > 0 {
// Keyword match condition is different for search terms of form "owner/repo"
if strings.Count(testCase.opts.Keyword, "/") == 1 {
// May still match as a whole...
wholeMatch := strings.Contains(repo.Name, testCase.opts.Keyword)
pieces := strings.Split(testCase.opts.Keyword, "/")
ownerName := pieces[0]
repoName := pieces[1]
// ... or match in parts
splitMatch := strings.Contains(repo.OwnerName, ownerName) && strings.Contains(repo.Name, repoName)
assert.True(t, wholeMatch || splitMatch, "Keyword '%s' does not match repo '%s/%s'", testCase.opts.Keyword, repo.Owner.Name, repo.Name)
} else {
assert.Contains(t, repo.Name, testCase.opts.Keyword)
}
}
if !testCase.opts.Private {
assert.False(t, repo.IsPrivate)
}
if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() {
assert.True(t, repo.IsFork && repo.IsMirror)
} else {
if testCase.opts.Fork.Has() {
assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork)
}
if testCase.opts.Mirror.Has() {
assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror)
}
}
if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic {
if testCase.opts.Collaborate.Has() {
if testCase.opts.Collaborate.Value() {
assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
} else {
assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
}
}
}
}
}
})
}
}
func TestCountRepository(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testCases := getTestCases()
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
count, err := repo_model.CountRepository(t.Context(), testCase.opts)
assert.NoError(t, err)
assert.Equal(t, int64(testCase.count), count)
})
}
}
func TestSearchRepositoryByTopicName(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testCases := []struct {
name string
opts repo_model.SearchRepoOptions
count int
}{
{
name: "AllPublic/SearchPublicRepositoriesFromTopicAndName",
opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"},
count: 2,
},
{
name: "AllPublic/OnlySearchPublicRepositoriesFromTopic",
opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true},
count: 1,
},
{
name: "AllPublic/OnlySearchMultipleKeywordPublicRepositoriesFromTopic",
opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true},
count: 2,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
_, count, err := repo_model.SearchRepositoryByName(t.Context(), testCase.opts)
assert.NoError(t, err)
assert.Equal(t, int64(testCase.count), count)
})
}
}
+237
View File
@@ -0,0 +1,237 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/markup"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
countRepospts = CountRepositoryOptions{OwnerID: 10}
countReposptsPublic = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
)
func TestGetRepositoryCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
count, err1 := CountRepositories(ctx, countRepospts)
privateCount, err2 := CountRepositories(ctx, countReposptsPrivate)
publicCount, err3 := CountRepositories(ctx, countReposptsPublic)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
assert.Equal(t, int64(3), count)
assert.Equal(t, privateCount+publicCount, count)
}
func TestGetPublicRepositoryCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
count, err := CountRepositories(t.Context(), countReposptsPublic)
assert.NoError(t, err)
assert.Equal(t, int64(1), count)
}
func TestGetPrivateRepositoryCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
count, err := CountRepositories(t.Context(), countReposptsPrivate)
assert.NoError(t, err)
assert.Equal(t, int64(2), count)
}
func TestRepoAPIURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 10})
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL())
}
func TestWatchRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.NoError(t, WatchRepo(t.Context(), user, repo, true))
unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
assert.NoError(t, WatchRepo(t.Context(), user, repo, false))
unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
}
func TestMetas(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := &Repository{Name: "testRepo"}
repo.Owner = &user_model.User{Name: "testOwner"}
repo.OwnerName = repo.Owner.Name
repo.Units = nil
metas := repo.ComposeCommentMetas(t.Context())
assert.Equal(t, "testRepo", metas["repo"])
assert.Equal(t, "testOwner", metas["user"])
externalTracker := RepoUnit{
Type: unit.TypeExternalTracker,
Config: &ExternalTrackerConfig{
ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}",
},
}
testSuccess := func(expectedStyle string) {
repo.Units = []*RepoUnit{&externalTracker}
repo.commonRenderingMetas = nil
metas := repo.ComposeCommentMetas(t.Context())
assert.Equal(t, expectedStyle, metas["style"])
assert.Equal(t, "testRepo", metas["repo"])
assert.Equal(t, "testOwner", metas["user"])
assert.Equal(t, "https://someurl.com/{user}/{repo}/{issue}", metas["format"])
}
testSuccess(markup.IssueNameStyleNumeric)
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleAlphanumeric
testSuccess(markup.IssueNameStyleAlphanumeric)
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleNumeric
testSuccess(markup.IssueNameStyleNumeric)
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
testSuccess(markup.IssueNameStyleRegexp)
repo, err := GetRepositoryByID(t.Context(), 3)
assert.NoError(t, err)
metas = repo.ComposeCommentMetas(t.Context())
assert.Contains(t, metas, "org")
assert.Contains(t, metas, "teams")
assert.Equal(t, "org3", metas["org"])
assert.Equal(t, ",owners,team1,", metas["teams"])
}
func TestGetRepositoryByURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("InvalidPath", func(t *testing.T) {
repo, err := GetRepositoryByURL(t.Context(), "something")
assert.Nil(t, repo)
assert.Error(t, err)
})
testRepo2 := func(t *testing.T, url string) {
repo, err := GetRepositoryByURL(t.Context(), url)
require.NoError(t, err)
assert.EqualValues(t, 2, repo.ID)
assert.EqualValues(t, 2, repo.OwnerID)
}
t.Run("ValidHttpURL", func(t *testing.T) {
testRepo2(t, "https://try.gitea.io/user2/repo2")
testRepo2(t, "https://try.gitea.io/user2/repo2.git")
})
t.Run("ValidGitSshURL", func(t *testing.T) {
testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2")
testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git")
testRepo2(t, "git+ssh://try.gitea.io/user2/repo2")
testRepo2(t, "git+ssh://try.gitea.io/user2/repo2.git")
})
t.Run("ValidImplicitSshURL", func(t *testing.T) {
testRepo2(t, "sshuser@try.gitea.io:user2/repo2")
testRepo2(t, "sshuser@try.gitea.io:user2/repo2.git")
testRelax := func(t *testing.T, url string) {
repo, err := GetRepositoryByURLRelax(t.Context(), url)
require.NoError(t, err)
assert.Equal(t, int64(2), repo.ID)
assert.Equal(t, int64(2), repo.OwnerID)
}
// TODO: it doesn't seem to be common git ssh URL, should we really support this?
testRelax(t, "try.gitea.io:user2/repo2")
testRelax(t, "try.gitea.io:user2/repo2.git")
})
}
func TestComposeSSHCloneURL(t *testing.T) {
defer test.MockVariableValue(&setting.SSH, setting.SSH)()
defer test.MockVariableValue(&setting.Repository, setting.Repository)()
setting.SSH.User = "git"
// test SSH_DOMAIN
setting.SSH.Domain = "domain"
setting.SSH.Port = 22
setting.Repository.UseCompatSSHURI = false
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
setting.Repository.UseCompatSSHURI = true
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
// test SSH_DOMAIN while use non-standard SSH port
setting.SSH.Port = 123
setting.Repository.UseCompatSSHURI = false
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
setting.Repository.UseCompatSSHURI = true
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
// test IPv6 SSH_DOMAIN
setting.Repository.UseCompatSSHURI = false
setting.SSH.Domain = "::1"
setting.SSH.Port = 22
assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
setting.SSH.Port = 123
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
setting.SSH.User = "(DOER_USERNAME)"
setting.SSH.Domain = "domain"
setting.SSH.Port = 22
assert.Equal(t, "doer@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
setting.SSH.Port = 123
assert.Equal(t, "ssh://doer@domain:123/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
}
func TestIsUsableRepoName(t *testing.T) {
assert.NoError(t, IsUsableRepoName("a"))
assert.NoError(t, IsUsableRepoName("-1_."))
assert.NoError(t, IsUsableRepoName(".profile"))
assert.Error(t, IsUsableRepoName("-"))
assert.Error(t, IsUsableRepoName("🌞"))
assert.Error(t, IsUsableRepoName("the/repo"))
assert.Error(t, IsUsableRepoName("the..repo"))
assert.Error(t, IsUsableRepoName("foo.wiki"))
assert.Error(t, IsUsableRepoName("foo.git"))
assert.Error(t, IsUsableRepoName("foo.RSS"))
}
func TestIsValidSSHAccessRepoName(t *testing.T) {
assert.True(t, IsValidSSHAccessRepoName("a"))
assert.True(t, IsValidSSHAccessRepoName("-1_."))
assert.True(t, IsValidSSHAccessRepoName(".profile"))
assert.True(t, IsValidSSHAccessRepoName("foo.wiki"))
assert.False(t, IsValidSSHAccessRepoName("-"))
assert.False(t, IsValidSSHAccessRepoName("🌞"))
assert.False(t, IsValidSSHAccessRepoName("the/repo"))
assert.False(t, IsValidSSHAccessRepoName("the..repo"))
assert.False(t, IsValidSSHAccessRepoName("foo.git"))
assert.False(t, IsValidSSHAccessRepoName("foo.RSS"))
}
+363
View File
@@ -0,0 +1,363 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
"gitea.dev/modules/json"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/xorm"
"xorm.io/xorm/convert"
)
// ErrUnitTypeNotExist represents a "UnitTypeNotExist" kind of error.
type ErrUnitTypeNotExist struct {
UT unit.Type
}
// IsErrUnitTypeNotExist checks if an error is a ErrUnitNotExist.
func IsErrUnitTypeNotExist(err error) bool {
_, ok := err.(ErrUnitTypeNotExist)
return ok
}
func (err ErrUnitTypeNotExist) Error() string {
return "Unit type does not exist: " + err.UT.LogString()
}
func (err ErrUnitTypeNotExist) Unwrap() error {
return util.ErrNotExist
}
// RepoUnit describes all units of a repository
type RepoUnit struct { //revive:disable-line:exported
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type unit.Type `xorm:"INDEX(s)"`
Config convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
}
func init() {
db.RegisterModel(new(RepoUnit))
}
// UnitConfig describes common unit config
type UnitConfig struct{}
// FromDB fills up a UnitConfig from serialized format.
func (cfg *UnitConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports a UnitConfig to a serialized format.
func (cfg *UnitConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// ExternalWikiConfig describes external wiki config
type ExternalWikiConfig struct {
ExternalWikiURL string
}
// FromDB fills up a ExternalWikiConfig from serialized format.
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports a ExternalWikiConfig to a serialized format.
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// ExternalTrackerConfig describes external tracker config
type ExternalTrackerConfig struct {
ExternalTrackerURL string
ExternalTrackerFormat string
ExternalTrackerStyle string
ExternalTrackerRegexpPattern string
}
// FromDB fills up a ExternalTrackerConfig from serialized format.
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports a ExternalTrackerConfig to a serialized format.
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// IssuesConfig describes issues config
type IssuesConfig struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableDependencies bool
}
// FromDB fills up a IssuesConfig from serialized format.
func (cfg *IssuesConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports a IssuesConfig to a serialized format.
func (cfg *IssuesConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// PullRequestsConfig describes pull requests config
type PullRequestsConfig struct {
IgnoreWhitespaceConflicts bool
AllowMerge bool
AllowRebase bool
AllowRebaseMerge bool
AllowSquash bool
AllowFastForwardOnly bool
AllowManualMerge bool
AutodetectManualMerge bool
AllowMergeUpdate bool
AllowRebaseUpdate bool
DefaultUpdateStyle UpdateStyle
DefaultDeleteBranchAfterMerge bool
DefaultMergeStyle MergeStyle
DefaultAllowMaintainerEdit bool
DefaultTargetBranch string
}
func DefaultPullRequestsConfig() *PullRequestsConfig {
cfg := &PullRequestsConfig{
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
AllowMergeUpdate: true,
AllowRebaseUpdate: true,
DefaultUpdateStyle: UpdateStyleMerge,
DefaultAllowMaintainerEdit: true,
}
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge)
return cfg
}
// FromDB fills up a PullRequestsConfig from serialized format.
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
// set default values for existing PullRequestConfig in DB
*cfg = *DefaultPullRequestsConfig()
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error
cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge)
return nil
}
// ToDB exports a PullRequestsConfig to a serialized format.
func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// IsMergeStyleAllowed returns if merge style is allowed
func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
return mergeStyle == MergeStyleMerge && cfg.AllowMerge ||
mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly ||
mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
}
// IsUpdateStyleAllowed returns if a pull request branch update style is allowed
func (cfg *PullRequestsConfig) IsUpdateStyleAllowed(updateStyle UpdateStyle) bool {
switch updateStyle {
case UpdateStyleMerge:
return cfg.AllowMergeUpdate
case UpdateStyleRebase:
return cfg.AllowRebaseUpdate
default:
return false
}
}
// ValidateUpdateSettings checks that the AllowMerge/RebaseUpdate flags and DefaultUpdateStyle are mutually consistent.
func (cfg *PullRequestsConfig) ValidateUpdateSettings() error {
if cfg.DefaultUpdateStyle != UpdateStyleMerge && cfg.DefaultUpdateStyle != UpdateStyleRebase {
return util.NewInvalidArgumentErrorf("default update style must be merge or rebase")
}
if !cfg.AllowMergeUpdate && !cfg.AllowRebaseUpdate {
return util.NewInvalidArgumentErrorf("at least one pull request branch update style must be enabled")
}
if !cfg.IsUpdateStyleAllowed(cfg.DefaultUpdateStyle) {
return util.NewInvalidArgumentErrorf("default update style must be enabled")
}
return nil
}
func DefaultPullRequestsUnit(repoID int64) RepoUnit {
return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()}
}
// ProjectsMode represents the projects enabled for a repository
type ProjectsMode string
const (
// ProjectsModeRepo allows only repo-level projects
ProjectsModeRepo ProjectsMode = "repo"
// ProjectsModeOwner allows only owner-level projects
ProjectsModeOwner ProjectsMode = "owner"
// ProjectsModeAll allows both kinds of projects
ProjectsModeAll ProjectsMode = "all"
// ProjectsModeNone doesn't allow projects
ProjectsModeNone ProjectsMode = "none"
)
// ProjectsConfig describes projects config
type ProjectsConfig struct {
ProjectsMode ProjectsMode
}
// FromDB fills up a ProjectsConfig from serialized format.
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
// TODO: remove GetProjectsMode, only use ProjectsMode
cfg.ProjectsMode = ProjectsModeAll
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports a ProjectsConfig to a serialized format.
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
if cfg.ProjectsMode != "" {
return cfg.ProjectsMode
}
return ProjectsModeAll
}
func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
projectsMode := cfg.GetProjectsMode()
if m == ProjectsModeNone {
return true
}
return projectsMode == m || projectsMode == ProjectsModeAll
}
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName {
case "type":
var err error
r.Type, _, err = db.CellToInt(val, unit.TypeInvalid)
if err != nil {
setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err)
}
switch r.Type {
case unit.TypeExternalWiki:
r.Config = new(ExternalWikiConfig)
case unit.TypeExternalTracker:
r.Config = new(ExternalTrackerConfig)
case unit.TypePullRequests:
r.Config = new(PullRequestsConfig)
case unit.TypeIssues:
r.Config = new(IssuesConfig)
case unit.TypeActions:
r.Config = new(ActionsConfig)
case unit.TypeProjects:
r.Config = new(ProjectsConfig)
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
fallthrough
default:
r.Config = new(UnitConfig)
}
case "config":
if *val == nil {
// XROM doesn't call FromDB if the value is nil, but we need to set default values for the config fields
_ = r.Config.FromDB(nil)
}
}
}
// Unit returns Unit
func (r *RepoUnit) Unit() unit.Unit {
return unit.Units[r.Type]
}
// CodeConfig returns config for unit.TypeCode
func (r *RepoUnit) CodeConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
// PullRequestsConfig returns config for unit.TypePullRequests
func (r *RepoUnit) PullRequestsConfig() *PullRequestsConfig {
return r.Config.(*PullRequestsConfig)
}
// ReleasesConfig returns config for unit.TypeReleases
func (r *RepoUnit) ReleasesConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
// ExternalWikiConfig returns config for unit.TypeExternalWiki
func (r *RepoUnit) ExternalWikiConfig() *ExternalWikiConfig {
return r.Config.(*ExternalWikiConfig)
}
// IssuesConfig returns config for unit.TypeIssues
func (r *RepoUnit) IssuesConfig() *IssuesConfig {
return r.Config.(*IssuesConfig)
}
// ExternalTrackerConfig returns config for unit.TypeExternalTracker
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
return r.Config.(*ExternalTrackerConfig)
}
// ActionsConfig returns config for unit.ActionsConfig
func (r *RepoUnit) ActionsConfig() *ActionsConfig {
return r.Config.(*ActionsConfig)
}
// ProjectsConfig returns config for unit.ProjectsConfig
func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
return r.Config.(*ProjectsConfig)
}
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
var tmpUnits []*RepoUnit
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
return nil, err
}
for _, u := range tmpUnits {
if !u.Type.UnitGlobalDisabled() {
units = append(units, u)
}
}
return units, nil
}
// UpdateRepoUnitConfig updates the config of the provided repo unit
func UpdateRepoUnitConfig(ctx context.Context, unit *RepoUnit) error {
_, err := db.GetEngine(ctx).ID(unit.ID).Cols("config").Update(unit)
return err
}
func UpdateRepoUnitPublicAccess(ctx context.Context, unit *RepoUnit) error {
_, err := db.GetEngine(ctx).Where("repo_id=? AND `type`=?", unit.RepoID, unit.Type).
Cols("anonymous_access_mode", "everyone_access_mode").Update(unit)
return err
}
+153
View File
@@ -0,0 +1,153 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"slices"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
)
// ActionsTokenPermissionMode defines the default permission mode for Actions tokens
type ActionsTokenPermissionMode string
const (
// ActionsTokenPermissionModePermissive - write access by default (current behavior, backwards compatible)
ActionsTokenPermissionModePermissive ActionsTokenPermissionMode = "permissive"
// ActionsTokenPermissionModeRestricted - read access by default
ActionsTokenPermissionModeRestricted ActionsTokenPermissionMode = "restricted"
)
func (ActionsTokenPermissionMode) EnumValues() []ActionsTokenPermissionMode {
return []ActionsTokenPermissionMode{ActionsTokenPermissionModePermissive /* default */, ActionsTokenPermissionModeRestricted}
}
// ActionsTokenPermissions defines the permissions for different repository units
type ActionsTokenPermissions struct {
UnitAccessModes map[unit.Type]perm.AccessMode `json:"unit_access_modes,omitempty"`
}
var ActionsTokenUnitTypes = []unit.Type{
unit.TypeCode,
unit.TypeIssues,
unit.TypePullRequests,
unit.TypePackages,
unit.TypeActions,
unit.TypeWiki,
unit.TypeReleases,
unit.TypeProjects,
}
func MakeActionsTokenPermissions(unitAccessMode perm.AccessMode) (ret ActionsTokenPermissions) {
ret.UnitAccessModes = make(map[unit.Type]perm.AccessMode)
for _, u := range ActionsTokenUnitTypes {
ret.UnitAccessModes[u] = unitAccessMode
}
return ret
}
// ClampActionsTokenPermissions ensures that the given permissions don't exceed the maximum
func ClampActionsTokenPermissions(p1, p2 ActionsTokenPermissions) (ret ActionsTokenPermissions) {
ret.UnitAccessModes = make(map[unit.Type]perm.AccessMode)
for _, ut := range ActionsTokenUnitTypes {
ret.UnitAccessModes[ut] = min(p1.UnitAccessModes[ut], p2.UnitAccessModes[ut])
}
return ret
}
// MakeRestrictedPermissions returns the restricted permissions
func MakeRestrictedPermissions() ActionsTokenPermissions {
ret := MakeActionsTokenPermissions(perm.AccessModeNone)
ret.UnitAccessModes[unit.TypeCode] = perm.AccessModeRead
ret.UnitAccessModes[unit.TypePackages] = perm.AccessModeRead
ret.UnitAccessModes[unit.TypeReleases] = perm.AccessModeRead
return ret
}
type ActionsConfig struct {
DisabledWorkflows []string
// CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos.
// Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions.
CollaborativeOwnerIDs []int64
// TokenPermissionMode defines the default permission mode (permissive, restricted, or custom)
TokenPermissionMode ActionsTokenPermissionMode `json:"token_permission_mode,omitempty"`
// MaxTokenPermissions defines the absolute maximum permissions any token can have in this context.
// Workflow YAML "permissions" keywords can reduce permissions but never exceed this ceiling.
MaxTokenPermissions *ActionsTokenPermissions `json:"max_token_permissions,omitempty"`
// OverrideOwnerConfig indicates if this repository should override the owner-level configuration (User or Org)
OverrideOwnerConfig bool `json:"override_owner_config,omitempty"`
}
func (cfg *ActionsConfig) EnableWorkflow(file string) {
cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool {
return slices.Contains(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) DisableWorkflow(file string) {
if slices.Contains(cfg.DisabledWorkflows, file) {
return
}
cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) {
if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) {
cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID)
}
}
func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) {
cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID)
}
func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool {
return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID)
}
// GetDefaultTokenPermissions returns the default token permissions by its TokenPermissionMode.
// It does not apply MaxTokenPermissions; callers must clamp if needed.
func (cfg *ActionsConfig) GetDefaultTokenPermissions() ActionsTokenPermissions {
switch cfg.TokenPermissionMode {
case ActionsTokenPermissionModeRestricted:
return MakeRestrictedPermissions()
case ActionsTokenPermissionModePermissive:
return MakeActionsTokenPermissions(perm.AccessModeWrite)
default:
return ActionsTokenPermissions{}
}
}
// GetMaxTokenPermissions returns the maximum allowed permissions
func (cfg *ActionsConfig) GetMaxTokenPermissions() ActionsTokenPermissions {
if cfg.MaxTokenPermissions != nil {
return *cfg.MaxTokenPermissions
}
// Default max is write for everything
return MakeActionsTokenPermissions(perm.AccessModeWrite)
}
// ClampPermissions ensures that the given permissions don't exceed the maximum
func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) ActionsTokenPermissions {
maxPerms := cfg.GetMaxTokenPermissions()
return ClampActionsTokenPermissions(perms, maxPerms)
}
// FromDB fills up a ActionsConfig from serialized format.
func (cfg *ActionsConfig) FromDB(bs []byte) error {
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg)
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
return nil
}
// ToDB exports a ActionsConfig to a serialized format.
func (cfg *ActionsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"strings"
"testing"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
"github.com/stretchr/testify/assert"
)
func TestActionsConfig(t *testing.T) {
cfg := &ActionsConfig{}
cfg.DisableWorkflow("test1.yaml")
assert.Equal(t, []string{"test1.yaml"}, cfg.DisabledWorkflows)
cfg.DisableWorkflow("test1.yaml")
assert.Equal(t, []string{"test1.yaml"}, cfg.DisabledWorkflows)
cfg.EnableWorkflow("test1.yaml")
assert.Equal(t, []string{}, cfg.DisabledWorkflows)
cfg.EnableWorkflow("test1.yaml")
assert.Equal(t, []string{}, cfg.DisabledWorkflows)
cfg.DisableWorkflow("test1.yaml")
cfg.DisableWorkflow("test2.yaml")
cfg.DisableWorkflow("test3.yaml")
assert.Equal(t, "test1.yaml,test2.yaml,test3.yaml", strings.Join(cfg.DisabledWorkflows, ","))
}
func TestActionsConfigTokenPermissions(t *testing.T) {
t.Run("Default Permission Mode", func(t *testing.T) {
cfg := &ActionsConfig{TokenPermissionMode: "invalid-value"}
_ = cfg.FromDB(nil)
assert.Equal(t, ActionsTokenPermissionModePermissive, cfg.TokenPermissionMode)
assert.Equal(t, perm.AccessModeWrite, cfg.GetDefaultTokenPermissions().UnitAccessModes[unit.TypeCode])
})
t.Run("Explicit Permission Mode", func(t *testing.T) {
cfg := &ActionsConfig{
TokenPermissionMode: ActionsTokenPermissionModeRestricted,
}
assert.Equal(t, ActionsTokenPermissionModeRestricted, cfg.TokenPermissionMode)
})
t.Run("Effective Permissions - Permissive Mode", func(t *testing.T) {
cfg := &ActionsConfig{
TokenPermissionMode: ActionsTokenPermissionModePermissive,
}
defaultPerms := cfg.GetDefaultTokenPermissions()
perms := cfg.ClampPermissions(defaultPerms)
assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypeCode])
assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypeIssues])
assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypePackages])
})
t.Run("Effective Permissions - Restricted Mode", func(t *testing.T) {
cfg := &ActionsConfig{
TokenPermissionMode: ActionsTokenPermissionModeRestricted,
}
defaultPerms := cfg.GetDefaultTokenPermissions()
perms := cfg.ClampPermissions(defaultPerms)
assert.Equal(t, perm.AccessModeRead, perms.UnitAccessModes[unit.TypeCode])
assert.Equal(t, perm.AccessModeNone, perms.UnitAccessModes[unit.TypeIssues])
assert.Equal(t, perm.AccessModeRead, perms.UnitAccessModes[unit.TypePackages])
})
t.Run("Clamp Permissions", func(t *testing.T) {
cfg := &ActionsConfig{
MaxTokenPermissions: &ActionsTokenPermissions{
UnitAccessModes: map[unit.Type]perm.AccessMode{
unit.TypeCode: perm.AccessModeRead,
unit.TypeIssues: perm.AccessModeWrite,
unit.TypePullRequests: perm.AccessModeRead,
unit.TypePackages: perm.AccessModeRead,
unit.TypeActions: perm.AccessModeNone,
unit.TypeWiki: perm.AccessModeWrite,
},
},
}
input := ActionsTokenPermissions{
UnitAccessModes: map[unit.Type]perm.AccessMode{
unit.TypeCode: perm.AccessModeWrite, // Should be clamped to Read
unit.TypeIssues: perm.AccessModeWrite, // Should stay Write
unit.TypePullRequests: perm.AccessModeWrite, // Should be clamped to Read
unit.TypePackages: perm.AccessModeWrite, // Should be clamped to Read
unit.TypeActions: perm.AccessModeRead, // Should be clamped to None
unit.TypeWiki: perm.AccessModeRead, // Should stay Read
},
}
clamped := cfg.ClampPermissions(input)
assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypeCode])
assert.Equal(t, perm.AccessModeWrite, clamped.UnitAccessModes[unit.TypeIssues])
assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypePullRequests])
assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypePackages])
assert.Equal(t, perm.AccessModeNone, clamped.UnitAccessModes[unit.TypeActions])
assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypeWiki])
})
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import "gitea.dev/models/db"
// OrderByMap represents all possible search order
var OrderByMap = map[string]map[string]db.SearchOrderBy{
"asc": {
"alpha": "owner_name ASC, name ASC",
"created": db.SearchOrderByOldest,
"updated": db.SearchOrderByLeastUpdated,
"size": "size ASC",
"git_size": "git_size ASC",
"lfs_size": "lfs_size ASC",
"id": db.SearchOrderByID,
"stars": db.SearchOrderByStars,
"forks": db.SearchOrderByForks,
},
"desc": {
"alpha": "owner_name DESC, name DESC",
"created": db.SearchOrderByNewest,
"updated": db.SearchOrderByRecentUpdated,
"size": "size DESC",
"git_size": "git_size DESC",
"lfs_size": "lfs_size DESC",
"id": db.SearchOrderByIDReverse,
"stars": db.SearchOrderByStarsReverse,
"forks": db.SearchOrderByForksReverse,
},
}
// OrderByFlatMap is similar to OrderByMap but use human language keywords
// to decide between asc and desc
var OrderByFlatMap = map[string]db.SearchOrderBy{
"newest": OrderByMap["desc"]["created"],
"oldest": OrderByMap["asc"]["created"],
"recentupdate": OrderByMap["desc"]["updated"],
"leastupdate": OrderByMap["asc"]["updated"],
"reversealphabetically": OrderByMap["desc"]["alpha"],
"alphabetically": OrderByMap["asc"]["alpha"],
"reversesize": OrderByMap["desc"]["size"],
"size": OrderByMap["asc"]["size"],
"reversegitsize": OrderByMap["desc"]["git_size"],
"gitsize": OrderByMap["asc"]["git_size"],
"reverselfssize": OrderByMap["desc"]["lfs_size"],
"lfssize": OrderByMap["asc"]["lfs_size"],
"moststars": OrderByMap["desc"]["stars"],
"feweststars": OrderByMap["asc"]["stars"],
"mostforks": OrderByMap["desc"]["forks"],
"fewestforks": OrderByMap["asc"]["forks"],
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/timeutil"
)
// Star represents a starred repo by a user.
type Star struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"UNIQUE(s)"`
RepoID int64 `xorm:"UNIQUE(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
func init() {
db.RegisterModel(new(Star))
}
// StarRepo or unstar repository.
func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
return db.WithTx(ctx, func(ctx context.Context) error {
staring := IsStaring(ctx, doer.ID, repo.ID)
if star {
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
return user_model.ErrBlockedUser
}
if staring {
return nil
}
if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
return err
}
} else {
if !staring {
return nil
}
if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
return err
}
}
return nil
})
}
// IsStaring checks if user has starred given repository.
func IsStaring(ctx context.Context, userID, repoID int64) bool {
has, _ := db.GetEngine(ctx).Get(&Star{UID: userID, RepoID: repoID})
return has
}
// GetStargazers returns the users that starred the repo.
func GetStargazers(ctx context.Context, repo *Repository, opts db.ListOptions) ([]*user_model.User, error) {
sess := db.GetEngine(ctx).Where("star.repo_id = ?", repo.ID).
Join("LEFT", "star", "`user`.id = star.uid")
if opts.Page > 0 {
db.SetSessionPagination(sess, &opts)
users := make([]*user_model.User, 0, opts.PageSize)
return users, sess.Find(&users)
}
users := make([]*user_model.User, 0, 8)
return users, sess.Find(&users)
}
// ClearRepoStars clears all stars for a repository and from the user that starred it.
// Used when a repository is set to private.
func ClearRepoStars(ctx context.Context, repoID int64) error {
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars=num_stars-1 WHERE id IN (SELECT `uid` FROM `star` WHERE repo_id = ?)", repoID); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = 0 WHERE id = ?", repoID); err != nil {
return err
}
return db.DeleteBeans(ctx, Star{RepoID: repoID})
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"github.com/stretchr/testify/assert"
)
func TestStarRepo(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})
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(t.Context(), user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(t.Context(), user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(t.Context(), user, repo, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
}
func TestIsStaring(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.True(t, repo_model.IsStaring(t.Context(), 2, 4))
assert.False(t, repo_model.IsStaring(t.Context(), 3, 4))
}
func TestRepository_GetStargazers(t *testing.T) {
// repo with stargazers
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
gazers, err := repo_model.GetStargazers(t.Context(), repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
if assert.Len(t, gazers, 1) {
assert.Equal(t, int64(2), gazers[0].ID)
}
}
func TestRepository_GetStargazers2(t *testing.T) {
// repo with stargazers
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
gazers, err := repo_model.GetStargazers(t.Context(), repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
assert.Empty(t, gazers)
}
func TestClearRepoStars(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})
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(t.Context(), user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(t.Context(), user, repo, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.ClearRepoStars(t.Context(), repo.ID))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
gazers, err := repo_model.GetStargazers(t.Context(), repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
assert.Empty(t, gazers)
}
+364
View File
@@ -0,0 +1,364 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"regexp"
"strings"
"gitea.dev/models/db"
"gitea.dev/modules/container"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
func init() {
db.RegisterModel(new(Topic))
db.RegisterModel(new(RepoTopic))
}
var topicPattern = regexp.MustCompile(`^[a-z0-9][-.a-z0-9]*$`)
// Topic represents a topic of repositories
type Topic struct {
ID int64 `xorm:"pk autoincr"`
Name string `xorm:"UNIQUE VARCHAR(50)"`
RepoCount int
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// RepoTopic represents associated repositories and topics
type RepoTopic struct { //revive:disable-line:exported
RepoID int64 `xorm:"pk"`
TopicID int64 `xorm:"pk"`
}
// ErrTopicNotExist represents an error that a topic is not exist
type ErrTopicNotExist struct {
Name string
}
// Error implements error interface
func (err ErrTopicNotExist) Error() string {
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
}
func (err ErrTopicNotExist) Unwrap() error {
return util.ErrNotExist
}
// ValidateTopic checks a topic by length and match pattern rules
func ValidateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
}
// SanitizeAndValidateTopics sanitizes and checks an array or topics
func SanitizeAndValidateTopics(topics []string) (validTopics, invalidTopics []string) {
validTopics = make([]string, 0)
mValidTopics := make(container.Set[string])
invalidTopics = make([]string, 0)
for _, topic := range topics {
topic = strings.TrimSpace(strings.ToLower(topic))
// ignore empty string
if len(topic) == 0 {
continue
}
// ignore same topic twice
if mValidTopics.Contains(topic) {
continue
}
if ValidateTopic(topic) {
validTopics = append(validTopics, topic)
mValidTopics.Add(topic)
} else {
invalidTopics = append(invalidTopics, topic)
}
}
return validTopics, invalidTopics
}
// GetTopicByName retrieves topic by name
func GetTopicByName(ctx context.Context, name string) (*Topic, error) {
var topic Topic
if has, err := db.GetEngine(ctx).Where("name = ?", name).Get(&topic); err != nil {
return nil, err
} else if !has {
return nil, ErrTopicNotExist{name}
}
return &topic, nil
}
// addTopicByNameToRepo adds a topic name to a repo and increments the topic count.
// Returns topic after the addition
func addTopicByNameToRepo(ctx context.Context, repoID int64, topicName string) (*Topic, error) {
var topic Topic
e := db.GetEngine(ctx)
has, err := e.Where("name = ?", topicName).Get(&topic)
if err != nil {
return nil, err
}
if !has {
topic.Name = topicName
topic.RepoCount = 1
if err := db.Insert(ctx, &topic); err != nil {
return nil, err
}
} else {
topic.RepoCount++
if _, err := e.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
return nil, err
}
}
if err := db.Insert(ctx, &RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return nil, err
}
return &topic, nil
}
// removeTopicFromRepo remove a topic from a repo and decrements the topic repo count
func removeTopicFromRepo(ctx context.Context, repoID int64, topic *Topic) error {
topic.RepoCount--
e := db.GetEngine(ctx)
if _, err := e.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
return err
}
if _, err := e.Delete(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return err
}
return nil
}
// RemoveTopicsFromRepo remove all topics from the repo and decrements respective topics repo count
func RemoveTopicsFromRepo(ctx context.Context, repoID int64) error {
e := db.GetEngine(ctx)
_, err := e.Where(
builder.In("id",
builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}),
),
).Decr("repo_count").Update(&Topic{})
if err != nil {
return err
}
if _, err = e.Delete(&RepoTopic{RepoID: repoID}); err != nil {
return err
}
return nil
}
// FindTopicOptions represents the options when fdin topics
type FindTopicOptions struct {
db.ListOptions
RepoID int64
Keyword string
}
func (opts *FindTopicOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
}
if opts.Keyword != "" {
cond = cond.And(builder.Like{"topic.name", opts.Keyword})
}
return cond
}
func (opts *FindTopicOptions) ToOrders() string {
orderBy := "topic.repo_count DESC"
if opts.RepoID > 0 {
orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
}
return orderBy
}
func (opts *FindTopicOptions) ToJoins() []db.JoinFunc {
if opts.RepoID <= 0 {
return nil
}
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
return nil
},
}
}
// GetRepoTopicByName retrieves topic from name for a repo if it exist
func GetRepoTopicByName(ctx context.Context, repoID int64, topicName string) (*Topic, error) {
cond := builder.NewCond()
var topic Topic
cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.name": topicName})
sess := db.GetEngine(ctx).Table("topic").Where(cond)
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
has, err := sess.Select("topic.*").Get(&topic)
if has {
return &topic, err
}
return nil, err
}
// AddTopic adds a topic name to a repository (if it does not already have it)
func AddTopic(ctx context.Context, repoID int64, topicName string) (*Topic, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*Topic, error) {
topic, err := GetRepoTopicByName(ctx, repoID, topicName)
if err != nil {
return nil, err
}
if topic != nil {
// Repo already have topic
return topic, nil
}
topic, err = addTopicByNameToRepo(ctx, repoID, topicName)
if err != nil {
return nil, err
}
if err = syncTopicsInRepository(ctx, repoID); err != nil {
return nil, err
}
return topic, nil
})
}
// DeleteTopic removes a topic name from a repository (if it has it)
func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, error) {
topic, err := GetRepoTopicByName(ctx, repoID, topicName)
if err != nil {
return nil, err
}
if topic == nil {
// Repo doesn't have topic, can't be removed
return nil, nil //nolint:nilnil // return nil to indicate that the topic does not exist
}
return db.WithTx2(ctx, func(ctx context.Context) (*Topic, error) {
if err = removeTopicFromRepo(ctx, repoID, topic); err != nil {
return nil, err
}
if err = syncTopicsInRepository(ctx, repoID); err != nil {
return nil, err
}
return topic, nil
})
}
// SaveTopics save topics to a repository
func SaveTopics(ctx context.Context, repoID int64, topicNames ...string) error {
topics, err := db.Find[Topic](ctx, &FindTopicOptions{
RepoID: repoID,
})
if err != nil {
return err
}
return db.WithTx(ctx, func(ctx context.Context) error {
var addedTopicNames []string
for _, topicName := range topicNames {
if strings.TrimSpace(topicName) == "" {
continue
}
var found bool
for _, t := range topics {
if strings.EqualFold(topicName, t.Name) {
found = true
break
}
}
if !found {
addedTopicNames = append(addedTopicNames, topicName)
}
}
var removeTopics []*Topic
for _, t := range topics {
var found bool
for _, topicName := range topicNames {
if strings.EqualFold(topicName, t.Name) {
found = true
break
}
}
if !found {
removeTopics = append(removeTopics, t)
}
}
for _, topicName := range addedTopicNames {
_, err := addTopicByNameToRepo(ctx, repoID, topicName)
if err != nil {
return err
}
}
for _, topic := range removeTopics {
err := removeTopicFromRepo(ctx, repoID, topic)
if err != nil {
return err
}
}
return syncTopicsInRepository(ctx, repoID)
})
}
// GenerateTopics generates topics from a template repository
func GenerateTopics(ctx context.Context, templateRepo, generateRepo *Repository) error {
for _, topic := range templateRepo.Topics {
if _, err := addTopicByNameToRepo(ctx, generateRepo.ID, topic); err != nil {
return err
}
}
return syncTopicsInRepository(ctx, generateRepo.ID)
}
// syncTopicsInRepository makes sure topics in the topics table are copied into the topics field of the repository
func syncTopicsInRepository(ctx context.Context, repoID int64) error {
topicNames := make([]string, 0, 25)
if err := db.GetEngine(ctx).Table("topic").Cols("name").
Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
Where("repo_topic.repo_id = ?", repoID).Asc("topic.name").Find(&topicNames); err != nil {
return err
}
if _, err := db.GetEngine(ctx).ID(repoID).Cols("topics").Update(&Repository{
Topics: topicNames,
}); err != nil {
return err
}
return nil
}
// CountOrphanedAttachments returns the number of topics that don't belong to any repository.
func CountOrphanedTopics(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where("repo_count = 0").Count(new(Topic))
}
// DeleteOrphanedAttachments delete all topics that don't belong to any repository.
func DeleteOrphanedTopics(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where("repo_count = 0").Delete(new(Topic))
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestAddTopic(t *testing.T) {
totalNrOfTopics := 6
repo1NrOfTopics := 3
assert.NoError(t, unittest.PrepareTestDatabase())
topics, err := db.Find[repo_model.Topic](t.Context(), &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, total, err := db.FindAndCount[repo_model.Topic](t.Context(), &repo_model.FindTopicOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 2},
})
assert.NoError(t, err)
assert.Len(t, topics, 2)
assert.EqualValues(t, 6, total)
topics, err = db.Find[repo_model.Topic](t.Context(), &repo_model.FindTopicOptions{
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, topics, repo1NrOfTopics)
assert.NoError(t, repo_model.SaveTopics(t.Context(), 2, "golang"))
repo2NrOfTopics := 1
topics, err = db.Find[repo_model.Topic](t.Context(), &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, err = db.Find[repo_model.Topic](t.Context(), &repo_model.FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
assert.Len(t, topics, repo2NrOfTopics)
assert.NoError(t, repo_model.SaveTopics(t.Context(), 2, "golang", "gitea"))
repo2NrOfTopics = 2
totalNrOfTopics++
topic, err := repo_model.GetTopicByName(t.Context(), "gitea")
assert.NoError(t, err)
assert.Equal(t, 1, topic.RepoCount)
topics, err = db.Find[repo_model.Topic](t.Context(), &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, err = db.Find[repo_model.Topic](t.Context(), &repo_model.FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
assert.Len(t, topics, repo2NrOfTopics)
}
func TestTopicValidator(t *testing.T) {
assert.True(t, repo_model.ValidateTopic("12345"))
assert.True(t, repo_model.ValidateTopic("2-test"))
assert.True(t, repo_model.ValidateTopic("foo.bar"))
assert.True(t, repo_model.ValidateTopic("test-3"))
assert.True(t, repo_model.ValidateTopic("first"))
assert.True(t, repo_model.ValidateTopic("second-test-topic"))
assert.True(t, repo_model.ValidateTopic("third-project-topic-with-max-length"))
assert.False(t, repo_model.ValidateTopic("$fourth-test,topic"))
assert.False(t, repo_model.ValidateTopic("-fifth-test-topic"))
assert.False(t, repo_model.ValidateTopic("sixth-go-project-topic-with-excess-length"))
assert.False(t, repo_model.ValidateTopic(".foo"))
}
+281
View File
@@ -0,0 +1,281 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"errors"
"fmt"
"gitea.dev/models/db"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrNoPendingRepoTransfer is an error type for repositories without a pending
// transfer request
type ErrNoPendingRepoTransfer struct {
RepoID int64
}
func (err ErrNoPendingRepoTransfer) Error() string {
return fmt.Sprintf("repository doesn't have a pending transfer [repo_id: %d]", err.RepoID)
}
// IsErrNoPendingTransfer is an error type when a repository has no pending
// transfers
func IsErrNoPendingTransfer(err error) bool {
_, ok := err.(ErrNoPendingRepoTransfer)
return ok
}
func (err ErrNoPendingRepoTransfer) Unwrap() error {
return util.ErrNotExist
}
// ErrRepoTransferInProgress represents the state of a repository that has an
// ongoing transfer
type ErrRepoTransferInProgress struct {
Uname string
Name string
}
// IsErrRepoTransferInProgress checks if an error is a ErrRepoTransferInProgress.
func IsErrRepoTransferInProgress(err error) bool {
_, ok := err.(ErrRepoTransferInProgress)
return ok
}
func (err ErrRepoTransferInProgress) Error() string {
return fmt.Sprintf("repository is already being transferred [uname: %s, name: %s]", err.Uname, err.Name)
}
func (err ErrRepoTransferInProgress) Unwrap() error {
return util.ErrAlreadyExist
}
// RepoTransfer is used to manage repository transfers
type RepoTransfer struct { //nolint:revive // export stutter
ID int64 `xorm:"pk autoincr"`
DoerID int64
Doer *user_model.User `xorm:"-"`
RecipientID int64
Recipient *user_model.User `xorm:"-"`
RepoID int64
Repo *Repository `xorm:"-"`
TeamIDs []int64
Teams []*organization.Team `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL updated"`
}
func init() {
db.RegisterModel(new(RepoTransfer))
}
func (r *RepoTransfer) LoadRecipient(ctx context.Context) error {
if r.Recipient == nil {
u, err := user_model.GetUserByID(ctx, r.RecipientID)
if err != nil {
return err
}
r.Recipient = u
}
return nil
}
func (r *RepoTransfer) LoadRepo(ctx context.Context) error {
if r.Repo == nil {
repo, err := GetRepositoryByID(ctx, r.RepoID)
if err != nil {
return err
}
r.Repo = repo
}
return nil
}
// LoadAttributes fetches the transfer recipient from the database
func (r *RepoTransfer) LoadAttributes(ctx context.Context) error {
if err := r.LoadRecipient(ctx); err != nil {
return err
}
if r.Recipient.IsOrganization() && r.Teams == nil {
teamsMap, err := organization.GetTeamsByIDs(ctx, r.TeamIDs)
if err != nil {
return err
}
for _, team := range teamsMap {
r.Teams = append(r.Teams, team)
}
}
if err := r.LoadRepo(ctx); err != nil {
return err
}
if r.Doer == nil {
u, err := user_model.GetUserByID(ctx, r.DoerID)
if err != nil {
return err
}
r.Doer = u
}
return nil
}
// CanUserAcceptOrRejectTransfer checks if the user has the rights to accept/decline a repo transfer.
// For user, it checks if it's himself
// For organizations, it checks if the user is able to create repos
func (r *RepoTransfer) CanUserAcceptOrRejectTransfer(ctx context.Context, u *user_model.User) bool {
if err := r.LoadAttributes(ctx); err != nil {
log.Error("LoadAttributes: %v", err)
return false
}
if !r.Recipient.IsOrganization() {
return r.RecipientID == u.ID
}
allowed, err := organization.CanCreateOrgRepo(ctx, r.RecipientID, u.ID)
if err != nil {
log.Error("CanCreateOrgRepo: %v", err)
return false
}
return allowed
}
type PendingRepositoryTransferOptions struct {
RepoID int64
SenderID int64
RecipientID int64
}
func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if opts.SenderID != 0 {
cond = cond.And(builder.Eq{"doer_id": opts.SenderID})
}
if opts.RecipientID != 0 {
cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID})
}
return cond
}
func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) {
transfers := make([]*RepoTransfer, 0, 10)
return transfers, db.GetEngine(ctx).
Where(opts.ToConds()).
Find(&transfers)
}
func IsRepositoryTransferExist(ctx context.Context, repoID int64) (bool, error) {
return db.GetEngine(ctx).Where("repo_id = ?", repoID).Exist(new(RepoTransfer))
}
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
// process for the repository
func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) {
transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
if err != nil {
return nil, err
}
if len(transfers) != 1 {
return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
}
return transfers[0], nil
}
func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(&RepoTransfer{})
return err
}
// TestRepositoryReadyForTransfer make sure repo is ready to transfer
func TestRepositoryReadyForTransfer(status RepositoryStatus) error {
switch status {
case RepositoryBeingMigrated:
return errors.New("repo is not ready, currently migrating")
case RepositoryPendingTransfer:
return ErrRepoTransferInProgress{}
}
return nil
}
// CreatePendingRepositoryTransfer transfer a repo from one owner to a new one.
// it marks the repository transfer as "pending"
func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team) error {
return db.WithTx(ctx, func(ctx context.Context) error {
repo, err := GetRepositoryByID(ctx, repoID)
if err != nil {
return err
}
if _, err := user_model.GetUserByID(ctx, newOwner.ID); err != nil {
return err
}
// Make sure repo is ready to transfer
if err := TestRepositoryReadyForTransfer(repo.Status); err != nil {
return err
}
exist, err := IsRepositoryTransferExist(ctx, repo.ID)
if err != nil {
return err
}
if exist {
return ErrRepoTransferInProgress{
Uname: repo.Owner.LowerName,
Name: repo.Name,
}
}
repo.Status = RepositoryPendingTransfer
if err := UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil {
return err
}
// Check if new owner has repository with same name.
if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has {
return ErrRepoAlreadyExist{
Uname: newOwner.LowerName,
Name: repo.Name,
}
}
transfer := &RepoTransfer{
RepoID: repo.ID,
RecipientID: newOwner.ID,
CreatedUnix: timeutil.TimeStampNow(),
UpdatedUnix: timeutil.TimeStampNow(),
DoerID: doer.ID,
TeamIDs: make([]int64, 0, len(teams)),
}
for k := range teams {
transfer.TeamIDs = append(transfer.TeamIDs, teams[k].ID)
}
return db.Insert(ctx, transfer)
})
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"time"
"gitea.dev/models/db"
"gitea.dev/modules/util"
)
// UpdateRepositoryOwnerNames updates repository owner_names (this should only be used when the ownerName has changed case)
func UpdateRepositoryOwnerNames(ctx context.Context, ownerID int64, ownerName string) error {
if ownerID == 0 {
return nil
}
if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").NoAutoTime().Update(&Repository{
OwnerName: ownerName,
}); err != nil {
return err
}
return nil
}
// UpdateRepositoryUpdatedTime updates a repository's updated time
func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime time.Time) error {
_, err := db.GetEngine(ctx).Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", updateTime.Unix(), repoID)
return err
}
// UpdateRepositoryColsWithAutoTime updates repository's columns and the timestamp fields automatically
func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, colName string, moreColNames ...string) error {
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(append([]string{colName}, moreColNames...)...).Update(repo)
return err
}
// UpdateRepositoryColsNoAutoTime updates repository's columns, doesn't change timestamp field automatically
func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, colName string, moreColNames ...string) error {
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(append([]string{colName}, moreColNames...)...).NoAutoTime().Update(repo)
return err
}
// ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error.
type ErrReachLimitOfRepo struct {
Limit int
}
// IsErrReachLimitOfRepo checks if an error is a ErrReachLimitOfRepo.
func IsErrReachLimitOfRepo(err error) bool {
_, ok := err.(ErrReachLimitOfRepo)
return ok
}
func (err ErrReachLimitOfRepo) Error() string {
return fmt.Sprintf("user has reached maximum limit of repositories [limit: %d]", err.Limit)
}
func (err ErrReachLimitOfRepo) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrRepoAlreadyExist represents a "RepoAlreadyExist" kind of error.
type ErrRepoAlreadyExist struct {
Uname string
Name string
}
// IsErrRepoAlreadyExist checks if an error is a ErrRepoAlreadyExist.
func IsErrRepoAlreadyExist(err error) bool {
_, ok := err.(ErrRepoAlreadyExist)
return ok
}
func (err ErrRepoAlreadyExist) Error() string {
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
}
func (err ErrRepoAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrRepoFilesAlreadyExist represents a "RepoFilesAlreadyExist" kind of error.
type ErrRepoFilesAlreadyExist struct {
Uname string
Name string
}
// IsErrRepoFilesAlreadyExist checks if an error is a ErrRepoAlreadyExist.
func IsErrRepoFilesAlreadyExist(err error) bool {
_, ok := err.(ErrRepoFilesAlreadyExist)
return ok
}
func (err ErrRepoFilesAlreadyExist) Error() string {
return fmt.Sprintf("repository files already exist [uname: %s, name: %s]", err.Uname, err.Name)
}
func (err ErrRepoFilesAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// UpdateRepoSize updates the repository size, calculating it using getDirectorySize
func UpdateRepoSize(ctx context.Context, repoID, gitSize, lfsSize int64) error {
_, err := db.GetEngine(ctx).ID(repoID).Cols("size", "git_size", "lfs_size").NoAutoTime().Update(&Repository{
Size: gitSize + lfsSize,
GitSize: gitSize,
LFSSize: lfsSize,
})
return err
}
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
gouuid "github.com/google/uuid"
)
// ErrUploadNotExist represents a "UploadNotExist" kind of error.
type ErrUploadNotExist struct {
ID int64
UUID string
}
// IsErrUploadNotExist checks if an error is a ErrUploadNotExist.
func IsErrUploadNotExist(err error) bool {
_, ok := err.(ErrUploadNotExist)
return ok
}
func (err ErrUploadNotExist) Error() string {
return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
}
func (err ErrUploadNotExist) Unwrap() error {
return util.ErrNotExist
}
// Upload represent a uploaded file to a repo to be deleted when moved
type Upload struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
Name string
}
func init() {
db.RegisterModel(new(Upload))
}
// LocalPath returns where uploads are temporarily stored in local file system based on given UUID.
func (upload *Upload) LocalPath() string {
uuid := upload.UUID
return setting.AppDataTempDir("repo-uploads").JoinPath(uuid[0:1], uuid[1:2], uuid)
}
// NewUpload creates a new upload object.
func NewUpload(ctx context.Context, name string, buf []byte, file multipart.File) (_ *Upload, err error) {
upload := &Upload{
UUID: gouuid.New().String(),
Name: name,
}
localPath := upload.LocalPath()
if err = os.MkdirAll(filepath.Dir(localPath), os.ModePerm); err != nil {
return nil, fmt.Errorf("MkdirAll: %w", err)
}
fw, err := os.Create(localPath)
if err != nil {
return nil, fmt.Errorf("Create: %w", err)
}
defer fw.Close()
if _, err = fw.Write(buf); err != nil {
return nil, fmt.Errorf("Write: %w", err)
} else if _, err = io.Copy(fw, file); err != nil {
return nil, fmt.Errorf("Copy: %w", err)
}
if _, err := db.GetEngine(ctx).Insert(upload); err != nil {
return nil, err
}
return upload, nil
}
// GetUploadByUUID returns the Upload by UUID
func GetUploadByUUID(ctx context.Context, uuid string) (*Upload, error) {
upload := &Upload{}
has, err := db.GetEngine(ctx).Where("uuid=?", uuid).Get(upload)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUploadNotExist{0, uuid}
}
return upload, nil
}
// GetUploadsByUUIDs returns multiple uploads by UUIDS
func GetUploadsByUUIDs(ctx context.Context, uuids []string) ([]*Upload, error) {
if len(uuids) == 0 {
return []*Upload{}, nil
}
// Silently drop invalid uuids.
uploads := make([]*Upload, 0, len(uuids))
return uploads, db.GetEngine(ctx).In("uuid", uuids).Find(&uploads)
}
// DeleteUploads deletes multiple uploads
func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) {
if len(uploads) == 0 {
return nil
}
ids := make([]int64, len(uploads))
for i := range uploads {
ids[i] = uploads[i].ID
}
if err = db.DeleteByIDs[Upload](ctx, ids...); err != nil {
return fmt.Errorf("delete uploads: %w", err)
}
for _, upload := range uploads {
localPath := upload.LocalPath()
if err := util.Remove(localPath); err != nil {
// just continue, don't fail the whole operation if a file is missing (removed by others)
log.Error("unable to remove upload file %s: %v", localPath, err)
}
}
return nil
}
// DeleteUploadByUUID deletes a upload by UUID
func DeleteUploadByUUID(ctx context.Context, uuid string) error {
upload, err := GetUploadByUUID(ctx, uuid)
if err != nil {
if IsErrUploadNotExist(err) {
return nil
}
return fmt.Errorf("GetUploadByUUID: %w", err)
}
if err := DeleteUploads(ctx, upload); err != nil {
return fmt.Errorf("DeleteUpload: %w", err)
}
return nil
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"xorm.io/builder"
)
type StarredReposOptions struct {
db.ListOptions
StarrerID int64
RepoOwnerID int64
IncludePrivate bool
}
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
func (opts *StarredReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"star.uid": opts.StarrerID,
}
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{
"repository.owner_id": opts.RepoOwnerID,
})
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{
"repository.is_private": false,
})
}
return cond
}
func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
return nil
},
}
}
// GetStarredRepos returns the repos starred by a particular user
func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
return db.Find[Repository](ctx, opts)
}
type WatchedReposOptions struct {
db.ListOptions
WatcherID int64
RepoOwnerID int64
IncludePrivate bool
}
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
func (opts *WatchedReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"watch.user_id": opts.WatcherID,
}
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{
"repository.owner_id": opts.RepoOwnerID,
})
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{
"repository.is_private": false,
})
}
return cond.And(builder.Neq{
"watch.mode": WatchModeDont,
})
}
func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
return nil
},
}
}
// GetWatchedRepos returns the repos watched by a particular user
func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
return db.FindAndCount[Repository](ctx, opts)
}
// GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests of the repository,
func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) {
if err = repo.LoadOwner(ctx); err != nil {
return nil, err
}
e := db.GetEngine(ctx)
userIDs := make([]int64, 0, 10)
if err = e.Table("access").
Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite).
Select("user_id").
Find(&userIDs); err != nil {
return nil, err
}
uniqueUserIDs := make(container.Set[int64])
uniqueUserIDs.AddMultiple(userIDs...)
if repo.Owner.IsOrganization() {
// issues and pull requests both need "assignee list"
additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests)
if err != nil {
return nil, err
}
uniqueUserIDs.AddMultiple(additionalUserIDs...)
}
// Leave a seat for owner itself to append later, but if owner is an organization
// and just waste 1 unit is cheaper than re-allocate memory once.
users := make([]*user_model.User, 0, len(uniqueUserIDs)+1)
if len(uniqueUserIDs) > 0 {
if err = e.In("id", uniqueUserIDs.Values()).
Where(builder.Eq{"`user`.is_active": true}).
OrderBy(user_model.GetOrderByName()).
Find(&users); err != nil {
return nil, err
}
}
if !repo.Owner.IsOrganization() && !uniqueUserIDs.Contains(repo.OwnerID) {
users = append(users, repo.Owner)
}
return users, nil
}
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
// It searches with the "user.name" and "user.full_name" fields case-insensitively.
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string) ([]*user_model.User, error) {
users := make([]*user_model.User, 0, 30)
cond := builder.In("`user`.id",
builder.Select("poster_id").From("issue").Where(
builder.Eq{"repo_id": repo.ID}.
And(builder.Eq{"is_pull": isPull}),
).GroupBy("poster_id"))
if search != "" {
var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
cond = cond.And(prefixCond)
}
return users, db.GetEngine(ctx).
Where(cond).
Cols("id", "name", "full_name", "avatar", "avatar_email", "use_custom_avatar").
OrderBy("name").
Limit(30).
Find(&users)
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/organization"
perm_model "gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserRepo(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("GetIssuePostersWithSearch", testUserRepoGetIssuePostersWithSearch)
t.Run("Assignees", testUserRepoAssignees)
t.Run("AssigneesNoTeamUnit", testRepoAssigneesNoTeamUnit)
}
func testUserRepoAssignees(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
users, err := repo_model.GetRepoAssignees(t.Context(), repo2)
assert.NoError(t, err)
assert.Len(t, users, 1)
assert.Equal(t, int64(2), users[0].ID)
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
users, err = repo_model.GetRepoAssignees(t.Context(), repo21)
assert.NoError(t, err)
if assert.Len(t, users, 4) {
assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
}
// do not return deactivated users
assert.NoError(t, user_model.UpdateUserCols(t.Context(), &user_model.User{ID: 15, IsActive: false}, "is_active"))
users, err = repo_model.GetRepoAssignees(t.Context(), repo21)
assert.NoError(t, err)
if assert.Len(t, users, 3) {
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
}
}
func testRepoAssigneesNoTeamUnit(t *testing.T) {
ctx := t.Context()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
require.NoError(t, repo.LoadOwner(ctx))
require.True(t, repo.Owner.IsOrganization())
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}, &access_model.Access{}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
team := &organization.Team{OrgID: repo.OwnerID, LowerName: "admin-team", AccessMode: perm_model.AccessModeAdmin}
require.NoError(t, db.Insert(ctx, team))
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: repo.OwnerID, TeamID: team.ID, UID: user.ID}))
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: repo.OwnerID, TeamID: team.ID, RepoID: repo.ID}))
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: repo.OwnerID, TeamID: team.ID, Type: unit.TypePullRequests, AccessMode: perm_model.AccessModeNone}))
users, err := repo_model.GetRepoAssignees(ctx, repo)
require.NoError(t, err)
require.Len(t, users, 1)
assert.ElementsMatch(t, []int64{4}, []int64{users[0].ID})
}
func testUserRepoGetIssuePostersWithSearch(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER")
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, "user2", users[0].Name)
users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O")
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, "user2", users[0].Name)
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
)
// WatchMode specifies what kind of watch the user has on a repository
type WatchMode int8
const (
// WatchModeNone don't watch
WatchModeNone WatchMode = iota // 0
// WatchModeNormal watch repository (from other sources)
WatchModeNormal // 1
// WatchModeDont explicit don't auto-watch
WatchModeDont // 2
// WatchModeAuto watch repository (from AutoWatchOnChanges)
WatchModeAuto // 3
)
// Watch is connection request for receiving repository notification.
type Watch struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch)"`
RepoID int64 `xorm:"UNIQUE(watch)"`
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
func init() {
db.RegisterModel(new(Watch))
}
// GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found
func GetWatch(ctx context.Context, userID, repoID int64) (Watch, error) {
watch := Watch{UserID: userID, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(&watch)
if err != nil {
return watch, err
}
if !has {
watch.Mode = WatchModeNone
}
return watch, nil
}
// IsWatchMode Decodes watchability of WatchMode
func IsWatchMode(mode WatchMode) bool {
return mode != WatchModeNone && mode != WatchModeDont
}
// IsWatching checks if user has watched given repository.
func IsWatching(ctx context.Context, userID, repoID int64) bool {
watch, err := GetWatch(ctx, userID, repoID)
return err == nil && IsWatchMode(watch.Mode)
}
func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) {
if watch.Mode == mode {
return nil
}
if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchMode(watch.Mode)) {
// Don't auto watch if already watching or deliberately not watching
return nil
}
hadrec := watch.Mode != WatchModeNone
needsrec := mode != WatchModeNone
repodiff := 0
if IsWatchMode(mode) && !IsWatchMode(watch.Mode) {
repodiff = 1
} else if !IsWatchMode(mode) && IsWatchMode(watch.Mode) {
repodiff = -1
}
watch.Mode = mode
if !hadrec && needsrec {
watch.Mode = mode
if err = db.Insert(ctx, watch); err != nil {
return err
}
} else if needsrec {
watch.Mode = mode
if _, err := db.GetEngine(ctx).ID(watch.ID).AllCols().Update(watch); err != nil {
return err
}
} else if _, err = db.DeleteByID[Watch](ctx, watch.ID); err != nil {
return err
}
if repodiff != 0 {
_, err = db.GetEngine(ctx).Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repodiff, watch.RepoID)
}
return err
}
// WatchRepo watch or unwatch repository.
func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
watch, err := GetWatch(ctx, doer.ID, repo.ID)
if err != nil {
return err
}
if !doWatch && watch.Mode == WatchModeAuto {
return watchRepoMode(ctx, watch, WatchModeDont)
} else if !doWatch {
return watchRepoMode(ctx, watch, WatchModeNone)
}
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
return user_model.ErrBlockedUser
}
return watchRepoMode(ctx, watch, WatchModeNormal)
}
// GetWatchers returns all watchers of given repository.
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
watches := make([]*Watch, 0, 10)
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
And("`watch`.mode<>?", WatchModeDont).
And("`user`.is_active=?", true).
And("`user`.prohibit_login=?", false).
Join("INNER", "`user`", "`user`.id = `watch`.user_id").
Find(&watches)
}
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetRepoWatchersIDs(ctx context.Context, repoID int64) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, db.GetEngine(ctx).Table("watch").
Where("watch.repo_id=?", repoID).
And("watch.mode<>?", WatchModeDont).
Select("user_id").
Find(&ids)
}
// GetRepoWatchers returns range of users watching given repository.
func GetRepoWatchers(ctx context.Context, repoID int64, opts db.ListOptions) ([]*user_model.User, error) {
sess := db.GetEngine(ctx).Where("watch.repo_id=?", repoID).
Join("LEFT", "watch", "`user`.id=`watch`.user_id").
And("`watch`.mode<>?", WatchModeDont)
if opts.Page > 0 {
db.SetSessionPagination(sess, &opts)
users := make([]*user_model.User, 0, opts.PageSize)
return users, sess.Find(&users)
}
users := make([]*user_model.User, 0, 8)
return users, sess.Find(&users)
}
// WatchIfAuto subscribes to repo if AutoWatchOnChanges is set
func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error {
if !isWrite || !setting.Service.AutoWatchOnChanges {
return nil
}
watch, err := GetWatch(ctx, userID, repoID)
if err != nil {
return err
}
if watch.Mode != WatchModeNone {
return nil
}
return watchRepoMode(ctx, watch, WatchModeAuto)
}
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
// Used when a repository is set to private.
func ClearRepoWatches(ctx context.Context, repoID int64) error {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
return err
}
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
}
+140
View File
@@ -0,0 +1,140 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsWatching(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.True(t, repo_model.IsWatching(t.Context(), 1, 1))
assert.True(t, repo_model.IsWatching(t.Context(), 4, 1))
assert.True(t, repo_model.IsWatching(t.Context(), 11, 1))
assert.False(t, repo_model.IsWatching(t.Context(), 1, 5))
assert.False(t, repo_model.IsWatching(t.Context(), 8, 1))
assert.False(t, repo_model.IsWatching(t.Context(), unittest.NonexistentID, unittest.NonexistentID))
}
func TestGetWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
assert.NoError(t, err)
// One watchers are inactive, thus minus 1
assert.Len(t, watches, repo.NumWatches-1)
for _, watch := range watches {
assert.Equal(t, repo.ID, watch.RepoID)
}
watches, err = repo_model.GetWatchers(t.Context(), unittest.NonexistentID)
assert.NoError(t, err)
assert.Empty(t, watches)
}
func TestRepository_GetWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
watchers, err := repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, repo.NumWatches)
for _, watcher := range watchers {
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: watcher.ID, RepoID: repo.ID})
}
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 9})
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Empty(t, watchers)
}
func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
watchers, err := repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, repo.NumWatches)
setting.Service.AutoWatchOnChanges = false
prevCount := repo.NumWatches
// Must not add watch
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 8, 1, true))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Should not add watch
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 10, 1, true))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
setting.Service.AutoWatchOnChanges = true
// Must not add watch
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 8, 1, true))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Should not add watch
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, false))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Should add watch
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, true))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount+1)
// Should remove watch, inhibit from adding auto
assert.NoError(t, repo_model.WatchRepo(t.Context(), user12, repo, false))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Must not add watch
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, true))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
}
func TestClearRepoWatches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const repoID int64 = 1
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
require.NoError(t, err)
require.NotEmpty(t, watchers)
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
assert.NoError(t, err)
assert.Empty(t, watchers)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
assert.Zero(t, repo.NumWatches)
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"strings"
user_model "gitea.dev/models/user"
"gitea.dev/modules/util"
)
// ErrWikiAlreadyExist represents a "WikiAlreadyExist" kind of error.
type ErrWikiAlreadyExist struct {
Title string
}
// IsErrWikiAlreadyExist checks if an error is an ErrWikiAlreadyExist.
func IsErrWikiAlreadyExist(err error) bool {
_, ok := err.(ErrWikiAlreadyExist)
return ok
}
func (err ErrWikiAlreadyExist) Error() string {
return fmt.Sprintf("wiki page already exists [title: %s]", err.Title)
}
func (err ErrWikiAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrWikiReservedName represents a reserved name error.
type ErrWikiReservedName struct {
Title string
}
// IsErrWikiReservedName checks if an error is an ErrWikiReservedName.
func IsErrWikiReservedName(err error) bool {
_, ok := err.(ErrWikiReservedName)
return ok
}
func (err ErrWikiReservedName) Error() string {
return "wiki title is reserved: " + err.Title
}
func (err ErrWikiReservedName) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrWikiInvalidFileName represents an invalid wiki file name.
type ErrWikiInvalidFileName struct {
FileName string
}
// IsErrWikiInvalidFileName checks if an error is an ErrWikiInvalidFileName.
func IsErrWikiInvalidFileName(err error) bool {
_, ok := err.(ErrWikiInvalidFileName)
return ok
}
func (err ErrWikiInvalidFileName) Error() string {
return "Invalid wiki filename: " + err.FileName
}
func (err ErrWikiInvalidFileName) Unwrap() error {
return util.ErrInvalidArgument
}
// WikiCloneLink returns clone URLs of repository wiki.
func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User) *CloneLink {
return repo.cloneLink(ctx, doer, repo.Name+".wiki")
}
func RelativeWikiPath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git"
}
// WikiStorageRepo returns the storage repo for the wiki
// The wiki repository should have the same object format as the code repository
func (repo *Repository) WikiStorageRepo() StorageRepo {
return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name))
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestRepository_WikiCloneLink(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
cloneLink := repo.WikiCloneLink(t.Context(), nil)
assert.Equal(t, "ssh://sshuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH)
assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS)
}
func TestRepository_RelativeWikiPath(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.Equal(t, "user2/repo1.wiki.git", repo_model.RelativeWikiPath(repo.OwnerName, repo.Name))
assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().RelativePath())
}