Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Anthony Wang 2022-07-27 10:29:54 -05:00
commit 8e5621c9c3
No known key found for this signature in database
GPG Key ID: BC96B00AEC5F2D76
102 changed files with 906 additions and 671 deletions

View File

@ -12,6 +12,7 @@ plugins:
- eslint-plugin-unicorn
- eslint-plugin-import
- eslint-plugin-jquery
- eslint-plugin-sonarjs
env:
es2022: true
@ -369,6 +370,38 @@ rules:
semi-spacing: [2, {before: false, after: true}]
semi-style: [2, last]
semi: [2, always, {omitLastInOneLineBlock: true}]
sonarjs/cognitive-complexity: [0]
sonarjs/elseif-without-else: [0]
sonarjs/max-switch-cases: [0]
sonarjs/no-all-duplicated-branches: [2]
sonarjs/no-collapsible-if: [0]
sonarjs/no-collection-size-mischeck: [2]
sonarjs/no-duplicate-string: [0]
sonarjs/no-duplicated-branches: [0]
sonarjs/no-element-overwrite: [2]
sonarjs/no-empty-collection: [2]
sonarjs/no-extra-arguments: [0]
sonarjs/no-gratuitous-expressions: [2]
sonarjs/no-identical-conditions: [2]
sonarjs/no-identical-expressions: [0]
sonarjs/no-identical-functions: [0]
sonarjs/no-ignored-return: [2]
sonarjs/no-inverted-boolean-check: [2]
sonarjs/no-nested-switch: [0]
sonarjs/no-nested-template-literals: [0]
sonarjs/no-one-iteration-loop: [2]
sonarjs/no-redundant-boolean: [2]
sonarjs/no-redundant-jump: [0]
sonarjs/no-same-line-conditional: [2]
sonarjs/no-small-switch: [0]
sonarjs/no-unused-collection: [2]
sonarjs/no-use-of-empty-return-value: [2]
sonarjs/no-useless-catch: [0]
sonarjs/non-existent-operator: [2]
sonarjs/prefer-immediate-return: [0]
sonarjs/prefer-object-literal: [0]
sonarjs/prefer-single-boolean-return: [0]
sonarjs/prefer-while: [2]
sort-imports: [0]
sort-keys: [0]
sort-vars: [0]

View File

@ -29,7 +29,7 @@ AIR_PACKAGE ?= github.com/cosmtrek/air@v1.40.4
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.5.0
ERRCHECK_PACKAGE ?= github.com/kisielk/errcheck@v1.6.1
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.3.1
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.1
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.0
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.29.0

View File

@ -92,7 +92,7 @@ func (o outputType) String() string {
}
var outputTypeEnum = &outputType{
Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4"},
Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
Default: "zip",
}

View File

@ -148,8 +148,9 @@ func runWeb(ctx *cli.Context) error {
go func() {
http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
log.Info("Starting pprof server on localhost:6060")
log.Info("%v", http.ListenAndServe("localhost:6060", nil))
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
finished()
}()
}

View File

@ -300,7 +300,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data.
- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. Note that this cache is disabled when `RUN_MODE` is "dev".
- `ENABLE_GZIP`: **false**: Enable gzip compression for runtime-generated content, static resources excluded.
- `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on localhost:6060. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)_<username>_<temporary id>`
- `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on `localhost:6060`. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)_<username>_<temporary id>`
- `PPROF_DATA_PATH`: **data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start Gitea as service
- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login, **custom**\]. Where custom would instead be any URL such as "/org/repo" or even `https://anotherwebsite.com`
- `LFS_START_SERVER`: **false**: Enables Git LFS support.

View File

@ -403,3 +403,9 @@ gitea doctor recreate-table
```
It is highly recommended to back-up your database before running these commands.
## Why are tabs/indents wrong when viewing files
If you are using Cloudflare, turn off the auto-minify option in the dashboard.
`Speed` -> `Optimization` -> Uncheck `HTML` within the `Auto-Minify` settings.

View File

@ -44,12 +44,13 @@ menu:
* This will greatly improve the chance that the root of the issue can be quickly discovered and resolved.
5. If you meet slow/hanging/deadlock problems, please report the stack trace when the problem occurs:
1. Enable pprof in `app.ini` and restart Gitea
```
```ini
[server]
ENABLE_PPROF = true
```
2. Trigger the bug, when Gitea gets stuck, use curl or browser to visit: `http://127.0.0.1:6060/debug/pprof/goroutine?debug=1` (IP is `127.0.0.1` and port is `6060`)
3. Report the output (the stack trace doesn't contain sensitive data)
2. Trigger the bug, when Gitea gets stuck, use curl or browser to visit: `http://127.0.0.1:6060/debug/pprof/goroutine?debug=1` (IP must be `127.0.0.1` and port must be `6060`).
3. If you are using Docker, please use `docker exec -it <container-name> curl "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1"`.
4. Report the output (the stack trace doesn't contain sensitive data)
## Bugs

View File

@ -47,9 +47,9 @@ pacman -S gitea
There is a [Gitea Snap](https://snapcraft.io/gitea) package which follows the latest stable version.
``sh
```sh
snap install gitea
``
```
## SUSE and openSUSE

View File

@ -309,6 +309,8 @@ To set required TOKEN and SECRET values, consider using Gitea's built-in [genera
Since SSH is running inside the container, SSH needs to be passed through from the host to the container if SSH support is desired. One option would be to run the container SSH on a non-standard port (or moving the host port to a non-standard port). Another option which might be more straightforward is for Gitea users to ssh to a Gitea user on the host which will then relay those connections to the docker.
### Understanding SSH access to Gitea (without passthrough)
To understand what needs to happen, you first need to understand what happens without passthrough. So we will try to explain this:
1. The client adds their SSH public key to Gitea using the webpage.

View File

@ -20,6 +20,7 @@ import (
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/packages/container/oci"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
)
@ -487,6 +488,13 @@ func TestPackageContainer(t *testing.T) {
assert.Equal(t, c.ExpectedTags, tagList.Tags)
assert.Equal(t, c.ExpectedLink, resp.Header().Get("Link"))
}
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s?type=container&q=%s", user.Name, image))
resp := MakeRequest(t, req, http.StatusOK)
var apiPackages []*api.Package
DecodeJSON(t, resp, &apiPackages)
assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..."
})
t.Run("Delete", func(t *testing.T) {

View File

@ -10,6 +10,8 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestAPIReposRaw(t *testing.T) {
@ -25,9 +27,11 @@ func TestAPIReposRaw(t *testing.T) {
"65f1bf27bc3bf70f64657658635e66094edbcb4d", // Commit
} {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/raw/%s/README.md?token="+token, user.Name, ref)
session.MakeRequest(t, req, http.StatusOK)
resp := session.MakeRequest(t, req, http.StatusOK)
assert.EqualValues(t, "file", resp.Header().Get("x-gitea-object-type"))
}
// Test default branch
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/raw/README.md?token="+token, user.Name)
session.MakeRequest(t, req, http.StatusOK)
resp := session.MakeRequest(t, req, http.StatusOK)
assert.EqualValues(t, "file", resp.Header().Get("x-gitea-object-type"))
}

View File

@ -116,6 +116,24 @@ func TestPrivateOrg(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
}
func TestOrgMembers(t *testing.T) {
defer prepareTestEnv(t)()
// not logged in user
req := NewRequest(t, "GET", "/org/org25/members")
MakeRequest(t, req, http.StatusOK)
// org member
session := loginUser(t, "user24")
req = NewRequest(t, "GET", "/org/org25/members")
session.MakeRequest(t, req, http.StatusOK)
// site admin
session = loginUser(t, "user1")
req = NewRequest(t, "GET", "/org/org25/members")
session.MakeRequest(t, req, http.StatusOK)
}
func TestOrgRestrictedUser(t *testing.T) {
defer prepareTestEnv(t)()

View File

@ -9,6 +9,7 @@ import (
"fmt"
"code.gitea.io/gitea/models/db"
project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
@ -222,6 +223,46 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
return nil
}
func (issues IssueList) getProjectIDs() []int64 {
ids := make(map[int64]struct{}, len(issues))
for _, issue := range issues {
projectID := issue.ProjectID()
if _, ok := ids[projectID]; !ok {
ids[projectID] = struct{}{}
}
}
return container.KeysInt64(ids)
}
func (issues IssueList) loadProjects(ctx context.Context) error {
projectIDs := issues.getProjectIDs()
if len(projectIDs) == 0 {
return nil
}
projectMaps := make(map[int64]*project_model.Project, len(projectIDs))
left := len(projectIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
err := db.GetEngine(ctx).
In("id", projectIDs[:limit]).
Find(&projectMaps)
if err != nil {
return err
}
left -= limit
projectIDs = projectIDs[limit:]
}
for _, issue := range issues {
issue.Project = projectMaps[issue.ProjectID()]
}
return nil
}
func (issues IssueList) loadAssignees(ctx context.Context) error {
if len(issues) == 0 {
return nil
@ -495,6 +536,10 @@ func (issues IssueList) loadAttributes(ctx context.Context) error {
return fmt.Errorf("issue.loadAttributes: loadMilestones: %v", err)
}
if err := issues.loadProjects(ctx); err != nil {
return fmt.Errorf("issue.loadAttributes: loadProjects: %v", err)
}
if err := issues.loadAssignees(ctx); err != nil {
return fmt.Errorf("issue.loadAttributes: loadAssignees: %v", err)
}

View File

@ -122,8 +122,9 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
// GetVersionsByPackageType gets all versions of a specific type
func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) {
pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
OwnerID: ownerID,
Type: packageType,
OwnerID: ownerID,
Type: packageType,
IsInternal: util.OptionalBoolFalse,
})
return pvs, err
}
@ -137,6 +138,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty
ExactMatch: true,
Value: name,
},
IsInternal: util.OptionalBoolFalse,
})
return pvs, err
}

View File

@ -6,6 +6,7 @@ package repo
import (
"context"
"errors"
"fmt"
"strings"
@ -695,6 +696,9 @@ func GetUserRepositories(opts *SearchRepoOptions) (RepositoryList, int64, error)
}
cond := builder.NewCond()
if opts.Actor == nil {
return nil, 0, errors.New("GetUserRepositories: Actor is needed but not given")
}
cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID})
if !opts.Private {
cond = cond.And(builder.Eq{"is_private": false})

View File

@ -59,25 +59,18 @@ func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session {
}
if opts.Actor != nil {
exprCond := builder.Expr("org_user.org_id = `user`.id")
// If Admin - they see all users!
if !opts.Actor.IsAdmin {
// Force visibility for privacy
var accessCond builder.Cond
// Users can see an organization they are a member of
accessCond := builder.In("id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": opts.Actor.ID}))
if !opts.Actor.IsRestricted {
accessCond = builder.Or(
builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
} else {
// restricted users only see orgs they are a member of
accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
// Not-Restricted users can see public and limited users/organizations
accessCond = accessCond.Or(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
}
// Don't forget about self
accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID})
cond = cond.And(accessCond)
}
} else {
// Force visibility for privacy
// Not logged in - only public users

View File

@ -16,6 +16,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
@ -268,6 +269,7 @@ func APIContexter() func(http.Handler) http.Handler {
}
}
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform")
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
ctx.Data["Context"] = &ctx

View File

@ -28,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/base"
mc "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@ -767,6 +768,7 @@ func Contexter() func(next http.Handler) http.Handler {
}
}
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform")
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
ctx.Data["CsrfToken"] = ctx.csrf.GetToken()

View File

@ -1001,6 +1001,8 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
return
}
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
return cancel
}
}

View File

@ -80,6 +80,9 @@ func (c *Commit) ParentCount() int {
// GetCommitByPath return the commit of relative path object.
func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
if c.repo.LastCommitCache != nil {
return c.repo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath)
}
return c.repo.getCommitByPathWithID(c.ID, relpath)
}

View File

