mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-12 02:33:50 +02:00
Merge branch 'main' into lunny/project_workflow
This commit is contained in:
commit
08d28090a4
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -8,3 +8,4 @@
|
|||||||
/vendor/** -text -eol linguist-vendored
|
/vendor/** -text -eol linguist-vendored
|
||||||
/web_src/js/vendor/** -text -eol linguist-vendored
|
/web_src/js/vendor/** -text -eol linguist-vendored
|
||||||
Dockerfile.* linguist-language=Dockerfile
|
Dockerfile.* linguist-language=Dockerfile
|
||||||
|
Makefile.* linguist-language=Makefile
|
||||||
|
|||||||
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
@ -51,7 +51,6 @@ modifies/internal:
|
|||||||
- ".github/**"
|
- ".github/**"
|
||||||
- ".gitea/**"
|
- ".gitea/**"
|
||||||
- ".devcontainer/**"
|
- ".devcontainer/**"
|
||||||
- "build.go"
|
|
||||||
- "build/**"
|
- "build/**"
|
||||||
- "contrib/**"
|
- "contrib/**"
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -127,3 +127,6 @@ prime/
|
|||||||
|
|
||||||
# Ignore worktrees when working on multiple branches
|
# Ignore worktrees when working on multiple branches
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# A Makefile for custom make targets
|
||||||
|
Makefile.local
|
||||||
|
|||||||
24
Makefile
24
Makefile
@ -41,7 +41,6 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
|||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
||||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
|
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
|
||||||
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
|
|
||||||
|
|
||||||
DOCKER_IMAGE ?= gitea/gitea
|
DOCKER_IMAGE ?= gitea/gitea
|
||||||
DOCKER_TAG ?= latest
|
DOCKER_TAG ?= latest
|
||||||
@ -199,6 +198,10 @@ TEST_MSSQL_DBNAME ?= gitea
|
|||||||
TEST_MSSQL_USERNAME ?= sa
|
TEST_MSSQL_USERNAME ?= sa
|
||||||
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
|
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
|
||||||
|
|
||||||
|
# Include local Makefile
|
||||||
|
# Makefile.local is listed in .gitignore
|
||||||
|
sinclude Makefile.local
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@ -276,19 +279,6 @@ fmt-check: fmt
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: fix
|
|
||||||
fix: ## apply automated fixes to Go code
|
|
||||||
$(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./...
|
|
||||||
|
|
||||||
.PHONY: fix-check
|
|
||||||
fix-check: fix
|
|
||||||
@diff=$$(git diff --color=always $(GO_SOURCES)); \
|
|
||||||
if [ -n "$$diff" ]; then \
|
|
||||||
echo "Please run 'make fix' and commit the result:"; \
|
|
||||||
printf "%s" "$${diff}"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: $(TAGS_EVIDENCE)
|
.PHONY: $(TAGS_EVIDENCE)
|
||||||
$(TAGS_EVIDENCE):
|
$(TAGS_EVIDENCE):
|
||||||
@mkdir -p $(MAKE_EVIDENCE_DIR)
|
@mkdir -p $(MAKE_EVIDENCE_DIR)
|
||||||
@ -328,7 +318,7 @@ checks: checks-frontend checks-backend ## run various consistency checks
|
|||||||
checks-frontend: lockfile-check svg-check ## check frontend files
|
checks-frontend: lockfile-check svg-check ## check frontend files
|
||||||
|
|
||||||
.PHONY: checks-backend
|
.PHONY: checks-backend
|
||||||
checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files
|
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-frontend lint-backend lint-spell ## lint everything
|
lint: lint-frontend lint-backend lint-spell ## lint everything
|
||||||
@ -400,8 +390,7 @@ lint-go-windows:
|
|||||||
.PHONY: lint-go-gitea-vet
|
.PHONY: lint-go-gitea-vet
|
||||||
lint-go-gitea-vet: ## lint go files with gitea-vet
|
lint-go-gitea-vet: ## lint go files with gitea-vet
|
||||||
@echo "Running gitea-vet..."
|
@echo "Running gitea-vet..."
|
||||||
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
|
@$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./...
|
||||||
@$(GO) vet -vettool=gitea-vet ./...
|
|
||||||
|
|
||||||
.PHONY: lint-go-gopls
|
.PHONY: lint-go-gopls
|
||||||
lint-go-gopls: ## lint go files with gopls
|
lint-go-gopls: ## lint go files with gopls
|
||||||
@ -852,7 +841,6 @@ deps-tools: ## install tool dependencies
|
|||||||
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
||||||
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
||||||
$(GO) install $(GOPLS_PACKAGE) & \
|
$(GO) install $(GOPLS_PACKAGE) & \
|
||||||
$(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \
|
|
||||||
wait
|
wait
|
||||||
|
|
||||||
node_modules: pnpm-lock.yaml
|
node_modules: pnpm-lock.yaml
|
||||||
|
|||||||
14
build.go
14
build.go
@ -1,14 +0,0 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build vendor
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
// Libraries that are included to vendor utilities used during Makefile build.
|
|
||||||
// These libraries will not be included in a normal compilation.
|
|
||||||
|
|
||||||
import (
|
|
||||||
// for vet
|
|
||||||
_ "code.gitea.io/gitea-vet"
|
|
||||||
)
|
|
||||||
@ -567,6 +567,11 @@ ENABLED = true
|
|||||||
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
|
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
|
||||||
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
|
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
|
||||||
;;
|
;;
|
||||||
|
;; The "issuer" claim identifies the principal that issued the JWT.
|
||||||
|
;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard.
|
||||||
|
;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash".
|
||||||
|
;JWT_CLAIM_ISSUER =
|
||||||
|
;;
|
||||||
;; Lifetime of an OAuth2 access token in seconds
|
;; Lifetime of an OAuth2 access token in seconds
|
||||||
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
|
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
|
||||||
;;
|
;;
|
||||||
|
|||||||
8
go.mod
8
go.mod
@ -1,6 +1,8 @@
|
|||||||
module code.gitea.io/gitea
|
module code.gitea.io/gitea
|
||||||
|
|
||||||
go 1.25.3
|
go 1.25.0
|
||||||
|
|
||||||
|
toolchain go1.25.4
|
||||||
|
|
||||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||||
// But some CAs use negative serial number, just relax the check. related:
|
// But some CAs use negative serial number, just relax the check. related:
|
||||||
@ -9,7 +11,6 @@ godebug x509negativeserial=1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/actions-proto-go v0.4.1
|
code.gitea.io/actions-proto-go v0.4.1
|
||||||
code.gitea.io/gitea-vet v0.2.3
|
|
||||||
code.gitea.io/sdk/gitea v0.22.0
|
code.gitea.io/sdk/gitea v0.22.0
|
||||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||||
connectrpc.com/connect v1.18.1
|
connectrpc.com/connect v1.18.1
|
||||||
@ -135,6 +136,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||||
|
code.gitea.io/gitea-vet v0.2.3 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||||
@ -307,3 +309,5 @@ exclude github.com/gofrs/uuid v4.0.0+incompatible
|
|||||||
exclude github.com/goccy/go-json v0.4.11
|
exclude github.com/goccy/go-json v0.4.11
|
||||||
|
|
||||||
exclude github.com/satori/go.uuid v1.2.0
|
exclude github.com/satori/go.uuid v1.2.0
|
||||||
|
|
||||||
|
tool code.gitea.io/gitea-vet
|
||||||
|
|||||||
@ -466,11 +466,13 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c
|
|||||||
return currentWhitelist, nil
|
return currentWhitelist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
whitelist = make([]int64, 0, len(newWhitelist))
|
whitelist = make([]int64, 0, len(newWhitelist))
|
||||||
for _, userID := range newWhitelist {
|
for _, userID := range newWhitelist {
|
||||||
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
|
if !prUserIDs.Contains(userID) {
|
||||||
return nil, err
|
|
||||||
} else if !reader {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
whitelist = append(whitelist, userID)
|
whitelist = append(whitelist, userID)
|
||||||
|
|||||||
@ -53,24 +53,45 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
|||||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
|
||||||
teams := make([]*Team, 0, 5)
|
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(teamIDs) == 0 {
|
||||||
|
return teams, nil
|
||||||
|
}
|
||||||
|
err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams)
|
||||||
|
return teams, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) {
|
||||||
sub := builder.Select("team_id").From("team_unit").
|
sub := builder.Select("team_id").From("team_unit").
|
||||||
Where(builder.Expr("team_unit.team_id = team.id")).
|
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||||
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||||
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
||||||
|
|
||||||
err := db.GetEngine(ctx).
|
err = db.GetEngine(ctx).
|
||||||
|
Select("team.id").
|
||||||
|
Table("team").
|
||||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||||
And("team_repo.org_id = ?", orgID).
|
And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID).
|
||||||
And("team_repo.repo_id = ?", repoID).
|
|
||||||
And(builder.Or(
|
And(builder.Or(
|
||||||
builder.Expr("team.authorize >= ?", mode),
|
builder.Expr("team.authorize >= ?", mode),
|
||||||
builder.In("team.id", sub),
|
builder.In("team.id", sub),
|
||||||
)).
|
)).
|
||||||
OrderBy("name").
|
Find(&teamIDs)
|
||||||
Find(&teams)
|
return teamIDs, err
|
||||||
|
}
|
||||||
return teams, err
|
|
||||||
|
func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) {
|
||||||
|
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(teamIDs) == 0 {
|
||||||
|
return userIDs, nil
|
||||||
|
}
|
||||||
|
err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs)
|
||||||
|
return userIDs, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@ -498,54 +499,44 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi
|
|||||||
return perm.HasAnyUnitAccess(), nil
|
return perm.HasAnyUnitAccess(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
|
func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) {
|
||||||
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
|
userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType)
|
||||||
if err = repo.LoadOwner(ctx); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(userIDs) == 0 {
|
||||||
e := db.GetEngine(ctx)
|
return users, nil
|
||||||
accesses := make([]*Access, 0, 10)
|
}
|
||||||
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave a seat for owner itself to append later, but if owner is an organization
|
|
||||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
|
||||||
users := make([]*user_model.User, 0, len(accesses)+1)
|
|
||||||
if len(accesses) > 0 {
|
|
||||||
userIDs := make([]int64, len(accesses))
|
|
||||||
for i := 0; i < len(accesses); i++ {
|
|
||||||
userIDs[i] = accesses[i].UserID
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = e.In("id", userIDs).Find(&users); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !repo.Owner.IsOrganization() {
|
|
||||||
users = append(users, repo.Owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoReaders returns all users that have explicit read access or higher to the repository.
|
func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) {
|
||||||
func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
userIDs := container.Set[int64]{}
|
||||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
|
e := db.GetEngine(ctx)
|
||||||
}
|
accesses := make([]*Access, 0, 10)
|
||||||
|
if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||||
// GetRepoWriters returns all users that have write access to the repository.
|
return nil, err
|
||||||
func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
|
||||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRepoReader returns true if user has explicit read access or higher to the repository.
|
|
||||||
func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) {
|
|
||||||
if repo.OwnerID == userID {
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{})
|
for _, a := range accesses {
|
||||||
|
userIDs.Add(a.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.LoadOwner(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !repo.Owner.IsOrganization() {
|
||||||
|
userIDs.Add(repo.Owner.ID)
|
||||||
|
} else {
|
||||||
|
teamUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, mode, unitType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userIDs.AddMultiple(teamUserIDs...)
|
||||||
|
}
|
||||||
|
return userIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
||||||
|
|||||||
@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
||||||
require.NoError(t, db.Insert(ctx, team))
|
require.NoError(t, db.Insert(ctx, team))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||||
|
|
||||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
|
||||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||||
@ -219,6 +219,15 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||||
|
|
||||||
|
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeRead, unit.TypeIssues)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, users, 1)
|
||||||
|
assert.Equal(t, user.ID, users[0].ID)
|
||||||
|
|
||||||
|
users, err = GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, users)
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||||
@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||||||
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
||||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
|
||||||
|
|
||||||
|
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, users, 1)
|
||||||
|
assert.Equal(t, user.ID, users[0].ID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,8 +41,8 @@ func naturalSortAdvance(str string, pos int) (end int, isNumber bool) {
|
|||||||
return end, isNumber
|
return end, isNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
// NaturalSortLess compares two strings so that they could be sorted in natural order
|
// NaturalSortCompare compares two strings so that they could be sorted in natural order
|
||||||
func NaturalSortLess(s1, s2 string) bool {
|
func NaturalSortCompare(s1, s2 string) int {
|
||||||
// There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997
|
// There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997
|
||||||
// text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997
|
// text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997
|
||||||
// So we need to handle the number parts by ourselves
|
// So we need to handle the number parts by ourselves
|
||||||
@ -55,16 +55,16 @@ func NaturalSortLess(s1, s2 string) bool {
|
|||||||
if isNum1 && isNum2 {
|
if isNum1 && isNum2 {
|
||||||
if part1 != part2 {
|
if part1 != part2 {
|
||||||
if len(part1) != len(part2) {
|
if len(part1) != len(part2) {
|
||||||
return len(part1) < len(part2)
|
return len(part1) - len(part2)
|
||||||
}
|
}
|
||||||
return part1 < part2
|
return c.CompareString(part1, part2)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if cmp := c.CompareString(part1, part2); cmp != 0 {
|
if cmp := c.CompareString(part1, part2); cmp != 0 {
|
||||||
return cmp < 0
|
return cmp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos1, pos2 = end1, end2
|
pos1, pos2 = end1, end2
|
||||||
}
|
}
|
||||||
return len(s1) < len(s2)
|
return len(s1) - len(s2)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,10 @@ import (
|
|||||||
|
|
||||||
func TestNaturalSortLess(t *testing.T) {
|
func TestNaturalSortLess(t *testing.T) {
|
||||||
testLess := func(s1, s2 string) {
|
testLess := func(s1, s2 string) {
|
||||||
assert.True(t, NaturalSortLess(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
|
assert.Negative(t, NaturalSortCompare(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
|
||||||
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
|
||||||
}
|
}
|
||||||
testEqual := func(s1, s2 string) {
|
testEqual := func(s1, s2 string) {
|
||||||
assert.False(t, NaturalSortLess(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
|
assert.Zero(t, NaturalSortCompare(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
|
||||||
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testEqual("", "")
|
testEqual("", "")
|
||||||
|
|||||||
@ -173,7 +173,6 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
|
|||||||
} else if entries, err = commit.Tree.ListEntries(); err != nil {
|
} else if entries, err = commit.Tree.ListEntries(); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
entries.Sort()
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.Run(benchmark.name, func(b *testing.B) {
|
b.Run(benchmark.name, func(b *testing.B) {
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@ -30,7 +31,11 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
|||||||
|
|
||||||
remainingCommitID := commitID
|
remainingCommitID := commitID
|
||||||
path := ""
|
path := ""
|
||||||
currentTree := notes.Tree.gogitTree
|
currentTree, err := notes.Tree.gogitTreeObject()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID)
|
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID)
|
||||||
var file *object.File
|
var file *object.File
|
||||||
for len(remainingCommitID) > 2 {
|
for len(remainingCommitID) > 2 {
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
|
|
||||||
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
|
|
||||||
return parseTreeEntries(data, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
|
||||||
entries := make([]*TreeEntry, 0, 10)
|
|
||||||
for pos := 0; pos < len(data); {
|
|
||||||
// expect line to be of the form "<mode> <type> <sha> <space-padded-size>\t<filename>"
|
|
||||||
entry := new(TreeEntry)
|
|
||||||
entry.gogitTreeEntry = &object.TreeEntry{}
|
|
||||||
entry.ptree = ptree
|
|
||||||
if pos+6 > len(data) {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
|
||||||
}
|
|
||||||
switch string(data[pos : pos+6]) {
|
|
||||||
case "100644":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Regular
|
|
||||||
pos += 12 // skip over "100644 blob "
|
|
||||||
case "100755":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Executable
|
|
||||||
pos += 12 // skip over "100755 blob "
|
|
||||||
case "120000":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Symlink
|
|
||||||
pos += 12 // skip over "120000 blob "
|
|
||||||
case "160000":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Submodule
|
|
||||||
pos += 14 // skip over "160000 object "
|
|
||||||
case "040000":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Dir
|
|
||||||
pos += 12 // skip over "040000 tree "
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// in hex format, not byte format ....
|
|
||||||
if pos+hash.Size*2 > len(data) {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
entry.ID, err = NewIDFromString(string(data[pos : pos+hash.Size*2]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid ls-tree output: %w", err)
|
|
||||||
}
|
|
||||||
entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue())
|
|
||||||
pos += 41 // skip over sha and trailing space
|
|
||||||
|
|
||||||
end := pos + bytes.IndexByte(data[pos:], '\t')
|
|
||||||
if end < pos {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree -l output: %s", string(data))
|
|
||||||
}
|
|
||||||
entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(data[pos:end])), 10, 64)
|
|
||||||
entry.sized = true
|
|
||||||
|
|
||||||
pos = end + 1
|
|
||||||
|
|
||||||
end = pos + bytes.IndexByte(data[pos:], '\n')
|
|
||||||
if end < pos {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case entry name is surrounded by double quotes(it happens only in git-shell).
|
|
||||||
if data[pos] == '"' {
|
|
||||||
var err error
|
|
||||||
entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entry.gogitTreeEntry.Name = string(data[pos:end])
|
|
||||||
}
|
|
||||||
|
|
||||||
pos = end + 1
|
|
||||||
entries = append(entries, entry)
|
|
||||||
}
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseTreeEntries(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
Input string
|
|
||||||
Expected []*TreeEntry
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Input: "",
|
|
||||||
Expected: []*TreeEntry{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n",
|
|
||||||
Expected: []*TreeEntry{
|
|
||||||
{
|
|
||||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
|
||||||
Name: "example/file2.txt",
|
|
||||||
Mode: filemode.Regular,
|
|
||||||
},
|
|
||||||
size: 1022,
|
|
||||||
sized: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: "120000 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 234131\t\"example/\\n.txt\"\n" +
|
|
||||||
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n",
|
|
||||||
Expected: []*TreeEntry{
|
|
||||||
{
|
|
||||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
|
||||||
Name: "example/\n.txt",
|
|
||||||
Mode: filemode.Symlink,
|
|
||||||
},
|
|
||||||
size: 234131,
|
|
||||||
sized: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
|
||||||
sized: true,
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Hash: plumbing.Hash(MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()),
|
|
||||||
Name: "example",
|
|
||||||
Mode: filemode.Dir,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
if len(entries) > 1 {
|
|
||||||
fmt.Println(testCase.Expected[0].ID)
|
|
||||||
fmt.Println(entries[0].ID)
|
|
||||||
}
|
|
||||||
assert.EqualValues(t, testCase.Expected, entries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -1,8 +1,6 @@
|
|||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -107,7 +107,7 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
commit.Tree.ID = ParseGogitHash(tree.Hash)
|
commit.Tree.ID = ParseGogitHash(tree.Hash)
|
||||||
commit.Tree.gogitTree = tree
|
commit.Tree.resolvedGogitTreeObject = tree
|
||||||
|
|
||||||
return commit, nil
|
return commit, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tree := NewTree(repo, id)
|
tree := NewTree(repo, id)
|
||||||
tree.gogitTree = gogitTree
|
tree.resolvedGogitTreeObject = gogitTree
|
||||||
return tree, nil
|
return tree, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,21 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TreeCommon struct {
|
||||||
|
ID ObjectID
|
||||||
|
ResolvedID ObjectID
|
||||||
|
|
||||||
|
repo *Repository
|
||||||
|
ptree *Tree // parent tree
|
||||||
|
}
|
||||||
|
|
||||||
// NewTree create a new tree according the repository and tree id
|
// NewTree create a new tree according the repository and tree id
|
||||||
func NewTree(repo *Repository, id ObjectID) *Tree {
|
func NewTree(repo *Repository, id ObjectID) *Tree {
|
||||||
return &Tree{
|
return &Tree{
|
||||||
ID: id,
|
TreeCommon: TreeCommon{
|
||||||
repo: repo,
|
ID: id,
|
||||||
|
repo: repo,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,22 +11,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||||
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
||||||
if len(relpath) == 0 {
|
if len(relpath) == 0 {
|
||||||
return &TreeEntry{
|
return &TreeEntry{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
// Type: ObjectTree,
|
ptree: t,
|
||||||
ptree: t,
|
name: "",
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
entryMode: EntryModeTree,
|
||||||
Name: "",
|
|
||||||
Mode: filemode.Dir,
|
|
||||||
Hash: plumbing.Hash(t.ID.RawValue()),
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,60 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TreeEntry the leaf in the git tree
|
||||||
|
type TreeEntry struct {
|
||||||
|
ID ObjectID
|
||||||
|
|
||||||
|
name string
|
||||||
|
ptree *Tree
|
||||||
|
|
||||||
|
entryMode EntryMode
|
||||||
|
|
||||||
|
size int64
|
||||||
|
sized bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the entry (base name)
|
||||||
|
func (te *TreeEntry) Name() string {
|
||||||
|
return te.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns the mode of the entry
|
||||||
|
func (te *TreeEntry) Mode() EntryMode {
|
||||||
|
return te.entryMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubModule if the entry is a submodule
|
||||||
|
func (te *TreeEntry) IsSubModule() bool {
|
||||||
|
return te.entryMode.IsSubModule()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir if the entry is a sub dir
|
||||||
|
func (te *TreeEntry) IsDir() bool {
|
||||||
|
return te.entryMode.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLink if the entry is a symlink
|
||||||
|
func (te *TreeEntry) IsLink() bool {
|
||||||
|
return te.entryMode.IsLink()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegular if the entry is a regular file
|
||||||
|
func (te *TreeEntry) IsRegular() bool {
|
||||||
|
return te.entryMode.IsRegular()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||||
|
func (te *TreeEntry) IsExecutable() bool {
|
||||||
|
return te.entryMode.IsExecutable()
|
||||||
|
}
|
||||||
|
|
||||||
// Type returns the type of the entry (commit, tree, blob)
|
// Type returns the type of the entry (commit, tree, blob)
|
||||||
func (te *TreeEntry) Type() string {
|
func (te *TreeEntry) Type() string {
|
||||||
switch te.Mode() {
|
switch te.Mode() {
|
||||||
@ -109,49 +157,16 @@ func (te *TreeEntry) GetSubJumpablePathName() string {
|
|||||||
// Entries a list of entry
|
// Entries a list of entry
|
||||||
type Entries []*TreeEntry
|
type Entries []*TreeEntry
|
||||||
|
|
||||||
type customSortableEntries struct {
|
|
||||||
Comparer func(s1, s2 string) bool
|
|
||||||
Entries
|
|
||||||
}
|
|
||||||
|
|
||||||
var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
|
|
||||||
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
|
|
||||||
return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
|
|
||||||
},
|
|
||||||
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
|
|
||||||
return cmp(t1.Name(), t2.Name())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctes customSortableEntries) Len() int { return len(ctes.Entries) }
|
|
||||||
|
|
||||||
func (ctes customSortableEntries) Swap(i, j int) {
|
|
||||||
ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctes customSortableEntries) Less(i, j int) bool {
|
|
||||||
t1, t2 := ctes.Entries[i], ctes.Entries[j]
|
|
||||||
var k int
|
|
||||||
for k = 0; k < len(sorter)-1; k++ {
|
|
||||||
s := sorter[k]
|
|
||||||
switch {
|
|
||||||
case s(t1, t2, ctes.Comparer):
|
|
||||||
return true
|
|
||||||
case s(t2, t1, ctes.Comparer):
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sorter[k](t1, t2, ctes.Comparer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort sort the list of entry
|
|
||||||
func (tes Entries) Sort() {
|
|
||||||
sort.Sort(customSortableEntries{func(s1, s2 string) bool {
|
|
||||||
return s1 < s2
|
|
||||||
}, tes})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomSort customizable string comparing sort entry list
|
// CustomSort customizable string comparing sort entry list
|
||||||
func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) {
|
func (tes Entries) CustomSort(cmp func(s1, s2 string) int) {
|
||||||
sort.Sort(customSortableEntries{cmp, tes})
|
slices.SortFunc(tes, func(a, b *TreeEntry) int {
|
||||||
|
s1Dir, s2Dir := a.IsDir() || a.IsSubModule(), b.IsDir() || b.IsSubModule()
|
||||||
|
if s1Dir != s2Dir {
|
||||||
|
if s1Dir {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return cmp(a.Name(), b.Name())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,25 +12,21 @@ import (
|
|||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TreeEntry the leaf in the git tree
|
// gogitFileModeToEntryMode converts go-git filemode to EntryMode
|
||||||
type TreeEntry struct {
|
func gogitFileModeToEntryMode(mode filemode.FileMode) EntryMode {
|
||||||
ID ObjectID
|
return EntryMode(mode)
|
||||||
|
|
||||||
gogitTreeEntry *object.TreeEntry
|
|
||||||
ptree *Tree
|
|
||||||
|
|
||||||
size int64
|
|
||||||
sized bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the name of the entry
|
func entryModeToGogitFileMode(mode EntryMode) filemode.FileMode {
|
||||||
func (te *TreeEntry) Name() string {
|
return filemode.FileMode(mode)
|
||||||
return te.gogitTreeEntry.Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode returns the mode of the entry
|
func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry {
|
||||||
func (te *TreeEntry) Mode() EntryMode {
|
return &object.TreeEntry{
|
||||||
return EntryMode(te.gogitTreeEntry.Mode)
|
Name: te.name,
|
||||||
|
Mode: entryModeToGogitFileMode(te.entryMode),
|
||||||
|
Hash: plumbing.Hash(te.ID.RawValue()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the size of the entry
|
// Size returns the size of the entry
|
||||||
@ -41,7 +37,11 @@ func (te *TreeEntry) Size() int64 {
|
|||||||
return te.size
|
return te.size
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
|
ptreeGogitTree, err := te.ptree.gogitTreeObject()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
file, err := ptreeGogitTree.TreeEntryFile(te.toGogitTreeEntry())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -51,40 +51,15 @@ func (te *TreeEntry) Size() int64 {
|
|||||||
return te.size
|
return te.size
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSubModule if the entry is a submodule
|
|
||||||
func (te *TreeEntry) IsSubModule() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Submodule
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDir if the entry is a sub dir
|
|
||||||
func (te *TreeEntry) IsDir() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLink if the entry is a symlink
|
|
||||||
func (te *TreeEntry) IsLink() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Symlink
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRegular if the entry is a regular file
|
|
||||||
func (te *TreeEntry) IsRegular() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Regular
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
|
||||||
func (te *TreeEntry) IsExecutable() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Executable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
// Blob returns the blob object the entry
|
||||||
func (te *TreeEntry) Blob() *Blob {
|
func (te *TreeEntry) Blob() *Blob {
|
||||||
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
|
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.toGogitTreeEntry().Hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Blob{
|
return &Blob{
|
||||||
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
|
ID: te.ID,
|
||||||
gogitEncodedObj: encodedObj,
|
gogitEncodedObj: encodedObj,
|
||||||
name: te.Name(),
|
name: te.Name(),
|
||||||
}
|
}
|
||||||
|
|||||||
27
modules/git/tree_entry_gogit_test.go
Normal file
27
modules/git/tree_entry_gogit_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEntryGogit(t *testing.T) {
|
||||||
|
cases := map[EntryMode]filemode.FileMode{
|
||||||
|
EntryModeBlob: filemode.Regular,
|
||||||
|
EntryModeCommit: filemode.Submodule,
|
||||||
|
EntryModeExec: filemode.Executable,
|
||||||
|
EntryModeSymlink: filemode.Symlink,
|
||||||
|
EntryModeTree: filemode.Dir,
|
||||||
|
}
|
||||||
|
for emode, fmode := range cases {
|
||||||
|
assert.EqualValues(t, fmode, entryModeToGogitFileMode(emode))
|
||||||
|
assert.EqualValues(t, emode, gogitFileModeToEntryMode(fmode))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,27 +7,6 @@ package git
|
|||||||
|
|
||||||
import "code.gitea.io/gitea/modules/log"
|
import "code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
// TreeEntry the leaf in the git tree
|
|
||||||
type TreeEntry struct {
|
|
||||||
ID ObjectID
|
|
||||||
ptree *Tree
|
|
||||||
|
|
||||||
entryMode EntryMode
|
|
||||||
name string
|
|
||||||
size int64
|
|
||||||
sized bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the entry (base name)
|
|
||||||
func (te *TreeEntry) Name() string {
|
|
||||||
return te.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mode returns the mode of the entry
|
|
||||||
func (te *TreeEntry) Mode() EntryMode {
|
|
||||||
return te.entryMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the size of the entry
|
// Size returns the size of the entry
|
||||||
func (te *TreeEntry) Size() int64 {
|
func (te *TreeEntry) Size() int64 {
|
||||||
if te.IsDir() {
|
if te.IsDir() {
|
||||||
@ -57,31 +36,6 @@ func (te *TreeEntry) Size() int64 {
|
|||||||
return te.size
|
return te.size
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSubModule if the entry is a submodule
|
|
||||||
func (te *TreeEntry) IsSubModule() bool {
|
|
||||||
return te.entryMode.IsSubModule()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDir if the entry is a sub dir
|
|
||||||
func (te *TreeEntry) IsDir() bool {
|
|
||||||
return te.entryMode.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLink if the entry is a symlink
|
|
||||||
func (te *TreeEntry) IsLink() bool {
|
|
||||||
return te.entryMode.IsLink()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRegular if the entry is a regular file
|
|
||||||
func (te *TreeEntry) IsRegular() bool {
|
|
||||||
return te.entryMode.IsRegular()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
|
||||||
func (te *TreeEntry) IsExecutable() bool {
|
|
||||||
return te.entryMode.IsExecutable()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
// Blob returns the blob object the entry
|
||||||
func (te *TreeEntry) Blob() *Blob {
|
func (te *TreeEntry) Blob() *Blob {
|
||||||
return &Blob{
|
return &Blob{
|
||||||
|
|||||||
@ -1,55 +1,29 @@
|
|||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand/v2"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTestEntries() Entries {
|
|
||||||
return Entries{
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEntriesSort(t *testing.T) {
|
|
||||||
entries := getTestEntries()
|
|
||||||
entries.Sort()
|
|
||||||
assert.Equal(t, "v1.0", entries[0].Name())
|
|
||||||
assert.Equal(t, "v12.0", entries[1].Name())
|
|
||||||
assert.Equal(t, "v2.0", entries[2].Name())
|
|
||||||
assert.Equal(t, "v2.1", entries[3].Name())
|
|
||||||
assert.Equal(t, "v2.12", entries[4].Name())
|
|
||||||
assert.Equal(t, "v2.2", entries[5].Name())
|
|
||||||
assert.Equal(t, "abc", entries[6].Name())
|
|
||||||
assert.Equal(t, "bcd", entries[7].Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEntriesCustomSort(t *testing.T) {
|
func TestEntriesCustomSort(t *testing.T) {
|
||||||
entries := getTestEntries()
|
entries := Entries{
|
||||||
entries.CustomSort(func(s1, s2 string) bool {
|
&TreeEntry{name: "a-dir", entryMode: EntryModeTree},
|
||||||
return s1 > s2
|
&TreeEntry{name: "a-submodule", entryMode: EntryModeCommit},
|
||||||
})
|
&TreeEntry{name: "b-dir", entryMode: EntryModeTree},
|
||||||
assert.Equal(t, "v2.2", entries[0].Name())
|
&TreeEntry{name: "b-submodule", entryMode: EntryModeCommit},
|
||||||
assert.Equal(t, "v2.12", entries[1].Name())
|
&TreeEntry{name: "a-file", entryMode: EntryModeBlob},
|
||||||
assert.Equal(t, "v2.1", entries[2].Name())
|
&TreeEntry{name: "b-file", entryMode: EntryModeBlob},
|
||||||
assert.Equal(t, "v2.0", entries[3].Name())
|
}
|
||||||
assert.Equal(t, "v12.0", entries[4].Name())
|
expected := slices.Clone(entries)
|
||||||
assert.Equal(t, "v1.0", entries[5].Name())
|
rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] })
|
||||||
assert.Equal(t, "bcd", entries[6].Name())
|
assert.NotEqual(t, expected, entries)
|
||||||
assert.Equal(t, "abc", entries[7].Name())
|
entries.CustomSort(strings.Compare)
|
||||||
|
assert.Equal(t, expected, entries)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,41 +15,34 @@ import (
|
|||||||
|
|
||||||
// Tree represents a flat directory listing.
|
// Tree represents a flat directory listing.
|
||||||
type Tree struct {
|
type Tree struct {
|
||||||
ID ObjectID
|
TreeCommon
|
||||||
ResolvedID ObjectID
|
|
||||||
repo *Repository
|
|
||||||
|
|
||||||
gogitTree *object.Tree
|
resolvedGogitTreeObject *object.Tree
|
||||||
|
|
||||||
// parent tree
|
|
||||||
ptree *Tree
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) loadTreeObject() error {
|
func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) {
|
||||||
gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
|
if t.resolvedGogitTreeObject == nil {
|
||||||
if err != nil {
|
t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
t.gogitTree = gogitTree
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
|
||||||
func (t *Tree) ListEntries() (Entries, error) {
|
|
||||||
if t.gogitTree == nil {
|
|
||||||
err := t.loadTreeObject()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return t.resolvedGogitTreeObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
entries := make([]*TreeEntry, len(t.gogitTree.Entries))
|
// ListEntries returns all entries of current tree.
|
||||||
for i, entry := range t.gogitTree.Entries {
|
func (t *Tree) ListEntries() (Entries, error) {
|
||||||
|
gogitTree, err := t.gogitTreeObject()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries := make([]*TreeEntry, len(gogitTree.Entries))
|
||||||
|
for i, gogitTreeEntry := range gogitTree.Entries {
|
||||||
entries[i] = &TreeEntry{
|
entries[i] = &TreeEntry{
|
||||||
ID: ParseGogitHash(entry.Hash),
|
ID: ParseGogitHash(gogitTreeEntry.Hash),
|
||||||
gogitTreeEntry: &t.gogitTree.Entries[i],
|
ptree: t,
|
||||||
ptree: t,
|
name: gogitTreeEntry.Name,
|
||||||
|
entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,37 +50,28 @@ func (t *Tree) ListEntries() (Entries, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
|
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
|
||||||
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) {
|
||||||
if t.gogitTree == nil {
|
gogitTree, err := t.gogitTreeObject()
|
||||||
err := t.loadTreeObject()
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries []*TreeEntry
|
walker := object.NewTreeWalker(gogitTree, true, nil)
|
||||||
seen := map[plumbing.Hash]bool{}
|
|
||||||
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
|
||||||
for {
|
for {
|
||||||
_, entry, err := walker.Next()
|
fullName, gogitTreeEntry, err := walker.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
} else if err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if seen[entry.Hash] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
convertedEntry := &TreeEntry{
|
convertedEntry := &TreeEntry{
|
||||||
ID: ParseGogitHash(entry.Hash),
|
ID: ParseGogitHash(gogitTreeEntry.Hash),
|
||||||
gogitTreeEntry: &entry,
|
name: fullName, // FIXME: the "name" field is abused, here it is a full path
|
||||||
ptree: t,
|
ptree: t, // FIXME: this ptree is not right, fortunately it isn't really used
|
||||||
|
entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode),
|
||||||
}
|
}
|
||||||
entries = append(entries, convertedEntry)
|
entries = append(entries, convertedEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,18 +14,10 @@ import (
|
|||||||
|
|
||||||
// Tree represents a flat directory listing.
|
// Tree represents a flat directory listing.
|
||||||
type Tree struct {
|
type Tree struct {
|
||||||
ID ObjectID
|
TreeCommon
|
||||||
ResolvedID ObjectID
|
|
||||||
repo *Repository
|
|
||||||
|
|
||||||
// parent tree
|
|
||||||
ptree *Tree
|
|
||||||
|
|
||||||
entries Entries
|
entries Entries
|
||||||
entriesParsed bool
|
entriesParsed bool
|
||||||
|
|
||||||
entriesRecursive Entries
|
|
||||||
entriesRecursiveParsed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
// ListEntries returns all entries of current tree.
|
||||||
@ -94,10 +86,6 @@ func (t *Tree) ListEntries() (Entries, error) {
|
|||||||
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
|
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
|
||||||
// extraArgs could be "-l" to get the size, which is slower
|
// extraArgs could be "-l" to get the size, which is slower
|
||||||
func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
|
func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
|
||||||
if t.entriesRecursiveParsed {
|
|
||||||
return t.entriesRecursive, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
|
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
|
||||||
AddArguments(extraArgs...).
|
AddArguments(extraArgs...).
|
||||||
AddDynamicArguments(t.ID.String()).
|
AddDynamicArguments(t.ID.String()).
|
||||||
@ -107,13 +95,9 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e
|
|||||||
return nil, runErr
|
return nil, runErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
// FIXME: the "name" field is abused, here it is a full path
|
||||||
t.entriesRecursive, err = parseTreeEntries(stdout, t)
|
// FIXME: this ptree is not right, fortunately it isn't really used
|
||||||
if err == nil {
|
return parseTreeEntries(stdout, t)
|
||||||
t.entriesRecursiveParsed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.entriesRecursive, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
||||||
|
|||||||
@ -96,6 +96,7 @@ var OAuth2 = struct {
|
|||||||
InvalidateRefreshTokens bool
|
InvalidateRefreshTokens bool
|
||||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||||
|
JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"`
|
||||||
MaxTokenLength int
|
MaxTokenLength int
|
||||||
DefaultApplications []string
|
DefaultApplications []string
|
||||||
}{
|
}{
|
||||||
|
|||||||
@ -250,6 +250,7 @@ func (a *AzureBlobStorage) Delete(path string) error {
|
|||||||
func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
|
func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
|
||||||
blobClient := a.getBlobClient(path)
|
blobClient := a.getBlobClient(path)
|
||||||
|
|
||||||
|
// TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
u, err := blobClient.GetSASURL(sas.BlobPermissions{
|
u, err := blobClient.GetSASURL(sas.BlobPermissions{
|
||||||
Read: true,
|
Read: true,
|
||||||
|
|||||||
@ -279,20 +279,44 @@ func (m *MinioStorage) Delete(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||||
func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||||
// copy serveDirectReqParams
|
// copy serveDirectReqParams
|
||||||
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
|
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we?
|
|
||||||
reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
|
// Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head.
|
||||||
|
// So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI.
|
||||||
|
// Detect content type by extension name, only support the well-known safe types for inline rendering.
|
||||||
|
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
|
||||||
|
ext := path.Ext(name)
|
||||||
|
inlineExtMimeTypes := map[string]string{
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".avif": "image/avif",
|
||||||
|
// ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy
|
||||||
|
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
|
||||||
|
// TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType"
|
||||||
|
}
|
||||||
|
if mimeType, ok := inlineExtMimeTypes[ext]; ok {
|
||||||
|
reqParams.Set("response-content-type", mimeType)
|
||||||
|
reqParams.Set("response-content-disposition", "inline")
|
||||||
|
} else {
|
||||||
|
reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)))
|
||||||
|
}
|
||||||
|
|
||||||
expires := 5 * time.Minute
|
expires := 5 * time.Minute
|
||||||
if method == http.MethodHead {
|
if method == http.MethodHead {
|
||||||
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||||
return u, convertMinioErr(err)
|
return u, convertMinioErr(err)
|
||||||
}
|
}
|
||||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||||
return u, convertMinioErr(err)
|
return u, convertMinioErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
public/assets/img/svg/gitea-colorblind-blueyellow.svg
generated
Normal file
1
public/assets/img/svg/gitea-colorblind-blueyellow.svg
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="svg gitea-colorblind-blueyellow" width="16" height="16" aria-hidden="true"><g clip-path="url(#gitea-colorblind-blueyellow__a)"><rect width="40" height="40" fill="#0000" rx="20"/><path fill="#1d69e0" d="M34.284 34.284c7.81-7.81 7.81-20.474 0-28.284L6 34.284c7.81 7.81 20.474 7.81 28.284 0"/><path fill="#fa4549" d="M34.283 34.284c7.81-7.81 7.81-20.474 0-28.284L20.14 20.142z"/><circle cx="20" cy="20" r="18" fill="#0000" stroke="#aaa" stroke-width="4"/></g><defs><clipPath id="gitea-colorblind-blueyellow__a"><rect width="40" height="40" fill="#0000" rx="20"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
@ -148,7 +148,7 @@ func EnumeratePackages(ctx *context.Context) {
|
|||||||
Timestamp: fileMetadata.Timestamp,
|
Timestamp: fileMetadata.Timestamp,
|
||||||
Build: fileMetadata.Build,
|
Build: fileMetadata.Build,
|
||||||
BuildNumber: fileMetadata.BuildNumber,
|
BuildNumber: fileMetadata.BuildNumber,
|
||||||
Dependencies: fileMetadata.Dependencies,
|
Dependencies: util.SliceNilAsEmpty(fileMetadata.Dependencies),
|
||||||
License: versionMetadata.License,
|
License: versionMetadata.License,
|
||||||
LicenseFamily: versionMetadata.LicenseFamily,
|
LicenseFamily: versionMetadata.LicenseFamily,
|
||||||
HashMD5: pfd.Blob.HashMD5,
|
HashMD5: pfd.Blob.HashMD5,
|
||||||
|
|||||||
@ -897,7 +897,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
|||||||
} else {
|
} else {
|
||||||
whitelistUsers = protectBranch.WhitelistUserIDs
|
whitelistUsers = protectBranch.WhitelistUserIDs
|
||||||
}
|
}
|
||||||
if form.ForcePushAllowlistDeployKeys != nil {
|
if form.ForcePushAllowlistUsernames != nil {
|
||||||
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
|
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if user_model.IsErrUserNotExist(err) {
|
if user_model.IsErrUserNotExist(err) {
|
||||||
|
|||||||
@ -370,11 +370,11 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
|
|||||||
},
|
},
|
||||||
Signoff: commonOpts.Signoff,
|
Signoff: commonOpts.Signoff,
|
||||||
}
|
}
|
||||||
if commonOpts.Dates.Author.IsZero() {
|
if changeFileOpts.Dates.Author.IsZero() {
|
||||||
commonOpts.Dates.Author = time.Now()
|
changeFileOpts.Dates.Author = time.Now()
|
||||||
}
|
}
|
||||||
if commonOpts.Dates.Committer.IsZero() {
|
if changeFileOpts.Dates.Committer.IsZero() {
|
||||||
commonOpts.Dates.Committer = time.Now()
|
changeFileOpts.Dates.Committer = time.Now()
|
||||||
}
|
}
|
||||||
ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
|
ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
|
||||||
}
|
}
|
||||||
|
|||||||
@ -436,6 +436,7 @@ func ViewProject(ctx *context.Context) {
|
|||||||
ctx.Data["Project"] = project
|
ctx.Data["Project"] = project
|
||||||
ctx.Data["IssuesMap"] = issuesMap
|
ctx.Data["IssuesMap"] = issuesMap
|
||||||
ctx.Data["Columns"] = columns
|
ctx.Data["Columns"] = columns
|
||||||
|
ctx.Data["Title"] = fmt.Sprintf("%s - %s", project.Title, ctx.ContextUser.DisplayName())
|
||||||
|
|
||||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||||
ctx.ServerError("RenderUserOrgHeader", err)
|
ctx.ServerError("RenderUserOrgHeader", err)
|
||||||
|
|||||||
@ -415,6 +415,8 @@ func Diff(ctx *context.Context) {
|
|||||||
ctx.ServerError("PostProcessCommitMessage", err)
|
ctx.ServerError("PostProcessCommitMessage", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else if !git.IsErrNotExist(err) {
|
||||||
|
log.Error("GetNote: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID)
|
pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID)
|
||||||
|
|||||||
@ -73,10 +73,9 @@ func SettingsProtectedBranch(c *context.Context) {
|
|||||||
|
|
||||||
c.Data["PageIsSettingsBranches"] = true
|
c.Data["PageIsSettingsBranches"] = true
|
||||||
c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
|
c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
|
||||||
|
users, err := access_model.GetUsersWithUnitAccess(c, c.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
users, err := access_model.GetRepoReaders(c, c.Repo.Repository)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ServerError("Repo.Repository.GetReaders", err)
|
c.ServerError("GetUsersWithUnitAccess", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Data["Users"] = users
|
c.Data["Users"] = users
|
||||||
|
|||||||
@ -149,9 +149,9 @@ func setTagsContext(ctx *context.Context) error {
|
|||||||
}
|
}
|
||||||
ctx.Data["ProtectedTags"] = protectedTags
|
ctx.Data["ProtectedTags"] = protectedTags
|
||||||
|
|
||||||
users, err := access_model.GetRepoReaders(ctx, ctx.Repo.Repository)
|
users, err := access_model.GetUsersWithUnitAccess(ctx, ctx.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Repo.Repository.GetReaders", err)
|
ctx.ServerError("GetUsersWithUnitAccess", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx.Data["Users"] = users
|
ctx.Data["Users"] = users
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TreeList(ctx *context.Context) {
|
|||||||
ctx.ServerError("ListEntriesRecursiveFast", err)
|
ctx.ServerError("ListEntriesRecursiveFast", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entries.CustomSort(base.NaturalSortLess)
|
entries.CustomSort(base.NaturalSortCompare)
|
||||||
|
|
||||||
files := make([]string, 0, len(entries))
|
files := make([]string, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
|
|||||||
@ -95,6 +95,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b
|
|||||||
|
|
||||||
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
|
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
|
||||||
if err != nil { // fallback to a plain file
|
if err != nil { // fallback to a plain file
|
||||||
|
fi.lfsMeta = &pointer
|
||||||
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
|
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
|
||||||
return buf, dataRc, fi, nil
|
return buf, dataRc, fi, nil
|
||||||
}
|
}
|
||||||
@ -307,7 +308,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
|||||||
ctx.ServerError("ListEntries", err)
|
ctx.ServerError("ListEntries", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
allEntries.CustomSort(base.NaturalSortLess)
|
allEntries.CustomSort(base.NaturalSortCompare)
|
||||||
|
|
||||||
commitInfoCtx := gocontext.Context(ctx)
|
commitInfoCtx := gocontext.Context(ctx)
|
||||||
if timeout > 0 {
|
if timeout > 0 {
|
||||||
|
|||||||
@ -67,7 +67,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*
|
|||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
||||||
fullPath := path.Join(parentDir, entry.Name())
|
fullPath := path.Join(parentDir, entry.Name())
|
||||||
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
|
if readmeFiles[i] == nil || base.NaturalSortCompare(readmeFiles[i].Name(), entry.Blob().Name()) < 0 {
|
||||||
if entry.IsLink() {
|
if entry.IsLink() {
|
||||||
res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
|
res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
|
||||||
if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
|
if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
|
||||||
|
|||||||
@ -567,7 +567,7 @@ func WikiPages(ctx *context.Context) {
|
|||||||
ctx.ServerError("ListEntries", err)
|
ctx.ServerError("ListEntries", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
allEntries.CustomSort(base.NaturalSortLess)
|
allEntries.CustomSort(base.NaturalSortCompare)
|
||||||
|
|
||||||
entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
|
entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -105,9 +105,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if source.AttributeAvatar != "" {
|
if source.AttributeAvatar != "" {
|
||||||
if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil {
|
_ = user_service.UploadAvatar(ctx, user, sr.Avatar)
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -139,7 +139,7 @@ func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T,
|
|||||||
|
|
||||||
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
|
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
|
||||||
func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection {
|
func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection {
|
||||||
readers, err := access_model.GetRepoReaders(ctx, repo)
|
readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetRepoReaders: %v", err)
|
log.Error("GetRepoReaders: %v", err)
|
||||||
}
|
}
|
||||||
@ -720,7 +720,7 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.
|
|||||||
|
|
||||||
// ToTagProtection convert a git.ProtectedTag to an api.TagProtection
|
// ToTagProtection convert a git.ProtectedTag to an api.TagProtection
|
||||||
func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection {
|
func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection {
|
||||||
readers, err := access_model.GetRepoReaders(ctx, repo)
|
readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetRepoReaders: %v", err)
|
log.Error("GetRepoReaders: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,8 +112,12 @@ func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt
|
|||||||
// to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer.
|
// to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer.
|
||||||
// * https://accounts.google.com/.well-known/openid-configuration
|
// * https://accounts.google.com/.well-known/openid-configuration
|
||||||
// * https://github.com/login/oauth/.well-known/openid-configuration
|
// * https://github.com/login/oauth/.well-known/openid-configuration
|
||||||
|
issuer := setting.OAuth2.JWTClaimIssuer
|
||||||
|
if issuer == "" {
|
||||||
|
issuer = strings.TrimSuffix(setting.AppURL, "/")
|
||||||
|
}
|
||||||
return jwt.RegisteredClaims{
|
return jwt.RegisteredClaims{
|
||||||
Issuer: strings.TrimSuffix(setting.AppURL, "/"),
|
Issuer: issuer,
|
||||||
Audience: []string{clientID},
|
Audience: []string{clientID},
|
||||||
Subject: strconv.FormatInt(grantUserID, 10),
|
Subject: strconv.FormatInt(grantUserID, 10),
|
||||||
ExpiresAt: exp,
|
ExpiresAt: exp,
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import (
|
|||||||
func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error {
|
func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error {
|
||||||
avatarData, err := avatar.ProcessAvatarImage(data)
|
avatarData, err := avatar.ProcessAvatarImage(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("UploadAvatar: failed to process repo avatar image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newAvatar := avatar.HashAvatar(repo.ID, data)
|
newAvatar := avatar.HashAvatar(repo.ID, data)
|
||||||
@ -36,19 +36,19 @@ func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte)
|
|||||||
// Then repo will be removed - only it avatar file will be removed
|
// Then repo will be removed - only it avatar file will be removed
|
||||||
repo.Avatar = newAvatar
|
repo.Avatar = newAvatar
|
||||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "avatar"); err != nil {
|
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "avatar"); err != nil {
|
||||||
return fmt.Errorf("UploadAvatar: Update repository avatar: %w", err)
|
return fmt.Errorf("UploadAvatar: failed to update repository avatar: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error {
|
if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||||
_, err := w.Write(avatarData)
|
_, err := w.Write(avatarData)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %w", repo.RelativePath(), newAvatar, err)
|
return fmt.Errorf("UploadAvatar: failed to save repo avatar %s: %w", newAvatar, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(oldAvatarPath) > 0 {
|
if len(oldAvatarPath) > 0 {
|
||||||
if err := storage.RepoAvatars.Delete(oldAvatarPath); err != nil {
|
if err := storage.RepoAvatars.Delete(oldAvatarPath); err != nil {
|
||||||
return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %w", oldAvatarPath, err)
|
return fmt.Errorf("UploadAvatar: failed to remove old repo avatar %s: %w", oldAvatarPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -21,21 +21,21 @@ import (
|
|||||||
func UploadAvatar(ctx context.Context, u *user_model.User, data []byte) error {
|
func UploadAvatar(ctx context.Context, u *user_model.User, data []byte) error {
|
||||||
avatarData, err := avatar.ProcessAvatarImage(data)
|
avatarData, err := avatar.ProcessAvatarImage(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("UploadAvatar: failed to process user avatar image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
u.UseCustomAvatar = true
|
u.UseCustomAvatar = true
|
||||||
u.Avatar = avatar.HashAvatar(u.ID, data)
|
u.Avatar = avatar.HashAvatar(u.ID, data)
|
||||||
if err = user_model.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil {
|
if err = user_model.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil {
|
||||||
return fmt.Errorf("updateUser: %w", err)
|
return fmt.Errorf("UploadAvatar: failed to update user avatar: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
|
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||||
_, err := w.Write(avatarData)
|
_, err := w.Write(avatarData)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
|
return fmt.Errorf("UploadAvatar: failed to save user avatar %s: %w", u.CustomAvatarRelativePath(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -39,6 +39,9 @@ func (info *ThemeMetaInfo) GetDescription() string {
|
|||||||
if info.ColorblindType == "red-green" {
|
if info.ColorblindType == "red-green" {
|
||||||
return "Red-green colorblind friendly"
|
return "Red-green colorblind friendly"
|
||||||
}
|
}
|
||||||
|
if info.ColorblindType == "blue-yellow" {
|
||||||
|
return "Blue-yellow colorblind friendly"
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +49,9 @@ func (info *ThemeMetaInfo) GetExtraIconName() string {
|
|||||||
if info.ColorblindType == "red-green" {
|
if info.ColorblindType == "red-green" {
|
||||||
return "gitea-colorblind-redgreen"
|
return "gitea-colorblind-redgreen"
|
||||||
}
|
}
|
||||||
|
if info.ColorblindType == "blue-yellow" {
|
||||||
|
return "gitea-colorblind-blueyellow"
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -78,18 +78,6 @@
|
|||||||
{{ctx.Locale.Tr "repo.release.downloads"}}
|
{{ctx.Locale.Tr "repo.release.downloads"}}
|
||||||
</summary>
|
</summary>
|
||||||
<ul class="ui divided list attachment-list">
|
<ul class="ui divided list attachment-list">
|
||||||
{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
|
|
||||||
<li class="item">
|
|
||||||
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow">
|
|
||||||
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="item">
|
|
||||||
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">
|
|
||||||
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{{end}}
|
|
||||||
{{range $att := $release.Attachments}}
|
{{range $att := $release.Attachments}}
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
|
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
|
||||||
@ -105,6 +93,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
|
||||||
|
<li class="item">
|
||||||
|
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow">
|
||||||
|
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="item">
|
||||||
|
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">
|
||||||
|
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{{if .HeatmapData}}
|
{{if .HeatmapData}}
|
||||||
|
<div class="activity-heatmap-container">
|
||||||
<div id="user-heatmap" class="is-loading"
|
<div id="user-heatmap" class="is-loading"
|
||||||
data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}"
|
data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}"
|
||||||
data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" (ctx.Locale.PrettyNumber .HeatmapTotalContributions)}}"
|
data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" (ctx.Locale.PrettyNumber .HeatmapTotalContributions)}}"
|
||||||
@ -6,5 +7,6 @@
|
|||||||
data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}"
|
data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}"
|
||||||
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"
|
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"
|
||||||
></div>
|
></div>
|
||||||
<div class="divider"></div>
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -237,6 +237,8 @@ func TestPackageConda(t *testing.T) {
|
|||||||
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5)
|
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5)
|
||||||
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256)
|
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256)
|
||||||
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size)
|
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size)
|
||||||
|
assert.NotNil(t, packageInfo.Dependencies)
|
||||||
|
assert.Empty(t, packageInfo.Dependencies)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run(".conda", func(t *testing.T) {
|
t.Run(".conda", func(t *testing.T) {
|
||||||
@ -268,6 +270,8 @@ func TestPackageConda(t *testing.T) {
|
|||||||
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5)
|
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5)
|
||||||
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256)
|
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256)
|
||||||
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size)
|
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size)
|
||||||
|
assert.NotNil(t, packageInfo.Dependencies)
|
||||||
|
assert.Empty(t, packageInfo.Dependencies)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -919,20 +919,32 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testOAuth2WellKnown(t *testing.T) {
|
func testOAuth2WellKnown(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")()
|
||||||
urlOpenidConfiguration := "/.well-known/openid-configuration"
|
urlOpenidConfiguration := "/.well-known/openid-configuration"
|
||||||
|
|
||||||
defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")()
|
t.Run("WellKnown", func(t *testing.T) {
|
||||||
req := NewRequest(t, "GET", urlOpenidConfiguration)
|
req := NewRequest(t, "GET", urlOpenidConfiguration)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
var respMap map[string]any
|
var respMap map[string]any
|
||||||
DecodeJSON(t, resp, &respMap)
|
DecodeJSON(t, resp, &respMap)
|
||||||
assert.Equal(t, "https://try.gitea.io", respMap["issuer"])
|
assert.Equal(t, "https://try.gitea.io", respMap["issuer"])
|
||||||
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
|
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
|
||||||
assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"])
|
assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"])
|
||||||
assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"])
|
assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"])
|
||||||
assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"])
|
assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"])
|
||||||
assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"])
|
assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"])
|
||||||
assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"])
|
assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WellKnownWithIssuer", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.OAuth2.JWTClaimIssuer, "https://try.gitea.io/")()
|
||||||
|
req := NewRequest(t, "GET", urlOpenidConfiguration)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var respMap map[string]any
|
||||||
|
DecodeJSON(t, resp, &respMap)
|
||||||
|
assert.Equal(t, "https://try.gitea.io/", respMap["issuer"]) // has trailing by JWTClaimIssuer
|
||||||
|
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
|
||||||
|
})
|
||||||
|
|
||||||
defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
|
defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
|
||||||
MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
|
MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
|
||||||
|
|||||||
@ -626,7 +626,6 @@ img.ui.avatar,
|
|||||||
font-family: var(--fonts-monospace);
|
font-family: var(--fonts-monospace);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
padding: 3px 5px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,23 +4,44 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* before the Vue component is mounted, show a loading indicator with dummy size */
|
.activity-heatmap-container {
|
||||||
/* the ratio is guesswork, see https://github.com/razorness/vue3-calendar-heatmap/issues/26 */
|
container-type: inline-size;
|
||||||
#user-heatmap.is-loading {
|
|
||||||
aspect-ratio: 5.415; /* the size is about 790 x 145 */
|
|
||||||
}
|
}
|
||||||
.user.profile #user-heatmap.is-loading {
|
|
||||||
aspect-ratio: 5.645; /* the size is about 953 x 169 */
|
@container (width > 0) {
|
||||||
|
#user-heatmap {
|
||||||
|
/* Set element to fixed height so that it does not resize after load. The calculation is complex
|
||||||
|
because the element does not scale with a fixed aspect ratio. */
|
||||||
|
height: calc((100cqw / 5) - (100cqw / 25) + 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback height adjustment above for browsers that don't support container queries */
|
||||||
|
@supports not (container-type: inline-size) {
|
||||||
|
/* Before the Vue component is mounted, show a loading indicator with dummy size */
|
||||||
|
/* The ratio is guesswork for legacy browsers, new browsers use the "@container" approach above */
|
||||||
|
#user-heatmap.is-loading {
|
||||||
|
aspect-ratio: 5.4823972051; /* the size is about 816 x 148.84 */
|
||||||
|
}
|
||||||
|
.user.profile #user-heatmap.is-loading {
|
||||||
|
aspect-ratio: 5.6290608387; /* the size is about 953 x 169.3 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#user-heatmap text {
|
#user-heatmap text {
|
||||||
fill: currentcolor !important;
|
fill: currentcolor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* root legend */
|
||||||
|
#user-heatmap .vch__container > .vch__legend {
|
||||||
|
display: flex;
|
||||||
|
font-size: 11px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
/* for the "Less" and "More" legend */
|
/* for the "Less" and "More" legend */
|
||||||
#user-heatmap .vch__legend .vch__legend {
|
#user-heatmap .vch__legend .vch__legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 11px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
}
|
}
|
||||||
@ -34,25 +55,3 @@
|
|||||||
#user-heatmap .vch__day__square:hover {
|
#user-heatmap .vch__day__square:hover {
|
||||||
outline: 1.5px solid var(--color-text);
|
outline: 1.5px solid var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* move the "? contributions in the last ? months" text from top to bottom */
|
|
||||||
#user-heatmap .total-contributions {
|
|
||||||
font-size: 11px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
#user-heatmap .total-contributions {
|
|
||||||
left: 21px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
#user-heatmap .total-contributions {
|
|
||||||
font-size: 10px;
|
|
||||||
left: 17px;
|
|
||||||
bottom: -4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -387,6 +387,7 @@ td .commit-summary {
|
|||||||
|
|
||||||
.repository.view.issue .pull-desc code {
|
.repository.view.issue .pull-desc code {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.view.issue .pull-desc a[data-clipboard-text] {
|
.repository.view.issue .pull-desc a[data-clipboard-text] {
|
||||||
|
|||||||
8
web_src/css/themes/theme-gitea-auto-tritanopia.css
Normal file
8
web_src/css/themes/theme-gitea-auto-tritanopia.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@import "./theme-gitea-light-tritanopia.css" (prefers-color-scheme: light);
|
||||||
|
@import "./theme-gitea-dark-tritanopia.css" (prefers-color-scheme: dark);
|
||||||
|
|
||||||
|
gitea-theme-meta-info {
|
||||||
|
--theme-display-name: "Auto";
|
||||||
|
--theme-colorblind-type: "blue-yellow";
|
||||||
|
--theme-color-scheme: "auto";
|
||||||
|
}
|
||||||
15
web_src/css/themes/theme-gitea-dark-tritanopia.css
Normal file
15
web_src/css/themes/theme-gitea-dark-tritanopia.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@import "./theme-gitea-dark-protanopia-deuteranopia.css";
|
||||||
|
|
||||||
|
gitea-theme-meta-info {
|
||||||
|
--theme-display-name: "Dark";
|
||||||
|
--theme-colorblind-type: "blue-yellow";
|
||||||
|
--theme-color-scheme: "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* blue/yellow colorblind-friendly colors */
|
||||||
|
/* from GitHub: blue yellow blindness is based on red green blindness, and --diffBlob-deletion-* restored to the normal theme color */
|
||||||
|
:root {
|
||||||
|
--color-diff-removed-linenum-bg: #482121;
|
||||||
|
--color-diff-removed-row-bg: #301e1e;
|
||||||
|
--color-diff-removed-word-bg: #6f3333;
|
||||||
|
}
|
||||||
15
web_src/css/themes/theme-gitea-light-tritanopia.css
Normal file
15
web_src/css/themes/theme-gitea-light-tritanopia.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@import "./theme-gitea-light-protanopia-deuteranopia.css";
|
||||||
|
|
||||||
|
gitea-theme-meta-info {
|
||||||
|
--theme-display-name: "Light";
|
||||||
|
--theme-colorblind-type: "blue-yellow";
|
||||||
|
--theme-color-scheme: "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* blue/yellow colorblind-friendly colors */
|
||||||
|
/* from GitHub: blue yellow blindness is based on red green blindness, and --diffBlob-deletion-* restored to the normal theme color */
|
||||||
|
:root {
|
||||||
|
--color-diff-removed-linenum-bg: #ffcecb;
|
||||||
|
--color-diff-removed-row-bg: #ffeef0;
|
||||||
|
--color-diff-removed-word-bg: #fdb8c0;
|
||||||
|
}
|
||||||
@ -53,9 +53,6 @@ function handleDayClick(e: Event & {date: Date}) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="total-contributions">
|
|
||||||
{{ locale.textTotalContributions }}
|
|
||||||
</div>
|
|
||||||
<calendar-heatmap
|
<calendar-heatmap
|
||||||
:locale="locale.heatMapLocale"
|
:locale="locale.heatMapLocale"
|
||||||
:no-data-text="locale.noDataText"
|
:no-data-text="locale.noDataText"
|
||||||
@ -65,5 +62,7 @@ function handleDayClick(e: Event & {date: Date}) {
|
|||||||
:range-color="colorRange"
|
:range-color="colorRange"
|
||||||
@day-click="handleDayClick($event)"
|
@day-click="handleDayClick($event)"
|
||||||
:tippy-props="{theme: 'tooltip'}"
|
:tippy-props="{theme: 'tooltip'}"
|
||||||
/>
|
>
|
||||||
|
<template #vch__legend-left>{{ locale.textTotalContributions }}</template>
|
||||||
|
</calendar-heatmap>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import './globals.ts';
|
|
||||||
import '../fomantic/build/fomantic.js';
|
import '../fomantic/build/fomantic.js';
|
||||||
import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in "switchToEasyMDE"
|
import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in "switchToEasyMDE"
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors
|
// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors
|
||||||
import './bootstrap.ts';
|
import './bootstrap.ts';
|
||||||
|
|
||||||
|
// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml)
|
||||||
|
// so load globals (including jQuery) as early as possible
|
||||||
|
import './globals.ts';
|
||||||
|
|
||||||
import './webcomponents/index.ts';
|
import './webcomponents/index.ts';
|
||||||
import {onDomReady} from './utils/dom.ts';
|
import {onDomReady} from './utils/dom.ts';
|
||||||
|
|
||||||
|
|||||||
13
web_src/svg/gitea-colorblind-blueyellow.svg
Normal file
13
web_src/svg/gitea-colorblind-blueyellow.svg
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<rect width="40" height="40" rx="20" fill="#0000"/>
|
||||||
|
<path d="M34.2843 34.2842C42.0948 26.4737 42.0948 13.8104 34.2843 5.9999L6 34.2842C13.8105 42.0947 26.4738 42.0947 34.2843 34.2842Z" fill="#1D69E0"/>
|
||||||
|
<path d="M34.2828 34.2842C42.0932 26.4737 42.0932 13.8104 34.2828 5.99995L20.1406 20.1421L34.2828 34.2842Z" fill="#fa4549"/>
|
||||||
|
<circle cx="20" cy="20" r="18" fill="#0000" stroke="#aaa" stroke-width="4"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="40" height="40" rx="20" fill="#0000"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 678 B |
Loading…
x
Reference in New Issue
Block a user