mirror of
https://github.com/go-gitea/gitea
synced 2024-09-27 00:37:49 +02:00
Merge 47e7c5869e
into 55d5a74bb3
This commit is contained in:
commit
0113e6c55b
5
assets/go-licenses.json
generated
5
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@ -5,6 +5,8 @@ package main
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -15,19 +17,22 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
prefix = "gitea-licenses"
|
||||
url = "https://api.github.com/repos/spdx/license-list-data/tarball"
|
||||
githubApiToken = ""
|
||||
githubUsername = ""
|
||||
destination = ""
|
||||
prefix = "gitea-licenses"
|
||||
url = "https://api.github.com/repos/spdx/license-list-data/tarball"
|
||||
githubApiToken = ""
|
||||
githubUsername = ""
|
||||
destination = ""
|
||||
licenseAliasesDestination = ""
|
||||
)
|
||||
|
||||
flag.StringVar(&destination, "dest", "options/license/", "destination for the licenses")
|
||||
flag.StringVar(&licenseAliasesDestination, "aliasesdest", "options/license-aliases.json", "destination for same license json")
|
||||
flag.StringVar(&githubUsername, "username", "", "github username")
|
||||
flag.StringVar(&githubApiToken, "token", "", "github api token")
|
||||
flag.Parse()
|
||||
@ -77,7 +82,7 @@ func main() {
|
||||
}
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
|
||||
aliasesFiles := make(map[string][]string)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
|
||||
@ -97,26 +102,97 @@ func main() {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filepath.Base(hdr.Name), "README") {
|
||||
fileBaseName := filepath.Base(hdr.Name)
|
||||
licenseName := strings.TrimSuffix(fileBaseName, ".txt")
|
||||
|
||||
if strings.HasPrefix(fileBaseName, "README") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filepath.Base(hdr.Name), "deprecated_") {
|
||||
if strings.HasPrefix(fileBaseName, "deprecated_") {
|
||||
continue
|
||||
}
|
||||
out, err := os.Create(path.Join(destination, strings.TrimSuffix(filepath.Base(hdr.Name), ".txt")))
|
||||
out, err := os.Create(path.Join(destination, licenseName))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create new file. %s", err)
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, tr); err != nil {
|
||||
// some license files have same content, so we need to detect these files and create a convert map into a json file
|
||||
// Later we use this convert map to avoid adding same license content with different license name
|
||||
h := md5.New()
|
||||
// calculate md5 and write file in the same time
|
||||
r := io.TeeReader(tr, h)
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
log.Fatalf("Failed to write new file. %s", err)
|
||||
} else {
|
||||
fmt.Printf("Written %s\n", out.Name())
|
||||
|
||||
md5 := hex.EncodeToString(h.Sum(nil))
|
||||
aliasesFiles[md5] = append(aliasesFiles[md5], licenseName)
|
||||
}
|
||||
}
|
||||
|
||||
// generate convert license name map
|
||||
licenseAliases := make(map[string]string)
|
||||
for _, fileNames := range aliasesFiles {
|
||||
if len(fileNames) > 1 {
|
||||
licenseName := getLicenseNameFromAliases(fileNames)
|
||||
for _, fileName := range fileNames {
|
||||
licenseAliases[fileName] = licenseName
|
||||
}
|
||||
}
|
||||
}
|
||||
// save convert license name map to file
|
||||
b, err := json.Marshal(licenseAliases)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create json bytes. %s", err)
|
||||
return
|
||||
}
|
||||
f, err := os.Create(licenseAliasesDestination)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create license aliases json file. %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err = f.Write(b); err != nil {
|
||||
log.Fatalf("Failed to write license aliases json file. %s", err)
|
||||
}
|
||||
|
||||
fmt.Println("Done")
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -67,6 +67,7 @@ require (
|
||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/go-github/v61 v61.0.0
|
||||
github.com/google/licenseclassifier/v2 v2.0.0
|
||||
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
|
3
go.sum
3
go.sum
@ -435,6 +435,8 @@ github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA=
|
||||
github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 h1:ASJ/LAqdCHOyMYI+dwNxn7Rd8FscNkMyTr1KZU1JI/M=
|
||||
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
@ -729,6 +731,7 @@ github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jN
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
|
1
models/fixtures/repo_license.yml
Normal file
1
models/fixtures/repo_license.yml
Normal file
@ -0,0 +1 @@
|
||||
[] # empty
|
@ -26,7 +26,7 @@
|
||||
fork_id: 0
|
||||
is_template: false
|
||||
template_id: 0
|
||||
size: 7597
|
||||
size: 8478
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
|
||||
|
@ -500,7 +500,7 @@ var migrations = []Migration{
|
||||
// v259 -> v260
|
||||
NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens),
|
||||
|
||||
// Gitea 1.20.0 ends at 260
|
||||
// Gitea 1.20.0 ends at v260
|
||||
|
||||
// v260 -> v261
|
||||
NewMigration("Drop custom_labels column of action_runner table", v1_21.DropCustomLabelsColumnOfActionRunner),
|
||||
@ -601,6 +601,8 @@ var migrations = []Migration{
|
||||
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
|
||||
// v304 -> v305
|
||||
NewMigration("Add index for release sha1", v1_23.AddIndexForReleaseSha1),
|
||||
// v305 -> v306
|
||||
NewMigration("Add Repository Licenses", v1_23.AddRepositoryLicenses),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
23
models/migrations/v1_23/v305.go
Normal file
23
models/migrations/v1_23/v305.go
Normal file
@ -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) INDEX NOT NULL"`
|
||||
CommitID string
|
||||
License string `xorm:"VARCHAR(50) UNIQUE(s) INDEX NOT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"`
|
||||
}
|
||||
|
||||
return x.Sync(new(RepoLicense))
|
||||
}
|
114
models/repo/license.go
Normal file
114
models/repo/license.go
Normal file
@ -0,0 +1,114 @@
|
||||
// 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) INDEX NOT NULL"`
|
||||
CommitID string
|
||||
// TODO: `INDEX` will be used to find repositories by license
|
||||
License string `xorm:"VARCHAR(50) UNIQUE(s) INDEX 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
|
||||
}
|
@ -23,7 +23,7 @@ type LicenseValues struct {
|
||||
func GetLicense(name string, values *LicenseValues) ([]byte, error) {
|
||||
data, err := options.License(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
|
||||
return nil, fmt.Errorf("GetLicense[%s]: %w", name, err)
|
||||
}
|
||||
return fillLicensePlaceholder(name, values, data), nil
|
||||
}
|
||||
|
@ -114,6 +114,7 @@ type Repository struct {
|
||||
MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer"`
|
||||
Topics []string `json:"topics"`
|
||||
Licenses []string `json:"licenses"`
|
||||
}
|
||||
|
||||
// CreateRepoOption options when creating repository
|
||||
|
@ -283,38 +283,47 @@ func CommonSkip(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsReadmeFileName reports whether name looks like a README file
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
FileTypeReadme FileType = "readme"
|
||||
FileTypeLicense FileType = "license"
|
||||
)
|
||||
|
||||
// IsFileName reports whether name looks like a target name file
|
||||
// based on its name.
|
||||
func IsReadmeFileName(name string) bool {
|
||||
func IsFileName(name string, fileType FileType) bool {
|
||||
name = strings.ToLower(name)
|
||||
if len(name) < 6 {
|
||||
lenFileType := len(fileType)
|
||||
if len(name) < lenFileType {
|
||||
return false
|
||||
} else if len(name) == 6 {
|
||||
return name == "readme"
|
||||
} else if len(name) == lenFileType {
|
||||
return name == string(fileType)
|
||||
}
|
||||
return name[:7] == "readme."
|
||||
return name[:lenFileType+1] == string(fileType)+"."
|
||||
}
|
||||
|
||||
// IsReadmeFileExtension reports whether name looks like a README file
|
||||
// IsFileExtension reports whether name looks like a target name file
|
||||
// based on its name. It will look through the provided extensions and check if the file matches
|
||||
// one of the extensions and provide the index in the extension list.
|
||||
// If the filename is `readme.` with an unmatched extension it will match with the index equaling
|
||||
// the length of the provided extension list.
|
||||
// Note that the '.' should be provided in ext, e.g ".md"
|
||||
func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
|
||||
func IsFileExtension(name string, fileType FileType, ext ...string) (int, bool) {
|
||||
name = strings.ToLower(name)
|
||||
if len(name) < 6 || name[:6] != "readme" {
|
||||
lenFileType := len(fileType)
|
||||
if len(name) < lenFileType || name[:lenFileType] != string(fileType) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for i, extension := range ext {
|
||||
extension = strings.ToLower(extension)
|
||||
if name[6:] == extension {
|
||||
if name[lenFileType:] == extension {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
|
||||
if name[6] == '.' {
|
||||
if name[lenFileType] == '.' {
|
||||
return len(ext), true
|
||||
}
|
||||
|
||||
|
@ -76,10 +76,10 @@ func TestMisc_IsReadmeFileName(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range trueTestCases {
|
||||
assert.True(t, IsReadmeFileName(testCase))
|
||||
assert.True(t, IsFileName(testCase, FileTypeReadme))
|
||||
}
|
||||
for _, testCase := range falseTestCases {
|
||||
assert.False(t, IsReadmeFileName(testCase))
|
||||
assert.False(t, IsFileName(testCase, FileTypeReadme))
|
||||
}
|
||||
|
||||
type extensionTestcase struct {
|
||||
@ -131,7 +131,7 @@ func TestMisc_IsReadmeFileName(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCasesExtensions {
|
||||
idx, ok := IsReadmeFileExtension(testCase.name, exts...)
|
||||
idx, ok := IsFileExtension(testCase.name, FileTypeReadme, exts...)
|
||||
assert.Equal(t, testCase.expected, ok)
|
||||
assert.Equal(t, testCase.idx, idx)
|
||||
}
|
||||
|
1
options/license-aliases.json
Normal file
1
options/license-aliases.json
Normal file
@ -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"}
|
@ -1040,6 +1040,7 @@ issue_labels_helper = Select an issue label set.
|
||||
license = License
|
||||
license_helper = Select a license file.
|
||||
license_helper_desc = A license governs what others can and can't do with your code. Not sure which one is right for your project? See <a target="_blank" rel="noopener noreferrer" href="%s">Choose a license.</a>
|
||||
multiple_licenses = Multiple Licenses
|
||||
object_format = Object Format
|
||||
object_format_helper = Object format of the repository. Cannot be changed later. SHA1 is most compatible.
|
||||
readme = README
|
||||
@ -2941,6 +2942,7 @@ dashboard.start_schedule_tasks = Start actions schedule tasks
|
||||
dashboard.sync_branch.started = Branches Sync started
|
||||
dashboard.sync_tag.started = Tags Sync started
|
||||
dashboard.rebuild_issue_indexer = Rebuild issue indexer
|
||||
dashboard.sync_repo_licenses = Sync repo licenses
|
||||
|
||||
users.user_manage_panel = User Account Management
|
||||
users.new_account = Create User Account
|
||||
|
@ -1325,6 +1325,7 @@ func Routes() *web.Router {
|
||||
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
|
||||
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
|
||||
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
||||
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
|
||||
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
|
||||
m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed)
|
||||
m.Group("/avatar", func() {
|
||||
|
51
routers/api/v1/repo/license.go
Normal file
51
routers/api/v1/repo/license.go
Normal file
@ -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)
|
||||
}
|
@ -731,6 +731,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
}
|
||||
|
||||
// Default branch only updated if changed and exist or the repository is empty
|
||||
updateRepoLicense := false
|
||||
if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
|
||||
if !repo.IsEmpty {
|
||||
if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil {
|
||||
@ -739,6 +740,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
return err
|
||||
}
|
||||
}
|
||||
updateRepoLicense = true
|
||||
}
|
||||
repo.DefaultBranch = *opts.DefaultBranch
|
||||
}
|
||||
@ -748,6 +750,15 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
return err
|
||||
}
|
||||
|
||||
if updateRepoLicense {
|
||||
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddRepoToLicenseUpdaterQueue", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
|
||||
return nil
|
||||
}
|
||||
|
@ -359,6 +359,13 @@ type swaggerLanguageStatistics struct {
|
||||
Body map[string]int64 `json:"body"`
|
||||
}
|
||||
|
||||
// LicensesList
|
||||
// swagger:response LicensesList
|
||||
type swaggerLicensesList struct {
|
||||
// in: body
|
||||
Body []string `json:"body"`
|
||||
}
|
||||
|
||||
// CombinedStatus
|
||||
// swagger:response CombinedStatus
|
||||
type swaggerCombinedStatus struct {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
// SetDefaultBranch updates the default branch
|
||||
@ -36,5 +37,15 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
@ -278,10 +278,19 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
|
||||
|
||||
branch := refFullName.BranchName()
|
||||
|
||||
// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
|
||||
if !repo.IsFork && branch == baseRepo.DefaultBranch {
|
||||
results = append(results, private.HookPostReceiveBranchResult{})
|
||||
continue
|
||||
if branch == baseRepo.DefaultBranch {
|
||||
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: repo.ID,
|
||||
}); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{Err: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
|
||||
if !repo.IsFork {
|
||||
results = append(results, private.HookPostReceiveBranchResult{})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
|
||||
|
@ -90,6 +90,11 @@ func Branches(ctx *context.Context) {
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.Data["DetectedLicenseFileName"], err = repo_service.GetDetectedLicenseFileName(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDetectedLicenseFileName", err)
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplBranch)
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
git_service "code.gitea.io/gitea/services/repository"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -101,7 +101,11 @@ func Commits(ctx *context.Context) {
|
||||
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.Data["DetectedLicenseFileName"], err = repo_service.GetDetectedLicenseFileName(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDetectedLicenseFileName", err)
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
}
|
||||
|
||||
@ -218,6 +222,12 @@ func SearchCommits(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["RefName"] = ctx.Repo.RefName
|
||||
ctx.Data["DetectedLicenseFileName"], err = repo_service.GetDetectedLicenseFileName(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDetectedLicenseFileName", err)
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
}
|
||||
|
||||
@ -264,11 +274,16 @@ func FileHistory(ctx *context.Context) {
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.Data["DetectedLicenseFileName"], err = repo_service.GetDetectedLicenseFileName(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDetectedLicenseFileName", err)
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
}
|
||||
|
||||
func LoadBranchesAndTags(ctx *context.Context) {
|
||||
response, err := git_service.LoadBranchesAndTags(ctx, ctx.Repo, ctx.PathParam("sha"))
|
||||
response, err := repo_service.LoadBranchesAndTags(ctx, ctx.Repo, ctx.PathParam("sha"))
|
||||
if err == nil {
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
return
|
||||
|
@ -289,7 +289,6 @@ func releasesOrTagsFeed(ctx *context.Context, isReleasesOnly bool, formatType st
|
||||
// SingleRelease renders a single release's page
|
||||
func SingleRelease(ctx *context.Context) {
|
||||
ctx.Data["PageIsReleaseList"] = true
|
||||
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
|
||||
|
||||
writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
|
||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
||||
|
@ -51,6 +51,7 @@ import (
|
||||
"code.gitea.io/gitea/routers/web/feed"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
@ -68,99 +69,6 @@ const (
|
||||
tplMigrating base.TplName = "repo/migrate/migrating"
|
||||
)
|
||||
|
||||
// locate a README for a tree in one of the supported paths.
|
||||
//
|
||||
// entries is passed to reduce calls to ListEntries(), so
|
||||
// this has precondition:
|
||||
//
|
||||
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
|
||||
//
|
||||
// FIXME: There has to be a more efficient way of doing this
|
||||
func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
|
||||
// Create a list of extensions in priority order
|
||||
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
|
||||
// 2. Txt files - e.g. README.txt
|
||||
// 3. No extension - e.g. README
|
||||
exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
|
||||
extCount := len(exts)
|
||||
readmeFiles := make([]*git.TreeEntry, extCount+1)
|
||||
|
||||
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
|
||||
for _, entry := range entries {
|
||||
if tryWellKnownDirs && entry.IsDir() {
|
||||
// as a special case for the top-level repo introduction README,
|
||||
// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
|
||||
// (note that docsEntries is ignored unless we are at the root)
|
||||
lowerName := strings.ToLower(entry.Name())
|
||||
switch lowerName {
|
||||
case "docs":
|
||||
if entry.Name() == "docs" || docsEntries[0] == nil {
|
||||
docsEntries[0] = entry
|
||||
}
|
||||
case ".gitea":
|
||||
if entry.Name() == ".gitea" || docsEntries[1] == nil {
|
||||
docsEntries[1] = entry
|
||||
}
|
||||
case ".github":
|
||||
if entry.Name() == ".github" || docsEntries[2] == nil {
|
||||
docsEntries[2] = entry
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
||||
log.Debug("Potential readme file: %s", entry.Name())
|
||||
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
|
||||
if entry.IsLink() {
|
||||
target, err := entry.FollowLinks()
|
||||
if err != nil && !git.IsErrBadLink(err) {
|
||||
return "", nil, err
|
||||
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
||||
readmeFiles[i] = entry
|
||||
}
|
||||
} else {
|
||||
readmeFiles[i] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var readmeFile *git.TreeEntry
|
||||
for _, f := range readmeFiles {
|
||||
if f != nil {
|
||||
readmeFile = f
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.TreePath == "" && readmeFile == nil {
|
||||
for _, subTreeEntry := range docsEntries {
|
||||
if subTreeEntry == nil {
|
||||
continue
|
||||
}
|
||||
subTree := subTreeEntry.Tree()
|
||||
if subTree == nil {
|
||||
// this should be impossible; if subTreeEntry exists so should this.
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
childEntries, err := subTree.ListEntries()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
return "", nil, err
|
||||
}
|
||||
if readmeFile != nil {
|
||||
return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", readmeFile, nil
|
||||
}
|
||||
|
||||
func renderDirectory(ctx *context.Context) {
|
||||
entries := renderDirectoryFiles(ctx, 1*time.Second)
|
||||
if ctx.Written() {
|
||||
@ -172,37 +80,15 @@ func renderDirectory(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
|
||||
}
|
||||
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
|
||||
subfolder, readmeFile, err := repo_service.FindFileInEntries(util.FileTypeReadme, entries, ctx.Repo.TreePath, ctx.Locale.Language(), true)
|
||||
if err != nil {
|
||||
ctx.ServerError("findReadmeFileInEntries", err)
|
||||
ctx.ServerError("findFileInEntries", err)
|
||||
return
|
||||
}
|
||||
|
||||
renderReadmeFile(ctx, subfolder, readmeFile)
|
||||
}
|
||||
|
||||
// localizedExtensions prepends the provided language code with and without a
|
||||
// regional identifier to the provided extension.
|
||||
// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
|
||||
// Note: ext should be prefixed with a `.`
|
||||
func localizedExtensions(ext, languageCode string) (localizedExts []string) {
|
||||
if len(languageCode) < 1 {
|
||||
return []string{ext}
|
||||
}
|
||||
|
||||
lowerLangCode := "." + strings.ToLower(languageCode)
|
||||
|
||||
if strings.Contains(lowerLangCode, "-") {
|
||||
underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
|
||||
indexOfDash := strings.Index(lowerLangCode, "-")
|
||||
// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
|
||||
return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext}
|
||||
}
|
||||
|
||||
// e.g. [.en.md, .md]
|
||||
return []string{lowerLangCode + ext, ext}
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
isTextFile bool
|
||||
isLFSFile bool
|
||||
@ -498,7 +384,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
|
||||
shouldRenderSource := ctx.FormString("display") == "source"
|
||||
readmeExist := util.IsReadmeFileName(blob.Name())
|
||||
readmeExist := util.IsFileName(blob.Name(), util.FileTypeReadme)
|
||||
ctx.Data["ReadmeExist"] = readmeExist
|
||||
|
||||
markupType := markup.DetectMarkupTypeByFileName(blob.Name())
|
||||
@ -1077,6 +963,11 @@ func renderHomeCode(ctx *context.Context) {
|
||||
ctx.Data["TreeLink"] = treeLink
|
||||
ctx.Data["TreeNames"] = treeNames
|
||||
ctx.Data["BranchLink"] = branchLink
|
||||
ctx.Data["DetectedLicenseFileName"], err = repo_service.GetDetectedLicenseFileName(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDetectedLicenseFileName", err)
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplRepoHome)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -544,6 +551,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
|
||||
ctx.Data["Title"] = owner.Name + "/" + repo.Name
|
||||
ctx.Data["Repository"] = repo
|
||||
ctx.Data["Owner"] = ctx.Repo.Repository.Owner
|
||||
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
|
||||
ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner()
|
||||
ctx.Data["IsRepositoryAdmin"] = ctx.Repo.IsAdmin()
|
||||
ctx.Data["RepoOwnerIsOrganization"] = repo.Owner.IsOrganization()
|
||||
|
@ -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
|
||||
|
@ -611,6 +611,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},
|
||||
|
129
services/repository/file.go
Normal file
129
services/repository/file.go
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// locate a README/LICENSE for a tree in one of the supported paths.
|
||||
//
|
||||
// entries is passed to reduce calls to ListEntries(), so
|
||||
// this has precondition:
|
||||
//
|
||||
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
|
||||
//
|
||||
// FIXME: There has to be a more efficient way of doing this
|
||||
func FindFileInEntries(fileType util.FileType, entries []*git.TreeEntry, treePath, language string, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
|
||||
// Create a list of extensions in priority order
|
||||
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
|
||||
// 2. Txt files - e.g. README.txt
|
||||
// 3. No extension - e.g. README
|
||||
exts := append(localizedExtensions(".md", language), ".txt", "") // sorted by priority
|
||||
extCount := len(exts)
|
||||
targetFiles := make([]*git.TreeEntry, extCount+1)
|
||||
|
||||
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
|
||||
for _, entry := range entries {
|
||||
if tryWellKnownDirs && entry.IsDir() {
|
||||
// as a special case for the top-level repo introduction README,
|
||||
// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
|
||||
// (note that docsEntries is ignored unless we are at the root)
|
||||
lowerName := strings.ToLower(entry.Name())
|
||||
switch lowerName {
|
||||
case "docs":
|
||||
if entry.Name() == "docs" || docsEntries[0] == nil {
|
||||
docsEntries[0] = entry
|
||||
}
|
||||
case ".gitea":
|
||||
if entry.Name() == ".gitea" || docsEntries[1] == nil {
|
||||
docsEntries[1] = entry
|
||||
}
|
||||
case ".github":
|
||||
if entry.Name() == ".github" || docsEntries[2] == nil {
|
||||
docsEntries[2] = entry
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i, ok := util.IsFileExtension(entry.Name(), fileType, exts...); ok {
|
||||
log.Debug("Potential %s file: %s", fileType, entry.Name())
|
||||
if targetFiles[i] == nil || base.NaturalSortLess(targetFiles[i].Name(), entry.Blob().Name()) {
|
||||
if entry.IsLink() {
|
||||
target, err := entry.FollowLinks()
|
||||
if err != nil && !git.IsErrBadLink(err) {
|
||||
return "", nil, err
|
||||
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
||||
targetFiles[i] = entry
|
||||
}
|
||||
} else {
|
||||
targetFiles[i] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var targetFile *git.TreeEntry
|
||||
for _, f := range targetFiles {
|
||||
if f != nil {
|
||||
targetFile = f
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if treePath == "" && targetFile == nil {
|
||||
for _, subTreeEntry := range docsEntries {
|
||||
if subTreeEntry == nil {
|
||||
continue
|
||||
}
|
||||
subTree := subTreeEntry.Tree()
|
||||
if subTree == nil {
|
||||
// this should be impossible; if subTreeEntry exists so should this.
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
childEntries, err := subTree.ListEntries()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
subfolder, targetFile, err := FindFileInEntries(fileType, childEntries, subTreeEntry.Name(), language, false)
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
return "", nil, err
|
||||
}
|
||||
if targetFile != nil {
|
||||
return path.Join(subTreeEntry.Name(), subfolder), targetFile, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", targetFile, nil
|
||||
}
|
||||
|
||||
// localizedExtensions prepends the provided language code with and without a
|
||||
// regional identifier to the provided extension.
|
||||
// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
|
||||
// Note: ext should be prefixed with a `.`
|
||||
func localizedExtensions(ext, languageCode string) (localizedExts []string) {
|
||||
if len(languageCode) < 1 {
|
||||
return []string{ext}
|
||||
}
|
||||
|
||||
lowerLangCode := "." + strings.ToLower(languageCode)
|
||||
|
||||
if strings.Contains(lowerLangCode, "-") {
|
||||
underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
|
||||
indexOfDash := strings.Index(lowerLangCode, "-")
|
||||
// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
|
||||
return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext}
|
||||
}
|
||||
|
||||
// e.g. [.en.md, .md]
|
||||
return []string{lowerLangCode + ext, ext}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
package repository
|
||||
|
||||
import (
|
||||
"reflect"
|
@ -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 {
|
||||
|
236
services/repository/license.go
Normal file
236
services/repository/license.go
Normal file
@ -0,0 +1,236 @@
|
||||
// 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"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
licenseclassifier "github.com/google/licenseclassifier/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
classifier *licenseclassifier.Classifier
|
||||
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-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 initClassifier() error {
|
||||
if classifier != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
licenseFile, err := findLicenseFile(commit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("findLicenseFile: %w", err)
|
||||
}
|
||||
|
||||
licenses := make([]string, 0)
|
||||
if licenseFile != nil {
|
||||
r, err := licenseFile.Blob().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)
|
||||
}
|
||||
|
||||
// GetDetectedLicenseFileName returns license file name in the repository if it exists
|
||||
func GetDetectedLicenseFileName(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) (string, error) {
|
||||
if commit == nil {
|
||||
return "", nil
|
||||
}
|
||||
licenseFile, err := findLicenseFile(commit)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("findLicenseFile: %w", err)
|
||||
}
|
||||
if licenseFile != nil {
|
||||
return licenseFile.Name(), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// findLicenseFile returns the entry of license file in the repository if it exists
|
||||
func findLicenseFile(commit *git.Commit) (*git.TreeEntry, error) {
|
||||
if commit == nil {
|
||||
return nil, nil
|
||||
}
|
||||
entries, err := commit.ListEntries()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListEntries: %w", err)
|
||||
}
|
||||
_, f, err := FindFileInEntries(util.FileTypeLicense, entries, "", "", false)
|
||||
return f, err
|
||||
}
|
||||
|
||||
// detectLicense returns the licenses detected by the given content buff
|
||||
func detectLicense(r io.Reader) ([]string, error) {
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := initClassifier(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
73
services/repository/license_test.go
Normal file
73
services/repository/license_test.go
Normal file
@ -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 = initClassifier()
|
||||
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
|
||||
}
|
||||
|
@ -13,7 +13,12 @@
|
||||
{{svg "octicon-tag"}} <b>{{ctx.Locale.PrettyNumber .NumTags}}</b> {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<span class="item not-mobile" {{if not (eq .Repository.Size 0)}}data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
|
||||
{{if .DetectedRepoLicenses}}
|
||||
<a class="item muted" href="{{.RepoLink}}/src/{{.DefaultBranch}}/{{PathEscapeSegments .DetectedLicenseFileName}}" data-tooltip-placement="top" data-tooltip-content="{{StringUtils.Join .DetectedRepoLicenses ", "}}">
|
||||
{{svg "octicon-law"}} <b>{{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}}</b>
|
||||
</a>
|
||||
{{end}}
|
||||
<span class="item not-mobile" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
|
||||
{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
|
||||
{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
|
||||
{{svg "octicon-database"}} <b>{{ctx.Locale.PrettyNumber (index $fileSizeFields 0)}}</b> {{index $fileSizeFields 1}}
|
||||
|
52
templates/swagger/v1_json.tmpl
generated
52
templates/swagger/v1_json.tmpl
generated
@ -10640,6 +10640,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/licenses": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Get repo licenses",
|
||||
"operationId": "repoGetLicenses",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/LicensesList"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/media/{filepath}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@ -24142,6 +24178,13 @@
|
||||
"type": "string",
|
||||
"x-go-name": "LanguagesURL"
|
||||
},
|
||||
"licenses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Licenses"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"x-go-name": "Link"
|
||||
@ -25717,6 +25760,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LicensesList": {
|
||||
"description": "LicensesList",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MarkdownRender": {
|
||||
"description": "MarkdownRender is a rendered markdown document",
|
||||
"schema": {
|
||||
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,2 @@
|
||||
x•ŽM
|
||||
Â0F]çÙ2“d&ñ*ù™`Á¶¶Æ…··Wð[>Þƒ¯®ó<
ëÐ<C3AB>Æ®j!Æ&=Õ*Êž1*¢@””TS*X3›WÞu–©cé.–êK<C3AA>Ð90E¦Äž”$h+µ„fòg<ÖÝ~_@ÞE{=,!€÷m»Ôu¾YŒ Ž„B´g8fz|ú_erkö9U]Þj~2]<¼
|
@ -1 +1 @@
|
||||
65f1bf27bc3bf70f64657658635e66094edbcb4d
|
||||
90c1019714259b24fb81711d4416ac0f18667dfa
|
||||
|
@ -304,11 +304,11 @@ func TestAPICron(t *testing.T) {
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
assert.Equal(t, "28", resp.Header().Get("X-Total-Count"))
|
||||
assert.Equal(t, "29", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
var crons []api.Cron
|
||||
DecodeJSON(t, resp, &crons)
|
||||
assert.Len(t, crons, 28)
|
||||
assert.Len(t, crons, 29)
|
||||
})
|
||||
|
||||
t.Run("Execute", func(t *testing.T) {
|
||||
|
80
tests/integration/api_repo_license_test.go
Normal file
80
tests/integration/api_repo_license_test.go
Normal file
@ -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
Block a user