From 76dffc862103eb23d51445ef9d611296308c8413 Mon Sep 17 00:00:00 2001
From: Gusted <postmaster@gusted.xyz>
Date: Fri, 18 Aug 2023 11:21:24 +0200
Subject: [PATCH] [GITEA] Improve HTML title on repositories

- The `<title>` element that lives inside the `<head>` element is an important element that gives browsers and search engine crawlers the title of the webpage, hence the element name. It's therefor important that this title is accurate.
- Currently there are three issues with titles on repositories. It doesn't use the `FullName` and instead only uses the repository name, this doesn't distinguish which user or organisation the repository is on. It doesn't show the full treepath in the title when visiting an file inside a directory and instead only uses the latest path in treepath. It can show the repository name twice if the `.Title` variable also included the repository name such as on the repository homepage.
- Use the repository's fullname (which include which user the repository is on) instead of just their name.
- Display the repository's fullname if it isn't already in `.Title`.
- Use the full treepath in the repository code view instead of just the
last path.
- Adds integration tests.
- Adds a new repository (`repo59`) that has 3 depths for folders, which
wasn't in any other fixture repository yet, so the full treepath for
could be properly tested.
- Resolves https://codeberg.org/forgejo/forgejo/issues/1276

(cherry picked from commit ff9a6a2cda34cf2b2e392cc47125ed0f619b287b)
---
 models/fixtures/release.yml                   |  14 +++
 models/fixtures/repo_unit.yml                 |  32 ++++++
 models/fixtures/repository.yml                |  14 +++
 models/fixtures/user.yml                      |   2 +-
 models/repo/repo_list_test.go                 |   6 +-
 routers/web/repo/view.go                      |   4 +-
 templates/base/head.tmpl                      |   3 +-
 .../user2/repo59.git/HEAD                     |   1 +
 .../user2/repo59.git/config                   |   4 +
 .../user2/repo59.git/description              |   1 +
 .../user2/repo59.git/info/exclude             |   6 +
 .../40/8bbd3bd1f96950f8cf2f98c479557f6b18817a | Bin 0 -> 63 bytes
 .../5d/5c87a90af64cc67f22d60a942d5efaef8bc96b | Bin 0 -> 50 bytes
 .../88/3e2970ed6937cbb63311e941adb97df0ae3a52 | Bin 0 -> 49 bytes
 .../8c/ac7a8f434451410cc91ab9c04d07baff974ad8 | Bin 0 -> 120 bytes
 .../a0/ccafed39086ef520be6886d9395eb2100d317e | Bin 0 -> 102 bytes
 .../ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 | Bin 0 -> 36 bytes
 .../cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 | Bin 0 -> 108 bytes
 .../d8/f53dfb33f6ccf4169c34970b5e747511c18beb | Bin 0 -> 784 bytes
 .../f3/c1ec36c0e7605be54e71f24035caa675b7ba41 | Bin 0 -> 48 bytes
 .../user2/repo59.git/packed-refs              |   3 +
 .../user2/repo59.git/refs/heads/cake-recipe   |   1 +
 tests/integration/api_repo_test.go            |   6 +-
 tests/integration/integration_test.go         |  15 +++
 tests/integration/repo_test.go                | 104 ++++++++++++++++++
 25 files changed, 206 insertions(+), 10 deletions(-)
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/HEAD
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/config
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/description
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/info/exclude
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/packed-refs
 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe

diff --git a/models/fixtures/release.yml b/models/fixtures/release.yml
index 4ed7df440d..844deb3a7b 100644
--- a/models/fixtures/release.yml
+++ b/models/fixtures/release.yml
@@ -136,3 +136,17 @@
   is_prerelease: false
   is_tag: false
   created_unix: 946684803
