Skip to content

Commit 0337173

Browse files
author
junoberryferry
committed
Archive entire organizations.
1 parent d782cad commit 0337173

File tree

24 files changed

+307
-15
lines changed

24 files changed

+307
-15
lines changed

models/organization/org.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package organization
77
import (
88
"context"
99
"fmt"
10+
"strconv"
1011
"strings"
12+
"time"
1113

1214
"code.gitea.io/gitea/models/db"
1315
"code.gitea.io/gitea/models/perm"
@@ -592,3 +594,65 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
592594
"team_user.uid": userID,
593595
})
594596
}
597+
598+
// ErrOrgIsArchived represents a "OrgIsArchived" error
599+
type ErrOrgIsArchived struct {
600+
OrgName string
601+
}
602+
603+
func (err ErrOrgIsArchived) Error() string {
604+
return fmt.Sprintf("organization is archived [name: %s]", err.OrgName)
605+
}
606+
607+
func (err ErrOrgIsArchived) Unwrap() error {
608+
return util.ErrPermissionDenied
609+
}
610+
611+
// IsArchived returns true if organization is archived
612+
func (org *Organization) IsArchived(ctx context.Context) bool {
613+
archived, _ := user_model.GetSetting(ctx, org.ID, "org_archived")
614+
return archived == "true"
615+
}
616+
617+
// SetArchived sets the archived status of the organization
618+
func (org *Organization) SetArchived(ctx context.Context, archived bool) error {
619+
archivedStr := "false"
620+
if archived {
621+
archivedStr = "true"
622+
}
623+
624+
if err := user_model.SetUserSetting(ctx, org.ID, "org_archived", archivedStr); err != nil {
625+
return err
626+
}
627+
628+
if archived {
629+
// Set the archived date
630+
return user_model.SetUserSetting(ctx, org.ID, "org_archived_date", strconv.FormatInt(time.Now().Unix(), 10))
631+
} else {
632+
// Clear the archived date when unarchiving
633+
return user_model.SetUserSetting(ctx, org.ID, "org_archived_date", "")
634+
}
635+
}
636+
637+
// GetArchivedDate returns the date when the organization was archived, or zero time if not archived
638+
func (org *Organization) GetArchivedDate(ctx context.Context) (time.Time, error) {
639+
dateStr, err := user_model.GetSetting(ctx, org.ID, "org_archived_date")
640+
if err != nil || dateStr == "" {
641+
return time.Time{}, err
642+
}
643+
644+
timestamp, err := strconv.ParseInt(dateStr, 10, 64)
645+
if err != nil {
646+
return time.Time{}, err
647+
}
648+
649+
return time.Unix(timestamp, 0), nil
650+
}
651+
652+
// MustNotBeArchived returns ErrOrgIsArchived if the organization is archived
653+
func (org *Organization) MustNotBeArchived(ctx context.Context) error {
654+
if org.IsArchived(ctx) {
655+
return ErrOrgIsArchived{OrgName: org.Name}
656+
}
657+
return nil
658+
}

models/repo/repo.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,21 @@ func (repo *Repository) IsBroken() bool {
312312
return repo.Status == RepositoryBroken
313313
}
314314

315+
// IsEffectivelyArchived indicates that repository is archived either directly or through its parent organization
316+
func (repo *Repository) IsEffectivelyArchived(ctx context.Context) bool {
317+
if repo.IsArchived {
318+
return true
319+
}
320+
321+
// Check if parent organization is archived (if repository belongs to an organization)
322+
if repo.Owner != nil && repo.Owner.IsOrganization() {
323+
archived, _ := user_model.GetSetting(ctx, repo.Owner.ID, "org_archived")
324+
return archived == "true"
325+
}
326+
327+
return false
328+
}
329+
315330
// MarkAsBrokenEmpty marks the repo as broken and empty
316331
// FIXME: the status "broken" and "is_empty" were abused,
317332
// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken"

modules/structs/org.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Organization struct {
1515
Location string `json:"location"`
1616
Visibility string `json:"visibility"`
1717
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
18+
Archived bool `json:"archived"`
1819
// username of the organization
1920
// deprecated
2021
UserName string `json:"username"`
@@ -58,6 +59,7 @@ type EditOrgOption struct {
5859
// enum: public,limited,private
5960
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
6061
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
62+
Archived *bool `json:"archived"`
6163
}
6264

6365
// RenameOrgOption options when renaming an organization

options/locale/locale_en-US.ini

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2874,6 +2874,18 @@ settings.confirm_delete_account = Confirm Deletion
28742874
settings.delete_failed = Deleting organization failed due to an internal error
28752875
settings.delete_successful = Organization <b>%s</b> has been deleted successfully.
28762876
settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.
2877+
settings.archive = Archive
2878+
settings.archive_this_org = Archive this organization
2879+
settings.archive_this_org_desc = Archiving will make this organization read-only. No repositories can be created, and existing repositories cannot be pushed to.
2880+
settings.archive_not_allowed = Only organization owners can archive organizations.
2881+
settings.archive_org_header = Archive This Organization
2882+
settings.archive_org_button = Archive Organization
2883+
settings.unarchive_org_header = Unarchive This Organization
2884+
settings.unarchive_org_button = Unarchive Organization
2885+
2886+
archived_create_repo_not_allowed = Cannot create repositories in an archived organization.
2887+
settings.archive_success = Organization has been successfully archived.
2888+
settings.unarchive_success = Organization has been successfully unarchived.
28772889

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

routers/api/v1/org/org.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,25 @@ func Edit(ctx *context.APIContext) {
386386
}
387387
}
388388