@ -17,7 +17,7 @@ import (
)
// GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) {
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
entryPaths := make([]string, len(tes)+1)
// Get the commit for the treePath itself
entryPaths[0] = ""
@ -35,15 +35,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
return nil, nil, err
}
var revs map[string]*object.Commit
if cache != nil {
var revs map[string]*Commit
if commit.repo.LastCommitCache != nil {
var unHitPaths []string
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
if err != nil {
return nil, nil, err
}
if len(unHitPaths) > 0 {
revs2, err := GetLastCommitForPaths(ctx, cache, c, treePath, unHitPaths)
revs2, err := GetLastCommitForPaths(ctx, commit.repo.LastCommitCache, c, treePath, unHitPaths)
if err != nil {
return nil, nil, err
}
@ -68,8 +68,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
}
// Check if we have found a commit for this entry in time
if rev, ok := revs[entry.Name()]; ok {
entryCommit := convertCommit(rev)
if entryCommit, ok := revs[entry.Name()]; ok {
commitsInfo[i].Commit = entryCommit
}
@ -96,10 +95,10 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
// get it for free during the tree traversal and it's used for listing
// pages to display information about newest commit for a given path.
var treeCommit *Commit
var ok bool
if treePath == "" {
treeCommit = commit
} else if rev, ok := revs[""]; ok {
treeCommit = convertCommit(rev)
} else if treeCommit, ok = revs[""]; ok {
treeCommit.repo = commit.repo
}
return commitsInfo, treeCommit, nil
@ -155,16 +154,16 @@ func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[
return hashes, nil
}
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*object.Commit, []string, error) {
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
var unHitEntryPaths []string
results := make(map[string]*object.Commit)
results := make(map[string]*Commit)
for _, p := range paths {
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
if err != nil {
return nil, nil, err
}
if lastCommit != nil {
results[p] = lastCommit.(*object.Commit)
results[p] = lastCommit
continue
}
@ -175,7 +174,7 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac
}
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) {
refSha := c.ID().String()
// We do a tree traversal with nodes sorted by commit time
@ -293,13 +292,13 @@ heaploop:
}
// Post-processing
result := make(map[string]*object.Commit)
result := make(map[string]*Commit)
for path, commitNode := range resultNodes {
var err error
result[path], err = commitNode.Commit()
commit, err := commitNode.Commit()
if err != nil {
return nil, err
}
result[path] = convertCommit(commit)
}
return result, nil

View File

@ -17,7 +17,7 @@ import (
)
// GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) {
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
entryPaths := make([]string, len(tes)+1)
// Get the commit for the treePath itself
entryPaths[0] = ""
@ -28,15 +28,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
var err error
var revs map[string]*Commit
if cache != nil {
if commit.repo.LastCommitCache != nil {
var unHitPaths []string
revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, cache)
revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
if err != nil {
return nil, nil, err
}
if len(unHitPaths) > 0 {
sort.Strings(unHitPaths)
commits, err := GetLastCommitForPaths(ctx, cache, commit, treePath, unHitPaths)
commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths)
if err != nil {
return nil, nil, err
}
@ -47,7 +47,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
}
} else {
sort.Strings(entryPaths)
revs, err = GetLastCommitForPaths(ctx, nil, commit, treePath, entryPaths)
revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths)
}
if err != nil {
return nil, nil, err
@ -99,18 +99,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
}
func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
wr, rd, cancel := cache.repo.CatFileBatch(ctx)
defer cancel()
var unHitEntryPaths []string
results := make(map[string]*Commit)
for _, p := range paths {
lastCommit, err := cache.Get(commitID, path.Join(treePath, p), wr, rd)
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
if err != nil {
return nil, nil, err
}
if lastCommit != nil {
results[p] = lastCommit.(*Commit)
results[p] = lastCommit
continue
}
@ -121,9 +118,9 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string
}
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits
revs, err := WalkGitLog(ctx, cache, commit.repo, commit, treePath, paths...)
revs, err := WalkGitLog(ctx, commit.repo, commit, treePath, paths...)
if err != nil {
return nil, err
}

View File

@ -91,7 +91,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
}
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
commitsInfo, treeCommit, err := entries.GetCommitsInfo(context.TODO(), commit, testCase.Path, nil)
commitsInfo, treeCommit, err := entries.GetCommitsInfo(context.TODO(), commit, testCase.Path)
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
if err != nil {
t.FailNow()
@ -170,7 +170,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
b.ResetTimer()
b.Run(benchmark.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _, err := entries.GetCommitsInfo(context.Background(), commit, "", nil)
_, _, err := entries.GetCommitsInfo(context.Background(), commit, "")
if err != nil {
b.Fatal(err)
}

View File

@ -9,6 +9,7 @@ import (
"fmt"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// Cache represents a caching interface
@ -19,16 +20,96 @@ type Cache interface {
Get(key string) interface{}
}
func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string {
hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, ref, entryPath)))
func getCacheKey(repoPath, commitID, entryPath string) string {
hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath)))
return fmt.Sprintf("last_commit:%x", hashBytes)
}
// LastCommitCache represents a cache to store last commit
type LastCommitCache struct {
repoPath string
ttl func() int64
repo *Repository
commitCache map[string]*Commit
cache Cache
}
// NewLastCommitCache creates a new last commit cache for repo
func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache Cache) *LastCommitCache {
if cache == nil {
return nil
}
if !setting.CacheService.LastCommit.Enabled || count < setting.CacheService.LastCommit.CommitsCount {
return nil
}
return &LastCommitCache{
repoPath: repoPath,
repo: gitRepo,
ttl: setting.LastCommitCacheTTLSeconds,
cache: cache,
}
}
// Put put the last commit id with commit and entry path
func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
if c == nil || c.cache == nil {
return nil
}
log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl())
return c.cache.Put(getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl())
}
// Get gets the last commit information by commit id and entry path
func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) {
if c == nil || c.cache == nil {
return nil, nil
}
commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)).(string)
if !ok || commitID == "" {
return nil, nil
}
log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, commitID)
if c.commitCache != nil {
if commit, ok := c.commitCache[commitID]; ok {
log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, commitID)
return commit, nil
}
}
commit, err := c.repo.GetCommit(commitID)
if err != nil {
return nil, err
}
if c.commitCache == nil {
c.commitCache = make(map[string]*Commit)
}
c.commitCache[commitID] = commit
return commit, nil
}
// GetCommitByPath gets the last commit for the entry in the provided commit
func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) {
sha1, err := NewIDFromString(commitID)
if err != nil {
return nil, err
}
lastCommit, err := c.Get(sha1.String(), entryPath)
if err != nil || lastCommit != nil {
return lastCommit, err
}
lastCommit, err = c.repo.getCommitByPathWithID(sha1, entryPath)
if err != nil {
return nil, err
}
if err := c.Put(commitID, entryPath, lastCommit.ID.String()); err != nil {
log.Error("Unable to cache %s as the last commit for %q in %s %s. Error %v", lastCommit.ID.String(), entryPath, commitID, c.repoPath, err)
}
return lastCommit, nil
}

View File

@ -9,71 +9,25 @@ package git
import (
"context"
"code.gitea.io/gitea/modules/log"
"github.com/go-git/go-git/v5/plumbing/object"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
// LastCommitCache represents a cache to store last commit
type LastCommitCache struct {
repoPath string
ttl func() int64
repo *Repository
commitCache map[string]*object.Commit
cache Cache
}
// NewLastCommitCache creates a new last commit cache for repo
func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, cache Cache) *LastCommitCache {
if cache == nil {
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context) error {
if c.repo.LastCommitCache == nil {
return nil
}
return &LastCommitCache{
repoPath: repoPath,
repo: gitRepo,
commitCache: make(map[string]*object.Commit),
ttl: ttl,
cache: cache,
}
}
commitNodeIndex, _ := c.repo.CommitNodeIndex()
// Get get the last commit information by commit id and entry path
func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
if vs, ok := v.(string); ok {
log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
if commit, ok := c.commitCache[vs]; ok {
log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
return commit, nil
}
id, err := c.repo.ConvertToSHA1(vs)
if err != nil {
return nil, err
}
commit, err := c.repo.GoGitRepo().CommitObject(id)
if err != nil {
return nil, err
}
c.commitCache[vs] = commit
return commit, nil
}
return nil, nil
}
// CacheCommit will cache the commit from the gitRepository
func (c *LastCommitCache) CacheCommit(ctx context.Context, commit *Commit) error {
commitNodeIndex, _ := commit.repo.CommitNodeIndex()
index, err := commitNodeIndex.Get(commit.ID)
index, err := commitNodeIndex.Get(c.ID)
if err != nil {
return err
}
return c.recursiveCache(ctx, index, &commit.Tree, "", 1)
return c.recursiveCache(ctx, index, &c.Tree, "", 1)
}
func (c *LastCommitCache) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
@ -90,7 +44,7 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, index cgobject.Com
entryMap[entry.Name()] = entry
}
commits, err := GetLastCommitForPaths(ctx, c, index, treePath, entryPaths)
commits, err := GetLastCommitForPaths(ctx, c.repo.LastCommitCache, index, treePath, entryPaths)
if err != nil {
return err
}

View File

@ -7,67 +7,18 @@
package git
import (
"bufio"
"context"
"code.gitea.io/gitea/modules/log"
)
// LastCommitCache represents a cache to store last commit
type LastCommitCache struct {
repoPath string
ttl func() int64
repo *Repository
commitCache map[string]*Commit
cache Cache
}
// NewLastCommitCache creates a new last commit cache for repo
func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, cache Cache) *LastCommitCache {
if cache == nil {
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context) error {
if c.repo.LastCommitCache == nil {
return nil
}
return &LastCommitCache{
repoPath: repoPath,
repo: gitRepo,
commitCache: make(map[string]*Commit),
ttl: ttl,
cache: cache,
}
return c.recursiveCache(ctx, &c.Tree, "", 1)
}
// Get get the last commit information by commit id and entry path
func (c *LastCommitCache) Get(ref, entryPath string, wr WriteCloserError, rd *bufio.Reader) (interface{}, error) {
v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
if vs, ok := v.(string); ok {
log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
if commit, ok := c.commitCache[vs]; ok {
log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
return commit, nil
}
id, err := c.repo.ConvertToSHA1(vs)
if err != nil {
return nil, err
}
if _, err := wr.Write([]byte(vs + "\n")); err != nil {
return nil, err
}
commit, err := c.repo.getCommitFromBatchReader(rd, id)
if err != nil {
return nil, err
}
c.commitCache[vs] = commit
return commit, nil
}
return nil, nil
}
// CacheCommit will cache the commit from the gitRepository
func (c *LastCommitCache) CacheCommit(ctx context.Context, commit *Commit) error {
return c.recursiveCache(ctx, commit, &commit.Tree, "", 1)
}
func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tree *Tree, treePath string, level int) error {
func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
@ -82,7 +33,7 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tr
entryPaths[i] = entry.Name()
}
_, err = WalkGitLog(ctx, c, commit.repo, commit, treePath, entryPaths...)
_, err = WalkGitLog(ctx, c.repo, c, treePath, entryPaths...)
if err != nil {
return err
}
@ -94,7 +45,7 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tr
if err != nil {
return err
}
if err := c.recursiveCache(ctx, commit, subTree, treeEntry.Name(), level-1); err != nil {
if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil {
return err
}
}

View File

@ -281,7 +281,7 @@ func (g *LogNameStatusRepoParser) Close() {
}
// WalkGitLog walks the git log --name-status for the head commit in the provided treepath and files
func WalkGitLog(ctx context.Context, cache *LastCommitCache, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
headRef := head.ID.String()
tree, err := head.SubTree(treepath)
@ -374,14 +374,14 @@ heaploop:
changed[i] = false
if results[i] == "" {
results[i] = current.CommitID
if err := cache.Put(headRef, path.Join(treepath, paths[i]), current.CommitID); err != nil {
if err := repo.LastCommitCache.Put(headRef, path.Join(treepath, paths[i]), current.CommitID); err != nil {
return nil, err
}
delete(path2idx, paths[i])
remaining--
if results[0] == "" {
results[0] = current.CommitID
if err := cache.Put(headRef, treepath, current.CommitID); err != nil {
if err := repo.LastCommitCache.Put(headRef, treepath, current.CommitID); err != nil {
return nil, err
}
delete(path2idx, "")

View File

@ -83,7 +83,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
log.Error("Unable to get the commit for the path %q. Error: %v", path, err)
return err
}
note.Commit = convertCommit(lastCommits[path])
note.Commit = lastCommits[path]
return nil
}

View File

@ -81,7 +81,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
path = path[idx+1:]
}
lastCommits, err := GetLastCommitForPaths(ctx, nil, notes, treePath, []string{path})
lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path})
if err != nil {
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
return err

View File

@ -31,7 +31,8 @@ type Repository struct {
gogitStorage *filesystem.Storage
gpgSettings *GPGSettings
Ctx context.Context
Ctx context.Context
LastCommitCache *LastCommitCache
}
// openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext.
@ -79,6 +80,8 @@ func (repo *Repository) Close() (err error) {
if err := repo.gogitStorage.Close(); err != nil {
gitealog.Error("Error closing storage: %v", err)
}
repo.LastCommitCache = nil
repo.tagCache = nil
return
}

View File

@ -32,7 +32,8 @@ type Repository struct {
checkReader *bufio.Reader
checkWriter WriteCloserError
Ctx context.Context
Ctx context.Context
LastCommitCache *LastCommitCache
}
// openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext.
@ -101,5 +102,7 @@ func (repo *Repository) Close() (err error) {
repo.checkReader = nil
repo.checkWriter = nil
}
repo.LastCommitCache = nil
repo.tagCache = nil
return err
}

