Compare commits

...

17 Commits

Author SHA1 Message Date
Lunny Xiao
136ec9ef81 Add changelog for 1.24.5 (#35261) 2025-08-13 08:58:05 -07:00
Giteabot
79018ae726 modules/setting/actions.go: fixed typo: ì->i (#35253) (#35254)
Backport #35253 by @TimB87

Hello,

I spotted this minor typo.

Co-authored-by: Tim Biermann <tbier@posteo.de>
2025-08-12 11:21:56 +00:00
Giteabot
e11176192a Fix a bug where lfs gc never worked. (#35198) (#35255)
Backport #35198 by @lunny

Fix #31113

After #22385 introduced LFS GC, it never worked due to a bug in the INI
library: fields in structs embedded more than one level deep are not
populated from the INI file.

This PR fixes the issue by replacing the multi-level embedded struct
with a single-level struct for parsing the cron.gc_lfs configuration.

Added a new test for retrieving cron settings to demonstrate the bug in
the INI package.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-12 10:57:50 +03:00
Giteabot
4e0269e890 Reload issue when sending webhook to make num comments is right. (#35243) (#35248)
Backport #35243 by @lunny

Fix #35229

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 10:12:47 -07:00
Lunny Xiao
04114c637a Fix bug when review pull request commits (#35192) (#35246)
The commit range in the UI follows a half-open, half-closed convention:
(,]. When reviewing a range of commits, the beforeCommitID should be set
to the commit immediately preceding the first selected commit. For
single-commit reviews, we must identify and use the previous commit of
that specific commit.

The endpoint ViewPullFilesStartingFromCommit is currently unused and can
be safely removed.

Fix #35157
Replace #35184
Partially extract from #35077
Backport #35192
2025-08-11 13:18:03 +02:00
6543
e5540bfa81 Nix flake build static with sqlite support (#35149) (#35225)
Backport #35149

with `nix develop -c $SHELL` you can enter the dev environment. now with
`make clean-all generate build -j1` you will get a static linked binary
that has sqlite support

outside of an nix dev shell if you set `STATIC=true` you also will get a
static binary
2025-08-07 23:37:46 +03:00
Giteabot
d22d6ca0d8 Vertically center "Show Resolved" (#35211) (#35218)
Backport #35211 by @silverwind

Before, "Show Resolved" slightly off-center:

<img width="174" height="60" alt="Screenshot 2025-08-04 at 15 07 13"
src="https://github.com/user-attachments/assets/a165f721-4749-4ea3-bde0-141ad43a6142"
/>

After: centered:

<img width="176" height="63" alt="Screenshot 2025-08-04 at 15 07 22"
src="https://github.com/user-attachments/assets/f87e16bc-d067-4040-b940-9adb4cf182c1"
/>

Co-authored-by: silverwind <me@silverwind.io>
2025-08-06 10:55:48 +02:00
Lunny Xiao
d49feab428 release notes for 1.24.4 (#35208) 2025-08-04 11:13:27 -07:00
wxiaoguang
9162f4403a Fix various bugs (1.24) (#35186)
Backport #35177, #35183
2025-07-31 12:04:47 +08:00
Giteabot
d05cf08fad Fix migrate input box bug (#35166) (#35171)
Backport #35166 by @lunny

Fix #35162

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-07-27 19:57:47 -07:00
Giteabot
f4b4b0bf98 Only hide dropzone when no files have been uploaded (#35156) (#35167)
Backport #35156 by @bartvdbraak

Instead of always hiding the dropzone when it's not active:
- hide it when when there are no uploaded files and it becomes inactive 
- don't hide it when there are uploaded files

Fixes #35125

Co-authored-by: Bart van der Braak <bartvdbraak@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-27 11:20:48 +03:00
wxiaoguang
99596044d7 Don't use full-file highlight when there is a git diff textconv (#35114) (#35119)
Fix #35106
2025-07-18 13:52:41 +00:00
Giteabot
693d26914f Fix submodule parsing when the gitmodules is missing (#35109) (#35118)
Backport #35109

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-18 20:22:18 +08:00
Giteabot
315f197790 Increase gap on latest commit (#35104) (#35113)
Backport #35104 by @silverwind

Before:

<img width="964" height="101" alt="Screenshot 2025-07-17 at 02 31 05"
src="https://github.com/user-attachments/assets/e02181f3-f730-40bb-8cfa-ecfea4ed4aec"
/>

After:

<img width="967" height="104" alt="Screenshot 2025-07-17 at 02 42 13"
src="https://github.com/user-attachments/assets/7ca7b9a8-1f59-4dc0-9bb0-c72346fd792a"
/>

Co-authored-by: silverwind <me@silverwind.io>
2025-07-18 08:25:51 +03:00
Giteabot
76b8f0c3a7 Fix review comment/dimiss comment x reference can be refereced back (#35094) (#35099) 2025-07-17 00:04:05 +08:00
Giteabot
f99bbd7f3f Fix submodule nil check (#35096) (#35098)
Backport #35096 by wxiaoguang

Fix  #35095

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-16 13:02:18 +00:00
Giteabot
f7ef657b5a nix flake update (#35085) (#35090)
Backport #35085 by @techknowlogick

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
2025-07-15 14:37:44 -07:00
41 changed files with 489 additions and 259 deletions

View File

@@ -4,7 +4,28 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.24.3](https://github.com/go-gitea/gitea/releases/tag/1.24.3) - 2025-07-15
## [1.24.5](https://github.com/go-gitea/gitea/releases/tag/v1.24.5) - 2025-08-12
* BUGFIXES
* Fix a bug where lfs gc never worked. (#35198) (#35255)
* Reload issue when sending webhook to make num comments is right. (#35243) (#35248)
* Fix bug when review pull request commits (#35192) (#35246)
* MISC
* Vertically center "Show Resolved" (#35211) (#35218)
## [1.24.4](https://github.com/go-gitea/gitea/releases/tag/v1.24.4) - 2025-08-03
* BUGFIXES
* Fix various bugs (1.24) (#35186)
* Fix migrate input box bug (#35166) (#35171)
* Only hide dropzone when no files have been uploaded (#35156) (#35167)
* Fix review comment/dimiss comment x reference can be refereced back (#35094) (#35099)
* Fix submodule nil check (#35096) (#35098)
* MISC
* Don't use full-file highlight when there is a git diff textconv (#35114) (#35119)
* Increase gap on latest commit (#35104) (#35113)
## [1.24.3](https://github.com/go-gitea/gitea/releases/tag/v1.24.3) - 2025-07-15
* BUGFIXES
* Fix form property assignment edge case (#35073) (#35078)
@@ -36,7 +57,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Skip updating timestamp when sync branch (#34875)
* Fix required contexts and commit status matching bug (#34815) (#34829)
## [1.24.2](https://github.com/go-gitea/gitea/releases/tag/1.24.2) - 2025-06-20
## [1.24.2](https://github.com/go-gitea/gitea/releases/tag/v1.24.2) - 2025-06-20
* BUGFIXES
* Fix container range bug (#34795) (#34796)
@@ -44,7 +65,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* BUILD
* Bump poetry feature to new url for dev container (#34787) (#34790)
## [1.24.1](https://github.com/go-gitea/gitea/releases/tag/1.24.1) - 2025-06-18
## [1.24.1](https://github.com/go-gitea/gitea/releases/tag/v1.24.1) - 2025-06-18
* ENHANCEMENTS
* Improve alignment of commit status icon on commit page (#34750) (#34757)
@@ -64,7 +85,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Hide href attribute of a tag if there is no target_url (#34556) (#34684)
* Fix tag target (#34781) #34783
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/v1.24.0) - 2025-05-26
* BREAKING
* Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
@@ -434,7 +455,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Bump x/net (#32896) (#32900)
* Only activity tab needs heatmap data loading (#34652)
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/v1.23.8) - 2025-05-11
* SECURITY
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
@@ -461,7 +482,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Bump go version in go.mod (#34160)
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/v1.23.7) - 2025-04-07
* Enhancements
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
@@ -559,7 +580,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* BUGFIXES
* Fix a bug caused by status webhook template #33512
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/v1.23.2) - 2025-02-04
* BREAKING
* Add tests for webhook and fix some webhook bugs (#33396) (#33442)
@@ -3089,7 +3110,7 @@ Key highlights of this release encompass significant changes categorized under `
* Improve decryption failure message (#24573) (#24575)
* Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/1.19.3) - 2023-05-03
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/v1.19.3) - 2023-05-03
* SECURITY
* Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
@@ -3102,7 +3123,7 @@ Key highlights of this release encompass significant changes categorized under `
* Fix incorrect CurrentUser check for docker rootless (#24435)
* Getting the tag list does not require being signed in (#24413) (#24416)
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/1.19.2) - 2023-04-26
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/v1.19.2) - 2023-04-26
* SECURITY
* Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
@@ -3601,7 +3622,7 @@ Key highlights of this release encompass significant changes categorized under `
* Display attachments of review comment when comment content is blank (#23035) (#23046)
* Return empty url for submodule tree entries (#23043) (#23048)
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/v1.18.4) - 2023-02-20
* SECURITY
* Provide the ability to set password hash algorithm parameters (#22942) (#22943)
@@ -4028,7 +4049,7 @@ Key highlights of this release encompass significant changes categorized under `
* Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
* Fix UI mis-align for PR commit history (#20845) (#20859)
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/v1.17.1) - 2022-08-17
* SECURITY
* Correctly escape within tribute.js (#20831) (#20832)

View File

@@ -47,6 +47,17 @@ ifeq ($(HAS_GO), yes)
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
endif
CGO_ENABLED ?= 0
ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS)))
CGO_ENABLED = 1
endif
STATIC ?=
EXTLDFLAGS ?=
ifneq ($(STATIC),)
EXTLDFLAGS = -extldflags "-static"
endif
ifeq ($(GOOS),windows)
IS_WINDOWS := yes
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
@@ -740,7 +751,10 @@ security-check:
go run $(GOVULNCHECK_PACKAGE) -show color ./...
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
$(error pam support set via TAGS doesn't support static builds)
endif
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
.PHONY: release
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"lastModified": 1752480373,
"narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
"type": "github"
},
"original": {

View File

@@ -11,33 +11,45 @@
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# generic
git
git-lfs
gnumake
gnused
gnutar
gzip
devShells.default =
with pkgs;
let
# only bump toolchain versions here
go = go_1_24;
nodejs = nodejs_24;
python3 = python312;
in
pkgs.mkShell {
buildInputs = [
# generic
git
git-lfs
gnumake
gnused
gnutar
gzip
# frontend
nodejs_22
# frontend
nodejs
# linting
python312
poetry
# linting
python3
poetry
# backend
go_1_24
gofumpt
sqlite
];
shellHook = ''
export GO="${pkgs.go_1_24}/bin/go"
export GOROOT="${pkgs.go_1_24}/share/go"
'';
};
# backend
go
glibc.static
gofumpt
sqlite
];
CFLAGS = "-I${glibc.static.dev}/include";
LDFLAGS = "-L ${glibc.static}/lib";
GO = "${go}/bin/go";
GOROOT = "${go}/share/go";
TAGS = "sqlite sqlite_unlock_notify";
STATIC = "true";
};
}
);
}

View File

@@ -235,7 +235,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
// AddCrossReferences add cross references
func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
if c.Type != CommentTypeCode && c.Type != CommentTypeComment {
if !c.Type.HasContentSupport() {
return nil
}
if err := c.LoadIssue(stdCtx); err != nil {

View File

@@ -20,6 +20,7 @@ const (
GitlabLanguage = "gitlab-language"
Lockable = "lockable"
Filter = "filter"
Diff = "diff"
)
var LinguistAttributes = []string{

View File

@@ -3,8 +3,6 @@
package git
import "path"
// CommitInfo describes the first commit with the provided entry
type CommitInfo struct {
Entry *TreeEntry
@@ -12,11 +10,14 @@ type CommitInfo struct {
SubmoduleFile *CommitSubmoduleFile
}
func getCommitInfoSubmoduleFile(repoLink string, entry *TreeEntry, commit *Commit, treePathDir string) (*CommitSubmoduleFile, error) {
fullPath := path.Join(treePathDir, entry.Name())
func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
submodule, err := commit.GetSubModule(fullPath)
if err != nil {
return nil, err
}
return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, entry.ID.String()), nil
if submodule == nil {
// unable to find submodule from ".gitmodules" file
return NewCommitSubmoduleFile(repoLink, fullPath, "", refCommitID.String()), nil
}
return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, refCommitID.String()), nil
}

View File

@@ -73,7 +73,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
// If the entry is a submodule, add a submodule file for this
if entry.IsSubModule() {
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
if err != nil {
return nil, nil, err
}

View File

@@ -67,7 +67,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
// If the entry is a submodule, add a submodule file for this
if entry.IsSubModule() {
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
if err != nil {
return nil, nil, err
}

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
@@ -120,6 +121,23 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
defer clonedRepo1.Close()
testGetCommitsInfo(t, clonedRepo1)
t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
commit, err := bareRepo1.GetCommit("HEAD")
require.NoError(t, err)
treeEntry, err := commit.GetTreeEntryByPath("file1.txt")
require.NoError(t, err)
cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID)
require.NoError(t, err)
assert.Equal(t, &CommitSubmoduleFile{
repoLink: "/any/repo-link",
fullPath: "file1.txt",
refURL: "",
refID: "e2129701f1a4d54dc44f03c93bca0a2aec7c5449",
}, cisf)
// since there is no refURL, it means that the submodule info doesn't exist, so it won't have a web link
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
})
}
func BenchmarkEntries_GetCommitsInfo(b *testing.B) {

View File

@@ -35,7 +35,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
return c.submoduleCache, nil
}
// GetSubModule get the submodule according entry name
// GetSubModule gets the submodule by the entry name.
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
modules, err := c.GetSubModules()
if err != nil {

View File

@@ -29,16 +29,20 @@ func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSub
return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID}
}
// RefID returns the commit ID of the submodule, it returns empty string for nil receiver
func (sf *CommitSubmoduleFile) RefID() string {
if sf == nil {
return ""
}
return sf.refID
}
func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink {
if sf == nil {
if sf == nil || sf.refURL == "" {
return nil
}
if strings.HasPrefix(sf.refURL, "../") {
targetLink := path.Join(sf.repoLink, path.Dir(sf.fullPath), sf.refURL)
targetLink := path.Join(sf.repoLink, sf.refURL)
return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
}
if !sf.parsed {
@@ -53,14 +57,13 @@ func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreL
}
// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver
// It returns nil if the submodule does not have a valid URL or is nil
func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
if sf == nil {
return nil
}
return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.refID))
return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.RefID()))
}
// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver
// It returns nil if the submodule does not have a valid URL or is nil
func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink {
return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2)
}

View File

@@ -12,6 +12,8 @@ import (
func TestCommitSubmoduleLink(t *testing.T) {
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context()))
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", ""))
assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkTree(t.Context()))
assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkCompare(t.Context(), "", ""))
t.Run("GitHubRepo", func(t *testing.T) {
sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa")
@@ -30,7 +32,7 @@ func TestCommitSubmoduleLink(t *testing.T) {
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink)
sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../../user/repo", "aaaa")
sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../user/repo", "aaaa")
wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)

View File

@@ -11,6 +11,8 @@ import (
"strconv"
"strings"
"sync"
"code.gitea.io/gitea/modules/util"
)
// ObjectCache provides thread-safe cache operations.
@@ -106,3 +108,16 @@ func HashFilePathForWebUI(s string) string {
_, _ = h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}
func SplitCommitTitleBody(commitMessage string, titleRuneLimit int) (title, body string) {
title, body, _ = strings.Cut(commitMessage, "\n")
title, title2 := util.EllipsisTruncateRunes(title, titleRuneLimit)
if title2 != "" {
if body == "" {
body = title2
} else {
body = title2 + "\n" + body
}
}
return title, body
}

View File

@@ -15,3 +15,17 @@ func TestHashFilePathForWebUI(t *testing.T) {
HashFilePathForWebUI("foobar"),
)
}
func TestSplitCommitTitleBody(t *testing.T) {
title, body := SplitCommitTitleBody("啊bcdefg", 4)
assert.Equal(t, "啊…", title)
assert.Equal(t, "…bcdefg", body)
title, body = SplitCommitTitleBody("abcdefg\n1234567", 4)
assert.Equal(t, "a…", title)
assert.Equal(t, "…bcdefg\n1234567", body)
title, body = SplitCommitTitleBody("abcdefg\n1234567", 100)
assert.Equal(t, "abcdefg", title)
assert.Equal(t, "1234567", body)
}

View File

@@ -24,7 +24,7 @@ var (
ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"`
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
SkipWorkflowStrings []string `ìni:"SKIP_WORKFLOW_STRINGS"`
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
}{
Enabled: true,
DefaultActionsURL: defaultActionsURLGitHub,

View File

@@ -41,3 +41,56 @@ EXTEND = true
assert.Equal(t, "white rabbit", extended.Second)
assert.True(t, extended.Extend)
}
// Test_getCronSettings2 tests that getCronSettings can not handle two levels of embedding
func Test_getCronSettings2(t *testing.T) {
type BaseStruct struct {
Enabled bool
RunAtStart bool
Schedule string
}
type Extended struct {
BaseStruct
Extend bool
}
type Extended2 struct {
Extended
Third string
}
iniStr := `
[cron.test]
ENABLED = TRUE
RUN_AT_START = TRUE
SCHEDULE = @every 1h
EXTEND = true
THIRD = white rabbit
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
extended := &Extended2{
Extended: Extended{
BaseStruct: BaseStruct{
Enabled: false,
RunAtStart: false,
Schedule: "@every 72h",
},
Extend: false,
},
Third: "black rabbit",
}
_, err = getCronSettings(cfg, "test", extended)
assert.NoError(t, err)
// This confirms the first level of embedding works
assert.Equal(t, "white rabbit", extended.Third)
assert.True(t, extended.Extend)
// This confirms 2 levels of embedding doesn't work
assert.False(t, extended.Enabled)
assert.False(t, extended.RunAtStart)
assert.Equal(t, "@every 72h", extended.Schedule)
}

View File

@@ -19,7 +19,7 @@ func IsLikelyEllipsisLeftPart(s string) bool {
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
}
func ellipsisGuessDisplayWidth(r rune) int {
func ellipsisDisplayGuessWidth(r rune) int {
// To make the truncated string as long as possible,
// CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
// Here we only make the best guess (better than counting them in bytes),
@@ -48,13 +48,17 @@ func ellipsisGuessDisplayWidth(r rune) int {
// It appends "…" or "..." at the end of truncated string.
// It guarantees the length of the returned runes doesn't exceed the limit.
func EllipsisDisplayString(str string, limit int) string {
s, _, _, _ := ellipsisDisplayString(str, limit)
s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
return s
}
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
func EllipsisDisplayStringX(str string, limit int) (left, right string) {
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit)
return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
}
func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
if truncated {
right = str[offset:]
r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
@@ -68,7 +72,7 @@ func EllipsisDisplayStringX(str string, limit int) (left, right string) {
return left, right
}
func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) {
func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
if len(str) <= limit {
return str, len(str), false, false
}
@@ -81,7 +85,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
for i, r := range str {
encounterInvalid = encounterInvalid || r == utf8.RuneError
pos = i
runeWidth := ellipsisGuessDisplayWidth(r)
runeWidth := widthGuess(r)
if used+runeWidth+3 > limit {
break
}
@@ -96,7 +100,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
if nextCnt >= 4 {
break
}
nextWidth += ellipsisGuessDisplayWidth(r)
nextWidth += widthGuess(r)
nextCnt++
}
if nextCnt <= 3 && used+nextWidth <= limit {
@@ -114,6 +118,10 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
return str[:offset] + ellipsis, offset, true, encounterInvalid
}
func EllipsisTruncateRunes(str string, limit int) (left, right string) {
return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
}
// TruncateRunes returns a truncated string with given rune limit,
// it returns input string if its rune length doesn't exceed the limit.
func TruncateRunes(str string, limit int) string {

View File

@@ -29,7 +29,7 @@ func TestEllipsisGuessDisplayWidth(t *testing.T) {
t.Run(c.r, func(t *testing.T) {
w := 0
for _, r := range c.r {
w += ellipsisGuessDisplayWidth(r)
w += ellipsisDisplayGuessWidth(r)
}
assert.Equal(t, c.want, w, "hex=% x", []byte(c.r))
})

View File

@@ -8,6 +8,7 @@ import (
"time"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
@@ -15,10 +16,14 @@ import (
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "")
if err != nil {
ctx.ServerError("ShowBranchFeed", err)
return
var commits []*git.Commit
var err error
if ctx.Repo.Commit != nil {
commits, err = ctx.Repo.Commit.CommitsByRange(0, 10, "")
if err != nil {
ctx.ServerError("ShowBranchFeed", err)
return
}
}
title := "Latest commits for branch " + ctx.Repo.BranchName

View File

@@ -643,8 +643,17 @@ func ViewPullCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplPullCommits)
}
func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
for i := range commits {
if commits[i].ID.String() == commitID {
return commits[i]
}
}
return nil
}
// ViewPullFiles render pull request changed files list page
func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommit string, willShowSpecifiedCommitRange, willShowSpecifiedCommit bool) {
func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
ctx.Data["PageIsPullList"] = true
ctx.Data["PageIsPullFiles"] = true
@@ -654,11 +663,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
pull := issue.PullRequest
var (
startCommitID string
endCommitID string
gitRepo = ctx.Repo.GitRepo
)
gitRepo := ctx.Repo.GitRepo
prInfo := preparePullViewPullInfo(ctx, issue)
if ctx.Written() {
@@ -668,77 +673,68 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
return
}
// Validate the given commit sha to show (if any passed)
if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
foundStartCommit := len(specifiedStartCommit) == 0
foundEndCommit := len(specifiedEndCommit) == 0
if !(foundStartCommit && foundEndCommit) {
for _, commit := range prInfo.Commits {
if commit.ID.String() == specifiedStartCommit {
foundStartCommit = true
}
if commit.ID.String() == specifiedEndCommit {
foundEndCommit = true
}
if foundStartCommit && foundEndCommit {
break
}
}
}
if !(foundStartCommit && foundEndCommit) {
ctx.NotFound(nil)
return
}
}
if ctx.Written() {
return
}
headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
}
ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit
isSingleCommit := beforeCommitID == "" && afterCommitID != ""
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID)
ctx.Data["IsShowingAllCommits"] = isShowAllCommits
if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
if len(specifiedEndCommit) > 0 {
endCommitID = specifiedEndCommit
if afterCommitID == "" || afterCommitID == headCommitID {
afterCommitID = headCommitID
}
afterCommit := indexCommit(prInfo.Commits, afterCommitID)
if afterCommit == nil {
ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits")
return
}
var beforeCommit *git.Commit
if !isSingleCommit {
if beforeCommitID == "" || beforeCommitID == prInfo.MergeBase {
beforeCommitID = prInfo.MergeBase
// mergebase commit is not in the list of the pull request commits
beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
} else {
endCommitID = headCommitID
beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
if beforeCommit == nil {
ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits")
return
}
}
if len(specifiedStartCommit) > 0 {
startCommitID = specifiedStartCommit
} else {
startCommitID = prInfo.MergeBase
}
ctx.Data["IsShowingAllCommits"] = false
} else {
endCommitID = headCommitID
startCommitID = prInfo.MergeBase
ctx.Data["IsShowingAllCommits"] = true
beforeCommit, err = afterCommit.Parent(0)
if err != nil {
ctx.ServerError("Parent", err)
return
}
beforeCommitID = beforeCommit.ID.String()
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["AfterCommitID"] = endCommitID
ctx.Data["BeforeCommitID"] = startCommitID
fileOnly := ctx.FormBool("file-only")
ctx.Data["MergeBase"] = prInfo.MergeBase
ctx.Data["AfterCommitID"] = afterCommitID
ctx.Data["BeforeCommitID"] = beforeCommitID
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
files := ctx.FormStrings("files")
fileOnly := ctx.FormBool("file-only")
if fileOnly && (len(files) == 2 || len(files) == 1) {
maxLines, maxFiles = -1, -1
}
diffOptions := &gitdiff.DiffOptions{
AfterCommitID: endCommitID,
BeforeCommitID: beforeCommitID,
AfterCommitID: afterCommitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
@@ -746,10 +742,6 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
}
if !willShowSpecifiedCommit {
diffOptions.BeforeCommitID = startCommitID
}
diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, diffOptions, files...)
if err != nil {
ctx.ServerError("GetDiff", err)
@@ -761,7 +753,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
// as the viewed information is designed to be loaded only on latest PR
// diff and if you're signed in.
var reviewState *pull_model.ReviewState
if ctx.IsSigned && !willShowSpecifiedCommit && !willShowSpecifiedCommitRange {
if ctx.IsSigned && isShowAllCommits {
reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions)
if err != nil {
ctx.ServerError("SyncUserSpecificDiff", err)
@@ -769,7 +761,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, startCommitID, endCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return
@@ -816,7 +808,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if !fileOnly {
// note: use mergeBase is set to false because we already have the merge base from the pull request info
diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, startCommitID, endCommitID)
diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, beforeCommitID, afterCommitID)
if err != nil {
ctx.ServerError("GetDiffTree", err)
return
@@ -836,17 +828,6 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
commit, err := gitRepo.GetCommit(endCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
if ctx.IsSigned && ctx.Doer != nil {
if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err)
@@ -854,7 +835,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
}
setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
setCompareContext(ctx, beforeCommit, afterCommit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
@@ -901,7 +882,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
if !willShowSpecifiedCommit && !willShowSpecifiedCommitRange && pull.Flow == issues_model.PullRequestFlowGithub {
if isShowAllCommits && pull.Flow == issues_model.PullRequestFlowGithub {
if err := pull.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err)
return
@@ -930,19 +911,17 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
func ViewPullFilesForSingleCommit(ctx *context.Context) {
viewPullFiles(ctx, "", ctx.PathParam("sha"), true, true)
// it doesn't support showing files from mergebase to the special commit
// otherwise it will be ambiguous
viewPullFiles(ctx, "", ctx.PathParam("sha"))
}
func ViewPullFilesForRange(ctx *context.Context) {
viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"), true, false)
}
func ViewPullFilesStartingFromCommit(ctx *context.Context) {
viewPullFiles(ctx, "", ctx.PathParam("sha"), true, false)
viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"))
}
func ViewPullFilesForAllCommitsOfPr(ctx *context.Context) {
viewPullFiles(ctx, "", "", false, false)
viewPullFiles(ctx, "", "")
}
// UpdatePullRequest merge PR's baseBranch into headBranch

View File

@@ -20,8 +20,8 @@ import (
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -309,34 +309,41 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
ctx.Redirect(link)
}
func handleRepoViewSubmodule(ctx *context.Context, submodule *git.SubModule) {
submoduleRepoURL, err := giturl.ParseRepositoryURL(ctx, submodule.URL)
if err != nil {
HandleGitError(ctx, "prepareToRenderDirOrFile: ParseRepositoryURL", err)
func isViewHomeOnlyContent(ctx *context.Context) bool {
return ctx.FormBool("only_content")
}
func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.CommitSubmoduleFile) {
submoduleWebLink := commitSubmoduleFile.SubmoduleWebLinkTree(ctx)
if submoduleWebLink == nil {
ctx.Data["NotFoundPrompt"] = ctx.Repo.TreePath
ctx.NotFound(nil)
return
}
submoduleURL := giturl.MakeRepositoryWebLink(submoduleRepoURL)
if httplib.IsCurrentGiteaSiteURL(ctx, submoduleURL) {
ctx.RedirectToCurrentSite(submoduleURL)
} else {
redirectLink := submoduleWebLink.CommitWebLink
if isViewHomeOnlyContent(ctx) {
ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = ctx.Resp.Write([]byte(htmlutil.HTMLFormat(`<a href="%s">%s</a>`, redirectLink, redirectLink)))
} else if !httplib.IsCurrentGiteaSiteURL(ctx, redirectLink) {
// don't auto-redirect to external URL, to avoid open redirect or phishing
ctx.Data["NotFoundPrompt"] = submoduleURL
ctx.Data["NotFoundPrompt"] = redirectLink
ctx.NotFound(nil)
} else {
ctx.Redirect(submoduleWebLink.CommitWebLink)
}
}
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
return func(ctx *context.Context) {
if entry.IsSubModule() {
submodule, err := ctx.Repo.Commit.GetSubModule(entry.Name())
commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
if err != nil {
HandleGitError(ctx, "prepareToRenderDirOrFile: GetSubModule", err)
HandleGitError(ctx, "prepareToRenderDirOrFile: GetCommitInfoSubmoduleFile", err)
return
}
handleRepoViewSubmodule(ctx, submodule)
return
}
if entry.IsDir() {
handleRepoViewSubmodule(ctx, commitSubmoduleFile)
} else if entry.IsDir() {
prepareToRenderDirectory(ctx)
} else {
prepareToRenderFile(ctx, entry)
@@ -472,7 +479,7 @@ func Home(ctx *context.Context) {
}
}
if ctx.FormBool("only_content") {
if isViewHomeOnlyContent(ctx) {
ctx.HTML(http.StatusOK, tplRepoViewContent)
} else if len(treeNames) != 0 {
ctx.HTML(http.StatusOK, tplRepoView)

View File

@@ -9,7 +9,6 @@ import (
"code.gitea.io/gitea/models/unittest"
git_module "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
@@ -19,14 +18,20 @@ func TestViewHomeSubmoduleRedirect(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
submodule := &git_module.SubModule{Path: "test-submodule", URL: setting.AppURL + "user2/repo-other.git"}
submodule := git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
handleRepoViewSubmodule(ctx, submodule)
assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Equal(t, "/user2/repo-other", ctx.Resp.Header().Get("Location"))
assert.Equal(t, "/user2/repo-other/tree/any-ref-id", ctx.Resp.Header().Get("Location"))
ctx, _ = contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
submodule = &git_module.SubModule{Path: "test-submodule", URL: "https://other/user2/repo-other.git"}
submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "https://other/user2/repo-other.git", "any-ref-id")
handleRepoViewSubmodule(ctx, submodule)
// do not auto-redirect for external URLs, to avoid open redirect or phishing
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
ctx, respWriter := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule?only_content=true")
submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
handleRepoViewSubmodule(ctx, submodule)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.Equal(t, `<a href="/user2/repo-other/tree/any-ref-id">/user2/repo-other/tree/any-ref-id</a>`, respWriter.Body.String())
}

View File

@@ -1509,7 +1509,7 @@ func registerWebRoutes(m *web.Router) {
m.Group("/commits", func() {
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
m.Get("/list", repo.GetPullCommits)
m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
m.Get("/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
})
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
@@ -1518,8 +1518,7 @@ func registerWebRoutes(m *web.Router) {
m.Post("/cleanup", context.RepoMustNotBeArchived(), repo.CleanUpPullRequest)
m.Group("/files", func() {
m.Get("", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit)
m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Get("/{shaFrom:[a-f0-9]{7,64}}..{shaTo:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Group("/reviews", func() {
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)

View File

@@ -260,11 +260,6 @@ func (n *actionsNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
ctx = withMethod(ctx, "UpdateComment")
if err := c.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return
}
if c.Issue.IsPull {
notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited)
return
@@ -275,11 +270,6 @@ func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.Us
func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) {
ctx = withMethod(ctx, "DeleteComment")
if err := comment.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return
}
if comment.Issue.IsPull {
notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted)
return
@@ -288,6 +278,7 @@ func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
}
func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) {
comment.Issue = nil // force issue to be loaded
if err := comment.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return

View File

@@ -171,34 +171,35 @@ func registerDeleteOldSystemNotices() {
})
}
type GCLFSConfig struct {
BaseConfig
OlderThan time.Duration
LastUpdatedMoreThanAgo time.Duration
NumberToCheckPerRepo int64
ProportionToCheckPerRepo float64
}
func registerGCLFS() {
if !setting.LFS.StartServer {
return
}
type GCLFSConfig struct {
OlderThanConfig
LastUpdatedMoreThanAgo time.Duration
NumberToCheckPerRepo int64
ProportionToCheckPerRepo float64
}
RegisterTaskFatal("gc_lfs", &GCLFSConfig{
OlderThanConfig: OlderThanConfig{
BaseConfig: BaseConfig{
Enabled: false,
RunAtStart: false,
Schedule: "@every 24h",
},
// Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
// and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
// an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
// changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
// objects.
//
// It is likely that a week is potentially excessive but it should definitely be enough that any
// unassociated LFS object is genuinely unassociated.
OlderThan: 24 * time.Hour * 7,
BaseConfig: BaseConfig{
Enabled: false,
RunAtStart: false,
Schedule: "@every 24h",
},
// Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
// and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
// an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
// changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
// objects.
//
// It is likely that a week is potentially excessive but it should definitely be enough that any
// unassociated LFS object is genuinely unassociated.
OlderThan: 24 * time.Hour * 7,
// Only GC things that haven't been looked at in the past 3 days
LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
NumberToCheckPerRepo: 100,

View File

@@ -0,0 +1,51 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cron
import (
"testing"
"time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func Test_GCLFSConfig(t *testing.T) {
cfg, err := setting.NewConfigProviderFromData(`
[cron.gc_lfs]
ENABLED = true
RUN_AT_START = true
SCHEDULE = "@every 2h"
OLDER_THAN = "1h"
LAST_UPDATED_MORE_THAN_AGO = "7h"
NUMBER_TO_CHECK_PER_REPO = 10
PROPORTION_TO_CHECK_PER_REPO = 0.1
`)
assert.NoError(t, err)
defer test.MockVariableValue(&setting.CfgProvider, cfg)()
config := &GCLFSConfig{
BaseConfig: BaseConfig{
Enabled: false,
RunAtStart: false,
Schedule: "@every 24h",
},
OlderThan: 24 * time.Hour * 7,
LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
NumberToCheckPerRepo: 100,
ProportionToCheckPerRepo: 0.6,
}
_, err = setting.GetCronSettings("gc_lfs", config)
assert.NoError(t, err)
assert.True(t, config.Enabled)
assert.True(t, config.RunAtStart)
assert.Equal(t, "@every 2h", config.Schedule)
assert.Equal(t, 1*time.Hour, config.OlderThan)
assert.Equal(t, 7*time.Hour, config.LastUpdatedMoreThanAgo)
assert.Equal(t, int64(10), config.NumberToCheckPerRepo)
assert.InDelta(t, 0.1, config.ProportionToCheckPerRepo, 0.001)
}

View File

@@ -1239,7 +1239,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
return nil, err
}
checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage})
checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage, attribute.Diff})
if err != nil {
return nil, err
}
@@ -1248,6 +1248,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
for _, diffFile := range diff.Files {
isVendored := optional.None[bool]()
isGenerated := optional.None[bool]()
attrDiff := optional.None[string]()
attrs, err := checker.CheckPath(diffFile.Name)
if err == nil {
isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated()
@@ -1255,6 +1256,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
if language.Has() {
diffFile.Language = language.Value()
}
attrDiff = attrs.Get(attribute.Diff).ToString()
}
// Populate Submodule URLs
@@ -1276,7 +1278,8 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
diffFile.Sections = append(diffFile.Sections, tailSection)
}
if !setting.Git.DisableDiffHighlight {
shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
if shouldFullFileHighlight {
if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.String())
}

View File

@@ -80,6 +80,12 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
return nil, err
}
// reload issue to ensure it has the latest data, especially the number of comments
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
return nil, err
}
notify_service.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
return comment, nil

View File

@@ -90,15 +90,8 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
if rangeStart >= len(entries) {
return tree, nil
}
var rangeEnd int
if len(entries) > perPage {
tree.Truncated = true
}
if rangeStart+perPage < len(entries) {
rangeEnd = rangeStart + perPage
} else {
rangeEnd = len(entries)
}
rangeEnd := min(rangeStart+perPage, len(entries))
tree.Truncated = rangeEnd < len(entries)
tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart)
for e := rangeStart; e < rangeEnd; e++ {
i := e - rangeStart
@@ -178,7 +171,9 @@ func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIcon
} else if subModule != nil {
submoduleFile := git.NewCommitSubmoduleFile(repoLink, node.FullPath, subModule.URL, entry.ID.String())
webLink := submoduleFile.SubmoduleWebLinkTree(ctx)
node.SubmoduleURL = webLink.CommitWebLink
if webLink != nil {
node.SubmoduleURL = webLink.CommitWebLink
}
}
}

View File

@@ -402,16 +402,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
}
rel, has := relMap[lowerTag]
parts := strings.SplitN(tag.Message, "\n", 2)
note := ""
if len(parts) > 1 {
note = parts[1]
}
title, note := git.SplitCommitTitleBody(tag.Message, 255)
if !has {
rel = &repo_model.Release{
RepoID: repo.ID,
Title: parts[0],
Title: title,
TagName: tags[i],
LowerTagName: lowerTag,
Target: "",
@@ -430,7 +425,7 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
rel.Sha1 = commit.ID.String()
rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
if rel.IsTag {
rel.Title = parts[0]
rel.Title = title
rel.Note = note
} else {
rel.IsDraft = false

View File

@@ -445,6 +445,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
log.Error("LoadPoster: %v", err)
return
}
comment.Issue = nil // reload issue to ensure it has the latest data, especially the number of comments
if err = comment.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return

View File

@@ -35,7 +35,7 @@
{{template "repo/diff/whitespace_dropdown" .}}
{{template "repo/diff/options_dropdown" .}}
{{if .PageIsPullFiles}}
<div id="diff-commit-select" data-issuelink="{{$.Issue.Link}}" data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-filter_changes_by_commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}">
<div id="diff-commit-select" data-merge-base="{{.MergeBase}}" data-issuelink="{{$.Issue.Link}}" data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-filter_changes_by_commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}">
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}}
<div class="ui jump dropdown tiny basic button custom">
{{svg "octicon-git-commit"}}

View File

@@ -15,7 +15,7 @@
</span>
{{end}}
</div>
<div>
<div class="tw-flex tw-items-center">
{{if or $invalid $resolved}}
<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}tw-hidden{{end}} btn tiny show-outdated">
{{svg "octicon-unfold" 16 "tw-mr-2"}}

View File

@@ -11,7 +11,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIReposGitTrees(t *testing.T) {
@@ -32,13 +36,21 @@ func TestAPIReposGitTrees(t *testing.T) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
// Test a public repo that anyone can GET the tree of
for _, ref := range [...]string{
"master", // Branch
repo1TreeSHA, // Tree SHA
} {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, ref)
MakeRequest(t, req, http.StatusOK)
}
_ = MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/repo1/git/trees/master"), http.StatusOK)
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/repo1/git/trees/62fb502a7172d4453f0322a2cc85bddffa57f07a?per_page=1"), http.StatusOK)
var respGitTree api.GitTreeResponse
DecodeJSON(t, resp, &respGitTree)
assert.True(t, respGitTree.Truncated)
require.Len(t, respGitTree.Entries, 1)
assert.Equal(t, "File-WoW", respGitTree.Entries[0].Path)
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/repo1/git/trees/62fb502a7172d4453f0322a2cc85bddffa57f07a?page=2&per_page=1"), http.StatusOK)
respGitTree = api.GitTreeResponse{}
DecodeJSON(t, resp, &respGitTree)
assert.False(t, respGitTree.Truncated)
require.Len(t, respGitTree.Entries, 1)
assert.Equal(t, "README.md", respGitTree.Entries[0].Path)
// Tests a private repo with no token so will fail
for _, ref := range [...]string{

View File

@@ -75,6 +75,11 @@ func TestEmptyRepoAddFile(t *testing.T) {
req = NewRequest(t, "GET", "/api/v1/repos/user30/empty/raw/main/README.md").AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusNotFound)
// test feed
req = NewRequest(t, "GET", "/user30/empty/rss/branch/main/README.md").AddTokenAuth(token).SetHeader("Accept", "application/rss+xml")
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "</rss>")
// create a new file
req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
resp = session.MakeRequest(t, req, http.StatusOK)

View File

@@ -25,10 +25,6 @@ func TestPullDiff_CommitRangePRDiff(t *testing.T) {
doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files/4ca8bcaf27e28504df7bf996819665986b01c847..23576dd018294e476c06e569b6b0f170d0558705", true, []string{"test2.txt", "test3.txt", "test4.txt"})
}
func TestPullDiff_StartingFromBaseToCommitPRDiff(t *testing.T) {
doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files/c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2", true, []string{"test1.txt", "test2.txt", "test3.txt"})
}
func doTestPRDiff(t *testing.T, prDiffURL string, reviewBtnDisabled bool, expectedFilenames []string) {
defer tests.PrepareTestEnv(t)()

View File

@@ -130,7 +130,7 @@ td .commit-summary {
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
gap: 0.25em;
gap: 0.5em;
}
@media (max-width: 767.98px) {

View File

@@ -32,6 +32,7 @@ export default defineComponent({
locale: {
filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),
} as Record<string, string>,
mergeBase: el.getAttribute('data-merge-base'),
commits: [] as Array<Commit>,
hoverActivated: false,
lastReviewCommitSha: '',
@@ -176,32 +177,38 @@ export default defineComponent({
}
},
/**
* When a commit is clicked with shift this enables the range
* selection. Second click (with shift) defines the end of the
* range. This opens the diff of this range
* Exception: first commit is the first commit of this PR. Then
* the diff from beginning of PR up to the second clicked commit is
* opened
* When a commit is clicked while holding Shift, it enables range selection.
* - The range selection is a half-open, half-closed range, meaning it excludes the start commit but includes the end commit.
* - The start of the commit range is always the previous commit of the first clicked commit.
* - If the first commit in the list is clicked, the mergeBase will be used as the start of the range instead.
* - The second Shift-click defines the end of the range.
* - Once both are selected, the diff view for the selected commit range will open.
*/
commitClickedShift(commit: Commit) {
this.hoverActivated = !this.hoverActivated;
commit.selected = true;
// Second click -> determine our range and open links accordingly
if (!this.hoverActivated) {
// since at least one commit is selected, we can determine the range
// find all selected commits and generate a link
if (this.commits[0].selected) {
// first commit is selected - generate a short url with only target sha
const lastCommitIdx = this.commits.findLastIndex((x) => x.selected);
if (lastCommitIdx === this.commits.length - 1) {
// user selected all commits - just show the normal diff page
window.location.assign(`${this.issueLink}/files${this.queryParams}`);
} else {
window.location.assign(`${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`);
}
const firstSelected = this.commits.findIndex((x) => x.selected);
const lastSelected = this.commits.findLastIndex((x) => x.selected);
let beforeCommitID: string;
if (firstSelected === 0) {
beforeCommitID = this.mergeBase;
} else {
const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id;
const end = this.commits.findLast((x) => x.selected).id;
window.location.assign(`${this.issueLink}/files/${start}..${end}${this.queryParams}`);
beforeCommitID = this.commits[firstSelected - 1].id;
}
const afterCommitID = this.commits[lastSelected].id;
if (firstSelected === lastSelected) {
// if the start and end are the same, we show this single commit
window.location.assign(`${this.issueLink}/commits/${afterCommitID}${this.queryParams}`);
} else if (beforeCommitID === this.mergeBase && afterCommitID === this.commits.at(-1).id) {
// if the first commit is selected and the last commit is selected, we show all commits
window.location.assign(`${this.issueLink}/files${this.queryParams}`);
} else {
window.location.assign(`${this.issueLink}/files/${beforeCommitID}..${afterCommitID}${this.queryParams}`);
}
}
},

View File

@@ -547,7 +547,12 @@ function initIssueTemplateCommentEditors(commentForm: HTMLFormElement) {
// deactivate all markdown editors
showElem(commentForm.querySelectorAll('.combo-editor-dropzone .form-field-real'));
hideElem(commentForm.querySelectorAll('.combo-editor-dropzone .combo-markdown-editor'));
hideElem(commentForm.querySelectorAll('.combo-editor-dropzone .form-field-dropzone'));
queryElems(commentForm, '.combo-editor-dropzone .form-field-dropzone', (dropzoneContainer) => {
// if "form-field-dropzone" exists, then "dropzone" must also exist
const dropzone = dropzoneContainer.querySelector<HTMLElement>('.dropzone').dropzone;
const hasUploadedFiles = dropzone.files.length !== 0;
toggleElem(dropzoneContainer, hasUploadedFiles);
});
// activate this markdown editor
hideElem(fieldTextarea);

View File

@@ -34,8 +34,12 @@ export function initRepoMigration() {
elCloneAddr.addEventListener('input', () => {
if (repoNameChanged) return;
let repoNameFromUrl = elCloneAddr.value.split(/[?#]/)[0];
repoNameFromUrl = /^(.*\/)?((.+?)\/?)$/.exec(repoNameFromUrl)[3];
repoNameFromUrl = repoNameFromUrl.split(/[?#]/)[0];
const parts = /^(.*\/)?((.+?)\/?)$/.exec(repoNameFromUrl);
if (!parts || parts.length < 4) {
elRepoName.value = '';
return;
}
repoNameFromUrl = parts[3].split(/[?#]/)[0];
elRepoName.value = sanitizeRepoName(repoNameFromUrl);
});
}