mirror of https://github.com/go-gitea/gitea
Support repo license (#24872)
Close #278 Close #24076 ## Solutions: - Use [google/licenseclassifier](https://github.com/google/licenseclassifier/) Test result between [google/licensecheck](https://github.com/google/licensecheck) and [go-license-detector](https://github.com/go-enry/go-license-detector): https://github.com/go-gitea/gitea/pull/24872#issuecomment-1560361167 Test result between [google/licensecheck](https://github.com/google/licensecheck) and [google/licenseclassifier](https://github.com/google/licenseclassifier/): https://github.com/go-gitea/gitea/pull/24872#issuecomment-1576092178 - Generate License Convert Name List to avoid import license templates with same contents Gitea automatically get latest license data from[ spdx/license-list-data](https://github.com/spdx/license-list-data). But unfortunately, some license templates have same contents. #20915 [click here to see the list](https://github.com/go-gitea/gitea/pull/24872#issuecomment-1584141684) So we will generate a list of these license templates with same contents and create a new file to save the result when using `make generate-license`. (Need to decide the save path) - Save License info into a new table `repo_license` Can easily support searching repo by license in the future. ## Screen shot Single License: ![image](https://github.com/go-gitea/gitea/assets/18380374/41260bd7-0b4c-4038-8592-508706cffa9f) Multiple Licenses: ![image](https://github.com/go-gitea/gitea/assets/18380374/34ce2f73-7e18-446b-9b96-ecc4fb61bd70) Triggers: - [x] Push commit to default branch - [x] Create repo - [x] Mirror repo - [x] When Default Branch is changed, licenses should be updated Todo: - [x] Save Licenses info in to DB when there's a change to license file in the commit - [x] DB Migration - [x] A nominal test? - [x] Select which library to use(https://github.com/go-gitea/gitea/pull/24872#issuecomment-1560361167) - [x] API Support - [x] Add repo license table - ~Select license in settings if there are several licenses(Not recommended)~ - License board(later, not in this PR) ![image](https://github.com/go-gitea/gitea/assets/18380374/2c3c3bf8-bcc2-4c6d-8ce0-81d1a9733878) --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Denys Konovalov <kontakt@denyskon.de> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: KN4CK3R <admin@oldschoolhack.me> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: 6543 <m.huber@kithara.com> Co-authored-by: a1012112796 <1012112796@qq.com> Co-authored-by: techknowlogick <techknowlogick@gitea.com>pull/32148/head^2
parent
f4b8f6fc40
commit
70b7df0e5e
File diff suppressed because one or more lines are too long
@ -0,0 +1,41 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package license |
||||||
|
|
||||||
|
import "strings" |
||||||
|
|
||||||
|
func GetLicenseNameFromAliases(fnl []string) string { |
||||||
|
if len(fnl) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
shortestItem := func(list []string) string { |
||||||
|
s := list[0] |
||||||
|
for _, l := range list[1:] { |
||||||
|
if len(l) < len(s) { |
||||||
|
s = l |
||||||
|
} |
||||||
|
} |
||||||
|
return s |
||||||
|
} |
||||||
|
allHasPrefix := func(list []string, s string) bool { |
||||||
|
for _, l := range list { |
||||||
|
if !strings.HasPrefix(l, s) { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
sl := shortestItem(fnl) |
||||||
|
slv := strings.Split(sl, "-") |
||||||
|
var result string |
||||||
|
for i := len(slv); i >= 0; i-- { |
||||||
|
result = strings.Join(slv[:i], "-") |
||||||
|
if allHasPrefix(fnl, result) { |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
return "" |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package license |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestGetLicenseNameFromAliases(t *testing.T) { |
||||||
|
tests := []struct { |
||||||
|
target string |
||||||
|
inputs []string |
||||||
|
}{ |
||||||
|
{ |
||||||
|
// real case which you can find in license-aliases.json
|
||||||
|
target: "AGPL-1.0", |
||||||
|
inputs: []string{ |
||||||
|
"AGPL-1.0-only", |
||||||
|
"AGPL-1.0-or-late", |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
target: "", |
||||||
|
inputs: []string{ |
||||||
|
"APSL-1.0", |
||||||
|
"AGPL-1.0-only", |
||||||
|
"AGPL-1.0-or-late", |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, tt := range tests { |
||||||
|
result := GetLicenseNameFromAliases(tt.inputs) |
||||||
|
assert.Equal(t, result, tt.target) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
[] # empty |
@ -0,0 +1,23 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
|
||||||
|
"xorm.io/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
func AddRepositoryLicenses(x *xorm.Engine) error { |
||||||
|
type RepoLicense struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
RepoID int64 `xorm:"UNIQUE(s) NOT NULL"` |
||||||
|
CommitID string |
||||||
|
License string `xorm:"VARCHAR(255) UNIQUE(s) NOT NULL"` |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` |
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"` |
||||||
|
} |
||||||
|
|
||||||
|
return x.Sync(new(RepoLicense)) |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
db.RegisterModel(new(RepoLicense)) |
||||||
|
} |
||||||
|
|
||||||
|
type RepoLicense struct { //revive:disable-line:exported
|
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
RepoID int64 `xorm:"UNIQUE(s) NOT NULL"` |
||||||
|
CommitID string |
||||||
|
License string `xorm:"VARCHAR(255) UNIQUE(s) NOT NULL"` |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` |
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"` |
||||||
|
} |
||||||
|
|
||||||
|
// RepoLicenseList defines a list of repo licenses
|
||||||
|
type RepoLicenseList []*RepoLicense //revive:disable-line:exported
|
||||||
|
|
||||||
|
func (rll RepoLicenseList) StringList() []string { |
||||||
|
var licenses []string |
||||||
|
for _, rl := range rll { |
||||||
|
licenses = append(licenses, rl.License) |
||||||
|
} |
||||||
|
return licenses |
||||||
|
} |
||||||
|
|
||||||
|
// GetRepoLicenses returns the license statistics for a repository
|
||||||
|
func GetRepoLicenses(ctx context.Context, repo *Repository) (RepoLicenseList, error) { |
||||||
|
licenses := make(RepoLicenseList, 0) |
||||||
|
if err := db.GetEngine(ctx).Where("`repo_id` = ?", repo.ID).Asc("`license`").Find(&licenses); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return licenses, nil |
||||||
|
} |
||||||
|
|
||||||
|
// UpdateRepoLicenses updates the license statistics for repository
|
||||||
|
func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string, licenses []string) error { |
||||||
|
oldLicenses, err := GetRepoLicenses(ctx, repo) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, license := range licenses { |
||||||
|
upd := false |
||||||
|
for _, o := range oldLicenses { |
||||||
|
// Update already existing license
|
||||||
|
if o.License == license { |
||||||
|
if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
upd = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
// Insert new license
|
||||||
|
if !upd { |
||||||
|
if err := db.Insert(ctx, &RepoLicense{ |
||||||
|
RepoID: repo.ID, |
||||||
|
CommitID: commitID, |
||||||
|
License: license, |
||||||
|
}); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// Delete old licenses
|
||||||
|
licenseToDelete := make([]int64, 0, len(oldLicenses)) |
||||||
|
for _, o := range oldLicenses { |
||||||
|
if o.CommitID != commitID { |
||||||
|
licenseToDelete = append(licenseToDelete, o.ID) |
||||||
|
} |
||||||
|
} |
||||||
|
if len(licenseToDelete) > 0 { |
||||||
|
if _, err := db.GetEngine(ctx).In("`id`", licenseToDelete).Delete(&RepoLicense{}); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// CopyLicense Copy originalRepo license information to destRepo (use for forked repo)
|
||||||
|
func CopyLicense(ctx context.Context, originalRepo, destRepo *Repository) error { |
||||||
|
repoLicenses, err := GetRepoLicenses(ctx, originalRepo) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if len(repoLicenses) > 0 { |
||||||
|
newRepoLicenses := make(RepoLicenseList, 0, len(repoLicenses)) |
||||||
|
|
||||||
|
for _, rl := range repoLicenses { |
||||||
|
newRepoLicense := &RepoLicense{ |
||||||
|
RepoID: destRepo.ID, |
||||||
|
CommitID: rl.CommitID, |
||||||
|
License: rl.License, |
||||||
|
} |
||||||
|
newRepoLicenses = append(newRepoLicenses, newRepoLicense) |
||||||
|
} |
||||||
|
if err := db.Insert(ctx, &newRepoLicenses); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// CleanRepoLicenses will remove all license record of the repo
|
||||||
|
func CleanRepoLicenses(ctx context.Context, repo *Repository) error { |
||||||
|
return db.DeleteBeans(ctx, &RepoLicense{ |
||||||
|
RepoID: repo.ID, |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
{"AGPL-1.0-only":"AGPL-1.0","AGPL-1.0-or-later":"AGPL-1.0","AGPL-3.0-only":"AGPL-3.0","AGPL-3.0-or-later":"AGPL-3.0","CAL-1.0":"CAL-1.0","CAL-1.0-Combined-Work-Exception":"CAL-1.0","GFDL-1.1-invariants-only":"GFDL-1.1","GFDL-1.1-invariants-or-later":"GFDL-1.1","GFDL-1.1-no-invariants-only":"GFDL-1.1","GFDL-1.1-no-invariants-or-later":"GFDL-1.1","GFDL-1.1-only":"GFDL-1.1","GFDL-1.1-or-later":"GFDL-1.1","GFDL-1.2-invariants-only":"GFDL-1.2","GFDL-1.2-invariants-or-later":"GFDL-1.2","GFDL-1.2-no-invariants-only":"GFDL-1.2","GFDL-1.2-no-invariants-or-later":"GFDL-1.2","GFDL-1.2-only":"GFDL-1.2","GFDL-1.2-or-later":"GFDL-1.2","GFDL-1.3-invariants-only":"GFDL-1.3","GFDL-1.3-invariants-or-later":"GFDL-1.3","GFDL-1.3-no-invariants-only":"GFDL-1.3","GFDL-1.3-no-invariants-or-later":"GFDL-1.3","GFDL-1.3-only":"GFDL-1.3","GFDL-1.3-or-later":"GFDL-1.3","GPL-1.0-only":"GPL-1.0","GPL-1.0-or-later":"GPL-1.0","GPL-2.0-only":"GPL-2.0","GPL-2.0-or-later":"GPL-2.0","GPL-3.0-only":"GPL-3.0","GPL-3.0-or-later":"GPL-3.0","LGPL-2.0-only":"LGPL-2.0","LGPL-2.0-or-later":"LGPL-2.0","LGPL-2.1-only":"LGPL-2.1","LGPL-2.1-or-later":"LGPL-2.1","LGPL-3.0-only":"LGPL-3.0","LGPL-3.0-or-later":"LGPL-3.0","MPL-2.0":"MPL-2.0","MPL-2.0-no-copyleft-exception":"MPL-2.0","OFL-1.0":"OFL-1.0","OFL-1.0-RFN":"OFL-1.0","OFL-1.0-no-RFN":"OFL-1.0","OFL-1.1":"OFL-1.1","OFL-1.1-RFN":"OFL-1.1","OFL-1.1-no-RFN":"OFL-1.1"} |
@ -0,0 +1,51 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
) |
||||||
|
|
||||||
|
// GetLicenses returns licenses
|
||||||
|
func GetLicenses(ctx *context.APIContext) { |
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/licenses repository repoGetLicenses
|
||||||
|
// ---
|
||||||
|
// summary: Get repo licenses
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/LicensesList"
|
||||||
|
|
||||||
|
licenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository) |
||||||
|
if err != nil { |
||||||
|
log.Error("GetRepoLicenses failed: %v", err) |
||||||
|
ctx.InternalServerError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
resp := make([]string, len(licenses)) |
||||||
|
for i := range licenses { |
||||||
|
resp[i] = licenses[i].License |
||||||
|
} |
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, resp) |
||||||
|
} |
@ -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]) |
||||||
|
}) |
||||||
|
} |
Binary file not shown.
Binary file not shown.
@ -1 +1 @@ |
|||||||
65f1bf27bc3bf70f64657658635e66094edbcb4d |
90c1019714259b24fb81711d4416ac0f18667dfa |
||||||
|
@ -0,0 +1,80 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth" |
||||||
|
api "code.gitea.io/gitea/modules/structs" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
var testLicenseContent = ` |
||||||
|
Copyright (c) 2024 Gitea
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: |
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. |
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
` |
||||||
|
|
||||||
|
func TestAPIRepoLicense(t *testing.T) { |
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) { |
||||||
|
session := loginUser(t, "user2") |
||||||
|
|
||||||
|
// Request editor page
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/_new/master/") |
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
doc := NewHTMLParser(t, resp.Body) |
||||||
|
lastCommit := doc.GetInputValueByName("last_commit") |
||||||
|
assert.NotEmpty(t, lastCommit) |
||||||
|
|
||||||
|
// Save new file to master branch
|
||||||
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{ |
||||||
|
"_csrf": doc.GetCSRF(), |
||||||
|
"last_commit": lastCommit, |
||||||
|
"tree_path": "LICENSE", |
||||||
|
"content": testLicenseContent, |
||||||
|
"commit_choice": "direct", |
||||||
|
}) |
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther) |
||||||
|
|
||||||
|
// let gitea update repo license
|
||||||
|
time.Sleep(time.Second) |
||||||
|
checkRepoLicense(t, "user2", "repo1", []string{"BSD-2-Clause"}) |
||||||
|
|
||||||
|
// Change default branch
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) |
||||||
|
branchName := "DefaultBranch" |
||||||
|
req = NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1", api.EditRepoOption{ |
||||||
|
DefaultBranch: &branchName, |
||||||
|
}).AddTokenAuth(token) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// let gitea update repo license
|
||||||
|
time.Sleep(time.Second) |
||||||
|
checkRepoLicense(t, "user2", "repo1", []string{"MIT"}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func checkRepoLicense(t *testing.T, owner, repo string, expected []string) { |
||||||
|
reqURL := fmt.Sprintf("/api/v1/repos/%s/%s/licenses", owner, repo) |
||||||
|
req := NewRequest(t, "GET", reqURL) |
||||||
|
resp := MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
var licenses []string |
||||||
|
DecodeJSON(t, resp, &licenses) |
||||||
|
|
||||||
|
assert.ElementsMatch(t, expected, licenses, 0) |
||||||
|
} |
Loading…
Reference in new issue