diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go index a3f63710de..efcf53b07c 100644 --- a/modules/git/commit_submodule_file.go +++ b/modules/git/commit_submodule_file.go @@ -42,7 +42,7 @@ func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreL 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 { diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go index 203939fb1b..33fe146444 100644 --- a/modules/git/commit_submodule_file_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -32,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) diff --git a/modules/git/utils.go b/modules/git/utils.go index 897306efd0..e2bdf7866b 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -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 +} diff --git a/modules/git/utils_test.go b/modules/git/utils_test.go index 1291cee637..f09a047136 100644 --- a/modules/git/utils_test.go +++ b/modules/git/utils_test.go @@ -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) +} diff --git a/modules/util/truncate.go b/modules/util/truncate.go index 2bce248281..52534d3cac 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -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 { diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index 9f4ad7dc20..6d71f38c0c 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -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)) }) diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go index 094fd987ac..eb7f6dc5bc 100644 --- a/routers/web/feed/branch.go +++ b/routers/web/feed/branch.go @@ -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 diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 419dbedd74..e481a3e7d2 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -90,11 +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 - } - rangeEnd = min(rangeStart+perPage, 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 diff --git a/services/repository/push.go b/services/repository/push.go index af3c873d15..7c68a7f176 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -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 diff --git a/tests/integration/api_repo_git_trees_test.go b/tests/integration/api_repo_git_trees_test.go index 47063d9091..ea7630f414 100644 --- a/tests/integration/api_repo_git_trees_test.go +++ b/tests/integration/api_repo_git_trees_test.go @@ -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{ diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 6a8c70f12f..127df5919d 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -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(), "") + // create a new file req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) resp = session.MakeRequest(t, req, http.StatusOK)