View File

@ -40,9 +40,11 @@ var (
// NewContext loads custom highlight map from local config
func NewContext() {
once.Do(func() {
keys := setting.Cfg.Section("highlight.mapping").Keys()
for i := range keys {
highlightMapping[keys[i].Name()] = keys[i].Value()
if setting.Cfg != nil {
keys := setting.Cfg.Section("highlight.mapping").Keys()
for i := range keys {
highlightMapping[keys[i].Name()] = keys[i].Value()
}
}
// The size 512 is simply a conservative rule of thumb

View File

@ -17,16 +17,23 @@ import (
)
// AddCacheControlToHeader adds suitable cache-control headers to response
func AddCacheControlToHeader(h http.Header, d time.Duration) {
func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
directives := make([]string, 0, 2+len(additionalDirectives))
if setting.IsProd {
h.Set("Cache-Control", "private, max-age="+strconv.Itoa(int(d.Seconds())))
if maxAge == 0 {
directives = append(directives, "no-store")
} else {
directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds())))
}
} else {
h.Set("Cache-Control", "no-store")
directives = append(directives, "no-store")
// to remind users they are using non-prod setting.
// some users may be confused by "Cache-Control: no-store" in their setup if they did wrong to `RUN_MODE` in `app.ini`.
h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
h.Add("X-Gitea-Debug", "CacheControl=no-store")
}
h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", "))
}
// generateETag generates an ETag based on size, filename and file modification time

View File

@ -1176,7 +1176,7 @@ func genDefaultLinkProcessor(defaultLink string) processor {
node.DataAtom = atom.A
node.Attr = []html.Attribute{
{Key: "href", Val: defaultLink},
{Key: "class", Val: "default-link"},
{Key: "class", Val: "default-link muted"},
}
node.FirstChild, node.LastChild = ch, ch
}

View File

