From d0ca2f6bc316b653829fa0d0890fd59698fac3bc Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 5 Nov 2025 18:18:26 +0100 Subject: [PATCH 01/20] Fix pull description code label background (#35865) Fix visual regression from https://github.com/go-gitea/gitea/pull/35567: Before: image After: image --- web_src/css/repo.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 070623d24e..206a591fbd 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -387,6 +387,7 @@ td .commit-summary { .repository.view.issue .pull-desc code { color: var(--color-primary); + background: transparent; } .repository.view.issue .pull-desc a[data-clipboard-text] { From 525265c1a8fda77b265bb4bdc59ca77bff003620 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 6 Nov 2025 01:48:38 +0800 Subject: [PATCH 02/20] Refactor ls-tree and git path related problems (#35858) Fix #35852, the root problem is that the "name" field is heavily abused (since #6816, and no way to get a clear fix) There are still a lot of legacy problems in old code. Co-authored-by: Giteabot --- modules/base/natural_sort.go | 12 +- modules/base/natural_sort_test.go | 6 +- modules/git/commit_info_test.go | 1 - modules/git/notes_gogit.go | 7 +- modules/git/parse_gogit.go | 96 ---------------- modules/git/parse_gogit_test.go | 78 ------------- .../{parse_nogogit.go => parse_treeentry.go} | 2 - ...ogogit_test.go => parse_treeentry_test.go} | 2 - modules/git/repo_commit_gogit.go | 2 +- modules/git/repo_tree_gogit.go | 2 +- modules/git/tree.go | 14 ++- modules/git/tree_blob_gogit.go | 14 +-- modules/git/tree_entry.go | 105 ++++++++++-------- modules/git/tree_entry_gogit.go | 61 +++------- modules/git/tree_entry_gogit_test.go | 27 +++++ modules/git/tree_entry_nogogit.go | 46 -------- modules/git/tree_entry_test.go | 58 +++------- modules/git/tree_gogit.go | 76 +++++-------- modules/git/tree_nogogit.go | 24 +--- routers/web/repo/commit.go | 2 + routers/web/repo/treelist.go | 2 +- routers/web/repo/view.go | 2 +- routers/web/repo/view_readme.go | 2 +- routers/web/repo/wiki.go | 2 +- 24 files changed, 193 insertions(+), 450 deletions(-) delete mode 100644 modules/git/parse_gogit.go delete mode 100644 modules/git/parse_gogit_test.go rename modules/git/{parse_nogogit.go => parse_treeentry.go} (99%) rename modules/git/{parse_nogogit_test.go => parse_treeentry_test.go} (99%) create mode 100644 modules/git/tree_entry_gogit_test.go diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go index acb9002276..d1ee7b04ec 100644 --- a/modules/base/natural_sort.go +++ b/modules/base/natural_sort.go @@ -41,8 +41,8 @@ func naturalSortAdvance(str string, pos int) (end int, isNumber bool) { return end, isNumber } -// NaturalSortLess compares two strings so that they could be sorted in natural order -func NaturalSortLess(s1, s2 string) bool { +// NaturalSortCompare compares two strings so that they could be sorted in natural order +func NaturalSortCompare(s1, s2 string) int { // There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997 // text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997 // So we need to handle the number parts by ourselves @@ -55,16 +55,16 @@ func NaturalSortLess(s1, s2 string) bool { if isNum1 && isNum2 { if part1 != part2 { if len(part1) != len(part2) { - return len(part1) < len(part2) + return len(part1) - len(part2) } - return part1 < part2 + return c.CompareString(part1, part2) } } else { if cmp := c.CompareString(part1, part2); cmp != 0 { - return cmp < 0 + return cmp } } pos1, pos2 = end1, end2 } - return len(s1) < len(s2) + return len(s1) - len(s2) } diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go index b001bc4ac9..451aba6618 100644 --- a/modules/base/natural_sort_test.go +++ b/modules/base/natural_sort_test.go @@ -11,12 +11,10 @@ import ( func TestNaturalSortLess(t *testing.T) { testLess := func(s1, s2 string) { - assert.True(t, NaturalSortLess(s1, s2), "s1 2 { diff --git a/modules/git/parse_gogit.go b/modules/git/parse_gogit.go deleted file mode 100644 index 74d258de8e..0000000000 --- a/modules/git/parse_gogit.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package git - -import ( - "bytes" - "fmt" - "strconv" - "strings" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/hash" - "github.com/go-git/go-git/v5/plumbing/object" -) - -// ParseTreeEntries parses the output of a `git ls-tree -l` command. -func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { - return parseTreeEntries(data, nil) -} - -func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - entries := make([]*TreeEntry, 0, 10) - for pos := 0; pos < len(data); { - // expect line to be of the form " \t" - entry := new(TreeEntry) - entry.gogitTreeEntry = &object.TreeEntry{} - entry.ptree = ptree - if pos+6 > len(data) { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - switch string(data[pos : pos+6]) { - case "100644": - entry.gogitTreeEntry.Mode = filemode.Regular - pos += 12 // skip over "100644 blob " - case "100755": - entry.gogitTreeEntry.Mode = filemode.Executable - pos += 12 // skip over "100755 blob " - case "120000": - entry.gogitTreeEntry.Mode = filemode.Symlink - pos += 12 // skip over "120000 blob " - case "160000": - entry.gogitTreeEntry.Mode = filemode.Submodule - pos += 14 // skip over "160000 object " - case "040000": - entry.gogitTreeEntry.Mode = filemode.Dir - pos += 12 // skip over "040000 tree " - default: - return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6])) - } - - // in hex format, not byte format .... - if pos+hash.Size*2 > len(data) { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - var err error - entry.ID, err = NewIDFromString(string(data[pos : pos+hash.Size*2])) - if err != nil { - return nil, fmt.Errorf("invalid ls-tree output: %w", err) - } - entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue()) - pos += 41 // skip over sha and trailing space - - end := pos + bytes.IndexByte(data[pos:], '\t') - if end < pos { - return nil, fmt.Errorf("Invalid ls-tree -l output: %s", string(data)) - } - entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(data[pos:end])), 10, 64) - entry.sized = true - - pos = end + 1 - - end = pos + bytes.IndexByte(data[pos:], '\n') - if end < pos { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - - // In case entry name is surrounded by double quotes(it happens only in git-shell). - if data[pos] == '"' { - var err error - entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end])) - if err != nil { - return nil, fmt.Errorf("Invalid ls-tree output: %w", err) - } - } else { - entry.gogitTreeEntry.Name = string(data[pos:end]) - } - - pos = end + 1 - entries = append(entries, entry) - } - return entries, nil -} diff --git a/modules/git/parse_gogit_test.go b/modules/git/parse_gogit_test.go deleted file mode 100644 index 3e171d7e56..0000000000 --- a/modules/git/parse_gogit_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package git - -import ( - "fmt" - "testing" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/stretchr/testify/assert" -) - -func TestParseTreeEntries(t *testing.T) { - testCases := []struct { - Input string - Expected []*TreeEntry - }{ - { - Input: "", - Expected: []*TreeEntry{}, - }, - { - Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n", - Expected: []*TreeEntry{ - { - ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()), - Name: "example/file2.txt", - Mode: filemode.Regular, - }, - size: 1022, - sized: true, - }, - }, - }, - { - Input: "120000 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 234131\t\"example/\\n.txt\"\n" + - "040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n", - Expected: []*TreeEntry{ - { - ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()), - Name: "example/\n.txt", - Mode: filemode.Symlink, - }, - size: 234131, - sized: true, - }, - { - ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"), - sized: true, - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()), - Name: "example", - Mode: filemode.Dir, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - entries, err := ParseTreeEntries([]byte(testCase.Input)) - assert.NoError(t, err) - if len(entries) > 1 { - fmt.Println(testCase.Expected[0].ID) - fmt.Println(entries[0].ID) - } - assert.EqualValues(t, testCase.Expected, entries) - } -} diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_treeentry.go similarity index 99% rename from modules/git/parse_nogogit.go rename to modules/git/parse_treeentry.go index 78a0162889..e14d9f17b5 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_treeentry.go @@ -1,8 +1,6 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build !gogit - package git import ( diff --git a/modules/git/parse_nogogit_test.go b/modules/git/parse_treeentry_test.go similarity index 99% rename from modules/git/parse_nogogit_test.go rename to modules/git/parse_treeentry_test.go index 6594c84269..4223cbb3d7 100644 --- a/modules/git/parse_nogogit_test.go +++ b/modules/git/parse_treeentry_test.go @@ -1,8 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build !gogit - package git import ( diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 896d656039..c84aabde1a 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -107,7 +107,7 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { } commit.Tree.ID = ParseGogitHash(tree.Hash) - commit.Tree.gogitTree = tree + commit.Tree.resolvedGogitTreeObject = tree return commit, nil } diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index e15663a32a..89d34e87da 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -26,7 +26,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { } tree := NewTree(repo, id) - tree.gogitTree = gogitTree + tree.resolvedGogitTreeObject = gogitTree return tree, nil } diff --git a/modules/git/tree.go b/modules/git/tree.go index 9c73aec735..c1898b20cb 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -11,11 +11,21 @@ import ( "code.gitea.io/gitea/modules/git/gitcmd" ) +type TreeCommon struct { + ID ObjectID + ResolvedID ObjectID + + repo *Repository + ptree *Tree // parent tree +} + // NewTree create a new tree according the repository and tree id func NewTree(repo *Repository, id ObjectID) *Tree { return &Tree{ - ID: id, - repo: repo, + TreeCommon: TreeCommon{ + ID: id, + repo: repo, + }, } } diff --git a/modules/git/tree_blob_gogit.go b/modules/git/tree_blob_gogit.go index f29e8f8b9e..2c0ff0e1b0 100644 --- a/modules/git/tree_blob_gogit.go +++ b/modules/git/tree_blob_gogit.go @@ -11,22 +11,16 @@ import ( "strings" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" ) // GetTreeEntryByPath get the tree entries according the sub dir func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { if len(relpath) == 0 { return &TreeEntry{ - ID: t.ID, - // Type: ObjectTree, - ptree: t, - gogitTreeEntry: &object.TreeEntry{ - Name: "", - Mode: filemode.Dir, - Hash: plumbing.Hash(t.ID.RawValue()), - }, + ID: t.ID, + ptree: t, + name: "", + entryMode: EntryModeTree, }, nil } diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 5099d8ee79..e7e4ea2d5b 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -6,12 +6,60 @@ package git import ( "path" - "sort" + "slices" "strings" "code.gitea.io/gitea/modules/util" ) +// TreeEntry the leaf in the git tree +type TreeEntry struct { + ID ObjectID + + name string + ptree *Tree + + entryMode EntryMode + + size int64 + sized bool +} + +// Name returns the name of the entry (base name) +func (te *TreeEntry) Name() string { + return te.name +} + +// Mode returns the mode of the entry +func (te *TreeEntry) Mode() EntryMode { + return te.entryMode +} + +// IsSubModule if the entry is a submodule +func (te *TreeEntry) IsSubModule() bool { + return te.entryMode.IsSubModule() +} + +// IsDir if the entry is a sub dir +func (te *TreeEntry) IsDir() bool { + return te.entryMode.IsDir() +} + +// IsLink if the entry is a symlink +func (te *TreeEntry) IsLink() bool { + return te.entryMode.IsLink() +} + +// IsRegular if the entry is a regular file +func (te *TreeEntry) IsRegular() bool { + return te.entryMode.IsRegular() +} + +// IsExecutable if the entry is an executable file (not necessarily binary) +func (te *TreeEntry) IsExecutable() bool { + return te.entryMode.IsExecutable() +} + // Type returns the type of the entry (commit, tree, blob) func (te *TreeEntry) Type() string { switch te.Mode() { @@ -109,49 +157,16 @@ func (te *TreeEntry) GetSubJumpablePathName() string { // Entries a list of entry type Entries []*TreeEntry -type customSortableEntries struct { - Comparer func(s1, s2 string) bool - Entries -} - -var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{ - func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool { - return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule() - }, - func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool { - return cmp(t1.Name(), t2.Name()) - }, -} - -func (ctes customSortableEntries) Len() int { return len(ctes.Entries) } - -func (ctes customSortableEntries) Swap(i, j int) { - ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i] -} - -func (ctes customSortableEntries) Less(i, j int) bool { - t1, t2 := ctes.Entries[i], ctes.Entries[j] - var k int - for k = 0; k < len(sorter)-1; k++ { - s := sorter[k] - switch { - case s(t1, t2, ctes.Comparer): - return true - case s(t2, t1, ctes.Comparer): - return false - } - } - return sorter[k](t1, t2, ctes.Comparer) -} - -// Sort sort the list of entry -func (tes Entries) Sort() { - sort.Sort(customSortableEntries{func(s1, s2 string) bool { - return s1 < s2 - }, tes}) -} - // CustomSort customizable string comparing sort entry list -func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) { - sort.Sort(customSortableEntries{cmp, tes}) +func (tes Entries) CustomSort(cmp func(s1, s2 string) int) { + slices.SortFunc(tes, func(a, b *TreeEntry) int { + s1Dir, s2Dir := a.IsDir() || a.IsSubModule(), b.IsDir() || b.IsSubModule() + if s1Dir != s2Dir { + if s1Dir { + return -1 + } + return 1 + } + return cmp(a.Name(), b.Name()) + }) } diff --git a/modules/git/tree_entry_gogit.go b/modules/git/tree_entry_gogit.go index e6845f1c77..27877a2e28 100644 --- a/modules/git/tree_entry_gogit.go +++ b/modules/git/tree_entry_gogit.go @@ -12,25 +12,21 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -// TreeEntry the leaf in the git tree -type TreeEntry struct { - ID ObjectID - - gogitTreeEntry *object.TreeEntry - ptree *Tree - - size int64 - sized bool +// gogitFileModeToEntryMode converts go-git filemode to EntryMode +func gogitFileModeToEntryMode(mode filemode.FileMode) EntryMode { + return EntryMode(mode) } -// Name returns the name of the entry -func (te *TreeEntry) Name() string { - return te.gogitTreeEntry.Name +func entryModeToGogitFileMode(mode EntryMode) filemode.FileMode { + return filemode.FileMode(mode) } -// Mode returns the mode of the entry -func (te *TreeEntry) Mode() EntryMode { - return EntryMode(te.gogitTreeEntry.Mode) +func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry { + return &object.TreeEntry{ + Name: te.name, + Mode: entryModeToGogitFileMode(te.entryMode), + Hash: plumbing.Hash(te.ID.RawValue()), + } } // Size returns the size of the entry @@ -41,7 +37,11 @@ func (te *TreeEntry) Size() int64 { return te.size } - file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry) + ptreeGogitTree, err := te.ptree.gogitTreeObject() + if err != nil { + return 0 + } + file, err := ptreeGogitTree.TreeEntryFile(te.toGogitTreeEntry()) if err != nil { return 0 } @@ -51,40 +51,15 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a submodule -func (te *TreeEntry) IsSubModule() bool { - return te.gogitTreeEntry.Mode == filemode.Submodule -} - -// IsDir if the entry is a sub dir -func (te *TreeEntry) IsDir() bool { - return te.gogitTreeEntry.Mode == filemode.Dir -} - -// IsLink if the entry is a symlink -func (te *TreeEntry) IsLink() bool { - return te.gogitTreeEntry.Mode == filemode.Symlink -} - -// IsRegular if the entry is a regular file -func (te *TreeEntry) IsRegular() bool { - return te.gogitTreeEntry.Mode == filemode.Regular -} - -// IsExecutable if the entry is an executable file (not necessarily binary) -func (te *TreeEntry) IsExecutable() bool { - return te.gogitTreeEntry.Mode == filemode.Executable -} - // Blob returns the blob object the entry func (te *TreeEntry) Blob() *Blob { - encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash) + encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.toGogitTreeEntry().Hash) if err != nil { return nil } return &Blob{ - ID: ParseGogitHash(te.gogitTreeEntry.Hash), + ID: te.ID, gogitEncodedObj: encodedObj, name: te.Name(), } diff --git a/modules/git/tree_entry_gogit_test.go b/modules/git/tree_entry_gogit_test.go new file mode 100644 index 0000000000..ed14b45e9e --- /dev/null +++ b/modules/git/tree_entry_gogit_test.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build gogit + +package git + +import ( + "testing" + + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/stretchr/testify/assert" +) + +func TestEntryGogit(t *testing.T) { + cases := map[EntryMode]filemode.FileMode{ + EntryModeBlob: filemode.Regular, + EntryModeCommit: filemode.Submodule, + EntryModeExec: filemode.Executable, + EntryModeSymlink: filemode.Symlink, + EntryModeTree: filemode.Dir, + } + for emode, fmode := range cases { + assert.EqualValues(t, fmode, entryModeToGogitFileMode(emode)) + assert.EqualValues(t, emode, gogitFileModeToEntryMode(fmode)) + } +} diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 8fad96cdf8..fd2f3c567f 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -7,27 +7,6 @@ package git import "code.gitea.io/gitea/modules/log" -// TreeEntry the leaf in the git tree -type TreeEntry struct { - ID ObjectID - ptree *Tree - - entryMode EntryMode - name string - size int64 - sized bool -} - -// Name returns the name of the entry (base name) -func (te *TreeEntry) Name() string { - return te.name -} - -// Mode returns the mode of the entry -func (te *TreeEntry) Mode() EntryMode { - return te.entryMode -} - // Size returns the size of the entry func (te *TreeEntry) Size() int64 { if te.IsDir() { @@ -57,31 +36,6 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a submodule -func (te *TreeEntry) IsSubModule() bool { - return te.entryMode.IsSubModule() -} - -// IsDir if the entry is a sub dir -func (te *TreeEntry) IsDir() bool { - return te.entryMode.IsDir() -} - -// IsLink if the entry is a symlink -func (te *TreeEntry) IsLink() bool { - return te.entryMode.IsLink() -} - -// IsRegular if the entry is a regular file -func (te *TreeEntry) IsRegular() bool { - return te.entryMode.IsRegular() -} - -// IsExecutable if the entry is an executable file (not necessarily binary) -func (te *TreeEntry) IsExecutable() bool { - return te.entryMode.IsExecutable() -} - // Blob returns the blob object the entry func (te *TreeEntry) Blob() *Blob { return &Blob{ diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index 9ca82675e0..b28abfb545 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -1,55 +1,29 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build gogit - package git import ( + "math/rand/v2" + "slices" + "strings" "testing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" ) -func getTestEntries() Entries { - return Entries{ - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}}, - } -} - -func TestEntriesSort(t *testing.T) { - entries := getTestEntries() - entries.Sort() - assert.Equal(t, "v1.0", entries[0].Name()) - assert.Equal(t, "v12.0", entries[1].Name()) - assert.Equal(t, "v2.0", entries[2].Name()) - assert.Equal(t, "v2.1", entries[3].Name()) - assert.Equal(t, "v2.12", entries[4].Name()) - assert.Equal(t, "v2.2", entries[5].Name()) - assert.Equal(t, "abc", entries[6].Name()) - assert.Equal(t, "bcd", entries[7].Name()) -} - func TestEntriesCustomSort(t *testing.T) { - entries := getTestEntries() - entries.CustomSort(func(s1, s2 string) bool { - return s1 > s2 - }) - assert.Equal(t, "v2.2", entries[0].Name()) - assert.Equal(t, "v2.12", entries[1].Name()) - assert.Equal(t, "v2.1", entries[2].Name()) - assert.Equal(t, "v2.0", entries[3].Name()) - assert.Equal(t, "v12.0", entries[4].Name()) - assert.Equal(t, "v1.0", entries[5].Name()) - assert.Equal(t, "bcd", entries[6].Name()) - assert.Equal(t, "abc", entries[7].Name()) + entries := Entries{ + &TreeEntry{name: "a-dir", entryMode: EntryModeTree}, + &TreeEntry{name: "a-submodule", entryMode: EntryModeCommit}, + &TreeEntry{name: "b-dir", entryMode: EntryModeTree}, + &TreeEntry{name: "b-submodule", entryMode: EntryModeCommit}, + &TreeEntry{name: "a-file", entryMode: EntryModeBlob}, + &TreeEntry{name: "b-file", entryMode: EntryModeBlob}, + } + expected := slices.Clone(entries) + rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] }) + assert.NotEqual(t, expected, entries) + entries.CustomSort(strings.Compare) + assert.Equal(t, expected, entries) } diff --git a/modules/git/tree_gogit.go b/modules/git/tree_gogit.go index 272b018ffd..fec6e2704e 100644 --- a/modules/git/tree_gogit.go +++ b/modules/git/tree_gogit.go @@ -15,41 +15,34 @@ import ( // Tree represents a flat directory listing. type Tree struct { - ID ObjectID - ResolvedID ObjectID - repo *Repository + TreeCommon - gogitTree *object.Tree - - // parent tree - ptree *Tree + resolvedGogitTreeObject *object.Tree } -func (t *Tree) loadTreeObject() error { - gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) - if err != nil { - return err - } - - t.gogitTree = gogitTree - return nil -} - -// ListEntries returns all entries of current tree. -func (t *Tree) ListEntries() (Entries, error) { - if t.gogitTree == nil { - err := t.loadTreeObject() +func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) { + if t.resolvedGogitTreeObject == nil { + t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) if err != nil { return nil, err } } + return t.resolvedGogitTreeObject, nil +} - entries := make([]*TreeEntry, len(t.gogitTree.Entries)) - for i, entry := range t.gogitTree.Entries { +// ListEntries returns all entries of current tree. +func (t *Tree) ListEntries() (Entries, error) { + gogitTree, err := t.gogitTreeObject() + if err != nil { + return nil, err + } + entries := make([]*TreeEntry, len(gogitTree.Entries)) + for i, gogitTreeEntry := range gogitTree.Entries { entries[i] = &TreeEntry{ - ID: ParseGogitHash(entry.Hash), - gogitTreeEntry: &t.gogitTree.Entries[i], - ptree: t, + ID: ParseGogitHash(gogitTreeEntry.Hash), + ptree: t, + name: gogitTreeEntry.Name, + entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode), } } @@ -57,37 +50,28 @@ func (t *Tree) ListEntries() (Entries, error) { } // ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees -func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { - if t.gogitTree == nil { - err := t.loadTreeObject() - if err != nil { - return nil, err - } +func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) { + gogitTree, err := t.gogitTreeObject() + if err != nil { + return nil, err } - var entries []*TreeEntry - seen := map[plumbing.Hash]bool{} - walker := object.NewTreeWalker(t.gogitTree, true, seen) + walker := object.NewTreeWalker(gogitTree, true, nil) for { - _, entry, err := walker.Next() + fullName, gogitTreeEntry, err := walker.Next() if err == io.EOF { break - } - if err != nil { + } else if err != nil { return nil, err } - if seen[entry.Hash] { - continue - } - convertedEntry := &TreeEntry{ - ID: ParseGogitHash(entry.Hash), - gogitTreeEntry: &entry, - ptree: t, + ID: ParseGogitHash(gogitTreeEntry.Hash), + name: fullName, // FIXME: the "name" field is abused, here it is a full path + ptree: t, // FIXME: this ptree is not right, fortunately it isn't really used + entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode), } entries = append(entries, convertedEntry) } - return entries, nil } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 956a5938f0..d0ddb1d041 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -14,18 +14,10 @@ import ( // Tree represents a flat directory listing. type Tree struct { - ID ObjectID - ResolvedID ObjectID - repo *Repository - - // parent tree - ptree *Tree + TreeCommon entries Entries entriesParsed bool - - entriesRecursive Entries - entriesRecursiveParsed bool } // ListEntries returns all entries of current tree. @@ -94,10 +86,6 @@ func (t *Tree) ListEntries() (Entries, error) { // listEntriesRecursive returns all entries of current tree recursively including all subtrees // extraArgs could be "-l" to get the size, which is slower func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) { - if t.entriesRecursiveParsed { - return t.entriesRecursive, nil - } - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r"). AddArguments(extraArgs...). AddDynamicArguments(t.ID.String()). @@ -107,13 +95,9 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e return nil, runErr } - var err error - t.entriesRecursive, err = parseTreeEntries(stdout, t) - if err == nil { - t.entriesRecursiveParsed = true - } - - return t.entriesRecursive, err + // FIXME: the "name" field is abused, here it is a full path + // FIXME: this ptree is not right, fortunately it isn't really used + return parseTreeEntries(stdout, t) } // ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 0383e4ca9e..6bb9a8ae77 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -415,6 +415,8 @@ func Diff(ctx *context.Context) { ctx.ServerError("PostProcessCommitMessage", err) return } + } else if !git.IsErrNotExist(err) { + log.Error("GetNote: %v", err) } pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID) diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index 340b2bc091..8a3ed0a1c9 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -33,7 +33,7 @@ func TreeList(ctx *context.Context) { ctx.ServerError("ListEntriesRecursiveFast", err) return } - entries.CustomSort(base.NaturalSortLess) + entries.CustomSort(base.NaturalSortCompare) files := make([]string, 0, len(entries)) for _, entry := range entries { diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 1d05a3aa51..79357bfd76 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -307,7 +307,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri ctx.ServerError("ListEntries", err) return nil } - allEntries.CustomSort(base.NaturalSortLess) + allEntries.CustomSort(base.NaturalSortCompare) commitInfoCtx := gocontext.Context(ctx) if timeout > 0 { diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index edf38b7892..f1fa5732f0 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -67,7 +67,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []* for _, entry := range entries { if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { fullPath := path.Join(parentDir, entry.Name()) - if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { + if readmeFiles[i] == nil || base.NaturalSortCompare(readmeFiles[i].Name(), entry.Blob().Name()) < 0 { if entry.IsLink() { res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry) if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 289db11a4f..e7c34ba1d6 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -567,7 +567,7 @@ func WikiPages(ctx *context.Context) { ctx.ServerError("ListEntries", err) return } - allEntries.CustomSort(base.NaturalSortLess) + allEntries.CustomSort(base.NaturalSortCompare) entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath) if err != nil { From 84d7496b9d3280072d838a7b5ddc00b1fa86ee09 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 5 Nov 2025 19:20:20 +0100 Subject: [PATCH 03/20] Remove `fix` Make targets (#35868) Since `modernize` is now included in `golangci-lint` since https://github.com/go-gitea/gitea/commit/850012bf5c0807908771d3cb155afaebf2742cc8, it makes not sense to have this as a separate make target anymore. --- Makefile | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Makefile b/Makefile index ffa7471aa0..9e7a4ed166 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,6 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0 -GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -276,19 +275,6 @@ fmt-check: fmt exit 1; \ fi -.PHONY: fix -fix: ## apply automated fixes to Go code - $(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./... - -.PHONY: fix-check -fix-check: fix - @diff=$$(git diff --color=always $(GO_SOURCES)); \ - if [ -n "$$diff" ]; then \ - echo "Please run 'make fix' and commit the result:"; \ - printf "%s" "$${diff}"; \ - exit 1; \ - fi - .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @mkdir -p $(MAKE_EVIDENCE_DIR) @@ -328,7 +314,7 @@ checks: checks-frontend checks-backend ## run various consistency checks checks-frontend: lockfile-check svg-check ## check frontend files .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files +checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files .PHONY: lint lint: lint-frontend lint-backend lint-spell ## lint everything @@ -852,7 +838,6 @@ deps-tools: ## install tool dependencies $(GO) install $(GOVULNCHECK_PACKAGE) & \ $(GO) install $(ACTIONLINT_PACKAGE) & \ $(GO) install $(GOPLS_PACKAGE) & \ - $(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \ wait node_modules: pnpm-lock.yaml From 61e5cc173e011b59d6176afb7803e8112081dff1 Mon Sep 17 00:00:00 2001 From: Divyun Raje Vaid Date: Thu, 6 Nov 2025 00:22:24 +0530 Subject: [PATCH 04/20] fix(api/repo/contents): set the dates to now when not specified by the caller (#35861) Since 1.25.0, the dates get set to `2001-01-01T00:00:00Z`, when not specified by the caller. Fixes #35860 Co-authored-by: Giteabot --- routers/api/v1/repo/file.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index ba98263819..ec34d54d22 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -370,11 +370,11 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) { }, Signoff: commonOpts.Signoff, } - if commonOpts.Dates.Author.IsZero() { - commonOpts.Dates.Author = time.Now() + if changeFileOpts.Dates.Author.IsZero() { + changeFileOpts.Dates.Author = time.Now() } - if commonOpts.Dates.Committer.IsZero() { - commonOpts.Dates.Committer = time.Now() + if changeFileOpts.Dates.Committer.IsZero() { + changeFileOpts.Dates.Committer = time.Now() } ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts } From 23a37b4b77ba7ec7f1079fe5b573fe4cc10fb142 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 02:32:39 +0100 Subject: [PATCH 05/20] Remove padding override on `.ui .sha.label` (#35864) Since upgrading to v1.25, I noticed the SHA labels have slightly different padding than before. I can't pinpoint exactly which change it was. Fix it by removing the padding override on `.ui .sha.label` and make the one on`.ui.label` (`2px 6px`) take effect which matches 1.24 rendering. Before: image After: image --- web_src/css/base.css | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index a09839ea1e..be28cd6fea 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -626,7 +626,6 @@ img.ui.avatar, font-family: var(--fonts-monospace); font-size: 13px; font-weight: var(--font-weight-normal); - padding: 3px 5px; flex-shrink: 0; } From aaa8033ee943691ed7a43c61c09781f7acd72b8f Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 07:04:38 +0100 Subject: [PATCH 06/20] Update to go 1.25.4 (#35877) https://tip.golang.org/doc/devel/release#go1.25.4 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 81187804a3..bfdfb06e2a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.25.3 +go 1.25.4 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: From eef9406c6b373afdb701e89fc1f1c1a15b0ced58 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 09:23:48 +0100 Subject: [PATCH 07/20] Contribution heatmap improvements (#35876) 1. Set a fixed height on the element, preventing the content after the element from shifting on page load. This uses CSS [container query length units](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries#container_query_length_units) as I saw no other way because of the non-linear scaling of the element. 2. Move the "total-contributions" text into the existing vue slot, eliminating the need for absolute positioning. --------- Co-authored-by: wxiaoguang --- templates/user/heatmap.tmpl | 4 +- web_src/css/features/heatmap.css | 57 +++++++++++------------ web_src/js/components/ActivityHeatmap.vue | 7 ++- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/templates/user/heatmap.tmpl b/templates/user/heatmap.tmpl index b604b929a3..6186edd4dd 100644 --- a/templates/user/heatmap.tmpl +++ b/templates/user/heatmap.tmpl @@ -1,4 +1,5 @@ {{if .HeatmapData}} +
-
+
+
{{end}} diff --git a/web_src/css/features/heatmap.css b/web_src/css/features/heatmap.css index c064590c46..e40adf1fe4 100644 --- a/web_src/css/features/heatmap.css +++ b/web_src/css/features/heatmap.css @@ -4,23 +4,44 @@ position: relative; } -/* before the Vue component is mounted, show a loading indicator with dummy size */ -/* the ratio is guesswork, see https://github.com/razorness/vue3-calendar-heatmap/issues/26 */ -#user-heatmap.is-loading { - aspect-ratio: 5.415; /* the size is about 790 x 145 */ +.activity-heatmap-container { + container-type: inline-size; } -.user.profile #user-heatmap.is-loading { - aspect-ratio: 5.645; /* the size is about 953 x 169 */ + +@container (width > 0) { + #user-heatmap { + /* Set element to fixed height so that it does not resize after load. The calculation is complex + because the element does not scale with a fixed aspect ratio. */ + height: calc((100cqw / 5) - (100cqw / 25) + 20px); + } +} + +/* Fallback height adjustment above for browsers that don't support container queries */ +@supports not (container-type: inline-size) { + /* Before the Vue component is mounted, show a loading indicator with dummy size */ + /* The ratio is guesswork for legacy browsers, new browsers use the "@container" approach above */ + #user-heatmap.is-loading { + aspect-ratio: 5.4823972051; /* the size is about 816 x 148.84 */ + } + .user.profile #user-heatmap.is-loading { + aspect-ratio: 5.6290608387; /* the size is about 953 x 169.3 */ + } } #user-heatmap text { fill: currentcolor !important; } +/* root legend */ +#user-heatmap .vch__container > .vch__legend { + display: flex; + font-size: 11px; + justify-content: space-between; +} + /* for the "Less" and "More" legend */ #user-heatmap .vch__legend .vch__legend { display: flex; - font-size: 11px; align-items: center; justify-content: right; } @@ -34,25 +55,3 @@ #user-heatmap .vch__day__square:hover { outline: 1.5px solid var(--color-text); } - -/* move the "? contributions in the last ? months" text from top to bottom */ -#user-heatmap .total-contributions { - font-size: 11px; - position: absolute; - bottom: 0; - left: 25px; -} - -@media (max-width: 1200px) { - #user-heatmap .total-contributions { - left: 21px; - } -} - -@media (max-width: 1000px) { - #user-heatmap .total-contributions { - font-size: 10px; - left: 17px; - bottom: -4px; - } -} diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index 296cb61cff..d805817630 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -53,9 +53,6 @@ function handleDayClick(e: Event & {date: Date}) { } From b2feeddf42622388450b04b710f2bf5487f8b950 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 21:09:31 +0100 Subject: [PATCH 08/20] Move `gitea-vet` to use `go tool` (#35878) Add it as a [tool dependency](https://go.dev/doc/modules/managing-dependencies#tools), eliminating the need for `build.go`. --- .github/labeler.yml | 1 - Makefile | 3 +-- build.go | 14 -------------- go.mod | 4 +++- 4 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 build.go diff --git a/.github/labeler.yml b/.github/labeler.yml index 49679d28cf..750f2b2cfb 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -51,7 +51,6 @@ modifies/internal: - ".github/**" - ".gitea/**" - ".devcontainer/**" - - "build.go" - "build/**" - "contrib/**" diff --git a/Makefile b/Makefile index 9e7a4ed166..de2a486ea1 100644 --- a/Makefile +++ b/Makefile @@ -386,8 +386,7 @@ lint-go-windows: .PHONY: lint-go-gitea-vet lint-go-gitea-vet: ## lint go files with gitea-vet @echo "Running gitea-vet..." - @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet - @$(GO) vet -vettool=gitea-vet ./... + @$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./... .PHONY: lint-go-gopls lint-go-gopls: ## lint go files with gopls diff --git a/build.go b/build.go deleted file mode 100644 index e81ba54690..0000000000 --- a/build.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build vendor - -package main - -// Libraries that are included to vendor utilities used during Makefile build. -// These libraries will not be included in a normal compilation. - -import ( - // for vet - _ "code.gitea.io/gitea-vet" -) diff --git a/go.mod b/go.mod index bfdfb06e2a..2be7558e55 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ godebug x509negativeserial=1 require ( code.gitea.io/actions-proto-go v0.4.1 - code.gitea.io/gitea-vet v0.2.3 code.gitea.io/sdk/gitea v0.22.0 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 connectrpc.com/connect v1.18.1 @@ -135,6 +134,7 @@ require ( require ( cloud.google.com/go/compute/metadata v0.8.0 // indirect + code.gitea.io/gitea-vet v0.2.3 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect @@ -307,3 +307,5 @@ exclude github.com/gofrs/uuid v4.0.0+incompatible exclude github.com/goccy/go-json v0.4.11 exclude github.com/satori/go.uuid v1.2.0 + +tool code.gitea.io/gitea-vet From 0ce7d66368128aea9976b9f326fc82f3f0b2fc60 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 7 Nov 2025 09:44:09 +0800 Subject: [PATCH 09/20] Fix avatar upload error handling (#35887) Fix #35884 --- services/auth/source/ldap/source_authenticate.go | 4 +--- services/repository/avatar.go | 8 ++++---- services/user/avatar.go | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 6005a4744a..4463bcc054 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -105,9 +105,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u } } if source.AttributeAvatar != "" { - if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil { - return user, err - } + _ = user_service.UploadAvatar(ctx, user, sr.Avatar) } } diff --git a/services/repository/avatar.go b/services/repository/avatar.go index 79da629aa6..7ab6badfc3 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -21,7 +21,7 @@ import ( func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error { avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { - return err + return fmt.Errorf("UploadAvatar: failed to process repo avatar image: %w", err) } newAvatar := avatar.HashAvatar(repo.ID, data) @@ -36,19 +36,19 @@ func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) // Then repo will be removed - only it avatar file will be removed repo.Avatar = newAvatar if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "avatar"); err != nil { - return fmt.Errorf("UploadAvatar: Update repository avatar: %w", err) + return fmt.Errorf("UploadAvatar: failed to update repository avatar: %w", err) } if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error { _, err := w.Write(avatarData) return err }); err != nil { - return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %w", repo.RelativePath(), newAvatar, err) + return fmt.Errorf("UploadAvatar: failed to save repo avatar %s: %w", newAvatar, err) } if len(oldAvatarPath) > 0 { if err := storage.RepoAvatars.Delete(oldAvatarPath); err != nil { - return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %w", oldAvatarPath, err) + return fmt.Errorf("UploadAvatar: failed to remove old repo avatar %s: %w", oldAvatarPath, err) } } return nil diff --git a/services/user/avatar.go b/services/user/avatar.go index df188e5adc..6a43681e9e 100644 --- a/services/user/avatar.go +++ b/services/user/avatar.go @@ -21,21 +21,21 @@ import ( func UploadAvatar(ctx context.Context, u *user_model.User, data []byte) error { avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { - return err + return fmt.Errorf("UploadAvatar: failed to process user avatar image: %w", err) } return db.WithTx(ctx, func(ctx context.Context) error { u.UseCustomAvatar = true u.Avatar = avatar.HashAvatar(u.ID, data) if err = user_model.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil { - return fmt.Errorf("updateUser: %w", err) + return fmt.Errorf("UploadAvatar: failed to update user avatar: %w", err) } if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { _, err := w.Write(avatarData) return err }); err != nil { - return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) + return fmt.Errorf("UploadAvatar: failed to save user avatar %s: %w", u.CustomAvatarRelativePath(), err) } return nil From bfaddbcd0df38fb7bc23b573475b78e6356e786e Mon Sep 17 00:00:00 2001 From: Luohao Wang Date: Sat, 8 Nov 2025 23:29:17 +0800 Subject: [PATCH 10/20] Fix conda null depend issue (#35900) Fix #35895 --- routers/api/packages/conda/conda.go | 2 +- tests/integration/api_packages_conda_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index f496002bb5..8519ae3e08 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -148,7 +148,7 @@ func EnumeratePackages(ctx *context.Context) { Timestamp: fileMetadata.Timestamp, Build: fileMetadata.Build, BuildNumber: fileMetadata.BuildNumber, - Dependencies: fileMetadata.Dependencies, + Dependencies: util.SliceNilAsEmpty(fileMetadata.Dependencies), License: versionMetadata.License, LicenseFamily: versionMetadata.LicenseFamily, HashMD5: pfd.Blob.HashMD5, diff --git a/tests/integration/api_packages_conda_test.go b/tests/integration/api_packages_conda_test.go index b69a8c9066..8dbcba5b54 100644 --- a/tests/integration/api_packages_conda_test.go +++ b/tests/integration/api_packages_conda_test.go @@ -237,6 +237,8 @@ func TestPackageConda(t *testing.T) { assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5) assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256) assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size) + assert.NotNil(t, packageInfo.Dependencies) + assert.Empty(t, packageInfo.Dependencies) }) t.Run(".conda", func(t *testing.T) { @@ -268,6 +270,8 @@ func TestPackageConda(t *testing.T) { assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5) assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256) assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size) + assert.NotNil(t, packageInfo.Dependencies) + assert.Empty(t, packageInfo.Dependencies) }) }) } From 367a289b290ec02731a96f227631a41543527f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=B1=80?= <131967983+lutinglt@users.noreply.github.com> Date: Sun, 9 Nov 2025 00:08:59 +0800 Subject: [PATCH 11/20] Display source code downloads last for release attachments (#35897) --- templates/repo/release/list.tmpl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 882ffe40b7..b7a60a44ed 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -78,18 +78,6 @@ {{ctx.Locale.Tr "repo.release.downloads"}} From c12bc4aa3081b7f990bf557282cf4443bc2efd5a Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 8 Nov 2025 20:48:16 +0100 Subject: [PATCH 12/20] Add toolchain directive to go.mod (#35901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From [docs](https://go.dev/doc/toolchain#config): > The go line declares the minimum required Go version for using the module or workspace. For compatibility reasons, if the go line is omitted from a go.mod file, the module is considered to have an implicit go 1.16 line, and if the go line is omitted from a go.work file, the workspace is considered to have an implicit go 1.18 line. > The toolchain line declares a suggested toolchain to use with the module or workspace. As described in “[Go toolchain selection](https://go.dev/doc/toolchain#select)” below, the go command may run this specific toolchain when operating in that module or workspace if the default toolchain’s version is less than the suggested toolchain’s version. If the toolchain line is omitted, the module or workspace is considered to have an implicit toolchain goV line, where V is the Go version from the go line. This is better than setting `go` to the latest version which may break builds when that go version is unavailable, for example with `GOTOOLCHAIN=local` in the official go docker images. --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2be7558e55..7d787cf11b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module code.gitea.io/gitea -go 1.25.4 +go 1.25.0 + +toolchain go1.25.4 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: From 919348665b434c5da6a716655fbda7cac0534960 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sat, 8 Nov 2025 15:23:55 -0500 Subject: [PATCH 13/20] Add ability for local makefile with personal customizations that wouldnt affect remote repo (#35836) This would allow developers to keep a local file that'd add personal makefile targets for niche convenience customization without having to have the git workspace polluted with uncommitted changes. --------- Signed-off-by: techknowlogick --- .gitattributes | 1 + .gitignore | 3 +++ Makefile | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/.gitattributes b/.gitattributes index e218bbe25d..afd02555f5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,4 @@ /vendor/** -text -eol linguist-vendored /web_src/js/vendor/** -text -eol linguist-vendored Dockerfile.* linguist-language=Dockerfile +Makefile.* linguist-language=Makefile diff --git a/.gitignore b/.gitignore index 7e8e5f84a7..11af4543bd 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ prime/ # Ignore worktrees when working on multiple branches .worktrees/ + +# A Makefile for custom make targets +Makefile.local diff --git a/Makefile b/Makefile index de2a486ea1..5dbf141723 100644 --- a/Makefile +++ b/Makefile @@ -198,6 +198,10 @@ TEST_MSSQL_DBNAME ?= gitea TEST_MSSQL_USERNAME ?= sa TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 +# Include local Makefile +# Makefile.local is listed in .gitignore +sinclude Makefile.local + .PHONY: all all: build From 050c9485dface2ea5d16343d8b46052c2f4be7fb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 9 Nov 2025 11:13:31 +0800 Subject: [PATCH 14/20] Fix team member access check (#35899) Fix #35499 --- models/git/protected_branch.go | 8 ++- models/organization/team_repo.go | 39 ++++++++--- models/perm/access/repo_permission.go | 71 +++++++++----------- models/perm/access/repo_permission_test.go | 16 ++++- routers/web/repo/setting/protected_branch.go | 5 +- routers/web/repo/setting/protected_tag.go | 4 +- services/convert/convert.go | 4 +- 7 files changed, 87 insertions(+), 60 deletions(-) diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 13e1ced0e1..1085c14cae 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -466,11 +466,13 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c return currentWhitelist, nil } + prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests) + if err != nil { + return nil, err + } whitelist = make([]int64, 0, len(newWhitelist)) for _, userID := range newWhitelist { - if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil { - return nil, err - } else if !reader { + if !prUserIDs.Contains(userID) { continue } whitelist = append(whitelist, userID) diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index b3e266dbc7..2652b34c6f 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -53,24 +53,45 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error { // GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit. // This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control. // FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details -func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) { - teams := make([]*Team, 0, 5) +func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) { + teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...) + if err != nil { + return nil, err + } + if len(teamIDs) == 0 { + return teams, nil + } + err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams) + return teams, err +} +func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) { sub := builder.Select("team_id").From("team_unit"). Where(builder.Expr("team_unit.team_id = team.id")). And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))). And(builder.Expr("team_unit.access_mode >= ?", mode)) - err := db.GetEngine(ctx). + err = db.GetEngine(ctx). + Select("team.id"). + Table("team"). Join("INNER", "team_repo", "team_repo.team_id = team.id"). - And("team_repo.org_id = ?", orgID). - And("team_repo.repo_id = ?", repoID). + And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID). And(builder.Or( builder.Expr("team.authorize >= ?", mode), builder.In("team.id", sub), )). - OrderBy("name"). - Find(&teams) - - return teams, err + Find(&teamIDs) + return teamIDs, err +} + +func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) { + teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...) + if err != nil { + return nil, err + } + if len(teamIDs) == 0 { + return userIDs, nil + } + err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs) + return userIDs, err } diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 034557db33..15526cb1e6 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -16,6 +16,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -498,54 +499,44 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi return perm.HasAnyUnitAccess(), nil } -// getUsersWithAccessMode returns users that have at least given access mode to the repository. -func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) { - if err = repo.LoadOwner(ctx); err != nil { +func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) { + userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType) + if err != nil { return nil, err } - - e := db.GetEngine(ctx) - accesses := make([]*Access, 0, 10) - if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil { + if len(userIDs) == 0 { + return users, nil + } + if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil { return nil, err } - - // Leave a seat for owner itself to append later, but if owner is an organization - // and just waste 1 unit is cheaper than re-allocate memory once. - users := make([]*user_model.User, 0, len(accesses)+1) - if len(accesses) > 0 { - userIDs := make([]int64, len(accesses)) - for i := 0; i < len(accesses); i++ { - userIDs[i] = accesses[i].UserID - } - - if err = e.In("id", userIDs).Find(&users); err != nil { - return nil, err - } - } - if !repo.Owner.IsOrganization() { - users = append(users, repo.Owner) - } - return users, nil } -// GetRepoReaders returns all users that have explicit read access or higher to the repository. -func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) { - return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead) -} - -// GetRepoWriters returns all users that have write access to the repository. -func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) { - return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite) -} - -// IsRepoReader returns true if user has explicit read access or higher to the repository. -func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) { - if repo.OwnerID == userID { - return true, nil +func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) { + userIDs := container.Set[int64]{} + e := db.GetEngine(ctx) + accesses := make([]*Access, 0, 10) + if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil { + return nil, err } - return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{}) + for _, a := range accesses { + userIDs.Add(a.UserID) + } + + if err := repo.LoadOwner(ctx); err != nil { + return nil, err + } + if !repo.Owner.IsOrganization() { + userIDs.Add(repo.Owner.ID) + } else { + teamUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, mode, unitType) + if err != nil { + return nil, err + } + userIDs.AddMultiple(teamUserIDs...) + } + return userIDs, nil } // CheckRepoUnitUser check whether user could visit the unit of this repository diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index d81dfba288..a36be213ec 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) team := &organization.Team{OrgID: org.ID, LowerName: "test_team"} require.NoError(t, db.Insert(ctx, team)) + require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) t.Run("DoerInTeamWithNoRepo", func(t *testing.T) { - require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) perm, err := GetUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) @@ -219,6 +219,15 @@ func TestGetUserRepoPermission(t *testing.T) { assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode]) assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues]) + + users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeRead, unit.TypeIssues) + require.NoError(t, err) + require.Len(t, users, 1) + assert.Equal(t, user.ID, users[0].ID) + + users, err = GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues) + require.NoError(t, err) + require.Empty(t, users) }) require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite})) @@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) { assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues]) + + users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues) + require.NoError(t, err) + require.Len(t, users, 1) + assert.Equal(t, user.ID, users[0].ID) }) } diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index c7bde087a2..4374e95340 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -73,10 +73,9 @@ func SettingsProtectedBranch(c *context.Context) { c.Data["PageIsSettingsBranches"] = true c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName - - users, err := access_model.GetRepoReaders(c, c.Repo.Repository) + users, err := access_model.GetUsersWithUnitAccess(c, c.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests) if err != nil { - c.ServerError("Repo.Repository.GetReaders", err) + c.ServerError("GetUsersWithUnitAccess", err) return } c.Data["Users"] = users diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go index 50f5a28c4c..4b560e6f22 100644 --- a/routers/web/repo/setting/protected_tag.go +++ b/routers/web/repo/setting/protected_tag.go @@ -149,9 +149,9 @@ func setTagsContext(ctx *context.Context) error { } ctx.Data["ProtectedTags"] = protectedTags - users, err := access_model.GetRepoReaders(ctx, ctx.Repo.Repository) + users, err := access_model.GetUsersWithUnitAccess(ctx, ctx.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests) if err != nil { - ctx.ServerError("Repo.Repository.GetReaders", err) + ctx.ServerError("GetUsersWithUnitAccess", err) return err } ctx.Data["Users"] = users diff --git a/services/convert/convert.go b/services/convert/convert.go index 9f8fff970c..8b10d93640 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -139,7 +139,7 @@ func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T, // ToBranchProtection convert a ProtectedBranch to api.BranchProtection func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection { - readers, err := access_model.GetRepoReaders(ctx, repo) + readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests) if err != nil { log.Error("GetRepoReaders: %v", err) } @@ -720,7 +720,7 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api. // ToTagProtection convert a git.ProtectedTag to an api.TagProtection func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection { - readers, err := access_model.GetRepoReaders(ctx, repo) + readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests) if err != nil { log.Error("GetRepoReaders: %v", err) } From c4c4cf5687ce6813996070b80ec7609a7cce26ec Mon Sep 17 00:00:00 2001 From: Alberty Pascal Date: Sun, 9 Nov 2025 22:23:46 +0100 Subject: [PATCH 15/20] Use correct form field for allowed force push users in branch protection API (#35894) Test was wrong and preventing update of force push allow users list by the API Resolves #35893 Signed-off-by: Alberty Pascal --- routers/api/v1/repo/branch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index a337ed7938..b9060e9cbd 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -897,7 +897,7 @@ func EditBranchProtection(ctx *context.APIContext) { } else { whitelistUsers = protectBranch.WhitelistUserIDs } - if form.ForcePushAllowlistDeployKeys != nil { + if form.ForcePushAllowlistUsernames != nil { forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { From 60314cb6889b44bba4e86c6676d70c90556b0462 Mon Sep 17 00:00:00 2001 From: Mithilesh Gupta Date: Mon, 10 Nov 2025 03:24:34 +0530 Subject: [PATCH 16/20] Add proper page title for project pages (#35773) Fix #35763 Co-authored-by: Mithilesh Gupta --- routers/web/org/projects.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 059cce8281..d524409c41 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -436,6 +436,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = columns + ctx.Data["Title"] = fmt.Sprintf("%s - %s", project.Title, ctx.ContextUser.DisplayName()) if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { ctx.ServerError("RenderUserOrgHeader", err) From 1c8c56503f99365a93fbbb653343f464643ac9e3 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 10 Nov 2025 13:31:25 +0800 Subject: [PATCH 17/20] Allow to display embed images/pdfs when SERVE_DIRECT was enabled on MinIO storage (#35882) Releated issue: https://github.com/go-gitea/gitea/issues/30487 --------- Co-authored-by: wxiaoguang --- modules/storage/azureblob.go | 1 + modules/storage/minio.go | 34 +++++++++++++++++++++++++++++----- routers/web/repo/view.go | 1 + 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go index 6860d81131..e7297cec77 100644 --- a/modules/storage/azureblob.go +++ b/modules/storage/azureblob.go @@ -250,6 +250,7 @@ func (a *AzureBlobStorage) Delete(path string) error { func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { blobClient := a.getBlobClient(path) + // TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage startTime := time.Now() u, err := blobClient.GetSASURL(sas.BlobPermissions{ Read: true, diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 01f2c16267..6993ac2d92 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -279,20 +279,44 @@ func (m *MinioStorage) Delete(path string) error { } // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { +func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { // copy serveDirectReqParams reqParams, err := url.ParseQuery(serveDirectReqParams.Encode()) if err != nil { return nil, err } - // TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we? - reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"") + + // Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head. + // So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI. + // Detect content type by extension name, only support the well-known safe types for inline rendering. + // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future + ext := path.Ext(name) + inlineExtMimeTypes := map[string]string{ + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".avif": "image/avif", + // ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline + ".pdf": "application/pdf", + + // TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType" + } + if mimeType, ok := inlineExtMimeTypes[ext]; ok { + reqParams.Set("response-content-type", mimeType) + reqParams.Set("response-content-disposition", "inline") + } else { + reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name))) + } + expires := 5 * time.Minute if method == http.MethodHead { - u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams) + u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams) return u, convertMinioErr(err) } - u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams) + u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams) return u, convertMinioErr(err) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 79357bfd76..09ac33cff4 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -95,6 +95,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid) if err != nil { // fallback to a plain file + fi.lfsMeta = &pointer log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err) return buf, dataRc, fi, nil } From e31f224ad2af66a24d87bf9224a00932f164a632 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 10 Nov 2025 23:45:01 +0800 Subject: [PATCH 18/20] Make OAuth2 issuer configurable (#35915) The new (correct) behavior breaks the old (incorrect) logins. Add a config option to support legacy "issuer". Fix #35830 --- custom/conf/app.example.ini | 5 ++++ modules/setting/oauth2.go | 1 + services/oauth2_provider/access_token.go | 6 +++- tests/integration/oauth_test.go | 36 ++++++++++++++++-------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5fee78af54..33bfe752a0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -567,6 +567,11 @@ ENABLED = true ;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one ;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret ;; +;; The "issuer" claim identifies the principal that issued the JWT. +;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard. +;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash". +;JWT_CLAIM_ISSUER = +;; ;; Lifetime of an OAuth2 access token in seconds ;ACCESS_TOKEN_EXPIRATION_TIME = 3600 ;; diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 1a88f3cb08..ae2a9d7bee 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -96,6 +96,7 @@ var OAuth2 = struct { InvalidateRefreshTokens bool JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` + JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"` MaxTokenLength int DefaultApplications []string }{ diff --git a/services/oauth2_provider/access_token.go b/services/oauth2_provider/access_token.go index dce4ac765b..3a77c86d9e 100644 --- a/services/oauth2_provider/access_token.go +++ b/services/oauth2_provider/access_token.go @@ -112,8 +112,12 @@ func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt // to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer. // * https://accounts.google.com/.well-known/openid-configuration // * https://github.com/login/oauth/.well-known/openid-configuration + issuer := setting.OAuth2.JWTClaimIssuer + if issuer == "" { + issuer = strings.TrimSuffix(setting.AppURL, "/") + } return jwt.RegisteredClaims{ - Issuer: strings.TrimSuffix(setting.AppURL, "/"), + Issuer: issuer, Audience: []string{clientID}, Subject: strconv.FormatInt(grantUserID, 10), ExpiresAt: exp, diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index eab95ba688..e7edace653 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -919,20 +919,32 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) { } func testOAuth2WellKnown(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() urlOpenidConfiguration := "/.well-known/openid-configuration" - defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() - req := NewRequest(t, "GET", urlOpenidConfiguration) - resp := MakeRequest(t, req, http.StatusOK) - var respMap map[string]any - DecodeJSON(t, resp, &respMap) - assert.Equal(t, "https://try.gitea.io", respMap["issuer"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"]) - assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"]) + t.Run("WellKnown", func(t *testing.T) { + req := NewRequest(t, "GET", urlOpenidConfiguration) + resp := MakeRequest(t, req, http.StatusOK) + var respMap map[string]any + DecodeJSON(t, resp, &respMap) + assert.Equal(t, "https://try.gitea.io", respMap["issuer"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"]) + assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"]) + }) + + t.Run("WellKnownWithIssuer", func(t *testing.T) { + defer test.MockVariableValue(&setting.OAuth2.JWTClaimIssuer, "https://try.gitea.io/")() + req := NewRequest(t, "GET", urlOpenidConfiguration) + resp := MakeRequest(t, req, http.StatusOK) + var respMap map[string]any + DecodeJSON(t, resp, &respMap) + assert.Equal(t, "https://try.gitea.io/", respMap["issuer"]) // has trailing by JWTClaimIssuer + assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) + }) defer test.MockVariableValue(&setting.OAuth2.Enabled, false)() MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound) From 9affb513a8fbbf323266531e95855ad1fad663e0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 12 Nov 2025 00:11:46 +0800 Subject: [PATCH 19/20] Load jQuery as early as possible to support custom scripts (#35926) Fix #35923 --- web_src/js/index-domready.ts | 1 - web_src/js/index.ts | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index ecd8cb1baa..df56c85c86 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -1,4 +1,3 @@ -import './globals.ts'; import '../fomantic/build/fomantic.js'; import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in "switchToEasyMDE" diff --git a/web_src/js/index.ts b/web_src/js/index.ts index af53cc488c..153b8049c9 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -1,5 +1,10 @@ // bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors import './bootstrap.ts'; + +// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) +// so load globals (including jQuery) as early as possible +import './globals.ts'; + import './webcomponents/index.ts'; import {onDomReady} from './utils/dom.ts'; From 2223be2cc455d678ba45a64758108c1508f7a7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=B1=80?= <131967983+lutinglt@users.noreply.github.com> Date: Wed, 12 Nov 2025 02:21:15 +0800 Subject: [PATCH 20/20] Support blue yellow colorblind theme (#35910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This icon is from GitHub: image --------- Signed-off-by: 鲁汀 <131967983+lutinglt@users.noreply.github.com> Co-authored-by: lutinglt --- .../img/svg/gitea-colorblind-blueyellow.svg | 1 + services/webtheme/webtheme.go | 6 ++++++ .../css/themes/theme-gitea-auto-tritanopia.css | 8 ++++++++ .../css/themes/theme-gitea-dark-tritanopia.css | 15 +++++++++++++++ .../css/themes/theme-gitea-light-tritanopia.css | 15 +++++++++++++++ web_src/svg/gitea-colorblind-blueyellow.svg | 13 +++++++++++++ 6 files changed, 58 insertions(+) create mode 100644 public/assets/img/svg/gitea-colorblind-blueyellow.svg create mode 100644 web_src/css/themes/theme-gitea-auto-tritanopia.css create mode 100644 web_src/css/themes/theme-gitea-dark-tritanopia.css create mode 100644 web_src/css/themes/theme-gitea-light-tritanopia.css create mode 100644 web_src/svg/gitea-colorblind-blueyellow.svg diff --git a/public/assets/img/svg/gitea-colorblind-blueyellow.svg b/public/assets/img/svg/gitea-colorblind-blueyellow.svg new file mode 100644 index 0000000000..63a101b50d --- /dev/null +++ b/public/assets/img/svg/gitea-colorblind-blueyellow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index 72f01a76c7..57d63f4e07 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -39,6 +39,9 @@ func (info *ThemeMetaInfo) GetDescription() string { if info.ColorblindType == "red-green" { return "Red-green colorblind friendly" } + if info.ColorblindType == "blue-yellow" { + return "Blue-yellow colorblind friendly" + } return "" } @@ -46,6 +49,9 @@ func (info *ThemeMetaInfo) GetExtraIconName() string { if info.ColorblindType == "red-green" { return "gitea-colorblind-redgreen" } + if info.ColorblindType == "blue-yellow" { + return "gitea-colorblind-blueyellow" + } return "" } diff --git a/web_src/css/themes/theme-gitea-auto-tritanopia.css b/web_src/css/themes/theme-gitea-auto-tritanopia.css new file mode 100644 index 0000000000..178a23983b --- /dev/null +++ b/web_src/css/themes/theme-gitea-auto-tritanopia.css @@ -0,0 +1,8 @@ +@import "./theme-gitea-light-tritanopia.css" (prefers-color-scheme: light); +@import "./theme-gitea-dark-tritanopia.css" (prefers-color-scheme: dark); + +gitea-theme-meta-info { + --theme-display-name: "Auto"; + --theme-colorblind-type: "blue-yellow"; + --theme-color-scheme: "auto"; +} diff --git a/web_src/css/themes/theme-gitea-dark-tritanopia.css b/web_src/css/themes/theme-gitea-dark-tritanopia.css new file mode 100644 index 0000000000..1dbd9967dd --- /dev/null +++ b/web_src/css/themes/theme-gitea-dark-tritanopia.css @@ -0,0 +1,15 @@ +@import "./theme-gitea-dark-protanopia-deuteranopia.css"; + +gitea-theme-meta-info { + --theme-display-name: "Dark"; + --theme-colorblind-type: "blue-yellow"; + --theme-color-scheme: "dark"; +} + +/* blue/yellow colorblind-friendly colors */ +/* from GitHub: blue yellow blindness is based on red green blindness, and --diffBlob-deletion-* restored to the normal theme color */ +:root { + --color-diff-removed-linenum-bg: #482121; + --color-diff-removed-row-bg: #301e1e; + --color-diff-removed-word-bg: #6f3333; +} diff --git a/web_src/css/themes/theme-gitea-light-tritanopia.css b/web_src/css/themes/theme-gitea-light-tritanopia.css new file mode 100644 index 0000000000..a50fd9c1a4 --- /dev/null +++ b/web_src/css/themes/theme-gitea-light-tritanopia.css @@ -0,0 +1,15 @@ +@import "./theme-gitea-light-protanopia-deuteranopia.css"; + +gitea-theme-meta-info { + --theme-display-name: "Light"; + --theme-colorblind-type: "blue-yellow"; + --theme-color-scheme: "light"; +} + +/* blue/yellow colorblind-friendly colors */ +/* from GitHub: blue yellow blindness is based on red green blindness, and --diffBlob-deletion-* restored to the normal theme color */ +:root { + --color-diff-removed-linenum-bg: #ffcecb; + --color-diff-removed-row-bg: #ffeef0; + --color-diff-removed-word-bg: #fdb8c0; +} diff --git a/web_src/svg/gitea-colorblind-blueyellow.svg b/web_src/svg/gitea-colorblind-blueyellow.svg new file mode 100644 index 0000000000..752ff88432 --- /dev/null +++ b/web_src/svg/gitea-colorblind-blueyellow.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file