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" .}}