@ -8,10 +8,9 @@ import (
"errors"
"fmt"
"regexp"
"strings"
"code.gitea.io/gitea/modules/log"
goversion "github.com/hashicorp/go-version"
)
const (
@ -56,7 +55,9 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR
if !namePattern.MatchString(name) {
return nil, ErrValidation
}
if _, err := goversion.NewSemver(version); err != nil {
v := strings.TrimSpace(version)
if v == "" {
return nil, ErrValidation
}
if user != "" && !namePattern.MatchString(user) {
@ -69,7 +70,7 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR
return nil, ErrValidation
}
return &RecipeReference{name, version, user, channel, revision}, nil
return &RecipeReference{name, v, user, channel, revision}, nil
}
func (r *RecipeReference) RevisionOrDefault() string {

View File

@ -34,6 +34,7 @@ func TestNewRecipeReference(t *testing.T) {
{"name", "1.0", "_", "_", "", true},
{"name", "1.0", "_", "_", "0", true},
{"name", "1.0", "", "", "0", true},
{"name", "1.0.0q", "", "", "0", true},
{"name", "1.0", "", "", "000000000000000000000000000000000000000000000000000000000000", false},
}

View File

@ -80,7 +80,6 @@ type gemspec struct {
VersionRequirements requirement `yaml:"version_requirements"`
} `yaml:"dependencies"`
Description string `yaml:"description"`
Email string `yaml:"email"`
Executables []string `yaml:"executables"`
Extensions []interface{} `yaml:"extensions"`
ExtraRdocFiles []string `yaml:"extra_rdoc_files"`

View File

@ -1356,7 +1356,6 @@ issues.due_date_form_remove=Odstranit
issues.due_date_not_writer=Potřebujete práva na zápis do repozitáře pro úpravy termínu dokončení úkolu.
issues.due_date_not_set=Žádný termín dokončení.
issues.due_date_added=přidal/a termín dokončení %s %s
issues.due_date_modified=upravil/a termín dokončení z %s na %s %s
issues.due_date_remove=odstranil/a termín dokončení %s %s
issues.due_date_overdue=Zpožděné
issues.due_date_invalid=Termín dokončení není platný nebo je mimo rozsah. Použijte prosím formát „rrrr-mm-dd“.

View File

@ -1417,7 +1417,6 @@ issues.due_date_form_remove=Entfernen
issues.due_date_not_writer=Du musst Schreibrechte in diesem Repository haben, um das Fälligkeitsdatum zu ändern.
issues.due_date_not_set=Kein Fälligkeitsdatum gesetzt.
issues.due_date_added=hat %[2]s das Fälligkeitsdatum %[1]s hinzugefügt
issues.due_date_modified=hat %[3]s das Fälligkeitsdatum von %[2]s zu %[1]s geändert
issues.due_date_remove=hat %[2]s das Fälligkeitsdatum %[1]s entfernt
issues.due_date_overdue=Überfällig
issues.due_date_invalid=Das Fälligkeitsdatum ist ungültig oder außerhalb des zulässigen Bereichs. Bitte verwende das Format „jjjj-mm-tt“.

View File

@ -1177,7 +1177,7 @@ projects.type.basic_kanban=Βασικό Kanban
projects.type.bug_triage=Διαλογή Σφαλμάτων
projects.template.desc=Πρότυπο έργου
projects.template.desc_helper=Επιλέξτε ένα πρότυπο έργου για να ξεκινήσετε
projects.type.uncategorized=Αταξινόμητο
projects.type.uncategorized=Χωρίς Κατηγορία
projects.board.edit=Επεξεργασία πίνακα
projects.board.edit_title=Νέο Όνομα Πίνακα
projects.board.new_title=Νέο Όνομα Πίνακα
@ -1186,7 +1186,7 @@ projects.board.new=Νέος Πίνακας
projects.board.set_default=Ορισμός Προεπιλογής
projects.board.set_default_desc=Ορίστε αυτόν τον πίνακα ως προεπιλογή για μη κατηγοριοποιημένα ζητήματα και pull requests
projects.board.delete=Διαγραφή Πίνακα
projects.board.deletion_desc=Η διαγραφή ενός πίνακα έργου μετακινεί όλα τα σχετιζόμενα ζητήματα σε 'Αταξινόμητα'. Συνέχεια;
projects.board.deletion_desc=Η διαγραφή ενός πίνακα έργου μετακινεί όλα τα σχετιζόμενα ζητήματα σε 'Χωρίς Κατηγορία'. Συνέχεια;
projects.board.color=Χρώμα
projects.open=Άνοιγμα
projects.close=Κλείσιμο
@ -1420,7 +1420,7 @@ issues.due_date_form_remove=Διαγραφή
issues.due_date_not_writer=Χρειάζεστε πρόσβαση εγγραφής στο αποθετήριο για να ενημερώσετε την ημερομηνία λήξης ενός ζητήματος.
issues.due_date_not_set=Δεν ορίστηκε ημερομηνία παράδοσης.
issues.due_date_added=πρόσθεσε την ημερομηνία παράδοσης %s %s
issues.due_date_modified=τροποποίησε την ημερομηνία παράδοσης σε %s από %s %s
issues.due_date_modified=τροποποίησε την ημερομηνία παράδοσης από %[2]s σε %[1]s %[3]s
issues.due_date_remove=αφαίρεσε την ημερομηνία παράδοσης %s %s
issues.due_date_overdue=Εκπρόθεσμο
issues.due_date_invalid=Η ημερομηνία παράδοσης δεν είναι έγκυρη ή εκτός εύρους. Παρακαλούμε χρησιμοποιήστε τη μορφή 'εεεε-μμ-ηη'.

View File

@ -3044,6 +3044,7 @@ title = Packages
desc = Manage repository packages.
empty = There are no packages yet.
empty.documentation = For more information on the package registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/overview">the documentation</a>.
empty.repo = Did you upload a package, but it's not shown here? Go to <a href="%[1]s">package settings</a> and link it to this repo.
filter.type = Type
filter.type.all = All
filter.no_result = Your filter produced no results.

View File

@ -1418,7 +1418,6 @@ issues.due_date_form_remove=Eliminar
issues.due_date_not_writer=Necesita acceso de escritura al repositorio para actualizar la fecha de vencimiento de un issue.
issues.due_date_not_set=Sin fecha de vencimiento.
issues.due_date_added=añadió la fecha de vencimiento %s %s
issues.due_date_modified=modificó la fecha de vencimiento a %s de %s %s
issues.due_date_remove=eliminó la fecha de vencimiento %s %s
issues.due_date_overdue=Vencido
issues.due_date_invalid=La fecha de vencimiento es inválida o está fuera de rango. Por favor utilice el formato 'aaaa-mm-dd'.

View File

@ -1309,7 +1309,6 @@ issues.due_date_form_remove=حذف/ساقط کردن
issues.due_date_not_writer=شما نیازمند دسترسی نوشتن به این مخزن را برای تغییر موعد مقرر این مسئله را دارید.
issues.due_date_not_set=هیچ موعد مقرری ثبت نشده.
issues.due_date_added=موعد مقرر اضافه شد %s %s
issues.due_date_modified=موعد مقرر از %s به %s %s تغییر کرد.
issues.due_date_remove=موعد مقرر %s %s حذف شد
issues.due_date_overdue=تاریخ گذشته
issues.due_date_invalid=موعد مقرر نامعتبر است یا خارج از محدوده. لطفاً از قالب 'yyy-mm-dd' استفاده کنید.

View File

@ -1306,7 +1306,6 @@ issues.due_date_form_remove=Supprimer
issues.due_date_not_writer=Vous devez avoir accès au dépôt en écriture pour mettre à jour l'échéance d'un ticket.
issues.due_date_not_set=Aucune échéance n'a été définie.
issues.due_date_added=a ajouté l'échéance %s %s
issues.due_date_modified=a modifié l'échéance de %[2]s vers %[1]s %[3]s
issues.due_date_remove=a supprimé l'échéance %s %s
issues.due_date_overdue=En retard
issues.due_date_invalid=La date déchéance est invalide ou hors plage. Veuillez utiliser le format 'aaaa-mm-dd'.

View File

@ -922,7 +922,6 @@ issues.due_date_form_add=Határidő hozzáadása
issues.due_date_form_edit=Szerkesztés
issues.due_date_form_remove=Eltávolítás
issues.due_date_not_set=Nincs beállítva határidő.
issues.due_date_modified=határidő módosítva %s-ről %s %s-re
issues.due_date_remove=%s %s-es határidő eltávolítva
issues.due_date_overdue=Lejárt
issues.dependency.title=Függőségek

View File

@ -1124,7 +1124,6 @@ issues.due_date_form_remove=Rimuovi
issues.due_date_not_writer=E' necessario l'accesso di scrittura del repository per aggiornare la data di una sua issue.
issues.due_date_not_set=Nessuna data di scadenza impostata.
issues.due_date_added=la data di scadenza %s è stata aggiunta %s
issues.due_date_modified=data di scadenza modificata da %s a %s %s
issues.due_date_remove=rimossa la data di scadenza %s %s
issues.due_date_overdue=Scaduto
issues.due_date_invalid=La data di scadenza non è valida o fuori intervallo. Si prega di utilizzare il formato 'aaaa-mm-dd'.

View File

@ -807,7 +807,6 @@ issues.due_date_form_remove=삭제
issues.due_date_not_writer=이슈의 마감일을 갱신하려면 저장소 쓰기 권한이 필요합니다.
issues.due_date_not_set=마감일이 설정되지 않았습니다.
issues.due_date_added=마감일 %s 를 추가 %s
issues.due_date_modified=%s 마감일이 %s %s 로 변경되었습니다
issues.due_date_remove=%s %s 마감일이 삭제되었습니다.
issues.due_date_overdue=기한 초과
issues.due_date_invalid=기한이 올바르지 않거나 범위를 벗어났습니다. 'yyyy-mm-dd'형식을 사용해주십시오.

View File

@ -1416,7 +1416,6 @@ issues.due_date_form_remove=Noņemt
issues.due_date_not_writer=Jums ir nepieciešamas rakstīšanas tiesības uz šo repozitoriju, lai mainītu izpildes termiņu.
issues.due_date_not_set=Izpildes termiņš nav uzstādīts.
issues.due_date_added=pievienoja izpildes termiņu %s %s
issues.due_date_modified=mainīja izpildes termiņu uz %s no %s %s
issues.due_date_remove=noņēma izpildes termiņu %s %s
issues.due_date_overdue=Nokavēts
issues.due_date_invalid=Datums līdz nav korekts. Izmantojiet formātu 'gggg-mm-dd'.

View File

@ -1141,7 +1141,6 @@ issues.due_date_form_remove=Verwijder
issues.due_date_not_writer=Je hebt schrijftoegang in deze repository nodig om de deadline van een kwestie aan te passen.
issues.due_date_not_set=Geen vervaldatum ingesteld.
issues.due_date_added=heeft %[2]s de deadline %[1]s toegevoegd
issues.due_date_modified=heeft %[3]s de deadline aangepast van %[1]s naar %[2]s
issues.due_date_remove=heeft %[2]s de deadline %[1]s verwijderd
issues.due_date_overdue=Over tijd
issues.due_date_invalid=De deadline is ongeldig of buiten bereik. Gebruik het formaat 'jjjj-mm-dd'.

View File

@ -1299,7 +1299,6 @@ issues.due_date_form_remove=Usuń
issues.due_date_not_writer=Potrzebujesz uprawnień zapisu w tym repozytorium, aby zaktualizować termin realizacji zgłoszenia.
issues.due_date_not_set=Brak ustawionego terminu realizacji.
issues.due_date_added=dodaje termin realizacji %s %s
issues.due_date_modified=zmienia termin realizacji na %s z %s %s
issues.due_date_remove=usuwa termin realizacji %s %s
issues.due_date_overdue=Zaległe
issues.due_date_invalid=Data realizacji jest niewłaściwa lub spoza zakresu. Użyj formatu 'yyyy-mm-dd'.

View File

@ -1419,7 +1419,6 @@ issues.due_date_form_remove=Remover
issues.due_date_not_writer=Você deve ter permissão de escrita no repositório para atualizar a data limite de uma issue.
issues.due_date_not_set=Data limite não informada.
issues.due_date_added=adicionou a data limite %s %s
issues.due_date_modified=modificou a data limite para %s ao invés de %s %s
issues.due_date_remove=removeu a data limite %s %s
issues.due_date_overdue=Em atraso
issues.due_date_invalid=A data limite é inválida ou está fora do intervalo. Por favor, use o formato 'dd/mm/aaaa'.
@ -1800,6 +1799,7 @@ settings.tracker_url_format_error=O formato da URL do issue tracker externo não
settings.tracker_issue_style=Formato de número do issue tracker externo
settings.tracker_issue_style.numeric=Numérico
settings.tracker_issue_style.alphanumeric=Alfanumérico
settings.tracker_issue_style.regexp=Expressão Regular
settings.tracker_url_format_desc=Use os espaços reservados <code>{user}</code>, <code>{repo}</code> e <code>{index}</code> para o nome de usuário, nome do repositório e o índice de problemas.
settings.enable_timetracker=Habilitar Cronômetro
settings.allow_only_contributors_to_track_time=Permitir que apenas os colaboradores acompanhem o contador de tempo

View File

@ -1376,7 +1376,6 @@ issues.due_date_form_remove=Удалить
issues.due_date_not_writer=Для обновления срока выполнения необходим доступ на запись в репозиторий.
issues.due_date_not_set=Срок выполнения не установлен.
issues.due_date_added=добавлено в срок выполнения %s %s
issues.due_date_modified=срок выполнения изменён на %s с %s %s
issues.due_date_remove=удалён срок выполнения %s %s
issues.due_date_overdue=Просроченные
issues.due_date_invalid=Срок действия недействителен или находится за пределами допустимого диапазона. Пожалуйста, используйте формат 'гггг-мм-дд'.

View File

@ -1254,7 +1254,6 @@ issues.due_date_form_remove=ඉවත් කරන්න
issues.due_date_not_writer=ඔබ නිකුත් ගේ නියමිත දිනය යාවත්කාලීන කිරීමට ගබඩාවක් ලිවීමට ප්රවේශය අවශ්ය.
issues.due_date_not_set=නියමිත දිනය නියම කර නැත.
issues.due_date_added=නියමිත දිනය එකතු %s %s
issues.due_date_modified=නියමිත දිනය %s සිට %s %sදක්වා වෙනස් කරන ලදි
issues.due_date_remove=නියමිත දිනය ඉවත් කරන ලදි %s %s
issues.due_date_overdue=කල් ඉකුත්වීම
issues.due_date_invalid=නියමිත දිනය අවලංගු හෝ පරාසයෙන් බැහැර වේ. කරුණාකර 'yyyy-mm-dd' ආකෘතිය භාවිතා කරන්න.

View File

@ -1068,7 +1068,6 @@ issues.due_date_form_remove=Ta bort
issues.due_date_not_writer=Du måste ha skrivrättigheter för att ändra ett ärendes förfallodatum.
issues.due_date_not_set=Inget förfallodatum satt.
issues.due_date_added=lade till förfallodatumet %s %s
issues.due_date_modified=ändrade förfallodatumet från %s till %s %s
issues.due_date_remove=tog bort förfallodatumet %s %s
issues.due_date_overdue=Försenad
issues.due_date_invalid=Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet 'åååå-mm-dd'.

View File

@ -1275,7 +1275,6 @@ issues.due_date_form_remove=Kaldır
issues.due_date_not_writer=Bir konunun bitiş tarihini değiştirmek için depoda yazma hakkınız olmalıdır.
issues.due_date_not_set=Bitiş tarihi atanmadı.
issues.due_date_added=%[2]s %[1]s bitiş tarihini ekledi
issues.due_date_modified=%s bitiş tarihini %s iken %s olarak değiştirildi
issues.due_date_remove=%[2]s %[1]s bitiş tarihini kaldırdı
issues.due_date_overdue=Süresi Geçmiş
issues.due_date_invalid=Bitiş tarihi geçersiz veya aralık dışında. Lütfen 'yyyy-aa-gg' biçimini kullanın.

View File

@ -1317,7 +1317,6 @@ issues.due_date_form_remove=Видалити
issues.due_date_not_writer=Вам потрібен доступ до запису в репозиторії, щоб оновити дату завершення задач.
issues.due_date_not_set=Термін виконання не встановлений.
issues.due_date_added=додав(ла) дату завершення %s %s
issues.due_date_modified=термін змінено з %s %s на %s
issues.due_date_remove=видалив(ла) дату завершення %s %s
issues.due_date_overdue=Прострочено
issues.due_date_invalid=Термін дії не дійсний або знаходиться за межами допустимого діапазону. Будь ласка використовуйте формат 'yyyy-mm-dd'.

View File

@ -1419,7 +1419,6 @@ issues.due_date_form_remove=删除
issues.due_date_not_writer=你需要仓库写入权限来修改工单到期时间。
issues.due_date_not_set=未设置到期时间。
issues.due_date_added=于 %[2]s 设置到期时间为 %[1]s
issues.due_date_modified=于 %[3]s 将到期时间从 %[2]s 修改为 %[1]s
issues.due_date_remove=于 %[2]s 删除了到期时间 %[1]s
issues.due_date_overdue=过期
issues.due_date_invalid=到期日期无效或超出范围。请使用 'yyyy-mm-dd' 格式。

View File

@ -1419,7 +1419,6 @@ issues.due_date_form_remove=移除
issues.due_date_not_writer=您需要儲存庫寫入權限來更改問題的截止日。
issues.due_date_not_set=未設定截止日期。
issues.due_date_added=新增了截止日期 %s %s
issues.due_date_modified=將截止日期修改為 %s ,原截止日期: %s %s
issues.due_date_remove=移除了截止日期 %s %s
issues.due_date_overdue=逾期
issues.due_date_invalid=截止日期無效或超出範圍請使用「yyyy-mm-dd」的格式。

20
package-lock.json generated
View File

@ -50,6 +50,7 @@
"eslint": "8.20.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jquery": "1.5.1",
"eslint-plugin-sonarjs": "0.13.0",
"eslint-plugin-unicorn": "43.0.2",
"eslint-plugin-vue": "9.2.0",
"jest": "28.1.3",
@ -5492,6 +5493,18 @@
"eslint": ">=5.4.0"
}
},
"node_modules/eslint-plugin-sonarjs": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.13.0.tgz",
"integrity": "sha512-t3m7ta0EspzDxSOZh3cEOJIJVZgN/TlJYaBGnQlK6W/PZNbWep8q4RQskkJkA7/zwNpX0BaoEOSUUrqaADVoqA==",
"dev": true,
"engines": {
"node": ">=12"
},
"peerDependencies": {
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "43.0.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-43.0.2.tgz",
@ -16787,6 +16800,13 @@
"dev": true,
"requires": {}
},
"eslint-plugin-sonarjs": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.13.0.tgz",
"integrity": "sha512-t3m7ta0EspzDxSOZh3cEOJIJVZgN/TlJYaBGnQlK6W/PZNbWep8q4RQskkJkA7/zwNpX0BaoEOSUUrqaADVoqA==",
"dev": true,
"requires": {}
},
"eslint-plugin-unicorn": {
"version": "43.0.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-43.0.2.tgz",

View File

@ -50,6 +50,7 @@
"eslint": "8.20.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jquery": "1.5.1",
"eslint-plugin-sonarjs": "0.13.0",
"eslint-plugin-unicorn": "43.0.2",
"eslint-plugin-vue": "9.2.0",
"jest": "28.1.3",

View File

@ -19,6 +19,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
@ -62,10 +63,11 @@ func SearchPackages(ctx *context.Context) {
}
opts := &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeComposer,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
Paginator: &paginator,
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeComposer,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse,
Paginator: &paginator,
}
if ctx.FormTrim("type") != "" {
opts.Properties = map[string]string{

View File

@ -19,6 +19,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
helm_module "code.gitea.io/gitea/modules/packages/helm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
@ -39,8 +40,9 @@ func apiError(ctx *context.Context, status int, obj interface{}) {
// Index generates the Helm charts index
func Index(ctx *context.Context) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeHelm,
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeHelm,
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@ -108,6 +110,7 @@ func DownloadPackageFile(ctx *context.Context) {
Value: ctx.Params("package"),
},
HasFileWithName: filename,
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)

View File

@ -18,6 +18,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
@ -261,6 +262,7 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo
Properties: map[string]string{
npm_module.TagProperty: tag,
},
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
return err

View File

@ -17,6 +17,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
)
@ -39,9 +40,10 @@ func ServiceIndex(ctx *context.Context) {
// SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
func SearchService(ctx *context.Context) {
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNuGet,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNuGet,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse,
Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("skip"),
ctx.FormInt("take"),

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
)
@ -40,8 +41,9 @@ func EnumeratePackages(ctx *context.Context) {
// EnumeratePackagesLatest serves the list of the latest version of every package
func EnumeratePackagesLatest(ctx *context.Context) {
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@ -289,6 +291,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
HasFileWithName: filename,
IsInternal: util.OptionalBoolFalse,
})
return pvs, err
}

View File

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
packages_service "code.gitea.io/gitea/services/packages"
)
@ -55,10 +56,11 @@ func ListPackages(ctx *context.APIContext) {
query := ctx.FormTrim("q")
pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
Paginator: &listOptions,
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
IsInternal: util.OptionalBoolFalse,
Paginator: &listOptions,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "SearchVersions", err)

View File

@ -18,7 +18,6 @@ import (
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
@ -33,6 +32,8 @@ import (
files_service "code.gitea.io/gitea/services/repository/files"
)
const giteaObjectTypeHeader = "X-Gitea-Object-Type"
// GetRawFile get a file by path on a repository
func GetRawFile(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
@ -72,11 +73,13 @@ func GetRawFile(ctx *context.APIContext) {
return
}
blob, lastModified := getBlobForEntry(ctx)
blob, entry, lastModified := getBlobForEntry(ctx)
if ctx.Written() {
return
}
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
}
@ -119,11 +122,13 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
return
}
blob, lastModified := getBlobForEntry(ctx)
blob, entry, lastModified := getBlobForEntry(ctx)
if ctx.Written() {
return
}
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
// LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
if blob.Size() > 1024 {
// First handle caching for the blob
@ -218,7 +223,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
}
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, lastModified time.Time) {
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified time.Time) {
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
@ -234,12 +239,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, lastModified time
return
}
var c *git.LastCommitCache
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
}
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:], c)
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
return
@ -251,7 +251,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, lastModified time
}
blob = entry.Blob()
return blob, lastModified
return blob, entry, lastModified
}
// GetArchive get archive of a repository

View File

