Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions models/organization/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import (
"context"
"fmt"
"strconv"
"strings"
"time"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
Expand Down Expand Up @@ -592,3 +594,65 @@
"team_user.uid": userID,
})
}

// ErrOrgIsArchived represents a "OrgIsArchived" error
type ErrOrgIsArchived struct {
OrgName string
}

func (err ErrOrgIsArchived) Error() string {
return fmt.Sprintf("organization is archived [name: %s]", err.OrgName)
}

func (err ErrOrgIsArchived) Unwrap() error {
return util.ErrPermissionDenied
}

// IsArchived returns true if organization is archived
func (org *Organization) IsArchived(ctx context.Context) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality should be implemented as a standalone function rather than as a method of Organization.

archived, _ := user_model.GetSetting(ctx, org.ID, "org_archived")
return archived == "true"
}

// SetArchived sets the archived status of the organization
func (org *Organization) SetArchived(ctx context.Context, archived bool) error {
archivedStr := "false"
if archived {
archivedStr = "true"
}

if err := user_model.SetUserSetting(ctx, org.ID, "org_archived", archivedStr); err != nil {
return err
}

if archived {
// Set the archived date
return user_model.SetUserSetting(ctx, org.ID, "org_archived_date", strconv.FormatInt(time.Now().Unix(), 10))
} else {

Check failure on line 631 in models/organization/org.go

View workflow job for this annotation

GitHub Actions / lint-backend

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)

Check failure on line 631 in models/organization/org.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)

Check failure on line 631 in models/organization/org.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)
// Clear the archived date when unarchiving
return user_model.SetUserSetting(ctx, org.ID, "org_archived_date", "")
}
}

// GetArchivedDate returns the date when the organization was archived, or zero time if not archived
func (org *Organization) GetArchivedDate(ctx context.Context) (time.Time, error) {
dateStr, err := user_model.GetSetting(ctx, org.ID, "org_archived_date")
if err != nil || dateStr == "" {
return time.Time{}, err
}

timestamp, err := strconv.ParseInt(dateStr, 10, 64)
if err != nil {
return time.Time{}, err
}

return time.Unix(timestamp, 0), nil
}

// MustNotBeArchived returns ErrOrgIsArchived if the organization is archived
func (org *Organization) MustNotBeArchived(ctx context.Context) error {
if org.IsArchived(ctx) {
return ErrOrgIsArchived{OrgName: org.Name}
}
return nil
}
15 changes: 15 additions & 0 deletions models/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,21 @@ func (repo *Repository) IsBroken() bool {
return repo.Status == RepositoryBroken
}

// IsEffectivelyArchived indicates that repository is archived either directly or through its parent organization
func (repo *Repository) IsEffectivelyArchived(ctx context.Context) bool {
if repo.IsArchived {
return true
}

// Check if parent organization is archived (if repository belongs to an organization)
if repo.Owner != nil && repo.Owner.IsOrganization() {
archived, _ := user_model.GetSetting(ctx, repo.Owner.ID, "org_archived")
return archived == "true"
}

return false
}

// MarkAsBrokenEmpty marks the repo as broken and empty
// FIXME: the status "broken" and "is_empty" were abused,
// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken"
Expand Down
2 changes: 2 additions & 0 deletions modules/structs/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Organization struct {
Location string `json:"location"`
Visibility string `json:"visibility"`
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
Archived bool `json:"archived"`
// username of the organization
// deprecated
UserName string `json:"username"`
Expand Down Expand Up @@ -58,6 +59,7 @@ type EditOrgOption struct {
// enum: public,limited,private
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
Archived *bool `json:"archived"`
}

// RenameOrgOption options when renaming an organization
Expand Down
12 changes: 12 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2874,6 +2874,18 @@ settings.confirm_delete_account = Confirm Deletion
settings.delete_failed = Deleting organization failed due to an internal error
settings.delete_successful = Organization <b>%s</b> has been deleted successfully.
settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.
settings.archive = Archive
settings.archive_this_org = Archive this organization
settings.archive_this_org_desc = Archiving will make this organization read-only. No repositories can be created, and existing repositories cannot be pushed to.
settings.archive_not_allowed = Only organization owners can archive organizations.
settings.archive_org_header = Archive This Organization
settings.archive_org_button = Archive Organization
settings.unarchive_org_header = Unarchive This Organization
settings.unarchive_org_button = Unarchive Organization

archived_create_repo_not_allowed = Cannot create repositories in an archived organization.
settings.archive_success = Organization has been successfully archived.
settings.unarchive_success = Organization has been successfully unarchived.

settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization.

Expand Down
19 changes: 19 additions & 0 deletions routers/api/v1/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,25 @@ func Edit(ctx *context.APIContext) {
}
}

