mirror of
https://github.com/go-gitea/gitea
synced 2025-12-07 13:28:25 +00:00
Merge branch 'main' into lunny/issue_dev
This commit is contained in:
@@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||
return 0, fmt.Errorf("split token failed")
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
|
||||
return TokenToTaskID(parts[1])
|
||||
}
|
||||
|
||||
// TokenToTaskID returns the TaskID associated with the provided JWT token
|
||||
func TokenToTaskID(token string) (int64, error) {
|
||||
parsedToken, err := jwt.ParseWithClaims(token, &actionsClaims{}, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
@@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
c, ok := token.Claims.(*actionsClaims)
|
||||
if !token.Valid || !ok {
|
||||
c, ok := parsedToken.Claims.(*actionsClaims)
|
||||
if !parsedToken.Valid || !ok {
|
||||
return 0, fmt.Errorf("invalid token claim")
|
||||
}
|
||||
|
||||
|
||||
@@ -128,18 +128,16 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
|
||||
if err != nil {
|
||||
return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err)
|
||||
}
|
||||
if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &git_model.CommitStatus{
|
||||
status := git_model.CommitStatus{
|
||||
SHA: sha,
|
||||
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
|
||||
Description: description,
|
||||
Context: ctxname,
|
||||
CreatorID: creator.ID,
|
||||
State: state,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("NewCommitStatus: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &status)
|
||||
}
|
||||
|
||||
func toCommitStatus(status actions_model.Status) api.CommitStatusState {
|
||||
|
||||
@@ -137,8 +137,12 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
|
||||
Type: issues_model.PullRequestGitea,
|
||||
Flow: issues_model.PullRequestFlowAGit,
|
||||
}
|
||||
|
||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
|
||||
prOpts := &pull_service.NewPullRequestOptions{
|
||||
Repo: repo,
|
||||
Issue: prIssue,
|
||||
PullRequest: pr,
|
||||
}
|
||||
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -5,6 +5,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -76,8 +77,8 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||
log.Trace("Basic Authorization: Attempting login with username as token")
|
||||
}
|
||||
|
||||
// check oauth2 token
|
||||
uid := CheckOAuthAccessToken(req.Context(), authToken)
|
||||
// get oauth2 token's user's ID
|
||||
_, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
|
||||
if uid != 0 {
|
||||
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
|
||||
|
||||
@@ -141,6 +142,15 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||
}
|
||||
|
||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||
// Check if the user has webAuthn registration
|
||||
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasWebAuthn {
|
||||
return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled")
|
||||
}
|
||||
|
||||
if err := validateTOTP(req, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+42
-12
@@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/oauth2_provider"
|
||||
)
|
||||
|
||||
@@ -25,28 +26,47 @@ var (
|
||||
_ Method = &OAuth2{}
|
||||
)
|
||||
|
||||
// CheckOAuthAccessToken returns uid of user from oauth token
|
||||
func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
|
||||
// JWT tokens require a "."
|
||||
if !strings.Contains(accessToken, ".") {
|
||||
return 0
|
||||
// GetOAuthAccessTokenScopeAndUserID returns access token scope and user id
|
||||
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
|
||||
var accessTokenScope auth_model.AccessTokenScope
|
||||
if !setting.OAuth2.Enabled {
|
||||
return accessTokenScope, 0
|
||||
}
|
||||
|
||||
// JWT tokens require a ".", if the token isn't like that, return early
|
||||
if !strings.Contains(accessToken, ".") {
|
||||
return accessTokenScope, 0
|
||||
}
|
||||
|
||||
token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
|
||||
if err != nil {
|
||||
log.Trace("oauth2.ParseToken: %v", err)
|
||||
return 0
|
||||
return accessTokenScope, 0
|
||||
}
|
||||
var grant *auth_model.OAuth2Grant
|
||||
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
|
||||
return 0
|
||||
return accessTokenScope, 0
|
||||
}
|
||||
if token.Kind != oauth2_provider.KindAccessToken {
|
||||
return 0
|
||||
return accessTokenScope, 0
|
||||
}
|
||||
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
|
||||
return 0
|
||||
return accessTokenScope, 0
|
||||
}
|
||||
return grant.UserID
|
||||
accessTokenScope = oauth2_provider.GrantAdditionalScopes(grant.Scope)
|
||||
return accessTokenScope, grant.UserID
|
||||
}
|
||||
|
||||
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
|
||||
func CheckTaskIsRunning(ctx context.Context, taskID int64) bool {
|
||||
// Verify the task exists
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify that it's running
|
||||
return task.Status == actions_model.StatusRunning
|
||||
}
|
||||
|
||||
// OAuth2 implements the Auth interface and authenticates requests
|
||||
@@ -92,10 +112,20 @@ func parseToken(req *http.Request) (string, bool) {
|
||||
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
|
||||
// Let's see if token is valid.
|
||||
if strings.Contains(tokenSHA, ".") {
|
||||
uid := CheckOAuthAccessToken(ctx, tokenSHA)
|
||||
// First attempt to decode an actions JWT, returning the actions user
|
||||
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
|
||||
if CheckTaskIsRunning(ctx, taskID) {
|
||||
store.GetData()["IsActionsToken"] = true
|
||||
store.GetData()["ActionsTaskID"] = taskID
|
||||
return user_model.ActionsUserID
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, check if this is an OAuth access token
|
||||
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(ctx, tokenSHA)
|
||||
if uid != 0 {
|
||||
store.GetData()["IsApiToken"] = true
|
||||
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
}
|
||||
return uid
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserIDFromToken(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("Actions JWT", func(t *testing.T) {
|
||||
const RunningTaskID = 47
|
||||
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ds := make(middleware.ContextData)
|
||||
|
||||
o := OAuth2{}
|
||||
uid := o.userIDFromToken(context.Background(), token, ds)
|
||||
assert.Equal(t, int64(user_model.ActionsUserID), uid)
|
||||
assert.Equal(t, ds["IsActionsToken"], true)
|
||||
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckTaskIsRunning(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
cases := map[string]struct {
|
||||
TaskID int64
|
||||
Expected bool
|
||||
}{
|
||||
"Running": {TaskID: 47, Expected: true},
|
||||
"Missing": {TaskID: 1, Expected: false},
|
||||
"Cancelled": {TaskID: 46, Expected: false},
|
||||
}
|
||||
|
||||
for name := range cases {
|
||||
c := cases[name]
|
||||
t.Run(name, func(t *testing.T) {
|
||||
actual := CheckTaskIsRunning(context.Background(), c.TaskID)
|
||||
assert.Equal(t, c.Expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
)
|
||||
|
||||
type syncType int
|
||||
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
|
||||
}
|
||||
|
||||
if action == syncAdd && !isMember {
|
||||
if err := models.AddTeamMember(ctx, team, user); err != nil {
|
||||
if err := org_service.AddTeamMember(ctx, team, user); err != nil {
|
||||
log.Error("group sync: Could not add user to team: %v", err)
|
||||
return err
|
||||
}
|
||||
} else if action == syncRemove && isMember {
|
||||
if err := models.RemoveTeamMember(ctx, team, user); err != nil {
|
||||
if err := org_service.RemoveTeamMember(ctx, team, user); err != nil {
|
||||
log.Error("group sync: Could not remove user from team: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ package automerge
|
||||
import (
|
||||
"context"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
@@ -44,3 +47,11 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
|
||||
// as reviews could have blocked a pending automerge let's recheck
|
||||
StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
|
||||
}
|
||||
|
||||
func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
|
||||
if status.State.IsSuccess() {
|
||||
if err := StartPRCheckAndAutoMergeBySHA(ctx, commit.Sha1, repo); err != nil {
|
||||
log.Error("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, sender.ID, commit.Sha1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ type contextValuePair struct {
|
||||
valueFn func() any
|
||||
}
|
||||
|
||||
type BaseContextKeyType struct{}
|
||||
|
||||
var BaseContextKey BaseContextKeyType
|
||||
|
||||
type Base struct {
|
||||
originCtx context.Context
|
||||
contextValues []contextValuePair
|
||||
@@ -315,6 +319,7 @@ func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, close
|
||||
Data: middleware.GetContextData(req.Context()),
|
||||
}
|
||||
b.Req = b.Req.WithContext(b)
|
||||
b.AppendContextValue(BaseContextKey, b)
|
||||
b.AppendContextValue(translation.ContextKey, b.Locale)
|
||||
b.AppendContextValue(httplib.RequestContextKey, b.Req)
|
||||
return b, b.cleanUp
|
||||
|
||||
@@ -65,6 +65,9 @@ type Context struct {
|
||||
type TemplateContext map[string]any
|
||||
|
||||
func init() {
|
||||
web.RegisterResponseStatusProvider[*Base](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||
return req.Context().Value(BaseContextKey).(*Base)
|
||||
})
|
||||
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||
return req.Context().Value(WebContextKey).(*Context)
|
||||
})
|
||||
|
||||
@@ -26,7 +26,6 @@ type Organization struct {
|
||||
Organization *organization.Organization
|
||||
OrgLink string
|
||||
CanCreateOrgRepo bool
|
||||
PublicMemberOnly bool // Only display public members
|
||||
|
||||
Team *organization.Team
|
||||
Teams []*organization.Team
|
||||
@@ -176,10 +175,10 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
|
||||
ctx.Data["OrgLink"] = ctx.Org.OrgLink
|
||||
|
||||
// Member
|
||||
ctx.Org.PublicMemberOnly = ctx.Doer == nil || !ctx.Org.IsMember && !ctx.Doer.IsAdmin
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
OrgID: org.ID,
|
||||
PublicOnly: ctx.Org.PublicMemberOnly,
|
||||
Doer: ctx.Doer,
|
||||
OrgID: org.ID,
|
||||
IsDoerMember: ctx.Org.IsMember,
|
||||
}
|
||||
ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
@@ -260,10 +259,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
|
||||
|
||||
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
if len(ctx.ContextUser.Description) != 0 {
|
||||
content, err := markdown.RenderString(&markup.RenderContext{
|
||||
Metas: map[string]string{"mode": "document"},
|
||||
Ctx: ctx,
|
||||
}, ctx.ContextUser.Description)
|
||||
content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
return
|
||||
|
||||
@@ -393,14 +393,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
}
|
||||
}
|
||||
|
||||
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPushMirrorsByRepoID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Repo.Repository = repo
|
||||
ctx.Data["PushMirrors"] = pushMirrors
|
||||
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
return &api.BranchProtection{
|
||||
BranchName: branchName,
|
||||
RuleName: bp.RuleName,
|
||||
Priority: bp.Priority,
|
||||
EnablePush: bp.CanPush,
|
||||
EnablePushWhitelist: bp.EnableWhitelist,
|
||||
PushWhitelistUsernames: pushWhitelistUsernames,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
var reposToFix []*repo_model.Repository
|
||||
|
||||
for page := 1; ; page++ {
|
||||
repos, _, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: repo_model.RepositoryListDefaultPageSize,
|
||||
Page: page,
|
||||
},
|
||||
Mirror: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("SearchRepository: %w", err)
|
||||
}
|
||||
if len(repos) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
if repo.UnitEnabled(ctx, unit_model.TypeActions) {
|
||||
reposToFix = append(reposToFix, repo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(reposToFix) == 0 {
|
||||
logger.Info("Found no mirror with actions unit enabled")
|
||||
} else {
|
||||
logger.Warn("Found %d mirrors with actions unit enabled", len(reposToFix))
|
||||
}
|
||||
if !autofix || len(reposToFix) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, repo := range reposToFix {
|
||||
if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.Info("Fixed %d mirrors with actions unit enabled", len(reposToFix))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Disable the actions unit for all mirrors",
|
||||
Name: "disable-mirror-actions-unit",
|
||||
IsDefault: false,
|
||||
Run: disableMirrorActionsUnit,
|
||||
Priority: 9,
|
||||
})
|
||||
}
|
||||
@@ -6,11 +6,11 @@ package doctor
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -29,7 +29,7 @@ func fixOwnerTeamCreateOrgRepo(ctx context.Context, logger log.Logger, autofix b
|
||||
return nil
|
||||
}
|
||||
|
||||
return models.UpdateTeam(ctx, team, false, false)
|
||||
return org_service.UpdateTeam(ctx, team, false, false)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -390,6 +390,12 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r
|
||||
}
|
||||
|
||||
func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
|
||||
// ignore pull sync message for pull requests refs
|
||||
// TODO: it's better to have a UI to let users chose
|
||||
if opts.RefFullName.IsPull() {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(commits)
|
||||
if err != nil {
|
||||
log.Error("json.Marshal: %v", err)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@@ -122,7 +122,7 @@ type RepoSettingForm struct {
|
||||
MirrorPassword string
|
||||
LFS bool `form:"mirror_lfs"`
|
||||
LFSEndpoint string `form:"mirror_lfs_endpoint"`
|
||||
PushMirrorID string
|
||||
PushMirrorID int64
|
||||
PushMirrorAddress string
|
||||
PushMirrorUsername string
|
||||
PushMirrorPassword string
|
||||
@@ -228,6 +228,10 @@ func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) bin
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
type ProtectBranchPriorityForm struct {
|
||||
IDs []int64
|
||||
}
|
||||
|
||||
// __ __ ___. .__ __
|
||||
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
|
||||
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
|
||||
@@ -447,10 +451,10 @@ type CreateIssueForm struct {
|
||||
Title string `binding:"Required;MaxSize(255)"`
|
||||
LabelIDs string `form:"label_ids"`
|
||||
AssigneeIDs string `form:"assignee_ids"`
|
||||
ReviewerIDs string `form:"reviewer_ids"`
|
||||
Ref string `form:"ref"`
|
||||
MilestoneID int64
|
||||
ProjectID int64
|
||||
AssigneeID int64
|
||||
Content string
|
||||
Files []string
|
||||
AllowMaintainerEdit bool
|
||||
|
||||
@@ -366,7 +366,7 @@ func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
|
||||
// EditOAuth2ApplicationForm form for editing oauth2 applications
|
||||
type EditOAuth2ApplicationForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
||||
RedirectURIs string `binding:"Required" form:"redirect_uris"`
|
||||
RedirectURIs string `binding:"Required;ValidUrlList" form:"redirect_uris"`
|
||||
ConfidentialClient bool `form:"confidential_client"`
|
||||
SkipSecondaryAuthorization bool `form:"skip_secondary_authorization"`
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
@@ -1,6 +0,0 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
+24
-12
@@ -61,7 +61,12 @@ func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, do
|
||||
}
|
||||
|
||||
// ReviewRequest add or remove a review request from a user for this PR, and make comment for it.
|
||||
func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
|
||||
func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, permDoer *access_model.Permission, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
|
||||
err = isValidReviewRequest(ctx, reviewer, doer, isAdd, issue, permDoer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isAdd {
|
||||
comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer)
|
||||
} else {
|
||||
@@ -79,8 +84,8 @@ func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewe
|
||||
return comment, err
|
||||
}
|
||||
|
||||
// IsValidReviewRequest Check permission for ReviewRequest
|
||||
func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
|
||||
// isValidReviewRequest Check permission for ReviewRequest
|
||||
func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
|
||||
if reviewer.IsOrganization() {
|
||||
return issues_model.ErrNotValidReviewRequest{
|
||||
Reason: "Organization can't be added as reviewer",
|
||||
@@ -109,12 +114,12 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
||||
}
|
||||
}
|
||||
|
||||
lastreview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
||||
lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
|
||||
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
|
||||
|
||||
if isAdd {
|
||||
if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) {
|
||||
@@ -137,7 +142,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
||||
return nil
|
||||
}
|
||||
|
||||
if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
|
||||
if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastReview != nil && lastReview.Type != issues_model.ReviewTypeRequest {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -152,7 +157,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
|
||||
if lastReview != nil && lastReview.Type == issues_model.ReviewTypeRequest && lastReview.ReviewerID == doer.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -163,8 +168,8 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidTeamReviewRequest Check permission for ReviewRequest Team
|
||||
func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
|
||||
// isValidTeamReviewRequest Check permission for ReviewRequest Team
|
||||
func isValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
|
||||
if doer.IsOrganization() {
|
||||
return issues_model.ErrNotValidReviewRequest{
|
||||
Reason: "Organization can't be doer to add reviewer",
|
||||
@@ -173,7 +178,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
|
||||
}
|
||||
}
|
||||
|
||||
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
|
||||
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
|
||||
|
||||
if isAdd {
|
||||
if issue.Repo.IsPrivate {
|
||||
@@ -212,6 +217,10 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
|
||||
|
||||
// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
|
||||
func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) {
|
||||
err = isValidTeamReviewRequest(ctx, reviewer, doer, isAdd, issue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isAdd {
|
||||
comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer)
|
||||
} else {
|
||||
@@ -267,9 +276,12 @@ func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doe
|
||||
}
|
||||
|
||||
// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
|
||||
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool {
|
||||
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, posterID int64) bool {
|
||||
if repo.IsArchived {
|
||||
return false
|
||||
}
|
||||
// The poster of the PR can change the reviewers
|
||||
if doer.ID == issue.PosterID {
|
||||
if doer.ID == posterID {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
)
|
||||
|
||||
|
||||
+10
-10
@@ -51,7 +51,7 @@ func GetListLockHandler(ctx *context.Context) {
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rv.User, rv.Repo)
|
||||
if err != nil {
|
||||
log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have pull access to list locks",
|
||||
})
|
||||
@@ -66,7 +66,7 @@ func GetListLockHandler(ctx *context.Context) {
|
||||
|
||||
authenticated := authenticate(ctx, repository, rv.Authorization, true, false)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have pull access to list locks",
|
||||
})
|
||||
@@ -143,7 +143,7 @@ func PostLockHandler(ctx *context.Context) {
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to create locks",
|
||||
})
|
||||
@@ -158,7 +158,7 @@ func PostLockHandler(ctx *context.Context) {
|
||||
|
||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to create locks",
|
||||
})
|
||||
@@ -191,7 +191,7 @@ func PostLockHandler(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if git_model.IsErrLFSUnauthorizedAction(err) {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to create locks : " + err.Error(),
|
||||
})
|
||||
@@ -215,7 +215,7 @@ func VerifyLockHandler(ctx *context.Context) {
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to verify locks",
|
||||
})
|
||||
@@ -230,7 +230,7 @@ func VerifyLockHandler(ctx *context.Context) {
|
||||
|
||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to verify locks",
|
||||
})
|
||||
@@ -286,7 +286,7 @@ func UnLockHandler(ctx *context.Context) {
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to delete locks",
|
||||
})
|
||||
@@ -301,7 +301,7 @@ func UnLockHandler(ctx *context.Context) {
|
||||
|
||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to delete locks",
|
||||
})
|
||||
@@ -324,7 +324,7 @@ func UnLockHandler(ctx *context.Context) {
|
||||
lock, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), repository, ctx.Doer, req.Force)
|
||||
if err != nil {
|
||||
if git_model.IsErrLFSUnauthorizedAction(err) {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
Message: "You must have push access to delete locks : " + err.Error(),
|
||||
})
|
||||
|
||||
+11
-11
@@ -21,7 +21,7 @@ import (
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
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"
|
||||
@@ -77,7 +77,7 @@ func CheckAcceptMediaType(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)\-(\d*).*`)
|
||||
var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)-(\d*).*`)
|
||||
|
||||
// DownloadHandler gets the content from the content store
|
||||
func DownloadHandler(ctx *context.Context) {
|
||||
@@ -507,11 +507,11 @@ func writeStatusMessage(ctx *context.Context, status int, message string) {
|
||||
}
|
||||
|
||||
// authenticate uses the authorization string to determine whether
|
||||
// or not to proceed. This server assumes an HTTP Basic auth format.
|
||||
// to proceed. This server assumes an HTTP Basic auth format.
|
||||
func authenticate(ctx *context.Context, repository *repo_model.Repository, authorization string, requireSigned, requireWrite bool) bool {
|
||||
accessMode := perm.AccessModeRead
|
||||
accessMode := perm_model.AccessModeRead
|
||||
if requireWrite {
|
||||
accessMode = perm.AccessModeWrite
|
||||
accessMode = perm_model.AccessModeWrite
|
||||
}
|
||||
|
||||
if ctx.Data["IsActionsToken"] == true {
|
||||
@@ -526,9 +526,9 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
|
||||
}
|
||||
|
||||
if task.IsForkPullRequest {
|
||||
return accessMode <= perm.AccessModeRead
|
||||
return accessMode <= perm_model.AccessModeRead
|
||||
}
|
||||
return accessMode <= perm.AccessModeWrite
|
||||
return accessMode <= perm_model.AccessModeWrite
|
||||
}
|
||||
|
||||
// ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess
|
||||
@@ -553,7 +553,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
|
||||
return true
|
||||
}
|
||||
|
||||
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
|
||||
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
|
||||
if !strings.Contains(tokenSHA, ".") {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -576,7 +576,7 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
|
||||
return nil, fmt.Errorf("invalid token claim")
|
||||
}
|
||||
|
||||
if mode == perm.AccessModeWrite && claims.Op != "upload" {
|
||||
if mode == perm_model.AccessModeWrite && claims.Op != "upload" {
|
||||
return nil, fmt.Errorf("invalid token claim")
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
|
||||
func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
|
||||
if authorization == "" {
|
||||
return nil, fmt.Errorf("no token")
|
||||
}
|
||||
@@ -608,6 +608,6 @@ func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Rep
|
||||
}
|
||||
|
||||
func requireAuth(ctx *context.Context) {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
writeStatus(ctx, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
+4
-10
@@ -18,12 +18,12 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -219,15 +219,9 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
|
||||
}
|
||||
|
||||
// This is the body of the new issue or comment, not the mail body
|
||||
body, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Repo: ctx.Issue.Repo,
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: ctx.Issue.Repo.HTMLURL(),
|
||||
},
|
||||
Metas: ctx.Issue.Repo.ComposeMetas(ctx),
|
||||
}, ctx.Content)
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx.Context, ctx.Issue.Repo).WithUseAbsoluteLink(true)
|
||||
body, err := markdown.RenderString(rctx,
|
||||
ctx.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
@@ -56,14 +56,9 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re
|
||||
locale := translation.NewLocale(lang)
|
||||
|
||||
var err error
|
||||
rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Repo: rel.Repo,
|
||||
Links: markup.Links{
|
||||
Base: rel.Repo.HTMLURL(),
|
||||
},
|
||||
Metas: rel.Repo.ComposeMetas(ctx),
|
||||
}, rel.Note)
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, rel.Repo).WithUseAbsoluteLink(true)
|
||||
rel.RenderedNote, err = markdown.RenderString(rctx,
|
||||
rel.Note)
|
||||
if err != nil {
|
||||
log.Error("markdown.RenderString(%d): %v", rel.RepoID, err)
|
||||
return
|
||||
|
||||
@@ -70,7 +70,7 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
|
||||
func TestComposeIssueCommentMessage(t *testing.T) {
|
||||
doer, _, issue, comment := prepareMailerTest(t)
|
||||
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
markup.Init(&markup.RenderHelperFuncs{
|
||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||
return username == doer.Name
|
||||
},
|
||||
|
||||
@@ -382,7 +382,7 @@ func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error {
|
||||
if _, err := msg.WriteTo(&buf); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Mail From: %s To: %v Body: %s", from, to, buf.String())
|
||||
log.Debug("Mail From: %s To: %v Body: %s", from, to, buf.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
)
|
||||
|
||||
|
||||
@@ -11,10 +11,8 @@ import (
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func ProcessorHelper() *markup.ProcessorHelper {
|
||||
return &markup.ProcessorHelper{
|
||||
ElementDir: "auto", // set dir="auto" for necessary (eg: <p>, <h?>, etc) tags
|
||||
|
||||
func ProcessorHelper() *markup.RenderHelperFuncs {
|
||||
return &markup.RenderHelperFuncs{
|
||||
RenderRepoFileCodePreview: renderRepoFileCodePreview,
|
||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||
mentionedUser, err := user.GetUserByName(ctx, username)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -119,14 +118,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func queueHandler(items ...*SyncRequest) []*SyncRequest {
|
||||
for _, req := range items {
|
||||
doMirrorSync(graceful.GetManager().ShutdownContext(), req)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSyncMirrors initializes a go routine to sync the mirrors
|
||||
func InitSyncMirrors() {
|
||||
StartSyncMirrors(queueHandler)
|
||||
StartSyncMirrors()
|
||||
}
|
||||
|
||||
@@ -28,12 +28,19 @@ type SyncRequest struct {
|
||||
ReferenceID int64 // RepoID for pull mirror, MirrorID for push mirror
|
||||
}
|
||||
|
||||
func queueHandler(items ...*SyncRequest) []*SyncRequest {
|
||||
for _, req := range items {
|
||||
doMirrorSync(graceful.GetManager().ShutdownContext(), req)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartSyncMirrors starts a go routine to sync the mirrors
|
||||
func StartSyncMirrors(queueHandle func(data ...*SyncRequest) []*SyncRequest) {
|
||||
func StartSyncMirrors() {
|
||||
if !setting.Mirror.Enabled {
|
||||
return
|
||||
}
|
||||
mirrorQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "mirror", queueHandle)
|
||||
mirrorQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "mirror", queueHandler)
|
||||
if mirrorQueue == nil {
|
||||
log.Fatal("Unable to create mirror queue")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package notify
|
||||
import (
|
||||
"context"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -74,4 +75,6 @@ type Notifier interface {
|
||||
PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor)
|
||||
|
||||
ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository)
|
||||
|
||||
CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package notify
|
||||
import (
|
||||
"context"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -367,3 +368,9 @@ func ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) {
|
||||
notifier.ChangeDefaultBranch(ctx, repo)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
|
||||
for _, notifier := range notifiers {
|
||||
notifier.CreateCommitStatus(ctx, repo, commit, sender, status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package notify
|
||||
import (
|
||||
"context"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -208,3 +209,6 @@ func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, p
|
||||
// ChangeDefaultBranch places a place holder function
|
||||
func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) {
|
||||
}
|
||||
|
||||
func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ package oauth2_provider //nolint
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
auth "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -68,6 +71,38 @@ type AccessTokenResponse struct {
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
}
|
||||
|
||||
// GrantAdditionalScopes returns valid scopes coming from grant
|
||||
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
|
||||
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
|
||||
generalScopesSupported := []string{
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
"groups",
|
||||
}
|
||||
|
||||
var accessScopes []string // the scopes for access control, but not for general information
|
||||
for _, scope := range strings.Split(grantScopes, " ") {
|
||||
if scope != "" && !slices.Contains(generalScopesSupported, scope) {
|
||||
accessScopes = append(accessScopes, scope)
|
||||
}
|
||||
}
|
||||
|
||||
// since version 1.22, access tokens grant full access to the API
|
||||
// with this access is reduced only if additional scopes are provided
|
||||
if len(accessScopes) > 0 {
|
||||
accessTokenScope := auth.AccessTokenScope(strings.Join(accessScopes, ","))
|
||||
if normalizedAccessTokenScope, err := accessTokenScope.Normalize(); err == nil {
|
||||
return normalizedAccessTokenScope
|
||||
}
|
||||
// TODO: if there are invalid access scopes (err != nil),
|
||||
// then it is treated as "all", maybe in the future we should make it stricter to return an error
|
||||
// at the moment, to avoid breaking 1.22 behavior, invalid tokens are also treated as "all"
|
||||
}
|
||||
// fallback, empty access scope is treated as "all" access
|
||||
return auth.AccessTokenScopeAll
|
||||
}
|
||||
|
||||
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
|
||||
if setting.OAuth2.InvalidateRefreshTokens {
|
||||
if err := grant.IncreaseCounter(ctx); err != nil {
|
||||
@@ -147,7 +182,7 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
|
||||
Nonce: grant.Nonce,
|
||||
}
|
||||
if grant.ScopeContains("profile") {
|
||||
idToken.Name = user.GetDisplayName()
|
||||
idToken.Name = user.DisplayName()
|
||||
idToken.PreferredUsername = user.Name
|
||||
idToken.Profile = user.HTMLURL()
|
||||
idToken.Picture = user.AvatarLink(ctx)
|
||||
@@ -160,7 +195,13 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
|
||||
idToken.EmailVerified = user.IsActive
|
||||
}
|
||||
if grant.ScopeContains("groups") {
|
||||
groups, err := GetOAuthGroupsForUser(ctx, user)
|
||||
accessTokenScope := GrantAdditionalScopes(grant.Scope)
|
||||
|
||||
// since version 1.22 does not verify if groups should be public-only,
|
||||
// onlyPublicGroups will be set only if 'public-only' is included in a valid scope
|
||||
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
|
||||
|
||||
groups, err := GetOAuthGroupsForUser(ctx, user, onlyPublicGroups)
|
||||
if err != nil {
|
||||
log.Error("Error getting groups: %v", err)
|
||||
return nil, &AccessTokenError{
|
||||
@@ -191,20 +232,24 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
|
||||
|
||||
// returns a list of "org" and "org:team" strings,
|
||||
// that the given user is a part of.
|
||||
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
|
||||
orgs, err := org_model.GetUserOrgsList(ctx, user)
|
||||
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
|
||||
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
|
||||
UserID: user.ID,
|
||||
IncludePrivate: !onlyPublicGroups,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserOrgList: %w", err)
|
||||
}
|
||||
|
||||
orgTeams, err := org_model.OrgList(orgs).LoadTeams(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadTeams: %w", err)
|
||||
}
|
||||
|
||||
var groups []string
|
||||
for _, org := range orgs {
|
||||
groups = append(groups, org.Name)
|
||||
teams, err := org.LoadTeams(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadTeams: %w", err)
|
||||
}
|
||||
for _, team := range teams {
|
||||
for _, team := range orgTeams[org.ID] {
|
||||
if team.IsMember(ctx, user.ID) {
|
||||
groups = append(groups, org.Name+":"+team.LowerName)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2_provider //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGrantAdditionalScopes(t *testing.T) {
|
||||
tests := []struct {
|
||||
grantScopes string
|
||||
expectedScopes string
|
||||
}{
|
||||
{"", "all"}, // for old tokens without scope, treat it as "all"
|
||||
{"openid profile email", "all"},
|
||||
{"openid profile email groups", "all"},
|
||||
{"openid profile email all", "all"},
|
||||
{"openid profile email read:user all", "all"},
|
||||
{"openid profile email groups read:user", "read:user"},
|
||||
{"read:user read:repository", "read:repository,read:user"},
|
||||
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
|
||||
{"openid profile email read:user", "read:user"},
|
||||
|
||||
// TODO: at the moment invalid tokens are treated as "all" to avoid breaking 1.22 behavior (more details are in GrantAdditionalScopes)
|
||||
{"read:invalid_scope", "all"},
|
||||
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("scope:"+test.grantScopes, func(t *testing.T) {
|
||||
result := GrantAdditionalScopes(test.grantScopes)
|
||||
assert.Equal(t, test.expectedScopes, string(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
)
|
||||
|
||||
// TeamAddRepository adds new repository to team of organization.
|
||||
func TeamAddRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) {
|
||||
if repo.OwnerID != t.OrgID {
|
||||
return errors.New("repository does not belong to organization")
|
||||
} else if organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
return models.AddRepository(ctx, t, repo)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// NewTeam creates a record of new team.
|
||||
// It's caller's responsibility to assign organization ID.
|
||||
func NewTeam(ctx context.Context, t *organization.Team) (err error) {
|
||||
if len(t.Name) == 0 {
|
||||
return util.NewInvalidArgumentErrorf("empty team name")
|
||||
}
|
||||
|
||||
if err = organization.IsUsableTeamName(t.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
has, err := db.ExistByID[user_model.User](ctx, t.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return organization.ErrOrgNotExist{ID: t.OrgID}
|
||||
}
|
||||
|
||||
t.LowerName = strings.ToLower(t.Name)
|
||||
has, err = db.Exist[organization.Team](ctx, builder.Eq{
|
||||
"org_id": t.OrgID,
|
||||
"lower_name": t.LowerName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if has {
|
||||
return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = db.Insert(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert units for team
|
||||
if len(t.Units) > 0 {
|
||||
for _, unit := range t.Units {
|
||||
unit.TeamID = t.ID
|
||||
}
|
||||
if err = db.Insert(ctx, &t.Units); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add all repositories to the team if it has access to all of them.
|
||||
if t.IncludesAllRepositories {
|
||||
err = repo_service.AddAllRepositoriesToTeam(ctx, t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("addAllRepositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update organization number of teams.
|
||||
if _, err = db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// UpdateTeam updates information of team.
|
||||
func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeAllChanged bool) (err error) {
|
||||
if len(t.Name) == 0 {
|
||||
return util.NewInvalidArgumentErrorf("empty team name")
|
||||
}
|
||||
|
||||
if len(t.Description) > 255 {
|
||||
t.Description = t.Description[:255]
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
t.LowerName = strings.ToLower(t.Name)
|
||||
has, err := db.Exist[organization.Team](ctx, builder.Eq{
|
||||
"org_id": t.OrgID,
|
||||
"lower_name": t.LowerName,
|
||||
}.And(builder.Neq{"id": t.ID}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description",
|
||||
"can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil {
|
||||
return fmt.Errorf("update: %w", err)
|
||||
}
|
||||
|
||||
// update units for team
|
||||
if len(t.Units) > 0 {
|
||||
for _, unit := range t.Units {
|
||||
unit.TeamID = t.ID
|
||||
}
|
||||
// Delete team-unit.
|
||||
if _, err := sess.
|
||||
Where("team_id=?", t.ID).
|
||||
Delete(new(organization.TeamUnit)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = sess.Cols("org_id", "team_id", "type", "access_mode").Insert(&t.Units); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update access for team members if needed.
|
||||
if authChanged {
|
||||
if err = t.LoadRepositories(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRepositories: %w", err)
|
||||
}
|
||||
|
||||
for _, repo := range t.Repos {
|
||||
if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
|
||||
return fmt.Errorf("recalculateTeamAccesses: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all repositories to the team if it has access to all of them.
|
||||
if includeAllChanged && t.IncludesAllRepositories {
|
||||
err = repo_service.AddAllRepositoriesToTeam(ctx, t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("addAllRepositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// DeleteTeam deletes given team.
|
||||
// It's caller's responsibility to assign organization ID.
|
||||
func DeleteTeam(ctx context.Context, t *organization.Team) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err := t.LoadRepositories(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.LoadMembers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update branch protections
|
||||
{
|
||||
protections := make([]*git_model.ProtectedBranch, 0, 10)
|
||||
err := db.GetEngine(ctx).In("repo_id",
|
||||
builder.Select("id").From("repository").Where(builder.Eq{"owner_id": t.OrgID})).
|
||||
Find(&protections)
|
||||
if err != nil {
|
||||
return fmt.Errorf("findProtectedBranches: %w", err)
|
||||
}
|
||||
for _, p := range protections {
|
||||
if err := git_model.RemoveTeamIDFromProtectedBranch(ctx, p, t.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := repo_service.RemoveAllRepositoriesFromTeam(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.DeleteBeans(ctx,
|
||||
&organization.Team{ID: t.ID},
|
||||
&organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID},
|
||||
&organization.TeamUnit{TeamID: t.ID},
|
||||
&organization.TeamInvite{TeamID: t.ID},
|
||||
&issues_model.Review{Type: issues_model.ReviewTypeRequest, ReviewerTeamID: t.ID}, // batch delete the binding relationship between team and PR (request review from team)
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tm := range t.Members {
|
||||
if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update organization number of teams.
|
||||
if _, err := db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// AddTeamMember adds new membership of given team to given organization,
|
||||
// the user will have membership to given organization automatically when needed.
|
||||
func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
|
||||
if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
|
||||
if err != nil || isAlreadyMember {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// check in transaction
|
||||
isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
|
||||
if err != nil || isAlreadyMember {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if err := db.Insert(ctx, &organization.TeamUser{
|
||||
UID: user.ID,
|
||||
OrgID: team.OrgID,
|
||||
TeamID: team.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if _, err := sess.Incr("num_members").ID(team.ID).Update(new(organization.Team)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
team.NumMembers++
|
||||
|
||||
// Give access to team repositories.
|
||||
// update exist access if mode become bigger
|
||||
subQuery := builder.Select("repo_id").From("team_repo").
|
||||
Where(builder.Eq{"team_id": team.ID})
|
||||
|
||||
if _, err := sess.Where("user_id=?", user.ID).
|
||||
In("repo_id", subQuery).
|
||||
And("mode < ?", team.AccessMode).
|
||||
SetExpr("mode", team.AccessMode).
|
||||
Update(new(access_model.Access)); err != nil {
|
||||
return fmt.Errorf("update user accesses: %w", err)
|
||||
}
|
||||
|
||||
// for not exist access
|
||||
var repoIDs []int64
|
||||
accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
|
||||
if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
|
||||
return fmt.Errorf("select id accesses: %w", err)
|
||||
}
|
||||
|
||||
accesses := make([]*access_model.Access, 0, 100)
|
||||
for i, repoID := range repoIDs {
|
||||
accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
|
||||
if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
|
||||
if err = db.Insert(ctx, accesses); err != nil {
|
||||
return fmt.Errorf("insert new user accesses: %w", err)
|
||||
}
|
||||
accesses = accesses[:0]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this behaviour may spend much time so run it in a goroutine
|
||||
// FIXME: Update watch repos batchly
|
||||
if setting.Service.AutoWatchNewRepos {
|
||||
// Get team and its repositories.
|
||||
if err := team.LoadRepositories(ctx); err != nil {
|
||||
log.Error("team.LoadRepositories failed: %v", err)
|
||||
}
|
||||
|
||||
// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
|
||||
go func(repos []*repo_model.Repository) {
|
||||
for _, repo := range repos {
|
||||
if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
|
||||
log.Error("watch repo failed: %v", err)
|
||||
}
|
||||
}
|
||||
}(team.Repos)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
|
||||
e := db.GetEngine(ctx)
|
||||
isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
|
||||
if err != nil || !isMember {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user to delete is the last member in owner team.
|
||||
if team.IsOwnerTeam() && team.NumMembers == 1 {
|
||||
return organization.ErrLastOrgOwner{UID: user.ID}
|
||||
}
|
||||
|
||||
team.NumMembers--
|
||||
|
||||
if err := team.LoadRepositories(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := e.Delete(&organization.TeamUser{
|
||||
UID: user.ID,
|
||||
OrgID: team.OrgID,
|
||||
TeamID: team.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if _, err = e.
|
||||
ID(team.ID).
|
||||
Cols("num_members").
|
||||
Update(team); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete access to team repositories.
|
||||
for _, repo := range team.Repos {
|
||||
if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove watches from now unaccessible
|
||||
if err := repo_service.ReconsiderWatches(ctx, repo, user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove issue assignments from now unaccessible
|
||||
if err := repo_service.ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return removeInvalidOrgUser(ctx, team.OrgID, user)
|
||||
}
|
||||
|
||||
func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
|
||||
// Check if the user is a member of any team in the organization.
|
||||
if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
|
||||
UID: user.ID,
|
||||
OrgID: orgID,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if count == 0 {
|
||||
org, err := organization.GetOrgByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return RemoveOrgUser(ctx, org, user)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTeamMember removes member from given team of given organization.
|
||||
func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
if err := removeTeamMember(ctx, team, user); err != nil {
|
||||
return err
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"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/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTeam_AddMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
|
||||
}
|
||||
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
test(team1, user2)
|
||||
test(team1, user4)
|
||||
test(team3, user2)
|
||||
}
|
||||
|
||||
func TestTeam_RemoveMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
|
||||
}
|
||||
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
testSuccess(team1, user4)
|
||||
testSuccess(team2, user2)
|
||||
testSuccess(team3, user2)
|
||||
|
||||
err := RemoveTeamMember(db.DefaultContext, team1, user2)
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
}
|
||||
|
||||
func TestNewTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const teamName = "newTeamName"
|
||||
team := &organization.Team{Name: teamName, OrgID: 3}
|
||||
assert.NoError(t, NewTeam(db.DefaultContext, team))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.Team{Name: teamName})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{}, &user_model.User{ID: team.OrgID})
|
||||
}
|
||||
|
||||
func TestUpdateTeam(t *testing.T) {
|
||||
// successful update
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
team.LowerName = "newname"
|
||||
team.Name = "newName"
|
||||
team.Description = strings.Repeat("A long description!", 100)
|
||||
team.AccessMode = perm.AccessModeAdmin
|
||||
assert.NoError(t, UpdateTeam(db.DefaultContext, team, true, false))
|
||||
|
||||
team = unittest.AssertExistsAndLoadBean(t, &organization.Team{Name: "newName"})
|
||||
assert.True(t, strings.HasPrefix(team.Description, "A long description!"))
|
||||
|
||||
access := unittest.AssertExistsAndLoadBean(t, &access_model.Access{UserID: 4, RepoID: 3})
|
||||
assert.EqualValues(t, perm.AccessModeAdmin, access.Mode)
|
||||
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
|
||||
}
|
||||
|
||||
func TestUpdateTeam2(t *testing.T) {
|
||||
// update to already-existing team
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
team.LowerName = "owners"
|
||||
team.Name = "Owners"
|
||||
team.Description = strings.Repeat("A long description!", 100)
|
||||
err := UpdateTeam(db.DefaultContext, team, true, false)
|
||||
assert.True(t, organization.IsErrTeamAlreadyExist(err))
|
||||
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
|
||||
}
|
||||
|
||||
func TestDeleteTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
assert.NoError(t, DeleteTeam(db.DefaultContext, team))
|
||||
unittest.AssertNotExistsBean(t, &organization.Team{ID: team.ID})
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: team.ID})
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamUser{TeamID: team.ID})
|
||||
|
||||
// check that team members don't have "leftover" access to repos
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
accessMode, err := access_model.AccessLevel(db.DefaultContext, user, repo)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, accessMode < perm.AccessModeWrite)
|
||||
}
|
||||
|
||||
func TestAddTeamMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
|
||||
}
|
||||
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
test(team1, user2)
|
||||
test(team1, user4)
|
||||
test(team3, user2)
|
||||
}
|
||||
|
||||
func TestRemoveTeamMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
|
||||
}
|
||||
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
testSuccess(team1, user4)
|
||||
testSuccess(team2, user2)
|
||||
testSuccess(team3, user2)
|
||||
|
||||
err := RemoveTeamMember(db.DefaultContext, team1, user2)
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses3(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
|
||||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
|
||||
// adding user29 to team5 should add an explicit access row for repo 23
|
||||
// even though repo 23 is public
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
|
||||
|
||||
has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
}
|
||||
|
||||
func TestIncludesAllRepositoriesTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testTeamRepositories := func(teamID int64, repoIDs []int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name)
|
||||
assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name)
|
||||
assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name)
|
||||
for i, rid := range repoIDs {
|
||||
if rid > 0 {
|
||||
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get an admin user.
|
||||
user, err := user_model.GetUserByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err, "GetUserByID")
|
||||
|
||||
// Create org.
|
||||
org := &organization.Organization{
|
||||
Name: "All_repo",
|
||||
IsActive: true,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, user), "CreateOrganization")
|
||||
|
||||
// Check Owner team.
|
||||
ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err, "GetOwnerTeam")
|
||||
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
|
||||
|
||||
// Create repos.
|
||||
repoIDs := make([]int64, 0)
|
||||
for i := 0; i < 3; i++ {
|
||||
r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
|
||||
assert.NoError(t, err, "CreateRepository %d", i)
|
||||
if r != nil {
|
||||
repoIDs = append(repoIDs, r.ID)
|
||||
}
|
||||
}
|
||||
// Get fresh copy of Owner team after creating repos.
|
||||
ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err, "GetOwnerTeam")
|
||||
|
||||
// Create teams and check repositories.
|
||||
teams := []*organization.Team{
|
||||
ownerTeam,
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team one",
|
||||
AccessMode: perm.AccessModeRead,
|
||||
IncludesAllRepositories: true,
|
||||
},
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team 2",
|
||||
AccessMode: perm.AccessModeRead,
|
||||
IncludesAllRepositories: false,
|
||||
},
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team three",
|
||||
AccessMode: perm.AccessModeWrite,
|
||||
IncludesAllRepositories: true,
|
||||
},
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team 4",
|
||||
AccessMode: perm.AccessModeWrite,
|
||||
IncludesAllRepositories: false,
|
||||
},
|
||||
}
|
||||
teamRepos := [][]int64{
|
||||
repoIDs,
|
||||
repoIDs,
|
||||
{},
|
||||
repoIDs,
|
||||
{},
|
||||
}
|
||||
for i, team := range teams {
|
||||
if i > 0 { // first team is Owner.
|
||||
assert.NoError(t, NewTeam(db.DefaultContext, team), "%s: NewTeam", team.Name)
|
||||
}
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Update teams and check repositories.
|
||||
teams[3].IncludesAllRepositories = false
|
||||
teams[4].IncludesAllRepositories = true
|
||||
teamRepos[4] = repoIDs
|
||||
for i, team := range teams {
|
||||
assert.NoError(t, UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name)
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Create repo and check teams repositories.
|
||||
r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: "repo-last"})
|
||||
assert.NoError(t, err, "CreateRepository last")
|
||||
if r != nil {
|
||||
repoIDs = append(repoIDs, r.ID)
|
||||
}
|
||||
teamRepos[0] = repoIDs
|
||||
teamRepos[1] = repoIDs
|
||||
teamRepos[4] = repoIDs
|
||||
for i, team := range teams {
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Remove repo and check teams repositories.
|
||||
assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository")
|
||||
teamRepos[0] = repoIDs[1:]
|
||||
teamRepos[1] = repoIDs[1:]
|
||||
teamRepos[3] = repoIDs[1:3]
|
||||
teamRepos[4] = repoIDs[1:]
|
||||
for i, team := range teams {
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Wipe created items.
|
||||
for i, rid := range repoIDs {
|
||||
if i > 0 { // first repo already deleted.
|
||||
assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i)
|
||||
}
|
||||
}
|
||||
assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// RemoveOrgUser removes user from given organization.
|
||||
func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
|
||||
ou := new(organization.OrgUser)
|
||||
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("uid=?", user.ID).
|
||||
And("org_id=?", org.ID).
|
||||
Get(ou)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get org-user: %w", err)
|
||||
} else if !has {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the user to delete is the last member in owner team.
|
||||
if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
|
||||
return err
|
||||
} else if isOwner {
|
||||
t, err := organization.GetOwnerTeam(ctx, org.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.NumMembers == 1 {
|
||||
if err := t.LoadMembers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Members[0].ID == user.ID {
|
||||
return organization.ErrLastOrgOwner{UID: user.ID}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
|
||||
return err
|
||||
} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete all repository accesses and unwatch them.
|
||||
env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("AccessibleReposEnv: %w", err)
|
||||
}
|
||||
repoIDs, err := env.RepoIDs(1, org.NumRepos)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
|
||||
}
|
||||
for _, repoID := range repoIDs {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(repoIDs) > 0 {
|
||||
if _, err = db.GetEngine(ctx).
|
||||
Where("user_id = ?", user.ID).
|
||||
In("repo_id", repoIDs).
|
||||
Delete(new(access_model.Access)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete member in their teams.
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range teams {
|
||||
if err = removeTeamMember(ctx, t, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUser_RemoveMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
// remove a user that is a member
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
|
||||
prevNumMembers := org.NumMembers
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
|
||||
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
|
||||
assert.Equal(t, prevNumMembers-1, org.NumMembers)
|
||||
|
||||
// remove a user that is not a member
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
|
||||
prevNumMembers = org.NumMembers
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
|
||||
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
|
||||
assert.Equal(t, prevNumMembers, org.NumMembers)
|
||||
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||
}
|
||||
|
||||
func TestRemoveOrgUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(org *organization.Organization, user *user_model.User) {
|
||||
expectedNumMembers := org.NumMembers
|
||||
if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
|
||||
expectedNumMembers--
|
||||
}
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
|
||||
assert.EqualValues(t, expectedNumMembers, org.NumMembers)
|
||||
}
|
||||
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
testSuccess(org3, user4)
|
||||
|
||||
org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess(org3, user4)
|
||||
|
||||
err := RemoveOrgUser(db.DefaultContext, org7, user5)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||
}
|
||||
+25
-2
@@ -18,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
@@ -42,8 +43,20 @@ func getPullWorkingLockKey(prID int64) string {
|
||||
return fmt.Sprintf("pull_working_%d", prID)
|
||||
}
|
||||
|
||||
type NewPullRequestOptions struct {
|
||||
Repo *repo_model.Repository
|
||||
Issue *issues_model.Issue
|
||||
LabelIDs []int64
|
||||
AttachmentUUIDs []string
|
||||
PullRequest *issues_model.PullRequest
|
||||
AssigneeIDs []int64
|
||||
Reviewers []*user_model.User
|
||||
TeamReviewers []*organization.Team
|
||||
}
|
||||
|
||||
// NewPullRequest creates new pull request with labels for repository.
|
||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
|
||||
func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
||||
repo, issue, labelIDs, uuids, pr, assigneeIDs := opts.Repo, opts.Issue, opts.LabelIDs, opts.AttachmentUUIDs, opts.PullRequest, opts.AssigneeIDs
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -215,7 +228,17 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
|
||||
}
|
||||
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
|
||||
}
|
||||
|
||||
permDoer, err := access_model.GetUserRepoPermission(ctx, repo, issue.Poster)
|
||||
for _, reviewer := range opts.Reviewers {
|
||||
if _, err = issue_service.ReviewRequest(ctx, pr.Issue, issue.Poster, &permDoer, reviewer, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, teamReviewer := range opts.TeamReviewers {
|
||||
if _, err = issue_service.TeamReviewRequest(ctx, pr.Issue, issue.Poster, teamReviewer, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// GetReviewers get all users can be requested to review:
|
||||
// - Poster should not be listed
|
||||
// - For collaborator, all users that have read access or higher to the repository.
|
||||
// - For repository under organization, users under the teams which have read permission or higher of pull request unit
|
||||
// - Owner will be listed if it's not an organization, not the poster and not in the list of reviewers
|
||||
func GetReviewers(ctx context.Context, repo *repo_model.Repository, doerID, posterID int64) ([]*user_model.User, error) {
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
uniqueUserIDs := make(container.Set[int64])
|
||||
|
||||
collaboratorIDs := make([]int64, 0, 10)
|
||||
if err := e.Table("collaboration").Where("repo_id=?", repo.ID).
|
||||
And("mode >= ?", perm.AccessModeRead).
|
||||
Select("user_id").
|
||||
Find(&collaboratorIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniqueUserIDs.AddMultiple(collaboratorIDs...)
|
||||
|
||||
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 >= ? AND `team_unit`.`type` = ?)",
|
||||
repo.ID, perm.AccessModeRead, unit.TypePullRequests).
|
||||
Distinct("`team_user`.uid").
|
||||
Select("`team_user`.uid").
|
||||
Find(&additionalUserIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
||||
}
|
||||
|
||||
uniqueUserIDs.Remove(posterID) // posterID should not be in the list of reviewers
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// add owner after all users are loaded because we can avoid load owner twice
|
||||
if repo.OwnerID != posterID && !repo.Owner.IsOrganization() && !uniqueUserIDs.Contains(repo.OwnerID) {
|
||||
users = append(users, repo.Owner)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetReviewerTeams get all teams can be requested to review
|
||||
func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*organization.Team, error) {
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoGetReviewers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// test public repo
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
ctx := db.DefaultContext
|
||||
reviewers, err := pull_service.GetReviewers(ctx, repo1, 2, 0)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, reviewers, 1) {
|
||||
assert.ElementsMatch(t, []int64{2}, []int64{reviewers[0].ID})
|
||||
}
|
||||
|
||||
// should not include doer and remove the poster
|
||||
reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 0)
|
||||
|
||||
// should not include PR poster, if PR poster would be otherwise eligible
|
||||
reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
|
||||
// test private user repo
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
|
||||
reviewers, err = pull_service.GetReviewers(ctx, repo2, 2, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
assert.EqualValues(t, reviewers[0].ID, 2)
|
||||
|
||||
// test private org repo
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
|
||||
reviewers, err = pull_service.GetReviewers(ctx, repo3, 2, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 2)
|
||||
|
||||
reviewers, err = pull_service.GetReviewers(ctx, repo3, 2, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
}
|
||||
|
||||
func TestRepoGetReviewerTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
teams, err := pull_service.GetReviewerTeams(db.DefaultContext, repo2)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, teams)
|
||||
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
teams, err = pull_service.GetReviewerTeams(db.DefaultContext, repo3)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, teams, 2)
|
||||
}
|
||||
@@ -142,6 +142,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
||||
return err
|
||||
}
|
||||
|
||||
rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255)
|
||||
rel.LowerTagName = strings.ToLower(rel.TagName)
|
||||
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
|
||||
return err
|
||||
|
||||
@@ -66,7 +66,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
||||
}
|
||||
}
|
||||
|
||||
if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil {
|
||||
if err := CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
|
||||
|
||||
func TestAdoptRepository(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.NoError(t, unittest.CopyDir(filepath.Join(setting.RepoRootPath, "user2", "repo1.git"), filepath.Join(setting.RepoRootPath, "user2", "test-adopt.git")))
|
||||
assert.NoError(t, unittest.SyncDirs(filepath.Join(setting.RepoRootPath, "user2", "repo1.git"), filepath.Join(setting.RepoRootPath, "user2", "test-adopt.git")))
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
_, err := AdoptRepository(db.DefaultContext, user2, user2, CreateRepoOptions{Name: "test-adopt"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -67,30 +67,36 @@ func (e RepoRefNotFoundError) Is(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// NewRequest creates an archival request, based on the URI. The
|
||||
// resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
|
||||
// if it's determined that the request still needs to be satisfied.
|
||||
func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) {
|
||||
r := &ArchiveRequest{
|
||||
RepoID: repoID,
|
||||
}
|
||||
|
||||
var ext string
|
||||
func ParseFileName(uri string) (ext string, tp git.ArchiveType, err error) {
|
||||
switch {
|
||||
case strings.HasSuffix(uri, ".zip"):
|
||||
ext = ".zip"
|
||||
r.Type = git.ZIP
|
||||
tp = git.ZIP
|
||||
case strings.HasSuffix(uri, ".tar.gz"):
|
||||
ext = ".tar.gz"
|
||||
r.Type = git.TARGZ
|
||||
tp = git.TARGZ
|
||||
case strings.HasSuffix(uri, ".bundle"):
|
||||
ext = ".bundle"
|
||||
r.Type = git.BUNDLE
|
||||
tp = git.BUNDLE
|
||||
default:
|
||||
return nil, ErrUnknownArchiveFormat{RequestFormat: uri}
|
||||
return "", 0, ErrUnknownArchiveFormat{RequestFormat: uri}
|
||||
}
|
||||
return ext, tp, nil
|
||||
}
|
||||
|
||||
// NewRequest creates an archival request, based on the URI. The
|
||||
// resulting ArchiveRequest is suitable for being passed to Await()
|
||||
// if it's determined that the request still needs to be satisfied.
|
||||
func NewRequest(repoID int64, repo *git.Repository, refName string, fileType git.ArchiveType) (*ArchiveRequest, error) {
|
||||
if fileType < git.ZIP || fileType > git.BUNDLE {
|
||||
return nil, ErrUnknownArchiveFormat{RequestFormat: fileType.String()}
|
||||
}
|
||||
|
||||
r.refName = strings.TrimSuffix(uri, ext)
|
||||
r := &ArchiveRequest{
|
||||
RepoID: repoID,
|
||||
refName: refName,
|
||||
Type: fileType,
|
||||
}
|
||||
|
||||
// Get corresponding commit.
|
||||
commitID, err := repo.ConvertToGitID(r.refName)
|
||||
@@ -151,13 +157,14 @@ func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver
|
||||
}
|
||||
}
|
||||
|
||||
// doArchive satisfies the ArchiveRequest being passed in. Processing
|
||||
// will occur in a separate goroutine, as this phase may take a while to
|
||||
// complete. If the archive already exists, doArchive will not do
|
||||
// anything. In all cases, the caller should be examining the *ArchiveRequest
|
||||
// being returned for completion, as it may be different than the one they passed
|
||||
// in.
|
||||
func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
|
||||
txCtx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
ctx, _, finished := process.GetManager().AddContext(txCtx, fmt.Sprintf("ArchiveRequest[%d]: %s", r.RepoID, r.GetArchiveName()))
|
||||
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("ArchiveRequest[%d]: %s", r.RepoID, r.GetArchiveName()))
|
||||
defer finished()
|
||||
|
||||
archiver, err := repo_model.GetRepoArchiver(ctx, r.RepoID, r.Type, r.CommitID)
|
||||
@@ -192,7 +199,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return archiver, committer.Commit()
|
||||
return archiver, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
@@ -261,17 +268,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
|
||||
}
|
||||
}
|
||||
|
||||
return archiver, committer.Commit()
|
||||
}
|
||||
|
||||
// ArchiveRepository satisfies the ArchiveRequest being passed in. Processing
|
||||
// will occur in a separate goroutine, as this phase may take a while to
|
||||
// complete. If the archive already exists, ArchiveRepository will not do
|
||||
// anything. In all cases, the caller should be examining the *ArchiveRequest
|
||||
// being returned for completion, as it may be different than the one they passed
|
||||
// in.
|
||||
func ArchiveRepository(ctx context.Context, request *ArchiveRequest) (*repo_model.RepoArchiver, error) {
|
||||
return doArchive(ctx, request)
|
||||
return archiver, nil
|
||||
}
|
||||
|
||||
var archiverQueue *queue.WorkerPoolQueue[*ArchiveRequest]
|
||||
@@ -281,8 +278,10 @@ func Init(ctx context.Context) error {
|
||||
handler := func(items ...*ArchiveRequest) []*ArchiveRequest {
|
||||
for _, archiveReq := range items {
|
||||
log.Trace("ArchiverData Process: %#v", archiveReq)
|
||||
if _, err := doArchive(ctx, archiveReq); err != nil {
|
||||
if archiver, err := doArchive(ctx, archiveReq); err != nil {
|
||||
log.Error("Archive %v failed: %v", archiveReq, err)
|
||||
} else {
|
||||
log.Trace("ArchiverData Success: %#v", archiver)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
@@ -31,47 +32,47 @@ func TestArchive_Basic(t *testing.T) {
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
|
||||
bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName())
|
||||
|
||||
// Check a series of bogus requests.
|
||||
// Step 1, valid commit with a bad extension.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, 100)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
// Step 2, missing commit.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff", git.ZIP)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
// Step 3, doesn't look like branch/tag/commit.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db", git.ZIP)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master", git.ZIP)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName())
|
||||
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive", git.ZIP)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName())
|
||||
|
||||
// Now two valid requests, firstCommit with valid extensions.
|
||||
zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, zipReq)
|
||||
|
||||
tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz")
|
||||
tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.TARGZ)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tgzReq)
|
||||
|
||||
secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip")
|
||||
secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit, git.ZIP)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, secondReq)
|
||||
|
||||
@@ -80,18 +81,18 @@ func TestArchive_Basic(t *testing.T) {
|
||||
inFlight[1] = tgzReq
|
||||
inFlight[2] = secondReq
|
||||
|
||||
ArchiveRepository(db.DefaultContext, zipReq)
|
||||
ArchiveRepository(db.DefaultContext, tgzReq)
|
||||
ArchiveRepository(db.DefaultContext, secondReq)
|
||||
doArchive(db.DefaultContext, zipReq)
|
||||
doArchive(db.DefaultContext, tgzReq)
|
||||
doArchive(db.DefaultContext, secondReq)
|
||||
|
||||
// Make sure sending an unprocessed request through doesn't affect the queue
|
||||
// count.
|
||||
ArchiveRepository(db.DefaultContext, zipReq)
|
||||
doArchive(db.DefaultContext, zipReq)
|
||||
|
||||
// Sleep two seconds to make sure the queue doesn't change.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP)
|
||||
assert.NoError(t, err)
|
||||
// This zipReq should match what's sitting in the queue, as we haven't
|
||||
// let it release yet. From the consumer's point of view, this looks like
|
||||
@@ -101,17 +102,17 @@ func TestArchive_Basic(t *testing.T) {
|
||||
// We still have the other three stalled at completion, waiting to remove
|
||||
// from archiveInProgress. Try to submit this new one before its
|
||||
// predecessor has cleared out of the queue.
|
||||
ArchiveRepository(db.DefaultContext, zipReq2)
|
||||
doArchive(db.DefaultContext, zipReq2)
|
||||
|
||||
// Now we'll submit a request and TimedWaitForCompletion twice, before and
|
||||
// after we release it. We should trigger both the timeout and non-timeout
|
||||
// cases.
|
||||
timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz")
|
||||
timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit, git.TARGZ)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, timedReq)
|
||||
ArchiveRepository(db.DefaultContext, timedReq)
|
||||
doArchive(db.DefaultContext, timedReq)
|
||||
|
||||
zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP)
|
||||
assert.NoError(t, err)
|
||||
// Now, we're guaranteed to have released the original zipReq from the queue.
|
||||
// Ensure that we don't get handed back the released entry somehow, but they
|
||||
|
||||
@@ -6,14 +6,64 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func AddOrUpdateCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User, mode perm.AccessMode) error {
|
||||
// only allow valid access modes, read, write and admin
|
||||
if mode < perm.AccessModeRead || mode > perm.AccessModeAdmin {
|
||||
return perm.ErrInvalidAccessMode
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
collaboration, has, err := db.Get[repo_model.Collaboration](ctx, builder.Eq{
|
||||
"repo_id": repo.ID,
|
||||
"user_id": u.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
if collaboration.Mode == mode {
|
||||
return nil
|
||||
}
|
||||
if _, err = db.GetEngine(ctx).
|
||||
Where("repo_id=?", repo.ID).
|
||||
And("user_id=?", u.ID).
|
||||
Cols("mode").
|
||||
Update(&repo_model.Collaboration{
|
||||
Mode: mode,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err = db.Insert(ctx, &repo_model.Collaboration{
|
||||
RepoID: repo.ID,
|
||||
UserID: u.ID,
|
||||
Mode: mode,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return access_model.RecalculateUserAccess(ctx, repo, u.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCollaboration removes collaboration relation between the user and repository.
|
||||
func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
|
||||
collaboration := &repo_model.Collaboration{
|
||||
@@ -45,14 +95,39 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, colla
|
||||
return err
|
||||
}
|
||||
|
||||
if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
|
||||
if err = ReconsiderWatches(ctx, repo, collaborator); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unassign a user from any issue (s)he has been assigned to in the repository
|
||||
if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
|
||||
if err := ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
|
||||
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
|
||||
In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
|
||||
Delete(&issues_model.IssueAssignees{}); err != nil {
|
||||
return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
|
||||
if has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo); err != nil || has {
|
||||
return err
|
||||
}
|
||||
if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all IssueWatches a user has subscribed to in the repository
|
||||
return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -14,6 +15,21 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepository_AddCollaborator(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(repoID, userID int64) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
assert.NoError(t, repo.LoadOwner(db.DefaultContext))
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
assert.NoError(t, AddOrUpdateCollaborator(db.DefaultContext, repo, user, perm.AccessModeWrite))
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID})
|
||||
}
|
||||
testSuccess(1, 4)
|
||||
testSuccess(1, 4)
|
||||
testSuccess(3, 4)
|
||||
}
|
||||
|
||||
func TestRepository_DeleteCollaboration(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
"code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
func getCacheKey(repoID int64, brancheName string) string {
|
||||
@@ -103,6 +104,8 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
|
||||
return err
|
||||
}
|
||||
|
||||
notify.CreateCommitStatus(ctx, repo, repo_module.CommitToPushCommit(commit), creator, status)
|
||||
|
||||
defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err)
|
||||
@@ -114,12 +117,6 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
|
||||
}
|
||||
}
|
||||
|
||||
if status.State.IsSuccess() {
|
||||
if err := automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil {
|
||||
return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
func TestRepository_ContributorsGraph(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
assert.NoError(t, repo.LoadOwner(db.DefaultContext))
|
||||
mockCache, err := cache.NewStringCache(setting.Cache{})
|
||||
@@ -46,7 +47,7 @@ func TestRepository_ContributorsGraph(t *testing.T) {
|
||||
|
||||
assert.EqualValues(t, &ContributorData{
|
||||
Name: "Ethan Koenig",
|
||||
AvatarLink: "https://secure.gravatar.com/avatar/b42fb195faa8c61b8d88abfefe30e9e3?d=identicon",
|
||||
AvatarLink: "/assets/img/avatar_default.png",
|
||||
TotalCommits: 1,
|
||||
Weeks: map[int64]*WeekData{
|
||||
1511654400000: {
|
||||
|
||||
@@ -13,8 +13,13 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"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"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -243,7 +248,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
|
||||
var rollbackRepo *repo_model.Repository
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
|
||||
if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -335,3 +340,141 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// CreateRepositoryByExample creates a repository for the user/organization.
|
||||
func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) {
|
||||
if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IsRepositoryExist: %w", err)
|
||||
} else if has {
|
||||
return repo_model.ErrRepoAlreadyExist{
|
||||
Uname: u.Name,
|
||||
Name: repo.Name,
|
||||
}
|
||||
}
|
||||
|
||||
repoPath := repo_model.RepoPath(u.Name, repo.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 {
|
||||
log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
|
||||
return repo_model.ErrRepoFilesAlreadyExist{
|
||||
Uname: u.Name,
|
||||
Name: repo.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, repo); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert units for repo
|
||||
defaultUnits := unit.DefaultRepoUnits
|
||||
switch {
|
||||
case isFork:
|
||||
defaultUnits = unit.DefaultForkRepoUnits
|
||||
case repo.IsMirror:
|
||||
defaultUnits = unit.DefaultMirrorRepoUnits
|
||||
case repo.IsTemplate:
|
||||
defaultUnits = unit.DefaultTemplateRepoUnits
|
||||
}
|
||||
units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
|
||||
for _, tp := range defaultUnits {
|
||||
if tp == unit.TypeIssues {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: tp,
|
||||
Config: &repo_model.IssuesConfig{
|
||||
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
|
||||
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
|
||||
EnableDependencies: setting.Service.DefaultEnableDependencies,
|
||||
},
|
||||
})
|
||||
} else if tp == unit.TypePullRequests {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: tp,
|
||||
Config: &repo_model.PullRequestsConfig{
|
||||
AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
|
||||
DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
|
||||
AllowRebaseUpdate: true,
|
||||
},
|
||||
})
|
||||
} else if tp == unit.TypeProjects {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: tp,
|
||||
Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
|
||||
})
|
||||
} else {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: tp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, units); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remember visibility preference.
|
||||
u.LastRepoVisibility = repo.IsPrivate
|
||||
if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil {
|
||||
return fmt.Errorf("UpdateUserCols: %w", err)
|
||||
}
|
||||
|
||||
if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil {
|
||||
return fmt.Errorf("IncrUserRepoNum: %w", err)
|
||||
}
|
||||
u.NumRepos++
|
||||
|
||||
// Give access to all members in teams with access to all repositories.
|
||||
if u.IsOrganization() {
|
||||
teams, err := organization.FindOrgTeams(ctx, u.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("FindOrgTeams: %w", err)
|
||||
}
|
||||
for _, t := range teams {
|
||||
if t.IncludesAllRepositories {
|
||||
if err := addRepositoryToTeam(ctx, t, repo); err != nil {
|
||||
return fmt.Errorf("AddRepository: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil {
|
||||
return fmt.Errorf("IsUserRepoAdmin: %w", err)
|
||||
} else if !isAdmin {
|
||||
// Make creator repo admin if it wasn't assigned automatically
|
||||
if err = AddOrUpdateCollaborator(ctx, repo, doer, perm.AccessModeAdmin); err != nil {
|
||||
return fmt.Errorf("AddCollaborator: %w", err)
|
||||
}
|
||||
}
|
||||
} else if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
|
||||
// Organization automatically called this in AddRepository method.
|
||||
return fmt.Errorf("RecalculateAccesses: %w", err)
|
||||
}
|
||||
|
||||
if setting.Service.AutoWatchNewRepos {
|
||||
if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
|
||||
return fmt.Errorf("WatchRepo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil {
|
||||
return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIncludesAllRepositoriesTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testTeamRepositories := func(teamID int64, repoIDs []int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name)
|
||||
assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name)
|
||||
assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name)
|
||||
for i, rid := range repoIDs {
|
||||
if rid > 0 {
|
||||
assert.True(t, HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get an admin user.
|
||||
user, err := user_model.GetUserByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err, "GetUserByID")
|
||||
|
||||
// Create org.
|
||||
org := &organization.Organization{
|
||||
Name: "All_repo",
|
||||
IsActive: true,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, user), "CreateOrganization")
|
||||
|
||||
// Check Owner team.
|
||||
ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err, "GetOwnerTeam")
|
||||
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
|
||||
|
||||
// Create repos.
|
||||
repoIDs := make([]int64, 0)
|
||||
for i := 0; i < 3; i++ {
|
||||
r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
|
||||
assert.NoError(t, err, "CreateRepository %d", i)
|
||||
if r != nil {
|
||||
repoIDs = append(repoIDs, r.ID)
|
||||
}
|
||||
}
|
||||
// Get fresh copy of Owner team after creating repos.
|
||||
ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err, "GetOwnerTeam")
|
||||
|
||||
// Create teams and check repositories.
|
||||
teams := []*organization.Team{
|
||||
ownerTeam,
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team one",
|
||||
AccessMode: perm.AccessModeRead,
|
||||
IncludesAllRepositories: true,
|
||||
},
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team 2",
|
||||
AccessMode: perm.AccessModeRead,
|
||||
IncludesAllRepositories: false,
|
||||
},
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team three",
|
||||
AccessMode: perm.AccessModeWrite,
|
||||
IncludesAllRepositories: true,
|
||||
},
|
||||
{
|
||||
OrgID: org.ID,
|
||||
Name: "team 4",
|
||||
AccessMode: perm.AccessModeWrite,
|
||||
IncludesAllRepositories: false,
|
||||
},
|
||||
}
|
||||
teamRepos := [][]int64{
|
||||
repoIDs,
|
||||
repoIDs,
|
||||
{},
|
||||
repoIDs,
|
||||
{},
|
||||
}
|
||||
for i, team := range teams {
|
||||
if i > 0 { // first team is Owner.
|
||||
assert.NoError(t, models.NewTeam(db.DefaultContext, team), "%s: NewTeam", team.Name)
|
||||
}
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Update teams and check repositories.
|
||||
teams[3].IncludesAllRepositories = false
|
||||
teams[4].IncludesAllRepositories = true
|
||||
teamRepos[4] = repoIDs
|
||||
for i, team := range teams {
|
||||
assert.NoError(t, models.UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name)
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Create repo and check teams repositories.
|
||||
r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: "repo-last"})
|
||||
assert.NoError(t, err, "CreateRepository last")
|
||||
if r != nil {
|
||||
repoIDs = append(repoIDs, r.ID)
|
||||
}
|
||||
teamRepos[0] = repoIDs
|
||||
teamRepos[1] = repoIDs
|
||||
teamRepos[4] = repoIDs
|
||||
for i, team := range teams {
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Remove repo and check teams repositories.
|
||||
assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository")
|
||||
teamRepos[0] = repoIDs[1:]
|
||||
teamRepos[1] = repoIDs[1:]
|
||||
teamRepos[3] = repoIDs[1:3]
|
||||
teamRepos[4] = repoIDs[1:]
|
||||
for i, team := range teams {
|
||||
testTeamRepositories(team.ID, teamRepos[i])
|
||||
}
|
||||
|
||||
// Wipe created items.
|
||||
for i, rid := range repoIDs {
|
||||
if i > 0 { // first repo already deleted.
|
||||
assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i)
|
||||
}
|
||||
}
|
||||
assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
|
||||
}
|
||||
@@ -348,87 +348,6 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeRepositoryFromTeam removes a repository from a team and recalculates access
|
||||
// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted)
|
||||
func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) {
|
||||
e := db.GetEngine(ctx)
|
||||
if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.NumRepos--
|
||||
if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't need to recalculate when delete a repository from organization.
|
||||
if recalculate {
|
||||
if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
|
||||
TeamID: t.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetTeamMembers: %w", err)
|
||||
}
|
||||
for _, member := range teamMembers {
|
||||
has, err := access_model.HasAnyUnitAccess(ctx, member.ID, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all IssueWatches a user has subscribed to in the repositories
|
||||
if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasRepository returns true if given repository belong to team.
|
||||
func HasRepository(ctx context.Context, t *organization.Team, repoID int64) bool {
|
||||
return organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID)
|
||||
}
|
||||
|
||||
// RemoveRepositoryFromTeam removes repository from team of organization.
|
||||
// If the team shall include all repositories the request is ignored.
|
||||
func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID int64) error {
|
||||
if !HasRepository(ctx, t, repoID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if t.IncludesAllRepositories {
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = removeRepositoryFromTeam(ctx, t, repo, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// DeleteOwnerRepositoriesDirectly calls DeleteRepositoryDirectly for all repos of the given owner
|
||||
func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User) error {
|
||||
for {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
@@ -20,6 +21,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
|
||||
@@ -134,7 +137,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
||||
}()
|
||||
|
||||
err = db.WithTx(ctx, func(txCtx context.Context) error {
|
||||
if err = repo_module.CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
|
||||
if err = CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -247,3 +250,24 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type findForksOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
Doer *user_model.User
|
||||
}
|
||||
|
||||
func (opts findForksOptions) ToConds() builder.Cond {
|
||||
return builder.Eq{"fork_id": opts.RepoID}.And(
|
||||
repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid),
|
||||
)
|
||||
}
|
||||
|
||||
// FindForks returns all the forks of the repository
|
||||
func FindForks(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, listOptions db.ListOptions) ([]*repo_model.Repository, int64, error) {
|
||||
return db.FindAndCount[repo_model.Repository](ctx, findForksOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: repo.ID,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ func generateRepository(ctx context.Context, doer, owner *user_model.User, templ
|
||||
ObjectFormatName: templateRepo.ObjectFormatName,
|
||||
}
|
||||
|
||||
if err = repo_module.CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
|
||||
if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -318,9 +318,10 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
|
||||
}
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
TagNames: tags,
|
||||
IncludeTags: true,
|
||||
RepoID: repo.ID,
|
||||
TagNames: tags,
|
||||
IncludeDrafts: true,
|
||||
IncludeTags: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
|
||||
@@ -407,13 +408,17 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
|
||||
|
||||
newReleases = append(newReleases, rel)
|
||||
} else {
|
||||
rel.Title = parts[0]
|
||||
rel.Note = note
|
||||
rel.Sha1 = commit.ID.String()
|
||||
rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
|
||||
rel.NumCommits = commitsCount
|
||||
if rel.IsTag && author != nil {
|
||||
rel.PublisherID = author.ID
|
||||
if rel.IsTag {
|
||||
rel.Title = parts[0]
|
||||
rel.Note = note
|
||||
if author != nil {
|
||||
rel.PublisherID = author.ID
|
||||
}
|
||||
} else {
|
||||
rel.IsDraft = false
|
||||
}
|
||||
if err = repo_model.UpdateRelease(ctx, rel); err != nil {
|
||||
return fmt.Errorf("Update: %w", err)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// TeamAddRepository adds new repository to team of organization.
|
||||
func TeamAddRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) {
|
||||
if repo.OwnerID != t.OrgID {
|
||||
return errors.New("repository does not belong to organization")
|
||||
} else if organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
return addRepositoryToTeam(ctx, t, repo)
|
||||
})
|
||||
}
|
||||
|
||||
func addRepositoryToTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) {
|
||||
if err = organization.AddTeamRepo(ctx, t.OrgID, t.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = organization.IncrTeamRepoNum(ctx, t.ID); err != nil {
|
||||
return fmt.Errorf("update team: %w", err)
|
||||
}
|
||||
|
||||
t.NumRepos++
|
||||
|
||||
if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
|
||||
return fmt.Errorf("recalculateAccesses: %w", err)
|
||||
}
|
||||
|
||||
// Make all team members watch this repo if enabled in global settings
|
||||
if setting.Service.AutoWatchNewRepos {
|
||||
if err = t.LoadMembers(ctx); err != nil {
|
||||
return fmt.Errorf("getMembers: %w", err)
|
||||
}
|
||||
for _, u := range t.Members {
|
||||
if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
|
||||
return fmt.Errorf("watchRepo: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddAllRepositoriesToTeam adds all repositories to the team.
|
||||
// If the team already has some repositories they will be left unchanged.
|
||||
func AddAllRepositoriesToTeam(ctx context.Context, t *organization.Team) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
orgRepos, err := organization.GetOrgRepositories(ctx, t.OrgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get org repos: %w", err)
|
||||
}
|
||||
|
||||
for _, repo := range orgRepos {
|
||||
if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
|
||||
if err := addRepositoryToTeam(ctx, t, repo); err != nil {
|
||||
return fmt.Errorf("AddRepository: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveAllRepositoriesFromTeam removes all repositories from team and recalculates access
|
||||
func RemoveAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (err error) {
|
||||
if t.IncludesAllRepositories {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = removeAllRepositoriesFromTeam(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// removeAllRepositoriesFromTeam removes all repositories from team and recalculates access
|
||||
// Note: Shall not be called if team includes all repositories
|
||||
func removeAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (err error) {
|
||||
e := db.GetEngine(ctx)
|
||||
// Delete all accesses.
|
||||
for _, repo := range t.Repos {
|
||||
if err := access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove watches from all users and now unaccessible repos
|
||||
for _, user := range t.Members {
|
||||
has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all IssueWatches a user has subscribed to in the repositories
|
||||
if err = issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete team-repo
|
||||
if _, err := e.
|
||||
Where("team_id=?", t.ID).
|
||||
Delete(new(organization.TeamRepo)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.NumRepos = 0
|
||||
if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveRepositoryFromTeam removes repository from team of organization.
|
||||
// If the team shall include all repositories the request is ignored.
|
||||
func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID int64) error {
|
||||
if !HasRepository(ctx, t, repoID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if t.IncludesAllRepositories {
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = removeRepositoryFromTeam(ctx, t, repo, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// removeRepositoryFromTeam removes a repository from a team and recalculates access
|
||||
// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted)
|
||||
func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) {
|
||||
e := db.GetEngine(ctx)
|
||||
if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.NumRepos--
|
||||
if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't need to recalculate when delete a repository from organization.
|
||||
if recalculate {
|
||||
if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
|
||||
TeamID: t.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetTeamMembers: %w", err)
|
||||
}
|
||||
for _, member := range teamMembers {
|
||||
has, err := access_model.HasAnyUnitAccess(ctx, member.ID, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all IssueWatches a user has subscribed to in the repositories
|
||||
if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasRepository returns true if given repository belong to team.
|
||||
func HasRepository(ctx context.Context, t *organization.Team, repoID int64) bool {
|
||||
return organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
)
|
||||
|
||||
// GetReviewerTeams get all teams can be requested to review
|
||||
func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*organization.Team, error) {
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoGetReviewerTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
teams, err := GetReviewerTeams(db.DefaultContext, repo2)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, teams)
|
||||
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
teams, err = GetReviewerTeams(db.DefaultContext, repo3)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, teams, 2)
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/globallock"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
@@ -60,7 +59,7 @@ func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, rep
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
if err := models.AddRepository(ctx, team, newRepo); err != nil {
|
||||
if err := addRepositoryToTeam(ctx, team, newRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -206,7 +205,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
|
||||
}
|
||||
for _, t := range teams {
|
||||
if t.IncludesAllRepositories {
|
||||
if err := models.AddRepository(ctx, t, repo); err != nil {
|
||||
if err := addRepositoryToTeam(ctx, t, repo); err != nil {
|
||||
return fmt.Errorf("AddRepository: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -419,10 +418,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
|
||||
return err
|
||||
}
|
||||
if !hasAccess {
|
||||
if err := repo_module.AddCollaborator(ctx, repo, newOwner); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := repo_model.ChangeCollaborationAccessMode(ctx, repo, newOwner.ID, perm.AccessModeRead); err != nil {
|
||||
if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
||||
break
|
||||
}
|
||||
for _, org := range orgs {
|
||||
if err := models.RemoveOrgUser(ctx, org, u); err != nil {
|
||||
if err := org_service.RemoveOrgUser(ctx, org, u); err != nil {
|
||||
if organization.IsErrLastOrgOwner(err) {
|
||||
err = org_service.DeleteOrganization(ctx, org, true)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -44,7 +45,7 @@ func TestDeleteUser(t *testing.T) {
|
||||
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
|
||||
for _, orgUser := range orgUsers {
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
|
||||
if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
|
||||
if err := org_service.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
// limit the commit message display to just the summary, otherwise it would be hard to read
|
||||
message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 1)[0], "\r")
|
||||
message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 2)[0], "\r")
|
||||
|
||||
// a limit of 50 is set because GitHub does the same
|
||||
if utf8.RuneCountInString(message) > 50 {
|
||||
|
||||
@@ -80,12 +80,26 @@ func TestDiscordPayload(t *testing.T) {
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("PushWithLongCommitMessage", func(t *testing.T) {
|
||||
t.Run("PushWithMultilineCommitMessage", func(t *testing.T) {
|
||||
p := pushTestMultilineCommitMessagePayload()
|
||||
|
||||
pl, err := dc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1", pl.Embeds[0].Description)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("PushWithLongCommitSummary", func(t *testing.T) {
|
||||
p := pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body")
|
||||
|
||||
pl, err := dc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1", pl.Embeds[0].Description)
|
||||
|
||||
@@ -68,7 +68,7 @@ func pushTestPayload() *api.PushPayload {
|
||||
}
|
||||
|
||||
func pushTestMultilineCommitMessagePayload() *api.PushPayload {
|
||||
return pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body.")
|
||||
return pushTestPayloadWithCommitMessage("chore: This is a commit summary\n\nThis is a commit description.")
|
||||
}
|
||||
|
||||
func pushTestPayloadWithCommitMessage(message string) *api.PushPayload {
|
||||
|
||||
@@ -6,6 +6,7 @@ package webhook
|
||||
import (
|
||||
"context"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
@@ -861,6 +862,36 @@ func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_mode
|
||||
}
|
||||
}
|
||||
|
||||
func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
|
||||
apiSender := convert.ToUser(ctx, sender, nil)
|
||||
apiCommit, err := repository.ToAPIPayloadCommit(ctx, map[string]*user_model.User{}, repo.RepoPath(), repo.HTMLURL(), commit)
|
||||
if err != nil {
|
||||
log.Error("commits.ToAPIPayloadCommits failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := api.CommitStatusPayload{
|
||||
Context: status.Context,
|
||||
CreatedAt: status.CreatedUnix.AsTime().UTC(),
|
||||
Description: status.Description,
|
||||
ID: status.ID,
|
||||
SHA: commit.Sha1,
|
||||
State: status.State.String(),
|
||||
TargetURL: status.TargetURL,
|
||||
|
||||
Commit: apiCommit,
|
||||
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
Sender: apiSender,
|
||||
}
|
||||
if !status.UpdatedUnix.IsZero() {
|
||||
t := status.UpdatedUnix.AsTime().UTC()
|
||||
payload.UpdatedAt = &t
|
||||
}
|
||||
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventStatus, &payload); err != nil {
|
||||
log.Error("PrepareWebhooks: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *webhookNotifier) SyncCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
|
||||
m.CreateRef(ctx, pusher, repo, refFullName, refID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user