@ -8,8 +8,10 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
)
// ResolveRefOrSha resolve ref to sha if exist
@ -19,6 +21,7 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
return ""
}
sha := ref
// Search branches and tags
for _, refType := range []string{"heads", "tags"} {
refSHA, lastMethodName, err := searchRefCommitByType(ctx, refType, ref)
@ -27,10 +30,27 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
return ""
}
if refSHA != "" {
return refSHA
sha = refSHA
break
}
}
return ref
if ctx.Repo.GitRepo != nil && ctx.Repo.GitRepo.LastCommitCache == nil {
commitsCount, err := cache.GetInt64(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, true), func() (int64, error) {
commit, err := ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
return 0, err
}
return commit.CommitsCount()
})
if err != nil {
log.Error("Unable to get commits count for %s in %s. Error: %v", sha, ctx.Repo.Repository.FullName(), err)
return sha
}
ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(commitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
}
return sha
}
// GetGitRefs return git references based on filter

View File

@ -9,6 +9,7 @@ import (
"net/http"
"path"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
@ -62,6 +63,7 @@ func installRecovery() func(next http.Handler) http.Handler {
"SignedUserName": "",
}
httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd {

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
)
@ -31,9 +32,10 @@ func Packages(ctx *context.Context) {
sort := ctx.FormTrim("sort")
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Type: packages_model.Type(packageType),
Name: packages_model.SearchValue{Value: query},
Sort: sort,
Type: packages_model.Type(packageType),
Name: packages_model.SearchValue{Value: query},
Sort: sort,
IsInternal: util.OptionalBoolFalse,
Paginator: &db.ListOptions{
PageSize: setting.UI.PackagesPagingNum,
Page: page,

View File

@ -158,6 +158,7 @@ func Recovery() func(next http.Handler) http.Handler {
store["SignedUserName"] = ""
}
httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd {

View File

@ -786,6 +786,19 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["IsDiffCompare"] = true
ctx.Data["RequireTribute"] = true
setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates)
// If a template content is set, prepend the "content". In this case that's only
// applicable if you have one commit to compare and that commit has a message.
// In that case the commit message will be prepend to the template body.
if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" {
if content, ok := ctx.Data["content"].(string); ok && content != "" {
// Re-use the same key as that's priortized over the "content" key.
// Add two new lines between the content to ensure there's always at least
// one empty line between them.
ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent
}
}
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")

View File

@ -10,7 +10,6 @@ import (
"time"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
@ -99,12 +98,7 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified time.Ti
return
}
var c *git.LastCommitCache
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
}
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:], c)
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return

View File

@ -9,9 +9,11 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
const (
@ -32,10 +34,11 @@ func Packages(ctx *context.Context) {
PageSize: setting.UI.PackagesPagingNum,
Page: page,
},
OwnerID: ctx.ContextUser.ID,
RepoID: ctx.Repo.Repository.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
OwnerID: ctx.ContextUser.ID,
RepoID: ctx.Repo.Repository.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("SearchLatestVersions", err)
@ -60,6 +63,9 @@ func Packages(ctx *context.Context) {
ctx.Data["Query"] = query
ctx.Data["PackageType"] = packageType
ctx.Data["HasPackages"] = hasPackages
if ctx.Repo != nil {
ctx.Data["CanWritePackages"] = ctx.IsUserRepoWriter([]unit.Type{unit.TypePackages}) || ctx.IsUserSiteAdmin()
}
ctx.Data["PackageDescriptors"] = pds
ctx.Data["Total"] = total
ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository

View File

@ -476,7 +476,7 @@ func SettingsPost(ctx *context.Context) {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
}
if form.EnablePackages && !unit_model.TypeProjects.UnitGlobalDisabled() {
if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypePackages,

View File

@ -27,7 +27,6 @@ import (
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
@ -812,11 +811,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
defer cancel()
}
var c *git.LastCommitCache
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
}
selected := map[string]bool{}
for _, pth := range ctx.FormStrings("f[]") {
selected[pth] = true
@ -833,7 +827,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
}
var latestCommit *git.Commit
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath, c)
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return nil

View File

@ -591,6 +591,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
LabelIDs: opts.LabelIDs,
Org: org,
Team: team,
RepoCond: opts.RepoCond,
}
issueStats, err = issues_model.GetUserIssueStats(statsOpts)

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages"
@ -43,9 +44,10 @@ func ListPackages(ctx *context.Context) {
PageSize: setting.UI.PackagesPagingNum,
Page: page,
},
OwnerID: ctx.ContextUser.ID,
Type: packages_model.Type(packageType),
Name: packages_model.SearchValue{Value: query},
OwnerID: ctx.ContextUser.ID,
Type: packages_model.Type(packageType),
Name: packages_model.SearchValue{Value: query},
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("SearchLatestVersions", err)
@ -112,7 +114,8 @@ func RedirectToLastVersion(ctx *context.Context) {
}
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
PackageID: p.ID,
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("GetPackageByName", err)
@ -157,8 +160,9 @@ func ViewPackageVersion(ctx *context.Context) {
})
default:
pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("SearchVersions", err)
@ -254,6 +258,7 @@ func ListPackageVersions(ctx *context.Context) {
ExactMatch: false,
Value: query,
},
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("SearchVersions", err)

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
@ -1012,6 +1013,7 @@ func RegisterRoutes(m *web.Route) {
return
}
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
})
}, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)

View File