// Handle archived field - only allow org owners to modify
if form.Archived != nil {
// Check if user is owner of the organization
isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !isOwner && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, "only organization owners and site admins can archive/unarchive organizations")
return
}

if err := ctx.Org.Organization.SetArchived(ctx, *form.Archived); err != nil {
ctx.APIErrorInternal(err)
return
}
}

opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
Expand Down
5 changes: 5 additions & 0 deletions routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ func CreateOrgRepo(ctx *context.APIContext) {
return
}

if err := org.MustNotBeArchived(ctx); err != nil {
ctx.APIError(http.StatusForbidden, err)
return
}

if !ctx.Doer.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
if err != nil {
Expand Down
49 changes: 49 additions & 0 deletions routers/web/org/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func Settings(ctx *context.Context) {
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
ctx.Data["IsArchived"] = ctx.Org.Organization.IsArchived(ctx)
ctx.Data["ContextUser"] = ctx.ContextUser

if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
Expand Down Expand Up @@ -165,6 +166,54 @@ func SettingsDeleteOrgPost(ctx *context.Context) {
ctx.JSONRedirect(setting.AppSubURL + "/")
}

// SettingsArchive archives an organization
func SettingsArchive(ctx *context.Context) {
if !ctx.Org.IsOwner {
ctx.JSONError(ctx.Tr("org.settings.archive_not_allowed"))
return
}

org := ctx.Org.Organization
orgName := ctx.FormString("org_name")

if orgName != org.Name {
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
return
}

if err := org.SetArchived(ctx, true); err != nil {
ctx.ServerError("SetArchived", err)
return
}

ctx.Flash.Success(ctx.Tr("org.settings.archive_success"))
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
}

// SettingsUnarchive unarchives an organization
func SettingsUnarchive(ctx *context.Context) {
if !ctx.Org.IsOwner {
ctx.JSONError(ctx.Tr("org.settings.archive_not_allowed"))
return
}

org := ctx.Org.Organization
orgName := ctx.FormString("org_name")

if orgName != org.Name {
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
return
}

if err := org.SetArchived(ctx, false); err != nil {
ctx.ServerError("SetArchived", err)
return
}

ctx.Flash.Success(ctx.Tr("org.settings.unarchive_success"))
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
}

// Webhooks render webhook list page
func Webhooks(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings")
Expand Down
8 changes: 8 additions & 0 deletions routers/web/repo/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ func ForkPost(ctx *context.Context) {
return
}

if ctxUser.IsOrganization() {
org := organization.OrgFromUser(ctxUser)
if err := org.MustNotBeArchived(ctx); err != nil {
ctx.JSONError(ctx.Tr("org.archived_create_repo_not_allowed"))
return
}
}

forkRepo := getForkRepository(ctx)
if ctx.Written() {
return
Expand Down
6 changes: 3 additions & 3 deletions routers/web/repo/githttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ func httpBase(ctx *context.Context) *serviceHandler {
repoExist = false
}

// Don't allow pushing if the repo is archived
if repoExist && repo.IsArchived && !isPull {
ctx.PlainText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
// Don't allow pushing if the repo or its organization is archived
if repoExist && repo.IsEffectivelyArchived(ctx) && !isPull {
ctx.PlainText(http.StatusForbidden, "This repository is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions routers/web/repo/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func Releases(ctx *context.Context) {
}

writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsEffectivelyArchived(ctx)

releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
ListOptions: listOptions,
Expand Down Expand Up @@ -274,7 +274,7 @@ func SingleRelease(ctx *context.Context) {
ctx.Data["PageIsReleaseList"] = true

writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsEffectivelyArchived(ctx)

releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 1},
Expand Down
10 changes: 9 additions & 1 deletion routers/web/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {

var orgsAvailable []*organization.Organization
for i := range orgs {
if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) {
if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) && !orgs[i].IsArchived(ctx) {
orgsAvailable = append(orgsAvailable, orgs[i])
}
}
Expand Down Expand Up @@ -225,6 +225,14 @@ func CreatePost(ctx *context.Context) {
}
ctx.Data["ContextUser"] = ctxUser

if ctxUser.IsOrganization() {
org := organization.OrgFromUser(ctxUser)
if err := org.MustNotBeArchived(ctx); err != nil {
ctx.RenderWithErr(ctx.Tr("org.archived_create_repo_not_allowed"), tplCreate, form)
return
}
}

if form.RepoTemplate > 0 {
templateRepo, err := repo_model.GetRepositoryByID(ctx, form.RepoTemplate)
if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) {
Expand Down
4 changes: 2 additions & 2 deletions routers/web/repo/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func handleSettingsPostUpdate(ctx *context.Context) {
func handleSettingsPostMirror(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsEffectivelyArchived(ctx) {
ctx.NotFound(nil)
return
}
Expand Down Expand Up @@ -321,7 +321,7 @@ func handleSettingsPostMirror(ctx *context.Context) {

func handleSettingsPostMirrorSync(ctx *context.Context) {
repo := ctx.Repo.Repository
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsEffectivelyArchived(ctx) {
ctx.NotFound(nil)
return
}
Expand Down
6 changes: 3 additions & 3 deletions routers/web/repo/wiki.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ func WikiPost(ctx *context.Context) {

// Wiki renders single wiki page
func Wiki(ctx *context.Context) {
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsEffectivelyArchived(ctx)

switch ctx.FormString("action") {
case "_pages":
Expand Down Expand Up @@ -508,7 +508,7 @@ func Wiki(ctx *context.Context) {

// WikiRevision renders file revision list of wiki page
func WikiRevision(ctx *context.Context) {
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsEffectivelyArchived(ctx)

if !ctx.Repo.Repository.HasWiki() {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
Expand Down Expand Up @@ -546,7 +546,7 @@ func WikiPages(ctx *context.Context) {
}

ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsEffectivelyArchived(ctx)

_, commit, err := findWikiRepoCommit(ctx)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions routers/web/shared/user/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult

result = &PrepareOwnerHeaderResult{}
if ctx.ContextUser.IsOrganization() {
org := organization.OrgFromUser(ctx.ContextUser)
ctx.Data["IsOrgArchived"] = org.IsArchived(ctx)

result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer)
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
Expand Down
2 changes: 2 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,8 @@ func registerWebRoutes(m *web.Router) {
}, actions.MustEnableActions)

m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost)
m.Post("/archive", org.SettingsArchive)
m.Post("/unarchive", org.SettingsUnarchive)
m.Post("/delete", org.SettingsDeleteOrgPost)

m.Group("/packages", func() {
Expand Down
10 changes: 9 additions & 1 deletion services/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func (r *Repository) GetObjectFormat() git.ObjectFormat {
// RepoMustNotBeArchived checks if a repo is archived
func RepoMustNotBeArchived() func(ctx *Context) {
return func(ctx *Context) {
if ctx.Repo.Repository.IsArchived {
if ctx.Repo.Repository.IsEffectivelyArchived(ctx) {
ctx.NotFound(errors.New(ctx.Locale.TrString("repo.archive.title")))
}
}
Expand Down Expand Up @@ -410,6 +410,14 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
ctx.Repo.Repository = repo
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty

// Check if repository is effectively archived (either directly or through its organization)
isEffectivelyArchived := repo.IsEffectivelyArchived(ctx)
ctx.Data["IsArchived"] = isEffectivelyArchived

// Override the repository's IsArchived field to reflect the effective archive status
// This ensures templates using .Repository.IsArchived will get the correct value
ctx.Repo.Repository.IsArchived = isEffectivelyArchived
}

// RepoAssignment returns a middleware to handle repository assignment
Expand Down
1 change: 1 addition & 0 deletions services/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ func ToOrganization(ctx context.Context, org *organization.Organization) *api.Or
Location: org.Location,
Visibility: org.Visibility.String(),
RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
Archived: org.IsArchived(ctx),
}
}

Expand Down
Loading
Loading