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:  Multiple Licenses:  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)  --------- 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>
This commit is contained in:
47 files changed
+906
-22
No files matched your search
Generated
+5
@@ -664,6 +664,11 @@
|
||||
"path": "github.com/google/go-tpm/LICENSE",
|
||||
"licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under tLine truncated
|
||||
},
|
||||
{
|
||||
"name": "github.com/google/licenseclassifier/v2",
|
||||
"path": "github.com/google/licenseclassifier/v2/LICENSE",
|
||||
"licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under tLine truncated
|
||||
},
|
||||
{
|
||||
"name": "github.com/google/pprof/profile",
|
||||
"path": "github.com/google/pprof/profile/LICENSE",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
@@ -5,6 +8,8 @@ package main
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,6 +20,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/build/license"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
@@ -77,7 +84,7 @@ func main() {
|
||||
}
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
|
||||
aliasesFiles := make(map[string][]string)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
|
||||
@@ -97,26 +104,73 @@ 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 := license.GetLicenseNameFromAliases(fileNames)
|
||||
if licenseName == "" {
|
||||
// license name should not be empty as expected
|
||||
// if it is empty, we need to rewrite the logic of GetLicenseNameFromAliases
|
||||
log.Fatalf("GetLicenseNameFromAliases: license name is empty")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
licenseAliasesDestination := filepath.Join(destination, "etc", "license-aliases.json")
|
||||
if err := os.MkdirAll(filepath.Dir(licenseAliasesDestination), 0o755); err != nil {
|
||||
log.Fatalf("Failed to create directory for license aliases json file. %s", err)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,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
|
||||
|
||||
@@ -441,6 +441,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=
|
||||
@@ -735,6 +737,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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -2942,6 +2943,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
|
||||
|
||||
@@ -1327,6 +1327,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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -172,6 +172,8 @@ func InitWebInstalled(ctx context.Context) {
|
||||
|
||||
actions_service.Init()
|
||||
|
||||
mustInit(repo_service.InitLicenseClassifier)
|
||||
|
||||
// Finally start up the cron
|
||||
cron.NewContext(ctx)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,7 +89,7 @@ func Branches(ctx *context.Context) {
|
||||
pager := context.NewPagination(int(branchesCount), pageSize, page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
||||
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,7 @@ func Commits(ctx *context.Context) {
|
||||
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
}
|
||||
|
||||
@@ -218,6 +218,8 @@ 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["LicenseFileName"] = repo_service.LicenseFileName
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
}
|
||||
|
||||
@@ -263,12 +265,12 @@ func FileHistory(ctx *context.Context) {
|
||||
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
||||
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"
|
||||
@@ -1077,6 +1078,7 @@ func renderHomeCode(ctx *context.Context) {
|
||||
ctx.Data["TreeLink"] = treeLink
|
||||
ctx.Data["TreeNames"] = treeNames
|
||||
ctx.Data["BranchLink"] = branchLink
|
||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
||||
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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Loaded 30 of 47 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user