@ -15,7 +15,6 @@ import (
"io"
"net/url"
"os"
"regexp"
"sort"
"strings"
"time"
@ -40,7 +39,7 @@ import (
"golang.org/x/text/transform"
)
// DiffLineType represents the type of a DiffLine.
// DiffLineType represents the type of DiffLine.
type DiffLineType uint8
// DiffLineType possible values.
@ -51,7 +50,7 @@ const (
DiffLineSection
)
// DiffFileType represents the type of a DiffFile.
// DiffFileType represents the type of DiffFile.
type DiffFileType uint8
// DiffFileType possible values.
@ -100,12 +99,12 @@ type DiffLineSectionInfo struct {
// BlobExcerptChunkSize represent max lines of excerpt
const BlobExcerptChunkSize = 20
// GetType returns the type of a DiffLine.
// GetType returns the type of DiffLine.
func (d *DiffLine) GetType() int {
return int(d.Type)
}
// CanComment returns whether or not a line can get commented
// CanComment returns whether a line can get commented
func (d *DiffLine) CanComment() bool {
return len(d.Comments) == 0 && d.Type != DiffLineSection
}
@ -191,287 +190,13 @@ var (
codeTagSuffix = []byte(`</span>`)
)
var (
unfinishedtagRegex = regexp.MustCompile(`<[^>]*$`)
trailingSpanRegex = regexp.MustCompile(`<span\s*[[:alpha:]="]*?[>]?$`)
entityRegex = regexp.MustCompile(`&[#]*?[0-9[:alpha:]]*$`)
)
// shouldWriteInline represents combinations where we manually write inline changes
func shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool {
if true &&
diff.Type == diffmatchpatch.DiffEqual ||
diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd ||
diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel {
return true
}
return false
}
func fixupBrokenSpans(diffs []diffmatchpatch.Diff) []diffmatchpatch.Diff {
// Create a new array to store our fixed up blocks
fixedup := make([]diffmatchpatch.Diff, 0, len(diffs))
// semantically label some numbers
const insert, delete, equal = 0, 1, 2
// record the positions of the last type of each block in the fixedup blocks
last := []int{-1, -1, -1}
operation := []diffmatchpatch.Operation{diffmatchpatch.DiffInsert, diffmatchpatch.DiffDelete, diffmatchpatch.DiffEqual}
// create a writer for insert and deletes
toWrite := []strings.Builder{
{},
{},
}
// make some flags for insert and delete
unfinishedTag := []bool{false, false}
unfinishedEnt := []bool{false, false}
// store stores the provided text in the writer for the typ
store := func(text string, typ int) {
(&(toWrite[typ])).WriteString(text)
}
// hasStored returns true if there is stored content
hasStored := func(typ int) bool {
return (&toWrite[typ]).Len() > 0
}
// stored will return that content
stored := func(typ int) string {
return (&toWrite[typ]).String()
}
// empty will empty the stored content
empty := func(typ int) {
(&toWrite[typ]).Reset()
}
// pop will remove the stored content appending to a diff block for that typ
pop := func(typ int, fixedup []diffmatchpatch.Diff) []diffmatchpatch.Diff {
if hasStored(typ) {
if last[typ] > last[equal] {
fixedup[last[typ]].Text += stored(typ)
} else {
fixedup = append(fixedup, diffmatchpatch.Diff{
Type: operation[typ],
Text: stored(typ),
})
}
empty(typ)
}
return fixedup
}
// Now we walk the provided diffs and check the type of each block in turn
for _, diff := range diffs {
typ := delete // flag for handling insert or delete typs
switch diff.Type {
case diffmatchpatch.DiffEqual:
// First check if there is anything stored
if hasStored(insert) || hasStored(delete) {
// There are two reasons for storing content:
// 1. Unfinished Entity <- Could be more efficient here by not doing this if we're looking for a tag
if unfinishedEnt[insert] || unfinishedEnt[delete] {
// we look for a ';' to finish an entity
idx := strings.IndexRune(diff.Text, ';')
if idx >= 0 {
// if we find a ';' store the preceding content to both insert and delete
store(diff.Text[:idx+1], insert)
store(diff.Text[:idx+1], delete)
// and remove it from this block
diff.Text = diff.Text[idx+1:]
// reset the ent flags
unfinishedEnt[insert] = false
unfinishedEnt[delete] = false
} else {
// otherwise store it all on insert and delete
store(diff.Text, insert)
store(diff.Text, delete)
// and empty this block
diff.Text = ""
}
}
// 2. Unfinished Tag
if unfinishedTag[insert] || unfinishedTag[delete] {
// we look for a '>' to finish a tag
idx := strings.IndexRune(diff.Text, '>')
if idx >= 0 {
store(diff.Text[:idx+1], insert)
store(diff.Text[:idx+1], delete)
diff.Text = diff.Text[idx+1:]
unfinishedTag[insert] = false
unfinishedTag[delete] = false
} else {
store(diff.Text, insert)
store(diff.Text, delete)
diff.Text = ""
}
}
// If we've completed the required tag/entities
if !(unfinishedTag[insert] || unfinishedTag[delete] || unfinishedEnt[insert] || unfinishedEnt[delete]) {
// pop off the stack
fixedup = pop(insert, fixedup)
fixedup = pop(delete, fixedup)
}
// If that has left this diff block empty then shortcut
if len(diff.Text) == 0 {
continue
}
}
// check if this block ends in an unfinished tag?
idx := unfinishedtagRegex.FindStringIndex(diff.Text)
if idx != nil {
unfinishedTag[insert] = true
unfinishedTag[delete] = true
} else {
// otherwise does it end in an unfinished entity?
idx = entityRegex.FindStringIndex(diff.Text)
if idx != nil {
unfinishedEnt[insert] = true
unfinishedEnt[delete] = true
}
}
// If there is an unfinished component
if idx != nil {
// Store the fragment
store(diff.Text[idx[0]:], insert)
store(diff.Text[idx[0]:], delete)
// and remove it from this block
diff.Text = diff.Text[:idx[0]]
}
// If that hasn't left the block empty
if len(diff.Text) > 0 {
// store the position of the last equal block and store it in our diffs
last[equal] = len(fixedup)
fixedup = append(fixedup, diff)
}
continue
case diffmatchpatch.DiffInsert:
typ = insert
fallthrough
case diffmatchpatch.DiffDelete:
// First check if there is anything stored for this type
if hasStored(typ) {
// if there is prepend it to this block, empty the storage and reset our flags
diff.Text = stored(typ) + diff.Text
empty(typ)
unfinishedEnt[typ] = false
unfinishedTag[typ] = false
}
// check if this block ends in an unfinished tag
idx := unfinishedtagRegex.FindStringIndex(diff.Text)
if idx != nil {
unfinishedTag[typ] = true
} else {
// otherwise does it end in an unfinished entity
idx = entityRegex.FindStringIndex(diff.Text)
if idx != nil {
unfinishedEnt[typ] = true
}
}
// If there is an unfinished component
if idx != nil {
// Store the fragment
store(diff.Text[idx[0]:], typ)
// and remove it from this block
diff.Text = diff.Text[:idx[0]]
}
// If that hasn't left the block empty
if len(diff.Text) > 0 {
// if the last block of this type was after the last equal block
if last[typ] > last[equal] {
// store this blocks content on that block
fixedup[last[typ]].Text += diff.Text
} else {
// otherwise store the position of the last block of this type and store the block
last[typ] = len(fixedup)
fixedup = append(fixedup, diff)
}
}
continue
}
}
// pop off any remaining stored content
fixedup = pop(insert, fixedup)
fixedup = pop(delete, fixedup)
return fixedup
}
func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) DiffInline {
func diffToHTML(lineWrapperTags []string, diffs []diffmatchpatch.Diff, lineType DiffLineType) string {
buf := bytes.NewBuffer(nil)
match := ""
diffs = fixupBrokenSpans(diffs)
// restore the line wrapper tags <span class="line"> and <span class="cl">, if necessary
for _, tag := range lineWrapperTags {
buf.WriteString(tag)
}
for _, diff := range diffs {
if shouldWriteInline(diff, lineType) {
if len(match) > 0 {
diff.Text = match + diff.Text
match = ""
}
// Chroma HTML syntax highlighting is done before diffing individual lines in order to maintain consistency.
// Since inline changes might split in the middle of a chroma span tag or HTML entity, make we manually put it back together
// before writing so we don't try insert added/removed code spans in the middle of one of those
// and create broken HTML. This is done by moving incomplete HTML forward until it no longer matches our pattern of
// a line ending with an incomplete HTML entity or partial/opening <span>.
// EX:
// diffs[{Type: dmp.DiffDelete, Text: "language</span><span "},
// {Type: dmp.DiffEqual, Text: "c"},
// {Type: dmp.DiffDelete, Text: "lass="p">}]
// After first iteration
// diffs[{Type: dmp.DiffDelete, Text: "language</span>"}, //write out
// {Type: dmp.DiffEqual, Text: "<span c"},
// {Type: dmp.DiffDelete, Text: "lass="p">,</span>}]
// After second iteration
// {Type: dmp.DiffEqual, Text: ""}, // write out
// {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}]
// Final
// {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}]
// end up writing <span class="removed-code"><span class="p">,</span></span>
// Instead of <span class="removed-code">lass="p",</span></span>
m := trailingSpanRegex.FindStringSubmatchIndex(diff.Text)
if m != nil {
match = diff.Text[m[0]:m[1]]
diff.Text = strings.TrimSuffix(diff.Text, match)
}
m = entityRegex.FindStringSubmatchIndex(diff.Text)
if m != nil {
match = diff.Text[m[0]:m[1]]
diff.Text = strings.TrimSuffix(diff.Text, match)
}
// Print an existing closing span first before opening added/remove-code span so it doesn't unintentionally close it
if strings.HasPrefix(diff.Text, "</span>") {
buf.WriteString("</span>")
diff.Text = strings.TrimPrefix(diff.Text, "</span>")
}
// If we weren't able to fix it then this should avoid broken HTML by not inserting more spans below
// The previous/next diff section will contain the rest of the tag that is missing here
if strings.Count(diff.Text, "<") != strings.Count(diff.Text, ">") {
buf.WriteString(diff.Text)
continue
}
}
switch {
case diff.Type == diffmatchpatch.DiffEqual:
buf.WriteString(diff.Text)
@ -485,7 +210,10 @@ func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineT
buf.Write(codeTagSuffix)
}
}
return DiffInlineWithUnicodeEscape(template.HTML(buf.String()))
for range lineWrapperTags {
buf.WriteString("</span>")
}
return buf.String()
}
// GetLine gets a specific line by type (add or del) and file line number
@ -597,10 +325,12 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) Dif
return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content)
}
diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, language, diff1[1:]), highlight.Code(diffSection.FileName, language, diff2[1:]), true)
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
hcd := newHighlightCodeDiff()
diffRecord := hcd.diffWithHighlight(diffSection.FileName, language, diff1[1:], diff2[1:])
// it seems that Gitea doesn't need the line wrapper of Chroma, so do not add them back
// if the line wrappers are still needed in the future, it can be added back by "diffToHTML(hcd.lineWrapperTags. ...)"
diffHTML := diffToHTML(nil, diffRecord, diffLine.Type)
return DiffInlineWithUnicodeEscape(template.HTML(diffHTML))
}
// DiffFile represents a file diff.
@ -1289,7 +1019,7 @@ func readFileName(rd *strings.Reader) (string, bool) {
if char == '"' {
fmt.Fscanf(rd, "%q ", &name)
if len(name) == 0 {
log.Error("Reader has no file name: %v", rd)
log.Error("Reader has no file name: reader=%+v", rd)
return "", true
}
@ -1311,7 +1041,7 @@ func readFileName(rd *strings.Reader) (string, bool) {
}
}
if len(name) < 2 {
log.Error("Unable to determine name from reader: %v", rd)
log.Error("Unable to determine name from reader: reader=%+v", rd)
return "", true
}
return name[2:], ambiguity

View File

@ -7,7 +7,6 @@ package gitdiff
import (
"fmt"
"html/template"
"strconv"
"strings"
"testing"
@ -17,93 +16,27 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
"gopkg.in/ini.v1"
)
func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
if s1 != string(s2) {
t.Errorf("Did not receive expected results:\nExpected: %s\nActual: %s", s1, s2)
}
}
func TestDiffToHTML(t *testing.T) {
setting.Cfg = ini.Empty()
assertEqual(t, "foo <span class=\"added-code\">bar</span> biz", diffToHTML("", []dmp.Diff{
assert.Equal(t, "foo <span class=\"added-code\">bar</span> biz", diffToHTML(nil, []dmp.Diff{
{Type: dmp.DiffEqual, Text: "foo "},
{Type: dmp.DiffInsert, Text: "bar"},
{Type: dmp.DiffDelete, Text: " baz"},
{Type: dmp.DiffEqual, Text: " biz"},
}, DiffLineAdd).Content)
}, DiffLineAdd))
assertEqual(t, "foo <span class=\"removed-code\">bar</span> biz", diffToHTML("", []dmp.Diff{
assert.Equal(t, "foo <span class=\"removed-code\">bar</span> biz", diffToHTML(nil, []dmp.Diff{
{Type: dmp.DiffEqual, Text: "foo "},
{Type: dmp.DiffDelete, Text: "bar"},
{Type: dmp.DiffInsert, Text: " baz"},
{Type: dmp.DiffEqual, Text: " biz"},
}, DiffLineDel).Content)
assertEqual(t, "<span class=\"k\">if</span> <span class=\"p\">!</span><span class=\"nx\">nohl</span> <span class=\"o\">&amp;&amp;</span> <span class=\"added-code\"><span class=\"p\">(</span></span><span class=\"nx\">lexer</span> <span class=\"o\">!=</span> <span class=\"kc\">nil</span><span class=\"added-code\"> <span class=\"o\">||</span> <span class=\"nx\">r</span><span class=\"p\">.</span><span class=\"nx\">GuessLanguage</span><span class=\"p\">)</span></span> <span class=\"p\">{</span>", diffToHTML("", []dmp.Diff{
{Type: dmp.DiffEqual, Text: "<span class=\"k\">if</span> <span class=\"p\">!</span><span class=\"nx\">nohl</span> <span class=\"o\">&amp;&amp;</span> <span class=\""},
{Type: dmp.DiffInsert, Text: "p\">(</span><span class=\""},
{Type: dmp.DiffEqual, Text: "nx\">lexer</span> <span class=\"o\">!=</span> <span class=\"kc\">nil"},
{Type: dmp.DiffInsert, Text: "</span> <span class=\"o\">||</span> <span class=\"nx\">r</span><span class=\"p\">.</span><span class=\"nx\">GuessLanguage</span><span class=\"p\">)"},
{Type: dmp.DiffEqual, Text: "</span> <span class=\"p\">{</span>"},
}, DiffLineAdd).Content)
assertEqual(t, "<span class=\"nx\">tagURL</span> <span class=\"o\">:=</span> <span class=\"removed-code\"><span class=\"nx\">fmt</span><span class=\"p\">.</span><span class=\"nf\">Sprintf</span><span class=\"p\">(</span><span class=\"s\">&#34;## [%s](%s/%s/%s/%s?q=&amp;type=all&amp;state=closed&amp;milestone=%d) - %s&#34;</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Milestone\"</span></span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">BaseURL</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Owner</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Repo</span><span class=\"p\">,</span> <span class=\"removed-code\"><span class=\"nx\">from</span><span class=\"p\">,</span> <span class=\"nx\">milestoneID</span><span class=\"p\">,</span> <span class=\"nx\">time</span><span class=\"p\">.</span><span class=\"nf\">Now</span><span class=\"p\">(</span><span class=\"p\">)</span><span class=\"p\">.</span><span class=\"nf\">Format</span><span class=\"p\">(</span><span class=\"s\">&#34;2006-01-02&#34;</span><span class=\"p\">)</span></span><span class=\"p\">)</span>", diffToHTML("", []dmp.Diff{
{Type: dmp.DiffEqual, Text: "<span class=\"nx\">tagURL</span> <span class=\"o\">:=</span> <span class=\"n"},
{Type: dmp.DiffDelete, Text: "x\">fmt</span><span class=\"p\">.</span><span class=\"nf\">Sprintf</span><span class=\"p\">(</span><span class=\"s\">&#34;## [%s](%s/%s/%s/%s?q=&amp;type=all&amp;state=closed&amp;milestone=%d) - %s&#34;</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Milestone\""},
{Type: dmp.DiffInsert, Text: "f\">getGiteaTagURL</span><span class=\"p\">(</span><span class=\"nx\">client"},
{Type: dmp.DiffEqual, Text: "</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">BaseURL</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Owner</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Repo</span><span class=\"p\">,</span> <span class=\"nx\">"},
{Type: dmp.DiffDelete, Text: "from</span><span class=\"p\">,</span> <span class=\"nx\">milestoneID</span><span class=\"p\">,</span> <span class=\"nx\">time</span><span class=\"p\">.</span><span class=\"nf\">Now</span><span class=\"p\">(</span><span class=\"p\">)</span><span class=\"p\">.</span><span class=\"nf\">Format</span><span class=\"p\">(</span><span class=\"s\">&#34;2006-01-02&#34;</span><span class=\"p\">)"},
{Type: dmp.DiffInsert, Text: "ge</span><span class=\"p\">.</span><span class=\"nx\">Milestone</span><span class=\"p\">,</span> <span class=\"nx\">from</span><span class=\"p\">,</span> <span class=\"nx\">milestoneID"},
{Type: dmp.DiffEqual, Text: "</span><span class=\"p\">)</span>"},
}, DiffLineDel).Content)
assertEqual(t, "<span class=\"nx\">r</span><span class=\"p\">.</span><span class=\"nf\">WrapperRenderer</span><span class=\"p\">(</span><span class=\"nx\">w</span><span class=\"p\">,</span> <span class=\"removed-code\"><span class=\"nx\">language</span><span class=\"p\">,</span> <span class=\"kc\">true</span><span class=\"p\">,</span> <span class=\"nx\">attrs</span></span><span class=\"p\">,</span> <span class=\"kc\">false</span><span class=\"p\">)</span>", diffToHTML("", []dmp.Diff{
{Type: dmp.DiffEqual, Text: "<span class=\"nx\">r</span><span class=\"p\">.</span><span class=\"nf\">WrapperRenderer</span><span class=\"p\">(</span><span class=\"nx\">w</span><span class=\"p\">,</span> <span class=\"nx\">"},
{Type: dmp.DiffDelete, Text: "language</span><span "},
{Type: dmp.DiffEqual, Text: "c"},
{Type: dmp.DiffDelete, Text: "lass=\"p\">,</span> <span class=\"kc\">true</span><span class=\"p\">,</span> <span class=\"nx\">attrs"},
{Type: dmp.DiffEqual, Text: "</span><span class=\"p\">,</span> <span class=\"kc\">false</span><span class=\"p\">)</span>"},
}, DiffLineDel).Content)
assertEqual(t, "<span class=\"added-code\">language</span><span class=\"p\">,</span> <span class=\"kc\">true</span><span class=\"p\">,</span> <span class=\"nx\">attrs</span></span><span class=\"p\">,</span> <span class=\"kc\">false</span><span class=\"p\">)</span>", diffToHTML("", []dmp.Diff{
{Type: dmp.DiffInsert, Text: "language</span><span "},
{Type: dmp.DiffEqual, Text: "c"},
{Type: dmp.DiffInsert, Text: "lass=\"p\">,</span> <span class=\"kc\">true</span><span class=\"p\">,</span> <span class=\"nx\">attrs"},
{Type: dmp.DiffEqual, Text: "</span><span class=\"p\">,</span> <span class=\"kc\">false</span><span class=\"p\">)</span>"},
}, DiffLineAdd).Content)
assertEqual(t, "<span class=\"k\">print</span><span class=\"added-code\"><span class=\"p\">(</span></span><span class=\"sa\"></span><span class=\"s2\">&#34;</span><span class=\"s2\">// </span><span class=\"s2\">&#34;</span><span class=\"p\">,</span> <span class=\"n\">sys</span><span class=\"o\">.</span><span class=\"n\">argv</span><span class=\"added-code\"><span class=\"p\">)</span></span>", diffToHTML("", []dmp.Diff{
{Type: dmp.DiffEqual, Text: "<span class=\"k\">print</span>"},
{Type: dmp.DiffInsert, Text: "<span"},
{Type: dmp.DiffEqual, Text: " "},
{Type: dmp.DiffInsert, Text: "class=\"p\">(</span>"},
{Type: dmp.DiffEqual, Text: "<span class=\"sa\"></span><span class=\"s2\">&#34;</span><span class=\"s2\">// </span><span class=\"s2\">&#34;</span><span class=\"p\">,</span> <span class=\"n\">sys</span><span class=\"o\">.</span><span class=\"n\">argv</span>"},
{Type: dmp.DiffInsert, Text: "<span class=\"p\">)</span>"},
}, DiffLineAdd).Content)
assertEqual(t, "sh <span class=\"added-code\">&#39;useradd -u $(stat -c &#34;%u&#34; .gitignore) jenkins&#39;</span>", diffToHTML("", []dmp.Diff{
{Type: dmp.DiffEqual, Text: "sh &#3"},
{Type: dmp.DiffDelete, Text: "4;useradd -u 111 jenkins&#34"},
{Type: dmp.DiffInsert, Text: "9;useradd -u $(stat -c &#34;%u&#34; .gitignore) jenkins&#39"},
{Type: dmp.DiffEqual, Text: ";"},
}, DiffLineAdd).Content)
assertEqual(t, "<span class=\"x\"> &lt;h<span class=\"added-code\">4 class=&#34;release-list-title df ac&#34;</span>&gt;</span>", diffToHTML("", []dmp.Diff{
{Type: dmp.DiffEqual, Text: "<span class=\"x\"> &lt;h"},
{Type: dmp.DiffInsert, Text: "4 class=&#"},
{Type: dmp.DiffEqual, Text: "3"},
{Type: dmp.DiffInsert, Text: "4;release-list-title df ac&#34;"},
{Type: dmp.DiffEqual, Text: "&gt;</span>"},
}, DiffLineAdd).Content)
}, DiffLineDel))
}
func TestParsePatch_skipTo(t *testing.T) {
@ -592,7 +525,6 @@ index 0000000..6bb8f39
if err != nil {
t.Errorf("ParsePatch failed: %s", err)
}
println(result)
diff2 := `diff --git "a/A \\ B" "b/A \\ B"
--- "a/A \\ B"
@ -712,18 +644,6 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
}
}
func TestDiffToHTML_14231(t *testing.T) {
setting.Cfg = ini.Empty()
diffRecord := diffMatchPatch.DiffMain(highlight.Code("main.v", "", " run()\n"), highlight.Code("main.v", "", " run(db)\n"), true)
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
expected := `<span class="line"><span class="cl"> <span class="n">run</span><span class="added-code"><span class="o">(</span><span class="n">db</span></span><span class="o">)</span>
</span></span>`
output := diffToHTML("main.v", diffRecord, DiffLineAdd)
assertEqual(t, expected, output.Content)
}
func TestNoCrashes(t *testing.T) {
type testcase struct {
gitdiff string

View File

@ -0,0 +1,223 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gitdiff
import (
"strings"
"code.gitea.io/gitea/modules/highlight"
"github.com/sergi/go-diff/diffmatchpatch"
)
// token is a html tag or entity, eg: "<span ...>", "</span>", "&lt;"
func extractHTMLToken(s string) (before, token, after string, valid bool) {
for pos1 := 0; pos1 < len(s); pos1++ {
if s[pos1] == '<' {
pos2 := strings.IndexByte(s[pos1:], '>')
if pos2 == -1 {
return "", "", s, false
}
return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true
} else if s[pos1] == '&' {
pos2 := strings.IndexByte(s[pos1:], ';')
if pos2 == -1 {
return "", "", s, false
}
return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true
}
}
return "", "", s, true
}
// highlightCodeDiff is used to do diff with highlighted HTML code.
// It totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes.
// The HTML tags and entities will be replaced by Unicode placeholders: "<span>{TEXT}</span>" => "\uE000{TEXT}\uE001"
// These Unicode placeholders are friendly to the diff.
// Then after diff, the placeholders in diff result will be recovered to the HTML tags and entities.
// It's guaranteed that the tags in final diff result are paired correctly.
type highlightCodeDiff struct {
placeholderBegin rune
placeholderMaxCount int
placeholderIndex int
placeholderTokenMap map[rune]string
tokenPlaceholderMap map[string]rune
placeholderOverflowCount int
lineWrapperTags []string
}
func newHighlightCodeDiff() *highlightCodeDiff {
return &highlightCodeDiff{
placeholderBegin: rune(0x100000), // Plane 16: Supplementary Private Use Area B (U+100000..U+10FFFD)
placeholderMaxCount: 64000,
placeholderTokenMap: map[rune]string{},
tokenPlaceholderMap: map[string]rune{},
}
}
// nextPlaceholder returns 0 if no more placeholder can be used
// the diff is done line by line, usually there are only a few (no more than 10) placeholders in one line
// so the placeholderMaxCount is impossible to be exhausted in real cases.
func (hcd *highlightCodeDiff) nextPlaceholder() rune {
for hcd.placeholderIndex < hcd.placeholderMaxCount {
r := hcd.placeholderBegin + rune(hcd.placeholderIndex)
hcd.placeholderIndex++
// only use non-existing (not used by code) rune as placeholders
if _, ok := hcd.placeholderTokenMap[r]; !ok {
return r
}
}
return 0 // no more available placeholder
}
func (hcd *highlightCodeDiff) isInPlaceholderRange(r rune) bool {
return hcd.placeholderBegin <= r && r < hcd.placeholderBegin+rune(hcd.placeholderMaxCount)
}
func (hcd *highlightCodeDiff) collectUsedRunes(code string) {
for _, r := range code {
if hcd.isInPlaceholderRange(r) {
// put the existing rune (used by code) in map, then this rune won't be used a placeholder anymore.
hcd.placeholderTokenMap[r] = ""
}
}
}
func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB string) []diffmatchpatch.Diff {
hcd.collectUsedRunes(codeA)
hcd.collectUsedRunes(codeB)
highlightCodeA := highlight.Code(filename, language, codeA)
highlightCodeB := highlight.Code(filename, language, codeB)
highlightCodeA = hcd.convertToPlaceholders(highlightCodeA)
highlightCodeB = hcd.convertToPlaceholders(highlightCodeB)
diffs := diffMatchPatch.DiffMain(highlightCodeA, highlightCodeB, true)
diffs = diffMatchPatch.DiffCleanupEfficiency(diffs)
for i := range diffs {
hcd.recoverOneDiff(&diffs[i])
}
return diffs
}
// convertToPlaceholders totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes.
func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string {
var tagStack []string
res := strings.Builder{}
firstRunForLineTags := hcd.lineWrapperTags == nil
var beforeToken, token string
var valid bool
// the standard chroma highlight HTML is "<span class="line [hl]"><span class="cl"> ... </span></span>"
for {
beforeToken, token, htmlCode, valid = extractHTMLToken(htmlCode)
if !valid || token == "" {
break
}
// write the content before the token into result string, and consume the token in the string
res.WriteString(beforeToken)
// the line wrapper tags should be removed before diff
if strings.HasPrefix(token, `<span class="line`) || strings.HasPrefix(token, `<span class="cl"`) {
if firstRunForLineTags {
// if this is the first run for converting, save the line wrapper tags for later use, they should be added back
hcd.lineWrapperTags = append(hcd.lineWrapperTags, token)
}
htmlCode = strings.TrimSuffix(htmlCode, "</span>")
continue
}
var tokenInMap string
if strings.HasSuffix(token, "</") { // for closing tag
if len(tagStack) == 0 {
break // invalid diff result, no opening tag but see closing tag
}
// make sure the closing tag in map is related to the open tag, to make the diff algorithm can match the opening/closing tags
// the closing tag will be recorded in the map by key "</span><!-- <span the-opening> -->" for "<span the-opening>"
tokenInMap = token + "<!-- " + tagStack[len(tagStack)-1] + "-->"
tagStack = tagStack[:len(tagStack)-1]
} else if token[0] == '<' { // for opening tag
tokenInMap = token
tagStack = append(tagStack, token)
} else if token[0] == '&' { // for html entity
tokenInMap = token
} // else: impossible
// remember the placeholder and token in the map
placeholder, ok := hcd.tokenPlaceholderMap[tokenInMap]
if !ok {
placeholder = hcd.nextPlaceholder()
if placeholder != 0 {
hcd.tokenPlaceholderMap[tokenInMap] = placeholder
hcd.placeholderTokenMap[placeholder] = tokenInMap
}
}
if placeholder != 0 {
res.WriteRune(placeholder) // use the placeholder to replace the token
} else {
// unfortunately, all private use runes has been exhausted, no more placeholder could be used, no more converting
// usually, the exhausting won't occur in real cases, the magnitude of used placeholders is not larger than that of the CSS classes outputted by chroma.
hcd.placeholderOverflowCount++
if strings.HasPrefix(token, "&") {
// when the token is a html entity, something must be outputted even if there is no placeholder.
res.WriteRune(0xFFFD) // replacement character TODO: how to handle this case more gracefully?
res.WriteString(token[1:]) // still output the entity code part, otherwise there will be no diff result.
}
}
}
// write the remaining string
res.WriteString(htmlCode)
return res.String()
}
func (hcd *highlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) {
sb := strings.Builder{}
var tagStack []string
for _, r := range diff.Text {
token, ok := hcd.placeholderTokenMap[r]
if !ok || token == "" {
sb.WriteRune(r) // if the rune is not a placeholder, write it as it is
continue
}
var tokenToRecover string
if strings.HasPrefix(token, "</") { // for closing tag
// only get the tag itself, ignore the trailing comment (for how the comment is generated, see the code in `convert` function)
tokenToRecover = token[:strings.IndexByte(token, '>')+1]
if len(tagStack) == 0 {
continue // if no opening tag in stack yet, skip the closing tag
}
tagStack = tagStack[:len(tagStack)-1]
} else if token[0] == '<' { // for opening tag
tokenToRecover = token
tagStack = append(tagStack, token)
} else if token[0] == '&' { // for html entity
tokenToRecover = token
} // else: impossible
sb.WriteString(tokenToRecover)
}
if len(tagStack) > 0 {
// close all opening tags
for i := len(tagStack) - 1; i >= 0; i-- {
tagToClose := tagStack[i]
// get the closing tag "</span>" from "<span class=...>" or "<span>"
pos := strings.IndexAny(tagToClose, " >")
if pos != -1 {
sb.WriteString("</" + tagToClose[1:pos] + ">")
} // else: impossible. every tag was pushed into the stack by the code above and is valid HTML opening tag
}
}
diff.Text = sb.String()
}

