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:
@@ -17,7 +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/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/oauth2_provider"
|
||||
)
|
||||
|
||||
// Ensure the struct implements the interface.
|
||||
@@ -31,7 +31,7 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
|
||||
if !strings.Contains(accessToken, ".") {
|
||||
return 0
|
||||
}
|
||||
token, err := oauth2.ParseToken(accessToken, oauth2.DefaultSigningKey)
|
||||
token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
|
||||
if err != nil {
|
||||
log.Trace("oauth2.ParseToken: %v", err)
|
||||
return 0
|
||||
@@ -40,7 +40,7 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
|
||||
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
|
||||
return 0
|
||||
}
|
||||
if token.Type != oauth2.TypeAccessToken {
|
||||
if token.Kind != oauth2_provider.KindAccessToken {
|
||||
return 0
|
||||
}
|
||||
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
|
||||
|
||||
@@ -30,10 +30,6 @@ const ProviderHeaderKey = "gitea-oauth2-provider"
|
||||
|
||||
// Init initializes the oauth source
|
||||
func Init(ctx context.Context) error {
|
||||
if err := InitSigningKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lock our mutex
|
||||
gothRWMutex.Lock()
|
||||
|
||||
|
||||
@@ -404,6 +404,13 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
ctx.Data["PushMirrors"] = pushMirrors
|
||||
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
||||
|
||||
repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoLicenses", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList()
|
||||
}
|
||||
|
||||
// RepoAssignment returns a middleware to handle repository assignment
|
||||
|
||||
@@ -175,6 +175,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
language = repo.PrimaryLanguage.Language
|
||||
}
|
||||
|
||||
repoLicenses, err := repo_model.GetRepoLicenses(ctx, repo)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
repoAPIURL := repo.APIURL()
|
||||
|
||||
return &api.Repository{
|
||||
@@ -238,6 +243,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
RepoTransfer: transfer,
|
||||
Topics: repo.Topics,
|
||||
ObjectFormatName: repo.ObjectFormatName,
|
||||
Licenses: repoLicenses.StringList(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,16 @@ func registerCleanupPackages() {
|
||||
})
|
||||
}
|
||||
|
||||
func registerSyncRepoLicenses() {
|
||||
RegisterTaskFatal("sync_repo_licenses", &BaseConfig{
|
||||
Enabled: false,
|
||||
RunAtStart: false,
|
||||
Schedule: "@annually",
|
||||
}, func(ctx context.Context, _ *user_model.User, config Config) error {
|
||||
return repo_service.SyncRepoLicenses(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func initBasicTasks() {
|
||||
if setting.Mirror.Enabled {
|
||||
registerUpdateMirrorTask()
|
||||
@@ -172,4 +182,5 @@ func initBasicTasks() {
|
||||
if setting.Packages.Enabled {
|
||||
registerCleanupPackages()
|
||||
}
|
||||
registerSyncRepoLicenses()
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -302,6 +303,8 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
toRepoName := "migrated"
|
||||
uploader := NewGiteaLocalUploader(context.Background(), fromRepoOwner, fromRepoOwner.Name, toRepoName)
|
||||
uploader.gitServiceType = structs.GiteaService
|
||||
|
||||
assert.NoError(t, repo_service.Init(context.Background()))
|
||||
assert.NoError(t, uploader.CreateRepo(&base.Repository{
|
||||
Description: "description",
|
||||
OriginalURL: fromRepo.RepoPath(),
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
// gitShortEmptySha Git short empty SHA
|
||||
@@ -559,6 +560,14 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Update License
|
||||
if err = repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: m.Repo.ID,
|
||||
}); err != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: unable to add repo to license updater queue: %v", m.Repo, err)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)
|
||||
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2_provider //nolint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
auth "code.gitea.io/gitea/models/auth"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
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/timeutil"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// AccessTokenErrorCode represents an error code specified in RFC 6749
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
type AccessTokenErrorCode string
|
||||
|
||||
const (
|
||||
// AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
|
||||
AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
|
||||
// AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
|
||||
AccessTokenErrorCodeInvalidClient = "invalid_client"
|
||||
// AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
|
||||
AccessTokenErrorCodeInvalidGrant = "invalid_grant"
|
||||
// AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
|
||||
AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
|
||||
// AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
|
||||
AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
|
||||
// AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
|
||||
AccessTokenErrorCodeInvalidScope = "invalid_scope"
|
||||
)
|
||||
|
||||
// AccessTokenError represents an error response specified in RFC 6749
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
type AccessTokenError struct {
|
||||
ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
// Error returns the error message
|
||||
func (err AccessTokenError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
|
||||
}
|
||||
|
||||
// TokenType specifies the kind of token
|
||||
type TokenType string
|
||||
|
||||
const (
|
||||
// TokenTypeBearer represents a token type specified in RFC 6749
|
||||
TokenTypeBearer TokenType = "bearer"
|
||||
// TokenTypeMAC represents a token type specified in RFC 6749
|
||||
TokenTypeMAC = "mac"
|
||||
)
|
||||
|
||||
// AccessTokenResponse represents a successful access token response
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
|
||||
type AccessTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType TokenType `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
}
|
||||
|
||||
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
|
||||
if setting.OAuth2.InvalidateRefreshTokens {
|
||||
if err := grant.IncreaseCounter(ctx); err != nil {
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidGrant,
|
||||
ErrorDescription: "cannot increase the grant counter",
|
||||
}
|
||||
}
|
||||
}
|
||||
// generate access token to access the API
|
||||
expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
|
||||
accessToken := &Token{
|
||||
GrantID: grant.ID,
|
||||
Kind: KindAccessToken,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
|
||||
},
|
||||
}
|
||||
signedAccessToken, err := accessToken.SignToken(serverKey)
|
||||
if err != nil {
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot sign token",
|
||||
}
|
||||
}
|
||||
|
||||
// generate refresh token to request an access token after it expired later
|
||||
refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime()
|
||||
refreshToken := &Token{
|
||||
GrantID: grant.ID,
|
||||
Counter: grant.Counter,
|
||||
Kind: KindRefreshToken,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(refreshExpirationDate),
|
||||
},
|
||||
}
|
||||
signedRefreshToken, err := refreshToken.SignToken(serverKey)
|
||||
if err != nil {
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot sign token",
|
||||
}
|
||||
}
|
||||
|
||||
// generate OpenID Connect id_token
|
||||
signedIDToken := ""
|
||||
if grant.ScopeContains("openid") {
|
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
|
||||
if err != nil {
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot find application",
|
||||
}
|
||||
}
|
||||
user, err := user_model.GetUserByID(ctx, grant.UserID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot find user",
|
||||
}
|
||||
}
|
||||
log.Error("Error loading user: %v", err)
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "server error",
|
||||
}
|
||||
}
|
||||
|
||||
idToken := &OIDCToken{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
|
||||
Issuer: setting.AppURL,
|
||||
Audience: []string{app.ClientID},
|
||||
Subject: fmt.Sprint(grant.UserID),
|
||||
},
|
||||
Nonce: grant.Nonce,
|
||||
}
|
||||
if grant.ScopeContains("profile") {
|
||||
idToken.Name = user.GetDisplayName()
|
||||
idToken.PreferredUsername = user.Name
|
||||
idToken.Profile = user.HTMLURL()
|
||||
idToken.Picture = user.AvatarLink(ctx)
|
||||
idToken.Website = user.Website
|
||||
idToken.Locale = user.Language
|
||||
idToken.UpdatedAt = user.UpdatedUnix
|
||||
}
|
||||
if grant.ScopeContains("email") {
|
||||
idToken.Email = user.Email
|
||||
idToken.EmailVerified = user.IsActive
|
||||
}
|
||||
if grant.ScopeContains("groups") {
|
||||
groups, err := GetOAuthGroupsForUser(ctx, user)
|
||||
if err != nil {
|
||||
log.Error("Error getting groups: %v", err)
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "server error",
|
||||
}
|
||||
}
|
||||
idToken.Groups = groups
|
||||
}
|
||||
|
||||
signedIDToken, err = idToken.SignToken(clientKey)
|
||||
if err != nil {
|
||||
return nil, &AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot sign token",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &AccessTokenResponse{
|
||||
AccessToken: signedAccessToken,
|
||||
TokenType: TokenTypeBearer,
|
||||
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
|
||||
RefreshToken: signedRefreshToken,
|
||||
IDToken: signedIDToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserOrgList: %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 {
|
||||
if team.IsMember(ctx, user.ID) {
|
||||
groups = append(groups, org.Name+":"+team.LowerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2_provider //nolint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Init initializes the oauth source
|
||||
func Init(ctx context.Context) error {
|
||||
if !setting.OAuth2.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return InitSigningKey()
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2
|
||||
package oauth2_provider //nolint
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2
|
||||
package oauth2_provider //nolint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,29 +12,22 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// ___________ __
|
||||
// \__ ___/___ | | __ ____ ____
|
||||
// | | / _ \| |/ // __ \ / \
|
||||
// | |( <_> ) <\ ___/| | \
|
||||
// |____| \____/|__|_ \\___ >___| /
|
||||
// \/ \/ \/
|
||||
|
||||
// Token represents an Oauth grant
|
||||
|
||||
// TokenType represents the type of token for an oauth application
|
||||
type TokenType int
|
||||
// TokenKind represents the type of token for an oauth application
|
||||
type TokenKind int
|
||||
|
||||
const (
|
||||
// TypeAccessToken is a token with short lifetime to access the api
|
||||
TypeAccessToken TokenType = 0
|
||||
// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
|
||||
TypeRefreshToken = iota
|
||||
// KindAccessToken is a token with short lifetime to access the api
|
||||
KindAccessToken TokenKind = 0
|
||||
// KindRefreshToken is token with long lifetime to refresh access tokens obtained by the client
|
||||
KindRefreshToken = iota
|
||||
)
|
||||
|
||||
// Token represents a JWT token used to authenticate a client
|
||||
type Token struct {
|
||||
GrantID int64 `json:"gnt"`
|
||||
Type TokenType `json:"tt"`
|
||||
Kind TokenKind `json:"tt"`
|
||||
Counter int64 `json:"cnt,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@@ -1013,6 +1013,8 @@ type CommitInfo struct {
|
||||
}
|
||||
|
||||
// GetPullCommits returns all commits on given pull request and the last review commit sha
|
||||
// Attention: The last review commit sha must be from the latest review whose commit id is not empty.
|
||||
// So the type of the latest review cannot be "ReviewTypeRequest".
|
||||
func GetPullCommits(ctx *gitea_context.Context, issue *issues_model.Issue) ([]CommitInfo, string, error) {
|
||||
pull := issue.PullRequest
|
||||
|
||||
@@ -1058,7 +1060,11 @@ func GetPullCommits(ctx *gitea_context.Context, issue *issues_model.Issue) ([]Co
|
||||
lastreview, err := issues_model.FindLatestReviews(ctx, issues_model.FindReviewOptions{
|
||||
IssueID: issue.ID,
|
||||
ReviewerID: ctx.Doer.ID,
|
||||
Type: issues_model.ReviewTypeUnknown,
|
||||
Types: []issues_model.ReviewType{
|
||||
issues_model.ReviewTypeApprove,
|
||||
issues_model.ReviewTypeComment,
|
||||
issues_model.ReviewTypeReject,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||
|
||||
@@ -348,7 +348,7 @@ func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *is
|
||||
reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
IssueID: pull.IssueID,
|
||||
Type: issues_model.ReviewTypeApprove,
|
||||
Types: []issues_model.ReviewType{issues_model.ReviewTypeApprove},
|
||||
Dismissed: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -616,6 +616,14 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
|
||||
return err
|
||||
}
|
||||
|
||||
if !repo.IsEmpty {
|
||||
if err := AddRepoToLicenseUpdaterQueue(&LicenseUpdaterOptions{
|
||||
RepoID: repo.ID,
|
||||
}); err != nil {
|
||||
log.Error("AddRepoToLicenseUpdaterQueue: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
notify_service.ChangeDefaultBranch(ctx, repo)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -303,6 +303,25 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
|
||||
rollbackRepo.OwnerID = u.ID
|
||||
return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
|
||||
}
|
||||
|
||||
// update licenses
|
||||
var licenses []string
|
||||
if len(opts.License) > 0 {
|
||||
licenses = append(licenses, ConvertLicenseName(opts.License))
|
||||
|
||||
stdout, _, err := git.NewCommand(ctx, "rev-parse", "HEAD").
|
||||
SetDescription(fmt.Sprintf("CreateRepository(git rev-parse HEAD): %s", repoPath)).
|
||||
RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
log.Error("CreateRepository(git rev-parse HEAD) in %v: Stdout: %s\nError: %v", repo, stdout, err)
|
||||
rollbackRepo = repo
|
||||
rollbackRepo.OwnerID = u.ID
|
||||
return fmt.Errorf("CreateRepository(git rev-parse HEAD): %w", err)
|
||||
}
|
||||
if err := repo_model.UpdateRepoLicenses(ctx, repo, stdout, licenses); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
if rollbackRepo != nil {
|
||||
|
||||
@@ -140,6 +140,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
||||
&git_model.Branch{RepoID: repoID},
|
||||
&git_model.LFSLock{RepoID: repoID},
|
||||
&repo_model.LanguageStat{RepoID: repoID},
|
||||
&repo_model.RepoLicense{RepoID: repoID},
|
||||
&issues_model.Milestone{RepoID: repoID},
|
||||
&repo_model.Mirror{RepoID: repoID},
|
||||
&activities_model.Notification{RepoID: repoID},
|
||||
|
||||
@@ -198,6 +198,9 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
||||
if err := repo_model.CopyLanguageStat(ctx, opts.BaseRepo, repo); err != nil {
|
||||
log.Error("Copy language stat from oldRepo failed: %v", err)
|
||||
}
|
||||
if err := repo_model.CopyLicense(ctx, opts.BaseRepo, repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
|
||||
licenseclassifier "github.com/google/licenseclassifier/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
classifier *licenseclassifier.Classifier
|
||||
LicenseFileName = "LICENSE"
|
||||
licenseAliases map[string]string
|
||||
|
||||
// licenseUpdaterQueue represents a queue to handle update repo licenses
|
||||
licenseUpdaterQueue *queue.WorkerPoolQueue[*LicenseUpdaterOptions]
|
||||
)
|
||||
|
||||
func AddRepoToLicenseUpdaterQueue(opts *LicenseUpdaterOptions) error {
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
return licenseUpdaterQueue.Push(opts)
|
||||
}
|
||||
|
||||
func loadLicenseAliases() error {
|
||||
if licenseAliases != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := options.AssetFS().ReadFile("license", "etc", "license-aliases.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(data, &licenseAliases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConvertLicenseName(name string) string {
|
||||
if err := loadLicenseAliases(); err != nil {
|
||||
return name
|
||||
}
|
||||
|
||||
v, ok := licenseAliases[name]
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func InitLicenseClassifier() error {
|
||||
// threshold should be 0.84~0.86 or the test will be failed
|
||||
classifier = licenseclassifier.NewClassifier(.85)
|
||||
licenseFiles, err := options.AssetFS().ListFiles("license", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existLicense := make(container.Set[string])
|
||||
if len(licenseFiles) > 0 {
|
||||
for _, licenseFile := range licenseFiles {
|
||||
licenseName := ConvertLicenseName(licenseFile)
|
||||
if existLicense.Contains(licenseName) {
|
||||
continue
|
||||
}
|
||||
existLicense.Add(licenseName)
|
||||
data, err := options.License(licenseFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
classifier.AddContent("License", licenseFile, licenseName, data)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LicenseUpdaterOptions struct {
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
func repoLicenseUpdater(items ...*LicenseUpdaterOptions) []*LicenseUpdaterOptions {
|
||||
ctx := graceful.GetManager().ShutdownContext()
|
||||
|
||||
for _, opts := range items {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, opts.RepoID)
|
||||
if err != nil {
|
||||
log.Error("repoLicenseUpdater [%d] failed: GetRepositoryByID: %v", opts.RepoID, err)
|
||||
continue
|
||||
}
|
||||
if repo.IsEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("repoLicenseUpdater [%d] failed: OpenRepository: %v", opts.RepoID, err)
|
||||
continue
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||
if err != nil {
|
||||
log.Error("repoLicenseUpdater [%d] failed: GetBranchCommit: %v", opts.RepoID, err)
|
||||
continue
|
||||
}
|
||||
if err = UpdateRepoLicenses(ctx, repo, commit); err != nil {
|
||||
log.Error("repoLicenseUpdater [%d] failed: updateRepoLicenses: %v", opts.RepoID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncRepoLicenses(ctx context.Context) error {
|
||||
log.Trace("Doing: SyncRepoLicenses")
|
||||
|
||||
if err := db.Iterate(
|
||||
ctx,
|
||||
nil,
|
||||
func(ctx context.Context, repo *repo_model.Repository) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return db.ErrCancelledf("before sync repo licenses for %s", repo.FullName())
|
||||
default:
|
||||
}
|
||||
return AddRepoToLicenseUpdaterQueue(&LicenseUpdaterOptions{RepoID: repo.ID})
|
||||
},
|
||||
); err != nil {
|
||||
log.Trace("Error: SyncRepoLicenses: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Trace("Finished: SyncReposLicenses")
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRepoLicenses will update repository licenses col if license file exists
|
||||
func UpdateRepoLicenses(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) error {
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := commit.GetBlobByPath(LicenseFileName)
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
return fmt.Errorf("GetBlobByPath: %w", err)
|
||||
}
|
||||
|
||||
if git.IsErrNotExist(err) {
|
||||
return repo_model.CleanRepoLicenses(ctx, repo)
|
||||
}
|
||||
|
||||
licenses := make([]string, 0)
|
||||
if b != nil {
|
||||
r, err := b.DataAsync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
licenses, err = detectLicense(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detectLicense: %w", err)
|
||||
}
|
||||
}
|
||||
return repo_model.UpdateRepoLicenses(ctx, repo, commit.ID.String(), licenses)
|
||||
}
|
||||
|
||||
// detectLicense returns the licenses detected by the given content buff
|
||||
func detectLicense(r io.Reader) ([]string, error) {
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
matches, err := classifier.MatchFrom(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(matches.Matches) > 0 {
|
||||
results := make(container.Set[string], len(matches.Matches))
|
||||
for _, r := range matches.Matches {
|
||||
if r.MatchType == "License" && !results.Contains(r.Variant) {
|
||||
results.Add(r.Variant)
|
||||
}
|
||||
}
|
||||
return results.Values(), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_detectLicense(t *testing.T) {
|
||||
type DetectLicenseTest struct {
|
||||
name string
|
||||
arg string
|
||||
want []string
|
||||
}
|
||||
|
||||
tests := []DetectLicenseTest{
|
||||
{
|
||||
name: "empty",
|
||||
arg: "",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no detected license",
|
||||
arg: "Copyright (c) 2023 Gitea",
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
repo_module.LoadRepoConfig()
|
||||
err := loadLicenseAliases()
|
||||
assert.NoError(t, err)
|
||||
for _, licenseName := range repo_module.Licenses {
|
||||
license, err := repo_module.GetLicense(licenseName, &repo_module.LicenseValues{
|
||||
Owner: "Gitea",
|
||||
Email: "teabot@gitea.io",
|
||||
Repo: "gitea",
|
||||
Year: "2024",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests = append(tests, DetectLicenseTest{
|
||||
name: fmt.Sprintf("single license test: %s", licenseName),
|
||||
arg: string(license),
|
||||
want: []string{ConvertLicenseName(licenseName)},
|
||||
})
|
||||
}
|
||||
|
||||
err = InitLicenseClassifier()
|
||||
assert.NoError(t, err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
license, err := detectLicense(strings.NewReader(tt.arg))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, license)
|
||||
})
|
||||
}
|
||||
|
||||
result, err := detectLicense(strings.NewReader(tests[2].arg + tests[3].arg + tests[4].arg))
|
||||
assert.NoError(t, err)
|
||||
t.Run("multiple licenses test", func(t *testing.T) {
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Contains(t, result, tests[2].want[0])
|
||||
assert.Contains(t, result, tests[3].want[0])
|
||||
assert.Contains(t, result, tests[4].want[0])
|
||||
})
|
||||
}
|
||||
@@ -172,6 +172,11 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
||||
return repo, fmt.Errorf("StoreMissingLfsObjectsInRepository: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update repo license
|
||||
if err := AddRepoToLicenseUpdaterQueue(&LicenseUpdaterOptions{RepoID: repo.ID}); err != nil {
|
||||
log.Error("Failed to add repo to license updater queue: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
@@ -96,6 +97,12 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN
|
||||
|
||||
// Init start repository service
|
||||
func Init(ctx context.Context) error {
|
||||
licenseUpdaterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "repo_license_updater", repoLicenseUpdater)
|
||||
if licenseUpdaterQueue == nil {
|
||||
return fmt.Errorf("unable to create repo_license_updater queue")
|
||||
}
|
||||
go graceful.GetManager().RunWithCancel(licenseUpdaterQueue)
|
||||
|
||||
if err := repo_module.LoadRepoConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user