From c54896ba709530daf99e63926ad20ae40d240692 Mon Sep 17 00:00:00 2001
From: Beowulf <beowulf@beocode.eu>
Date: Sun, 28 Apr 2024 13:47:52 +0000
Subject: [PATCH] Show repo activities even if only code unit active or git
 repo is empty but issue is active (#3455)

When all repository units are deactivated except for the code unit, the activity tab will not be shown.
Since the activities tab also shows contributing stats, it would be good to show the activities tab also when only code is active.
This commit changes the behavior when the activities tab is shown.
Previous it would only be shown when Issues, Pull-Requests or Releases are activated. Now it would additionally be shown when the code unit is activated.

Refs: #3429

| Before (Code + Issues - Owner) | Before (Code - Viewer) | After (Code + Issues - Owner) | After (Code - Viewer) |
| -- | -- | -- | -- |
| ![image](/attachments/2af997bc-1f38-48c6-bdf3-cfbd7087b220)  | ![image](/attachments/ef1797f0-5c9a-4a1a-ba82-749f3ab4f403) | ![image](/attachments/fd28a96c-04ca-407e-a70d-d28b393f223d) | ![image](/attachments/2cd0d559-a6de-4ca0-a736-29c5fea81b5a) |
|  | `/activity` returns 404 for everyone | ![image](/attachments/e0e97d8f-48cb-4c16-a505-1fafa46c4b8e)  | - |

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3455
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Beowulf <beowulf@beocode.eu>
Co-committed-by: Beowulf <beowulf@beocode.eu>
---
 routers/web/repo/activity.go            |   2 +-
 routers/web/web.go                      |   8 +-
 templates/repo/activity.tmpl            |   8 +-
 templates/repo/header.tmpl              |   2 +-
 tests/integration/repo_activity_test.go | 126 ++++++++++++++++++++++++
 5 files changed, 137 insertions(+), 9 deletions(-)

diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 6f6641cc65..ba776c84d3 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -57,7 +57,7 @@ func Activity(ctx *context.Context) {
 		ctx.Repo.CanRead(unit.TypeReleases),
 		ctx.Repo.CanRead(unit.TypeIssues),
 		ctx.Repo.CanRead(unit.TypePullRequests),
-		ctx.Repo.CanRead(unit.TypeCode)); err != nil {
+		ctx.Repo.CanRead(unit.TypeCode) && !ctx.Repo.Repository.IsEmpty); err != nil {
 		ctx.ServerError("GetActivityStats", err)
 		return
 	}
diff --git a/routers/web/web.go b/routers/web/web.go
index 154e1ce24b..8faedca178 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1423,16 +1423,16 @@ func registerRoutes(m *web.Route) {
 			m.Group("/contributors", func() {
 				m.Get("", repo.Contributors)
 				m.Get("/data", repo.ContributorsData)
-			})
+			}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
 			m.Group("/code-frequency", func() {
 				m.Get("", repo.CodeFrequency)
 				m.Get("/data", repo.CodeFrequencyData)
-			})
+			}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
 			m.Group("/recent-commits", func() {
 				m.Get("", repo.RecentCommits)
 				m.Get("/data", repo.RecentCommitsData)
-			})
-		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
+			}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
+		}, context.RepoRef(), context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
 
 		m.Group("/activity_author_data", func() {
 			m.Get("", repo.ActivityAuthors)
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index a19fb66261..19a09b99b0 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -2,9 +2,11 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository commits">
 	{{template "repo/header" .}}
 	<div class="ui container flex-container">
-		<div class="flex-container-nav">
-			{{template "repo/navbar" .}}
-		</div>
+		{{if and (not .IsEmptyRepo) (.Permission.CanRead $.UnitTypeCode)}}
+			<div class="flex-container-nav">
+				{{template "repo/navbar" .}}
+			</div>
+		{{end}}
 		<div class="flex-container-main">
 			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
 			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 95c2e059d5..cf8ee7d772 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -159,7 +159,7 @@
 					</a>
 				{{end}}
 
-				{{if and (.Permission.CanReadAny $.UnitTypePullRequests $.UnitTypeIssues $.UnitTypeReleases) (not .IsEmptyRepo)}}
+				{{if and (.Permission.CanReadAny $.UnitTypeCode $.UnitTypePullRequests $.UnitTypeIssues $.UnitTypeReleases)}}
 					<a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity">
 						{{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}}
 					</a>
diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go
index 792554db4b..0b1e9939a1 100644
--- a/tests/integration/repo_activity_test.go
+++ b/tests/integration/repo_activity_test.go
@@ -4,13 +4,20 @@
 package integration
 
 import (
+	"fmt"
 	"net/http"
 	"net/url"
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
+	unit_model "code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/test"
+	repo_service "code.gitea.io/gitea/services/repository"
+	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -63,3 +70,122 @@ func TestRepoActivity(t *testing.T) {
 		assert.Len(t, list.Nodes, 3)
 	})
 }
+
+func TestRepoActivityAllUnitsDisabled(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+	session := loginUser(t, user.Name)
+
+	unit_model.LoadUnitConfig()
+
+	// Create a repo, with no unit enabled.
+	repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+		Name:     "empty-repo",
+		AutoInit: false,
+	})
+	assert.NoError(t, err)
+	assert.NotEmpty(t, repo)
+
+	enabledUnits := make([]repo_model.RepoUnit, 0)
+	disabledUnits := []unit_model.Type{unit_model.TypeCode, unit_model.TypeIssues, unit_model.TypePullRequests, unit_model.TypeReleases}
+	err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, disabledUnits)
+	assert.NoError(t, err)
+
+	req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestRepoActivityOnlyCodeUnitWithEmptyRepo(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+	session := loginUser(t, user.Name)
+
+	unit_model.LoadUnitConfig()
+
+	// Create a empty repo, with only code unit enabled.
+	repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+		Name:     "empty-repo",
+		AutoInit: false,
+	})
+	assert.NoError(t, err)
+	assert.NotEmpty(t, repo)
+
+	enabledUnits := make([]repo_model.RepoUnit, 1)
+	enabledUnits[0] = repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeCode}
+	disabledUnits := []unit_model.Type{unit_model.TypeIssues, unit_model.TypePullRequests, unit_model.TypeReleases}
+	err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, disabledUnits)
+	assert.NoError(t, err)
+
+	req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link()))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// Git repo empty so no activity for contributors etc
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestRepoActivityOnlyCodeUnitWithNonEmptyRepo(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+	session := loginUser(t, user.Name)
+
+	unit_model.LoadUnitConfig()
+
+	// Create a repo, with only code unit enabled.
+	repo, _, f := CreateDeclarativeRepo(t, user, "", []unit_model.Type{unit_model.TypeCode}, nil, nil)
+	defer f()
+
+	req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link()))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// Git repo not empty so activity for contributors etc
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link()))
+	session.MakeRequest(t, req, http.StatusOK)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link()))
+	session.MakeRequest(t, req, http.StatusOK)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link()))
+	session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestRepoActivityOnlyIssuesUnit(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+	session := loginUser(t, user.Name)
+
+	unit_model.LoadUnitConfig()
+
+	// Create a empty repo, with only code unit enabled.
+	repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+		Name:     "empty-repo",
+		AutoInit: false,
+	})
+	assert.NoError(t, err)
+	assert.NotEmpty(t, repo)
+
+	enabledUnits := make([]repo_model.RepoUnit, 1)
+	enabledUnits[0] = repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeIssues}
+	disabledUnits := []unit_model.Type{unit_model.TypeCode, unit_model.TypePullRequests, unit_model.TypeReleases}
+	err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, disabledUnits)
+	assert.NoError(t, err)
+
+	req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link()))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// Git repo empty so no activity for contributors etc
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+	req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link()))
+	session.MakeRequest(t, req, http.StatusNotFound)
+}