View File

@ -0,0 +1,126 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gitdiff
import (
"fmt"
"strings"
"testing"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
)
func TestDiffWithHighlight(t *testing.T) {
hcd := newHighlightCodeDiff()
diffs := hcd.diffWithHighlight(
"main.v", "",
" run('<>')\n",
" run(db)\n",
)
expected := ` <span class="n">run</span><span class="o">(</span><span class="removed-code"><span class="k">&#39;</span><span class="o">&lt;</span><span class="o">&gt;</span><span class="k">&#39;</span></span><span class="o">)</span>` + "\n"
output := diffToHTML(nil, diffs, DiffLineDel)
assert.Equal(t, expected, output)
expected = ` <span class="n">run</span><span class="o">(</span><span class="added-code"><span class="n">db</span></span><span class="o">)</span>` + "\n"
output = diffToHTML(nil, diffs, DiffLineAdd)
assert.Equal(t, expected, output)
hcd = newHighlightCodeDiff()
hcd.placeholderTokenMap['O'] = "<span>"
hcd.placeholderTokenMap['C'] = "</span>"
diff := diffmatchpatch.Diff{}
diff.Text = "OC"
hcd.recoverOneDiff(&diff)
assert.Equal(t, "<span></span>", diff.Text)
diff.Text = "O"
hcd.recoverOneDiff(&diff)
assert.Equal(t, "<span></span>", diff.Text)
diff.Text = "C"
hcd.recoverOneDiff(&diff)
assert.Equal(t, "", diff.Text)
}
func TestDiffWithHighlightPlaceholder(t *testing.T) {
hcd := newHighlightCodeDiff()
diffs := hcd.diffWithHighlight(
"main.js", "",
"a='\U00100000'",
"a='\U0010FFFD''",
)
assert.Equal(t, "", hcd.placeholderTokenMap[0x00100000])
assert.Equal(t, "", hcd.placeholderTokenMap[0x0010FFFD])
expected := fmt.Sprintf(`<span class="line"><span class="cl"><span class="nx">a</span><span class="o">=</span><span class="s1">&#39;</span><span class="removed-code">%s</span>&#39;</span></span>`, "\U00100000")
output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel)
assert.Equal(t, expected, output)
hcd = newHighlightCodeDiff()
diffs = hcd.diffWithHighlight(
"main.js", "",
"a='\U00100000'",
"a='\U0010FFFD'",
)
expected = fmt.Sprintf(`<span class="nx">a</span><span class="o">=</span><span class="s1">&#39;</span><span class="added-code">%s</span>&#39;`, "\U0010FFFD")
output = diffToHTML(nil, diffs, DiffLineAdd)
assert.Equal(t, expected, output)
}
func TestDiffWithHighlightPlaceholderExhausted(t *testing.T) {
hcd := newHighlightCodeDiff()
hcd.placeholderMaxCount = 0
diffs := hcd.diffWithHighlight(
"main.js", "",
"'",
``,
)
output := diffToHTML(nil, diffs, DiffLineDel)
expected := fmt.Sprintf(`<span class="removed-code">%s#39;</span>`, "\uFFFD")
assert.Equal(t, expected, output)
hcd = newHighlightCodeDiff()
hcd.placeholderMaxCount = 0
diffs = hcd.diffWithHighlight(
"main.js", "",
"a < b",
"a > b",
)
output = diffToHTML(nil, diffs, DiffLineDel)
expected = fmt.Sprintf(`a %s<span class="removed-code">l</span>t; b`, "\uFFFD")
assert.Equal(t, expected, output)
output = diffToHTML(nil, diffs, DiffLineAdd)
expected = fmt.Sprintf(`a %s<span class="added-code">g</span>t; b`, "\uFFFD")
assert.Equal(t, expected, output)
}
func TestDiffWithHighlightTagMatch(t *testing.T) {
totalOverflow := 0
for i := 0; i < 100; i++ {
hcd := newHighlightCodeDiff()
hcd.placeholderMaxCount = i
diffs := hcd.diffWithHighlight(
"main.js", "",
"a='1'",
"b='2'",
)
totalOverflow += hcd.placeholderOverflowCount
output := diffToHTML(nil, diffs, DiffLineDel)
c1 := strings.Count(output, "<span")
c2 := strings.Count(output, "</span")
assert.Equal(t, c1, c2)
output = diffToHTML(nil, diffs, DiffLineAdd)
c1 = strings.Count(output, "<span")
c2 = strings.Count(output, "</span")
assert.Equal(t, c1, c2)
}
assert.NotZero(t, totalOverflow)
}

