mirror of https://github.com/go-gitea/gitea.git
Merge branch 'main' into lunny/project_workflow
commit
08d28090a4
|
|
@ -8,3 +8,4 @@
|
|||
/vendor/** -text -eol linguist-vendored
|
||||
/web_src/js/vendor/** -text -eol linguist-vendored
|
||||
Dockerfile.* linguist-language=Dockerfile
|
||||
Makefile.* linguist-language=Makefile
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ modifies/internal:
|
|||
- ".github/**"
|
||||
- ".gitea/**"
|
||||
- ".devcontainer/**"
|
||||
- "build.go"
|
||||
- "build/**"
|
||||
- "contrib/**"
|
||||
|
||||
|
|
|
|||
|
|
@ -127,3 +127,6 @@ prime/
|
|||
|
||||
# Ignore worktrees when working on multiple branches
|
||||
.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
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
||||
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_TAG ?= latest
|
||||
|
|
@ -199,6 +198,10 @@ TEST_MSSQL_DBNAME ?= gitea
|
|||
TEST_MSSQL_USERNAME ?= sa
|
||||
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
|
||||
|
||||
# Include local Makefile
|
||||
# Makefile.local is listed in .gitignore
|
||||
sinclude Makefile.local
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
|
|
@ -276,19 +279,6 @@ fmt-check: fmt
|
|||
exit 1; \
|
||||
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)
|
||||
$(TAGS_EVIDENCE):
|
||||
@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
|
||||
|
||||
.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
|
||||
lint: lint-frontend lint-backend lint-spell ## lint everything
|
||||
|
|
@ -400,8 +390,7 @@ lint-go-windows:
|
|||
.PHONY: lint-go-gitea-vet
|
||||
lint-go-gitea-vet: ## lint go files with gitea-vet
|
||||
@echo "Running gitea-vet..."
|
||||
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet ./...
|
||||
@$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./...
|
||||
|
||||
.PHONY: lint-go-gopls
|
||||
lint-go-gopls: ## lint go files with gopls
|
||||
|
|
@ -852,7 +841,6 @@ deps-tools: ## install tool dependencies
|
|||
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
||||
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
||||
$(GO) install $(GOPLS_PACKAGE) & \
|
||||
$(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \
|
||||
wait
|
||||
|
||||
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
|
||||
;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
|
||||
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
|
||||
;;
|
||||
|
|
|
|||
8
go.mod
8
go.mod
|
|
@ -1,6 +1,8 @@
|
|||
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."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
|
|
@ -9,7 +11,6 @@ godebug x509negativeserial=1
|
|||
|
||||
require (
|
||||
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
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||
connectrpc.com/connect v1.18.1
|
||||
|
|
@ -135,6 +136,7 @@ require (
|
|||
|
||||
require (
|
||||
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
|
||||
filippo.io/edwards25519 v1.1.0 // 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/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
|
||||
}
|
||||
|
||||
prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
whitelist = make([]int64, 0, len(newWhitelist))
|
||||
for _, userID := range newWhitelist {
|
||||
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
|
||||
return nil, err
|
||||
} else if !reader {
|
||||
if !prUserIDs.Contains(userID) {
|
||||
continue
|
||||
}
|
||||
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.
|
||||
// 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
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
|
||||
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").
|
||||
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||
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").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID).
|
||||
And(builder.Or(
|
||||
builder.Expr("team.authorize >= ?", mode),
|
||||
builder.In("team.id", sub),
|
||||
)).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
|
||||
return teams, err
|
||||
Find(&teamIDs)
|
||||
return teamIDs, 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"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
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/setting"
|
||||
"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
|
||||
}
|
||||
|
||||
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
|
||||
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) {
|
||||
userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||
if len(userIDs) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// GetRepoReaders returns all users that have explicit read access or higher to the repository.
|
||||
func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
|
||||
}
|
||||
|
||||
// GetRepoWriters returns all users that have write access to the repository.
|
||||
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
|
||||
func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) {
|
||||
userIDs := container.Set[int64]{}
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
team := &organization.Team{OrgID: org.ID, LowerName: "test_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) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
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.unitsMode[unit.TypeCode])
|
||||
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}))
|
||||
|
|
@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||
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.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
|
||||
}
|
||||
|
||||
// NaturalSortLess compares two strings so that they could be sorted in natural order
|
||||
func NaturalSortLess(s1, s2 string) bool {
|
||||
// NaturalSortCompare compares two strings so that they could be sorted in natural order
|
||||
func NaturalSortCompare(s1, s2 string) int {
|
||||
// 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
|
||||
// So we need to handle the number parts by ourselves
|
||||
|
|
@ -55,16 +55,16 @@ func NaturalSortLess(s1, s2 string) bool {
|
|||
if isNum1 && isNum2 {
|
||||
if part1 != 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 {
|
||||
if cmp := c.CompareString(part1, part2); cmp != 0 {
|
||||
return cmp < 0
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
pos1, pos2 = end1, end2
|
||||
}
|
||||
return len(s1) < len(s2)
|
||||
return len(s1) - len(s2)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,10 @@ import (
|
|||
|
||||
func TestNaturalSortLess(t *testing.T) {
|
||||
testLess := func(s1, s2 string) {
|
||||
assert.True(t, NaturalSortLess(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)
|
||||
assert.Negative(t, NaturalSortCompare(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
testEqual := func(s1, s2 string) {
|
||||
assert.False(t, NaturalSortLess(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)
|
||||
assert.Zero(t, NaturalSortCompare(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
|
||||
testEqual("", "")
|
||||
|
|
|
|||
|
|
@ -173,7 +173,6 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
|
|||
} else if entries, err = commit.Tree.ListEntries(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
entries.Sort()
|
||||
b.ResetTimer()
|
||||
b.Run(benchmark.name, func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package git
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
|
@ -30,7 +31,11 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
|||
|
||||
remainingCommitID := commitID
|
||||
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)
|
||||
var file *object.File
|
||||
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.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
|
|
@ -107,7 +107,7 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
|
|||
}
|
||||
|
||||
commit.Tree.ID = ParseGogitHash(tree.Hash)
|
||||
commit.Tree.gogitTree = tree
|
||||
commit.Tree.resolvedGogitTreeObject = tree
|
||||
|
||||
return commit, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
|||
}
|
||||
|
||||
tree := NewTree(repo, id)
|
||||
tree.gogitTree = gogitTree
|
||||
tree.resolvedGogitTreeObject = gogitTree
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,21 @@ import (
|
|||
"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
|
||||
func NewTree(repo *Repository, id ObjectID) *Tree {
|
||||
return &Tree{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
TreeCommon: TreeCommon{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,22 +11,16 @@ import (
|
|||
"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/object"
|
||||
)
|
||||
|
||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
||||
if len(relpath) == 0 {
|
||||
return &TreeEntry{
|
||||
ID: t.ID,
|
||||
// Type: ObjectTree,
|
||||
ptree: t,
|
||||
gogitTreeEntry: &object.TreeEntry{
|
||||
Name: "",
|
||||
Mode: filemode.Dir,
|
||||
Hash: plumbing.Hash(t.ID.RawValue()),
|
||||
},
|
||||
ID: t.ID,
|
||||
ptree: t,
|
||||
name: "",
|
||||
entryMode: EntryModeTree,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,60 @@ package git
|
|||
|
||||
import (
|
||||
"path"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"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)
|
||||
func (te *TreeEntry) Type() string {
|
||||
switch te.Mode() {
|
||||
|
|
@ -109,49 +157,16 @@ func (te *TreeEntry) GetSubJumpablePathName() string {
|
|||
// Entries a list of entry
|
||||
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
|
||||
func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) {
|
||||
sort.Sort(customSortableEntries{cmp, tes})
|
||||
func (tes Entries) CustomSort(cmp func(s1, s2 string) int) {
|
||||
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"
|
||||
)
|
||||
|
||||
// TreeEntry the leaf in the git tree
|
||||
type TreeEntry struct {
|
||||
ID ObjectID
|
||||
|
||||
gogitTreeEntry *object.TreeEntry
|
||||
ptree *Tree
|
||||
|
||||
size int64
|
||||
sized bool
|
||||
// gogitFileModeToEntryMode converts go-git filemode to EntryMode
|
||||
func gogitFileModeToEntryMode(mode filemode.FileMode) EntryMode {
|
||||
return EntryMode(mode)
|
||||
}
|
||||
|
||||
// Name returns the name of the entry
|
||||
func (te *TreeEntry) Name() string {
|
||||
return te.gogitTreeEntry.Name
|
||||
func entryModeToGogitFileMode(mode EntryMode) filemode.FileMode {
|
||||
return filemode.FileMode(mode)
|
||||
}
|
||||
|
||||
// Mode returns the mode of the entry
|
||||
func (te *TreeEntry) Mode() EntryMode {
|
||||
return EntryMode(te.gogitTreeEntry.Mode)
|
||||
func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry {
|
||||
return &object.TreeEntry{
|
||||
Name: te.name,
|
||||
Mode: entryModeToGogitFileMode(te.entryMode),
|
||||
Hash: plumbing.Hash(te.ID.RawValue()),
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the size of the entry
|
||||
|
|
@ -41,7 +37,11 @@ func (te *TreeEntry) Size() int64 {
|
|||
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 {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -51,40 +51,15 @@ func (te *TreeEntry) Size() int64 {
|
|||
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
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Blob{
|
||||
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
|
||||
ID: te.ID,
|
||||
gogitEncodedObj: encodedObj,
|
||||
name: te.Name(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
// 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
|
||||
func (te *TreeEntry) Size() int64 {
|
||||
if te.IsDir() {
|
||||
|
|
@ -57,31 +36,6 @@ func (te *TreeEntry) Size() int64 {
|
|||
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
|
||||
func (te *TreeEntry) Blob() *Blob {
|
||||
return &Blob{
|
||||
|
|
|
|||
|
|
@ -1,55 +1,29 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"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) {
|
||||
entries := getTestEntries()
|
||||
entries.CustomSort(func(s1, s2 string) bool {
|
||||
return s1 > s2
|
||||
})
|
||||
assert.Equal(t, "v2.2", entries[0].Name())
|
||||
assert.Equal(t, "v2.12", entries[1].Name())
|
||||
assert.Equal(t, "v2.1", entries[2].Name())
|
||||
assert.Equal(t, "v2.0", entries[3].Name())
|
||||
assert.Equal(t, "v12.0", entries[4].Name())
|
||||
assert.Equal(t, "v1.0", entries[5].Name())
|
||||
assert.Equal(t, "bcd", entries[6].Name())
|
||||
assert.Equal(t, "abc", entries[7].Name())
|
||||
entries := Entries{
|
||||
&TreeEntry{name: "a-dir", entryMode: EntryModeTree},
|
||||
&TreeEntry{name: "a-submodule", entryMode: EntryModeCommit},
|
||||
&TreeEntry{name: "b-dir", entryMode: EntryModeTree},
|
||||
&TreeEntry{name: "b-submodule", entryMode: EntryModeCommit},
|
||||
&TreeEntry{name: "a-file", entryMode: EntryModeBlob},
|
||||
&TreeEntry{name: "b-file", entryMode: EntryModeBlob},
|
||||
}
|
||||
expected := slices.Clone(entries)
|
||||
rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] })
|
||||
assert.NotEqual(t, expected, entries)
|
||||
entries.CustomSort(strings.Compare)
|
||||
assert.Equal(t, expected, entries)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,41 +15,34 @@ import (
|
|||
|
||||
// Tree represents a flat directory listing.
|
||||
type Tree struct {
|
||||
ID ObjectID
|
||||
ResolvedID ObjectID
|
||||
repo *Repository
|
||||
TreeCommon
|
||||
|
||||
gogitTree *object.Tree
|
||||
|
||||
// parent tree
|
||||
ptree *Tree
|
||||
resolvedGogitTreeObject *object.Tree
|
||||
}
|
||||
|
||||
func (t *Tree) loadTreeObject() error {
|
||||
gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
|
||||
if err != nil {
|
||||
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()
|
||||
func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) {
|
||||
if t.resolvedGogitTreeObject == nil {
|
||||
t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t.resolvedGogitTreeObject, nil
|
||||
}
|
||||
|
||||
entries := make([]*TreeEntry, len(t.gogitTree.Entries))
|
||||
for i, entry := range t.gogitTree.Entries {
|
||||
// ListEntries returns all entries of current tree.
|
||||
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{
|
||||
ID: ParseGogitHash(entry.Hash),
|
||||
gogitTreeEntry: &t.gogitTree.Entries[i],
|
||||
ptree: t,
|
||||
ID: ParseGogitHash(gogitTreeEntry.Hash),
|
||||
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
|
||||
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||
if t.gogitTree == nil {
|
||||
err := t.loadTreeObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) {
|
||||
gogitTree, err := t.gogitTreeObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entries []*TreeEntry
|
||||
seen := map[plumbing.Hash]bool{}
|
||||
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
||||
walker := object.NewTreeWalker(gogitTree, true, nil)
|
||||
for {
|
||||
_, entry, err := walker.Next()
|
||||
fullName, gogitTreeEntry, err := walker.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seen[entry.Hash] {
|
||||
continue
|
||||
}
|
||||
|
||||
convertedEntry := &TreeEntry{
|
||||
ID: ParseGogitHash(entry.Hash),
|
||||
gogitTreeEntry: &entry,
|
||||
ptree: t,
|
||||
ID: ParseGogitHash(gogitTreeEntry.Hash),
|
||||
name: fullName, // FIXME: the "name" field is abused, here it is a full path
|
||||
ptree: t, // FIXME: this ptree is not right, fortunately it isn't really used
|
||||
entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode),
|
||||
}
|
||||
entries = append(entries, convertedEntry)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,18 +14,10 @@ import (
|
|||
|
||||
// Tree represents a flat directory listing.
|
||||
type Tree struct {
|
||||
ID ObjectID
|
||||
ResolvedID ObjectID
|
||||
repo *Repository
|
||||
|
||||
// parent tree
|
||||
ptree *Tree
|
||||
TreeCommon
|
||||
|
||||
entries Entries
|
||||
entriesParsed bool
|
||||
|
||||
entriesRecursive Entries
|
||||
entriesRecursiveParsed bool
|
||||
}
|
||||
|
||||
// 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
|
||||
// extraArgs could be "-l" to get the size, which is slower
|
||||
func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
|
||||
if t.entriesRecursiveParsed {
|
||||
return t.entriesRecursive, nil
|
||||
}
|
||||
|
||||
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
|
||||
AddArguments(extraArgs...).
|
||||
AddDynamicArguments(t.ID.String()).
|
||||
|
|
@ -107,13 +95,9 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e
|
|||
return nil, runErr
|
||||
}
|
||||
|
||||
var err error
|
||||
t.entriesRecursive, err = parseTreeEntries(stdout, t)
|
||||
if err == nil {
|
||||
t.entriesRecursiveParsed = true
|
||||
}
|
||||
|
||||
return t.entriesRecursive, err
|
||||
// FIXME: the "name" field is abused, here it is a full path
|
||||
// FIXME: this ptree is not right, fortunately it isn't really used
|
||||
return parseTreeEntries(stdout, t)
|
||||
}
|
||||
|
||||
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ var OAuth2 = struct {
|
|||
InvalidateRefreshTokens bool
|
||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||
JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"`
|
||||
MaxTokenLength int
|
||||
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) {
|
||||
blobClient := a.getBlobClient(path)
|
||||
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage
|
||||
startTime := time.Now()
|
||||
u, err := blobClient.GetSASURL(sas.BlobPermissions{
|
||||
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.
|
||||
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
|
||||
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
|
||||
if err != nil {
|
||||
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
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
Build: fileMetadata.Build,
|
||||
BuildNumber: fileMetadata.BuildNumber,
|
||||
Dependencies: fileMetadata.Dependencies,
|
||||
Dependencies: util.SliceNilAsEmpty(fileMetadata.Dependencies),
|
||||
License: versionMetadata.License,
|
||||
LicenseFamily: versionMetadata.LicenseFamily,
|
||||
HashMD5: pfd.Blob.HashMD5,
|
||||
|
|
|
|||
|
|
@ -897,7 +897,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
|||
} else {
|
||||
whitelistUsers = protectBranch.WhitelistUserIDs
|
||||
}
|
||||
if form.ForcePushAllowlistDeployKeys != nil {
|
||||
if form.ForcePushAllowlistUsernames != nil {
|
||||
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
|
|
|
|||
|
|
@ -370,11 +370,11 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
|
|||
},
|
||||
Signoff: commonOpts.Signoff,
|
||||
}
|
||||
if commonOpts.Dates.Author.IsZero() {
|
||||
commonOpts.Dates.Author = time.Now()
|
||||
if changeFileOpts.Dates.Author.IsZero() {
|
||||
changeFileOpts.Dates.Author = time.Now()
|
||||
}
|
||||
if commonOpts.Dates.Committer.IsZero() {
|
||||
commonOpts.Dates.Committer = time.Now()
|
||||
if changeFileOpts.Dates.Committer.IsZero() {
|
||||
changeFileOpts.Dates.Committer = time.Now()
|
||||
}
|
||||
ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
|
||||
}
|
||||
|
|
|
|||
|
|
@ -436,6 +436,7 @@ func ViewProject(ctx *context.Context) {
|
|||
ctx.Data["Project"] = project
|
||||
ctx.Data["IssuesMap"] = issuesMap
|
||||
ctx.Data["Columns"] = columns
|
||||
ctx.Data["Title"] = fmt.Sprintf("%s - %s", project.Title, ctx.ContextUser.DisplayName())
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
|
|
|
|||
|
|
@ -415,6 +415,8 @@ func Diff(ctx *context.Context) {
|
|||
ctx.ServerError("PostProcessCommitMessage", err)
|
||||
return
|
||||
}
|
||||
} else if !git.IsErrNotExist(err) {
|
||||
log.Error("GetNote: %v", err)
|
||||
}
|
||||
|
||||
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["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
|
||||
|
||||
users, err := access_model.GetRepoReaders(c, c.Repo.Repository)
|
||||
users, err := access_model.GetUsersWithUnitAccess(c, c.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
c.ServerError("Repo.Repository.GetReaders", err)
|
||||
c.ServerError("GetUsersWithUnitAccess", err)
|
||||
return
|
||||
}
|
||||
c.Data["Users"] = users
|
||||
|
|
|
|||
|
|
@ -149,9 +149,9 @@ func setTagsContext(ctx *context.Context) error {
|
|||
}
|
||||
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 {
|
||||
ctx.ServerError("Repo.Repository.GetReaders", err)
|
||||
ctx.ServerError("GetUsersWithUnitAccess", err)
|
||||
return err
|
||||
}
|
||||
ctx.Data["Users"] = users
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func TreeList(ctx *context.Context) {
|
|||
ctx.ServerError("ListEntriesRecursiveFast", err)
|
||||
return
|
||||
}
|
||||
entries.CustomSort(base.NaturalSortLess)
|
||||
entries.CustomSort(base.NaturalSortCompare)
|
||||
|
||||
files := make([]string, 0, len(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)
|
||||
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)
|
||||
return buf, dataRc, fi, nil
|
||||
}
|
||||
|
|
@ -307,7 +308,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
|||
ctx.ServerError("ListEntries", err)
|
||||
return nil
|
||||
}
|
||||
allEntries.CustomSort(base.NaturalSortLess)
|
||||
allEntries.CustomSort(base.NaturalSortCompare)
|
||||
|
||||
commitInfoCtx := gocontext.Context(ctx)
|
||||
if timeout > 0 {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*
|
|||
for _, entry := range entries {
|
||||
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
||||
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() {
|
||||
res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
|
||||
if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
|
||||
|
|
|
|||
|
|
@ -567,7 +567,7 @@ func WikiPages(ctx *context.Context) {
|
|||
ctx.ServerError("ListEntries", err)
|
||||
return
|
||||
}
|
||||
allEntries.CustomSort(base.NaturalSortLess)
|
||||
allEntries.CustomSort(base.NaturalSortCompare)
|
||||
|
||||
entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -105,9 +105,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
|||
}
|
||||
}
|
||||
if source.AttributeAvatar != "" {
|
||||
if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil {
|
||||
return user, err
|
||||
}
|
||||
_ = user_service.UploadAvatar(ctx, user, sr.Avatar)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T,
|
|||
|
||||
// ToBranchProtection convert a ProtectedBranch to 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 {
|
||||
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
|
||||
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 {
|
||||
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.
|
||||
// * https://accounts.google.com/.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{
|
||||
Issuer: strings.TrimSuffix(setting.AppURL, "/"),
|
||||
Issuer: issuer,
|
||||
Audience: []string{clientID},
|
||||
Subject: strconv.FormatInt(grantUserID, 10),
|
||||
ExpiresAt: exp,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import (
|
|||
func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error {
|
||||
avatarData, err := avatar.ProcessAvatarImage(data)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UploadAvatar: failed to process repo avatar image: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
repo.Avatar = newAvatar
|
||||
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 {
|
||||
_, err := w.Write(avatarData)
|
||||
return err
|
||||
}); 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 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
|
||||
|
|
|
|||
|
|
@ -21,21 +21,21 @@ import (
|
|||
func UploadAvatar(ctx context.Context, u *user_model.User, data []byte) error {
|
||||
avatarData, err := avatar.ProcessAvatarImage(data)
|
||||
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 {
|
||||
u.UseCustomAvatar = true
|
||||
u.Avatar = avatar.HashAvatar(u.ID, data)
|
||||
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 {
|
||||
_, err := w.Write(avatarData)
|
||||
return err
|
||||
}); 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
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ func (info *ThemeMetaInfo) GetDescription() string {
|
|||
if info.ColorblindType == "red-green" {
|
||||
return "Red-green colorblind friendly"
|
||||
}
|
||||
if info.ColorblindType == "blue-yellow" {
|
||||
return "Blue-yellow colorblind friendly"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +49,9 @@ func (info *ThemeMetaInfo) GetExtraIconName() string {
|
|||
if info.ColorblindType == "red-green" {
|
||||
return "gitea-colorblind-redgreen"
|
||||
}
|
||||
if info.ColorblindType == "blue-yellow" {
|
||||
return "gitea-colorblind-blueyellow"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,18 +78,6 @@
|
|||
{{ctx.Locale.Tr "repo.release.downloads"}}
|
||||
</summary>
|
||||
<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}}
|
||||
<li class="item">
|
||||
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
|
||||
|
|
@ -105,6 +93,18 @@
|
|||
</div>
|
||||
</li>
|
||||
{{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>
|
||||
</details>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{{if .HeatmapData}}
|
||||
<div class="activity-heatmap-container">
|
||||
<div id="user-heatmap" class="is-loading"
|
||||
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)}}"
|
||||
|
|
@ -6,5 +7,6 @@
|
|||
data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}"
|
||||
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"
|
||||
></div>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
{{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.HashSHA256, packageInfo.HashSHA256)
|
||||
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) {
|
||||
|
|
@ -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.HashSHA256, packageInfo.HashSHA256)
|
||||
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) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")()
|
||||
urlOpenidConfiguration := "/.well-known/openid-configuration"
|
||||
|
||||
defer test.MockVariableValue(&setting.AppURL, "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"])
|
||||
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/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/introspect", respMap["introspection_endpoint"])
|
||||
assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"])
|
||||
t.Run("WellKnown", func(t *testing.T) {
|
||||
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"])
|
||||
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/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/introspect", respMap["introspection_endpoint"])
|
||||
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)()
|
||||
MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
|
||||
|
|
|
|||
|
|
@ -626,7 +626,6 @@ img.ui.avatar,
|
|||
font-family: var(--fonts-monospace);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: 3px 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,23 +4,44 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
/* before the Vue component is mounted, show a loading indicator with dummy size */
|
||||
/* the ratio is guesswork, see https://github.com/razorness/vue3-calendar-heatmap/issues/26 */
|
||||
#user-heatmap.is-loading {
|
||||
aspect-ratio: 5.415; /* the size is about 790 x 145 */
|
||||
.activity-heatmap-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
.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 {
|
||||
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 */
|
||||
#user-heatmap .vch__legend .vch__legend {
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
|
|
@ -34,25 +55,3 @@
|
|||
#user-heatmap .vch__day__square:hover {
|
||||
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 {
|
||||
color: var(--color-primary);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.repository.view.issue .pull-desc a[data-clipboard-text] {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
<template>
|
||||
<div class="total-contributions">
|
||||
{{ locale.textTotalContributions }}
|
||||
</div>
|
||||
<calendar-heatmap
|
||||
:locale="locale.heatMapLocale"
|
||||
:no-data-text="locale.noDataText"
|
||||
|
|
@ -65,5 +62,7 @@ function handleDayClick(e: Event & {date: Date}) {
|
|||
:range-color="colorRange"
|
||||
@day-click="handleDayClick($event)"
|
||||
:tippy-props="{theme: 'tooltip'}"
|
||||
/>
|
||||
>
|
||||
<template #vch__legend-left>{{ locale.textTotalContributions }}</template>
|
||||
</calendar-heatmap>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import './globals.ts';
|
||||
import '../fomantic/build/fomantic.js';
|
||||
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
|
||||
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 {onDomReady} from './utils/dom.ts';
|
||||
|
||||
|
|
|
|||
|
|
@ -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…
Reference in New Issue