389+
// Handle archived field - only allow org owners to modify
390+
if form.Archived != nil {
391+
// Check if user is owner of the organization
392+
isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID)
393+
if err != nil {
394+
ctx.APIErrorInternal(err)
395+
return
396+
}
397+
if !isOwner && !ctx.Doer.IsAdmin {
398+
ctx.APIError(http.StatusForbidden, "only organization owners and site admins can archive/unarchive organizations")
399+
return
400+
}
401+
402+
if err := ctx.Org.Organization.SetArchived(ctx, *form.Archived); err != nil {
403+
ctx.APIErrorInternal(err)
404+
return
405+
}
406+
}
407+
389408
opts := &user_service.UpdateOptions{
390409
FullName: optional.Some(form.FullName),
391410
Description: optional.Some(form.Description),

routers/api/v1/repo/repo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,11 @@ func CreateOrgRepo(ctx *context.APIContext) {
510510
return
511511
}
512512

513+
if err := org.MustNotBeArchived(ctx); err != nil {
514+
ctx.APIError(http.StatusForbidden, err)
515+
return
516+
}
517+
513518
if !ctx.Doer.IsAdmin {
514519
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
515520
if err != nil {

routers/web/org/setting.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func Settings(ctx *context.Context) {
4545
ctx.Data["PageIsSettingsOptions"] = true
4646
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
4747
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
48+
ctx.Data["IsArchived"] = ctx.Org.Organization.IsArchived(ctx)
4849
ctx.Data["ContextUser"] = ctx.ContextUser
4950

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

169+
// SettingsArchive archives an organization
170+
func SettingsArchive(ctx *context.Context) {
171+
if !ctx.Org.IsOwner {
172+
ctx.JSONError(ctx.Tr("org.settings.archive_not_allowed"))
173+
return
174+
}
175+
176+
org := ctx.Org.Organization
177+
orgName := ctx.FormString("org_name")
178+
179+
if orgName != org.Name {
180+
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
181+
return
182+
}
183+
184+
if err := org.SetArchived(ctx, true); err != nil {
185+
ctx.ServerError("SetArchived", err)
186+
return
187+
}
188+
189+
ctx.Flash.Success(ctx.Tr("org.settings.archive_success"))
190+
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
191+
}
192+
193+
// SettingsUnarchive unarchives an organization
194+
func SettingsUnarchive(ctx *context.Context) {
195+
if !ctx.Org.IsOwner {
196+
ctx.JSONError(ctx.Tr("org.settings.archive_not_allowed"))
197+
return
198+
}
199+
200+
org := ctx.Org.Organization
201+
orgName := ctx.FormString("org_name")
202+
203+
if orgName != org.Name {
204+
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
205+
return
206+
}
207+
208+
if err := org.SetArchived(ctx, false); err != nil {
209+
ctx.ServerError("SetArchived", err)
210+
return
211+
}
212+
213+
ctx.Flash.Success(ctx.Tr("org.settings.unarchive_success"))
214+
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
215+
}
216+
168217
// Webhooks render webhook list page
169218
func Webhooks(ctx *context.Context) {
170219
ctx.Data["Title"] = ctx.Tr("org.settings")

routers/web/repo/fork.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ func ForkPost(ctx *context.Context) {
143143
return
144144
}
145145

146+
if ctxUser.IsOrganization() {
147+
org := organization.OrgFromUser(ctxUser)
148+
if err := org.MustNotBeArchived(ctx); err != nil {
149+
ctx.JSONError(ctx.Tr("org.archived_create_repo_not_allowed"))
150+
return
151+
}
152+
}
153+
146154
forkRepo := getForkRepository(ctx)
147155
if ctx.Written() {
148156
return

routers/web/repo/githttp.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ func httpBase(ctx *context.Context) *serviceHandler {
118118
repoExist = false
119119
}
120120

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

routers/web/repo/release.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func Releases(ctx *context.Context) {
165165
}
166166

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

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

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

279279
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
280280
ListOptions: db.ListOptions{Page: 1, PageSize: 1},

0 commit comments

Comments
 (0)