View File

@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/util"
container_service "code.gitea.io/gitea/services/packages/container"
)
@ -462,7 +463,8 @@ func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
PageSize: repo_model.RepositoryListDefaultPageSize,
Page: 1,
},
OwnerID: userID,
OwnerID: userID,
IsInternal: util.OptionalBoolNone,
})
if err != nil {
return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)

View File

@ -34,15 +34,13 @@ func CacheRef(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep
return err
}
commitsCount, err := cache.GetInt64(repo.GetCommitsCountCacheKey(getRefName(fullRefName), true), commit.CommitsCount)
if err != nil {
return err
}
if commitsCount < setting.CacheService.LastCommit.CommitsCount {
return nil
if gitRepo.LastCommitCache == nil {
commitsCount, err := cache.GetInt64(repo.GetCommitsCountCacheKey(getRefName(fullRefName), true), commit.CommitsCount)
if err != nil {
return err
}
gitRepo.LastCommitCache = git.NewLastCommitCache(commitsCount, repo.FullName(), gitRepo, cache.GetCache())
}
commitCache := git.NewLastCommitCache(repo.FullName(), gitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
return commitCache.CacheCommit(ctx, commit)
return commit.CacheCommit(ctx)
}

View File

@ -101,6 +101,22 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, treePat
return fileList, nil
}
// GetObjectTypeFromTreeEntry check what content is behind it
func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
switch {
case entry.IsDir():
return ContentTypeDir
case entry.IsSubModule():
return ContentTypeSubmodule
case entry.IsExecutable(), entry.IsRegular():
return ContentTypeRegular
case entry.IsLink():
return ContentTypeLink
default:
return ""
}
}
// GetContents gets the meta data on a file's contents. Ref can be a branch, commit or tag
func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref string, forList bool) (*api.ContentsResponse, error) {
if ref == "" {

View File

@ -75,6 +75,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
},
Private: true,
OwnerID: u.ID,
Actor: u,
})
if err != nil {
return fmt.Errorf("SearchRepositoryByName: %v", err)

View File

@ -60,6 +60,26 @@ func TestDeleteUser(t *testing.T) {
assert.Error(t, DeleteUser(db.DefaultContext, org, false))
}
func TestPurgeUser(t *testing.T) {
test := func(userID int64) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}).(*user_model.User)
err := DeleteUser(db.DefaultContext, user, true)
assert.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})
}
test(2)
test(4)
test(8)
test(11)
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
assert.Error(t, DeleteUser(db.DefaultContext, org, false))
}
func TestCreateUser(t *testing.T) {
user := &user_model.User{
Name: "GiteaBot",

View File

@ -114,7 +114,7 @@
</div>
</div>
<a href="{{AppSubUrl}}/notifications" class="item tooltip not-mobile" data-content='{{.locale.Tr "notifications"}}'>
<a href="{{AppSubUrl}}/notifications" class="item tooltip not-mobile" data-content="{{.locale.Tr "notifications"}}" aria-label="{{.locale.Tr "notifications"}}">
<span class="text">
<span class="fitted">{{svg "octicon-bell"}}</span>
<span class="ui red label {{if not $notificationUnreadCount}}hidden{{end}} notification_count">

View File

@ -29,7 +29,7 @@
{{end}}
</div>
</div>
{{if not .PublicOnly}}
{{if not $.PublicOnly}}
<div class="ui three wide column center">
<div class="meta">
{{$.locale.Tr "org.members.member_role"}}

View File

@ -47,6 +47,10 @@
<div class="empty center">
{{svg "octicon-package" 32}}
<h2>{{.locale.Tr "packages.empty"}}</h2>
{{if and .Repository .CanWritePackages}}
{{$packagesUrl := URLJoin .Owner.HTMLURL "-" "packages" }}
<p>{{.locale.Tr "packages.empty.repo" $packagesUrl | Safe}}</p>
{{end}}
<p>{{.locale.Tr "packages.empty.documentation" | Safe}}</p>
</div>
{{else}}

View File

@ -19,6 +19,6 @@
document.getElementById('repo-clone-url').value = btn ? btn.getAttribute('data-link') : '';
})();
</script>
<button class="ui basic icon button tooltip" id="clipboard-btn" data-content="{{.locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url">
<button class="ui basic icon button tooltip" id="clipboard-btn" data-content="{{.locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{.locale.Tr "copy_url"}}">
{{svg "octicon-paste"}}
</button>

View File

@ -484,7 +484,7 @@
</div>
</div>
<div class="ui attached segment comment-body">
<div class="render-content markup">
<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
{{if .RenderedContent}}
{{.RenderedContent|Str2html}}
{{else}}

View File

@ -8,9 +8,9 @@
{{if .LatestCommitUser}}
{{avatar .LatestCommitUser 24}}
{{if .LatestCommitUser.FullName}}
<a href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
<a class="muted" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
{{else}}
<a href="{{.LatestCommitUser.HomeLink}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>
<a class="muted" href="{{.LatestCommitUser.HomeLink}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>
{{end}}
{{else}}
{{if .LatestCommit.Author}}
@ -54,7 +54,7 @@
{{svg "octicon-file-submodule"}}
{{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}}
{{if $refURL}}
<a href="{{$refURL}}">{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
<a class="muted" href="{{$refURL}}">{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
{{else}}
{{$entry.Name}}<span class="at">@</span>{{ShortSha $subModuleFile.RefID}}
{{end}}
@ -63,16 +63,16 @@
{{$subJumpablePathName := $entry.GetSubJumpablePathName}}
{{$subJumpablePath := SubJumpablePath $subJumpablePathName}}
{{svg "octicon-file-directory-fill"}}
<a href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}">
<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}">
{{if eq (len $subJumpablePath) 2}}
<span class="jumpable-path">{{index $subJumpablePath 0}}</span>{{index $subJumpablePath 1}}
<span class="color-text-light-2">{{index $subJumpablePath 0}}</span>{{index $subJumpablePath 1}}
{{else}}
{{index $subJumpablePath 0}}
{{end}}
</a>
{{else}}
{{svg (printf "octicon-%s" (EntryIcon $entry))}}
<a href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a>
<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a>
{{end}}
{{end}}
</span>

View File

@ -89,6 +89,11 @@
{{svg "octicon-milestone" 14 "mr-2"}}{{.Milestone.Name}}
</a>
{{end}}
{{if .Project}}
<a class="project" {{if $.RepoLink}}href="{{$.RepoLink}}/projects/{{.Project.ID}}"{{else}}href="{{.Repo.Link}}/projects/{{.Project.ID}}"{{end}}>
{{svg "octicon-project" 14 "mr-2"}}{{.Project.Title}}
</a>
{{end}}
{{if .Ref}}
<a class="ref" {{if $.RepoLink}}href="{{index $.IssueRefURLs .ID}}"{{else}}href="{{.Repo.Link}}{{index $.IssueRefURLs .ID}}"{{end}}>
{{svg "octicon-git-branch" 14 "mr-2"}}{{index $.IssueRefEndNames .ID}}

View File

@ -389,7 +389,8 @@ export function initGlobalButtons() {
*/
export function checkAppUrl() {
const curUrl = window.location.href;
if (curUrl.startsWith(appUrl)) {
// some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning
if (curUrl.startsWith(appUrl) || `${curUrl}/` === appUrl) {
return;
}
if (document.querySelector('.page-content.install')) {

View File

@ -140,7 +140,7 @@ function updateStopwatchData(data) {
$('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`);
$('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`);
$('.stopwatch-time').text(prettyMilliseconds(seconds * 1000));
updateStopwatchTime(seconds);
updateTimeInterval = updateStopwatchTime(seconds);
btnEl.removeClass('hidden');
}
@ -149,10 +149,10 @@ function updateStopwatchData(data) {
function updateStopwatchTime(seconds) {
const secs = parseInt(seconds);
if (!Number.isFinite(secs)) return;
if (!Number.isFinite(secs)) return null;
const start = Date.now();
updateTimeInterval = setInterval(() => {
return setInterval(() => {
const delta = Date.now() - start;
const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
$('.stopwatch-time').text(dur);

View File

@ -64,7 +64,7 @@ export function parseIssueHref(href) {
export function strSubMatch(full, sub) {
const res = [''];
let i = 0, j = 0;
for (; i < sub.length && j < full.length;) {
while (i < sub.length && j < full.length) {
while (j < full.length) {
if (sub[i] === full[j]) {
if (res.length % 2 !== 0) res.push('');

View File

@ -118,6 +118,7 @@
--color-text-dark: #080808;
--color-text: #212121;
--color-text-light: #555555;
--color-text-light-1: #6a6a6a;
--color-text-light-2: #808080;
--color-text-light-3: #a0a0a0;
--color-box-header: #f7f7f7;
@ -275,6 +276,7 @@ a.muted {
a:hover,
a.muted:hover,
a.muted:hover [class*="color-text"],
.ui.breadcrumb a:hover {
color: var(--color-primary);
}
@ -2206,3 +2208,7 @@ table th[data-sortt-desc] {
}
}
}
.color-text-light-2 {
color: var(--color-text-light-2);
}

View File

@ -367,6 +367,8 @@
}
&.message {
color: var(--color-text-light-1);
@media @mediaXl {
max-width: 400px;
}
@ -381,6 +383,7 @@
&.age {
width: 120px;
color: var(--color-text-light-1);
}
.truncate {
@ -432,10 +435,6 @@
padding-bottom: 8px;
width: calc(100% - 1.25rem);
}
.jumpable-path {
color: var(--color-text-light-2);
}
}
.non-diff-file-content {

Some files were not shown because too many files have changed in this diff Show More