+
+- id: 11
+  repo_id: 59
+  publisher_id: 2
+  tag_name: "v1.0"
+  lower_tag_name: "v1.0"
+  target: "main"
+  title: "v1.0"
+  sha1: "d8f53dfb33f6ccf4169c34970b5e747511c18beb"
+  num_commits: 1
+  is_draft: false
+  is_prerelease: false
+  is_tag: false
+  created_unix: 946684803
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index c22eb8c2a2..6afef2a432 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -608,6 +608,38 @@
   type: 1
   created_unix: 946684810
 
+# BEGIN Forgejo [GITEA] Improve HTML title on repositories
+-
+  id: 1093
+  repo_id: 59
+  type: 1
+  created_unix: 946684810
+
+-
+  id: 1094
+  repo_id: 59
+  type: 2
+  created_unix: 946684810
+
+-
+  id: 1095
+  repo_id: 59
+  type: 3
+  created_unix: 946684810
+
+-
+  id: 1096
+  repo_id: 59
+  type: 4
+  created_unix: 946684810
+
+-
+  id: 1097
+  repo_id: 59
+  type: 5
+  created_unix: 946684810
+# END Forgejo [GITEA] Improve HTML title on repositories
+
 -
   id: 91
   repo_id: 58
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index 15668e6cae..5dd3e8cc90 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -1467,6 +1467,7 @@
   owner_name: user27
   lower_name: repo49
   name: repo49
+  description: A wonderful repository with more than just a README.md
   default_branch: master
   num_watches: 0
   num_stars: 0
@@ -1693,3 +1694,16 @@
   size: 0
   is_fsck_enabled: true
   close_issues_via_commit_in_any_branch: false
+
+-
+  id: 59
+  owner_id: 2
+  owner_name: user2
+  lower_name: repo59
+  name: repo59
+  default_branch: master
+  is_empty: false
+  is_archived: false
+  is_private: false
+  status: 0
+  num_issues: 0
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index c7c5c024be..8a9de671c1 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -66,7 +66,7 @@
   num_followers: 2
   num_following: 1
   num_stars: 2
-  num_repos: 14
+  num_repos: 15
   num_teams: 0
   num_members: 0
   visibility: 0
diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go
index 7097b6ea14..3be1ebb3c9 100644
--- a/models/repo/repo_list_test.go
+++ b/models/repo/repo_list_test.go
@@ -138,12 +138,12 @@ func getTestCases() []struct {
 		{
 			name:  "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
 			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
-			count: 31,
+			count: 32,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
 			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
-			count: 36,
+			count: 37,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
@@ -158,7 +158,7 @@ func getTestCases() []struct {
 		{
 			name:  "AllPublic/PublicRepositoriesOfOrganization",
 			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
-			count: 31,
+			count: 32,
 		},
 		{
 			name:  "AllTemplates",
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 15c85f6427..5278f5b55d 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -164,7 +164,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
 
 	if ctx.Repo.TreePath != "" {
 		ctx.Data["HideRepoInfo"] = true
-		ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
+		ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+util.PathEscapeSegments(ctx.Repo.TreePath), ctx.Repo.RefName)
 	}
 
 	subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
@@ -343,7 +343,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
 	}
 	defer dataRc.Close()
 
-	ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
+	ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+util.PathEscapeSegments(ctx.Repo.TreePath), ctx.Repo.RefName)
 	ctx.Data["FileIsSymlink"] = entry.IsLink()
 	ctx.Data["FileName"] = blob.Name()
 	ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 8eebaebd70..ba22574331 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -2,7 +2,8 @@
 <html lang="{{ctx.Locale.Lang}}" class="theme-{{if .SignedUser.Theme}}{{.SignedUser.Theme}}{{else}}{{DefaultTheme}}{{end}}">
 <head>
 	<meta name="viewport" content="width=device-width, initial-scale=1">
-	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
+	{{/* Display `- .Repsository.FullName` only if `.Title` does not already start with that. */}}
+	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if and (.Repository.Name) (not (StringUtils.HasPrefix .Title .Repository.FullName))}}{{.Repository.FullName}} - {{end}}{{AppName}}</title>
 	{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
 	<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
diff --git a/tests/gitea-repositories-meta/user2/repo59.git/HEAD b/tests/gitea-repositories-meta/user2/repo59.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo59.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo59.git/config b/tests/gitea-repositories-meta/user2/repo59.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo59.git/config
@@ -0,0 +1,4 @@
+[core]
+	repositoryformatversion = 0
+	filemode = true
+	bare = true
diff --git a/tests/gitea-repositories-meta/user2/repo59.git/description b/tests/gitea-repositories-meta/user2/repo59.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo59.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo59.git/info/exclude b/tests/gitea-repositories-meta/user2/repo59.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo59.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a b/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a
new file mode 100644
index 0000000000000000000000000000000000000000..567284ef1c21f472841f3d729cbfe024d78c448c
GIT binary patch
literal 63
zcmV-F0Korv0ZYosPf{>3XHZrMN-fAQ&Me6<s#GY?EXhzvR7lB8OG_<E%_~vJ&df_G
VR>)6NNXyJgE!N}W0ssfX5``$?8bAO5

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b b/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b
new file mode 100644
index 0000000000000000000000000000000000000000..f23960f4cc8c09af29fc4765f683b697a4547d74
GIT binary patch
literal 50
zcmV-20L}k+0V^p=O;s>9VK6ZO0)@QP;*!j~bcW9d-<TbEo)G=iuke$D>8WL<+jltv
I07;Az9utceGXMYp

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 b/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52
new file mode 100644
index 0000000000000000000000000000000000000000..46cc9e3e5ec14999b5424f35c1c548db9e9e33c1
GIT binary patch
literal 49
zcmb<m^geacKghr=?SqG|SEzoN#<}ypp&s78I-0s(p&p*PetM^LG)|s5fB7*JgWNSf
F1pvu25=;O9

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8 b/tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8
new file mode 100644
index 0000000000000000000000000000000000000000..5a1e79326f2c2ee1a4c451167389f5096eabdf72
GIT binary patch
literal 120
zcmV-;0Ehp00V^p=O;s>7FlR6{FfcPQQ3!H%bn$i7%S~Z$=-z96@n>ehkMsI7j#P%$
zXG=6znHT_pLP~0C0Yhv|`%12FKF8{nu5nG#jr;Y!`(!rMjI`9mlG377y^@L&h7LQ;
ag14FGr?(jkzI0r>v-ZO}s~`Z9QY~(zy*Y~j

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e b/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e
new file mode 100644
index 0000000000000000000000000000000000000000..3b71228a7ec7b91f1066cda387ef6efa28c2ae68
GIT binary patch
literal 102
zcmV-s0Ga=I0V^p=O;xb4U@$Z=Ff%bx2y%6F@paY9O<`F5Xyx6%^&$E{W*@XnSgCoZ
zXGP9TsG{Q3<f7D)_~OLU<ka}0)a1;9RK1dl5{9$w^S7OkuHMJL$BOTZPutYLFJ^@S
I0N_g~9D6=E761SM

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 b/tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99
new file mode 100644
index 0000000000000000000000000000000000000000..dcbd3b3eb9f03f5d60d082bd174cf3e8d07c6e8a
GIT binary patch
literal 36
rcmb<m^geacKghr+As~$7q(`WRpRb;mZlH#rj;~+n6DEer296E@-c1XM

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 b/tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56
new file mode 100644
index 0000000000000000000000000000000000000000..cd9cea6797da4bacfae03d6da3b053b81d969db2
GIT binary patch
literal 108
zcmV-y0F(cC0VRwv4udcd1XKGJ+abA#LWg(<d4O#eumBtRE|K_sPD(jDtBFUA=wHKQ
z+6ZB4R5Yf1adggmyi14&p%t^q`_d8rXPMbLvGto1u?0b?YK5Zs2-IpjZ^2TVK^B}8
O2`iuQ-u(dQ948US(J)2;

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb b/tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb
new file mode 100644
index 0000000000000000000000000000000000000000..bea28c9f69b13925696567043b55ee3a544ce94c
GIT binary patch
literal 784
zcmV+r1MmEJ0hN=<uB%1>M6>2AdT%A&7_dRgi_+j`HiK;pmmM?MU`7Mx>)%bH?6OHM
zk5sBnouqA=Y63LcbH7YOmH|GAl4Hc@EW@%K&C)1I1Uia^1hFYP#!;RNM>a}%Dtb?4
zJAn6?4K(;YO4A`5NBYlfjhe2`eoNZs4?rJ;J;Mr|fWQvz5u(27_uQ2I-(JxbV^x4(
z|B6Ty%>s_%fUBlh_~u>6-<$#zs9bFmF%~6^Q@J9f=}|(LabtH3q_<N6zqB~{6bz|(
z88^4w(!a1&Jl%ge1xO9gmJ0>NxV$YbR02+h-W<i4Z3HuNB`n?WDPMf^Mf~0>y>8SN
zL-n>NnOHxD1_ktBKMTFMF~Oeo44c$Nx1zjPg-n1^s{~0y49o8@?{IqT_1l9F<ml+Q
z9C}RBeGn<|zST=%w;6q?N4)b-XXc8BYB)(@zga5$><Ig{cl7%sA$kIRHm~J}{?`20
z`NP{m$x&d*S7)08xz__Sb<D?XcDfv)v6OGKxlD#%XJSAuT`t5cYAfG8x2j*Vewi12
zTJ2^Ea1neQx#)5*l>DBH+t<aF%ENli#a$-YU1bW@izqP7T$`cw$TekB##S1PNpyTh
z+k*mF^zUgl%VLdhnmP+=@!?Ac>z~X}{gyB7!qm$KrCW*^)AW)NzRtpG4}}%%=|SNs
z@Nz?3mnX@zAyYKZV*tH#nR{bxS}d9i?M(85HRz`|j6_MUVShjg2W|Ppe8h&<LkhU9
z`dWpstae%^_i+Gs#s*K5<(k3j78iSEG>7*MdL7JHfOBCJr|RNl?>o$nk$Jg%Kk7Ay
z>ilZ^!I!SZjq{9}32tH%t5<moRy?*E(a}TN4~8WWew8kHGc1@^G5^rQS*HLDtNP<q
zH4^z!KGR{%CV7@+SIOuqcS&zzJ_#CxU$9z^2UsDM%&P1dVXr;XOwv;d$eaD_+~sl9
z`_bldN7Pg|IhDS5$9pE_7Ok3Zva>4`QEQTUns*)fL4hA0LId@WGsHUmmoZfJs-`Nc
O!MB&yMEwL3??r>^yMtc<

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 b/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41
new file mode 100644
index 0000000000000000000000000000000000000000..b6803eb5a271b9d33c981d1ba24fa4126ae409db
GIT binary patch
literal 48
zcmV-00MGw;0V^p=O;s>9W-u`T0)@2voRrieh6QKVzqRDZ`>L=nqwS_;+$I5D!#V&X
Gkq<4~#T1zU

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo59.git/packed-refs b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs
new file mode 100644
index 0000000000..114c84d2aa
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs
@@ -0,0 +1,3 @@
+# pack-refs with: peeled fully-peeled sorted 
+d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/heads/master
+d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/tags/v1.0
diff --git a/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe b/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe
new file mode 100644
index 0000000000..63bbea6692
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe
@@ -0,0 +1 @@
+d8f53dfb33f6ccf4169c34970b5e747511c18beb
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
index 3933298f23..43fab43f5b 100644
--- a/tests/integration/api_repo_test.go
+++ b/tests/integration/api_repo_test.go
@@ -93,9 +93,9 @@ func TestAPISearchRepo(t *testing.T) {
 	}{
 		{
 			name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{
-				nil:   {count: 33},
-				user:  {count: 33},
-				user2: {count: 33},
+				nil:   {count: 34},
+				user:  {count: 34},
+				user2: {count: 34},
 			},
 		},
 		{
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index 49a714c343..a3d0eb1019 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -531,3 +531,18 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
 	doc := NewHTMLParser(t, resp.Body)
 	return doc.GetCSRF()
 }
+
+func GetHTMLTitle(t testing.TB, session *TestSession, urlStr string) string {
+	t.Helper()
+
+	req := NewRequest(t, "GET", urlStr)
+	var resp *httptest.ResponseRecorder
+	if session == nil {
+		resp = MakeRequest(t, req, http.StatusOK)
+	} else {
+		resp = session.MakeRequest(t, req, http.StatusOK)
+	}
+
+	doc := NewHTMLParser(t, resp.Body)
+	return doc.Find("head title").Text()
+}
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index 9ace3ca30c..cccfd4b7bb 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -444,3 +444,107 @@ func TestGeneratedSourceLink(t *testing.T) {
 		assert.Equal(t, "/user27/repo49/src/commit/aacbdfe9e1c4b47f60abe81849045fa4e96f1d75/test/test.txt", dataURL)
 	})
 }
+
+func TestRepoHTMLTitle(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	t.Run("Repository homepage", func(t *testing.T) {
+		t.Run("Without description", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1")
+			assert.EqualValues(t, "user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
+		})
+		t.Run("With description", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			htmlTitle := GetHTMLTitle(t, nil, "/user27/repo49")
+			assert.EqualValues(t, "user27/repo49: A wonderful repository with more than just a README.md - Gitea: Git with a cup of tea", htmlTitle)
+		})
+	})
+
+	t.Run("Code view", func(t *testing.T) {
+		t.Run("Directory", func(t *testing.T) {
+			t.Run("Default branch", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/master/deep/nesting")
+				assert.EqualValues(t, "repo59/deep/nesting at master - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+			t.Run("Non-default branch", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/cake-recipe/deep/nesting")
+				assert.EqualValues(t, "repo59/deep/nesting at cake-recipe - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+			t.Run("Commit", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/commit/d8f53dfb33f6ccf4169c34970b5e747511c18beb/deep/nesting/")
+				assert.EqualValues(t, "repo59/deep/nesting at d8f53dfb33f6ccf4169c34970b5e747511c18beb - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+			t.Run("Tag", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/tag/v1.0/deep/nesting/")
+				assert.EqualValues(t, "repo59/deep/nesting at v1.0 - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+		})
+		t.Run("File", func(t *testing.T) {
+			t.Run("Default branch", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/master/deep/nesting/folder/secret_sauce_recipe.txt")
+				assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at master - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+			t.Run("Non-default branch", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/cake-recipe/deep/nesting/folder/secret_sauce_recipe.txt")
+				assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at cake-recipe - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+			t.Run("Commit", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/commit/d8f53dfb33f6ccf4169c34970b5e747511c18beb/deep/nesting/folder/secret_sauce_recipe.txt")
+				assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at d8f53dfb33f6ccf4169c34970b5e747511c18beb - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+			t.Run("Tag", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/tag/v1.0/deep/nesting/folder/secret_sauce_recipe.txt")
+				assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at v1.0 - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
+			})
+		})
+	})
+
+	t.Run("Issues view", func(t *testing.T) {
+		t.Run("Overview page", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/issues")
+			assert.EqualValues(t, "Issues - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
+		})
+		t.Run("View issue page", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/issues/1")
+			assert.EqualValues(t, "#1 - issue1 - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
+		})
+	})
+
+	t.Run("Pull requests view", func(t *testing.T) {
+		t.Run("Overview page", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/pulls")
+			assert.EqualValues(t, "Pull Requests - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
+		})
+		t.Run("View pull request", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/pulls/2")
+			assert.EqualValues(t, "#2 - issue2 - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
+		})
+	})
+}