mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-06 05:50:16 +00:00
feat(licenses): support REUSE specification in licenses (#38720)
extends the current license detection to support two modes: - legacy which is using classification and was expanded to handle more paths (extensions, different spelling or GNU copying file) - REUSE which avoids classification by relying on the spec dictating that license must be named as SPDX-ID.extension. Newly created repositories will default to REUSE based paths Use of styles at the same time is not allowed by design. Extends the UI to show all the detected licenses and paths to them, deduplicating them per SPDX-ID in database as is in github closes: https://github.com/go-gitea/gitea/issues/28672 --------- Assisted-By: omp:glm5.2 Assisted-By: omp:mimo-v2.5-pro Assisted-By: omp:mimimax-m3 Assisted-By: omp:kimi-k3 Assisted-By: omp:deepseek-v4-flash Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -426,6 +426,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(343, "Add max_parallel column to action_run_job", v1_28.AddMaxParallelToActionRunJob),
|
||||
newMigration(344, "Add deferred-matrix columns to ActionRunJob", v1_28.AddDeferredMatrixColumnsToActionRunJob),
|
||||
newMigration(345, "Add block on CODEOWNERS reviews branch protection", v1_28.AddBlockOnCodeownerReviews),
|
||||
newMigration(346, "Add license_path column to repo_license and backfill", v1_28.AddLicensePathToRepoLicense),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_28
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func AddLicensePathToRepoLicense(ctx context.Context, x base.EngineMigration) error {
|
||||
// Drop the old 2-column UNIQUE(s) index on (repo_id, license) and, on
|
||||
// re-runs, the index created under the new name.
|
||||
// xorm Sync cannot reliably update an index when its column set changes.
|
||||
indexes, err := x.Dialect().GetIndexes(x.DB(), ctx, "repo_license")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, idx := range indexes {
|
||||
if idx.Name == "s" || idx.Name == "path" {
|
||||
if _, err := x.Exec(x.Dialect().DropIndexSQL("repo_license", idx)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add license_path column. The DEFAULT backfills existing rows: all repos
|
||||
// created before this migration used the single LICENSE file convention.
|
||||
type RepoLicense struct {
|
||||
LicensePath string `xorm:"VARCHAR(255) NOT NULL DEFAULT 'LICENSE'"`
|
||||
}
|
||||
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(RepoLicense)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new 3-column UNIQUE(path) index on (repo_id, license, license_path);
|
||||
// xorm prefixes the name to UQE_repo_license_path.
|
||||
newIndex := schemas.NewIndex("path", schemas.UniqueType)
|
||||
newIndex.AddColumn("repo_id", "license", "license_path")
|
||||
if _, err := x.Exec(x.Dialect().CreateIndexSQL("repo_license", newIndex)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_28
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modelmigration/migrationtest"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// repoLicenseBeforeV345 mirrors the pre-migration repo_license table: no
|
||||
// license_path column and a 2-column UNIQUE(s) index on (repo_id, license).
|
||||
type repoLicenseBeforeV345 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:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
func (repoLicenseBeforeV345) TableName() string { return "repo_license" }
|
||||
|
||||
func Test_AddLicensePathToRepoLicense(t *testing.T) {
|
||||
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(repoLicenseBeforeV345))
|
||||
defer deferable()
|
||||
|
||||
_, err := x.Insert(&repoLicenseBeforeV345{RepoID: 1, CommitID: "c1", License: "MIT"})
|
||||
require.NoError(t, err)
|
||||
_, err = x.Insert(&repoLicenseBeforeV345{RepoID: 1, CommitID: "c1", License: "Apache-2.0"})
|
||||
require.NoError(t, err)
|
||||
_, err = x.Insert(&repoLicenseBeforeV345{RepoID: 2, CommitID: "c2", License: "MIT"})
|
||||
require.NoError(t, err)
|
||||
|
||||
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "repo_license")
|
||||
require.NoError(t, err)
|
||||
oldIdx, ok := indexes["s"] // GetIndexes strips the UQE_repo_license_ prefix
|
||||
require.True(t, ok, "old 2-column unique s index should exist before migration")
|
||||
assert.Equal(t, schemas.UniqueType, oldIdx.Type)
|
||||
assert.Equal(t, []string{"repo_id", "license"}, oldIdx.Cols)
|
||||
|
||||
require.NoError(t, AddLicensePathToRepoLicense(t.Context(), x))
|
||||
require.NoError(t, AddLicensePathToRepoLicense(t.Context(), x)) // idempotent
|
||||
|
||||
indexes, err = x.Dialect().GetIndexes(x.DB(), context.Background(), "repo_license")
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, indexes, "s", "old 2-column unique s index should be gone after migration")
|
||||
newIdx, ok := indexes["path"] // GetIndexes strips the UQE_repo_license_ prefix
|
||||
require.True(t, ok, "new index must be named path (UQE_repo_license_path)")
|
||||
assert.Equal(t, schemas.UniqueType, newIdx.Type)
|
||||
assert.Equal(t, []string{"repo_id", "license", "license_path"}, newIdx.Cols)
|
||||
|
||||
// pre-existing rows must default to the LICENSE path
|
||||
type licenseRow struct {
|
||||
RepoID int64
|
||||
License string
|
||||
LicensePath string
|
||||
}
|
||||
var rows []licenseRow
|
||||
require.NoError(t, x.SQL("SELECT repo_id, license, license_path FROM repo_license ORDER BY id").Find(&rows))
|
||||
require.Len(t, rows, 3)
|
||||
for _, r := range rows {
|
||||
assert.Equal(t, "LICENSE", r.LicensePath)
|
||||
}
|
||||
|
||||
// the new index is unique per (repo_id, license, license_path): the exact
|
||||
// duplicate must be rejected, while a second path for the same license
|
||||
// in the same repo is allowed
|
||||
_, err = x.Exec("INSERT INTO repo_license (repo_id, commit_id, license, license_path) VALUES (1, 'c1', 'MIT', 'LICENSE')")
|
||||
require.Error(t, err)
|
||||
_, err = x.Exec("INSERT INTO repo_license (repo_id, commit_id, license, license_path) VALUES (1, 'c1', 'MIT', 'LICENSES/MIT.txt')")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user