feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -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())
}
+42 -8
View File
@@ -11,7 +11,6 @@ import (
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -27,11 +26,46 @@ const (
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 git.ArchiveType `xorm:"unique(s)"`
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"`
@@ -56,15 +90,15 @@ func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) {
if err != nil {
return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid repo id")
}
commitID, archiveType := git.SplitArchiveNameType(parts[2])
if archiveType == git.ArchiveUnknown {
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 git.ArchiveType, commitID string) (*RepoArchiver, error) {
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 {
@@ -73,7 +107,7 @@ func GetRepoArchiver(ctx context.Context, repoID int64, tp git.ArchiveType, comm
if has {
return &archiver, nil
}
return nil, 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
+3 -9
View File
@@ -41,20 +41,14 @@ func generateRandomAvatar(ctx context.Context, repo *Repository) error {
idToString := strconv.FormatInt(repo.ID, 10)
seed := idToString
img, err := avatar.RandomImage([]byte(seed))
if err != nil {
return fmt.Errorf("RandomImage: %w", err)
}
img := avatar.RandomImageDefaultSize([]byte(seed))
repo.Avatar = idToString
if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error {
if err := png.Encode(w, img); err != nil {
log.Error("Encode: %v", err)
}
return err
return png.Encode(w, img)
}); err != nil {
return fmt.Errorf("Failed to create dir %s: %w", repo.CustomAvatarRelativePath(), err)
return fmt.Errorf("failed to create dir %s: %w", repo.CustomAvatarRelativePath(), err)
}
log.Info("New random avatar created for repository: %d", repo.ID)
+1 -1
View File
@@ -49,7 +49,7 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
return nil, err
}
if !has {
return nil, nil
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
}
return &forkedRepo, nil
}
@@ -0,0 +1,16 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/util"
)
func (repo *Repository) GetPullRequestTargetBranch(ctx context.Context) string {
unitPRConfig := repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
return util.IfZero(unitPRConfig.DefaultTargetBranch, repo.DefaultBranch)
}
@@ -0,0 +1,32 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
"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))
}
+22 -60
View File
@@ -229,10 +229,6 @@ func RelativePath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git"
}
func RelativeWikiPath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git"
}
// RelativePath should be an unix style path like username/reponame.git
func (repo *Repository) RelativePath() string {
return RelativePath(repo.OwnerName, repo.Name)
@@ -245,12 +241,6 @@ func (sr StorageRepo) RelativePath() string {
return string(sr)
}
// 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))
}
// SanitizedOriginalURL returns a sanitized OriginalURL
func (repo *Repository) SanitizedOriginalURL() string {
if repo.OriginalURL == "" {
@@ -291,7 +281,7 @@ func (repo *Repository) SizeDetailsString() string {
var str strings.Builder
sizeDetails := repo.SizeDetails()
for _, detail := range sizeDetails {
str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size)))
fmt.Fprintf(&str, "%s: %s, ", detail.Name, base.FileSize(detail.Size))
}
return strings.TrimSuffix(str.String(), ", ")
}
@@ -432,52 +422,37 @@ func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool {
return false
}
// MustGetUnit always returns a RepoUnit object
// 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:
return &RepoUnit{
Type: tp,
Config: new(ExternalWikiConfig),
}
ru.Config = new(ExternalWikiConfig)
case unit.TypeExternalTracker:
return &RepoUnit{
Type: tp,
Config: new(ExternalTrackerConfig),
}
ru.Config = new(ExternalTrackerConfig)
case unit.TypePullRequests:
return &RepoUnit{
Type: tp,
Config: new(PullRequestsConfig),
}
ru.Config = new(PullRequestsConfig)
case unit.TypeIssues:
return &RepoUnit{
Type: tp,
Config: new(IssuesConfig),
}
ru.Config = new(IssuesConfig)
case unit.TypeActions:
return &RepoUnit{
Type: tp,
Config: new(ActionsConfig),
}
ru.Config = new(ActionsConfig)
case unit.TypeProjects:
cfg := new(ProjectsConfig)
cfg.ProjectsMode = ProjectsModeNone
return &RepoUnit{
Type: tp,
Config: cfg,
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 &RepoUnit{
Type: tp,
Config: new(UnitConfig),
}
return ru
}
// GetUnit returns a RepoUnit object
@@ -605,7 +580,7 @@ func (repo *Repository) IsGenerated() bool {
// RepoPath returns repository path by given user and repository name.
func RepoPath(userName, repoName string) string { //revive:disable-line:exported
return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git")
return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName)), filepath.Clean(strings.ToLower(repoName)+".git"))
}
// RepoPath returns the repository path
@@ -623,16 +598,13 @@ func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) strin
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, branchName string) string {
if baseRepo == nil {
baseRepo = repo
}
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(baseRepo.DefaultBranch), cmpBranchEscaped)
return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseBranch), cmpBranchEscaped)
}
// IsOwnedBy returns true when user owns this repository
@@ -879,16 +851,6 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
}
// IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed.
func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
has, err := IsRepositoryModelExist(ctx, u, repoName)
if err != nil {
return false, err
}
isDir, err := util.IsDir(RepoPath(u.Name, repoName))
return has || isDir, err
}
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
return db.GetEngine(ctx).Get(&Repository{
OwnerID: u.ID,
@@ -901,7 +863,7 @@ func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName st
// non-generated repositories, and TemplateRepo will be left untouched)
func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) {
if !repo.IsGenerated() {
return nil, nil
return nil, nil //nolint:nilnil // return nil for non-generated repositories
}
return GetRepositoryByID(ctx, repo.TemplateID)
@@ -212,6 +212,13 @@ type SearchRepoOptions struct {
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{
@@ -778,3 +785,11 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor
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)
}
+37 -53
View File
@@ -5,8 +5,6 @@ package repo
import (
"context"
"slices"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
@@ -131,12 +129,29 @@ type PullRequestsConfig struct {
DefaultDeleteBranchAfterMerge bool
DefaultMergeStyle MergeStyle
DefaultAllowMaintainerEdit bool
DefaultTargetBranch string
}
func DefaultPullRequestsConfig() *PullRequestsConfig {
cfg := &PullRequestsConfig{
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
AllowRebaseUpdate: true,
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 {
// AllowRebaseUpdate = true as default for existing PullRequestConfig in DB
cfg.AllowRebaseUpdate = true
// set default values for existing PullRequestConfig in DB
*cfg = *DefaultPullRequestsConfig()
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
@@ -155,51 +170,8 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
}
// GetDefaultMergeStyle returns the default merge style for this pull request
func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
if len(cfg.DefaultMergeStyle) != 0 {
return cfg.DefaultMergeStyle
}
if setting.Repository.PullRequest.DefaultMergeStyle != "" {
return MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
}
return MergeStyleMerge
}
type ActionsConfig struct {
DisabledWorkflows []string
}
func (cfg *ActionsConfig) EnableWorkflow(file string) {
cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) ToString() string {
return strings.Join(cfg.DisabledWorkflows, ",")
}
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)
}
// FromDB fills up a ActionsConfig from serialized format.
func (cfg *ActionsConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports a ActionsConfig to a serialized format.
func (cfg *ActionsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
func DefaultPullRequestsUnit(repoID int64) RepoUnit {
return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()}
}
// ProjectsMode represents the projects enabled for a repository
@@ -223,6 +195,8 @@ type ProjectsConfig struct {
// 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)
}
@@ -253,7 +227,12 @@ func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName {
case "type":
switch unit.Type(db.Cell2Int64(val)) {
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:
@@ -271,6 +250,11 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
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)
}
}
}
@@ -334,9 +318,9 @@ func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err
return units, nil
}
// UpdateRepoUnit updates the provided repo unit
func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
// 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
}
@@ -0,0 +1,153 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"slices"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/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)
}
@@ -4,8 +4,12 @@
package repo
import (
"strings"
"testing"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
"github.com/stretchr/testify/assert"
)
@@ -26,5 +30,75 @@ func TestActionsConfig(t *testing.T) {
cfg.DisableWorkflow("test1.yaml")
cfg.DisableWorkflow("test2.yaml")
cfg.DisableWorkflow("test3.yaml")
assert.Equal(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString())
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])
})
}
+1 -1
View File
@@ -257,7 +257,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e
}
if topic == nil {
// Repo doesn't have topic, can't be removed
return nil, nil
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) {
@@ -9,8 +9,6 @@ import (
"time"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
@@ -106,35 +104,6 @@ func (err ErrRepoFilesAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// CheckCreateRepository check if doer could create a repository in new owner
func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error {
if !doer.CanCreateRepoIn(owner) {
return ErrReachLimitOfRepo{owner.MaxRepoCreation}
}
if err := IsUsableRepoName(name); err != nil {
return err
}
has, err := IsRepositoryModelOrDirExist(ctx, owner, name)
if err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has {
return ErrRepoAlreadyExist{owner.Name, name}
}
repoPath := RepoPath(owner.Name, name)
isExist, err := util.IsExist(repoPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
return err
}
if !overwriteOrAdopt && isExist {
return ErrRepoFilesAlreadyExist{owner.Name, name}
}
return nil
}
// 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{
+26 -18
View File
@@ -8,6 +8,7 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -23,6 +24,12 @@ type StarredReposOptions struct {
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,
@@ -61,6 +68,12 @@ type WatchedReposOptions struct {
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,
@@ -94,8 +107,7 @@ func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Reposit
return db.FindAndCount[Repository](ctx, opts)
}
// GetRepoAssignees returns all users that have write access and can be assigned to issues
// of the repository,
// 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
@@ -114,15 +126,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
uniqueUserIDs.AddMultiple(userIDs...)
if repo.Owner.IsOrganization() {
additionalUserIDs := make([]int64, 0, 10)
if err = e.Table("team_user").
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 = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
// 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...)
@@ -147,19 +153,21 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
}
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
// If isShowFullName is set to true, also include full name prefix search
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
// 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)
var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
if isShowFullName {
prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
}
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")).And(prefixCond)
).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).
@@ -6,7 +6,12 @@ package repo_test
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -14,9 +19,14 @@ import (
"github.com/stretchr/testify/require"
)
func TestRepoAssignees(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
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)
@@ -39,17 +49,37 @@ func TestRepoAssignees(t *testing.T) {
}
}
func TestGetIssuePostersWithSearch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
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", false /* full name */)
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", true /* full 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)
+6 -7
View File
@@ -7,7 +7,6 @@ package repo
import (
"context"
"fmt"
"path/filepath"
"strings"
user_model "code.gitea.io/gitea/models/user"
@@ -76,12 +75,12 @@ func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User
return repo.cloneLink(ctx, doer, repo.Name+".wiki")
}
// WikiPath returns wiki data path by given user and repository name.
func WikiPath(userName, repoName string) string {
return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".wiki.git")
func RelativeWikiPath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git"
}
// WikiPath returns wiki data path for given repository.
func (repo *Repository) WikiPath() string {
return WikiPath(repo.OwnerName, repo.Name)
// 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))
}
+3 -10
View File
@@ -4,12 +4,10 @@
package repo_test
import (
"path/filepath"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
@@ -23,15 +21,10 @@ func TestRepository_WikiCloneLink(t *testing.T) {
assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS)
}
func TestWikiPath(t *testing.T) {
func TestRepository_RelativeWikiPath(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git")
assert.Equal(t, expected, repo_model.WikiPath("user2", "repo1"))
}
func TestRepository_WikiPath(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git")
assert.Equal(t, expected, repo.WikiPath())
assert.Equal(t, "user2/repo1.wiki.git", repo_model.RelativeWikiPath(repo.OwnerName, repo.Name))
assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().RelativePath())
}