diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 2264c9e822..f459e3910d 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -59,6 +59,8 @@ jobs: aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress nightly-docker-rootful: runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -85,6 +87,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: fetch go modules run: make vendor - name: build rootful docker image @@ -93,9 +101,13 @@ jobs: context: . platforms: linux/amd64,linux/arm64 push: true - tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} + tags: |- + gitea/gitea:${{ steps.clean_name.outputs.branch }} + ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }} nightly-docker-rootless: runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -122,6 +134,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: fetch go modules run: make vendor - name: build rootless docker image @@ -131,4 +149,6 @@ jobs: platforms: linux/amd64,linux/arm64 push: true file: Dockerfile.rootless - tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless + tags: |- + gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless + ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index a406602dc0..02da6d1eab 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -69,6 +69,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -79,7 +81,9 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea flavor: | latest=false # 1.2.3-rc0 @@ -90,6 +94,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootful docker image uses: docker/build-push-action@v5 with: @@ -100,6 +110,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} docker-rootless: runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -110,7 +122,9 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea # each tag below will have the suffix of -rootless flavor: | latest=false @@ -123,6 +137,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootless docker image uses: docker/build-push-action@v5 with: diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 08bb9baecf..158945b615 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -14,6 +14,8 @@ concurrency: jobs: binary: runs-on: namespace-profile-gitea-release-binary + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -71,6 +73,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -81,7 +85,9 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea # this will generate tags in the following format: # latest # 1 @@ -96,6 +102,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootful docker image uses: docker/build-push-action@v5 with: @@ -116,7 +128,9 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea # each tag below will have the suffix of -rootless flavor: | suffix=-rootless,onlatest=true @@ -134,6 +148,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootless docker image uses: docker/build-push-action@v5 with: diff --git a/modules/git/attribute.go b/modules/git/attribute.go deleted file mode 100644 index 4dfa510369..0000000000 --- a/modules/git/attribute.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "code.gitea.io/gitea/modules/optional" -) - -const ( - AttributeLinguistVendored = "linguist-vendored" - AttributeLinguistGenerated = "linguist-generated" - AttributeLinguistDocumentation = "linguist-documentation" - AttributeLinguistDetectable = "linguist-detectable" - AttributeLinguistLanguage = "linguist-language" - AttributeGitlabLanguage = "gitlab-language" -) - -// true if "set"/"true", false if "unset"/"false", none otherwise -func AttributeToBool(attr map[string]string, name string) optional.Option[bool] { - switch attr[name] { - case "set", "true": - return optional.Some(true) - case "unset", "false": - return optional.Some(false) - } - return optional.None[bool]() -} - -func AttributeToString(attr map[string]string, name string) optional.Option[string] { - if value, has := attr[name]; has && value != "unspecified" { - return optional.Some(value) - } - return optional.None[string]() -} diff --git a/modules/git/attribute/attribute.go b/modules/git/attribute/attribute.go new file mode 100644 index 0000000000..adf323ef41 --- /dev/null +++ b/modules/git/attribute/attribute.go @@ -0,0 +1,114 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "strings" + + "code.gitea.io/gitea/modules/optional" +) + +type Attribute string + +const ( + LinguistVendored = "linguist-vendored" + LinguistGenerated = "linguist-generated" + LinguistDocumentation = "linguist-documentation" + LinguistDetectable = "linguist-detectable" + LinguistLanguage = "linguist-language" + GitlabLanguage = "gitlab-language" + Lockable = "lockable" + Filter = "filter" +) + +var LinguistAttributes = []string{ + LinguistVendored, + LinguistGenerated, + LinguistDocumentation, + LinguistDetectable, + LinguistLanguage, + GitlabLanguage, +} + +func (a Attribute) IsUnspecified() bool { + return a == "" || a == "unspecified" +} + +func (a Attribute) ToString() optional.Option[string] { + if !a.IsUnspecified() { + return optional.Some(string(a)) + } + return optional.None[string]() +} + +// ToBool converts the attribute value to optional boolean: true if "set"/"true", false if "unset"/"false", none otherwise +func (a Attribute) ToBool() optional.Option[bool] { + switch a { + case "set", "true": + return optional.Some(true) + case "unset", "false": + return optional.Some(false) + } + return optional.None[bool]() +} + +type Attributes struct { + m map[string]Attribute +} + +func NewAttributes() *Attributes { + return &Attributes{m: make(map[string]Attribute)} +} + +func (attrs *Attributes) Get(name string) Attribute { + if value, has := attrs.m[name]; has { + return value + } + return "" +} + +func (attrs *Attributes) GetVendored() optional.Option[bool] { + return attrs.Get(LinguistVendored).ToBool() +} + +func (attrs *Attributes) GetGenerated() optional.Option[bool] { + return attrs.Get(LinguistGenerated).ToBool() +} + +func (attrs *Attributes) GetDocumentation() optional.Option[bool] { + return attrs.Get(LinguistDocumentation).ToBool() +} + +func (attrs *Attributes) GetDetectable() optional.Option[bool] { + return attrs.Get(LinguistDetectable).ToBool() +} + +func (attrs *Attributes) GetLinguistLanguage() optional.Option[string] { + return attrs.Get(LinguistLanguage).ToString() +} + +func (attrs *Attributes) GetGitlabLanguage() optional.Option[string] { + attrStr := attrs.Get(GitlabLanguage).ToString() + if attrStr.Has() { + raw := attrStr.Value() + // gitlab-language may have additional parameters after the language + // ignore them and just use the main language + // https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type + if idx := strings.IndexByte(raw, '?'); idx >= 0 { + return optional.Some(raw[:idx]) + } + } + return attrStr +} + +func (attrs *Attributes) GetLanguage() optional.Option[string] { + // prefer linguist-language over gitlab-language + // if linguist-language is not set, use gitlab-language + // if both are not set, return none + language := attrs.GetLinguistLanguage() + if language.Value() == "" { + language = attrs.GetGitlabLanguage() + } + return language +} diff --git a/modules/git/attribute/attribute_test.go b/modules/git/attribute/attribute_test.go new file mode 100644 index 0000000000..dadb5582a3 --- /dev/null +++ b/modules/git/attribute/attribute_test.go @@ -0,0 +1,37 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Attribute(t *testing.T) { + assert.Empty(t, Attribute("").ToString().Value()) + assert.Empty(t, Attribute("unspecified").ToString().Value()) + assert.Equal(t, "python", Attribute("python").ToString().Value()) + assert.Equal(t, "Java", Attribute("Java").ToString().Value()) + + attributes := Attributes{ + m: map[string]Attribute{ + LinguistGenerated: "true", + LinguistDocumentation: "false", + LinguistDetectable: "set", + LinguistLanguage: "Python", + GitlabLanguage: "Java", + "filter": "unspecified", + "test": "", + }, + } + + assert.Empty(t, attributes.Get("test").ToString().Value()) + assert.Empty(t, attributes.Get("filter").ToString().Value()) + assert.Equal(t, "Python", attributes.Get(LinguistLanguage).ToString().Value()) + assert.Equal(t, "Java", attributes.Get(GitlabLanguage).ToString().Value()) + assert.True(t, attributes.Get(LinguistGenerated).ToBool().Value()) + assert.False(t, attributes.Get(LinguistDocumentation).ToBool().Value()) + assert.True(t, attributes.Get(LinguistDetectable).ToBool().Value()) +} diff --git a/modules/git/attribute/batch.go b/modules/git/attribute/batch.go new file mode 100644 index 0000000000..4e31fda575 --- /dev/null +++ b/modules/git/attribute/batch.go @@ -0,0 +1,216 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "time" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// BatchChecker provides a reader for check-attribute content that can be long running +type BatchChecker struct { + attributesNum int + repo *git.Repository + stdinWriter *os.File + stdOut *nulSeparatedAttributeWriter + ctx context.Context + cancel context.CancelFunc + cmd *git.Command +} + +// NewBatchChecker creates a check attribute reader for the current repository and provided commit ID +// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo +func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) { + ctx, cancel := context.WithCancel(repo.Ctx) + defer func() { + if returnedErr != nil { + cancel() + } + }() + + cmd, envs, cleanup, err := checkAttrCommand(repo, treeish, nil, attributes) + if err != nil { + return nil, err + } + defer func() { + if returnedErr != nil { + cleanup() + } + }() + + cmd.AddArguments("--stdin") + + checker = &BatchChecker{ + attributesNum: len(attributes), + repo: repo, + ctx: ctx, + cmd: cmd, + cancel: func() { + cancel() + cleanup() + }, + } + + stdinReader, stdinWriter, err := os.Pipe() + if err != nil { + return nil, err + } + checker.stdinWriter = stdinWriter + + lw := new(nulSeparatedAttributeWriter) + lw.attributes = make(chan attributeTriple, len(attributes)) + lw.closed = make(chan struct{}) + checker.stdOut = lw + + go func() { + defer func() { + _ = stdinReader.Close() + _ = lw.Close() + }() + stdErr := new(bytes.Buffer) + err := cmd.Run(ctx, &git.RunOpts{ + Env: envs, + Dir: repo.Path, + Stdin: stdinReader, + Stdout: lw, + Stderr: stdErr, + }) + + if err != nil && !git.IsErrCanceledOrKilled(err) { + log.Error("Attribute checker for commit %s exits with error: %v", treeish, err) + } + checker.cancel() + }() + + return checker, nil +} + +// CheckPath check attr for given path +func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) { + defer func() { + if err != nil && err != c.ctx.Err() { + log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.repo.Path), err) + } + }() + + select { + case <-c.ctx.Done(): + return nil, c.ctx.Err() + default: + } + + if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil { + defer c.Close() + return nil, err + } + + reportTimeout := func() error { + stdOutClosed := false + select { + case <-c.stdOut.closed: + stdOutClosed = true + default: + } + debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.repo.Path)) + debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed) + if c.cmd != nil { + debugMsg += fmt.Sprintf(", process state: %q", c.cmd.ProcessState()) + } + _ = c.Close() + return fmt.Errorf("CheckPath timeout: %s", debugMsg) + } + + rs = NewAttributes() + for i := 0; i < c.attributesNum; i++ { + select { + case <-time.After(5 * time.Second): + // there is no "hang" problem now. This code is just used to catch other potential problems. + return nil, reportTimeout() + case attr, ok := <-c.stdOut.ReadAttribute(): + if !ok { + return nil, c.ctx.Err() + } + rs.m[attr.Attribute] = Attribute(attr.Value) + case <-c.ctx.Done(): + return nil, c.ctx.Err() + } + } + return rs, nil +} + +func (c *BatchChecker) Close() error { + c.cancel() + err := c.stdinWriter.Close() + return err +} + +type attributeTriple struct { + Filename string + Attribute string + Value string +} + +type nulSeparatedAttributeWriter struct { + tmp []byte + attributes chan attributeTriple + closed chan struct{} + working attributeTriple + pos int +} + +func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { + l, read := len(p), 0 + + nulIdx := bytes.IndexByte(p, '\x00') + for nulIdx >= 0 { + wr.tmp = append(wr.tmp, p[:nulIdx]...) + switch wr.pos { + case 0: + wr.working = attributeTriple{ + Filename: string(wr.tmp), + } + case 1: + wr.working.Attribute = string(wr.tmp) + case 2: + wr.working.Value = string(wr.tmp) + } + wr.tmp = wr.tmp[:0] + wr.pos++ + if wr.pos > 2 { + wr.attributes <- wr.working + wr.pos = 0 + } + read += nulIdx + 1 + if l > read { + p = p[nulIdx+1:] + nulIdx = bytes.IndexByte(p, '\x00') + } else { + return l, nil + } + } + wr.tmp = append(wr.tmp, p...) + return l, nil +} + +func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { + return wr.attributes +} + +func (wr *nulSeparatedAttributeWriter) Close() error { + select { + case <-wr.closed: + return nil + default: + } + close(wr.attributes) + close(wr.closed) + return nil +} diff --git a/modules/git/repo_attribute_test.go b/modules/git/attribute/batch_test.go similarity index 50% rename from modules/git/repo_attribute_test.go rename to modules/git/attribute/batch_test.go index d8fd9f0e8d..30a3d805fe 100644 --- a/modules/git/repo_attribute_test.go +++ b/modules/git/attribute/batch_test.go @@ -1,13 +1,19 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package attribute import ( + "path/filepath" "testing" "time" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { @@ -24,7 +30,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { select { case attr := <-wr.ReadAttribute(): assert.Equal(t, ".gitignore\"\n", attr.Filename) - assert.Equal(t, AttributeLinguistVendored, attr.Attribute) + assert.Equal(t, LinguistVendored, attr.Attribute) assert.Equal(t, "unspecified", attr.Value) case <-time.After(100 * time.Millisecond): assert.FailNow(t, "took too long to read an attribute from the list") @@ -38,7 +44,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { select { case attr := <-wr.ReadAttribute(): assert.Equal(t, ".gitignore\"\n", attr.Filename) - assert.Equal(t, AttributeLinguistVendored, attr.Attribute) + assert.Equal(t, LinguistVendored, attr.Attribute) assert.Equal(t, "unspecified", attr.Value) case <-time.After(100 * time.Millisecond): assert.FailNow(t, "took too long to read an attribute from the list") @@ -77,21 +83,90 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { assert.NoError(t, err) assert.Equal(t, attributeTriple{ Filename: "shouldbe.vendor", - Attribute: AttributeLinguistVendored, + Attribute: LinguistVendored, Value: "set", }, attr) attr = <-wr.ReadAttribute() assert.NoError(t, err) assert.Equal(t, attributeTriple{ Filename: "shouldbe.vendor", - Attribute: AttributeLinguistGenerated, + Attribute: LinguistGenerated, Value: "unspecified", }, attr) attr = <-wr.ReadAttribute() assert.NoError(t, err) assert.Equal(t, attributeTriple{ Filename: "shouldbe.vendor", - Attribute: AttributeLinguistLanguage, + Attribute: LinguistLanguage, Value: "unspecified", }, attr) } + +func expectedAttrs() *Attributes { + return &Attributes{ + m: map[string]Attribute{ + LinguistGenerated: "unspecified", + LinguistDetectable: "unspecified", + LinguistDocumentation: "unspecified", + LinguistVendored: "unspecified", + LinguistLanguage: "Python", + GitlabLanguage: "unspecified", + }, + } +} + +func Test_BatchChecker(t *testing.T) { + setting.AppDataPath = t.TempDir() + repoPath := "../tests/repos/language_stats_repo" + gitRepo, err := git.OpenRepository(t.Context(), repoPath) + require.NoError(t, err) + defer gitRepo.Close() + + commitID := "8fee858da5796dfb37704761701bb8e800ad9ef3" + + t.Run("Create index file to run git check-attr", func(t *testing.T) { + defer test.MockVariableValue(&git.DefaultFeatures().SupportCheckAttrOnBare, false)() + checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes) + assert.NoError(t, err) + defer checker.Close() + attributes, err := checker.CheckPath("i-am-a-python.p") + assert.NoError(t, err) + assert.Equal(t, expectedAttrs(), attributes) + }) + + // run git check-attr on work tree + t.Run("Run git check-attr on git work tree", func(t *testing.T) { + dir := filepath.Join(t.TempDir(), "test-repo") + err := git.Clone(t.Context(), repoPath, dir, git.CloneRepoOptions{ + Shared: true, + Branch: "master", + }) + assert.NoError(t, err) + + tempRepo, err := git.OpenRepository(t.Context(), dir) + assert.NoError(t, err) + defer tempRepo.Close() + + checker, err := NewBatchChecker(tempRepo, "", LinguistAttributes) + assert.NoError(t, err) + defer checker.Close() + attributes, err := checker.CheckPath("i-am-a-python.p") + assert.NoError(t, err) + assert.Equal(t, expectedAttrs(), attributes) + }) + + if !git.DefaultFeatures().SupportCheckAttrOnBare { + t.Skip("git version 2.40 is required to support run check-attr on bare repo") + return + } + + t.Run("Run git check-attr in bare repository", func(t *testing.T) { + checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes) + assert.NoError(t, err) + defer checker.Close() + + attributes, err := checker.CheckPath("i-am-a-python.p") + assert.NoError(t, err) + assert.Equal(t, expectedAttrs(), attributes) + }) +} diff --git a/modules/git/attribute/checker.go b/modules/git/attribute/checker.go new file mode 100644 index 0000000000..c17006a154 --- /dev/null +++ b/modules/git/attribute/checker.go @@ -0,0 +1,96 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + + "code.gitea.io/gitea/modules/git" +) + +func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attributes []string) (*git.Command, []string, func(), error) { + cancel := func() {} + envs := []string{"GIT_FLUSH=1"} + cmd := git.NewCommand("check-attr", "-z") + if len(attributes) == 0 { + cmd.AddArguments("--all") + } + + // there is treeish, read from bare repo or temp index created by "read-tree" + if treeish != "" { + if git.DefaultFeatures().SupportCheckAttrOnBare { + cmd.AddArguments("--source") + cmd.AddDynamicArguments(treeish) + } else { + indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(treeish) + if err != nil { + return nil, nil, nil, err + } + + cmd.AddArguments("--cached") + envs = append(envs, + "GIT_INDEX_FILE="+indexFilename, + "GIT_WORK_TREE="+worktree, + ) + cancel = deleteTemporaryFile + } + } // else: no treeish, assume it is a not a bare repo, read from working directory + + cmd.AddDynamicArguments(attributes...) + if len(filenames) > 0 { + cmd.AddDashesAndList(filenames...) + } + return cmd, envs, cancel, nil +} + +type CheckAttributeOpts struct { + Filenames []string + Attributes []string +} + +// CheckAttributes return the attributes of the given filenames and attributes in the given treeish. +// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo +func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish string, opts CheckAttributeOpts) (map[string]*Attributes, error) { + cmd, envs, cancel, err := checkAttrCommand(gitRepo, treeish, opts.Filenames, opts.Attributes) + if err != nil { + return nil, err + } + defer cancel() + + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + + if err := cmd.Run(ctx, &git.RunOpts{ + Env: append(os.Environ(), envs...), + Dir: gitRepo.Path, + Stdout: stdOut, + Stderr: stdErr, + }); err != nil { + return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) + } + + fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) + if len(fields)%3 != 1 { + return nil, errors.New("wrong number of fields in return from check-attr") + } + + attributesMap := make(map[string]*Attributes) + for i := 0; i < (len(fields) / 3); i++ { + filename := string(fields[3*i]) + attribute := string(fields[3*i+1]) + info := string(fields[3*i+2]) + attribute2info, ok := attributesMap[filename] + if !ok { + attribute2info = NewAttributes() + attributesMap[filename] = attribute2info + } + attribute2info.m[attribute] = Attribute(info) + } + + return attributesMap, nil +} diff --git a/modules/git/attribute/checker_test.go b/modules/git/attribute/checker_test.go new file mode 100644 index 0000000000..97db43460b --- /dev/null +++ b/modules/git/attribute/checker_test.go @@ -0,0 +1,74 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Checker(t *testing.T) { + setting.AppDataPath = t.TempDir() + repoPath := "../tests/repos/language_stats_repo" + gitRepo, err := git.OpenRepository(t.Context(), repoPath) + require.NoError(t, err) + defer gitRepo.Close() + + commitID := "8fee858da5796dfb37704761701bb8e800ad9ef3" + + t.Run("Create index file to run git check-attr", func(t *testing.T) { + defer test.MockVariableValue(&git.DefaultFeatures().SupportCheckAttrOnBare, false)() + attrs, err := CheckAttributes(t.Context(), gitRepo, commitID, CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) + + // run git check-attr on work tree + t.Run("Run git check-attr on git work tree", func(t *testing.T) { + dir := filepath.Join(t.TempDir(), "test-repo") + err := git.Clone(t.Context(), repoPath, dir, git.CloneRepoOptions{ + Shared: true, + Branch: "master", + }) + assert.NoError(t, err) + + tempRepo, err := git.OpenRepository(t.Context(), dir) + assert.NoError(t, err) + defer tempRepo.Close() + + attrs, err := CheckAttributes(t.Context(), tempRepo, "", CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) + + if !git.DefaultFeatures().SupportCheckAttrOnBare { + t.Skip("git version 2.40 is required to support run check-attr on bare repo") + return + } + + t.Run("Run git check-attr in bare repository", func(t *testing.T) { + attrs, err := CheckAttributes(t.Context(), gitRepo, commitID, CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) +} diff --git a/modules/git/attribute/main_test.go b/modules/git/attribute/main_test.go new file mode 100644 index 0000000000..df8241bfb0 --- /dev/null +++ b/modules/git/attribute/main_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "context" + "fmt" + "os" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +func testRun(m *testing.M) error { + gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home") + if err != nil { + return fmt.Errorf("unable to create temp dir: %w", err) + } + defer util.RemoveAll(gitHomePath) + setting.Git.HomePath = gitHomePath + + if err = git.InitFull(context.Background()); err != nil { + return fmt.Errorf("failed to call Init: %w", err) + } + + exitCode := m.Run() + if exitCode != 0 { + return fmt.Errorf("run test failed, ExitCode=%d", exitCode) + } + return nil +} + +func TestMain(m *testing.M) { + if err := testRun(m); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err) + os.Exit(1) + } +} diff --git a/modules/git/command.go b/modules/git/command.go index f449f1ff0e..eaaa4969d0 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -80,6 +80,13 @@ func (c *Command) LogString() string { return strings.Join(a, " ") } +func (c *Command) ProcessState() string { + if c.cmd == nil { + return "" + } + return c.cmd.ProcessState.String() +} + // NewCommand creates and returns a new Git Command based on given command and arguments. // Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. func NewCommand(args ...internal.CmdArg) *Command { diff --git a/modules/git/git.go b/modules/git/git.go index 2b593910a2..a2ffd6d289 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -30,6 +30,7 @@ type Features struct { SupportProcReceive bool // >= 2.29 SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ SupportedObjectFormats []ObjectFormat // sha1, sha256 + SupportCheckAttrOnBare bool // >= 2.40 } var ( @@ -77,6 +78,7 @@ func loadGitVersionFeatures() (*Features, error) { if features.SupportHashSha256 { features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat) } + features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40") return features, nil } diff --git a/modules/git/repo_language_stats.go b/modules/git/languagestats/language_stats.go similarity index 59% rename from modules/git/repo_language_stats.go rename to modules/git/languagestats/language_stats.go index 8551ea9d24..a71284c3e4 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/languagestats/language_stats.go @@ -1,13 +1,15 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package languagestats import ( + "context" "strings" "unicode" - "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" ) const ( @@ -49,19 +51,15 @@ func mergeLanguageStats(stats map[string]int64) map[string]int64 { return res } -func TryReadLanguageAttribute(attrs map[string]string) optional.Option[string] { - language := AttributeToString(attrs, AttributeLinguistLanguage) - if language.Value() == "" { - language = AttributeToString(attrs, AttributeGitlabLanguage) - if language.Has() { - raw := language.Value() - // gitlab-language may have additional parameters after the language - // ignore them and just use the main language - // https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type - if idx := strings.IndexByte(raw, '?'); idx >= 0 { - language = optional.Some(raw[:idx]) - } - } +// GetFileLanguage tries to get the (linguist) language of the file content +func GetFileLanguage(ctx context.Context, gitRepo *git.Repository, treeish, treePath string) (string, error) { + attributesMap, err := attribute.CheckAttributes(ctx, gitRepo, treeish, attribute.CheckAttributeOpts{ + Attributes: []string{attribute.LinguistLanguage, attribute.GitlabLanguage}, + Filenames: []string{treePath}, + }) + if err != nil { + return "", err } - return language + + return attributesMap[treePath].GetLanguage().Value(), nil } diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/languagestats/language_stats_gogit.go similarity index 73% rename from modules/git/repo_language_stats_gogit.go rename to modules/git/languagestats/language_stats_gogit.go index a34c03c781..418c05b157 100644 --- a/modules/git/repo_language_stats_gogit.go +++ b/modules/git/languagestats/language_stats_gogit.go @@ -3,13 +3,15 @@ //go:build gogit -package git +package languagestats import ( "bytes" "io" "code.gitea.io/gitea/modules/analyze" + git_module "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/optional" "github.com/go-enry/go-enry/v2" @@ -19,7 +21,7 @@ import ( ) // GetLanguageStats calculates language stats for git repository at specified commit -func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) { +func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string]int64, error) { r, err := git.PlainOpen(repo.Path) if err != nil { return nil, err @@ -40,8 +42,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } - checker, deferable := repo.CheckAttributeReader(commitID) - defer deferable() + checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes) + if err != nil { + return nil, err + } + defer checker.Close() // sizes contains the current calculated size of all files by language sizes := make(map[string]int64) @@ -62,43 +67,41 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err isDocumentation := optional.None[bool]() isDetectable := optional.None[bool]() - if checker != nil { - attrs, err := checker.CheckPath(f.Name) - if err == nil { - isVendored = AttributeToBool(attrs, AttributeLinguistVendored) - if isVendored.ValueOrDefault(false) { - return nil + attrs, err := checker.CheckPath(f.Name) + if err == nil { + isVendored = attrs.GetVendored() + if isVendored.ValueOrDefault(false) { + return nil + } + + isGenerated = attrs.GetGenerated() + if isGenerated.ValueOrDefault(false) { + return nil + } + + isDocumentation = attrs.GetDocumentation() + if isDocumentation.ValueOrDefault(false) { + return nil + } + + isDetectable = attrs.GetDetectable() + if !isDetectable.ValueOrDefault(true) { + return nil + } + + hasLanguage := attrs.GetLanguage() + if hasLanguage.Value() != "" { + language := hasLanguage.Value() + + // group languages, such as Pug -> HTML; SCSS -> CSS + group := enry.GetLanguageGroup(language) + if len(group) != 0 { + language = group } - isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated) - if isGenerated.ValueOrDefault(false) { - return nil - } - - isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation) - if isDocumentation.ValueOrDefault(false) { - return nil - } - - isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable) - if !isDetectable.ValueOrDefault(true) { - return nil - } - - hasLanguage := TryReadLanguageAttribute(attrs) - if hasLanguage.Value() != "" { - language := hasLanguage.Value() - - // group languages, such as Pug -> HTML; SCSS -> CSS - group := enry.GetLanguageGroup(language) - if len(group) != 0 { - language = group - } - - // this language will always be added to the size - sizes[language] += f.Size - return nil - } + // this language will always be added to the size + sizes[language] += f.Size + return nil } } diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/languagestats/language_stats_nogogit.go similarity index 73% rename from modules/git/repo_language_stats_nogogit.go rename to modules/git/languagestats/language_stats_nogogit.go index de7707bd6c..34797263a6 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/languagestats/language_stats_nogogit.go @@ -3,13 +3,15 @@ //go:build !gogit -package git +package languagestats import ( "bytes" "io" "code.gitea.io/gitea/modules/analyze" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -17,7 +19,7 @@ import ( ) // GetLanguageStats calculates language stats for git repository at specified commit -func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) { +func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) { // We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary. // so let's create a batch stdin and stdout batchStdinWriter, batchReader, cancel, err := repo.CatFileBatch(repo.Ctx) @@ -34,19 +36,19 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if err := writeID(commitID); err != nil { return nil, err } - shaBytes, typ, size, err := ReadBatchLine(batchReader) + shaBytes, typ, size, err := git.ReadBatchLine(batchReader) if typ != "commit" { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) - return nil, ErrNotExist{commitID, ""} + return nil, git.ErrNotExist{ID: commitID} } - sha, err := NewIDFromString(string(shaBytes)) + sha, err := git.NewIDFromString(string(shaBytes)) if err != nil { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) - return nil, ErrNotExist{commitID, ""} + return nil, git.ErrNotExist{ID: commitID} } - commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size)) + commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, size)) if err != nil { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, err @@ -62,8 +64,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } - checker, deferable := repo.CheckAttributeReader(commitID) - defer deferable() + checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes) + if err != nil { + return nil, err + } + defer checker.Close() contentBuf := bytes.Buffer{} var content []byte @@ -96,43 +101,36 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err isDocumentation := optional.None[bool]() isDetectable := optional.None[bool]() - if checker != nil { - attrs, err := checker.CheckPath(f.Name()) - if err == nil { - isVendored = AttributeToBool(attrs, AttributeLinguistVendored) - if isVendored.ValueOrDefault(false) { - continue + attrs, err := checker.CheckPath(f.Name()) + if err == nil { + if isVendored = attrs.GetVendored(); isVendored.ValueOrDefault(false) { + continue + } + + if isGenerated = attrs.GetGenerated(); isGenerated.ValueOrDefault(false) { + continue + } + + if isDocumentation = attrs.GetDocumentation(); isDocumentation.ValueOrDefault(false) { + continue + } + + if isDetectable = attrs.GetDetectable(); !isDetectable.ValueOrDefault(true) { + continue + } + + if hasLanguage := attrs.GetLanguage(); hasLanguage.Value() != "" { + language := hasLanguage.Value() + + // group languages, such as Pug -> HTML; SCSS -> CSS + group := enry.GetLanguageGroup(language) + if len(group) != 0 { + language = group } - isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated) - if isGenerated.ValueOrDefault(false) { - continue - } - - isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation) - if isDocumentation.ValueOrDefault(false) { - continue - } - - isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable) - if !isDetectable.ValueOrDefault(true) { - continue - } - - hasLanguage := TryReadLanguageAttribute(attrs) - if hasLanguage.Value() != "" { - language := hasLanguage.Value() - - // group languages, such as Pug -> HTML; SCSS -> CSS - group := enry.GetLanguageGroup(language) - if len(group) != 0 { - language = group - } - - // this language will always be added to the size - sizes[language] += f.Size() - continue - } + // this language will always be added to the size + sizes[language] += f.Size() + continue } } @@ -149,7 +147,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if err := writeID(f.ID.String()); err != nil { return nil, err } - _, _, size, err := ReadBatchLine(batchReader) + _, _, size, err := git.ReadBatchLine(batchReader) if err != nil { log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err) return nil, err @@ -167,7 +165,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } content = contentBuf.Bytes() - if err := DiscardFull(batchReader, discard); err != nil { + if err := git.DiscardFull(batchReader, discard); err != nil { return nil, err } } diff --git a/modules/git/repo_language_stats_test.go b/modules/git/languagestats/language_stats_test.go similarity index 75% rename from modules/git/repo_language_stats_test.go rename to modules/git/languagestats/language_stats_test.go index 12ce958c6e..b908ae6413 100644 --- a/modules/git/repo_language_stats_test.go +++ b/modules/git/languagestats/language_stats_test.go @@ -3,12 +3,12 @@ //go:build !gogit -package git +package languagestats import ( - "path/filepath" "testing" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -17,13 +17,12 @@ import ( func TestRepository_GetLanguageStats(t *testing.T) { setting.AppDataPath = t.TempDir() - repoPath := filepath.Join(testReposDir, "language_stats_repo") - gitRepo, err := openRepositoryWithDefaultContext(repoPath) + repoPath := "../tests/repos/language_stats_repo" + gitRepo, err := git.OpenRepository(t.Context(), repoPath) require.NoError(t, err) - defer gitRepo.Close() - stats, err := gitRepo.GetLanguageStats("8fee858da5796dfb37704761701bb8e800ad9ef3") + stats, err := GetLanguageStats(gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3") require.NoError(t, err) assert.Equal(t, map[string]int64{ diff --git a/modules/git/languagestats/main_test.go b/modules/git/languagestats/main_test.go new file mode 100644 index 0000000000..707d268c81 --- /dev/null +++ b/modules/git/languagestats/main_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package languagestats + +import ( + "context" + "fmt" + "os" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +func testRun(m *testing.M) error { + gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home") + if err != nil { + return fmt.Errorf("unable to create temp dir: %w", err) + } + defer util.RemoveAll(gitHomePath) + setting.Git.HomePath = gitHomePath + + if err = git.InitFull(context.Background()); err != nil { + return fmt.Errorf("failed to call Init: %w", err) + } + + exitCode := m.Run() + if exitCode != 0 { + return fmt.Errorf("run test failed, ExitCode=%d", exitCode) + } + return nil +} + +func TestMain(m *testing.M) { + if err := testRun(m); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err) + os.Exit(1) + } +} diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go deleted file mode 100644 index fde42d4730..0000000000 --- a/modules/git/repo_attribute.go +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "time" - - "code.gitea.io/gitea/modules/log" -) - -// CheckAttributeOpts represents the possible options to CheckAttribute -type CheckAttributeOpts struct { - CachedOnly bool - AllAttributes bool - Attributes []string - Filenames []string - IndexFile string - WorkTree string -} - -// CheckAttribute return the Blame object of file -func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { - env := []string{} - - if len(opts.IndexFile) > 0 { - env = append(env, "GIT_INDEX_FILE="+opts.IndexFile) - } - if len(opts.WorkTree) > 0 { - env = append(env, "GIT_WORK_TREE="+opts.WorkTree) - } - - if len(env) > 0 { - env = append(os.Environ(), env...) - } - - stdOut := new(bytes.Buffer) - stdErr := new(bytes.Buffer) - - cmd := NewCommand("check-attr", "-z") - - if opts.AllAttributes { - cmd.AddArguments("-a") - } else { - for _, attribute := range opts.Attributes { - if attribute != "" { - cmd.AddDynamicArguments(attribute) - } - } - } - - if opts.CachedOnly { - cmd.AddArguments("--cached") - } - - cmd.AddDashesAndList(opts.Filenames...) - - if err := cmd.Run(repo.Ctx, &RunOpts{ - Env: env, - Dir: repo.Path, - Stdout: stdOut, - Stderr: stdErr, - }); err != nil { - return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) - } - - // FIXME: This is incorrect on versions < 1.8.5 - fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) - - if len(fields)%3 != 1 { - return nil, errors.New("wrong number of fields in return from check-attr") - } - - name2attribute2info := make(map[string]map[string]string) - - for i := 0; i < (len(fields) / 3); i++ { - filename := string(fields[3*i]) - attribute := string(fields[3*i+1]) - info := string(fields[3*i+2]) - attribute2info := name2attribute2info[filename] - if attribute2info == nil { - attribute2info = make(map[string]string) - } - attribute2info[attribute] = info - name2attribute2info[filename] = attribute2info - } - - return name2attribute2info, nil -} - -// CheckAttributeReader provides a reader for check-attribute content that can be long running -type CheckAttributeReader struct { - // params - Attributes []string - Repo *Repository - IndexFile string - WorkTree string - - stdinReader io.ReadCloser - stdinWriter *os.File - stdOut *nulSeparatedAttributeWriter - cmd *Command - env []string - ctx context.Context - cancel context.CancelFunc -} - -// Init initializes the CheckAttributeReader -func (c *CheckAttributeReader) Init(ctx context.Context) error { - if len(c.Attributes) == 0 { - lw := new(nulSeparatedAttributeWriter) - lw.attributes = make(chan attributeTriple) - lw.closed = make(chan struct{}) - - c.stdOut = lw - c.stdOut.Close() - return errors.New("no provided Attributes to check") - } - - c.ctx, c.cancel = context.WithCancel(ctx) - c.cmd = NewCommand("check-attr", "--stdin", "-z") - - if len(c.IndexFile) > 0 { - c.cmd.AddArguments("--cached") - c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile) - } - - if len(c.WorkTree) > 0 { - c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree) - } - - c.env = append(c.env, "GIT_FLUSH=1") - - c.cmd.AddDynamicArguments(c.Attributes...) - - var err error - - c.stdinReader, c.stdinWriter, err = os.Pipe() - if err != nil { - c.cancel() - return err - } - - lw := new(nulSeparatedAttributeWriter) - lw.attributes = make(chan attributeTriple, 5) - lw.closed = make(chan struct{}) - c.stdOut = lw - return nil -} - -func (c *CheckAttributeReader) Run() error { - defer func() { - _ = c.stdinReader.Close() - _ = c.stdOut.Close() - }() - stdErr := new(bytes.Buffer) - err := c.cmd.Run(c.ctx, &RunOpts{ - Env: c.env, - Dir: c.Repo.Path, - Stdin: c.stdinReader, - Stdout: c.stdOut, - Stderr: stdErr, - }) - if err != nil && !IsErrCanceledOrKilled(err) { - return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) - } - return nil -} - -// CheckPath check attr for given path -func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) { - defer func() { - if err != nil && err != c.ctx.Err() { - log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.Repo.Path), err) - } - }() - - select { - case <-c.ctx.Done(): - return nil, c.ctx.Err() - default: - } - - if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil { - defer c.Close() - return nil, err - } - - reportTimeout := func() error { - stdOutClosed := false - select { - case <-c.stdOut.closed: - stdOutClosed = true - default: - } - debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.Repo.Path)) - debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed) - if c.cmd.cmd != nil { - debugMsg += fmt.Sprintf(", process state: %q", c.cmd.cmd.ProcessState.String()) - } - _ = c.Close() - return fmt.Errorf("CheckPath timeout: %s", debugMsg) - } - - rs = make(map[string]string) - for range c.Attributes { - select { - case <-time.After(5 * time.Second): - // There is a strange "hang" problem in gitdiff.GetDiff -> CheckPath - // So add a timeout here to mitigate the problem, and output more logs for debug purpose - // In real world, if CheckPath runs long than seconds, it blocks the end user's operation, - // and at the moment the CheckPath result is not so important, so we can just ignore it. - return nil, reportTimeout() - case attr, ok := <-c.stdOut.ReadAttribute(): - if !ok { - return nil, c.ctx.Err() - } - rs[attr.Attribute] = attr.Value - case <-c.ctx.Done(): - return nil, c.ctx.Err() - } - } - return rs, nil -} - -func (c *CheckAttributeReader) Close() error { - c.cancel() - err := c.stdinWriter.Close() - return err -} - -type attributeTriple struct { - Filename string - Attribute string - Value string -} - -type nulSeparatedAttributeWriter struct { - tmp []byte - attributes chan attributeTriple - closed chan struct{} - working attributeTriple - pos int -} - -func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { - l, read := len(p), 0 - - nulIdx := bytes.IndexByte(p, '\x00') - for nulIdx >= 0 { - wr.tmp = append(wr.tmp, p[:nulIdx]...) - switch wr.pos { - case 0: - wr.working = attributeTriple{ - Filename: string(wr.tmp), - } - case 1: - wr.working.Attribute = string(wr.tmp) - case 2: - wr.working.Value = string(wr.tmp) - } - wr.tmp = wr.tmp[:0] - wr.pos++ - if wr.pos > 2 { - wr.attributes <- wr.working - wr.pos = 0 - } - read += nulIdx + 1 - if l > read { - p = p[nulIdx+1:] - nulIdx = bytes.IndexByte(p, '\x00') - } else { - return l, nil - } - } - wr.tmp = append(wr.tmp, p...) - return l, nil -} - -func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { - return wr.attributes -} - -func (wr *nulSeparatedAttributeWriter) Close() error { - select { - case <-wr.closed: - return nil - default: - } - close(wr.attributes) - close(wr.closed) - return nil -} - -// CheckAttributeReader creates a check attribute reader for the current repository and provided commit ID -func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) { - indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID) - if err != nil { - return nil, func() {} - } - - checker := &CheckAttributeReader{ - Attributes: []string{ - AttributeLinguistVendored, - AttributeLinguistGenerated, - AttributeLinguistDocumentation, - AttributeLinguistDetectable, - AttributeLinguistLanguage, - AttributeGitlabLanguage, - }, - Repo: repo, - IndexFile: indexFilename, - WorkTree: worktree, - } - ctx, cancel := context.WithCancel(repo.Ctx) - if err := checker.Init(ctx); err != nil { - log.Error("Unable to open attribute checker for commit %s, error: %v", commitID, err) - } else { - go func() { - err := checker.Run() - if err != nil && !IsErrCanceledOrKilled(err) { - log.Error("Attribute checker for commit %s exits with error: %v", commitID, err) - } - cancel() - }() - } - deferrable := func() { - _ = checker.Close() - cancel() - deleteTemporaryFile() - } - - return checker, deferrable -} diff --git a/modules/git/tests/repos/language_stats_repo/config b/modules/git/tests/repos/language_stats_repo/config index 515f483629..a4ef456cbc 100644 --- a/modules/git/tests/repos/language_stats_repo/config +++ b/modules/git/tests/repos/language_stats_repo/config @@ -1,5 +1,5 @@ [core] repositoryformatversion = 0 filemode = true - bare = false + bare = true logallrefupdates = true diff --git a/modules/git/tests/repos/repo3_notes/config b/modules/git/tests/repos/repo3_notes/config index d545cdabdb..5ed22e23d1 100644 --- a/modules/git/tests/repos/repo3_notes/config +++ b/modules/git/tests/repos/repo3_notes/config @@ -1,7 +1,7 @@ [core] repositoryformatversion = 0 filemode = false - bare = false + bare = true logallrefupdates = true symlinks = false ignorecase = true diff --git a/modules/git/tests/repos/repo4_commitsbetween/config b/modules/git/tests/repos/repo4_commitsbetween/config index d545cdabdb..5ed22e23d1 100644 --- a/modules/git/tests/repos/repo4_commitsbetween/config +++ b/modules/git/tests/repos/repo4_commitsbetween/config @@ -1,7 +1,7 @@ [core] repositoryformatversion = 0 filemode = false - bare = false + bare = true logallrefupdates = true symlinks = false ignorecase = true diff --git a/modules/indexer/stats/db.go b/modules/indexer/stats/db.go index 067a6f609b..199d493e97 100644 --- a/modules/indexer/stats/db.go +++ b/modules/indexer/stats/db.go @@ -8,6 +8,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/languagestats" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" @@ -62,7 +63,7 @@ func (db *DBIndexer) Index(id int64) error { } // Calculate and save language statistics to database - stats, err := gitRepo.GetLanguageStats(commitID) + stats, err := languagestats.GetLanguageStats(gitRepo, commitID) if err != nil { if !setting.IsInTesting { log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.FullName(), err) diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 85f5b08a42..1540a97f4c 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -113,6 +113,7 @@ copy_type_unsupported=无法复制此类型的文件内容 write=撰写 preview=预览 loading=正在加载... +files=文件 error=错误 error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。 @@ -169,6 +170,10 @@ search=搜索... type_tooltip=搜索类型 fuzzy=模糊 fuzzy_tooltip=包含近似匹配搜索词的结果 +words=词 +words_tooltip=仅包含匹配搜索词的结果 +regexp=正则表达式 +regexp_tooltip=仅包含匹配正则表达式搜索词的结果 exact=精确 exact_tooltip=仅包含精确匹配搜索词的结果 repo_kind=搜索仓库... @@ -385,6 +390,12 @@ show_only_public=只显示公开的 issues.in_your_repos=在您的仓库中 +guide_title=无活动 +guide_desc=您目前没有关注任何仓库或用户,所以没有要显示的内容。 您可以从下面的链接中探索感兴趣的仓库或用户。 +explore_repos=探索仓库 +explore_users=探索用户 +empty_org=目前还没有组织。 +empty_repo=目前还没有仓库。 [explore] repos=仓库 @@ -446,6 +457,7 @@ oauth_signup_submit=完成账号 oauth_signin_tab=绑定到现有帐号 oauth_signin_title=登录以授权绑定帐户 oauth_signin_submit=绑定账号 +oauth.signin.error.general=处理授权请求时出错:%s。如果此错误仍然存在,请与站点管理员联系。 oauth.signin.error.access_denied=授权请求被拒绝。 oauth.signin.error.temporarily_unavailable=授权失败,因为认证服务器暂时不可用。请稍后再试。 oauth_callback_unable_auto_reg=自动注册已启用,但OAuth2 提供商 %[1]s 返回缺失的字段:%[2]s,无法自动创建帐户,请创建或链接到一个帐户,或联系站点管理员。 @@ -718,6 +730,8 @@ public_profile=公开信息 biography_placeholder=告诉我们一点您自己! (您可以使用Markdown) location_placeholder=与他人分享你的大概位置 profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作 +password_username_disabled=您不被允许更改你的用户名。更多详情请联系您的系统管理员。 +password_full_name_disabled=您不被允许更改你的全名。请联系您的站点管理员了解更多详情。 full_name=自定义名称 website=个人网站 location=所在地区 @@ -912,6 +926,7 @@ permission_not_set=未设置 permission_no_access=无访问权限 permission_read=可读 permission_write=读写 +permission_anonymous_read=匿名读 access_token_desc=所选令牌权限仅限于对应的 <a %s>API</a> 路由的授权。阅读 <a %s>文档</a> 以获取更多信息。 at_least_one_permission=你需要选择至少一个权限才能创建令牌 permissions_list=权限: @@ -1014,6 +1029,9 @@ new_repo_helper=代码仓库包含了所有的项目文件,包括版本历史 owner=拥有者 owner_helper=由于最大仓库数量限制,一些组织可能不会显示在下拉列表中。 repo_name=仓库名称 +repo_name_profile_public_hint=.profile 是一个特殊的存储库,您可以使用它将 README.md 添加到您的公共组织资料中,任何人都可以看到。请确保它是公开的,并使用个人资料目录中的 README 对其进行初始化以开始使用。 +repo_name_profile_private_hint=.profile-private 是一个特殊的存储库,您可以使用它向您的组织成员个人资料添加 README.md,仅对组织成员可见。请确保它是私有的,并使用个人资料目录中的 README 对其进行初始化以开始使用。 +repo_name_helper=理想的仓库名称应由简短、有意义和独特的关键词组成。".profile" 和 ".profile-private" 可用于为用户/组织添加 README.md。 repo_size=仓库大小 template=模板 template_select=选择模板 @@ -1110,6 +1128,7 @@ blame.ignore_revs=忽略 <a href="%s">.git-blame-ignore-revs</a> 的修订。点 blame.ignore_revs.failed=忽略 <a href="%s">.git-blame-ignore-revs</a> 版本失败。 user_search_tooltip=最多显示30名用户 +tree_path_not_found=%[2]s 中不存在路径 %[1]s transfer.accept=接受转移 transfer.accept_desc=`转移到 "%s"` @@ -1120,6 +1139,7 @@ transfer.no_permission_to_reject=您没有权限拒绝此转让。 desc.private=私有库 desc.public=公开 +desc.public_access=公开访问 desc.template=模板 desc.internal=内部 desc.archived=已存档 @@ -1227,6 +1247,7 @@ create_new_repo_command=从命令行创建一个新的仓库 push_exist_repo=从命令行推送已经创建的仓库 empty_message=这个家伙很懒,什么都没有推送。 broken_message=无法读取此仓库下的 Git 数据。 联系此实例的管理员或删除此仓库。 +no_branch=该仓库没有任何分支。 code=代码 code.desc=查看源码、文件、提交和分支。 @@ -1339,6 +1360,8 @@ editor.new_branch_name_desc=新的分支名称... editor.cancel=取消 editor.filename_cannot_be_empty=文件名不能为空。 editor.filename_is_invalid=文件名 %s 无效 +editor.commit_email=提交邮箱地址 +editor.invalid_commit_email=提交的邮箱地址无效。 editor.branch_does_not_exist=此仓库中不存在名为 %s 的分支。 editor.branch_already_exists=此仓库已存在名为 %s 的分支。 editor.directory_is_a_file=%s 已经作为文件名在此仓库中存在。 @@ -1387,6 +1410,7 @@ commits.signed_by_untrusted_user_unmatched=由与提交者不匹配的未授信 commits.gpg_key_id=GPG 密钥 ID commits.ssh_key_fingerprint=SSH 密钥指纹 commits.view_path=在历史记录中的此处查看 +commits.view_file_diff=查看提交中的文件更改 commit.operations=操作 commit.revert=还原 @@ -1398,7 +1422,7 @@ commit.cherry-pick-content=选择 cherry-pick 的目标分支: commitstatus.error=错误 commitstatus.failure=失败 -commitstatus.pending=待定 +commitstatus.pending=队列 commitstatus.success=成功 ext_issues=访问外部工单 @@ -1433,7 +1457,7 @@ projects.column.set_default=设为默认 projects.column.set_default_desc=设置此列为未分类问题和合并请求的默认值 projects.column.delete=删除列 projects.column.deletion_desc=删除项目列会将所有相关问题移到“未分类”。是否继续? -projects.column.color=彩色 +projects.column.color=颜色 projects.open=开启 projects.close=关闭 projects.column.assigned_to=指派给 @@ -1447,6 +1471,8 @@ issues.filter_milestones=筛选里程碑 issues.filter_projects=筛选项目 issues.filter_labels=筛选标签 issues.filter_reviewers=筛选审核者 +issues.filter_no_results=没有结果 +issues.filter_no_results_placeholder=请尝试调整您的搜索过滤器。 issues.new=创建工单 issues.new.title_empty=标题不能为空 issues.new.labels=标签 @@ -1520,8 +1546,10 @@ issues.filter_milestone_open=进行中的里程碑 issues.filter_milestone_closed=已关闭的里程碑 issues.filter_project=项目 issues.filter_project_all=所有项目 -issues.filter_project_none=暂无项目 +issues.filter_project_none=未加项目 issues.filter_assignee=指派人筛选 +issues.filter_assignee_no_assignee=未指派给任何人 +issues.filter_assignee_any_assignee=已有指派 issues.filter_poster=作者 issues.filter_user_placeholder=搜索用户 issues.filter_user_no_select=所有用户 @@ -1595,9 +1623,9 @@ issues.ref_reopened_from=`<a href="%[3]s">重新打开这个工单 %[4]s</a> <a issues.ref_from=`来自 %[1]s` issues.author=作者 issues.author_helper=此用户是作者。 -issues.role.owner=管理员 +issues.role.owner=所有者 issues.role.owner_helper=该用户是该仓库的所有者。 -issues.role.member=普通成员 +issues.role.member=成员 issues.role.member_helper=该用户是拥有该仓库的组织成员。 issues.role.collaborator=协作者 issues.role.collaborator_helper=该用户已被邀请在仓库上进行协作。 @@ -1676,11 +1704,13 @@ issues.timetracker_timer_manually_add=添加时间 issues.time_estimate_set=设置预计时间 issues.time_estimate_display=预计: %s +issues.change_time_estimate_at=预估时间已修改为 <b>%[1]s</b> %[2]s issues.remove_time_estimate_at=删除预计时间 %s issues.time_estimate_invalid=预计时间格式无效 issues.start_tracking_history=`开始工作 %s` issues.tracker_auto_close=当此工单关闭时,自动停止计时器 issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!` +issues.stop_tracking=停止计时器 issues.cancel_tracking_history=`取消时间跟踪 %s` issues.del_time=删除此时间跟踪日志 issues.del_time_history=`已删除时间 %s` @@ -1912,6 +1942,7 @@ pulls.outdated_with_base_branch=此分支相比基础分支已过期 pulls.close=关闭合并请求 pulls.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此合并请求 ` pulls.reopened_at=`重新打开此合并请求 <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.cmd_instruction_hint=查看命令行提示 pulls.cmd_instruction_checkout_title=检出 pulls.cmd_instruction_checkout_desc=从你的仓库中检出一个新的分支并测试变更。 pulls.cmd_instruction_merge_title=合并 @@ -2099,6 +2130,7 @@ contributors.contribution_type.deletions=删除 settings=设置 settings.desc=设置是你可以管理仓库设置的地方 settings.options=仓库 +settings.public_access=公开访问 settings.collaboration=协作者 settings.collaboration.admin=管理员 settings.collaboration.write=可写权限 @@ -2312,6 +2344,8 @@ settings.event_fork=派生 settings.event_fork_desc=仓库被派生。 settings.event_wiki=百科 settings.event_wiki_desc=创建、重命名、编辑或删除了百科页面。 +settings.event_statuses=状态 +settings.event_statuses_desc=已从 API 更新提交状态。 settings.event_release=版本发布 settings.event_release_desc=发布、更新或删除版本时。 settings.event_push=推送 @@ -2349,6 +2383,8 @@ settings.event_pull_request_review_request=发起合并请求评审 settings.event_pull_request_review_request_desc=合并请求评审已请求或已取消 settings.event_pull_request_approvals=合并请求批准 settings.event_pull_request_merge=合并请求合并 +settings.event_header_workflow=工作流程事件 +settings.event_workflow_job=工作流任务 settings.event_package=软件包 settings.event_package_desc=软件包已在仓库中被创建或删除。 settings.branch_filter=分支过滤 @@ -2611,6 +2647,9 @@ diff.image.overlay=叠加 diff.has_escaped=这一行有隐藏的 Unicode 字符 diff.show_file_tree=显示文件树 diff.hide_file_tree=隐藏文件树 +diff.submodule_added=子模块 %[1]s 已添加到 %[2]s +diff.submodule_deleted=子模块 %[1]s 已从 %[2]s 中删除 +diff.submodule_updated=子模块 %[1]s 已更新:%[2]s releases.desc=跟踪项目版本和下载。 release.releases=版本发布 @@ -2681,6 +2720,7 @@ branch.restore_success=分支 "%s"已还原。 branch.restore_failed=还原分支 "%s"失败。 branch.protected_deletion_failed=不能删除受保护的分支 "%s"。 branch.default_deletion_failed=不能删除默认分支"%s"。 +branch.default_branch_not_exist=默认分支 %s 不存在。 branch.restore=`还原分支 "%s"` branch.download=`下载分支 "%s"` branch.rename=`重命名分支 "%s"` @@ -2695,6 +2735,8 @@ branch.create_branch_operation=创建分支 branch.new_branch=创建新分支 branch.new_branch_from=基于"%s"创建新分支 branch.renamed=分支 %s 被重命名为 %s。 +branch.rename_default_or_protected_branch_error=只有管理员能重命名默认分支和受保护的分支。 +branch.rename_protected_branch_failed=此分支受到 glob 语法规则的保护。 tag.create_tag=创建标签 %s tag.create_tag_operation=创建标签 @@ -2849,7 +2891,15 @@ teams.invite.title=您已被邀请加入组织 <strong>%s</strong> 中的团队 teams.invite.by=邀请人 %s teams.invite.description=请点击下面的按钮加入团队。 +view_as_role=以 %s 身份查看 +view_as_public_hint=您正在以公开用户的身份查看 README +view_as_member_hint=您正在以组织成员的身份查看 README +worktime=工作时间 +worktime.date_range_start=起始日期 +worktime.date_range_end=结束日期 +worktime.query=查询 +worktime.time=时间 [admin] maintenance=维护 @@ -3343,6 +3393,7 @@ monitor.previous=上次执行时间 monitor.execute_times=执行次数 monitor.process=运行中进程 monitor.stacktrace=调用栈踪迹 +monitor.trace=追踪 monitor.performance_logs=性能日志 monitor.processes_count=%d 个进程 monitor.download_diagnosis_report=下载诊断报告 @@ -3518,10 +3569,11 @@ versions=版本 versions.view_all=查看全部 dependency.id=ID dependency.version=版本 +search_in_external_registry=在 %s 中搜索 alpine.registry=通过在您的 <code>/etc/apk/repositories</code> 文件中添加 URL 来设置此注册中心: alpine.registry.key=下载注册中心公开的 RSA 密钥到 <code>/etc/apk/keys/</code> 文件夹来验证索引签名: alpine.registry.info=从下面的列表中选择 $branch 和 $repository。 -alpine.install=要安装包,请运行以下命令: +alpine.install=要安装软件包,请运行以下命令: alpine.repository=仓库信息 alpine.repository.branches=分支 alpine.repository.repositories=仓库 @@ -3534,7 +3586,7 @@ arch.repository.architectures=架构 cargo.registry=在 Cargo 配置文件中设置此注册中心(例如:<code>~/.cargo/config.toml</code>): cargo.install=要使用 Cargo 安装软件包,请运行以下命令: chef.registry=在您的 <code>~/.chef/config.rb</code> 文件中设置此注册中心: -chef.install=要安装包,请运行以下命令: +chef.install=要安装软件包,请运行以下命令: composer.registry=在您的 <code>~/.composer/config.json</code> 文件中设置此注册中心: composer.install=要使用 Composer 安装软件包,请运行以下命令: composer.dependencies=依赖 @@ -3548,16 +3600,17 @@ container.details.type=镜像类型 container.details.platform=平台 container.pull=从命令行拉取镜像: container.images=镜像 +container.digest=摘要 container.multi_arch=OS / Arch container.layers=镜像层 container.labels=标签 container.labels.key=键 container.labels.value=值 cran.registry=在您的 <code>Rprofile.site</code> 文件中设置此注册中心: -cran.install=要安装包,请运行以下命令: +cran.install=要安装软件包,请运行以下命令: debian.registry=从命令行设置此注册中心: debian.registry.info=从下面的列表中选择 $distribution 和 $component。 -debian.install=要安装包,请运行以下命令: +debian.install=要安装软件包,请运行以下命令: debian.repository=仓库信息 debian.repository.distributions=发行版 debian.repository.components=组件 @@ -3588,7 +3641,7 @@ pypi.install=要使用 pip 安装软件包,请运行以下命令: rpm.registry=从命令行设置此注册中心: rpm.distros.redhat=在基于 RedHat 的发行版 rpm.distros.suse=在基于 SUSE 的发行版 -rpm.install=要安装包,请运行以下命令: +rpm.install=要安装软件包,请运行以下命令: rpm.repository=仓库信息 rpm.repository.architectures=架构 rpm.repository.multiple_groups=此软件包可在多个组中使用。 @@ -3654,6 +3707,7 @@ creation=添加密钥 creation.description=组织描述 creation.name_placeholder=不区分大小写,字母数字或下划线不能以GITEA_ 或 GITHUB_ 开头。 creation.value_placeholder=输入任何内容,开头和结尾的空白都会被省略 +creation.description_placeholder=输入简短描述(可选)。 creation.success=您的密钥 '%s' 添加成功。 creation.failed=添加密钥失败。 deletion=删除密钥 @@ -3684,7 +3738,7 @@ runners.status=状态 runners.id=ID runners.name=名称 runners.owner_type=类型 -runners.description=组织描述 +runners.description=描述 runners.labels=标签 runners.last_online=上次在线时间 runners.runner_title=Runner @@ -3707,10 +3761,11 @@ runners.delete_runner_notice=如果一个任务正在运行在此运行器上, runners.none=无可用的 Runner runners.status.unspecified=未知 runners.status.idle=空闲 -runners.status.active=激活 +runners.status.active=启用 runners.status.offline=离线 runners.version=版本 runners.reset_registration_token=重置注册令牌 +runners.reset_registration_token_confirm=是否吊销当前令牌并生成一个新令牌? runners.reset_registration_token_success=成功重置运行器注册令牌 runs.all_workflows=所有工作流 @@ -3743,6 +3798,7 @@ workflow.not_found=工作流 %s 未找到。 workflow.run_success=工作流 %s 已成功运行。 workflow.from_ref=使用工作流从 workflow.has_workflow_dispatch=此 Workflow 有一个 Workflow_dispatch 事件触发器。 +workflow.has_no_workflow_dispatch=工作流 %s 没有 workflow_dispatch 事件的触发器。 need_approval_desc=该工作流由派生仓库的合并请求所触发,需要批准方可运行。 @@ -3762,6 +3818,8 @@ variables.creation.success=变量 “%s” 添加成功。 variables.update.failed=编辑变量失败。 variables.update.success=该变量已被编辑。 +logs.always_auto_scroll=总是自动滚动日志 +logs.always_expand_running=总是展开运行日志 [projects] deleted.display_name=已删除项目 diff --git a/routers/web/org/block.go b/routers/web/org/block.go index aeb4bd51a8..60f722dd39 100644 --- a/routers/web/org/block.go +++ b/routers/web/org/block.go @@ -20,6 +20,11 @@ func BlockedUsers(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsBlockedUsers"] = true + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } + shared_user.BlockedUsers(ctx, ctx.ContextUser) if ctx.Written() { return @@ -29,6 +34,11 @@ func BlockedUsers(ctx *context.Context) { } func BlockedUsersPost(ctx *context.Context) { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } + shared_user.BlockedUsersPost(ctx, ctx.ContextUser) if ctx.Written() { return diff --git a/routers/web/org/home.go b/routers/web/org/home.go index e3c2dcf0bd..8981af1691 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -86,12 +86,6 @@ func home(ctx *context.Context, viewRepositories bool) { private := ctx.FormOptionalBool("private") ctx.Data["IsPrivate"] = private - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } - opts := &organization.FindOrgMembersOpts{ Doer: ctx.Doer, OrgID: org.ID, @@ -109,9 +103,9 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 - prepareResult, err := shared_user.PrepareOrgHeader(ctx) + prepareResult, err := shared_user.RenderUserOrgHeader(ctx) if err != nil { - ctx.ServerError("PrepareOrgHeader", err) + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -154,7 +148,7 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.HTML(http.StatusOK, tplOrgHome) } -func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOrgHeaderResult) bool { +func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOwnerHeaderResult) bool { viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public")) viewAsMember := viewAs == "member" diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 7d88d6b1ad..2cbe75989a 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -54,9 +54,8 @@ func Members(ctx *context.Context) { return } - _, err = shared_user.PrepareOrgHeader(ctx) - if err != nil { - ctx.ServerError("PrepareOrgHeader", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index cd1d0d4dac..f423e9cb36 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -43,7 +43,10 @@ func MustEnableProjects(ctx *context.Context) { // Projects renders the home page of projects func Projects(ctx *context.Context) { - shared_user.PrepareContextForProfileBigAvatar(ctx) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } ctx.Data["Title"] = ctx.Tr("repo.projects") sortType := ctx.FormTrim("sort") @@ -101,7 +104,6 @@ func Projects(ctx *context.Context) { } ctx.Data["Projects"] = projects - shared_user.RenderUserHeader(ctx) if isShowClosed { ctx.Data["State"] = "closed" @@ -113,12 +115,6 @@ func Projects(ctx *context.Context) { project.RenderedContent = renderUtils.MarkdownToHtml(project.Description) } - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } - numPages := 0 if total > 0 { numPages = (int(total) - 1/setting.UI.IssuePagingNum) @@ -152,11 +148,8 @@ func RenderNewProject(ctx *context.Context) { ctx.Data["PageIsViewProjects"] = true ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects" - shared_user.RenderUserHeader(ctx) - - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -167,7 +160,10 @@ func RenderNewProject(ctx *context.Context) { func NewProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateProjectForm) ctx.Data["Title"] = ctx.Tr("repo.projects.new") - shared_user.RenderUserHeader(ctx) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } if ctx.HasError() { RenderNewProject(ctx) @@ -248,7 +244,10 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CardTypes"] = project_model.GetCardConfig() - shared_user.RenderUserHeader(ctx) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { @@ -282,11 +281,8 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["CardTypes"] = project_model.GetCardConfig() ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID) - shared_user.RenderUserHeader(ctx) - - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -443,11 +439,9 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = columns - shared_user.RenderUserHeader(ctx) - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index a8a81e0ade..82c3bce722 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -48,9 +48,8 @@ func Settings(ctx *context.Context) { ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess ctx.Data["ContextUser"] = ctx.ContextUser - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -194,9 +193,8 @@ func SettingsDelete(ctx *context.Context) { return } - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -218,9 +216,8 @@ func Webhooks(ctx *context.Context) { return } - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -246,9 +243,8 @@ func Labels(ctx *context.Context) { ctx.Data["PageIsOrgSettingsLabels"] = true ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go index c93058477e..47f653bf88 100644 --- a/routers/web/org/setting_oauth2.go +++ b/routers/web/org/setting_oauth2.go @@ -45,9 +45,8 @@ func Applications(ctx *context.Context) { } ctx.Data["Applications"] = apps - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go index 0912a9e0fd..ec80e2867c 100644 --- a/routers/web/org/setting_packages.go +++ b/routers/web/org/setting_packages.go @@ -25,9 +25,8 @@ func Packages(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -41,9 +40,8 @@ func PackagesRuleAdd(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -57,9 +55,8 @@ func PackagesRuleEdit(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -99,9 +96,8 @@ func PackagesRulePreview(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index b1b0dd2c49..676c6d0c63 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -46,6 +46,10 @@ const ( // Teams render teams list page func Teams(ctx *context.Context) { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } org := ctx.Org.Organization ctx.Data["Title"] = org.FullName ctx.Data["PageIsOrgTeams"] = true @@ -58,12 +62,6 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams - _, err := shared_user.PrepareOrgHeader(ctx) - if err != nil { - ctx.ServerError("PrepareOrgHeader", err) - return - } - ctx.HTML(http.StatusOK, tplTeams) } @@ -272,15 +270,15 @@ func TeamsRepoAction(ctx *context.Context) { // NewTeam render create new team page func NewTeam(ctx *context.Context) { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } ctx.Data["Title"] = ctx.Org.Organization.FullName ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamsNew"] = true ctx.Data["Team"] = &org_model.Team{} ctx.Data["Units"] = unit_model.Units - if err := shared_user.LoadHeaderCount(ctx); err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } ctx.HTML(http.StatusOK, tplTeamNew) } @@ -370,15 +368,15 @@ func NewTeamPost(ctx *context.Context) { // TeamMembers render team members page func TeamMembers(ctx *context.Context) { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } + ctx.Data["Title"] = ctx.Org.Team.Name ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamMembers"] = true - if err := shared_user.LoadHeaderCount(ctx); err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } - if err := ctx.Org.Team.LoadMembers(ctx); err != nil { ctx.ServerError("GetMembers", err) return @@ -398,15 +396,15 @@ func TeamMembers(ctx *context.Context) { // TeamRepositories show the repositories of team func TeamRepositories(ctx *context.Context) { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } + ctx.Data["Title"] = ctx.Org.Team.Name ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamRepos"] = true - if err := shared_user.LoadHeaderCount(ctx); err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } - repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ TeamID: ctx.Org.Team.ID, }) @@ -463,16 +461,16 @@ func SearchTeam(ctx *context.Context) { // EditTeam render team edit page func EditTeam(ctx *context.Context) { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } ctx.Data["Title"] = ctx.Org.Organization.FullName ctx.Data["PageIsOrgTeams"] = true if err := ctx.Org.Team.LoadUnits(ctx); err != nil { ctx.ServerError("LoadUnits", err) return } - if err := shared_user.LoadHeaderCount(ctx); err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } ctx.Data["Team"] = ctx.Org.Team ctx.Data["Units"] = unit_model.Units ctx.HTML(http.StatusOK, tplTeamNew) diff --git a/routers/web/org/worktime.go b/routers/web/org/worktime.go index a576dd9a11..c7b44baf7b 100644 --- a/routers/web/org/worktime.go +++ b/routers/web/org/worktime.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/modules/templates" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) @@ -70,6 +71,12 @@ func Worktime(ctx *context.Context) { ctx.ServerError("GetWorktime", err) return } + + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } + ctx.Data["WorktimeSumResult"] = worktimeSumResult ctx.HTML(http.StatusOK, tplByRepos) } diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index efd85b9452..e125267524 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -15,13 +15,13 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/languagestats" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" - files_service "code.gitea.io/gitea/services/repository/files" ) type blameRow struct { @@ -234,7 +234,7 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) { repoLink := ctx.Repo.RepoLink - language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) + language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) if err != nil { log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) } diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index efda9bda58..a065620b2b 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/git/pipeline" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -134,39 +135,24 @@ func LFSLocks(ctx *context.Context) { } defer gitRepo.Close() - filenames := make([]string, len(lfsLocks)) - - for i, lock := range lfsLocks { - filenames[i] = lock.Path - } - - if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil { - log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err) - ctx.ServerError("LFSLocks", fmt.Errorf("unable to read the default branch to the index: %s (%w)", ctx.Repo.Repository.DefaultBranch, err)) - return - } - - name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"lockable"}, - Filenames: filenames, - CachedOnly: true, - }) + checker, err := attribute.NewBatchChecker(gitRepo, ctx.Repo.Repository.DefaultBranch, []string{attribute.Lockable}) if err != nil { log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err) ctx.ServerError("LFSLocks", err) return } + defer checker.Close() lockables := make([]bool, len(lfsLocks)) + filenames := make([]string, len(lfsLocks)) for i, lock := range lfsLocks { - attribute2info, has := name2attribute2info[lock.Path] - if !has { + filenames[i] = lock.Path + attrs, err := checker.CheckPath(lock.Path) + if err != nil { + log.Error("Unable to check attributes in %s: %s (%v)", tmpBasePath, lock.Path, err) continue } - if attribute2info["lockable"] != "set" { - continue - } - lockables[i] = true + lockables[i] = attrs.Get(attribute.Lockable).ToBool().Value() } ctx.Data["Lockables"] = lockables diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index 46cb875f9b..c6e2d18249 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -44,9 +44,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) { } if ctx.Data["PageIsOrgSettings"] == true { - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return nil, nil } return &secretsCtx{ diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 12083a1ced..ff0e1b4d54 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -25,7 +26,6 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" - files_service "code.gitea.io/gitea/services/repository/files" "github.com/nektos/act/pkg/model" ) @@ -147,6 +147,23 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") } + // read all needed attributes which will be used later + // there should be no performance different between reading 2 or 4 here + attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{ + Filenames: []string{ctx.Repo.TreePath}, + Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage}, + }) + if err != nil { + ctx.ServerError("attribute.CheckAttributes", err) + return + } + attrs := attrsMap[ctx.Repo.TreePath] + if attrs == nil { + // this case shouldn't happen, just in case. + setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath) + attrs = attribute.NewAttributes() + } + switch { case isRepresentableAsText: if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { @@ -209,11 +226,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 } - language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) - if err != nil { - log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) - } - + language := attrs.GetLanguage().Value() fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) ctx.Data["LexerName"] = lexerName if err != nil { @@ -283,17 +296,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } - if ctx.Repo.GitRepo != nil { - checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID) - if checker != nil { - defer deferable() - attrs, err := checker.CheckPath(ctx.Repo.TreePath) - if err == nil { - ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value() - ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value() - } - } - } + ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value() if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() { img, _, err := image.DecodeConfig(bytes.NewReader(buf)) diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 444bd960db..a87f6ce4dc 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -57,9 +57,8 @@ func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) { } if ctx.Data["PageIsOrgSettings"] == true { - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return nil, nil } return &runnersCtx{ diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index 9cc1676d7b..a43c2c2690 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -49,9 +49,8 @@ func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { } if ctx.Data["PageIsOrgSettings"] == true { - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return nil, nil } return &variablesCtx{ diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 62b146c7f3..48a5d58ea4 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -24,19 +24,8 @@ import ( "code.gitea.io/gitea/services/context" ) -// prepareContextForCommonProfile store some common data into context data for user's profile related pages (including the nav menu) -// It is designed to be fast and safe to be called multiple times in one request -func prepareContextForCommonProfile(ctx *context.Context) { - ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - ctx.Data["EnableFeed"] = setting.Other.EnableFeed - ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink() -} - -// PrepareContextForProfileBigAvatar set the context for big avatar view on the profile page -func PrepareContextForProfileBigAvatar(ctx *context.Context) { - prepareContextForCommonProfile(ctx) - +// prepareContextForProfileBigAvatar set the context for big avatar view on the profile page +func prepareContextForProfileBigAvatar(ctx *context.Context) { ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate if setting.Service.UserLocationMapURL != "" { @@ -138,16 +127,44 @@ func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProf return profileDbRepo, profileReadmeBlob } -func RenderUserHeader(ctx *context.Context) { - prepareContextForCommonProfile(ctx) - - _, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer) - ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil +type PrepareOwnerHeaderResult struct { + ProfilePublicRepo *repo_model.Repository + ProfilePublicReadmeBlob *git.Blob + ProfilePrivateRepo *repo_model.Repository + ProfilePrivateReadmeBlob *git.Blob + HasOrgProfileReadme bool } -func LoadHeaderCount(ctx *context.Context) error { - prepareContextForCommonProfile(ctx) +const ( + RepoNameProfilePrivate = ".profile-private" + RepoNameProfile = ".profile" +) +func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) { + ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + ctx.Data["EnableFeed"] = setting.Other.EnableFeed + ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink() + + if err := loadHeaderCount(ctx); err != nil { + return nil, err + } + + result = &PrepareOwnerHeaderResult{} + if ctx.ContextUser.IsOrganization() { + result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer) + result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate) + result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil + ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab + } else { + _, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer) + ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil + prepareContextForProfileBigAvatar(ctx) + } + return result, nil +} + +func loadHeaderCount(ctx *context.Context) error { repoCount, err := repo_model.CountRepository(ctx, &repo_model.SearchRepoOptions{ Actor: ctx.Doer, OwnerID: ctx.ContextUser.ID, @@ -178,29 +195,3 @@ func LoadHeaderCount(ctx *context.Context) error { return nil } - -const ( - RepoNameProfilePrivate = ".profile-private" - RepoNameProfile = ".profile" -) - -type PrepareOrgHeaderResult struct { - ProfilePublicRepo *repo_model.Repository - ProfilePublicReadmeBlob *git.Blob - ProfilePrivateRepo *repo_model.Repository - ProfilePrivateReadmeBlob *git.Blob - HasOrgProfileReadme bool -} - -func PrepareOrgHeader(ctx *context.Context) (result *PrepareOrgHeaderResult, err error) { - if err = LoadHeaderCount(ctx); err != nil { - return nil, err - } - - result = &PrepareOrgHeaderResult{} - result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer) - result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate) - result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil - ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab - return result, nil -} diff --git a/routers/web/user/code.go b/routers/web/user/code.go index f9aa58b877..f2153c6d54 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -26,11 +26,8 @@ func CodeSearch(ctx *context.Context) { ctx.Redirect(ctx.ContextUser.HomeLink()) return } - shared_user.PrepareContextForProfileBigAvatar(ctx) - shared_user.RenderUserHeader(ctx) - - if err := shared_user.LoadHeaderCount(ctx); err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } diff --git a/routers/web/user/package.go b/routers/web/user/package.go index c01bc96e2b..e96f5a04ea 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -42,7 +42,10 @@ const ( // ListPackages displays a list of all packages of the context user func ListPackages(ctx *context.Context) { - shared_user.PrepareContextForProfileBigAvatar(ctx) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } page := ctx.FormInt("page") if page <= 1 { page = 1 @@ -94,8 +97,6 @@ func ListPackages(ctx *context.Context) { return } - shared_user.RenderUserHeader(ctx) - ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["IsPackagesPage"] = true ctx.Data["Query"] = query @@ -106,9 +107,8 @@ func ListPackages(ctx *context.Context) { ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = repositoryAccessMap - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -126,11 +126,9 @@ func ListPackages(ctx *context.Context) { ctx.Data["IsOrganizationOwner"] = false } } - pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager - ctx.HTML(http.StatusOK, tplPackagesList) } @@ -164,16 +162,17 @@ func RedirectToLastVersion(ctx *context.Context) { ctx.ServerError("GetPackageDescriptor", err) return } - ctx.Redirect(pd.VersionWebLink()) } // ViewPackageVersion displays a single package version func ViewPackageVersion(ctx *context.Context) { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } + pd := ctx.Package.Descriptor - - shared_user.RenderUserHeader(ctx) - ctx.Data["Title"] = pd.Package.Name ctx.Data["IsPackagesPage"] = true ctx.Data["PackageDescriptor"] = pd @@ -301,19 +300,16 @@ func ViewPackageVersion(ctx *context.Context) { hasRepositoryAccess = permission.HasAnyUnitAccess() } ctx.Data["HasRepositoryAccess"] = hasRepositoryAccess - - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } - ctx.HTML(http.StatusOK, tplPackagesView) } // ListPackageVersions lists all versions of a package func ListPackageVersions(ctx *context.Context) { - shared_user.PrepareContextForProfileBigAvatar(ctx) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } + p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.Type(ctx.PathParam("type")), ctx.PathParam("name")) if err != nil { if err == packages_model.ErrPackageNotExist { @@ -336,8 +332,6 @@ func ListPackageVersions(ctx *context.Context) { query := ctx.FormTrim("q") sort := ctx.FormTrim("sort") - shared_user.RenderUserHeader(ctx) - ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["IsPackagesPage"] = true ctx.Data["PackageDescriptor"] = &packages_model.PackageDescriptor{ @@ -393,12 +387,6 @@ func ListPackageVersions(ctx *context.Context) { ctx.Data["Total"] = total - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } - pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager @@ -410,7 +398,10 @@ func ListPackageVersions(ctx *context.Context) { func PackageSettings(ctx *context.Context) { pd := ctx.Package.Descriptor - shared_user.RenderUserHeader(ctx) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } ctx.Data["Title"] = pd.Package.Name ctx.Data["IsPackagesPage"] = true @@ -423,12 +414,6 @@ func PackageSettings(ctx *context.Context) { ctx.Data["Repos"] = repos ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } - ctx.HTML(http.StatusOK, tplPackagesSettings) } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 39f066a53c..ee19665109 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -78,8 +78,15 @@ func userProfile(ctx *context.Context) { showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob) - // call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing - shared_user.PrepareContextForProfileBigAvatar(ctx) + + // prepare the user nav header data after "prepareUserProfileTabData" to avoid re-querying the NumFollowers & NumFollowing + // because ctx.Data["NumFollowers"] and "NumFollowing" logic duplicates in both of them + // and the "profile readme" related logic also duplicates in both of FindOwnerProfileReadme and RenderUserOrgHeader + // TODO: it is a bad design and should be refactored later, + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } ctx.HTML(http.StatusOK, tplProfile) } @@ -302,9 +309,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb ctx.Data["Repos"] = repos ctx.Data["Total"] = total - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } @@ -328,9 +334,11 @@ func ActionUserFollow(ctx *context.Context) { ctx.HTTPError(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action"))) return } - + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return + } if ctx.ContextUser.IsIndividual() { - shared_user.PrepareContextForProfileBigAvatar(ctx) ctx.HTML(http.StatusOK, tplProfileBigAvatar) return } else if ctx.ContextUser.IsOrganization() { diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go index d4da468a85..f460acce10 100644 --- a/routers/web/user/setting/oauth2_common.go +++ b/routers/web/user/setting/oauth2_common.go @@ -28,8 +28,8 @@ func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) { ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID) if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() { - if err := shared_user.LoadHeaderCount(ctx); err != nil { - ctx.ServerError("LoadHeaderCount", err) + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) return } } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index b9781cf8d0..9ee86d9dfc 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -1237,24 +1238,21 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp return nil, err } - checker, deferrable := gitRepo.CheckAttributeReader(opts.AfterCommitID) - defer deferrable() + checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage}) + if err != nil { + return nil, err + } + defer checker.Close() for _, diffFile := range diff.Files { isVendored := optional.None[bool]() isGenerated := optional.None[bool]() - if checker != nil { - attrs, err := checker.CheckPath(diffFile.Name) - if err == nil { - isVendored = git.AttributeToBool(attrs, git.AttributeLinguistVendored) - isGenerated = git.AttributeToBool(attrs, git.AttributeLinguistGenerated) - - language := git.TryReadLanguageAttribute(attrs) - if language.Has() { - diffFile.Language = language.Value() - } - } else { - checker = nil // CheckPath fails, it's not impossible to "check" anymore + attrs, err := checker.CheckPath(diffFile.Name) + if err == nil { + isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated() + language := attrs.GetLanguage() + if language.Has() { + diffFile.Language = language.Value() } } diff --git a/services/markup/renderhelper_codepreview.go b/services/markup/renderhelper_codepreview.go index 28d1120984..fa1eb824a2 100644 --- a/services/markup/renderhelper_codepreview.go +++ b/services/markup/renderhelper_codepreview.go @@ -14,13 +14,13 @@ import ( "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/git/languagestats" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" gitea_context "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/repository/files" ) func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) { @@ -61,7 +61,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie return "", err } - language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath) + language, _ := languagestats.GetFileLanguage(ctx, gitRepo, opts.CommitID, opts.FilePath) blob, err := commit.GetBlobByPath(opts.FilePath) if err != nil { return "", err diff --git a/services/repository/files/content.go b/services/repository/files/content.go index e23cd1abce..0327e7f2ce 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -277,28 +277,3 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git Content: content, }, nil } - -// TryGetContentLanguage tries to get the (linguist) language of the file content -func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (string, error) { - indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitID) - if err != nil { - return "", err - } - - defer deleteTemporaryFile() - - filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ - CachedOnly: true, - Attributes: []string{git.AttributeLinguistLanguage, git.AttributeGitlabLanguage}, - Filenames: []string{treePath}, - IndexFile: indexFilename, - WorkTree: worktree, - }) - if err != nil { - return "", err - } - - language := git.TryReadLanguageAttribute(filename2attribute2info[treePath]) - - return language.Value(), nil -} diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 3f6255e77a..75ede4976f 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -15,6 +15,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -488,16 +489,15 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file var lfsMetaObject *git_model.LFSMetaObject if setting.LFS.StartServer && hasOldBranch { // Check there is no way this can return multiple infos - filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"filter"}, + attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ + Attributes: []string{attribute.Filter}, Filenames: []string{file.Options.treePath}, - CachedOnly: true, }) if err != nil { return err } - if filename2attribute2info[file.Options.treePath] != nil && filename2attribute2info[file.Options.treePath]["filter"] == "lfs" { + if attributesMap[file.Options.treePath] != nil && attributesMap[file.Options.treePath].Get(attribute.Filter).ToString().Value() == "lfs" { // OK so we are supposed to LFS this data! pointer, err := lfs.GeneratePointer(treeObjectContentReader) if err != nil { diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index 2e4ed1744e..f348cb68ab 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -14,6 +14,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" ) @@ -105,12 +106,11 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } } - var filename2attribute2info map[string]map[string]string + var attributesMap map[string]*attribute.Attributes if setting.LFS.StartServer { - filename2attribute2info, err = t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"filter"}, + attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ + Attributes: []string{attribute.Filter}, Filenames: names, - CachedOnly: true, }) if err != nil { return err @@ -119,7 +119,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use // Copy uploaded files into repository. for i := range infos { - if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], filename2attribute2info, t, opts.TreePath); err != nil { + if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil { return err } } @@ -176,7 +176,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return repo_model.DeleteUploads(ctx, uploads...) } -func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, filename2attribute2info map[string]map[string]string, t *TemporaryUploadRepository, treePath string) error { +func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, attributesMap map[string]*attribute.Attributes, t *TemporaryUploadRepository, treePath string) error { file, err := os.Open(info.upload.LocalPath()) if err != nil { return err @@ -184,7 +184,7 @@ func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, fi defer file.Close() var objectHash string - if setting.LFS.StartServer && filename2attribute2info[info.upload.Name] != nil && filename2attribute2info[info.upload.Name]["filter"] == "lfs" { + if setting.LFS.StartServer && attributesMap[info.upload.Name] != nil && attributesMap[info.upload.Name].Get(attribute.Filter).ToString().Value() == "lfs" { // Handle LFS // FIXME: Inefficient! this should probably happen in models.Upload pointer, err := lfs.GeneratePointer(file) diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 2d3af2d559..d876dabb44 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -44,7 +44,7 @@ {{end}} </a> {{end}} - {{if .IsOrganizationOwner}} + {{if and EnableTimetracking .IsOrganizationOwner}} <a class="{{if $.PageIsOrgTimes}}active{{end}} item" href="{{$.OrgLink}}/worktime"> {{svg "octicon-clock"}} {{ctx.Locale.Tr "org.worktime"}} </a> diff --git a/templates/org/projects/new.tmpl b/templates/org/projects/new.tmpl index fc52130f68..c021c5a0fe 100644 --- a/templates/org/projects/new.tmpl +++ b/templates/org/projects/new.tmpl @@ -1,9 +1,24 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content organization projects edit-project new"> - {{template "shared/user/org_profile_avatar" .}} +{{if .ContextUser.IsOrganization}} +<div role="main" aria-label="{{.Title}}" class="page-content organization projects"> + {{template "org/header" .}} <div class="ui container"> - {{template "user/overview/header" .}} - {{template "projects/new" .}} + {{template "projects/new" .}} </div> </div> +{{else}} +<div role="main" aria-label="{{.Title}}" class="page-content user profile"> + <div class="ui container"> + <div class="ui stackable grid"> + <div class="ui four wide column"> + {{template "shared/user/profile_big_avatar" .}} + </div> + <div class="ui twelve wide column tw-mb-4"> + {{template "user/overview/header" .}} + {{template "projects/new" .}} + </div> + </div> + </div> +</div> +{{end}} {{template "base/footer" .}} diff --git a/templates/package/shared/view.tmpl b/templates/package/shared/view.tmpl new file mode 100644 index 0000000000..713e1bbfc5 --- /dev/null +++ b/templates/package/shared/view.tmpl @@ -0,0 +1,106 @@ +<div class="issue-title-header"> + <h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1> + <div> + {{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} + {{if .HasRepositoryAccess}} + {{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName .PackageDescriptor.Repository.Link .PackageDescriptor.Repository.FullName}} + {{else}} + {{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName}} + {{end}} + </div> +</div> +<div class="issue-content"> + <div class="issue-content-left"> + {{template "package/content/alpine" .}} + {{template "package/content/arch" .}} + {{template "package/content/cargo" .}} + {{template "package/content/chef" .}} + {{template "package/content/composer" .}} + {{template "package/content/conan" .}} + {{template "package/content/conda" .}} + {{template "package/content/container" .}} + {{template "package/content/cran" .}} + {{template "package/content/debian" .}} + {{template "package/content/generic" .}} + {{template "package/content/go" .}} + {{template "package/content/helm" .}} + {{template "package/content/maven" .}} + {{template "package/content/npm" .}} + {{template "package/content/nuget" .}} + {{template "package/content/pub" .}} + {{template "package/content/pypi" .}} + {{template "package/content/rpm" .}} + {{template "package/content/rubygems" .}} + {{template "package/content/swift" .}} + {{template "package/content/vagrant" .}} + </div> + <div class="issue-content-right ui segment"> + <strong>{{ctx.Locale.Tr "packages.details"}}</strong> + <div class="ui relaxed list flex-items-block"> + <div class="item">{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}</div> + {{if .HasRepositoryAccess}} + <div class="item">{{svg "octicon-repo"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div> + {{end}} + <div class="item">{{svg "octicon-calendar"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}</div> + <div class="item">{{svg "octicon-download"}} {{.PackageDescriptor.Version.DownloadCount}}</div> + {{template "package/metadata/alpine" .}} + {{template "package/metadata/arch" .}} + {{template "package/metadata/cargo" .}} + {{template "package/metadata/chef" .}} + {{template "package/metadata/composer" .}} + {{template "package/metadata/conan" .}} + {{template "package/metadata/conda" .}} + {{template "package/metadata/container" .}} + {{template "package/metadata/cran" .}} + {{template "package/metadata/debian" .}} + {{template "package/metadata/generic" .}} + {{template "package/metadata/helm" .}} + {{template "package/metadata/maven" .}} + {{template "package/metadata/npm" .}} + {{template "package/metadata/nuget" .}} + {{template "package/metadata/pub" .}} + {{template "package/metadata/pypi" .}} + {{template "package/metadata/rpm" .}} + {{template "package/metadata/rubygems" .}} + {{template "package/metadata/swift" .}} + {{template "package/metadata/vagrant" .}} + {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} + <div class="item">{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div> + {{end}} + </div> + {{if not (eq .PackageDescriptor.Package.Type "container")}} + <div class="divider"></div> + <strong>{{ctx.Locale.Tr "packages.assets"}} ({{len .PackageDescriptor.Files}})</strong> + <div class="ui relaxed list"> + {{range .PackageDescriptor.Files}} + <div class="item"> + <a href="{{$.Link}}/files/{{.File.ID}}">{{.File.Name}}</a> + <span class="text small file-size">{{FileSize .Blob.Size}}</span> + </div> + {{end}} + </div> + {{end}} + <div class="divider"></div> + <strong>{{ctx.Locale.Tr "packages.versions"}} ({{.TotalVersionCount}})</strong> + <a class="tw-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a> + <div class="ui relaxed list"> + {{range .LatestVersions}} + <div class="item tw-flex"> + <a class="tw-flex-1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a> + <span class="text small">{{DateUtils.AbsoluteShort .CreatedUnix}}</span> + </div> + {{end}} + </div> + {{if or .CanWritePackages .HasRepositoryAccess}} + <div class="divider"></div> + <div class="ui relaxed list flex-items-block"> + {{if .HasRepositoryAccess}} + <div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div> + {{end}} + {{if .CanWritePackages}} + <div class="item">{{svg "octicon-tools"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> + {{end}} + </div> + {{end}} + </div> +</div> diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 9e92207466..9067f44296 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -1,114 +1,24 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content repository packages"> - {{template "shared/user/org_profile_avatar" .}} +{{if .ContextUser.IsOrganization}} +<div role="main" aria-label="{{.Title}}" class="page-content organization packages"> + {{template "org/header" .}} <div class="ui container"> - {{template "user/overview/header" .}} - <div class="issue-title-header"> - <h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1> - <div> - {{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} - {{if .HasRepositoryAccess}} - {{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName .PackageDescriptor.Repository.Link .PackageDescriptor.Repository.FullName}} - {{else}} - {{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName}} - {{end}} + {{template "package/shared/view" .}} + </div> +</div> +{{else}} +<div role="main" aria-label="{{.Title}}" class="page-content user profile packages"> + <div class="ui container"> + <div class="ui stackable grid"> + <div class="ui four wide column"> + {{template "shared/user/profile_big_avatar" .}} </div> - </div> - <div class="issue-content"> - <div class="issue-content-left"> - {{template "package/content/alpine" .}} - {{template "package/content/arch" .}} - {{template "package/content/cargo" .}} - {{template "package/content/chef" .}} - {{template "package/content/composer" .}} - {{template "package/content/conan" .}} - {{template "package/content/conda" .}} - {{template "package/content/container" .}} - {{template "package/content/cran" .}} - {{template "package/content/debian" .}} - {{template "package/content/generic" .}} - {{template "package/content/go" .}} - {{template "package/content/helm" .}} - {{template "package/content/maven" .}} - {{template "package/content/npm" .}} - {{template "package/content/nuget" .}} - {{template "package/content/pub" .}} - {{template "package/content/pypi" .}} - {{template "package/content/rpm" .}} - {{template "package/content/rubygems" .}} - {{template "package/content/swift" .}} - {{template "package/content/vagrant" .}} - </div> - <div class="issue-content-right ui segment"> - <strong>{{ctx.Locale.Tr "packages.details"}}</strong> - <div class="ui relaxed list flex-items-block"> - <div class="item">{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}</div> - {{if .HasRepositoryAccess}} - <div class="item">{{svg "octicon-repo"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div> - {{end}} - <div class="item">{{svg "octicon-calendar"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}</div> - <div class="item">{{svg "octicon-download"}} {{.PackageDescriptor.Version.DownloadCount}}</div> - {{template "package/metadata/alpine" .}} - {{template "package/metadata/arch" .}} - {{template "package/metadata/cargo" .}} - {{template "package/metadata/chef" .}} - {{template "package/metadata/composer" .}} - {{template "package/metadata/conan" .}} - {{template "package/metadata/conda" .}} - {{template "package/metadata/container" .}} - {{template "package/metadata/cran" .}} - {{template "package/metadata/debian" .}} - {{template "package/metadata/generic" .}} - {{template "package/metadata/helm" .}} - {{template "package/metadata/maven" .}} - {{template "package/metadata/npm" .}} - {{template "package/metadata/nuget" .}} - {{template "package/metadata/pub" .}} - {{template "package/metadata/pypi" .}} - {{template "package/metadata/rpm" .}} - {{template "package/metadata/rubygems" .}} - {{template "package/metadata/swift" .}} - {{template "package/metadata/vagrant" .}} - {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} - <div class="item">{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div> - {{end}} - </div> - {{if not (eq .PackageDescriptor.Package.Type "container")}} - <div class="divider"></div> - <strong>{{ctx.Locale.Tr "packages.assets"}} ({{len .PackageDescriptor.Files}})</strong> - <div class="ui relaxed list"> - {{range .PackageDescriptor.Files}} - <div class="item"> - <a href="{{$.Link}}/files/{{.File.ID}}">{{.File.Name}}</a> - <span class="text small file-size">{{FileSize .Blob.Size}}</span> - </div> - {{end}} - </div> - {{end}} - <div class="divider"></div> - <strong>{{ctx.Locale.Tr "packages.versions"}} ({{.TotalVersionCount}})</strong> - <a class="tw-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a> - <div class="ui relaxed list"> - {{range .LatestVersions}} - <div class="item tw-flex"> - <a class="tw-flex-1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a> - <span class="text small">{{DateUtils.AbsoluteShort .CreatedUnix}}</span> - </div> - {{end}} - </div> - {{if or .CanWritePackages .HasRepositoryAccess}} - <div class="divider"></div> - <div class="ui relaxed list flex-items-block"> - {{if .HasRepositoryAccess}} - <div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div> - {{end}} - {{if .CanWritePackages}} - <div class="item">{{svg "octicon-tools"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> - {{end}} - </div> - {{end}} + <div class="ui twelve wide column tw-mb-4"> + {{template "user/overview/header" .}} + {{template "package/shared/view" .}} </div> </div> </div> </div> +{{end}} {{template "base/footer" .}}