Skip to content

Commit 70b7df0

Browse files
yp05327silverwindlunnydenyskondelvh
authored
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): #24872 (comment) Test result between [google/licensecheck](https://github.com/google/licensecheck) and [google/licenseclassifier](https://github.com/google/licenseclassifier/): #24872 (comment) - 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](#24872 (comment)) 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(#24872 (comment)) - [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>
1 parent f4b8f6f commit 70b7df0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+906
-22
lines changed

assets/go-licenses.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/generate-licenses.go

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
14
//go:build ignore
25

36
package main
47

58
import (
69
"archive/tar"
710
"compress/gzip"
11+
"crypto/md5"
12+
"encoding/hex"
813
"flag"
914
"fmt"
1015
"io"
@@ -15,6 +20,8 @@ import (
1520
"path/filepath"
1621
"strings"
1722

23+
"code.gitea.io/gitea/build/license"
24+
"code.gitea.io/gitea/modules/json"
1825
"code.gitea.io/gitea/modules/util"
1926
)
2027

@@ -77,7 +84,7 @@ func main() {
7784
}
7885

7986
tr := tar.NewReader(gz)
80-
87+
aliasesFiles := make(map[string][]string)
8188
for {
8289
hdr, err := tr.Next()
8390

@@ -97,26 +104,73 @@ func main() {
97104
continue
98105
}
99106

100-
if strings.HasPrefix(filepath.Base(hdr.Name), "README") {
107+
fileBaseName := filepath.Base(hdr.Name)
108+
licenseName := strings.TrimSuffix(fileBaseName, ".txt")
109+
110+
if strings.HasPrefix(fileBaseName, "README") {
101111
continue
102112
}
103113

104-
if strings.HasPrefix(filepath.Base(hdr.Name), "deprecated_") {
114+
if strings.HasPrefix(fileBaseName, "deprecated_") {
105115
continue
106116
}
107-
out, err := os.Create(path.Join(destination, strings.TrimSuffix(filepath.Base(hdr.Name), ".txt")))
117+
out, err := os.Create(path.Join(destination, licenseName))
108118
if err != nil {
109119
log.Fatalf("Failed to create new file. %s", err)
110120
}
111121

112122
defer out.Close()
113123

114-
if _, err := io.Copy(out, tr); err != nil {
124+
// some license files have same content, so we need to detect these files and create a convert map into a json file
125+
// Later we use this convert map to avoid adding same license content with different license name
126+
h := md5.New()
127+
// calculate md5 and write file in the same time
128+
r := io.TeeReader(tr, h)
129+
if _, err := io.Copy(out, r); err != nil {
115130
log.Fatalf("Failed to write new file. %s", err)
116131
} else {
117132
fmt.Printf("Written %s\n", out.Name())
133+
134+
md5 := hex.EncodeToString(h.Sum(nil))
135+
aliasesFiles[md5] = append(aliasesFiles[md5], licenseName)
118136
}
119137
}
120138

139+
// generate convert license name map
140+
licenseAliases := make(map[string]string)
141+
for _, fileNames := range aliasesFiles {
142+
if len(fileNames) > 1 {
143+
licenseName := license.GetLicenseNameFromAliases(fileNames)
144+
if licenseName == "" {
145+
// license name should not be empty as expected
146+
// if it is empty, we need to rewrite the logic of GetLicenseNameFromAliases
147+
log.Fatalf("GetLicenseNameFromAliases: license name is empty")
148+
}
149+
for _, fileName := range fileNames {
150+
licenseAliases[fileName] = licenseName
151+
}
152+
}
153+
}
154+
// save convert license name map to file
155+
b, err := json.Marshal(licenseAliases)
156+
if err != nil {
157+
log.Fatalf("Failed to create json bytes. %s", err)
158+
}
159+
160+
licenseAliasesDestination := filepath.Join(destination, "etc", "license-aliases.json")
161+
if err := os.MkdirAll(filepath.Dir(licenseAliasesDestination), 0o755); err != nil {
162+
log.Fatalf("Failed to create directory for license aliases json file. %s", err)
163+
}
164+
165+
f, err := os.Create(licenseAliasesDestination)
166+
if err != nil {
167+
log.Fatalf("Failed to create license aliases json file. %s", err)
168+
}
169+
defer f.Close()
170+
171+
if _, err = f.Write(b); err != nil {
172+
log.Fatalf("Failed to write license aliases json file. %s", err)
173+
}
174+
121175
fmt.Println("Done")
122176
}

build/license/aliasgenerator.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package license
5+
6+
import "strings"
7+
8+
func GetLicenseNameFromAliases(fnl []string) string {
9+
if len(fnl) == 0 {
10+
return ""
11+
}
12+
13+
shortestItem := func(list []string) string {
14+
s := list[0]
15+
for _, l := range list[1:] {
16+
if len(l) < len(s) {
17+
s = l
18+
}
19+
}
20+
return s
21+
}
22+
allHasPrefix := func(list []string, s string) bool {
23+
for _, l := range list {
24+
if !strings.HasPrefix(l, s) {
25+
return false
26+
}
27+
}
28+
return true
29+
}
30+
31+
sl := shortestItem(fnl)
32+
slv := strings.Split(sl, "-")
33+
var result string
34+
for i := len(slv); i >= 0; i-- {
35+
result = strings.Join(slv[:i], "-")
36+
if allHasPrefix(fnl, result) {
37+
return result
38+
}
39+
}
40+
return ""
41+
}

build/license/aliasgenerator_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package license
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGetLicenseNameFromAliases(t *testing.T) {
13+
tests := []struct {
14+
target string
15+
inputs []string
16+
}{
17+
{
18+
// real case which you can find in license-aliases.json
19+
target: "AGPL-1.0",
20+
inputs: []string{
21+
"AGPL-1.0-only",
22+
"AGPL-1.0-or-late",
23+
},
24+
},
25+
{
26+
target: "",
27+
inputs: []string{
28+
"APSL-1.0",
29+
"AGPL-1.0-only",
30+
"AGPL-1.0-or-late",
31+
},
32+
},
33+
}
34+
35+
for _, tt := range tests {
36+
result := GetLicenseNameFromAliases(tt.inputs)
37+
assert.Equal(t, result, tt.target)
38+
}
39+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ require (
6868
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
6969
github.com/golang-jwt/jwt/v5 v5.2.1
7070
github.com/google/go-github/v61 v61.0.0
71+
github.com/google/licenseclassifier/v2 v2.0.0
7172
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8
7273
github.com/google/uuid v1.6.0
7374
github.com/gorilla/feeds v1.2.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,8 @@ github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47
441441
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
442442
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
443443
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
444+
github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA=
445+
github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM=
444446
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
445447
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 h1:ASJ/LAqdCHOyMYI+dwNxn7Rd8FscNkMyTr1KZU1JI/M=
446448
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
@@ -735,6 +737,7 @@ github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jN
735737
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
736738
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
737739
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
740+
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
738741
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
739742
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
740743
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=

models/fixtures/repo_license.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[] # empty

models/fixtures/repository.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
fork_id: 0
2727
is_template: false
2828
template_id: 0
29-
size: 7597
29+
size: 8478
3030
is_fsck_enabled: true
3131
close_issues_via_commit_in_any_branch: false
3232

models/migrations/migrations.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ var migrations = []Migration{
500500
// v259 -> v260
501501
NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens),
502502

503-
// Gitea 1.20.0 ends at 260
503+
// Gitea 1.20.0 ends at v260
504504

505505
// v260 -> v261
506506
NewMigration("Drop custom_labels column of action_runner table", v1_21.DropCustomLabelsColumnOfActionRunner),
@@ -601,6 +601,8 @@ var migrations = []Migration{
601601
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
602602
// v304 -> v305
603603
NewMigration("Add index for release sha1", v1_23.AddIndexForReleaseSha1),
604+
// v305 -> v306
605+
NewMigration("Add Repository Licenses", v1_23.AddRepositoryLicenses),
604606
}
605607

606608
// GetCurrentDBVersion returns the current db version

models/migrations/v1_23/v305.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_23 //nolint
5+
6+
import (
7+
"code.gitea.io/gitea/modules/timeutil"
8+
9+
"xorm.io/xorm"
10+
)
11+
12+
func AddRepositoryLicenses(x *xorm.Engine) error {
13+
type RepoLicense struct {
14+
ID int64 `xorm:"pk autoincr"`
15+
RepoID int64 `xorm:"UNIQUE(s) NOT NULL"`
16+
CommitID string
17+
License string `xorm:"VARCHAR(255) UNIQUE(s) NOT NULL"`
18+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
19+
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"`
20+
}
21+
22+
return x.Sync(new(RepoLicense))
23+
}

models/repo/license.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"context"
8+
9+
"code.gitea.io/gitea/models/db"
10+
"code.gitea.io/gitea/modules/timeutil"
11+
)
12+
13+
func init() {
14+
db.RegisterModel(new(RepoLicense))
15+
}
16+
17+
type RepoLicense struct { //revive:disable-line:exported
18+
ID int64 `xorm:"pk autoincr"`
19+
RepoID int64 `xorm:"UNIQUE(s) NOT NULL"`
20+
CommitID string
21+
License string `xorm:"VARCHAR(255) UNIQUE(s) NOT NULL"`
22+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
23+
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"`
24+
}
25+
26+
// RepoLicenseList defines a list of repo licenses
27+
type RepoLicenseList []*RepoLicense //revive:disable-line:exported
28+
29+
func (rll RepoLicenseList) StringList() []string {
30+
var licenses []string
31+
for _, rl := range rll {
32+
licenses = append(licenses, rl.License)
33+
}
34+
return licenses
35+
}
36+
37+
// GetRepoLicenses returns the license statistics for a repository
38+
func GetRepoLicenses(ctx context.Context, repo *Repository) (RepoLicenseList, error) {
39+
licenses := make(RepoLicenseList, 0)
40+
if err := db.GetEngine(ctx).Where("`repo_id` = ?", repo.ID).Asc("`license`").Find(&licenses); err != nil {
41+
return nil, err
42+
}
43+
return licenses, nil
44+
}
45+
46+
// UpdateRepoLicenses updates the license statistics for repository
47+
func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string, licenses []string) error {
48+
oldLicenses, err := GetRepoLicenses(ctx, repo)
49+
if err != nil {
50+
return err
51+
}
52+
for _, license := range licenses {
53+
upd := false
54+
for _, o := range oldLicenses {
55+
// Update already existing license
56+
if o.License == license {
57+
if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil {
58+
return err
59+
}
60+
upd = true
61+
break
62+
}
63+
}
64+
// Insert new license
65+
if !upd {
66+
if err := db.Insert(ctx, &RepoLicense{
67+
RepoID: repo.ID,
68+
CommitID: commitID,
69+
License: license,
70+
}); err != nil {
71+
return err
72+
}
73+
}
74+
}
75+
// Delete old licenses
76+
licenseToDelete := make([]int64, 0, len(oldLicenses))
77+
for _, o := range oldLicenses {
78+
if o.CommitID != commitID {
79+
licenseToDelete = append(licenseToDelete, o.ID)
80+
}
81+
}
82+
if len(licenseToDelete) > 0 {
83+
if _, err := db.GetEngine(ctx).In("`id`", licenseToDelete).Delete(&RepoLicense{}); err != nil {
84+
return err
85+
}
86+
}
87+
88+
return nil
89+
}
90+
91+
// CopyLicense Copy originalRepo license information to destRepo (use for forked repo)
92+
func CopyLicense(ctx context.Context, originalRepo, destRepo *Repository) error {
93+
repoLicenses, err := GetRepoLicenses(ctx, originalRepo)
94+
if err != nil {
95+
return err
96+
}
97+
if len(repoLicenses) > 0 {
98+
newRepoLicenses := make(RepoLicenseList, 0, len(repoLicenses))
99+
100+
for _, rl := range repoLicenses {
101+
newRepoLicense := &RepoLicense{
102+
RepoID: destRepo.ID,
103+
CommitID: rl.CommitID,
104+
License: rl.License,
105+
}
106+
newRepoLicenses = append(newRepoLicenses, newRepoLicense)
107+
}
108+
if err := db.Insert(ctx, &newRepoLicenses); err != nil {
109+
return err
110+
}
111+
}
112+
return nil
113+
}
114+
115+
// CleanRepoLicenses will remove all license record of the repo
116+
func CleanRepoLicenses(ctx context.Context, repo *Repository) error {
117+
return db.DeleteBeans(ctx, &RepoLicense{
118+
RepoID: repo.ID,
119+
})
120+
}

0 commit comments

Comments
 (0)