Skip to content

Commit a0e88df

Browse files
davidsvantessonlafriks
authored andcommitted
Add teams to repo on collaboration page. (#8045)
* Add teams to repo on collaboration page. Signed-off-by: David Svantesson <[email protected]> * Add option for repository admins to change teams access to repo. Signed-off-by: David Svantesson <[email protected]> * Add comment for functions Signed-off-by: David Svantesson <[email protected]> * Make RepoAdminChangeTeamAccess default false in xorm and make it default checked in template instead. Signed-off-by: David Svantesson <[email protected]> * Make proper language strings and fix error redirection. * Add unit tests for adding and deleting team from repository. Signed-off-by: David Svantesson <[email protected]> * Add database migration Signed-off-by: David Svantesson <[email protected]> * Fix redirect Signed-off-by: David Svantesson <[email protected]> * Fix locale string mismatch. Signed-off-by: David Svantesson <[email protected]> * Move team access mode text logic to template. * Move collaborator access mode text logic to template.
1 parent 63ff616 commit a0e88df

30 files changed

+575
-79
lines changed

models/error.go

+17
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,23 @@ func (err ErrTeamAlreadyExist) Error() string {
13701370
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
13711371
}
13721372

1373+
// ErrTeamNotExist represents a "TeamNotExist" error
1374+
type ErrTeamNotExist struct {
1375+
OrgID int64
1376+
TeamID int64
1377+
Name string
1378+
}
1379+
1380+
// IsErrTeamNotExist checks if an error is a ErrTeamNotExist.
1381+
func IsErrTeamNotExist(err error) bool {
1382+
_, ok := err.(ErrTeamNotExist)
1383+
return ok
1384+
}
1385+
1386+
func (err ErrTeamNotExist) Error() string {
1387+
return fmt.Sprintf("team does not exist [org_id %d, team_id %d, name: %s]", err.OrgID, err.TeamID, err.Name)
1388+
}
1389+
13731390
//
13741391
// Two-factor authentication
13751392
//

models/fixtures/repository.yml

+11
Original file line numberDiff line numberDiff line change
@@ -508,4 +508,15 @@
508508
num_stars: 0
509509
num_forks: 0
510510
num_issues: 0
511+
is_mirror: false
512+
513+
-
514+
id: 43
515+
owner_id: 26
516+
lower_name: repo26
517+
name: repo26
518+
is_private: true
519+
num_stars: 0
520+
num_forks: 0
521+
num_issues: 0
511522
is_mirror: false

models/fixtures/team.yml

+9
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,12 @@
8787
authorize: 1 # owner
8888
num_repos: 0
8989
num_members: 1
90+
91+
-
92+
id: 11
93+
org_id: 26
94+
lower_name: team11
95+
name: team11
96+
authorize: 1 # read
97+
num_repos: 0
98+
num_members: 0

models/fixtures/user.yml

+18
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,21 @@
410410
num_repos: 0
411411
num_members: 1
412412
num_teams: 1
413+
414+
-
415+
id: 26
416+
lower_name: org26
417+
name: org26
418+
full_name: "Org26"
419+
420+
email_notifications_preference: onmention
421+
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
422+
type: 1 # organization
423+
salt: ZogKvWdyEx
424+
is_admin: false
425+
avatar: avatar26
426+
avatar_email: [email protected]
427+
num_repos: 1
428+
num_members: 0
429+
num_teams: 1
430+
repo_admin_change_team_access: true

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ var migrations = []Migration{
248248
NewMigration("add table columns for cross referencing issues", addCrossReferenceColumns),
249249
// v96 -> v97
250250
NewMigration("delete orphaned attachments", deleteOrphanedAttachments),
251+
// v97 -> v98
252+
NewMigration("add repo_admin_change_team_access to user", addRepoAdminChangeTeamAccessColumnForUser),
251253
}
252254

253255
// Migrate database to current version

models/migrations/v97.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import "github.com/go-xorm/xorm"
8+
9+
func addRepoAdminChangeTeamAccessColumnForUser(x *xorm.Engine) error {
10+
type User struct {
11+
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
12+
}
13+
14+
return x.Sync2(new(User))
15+
}

models/org.go

+1-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
package models
77

88
import (
9-
"errors"
109
"fmt"
1110
"os"
1211
"strings"
@@ -20,11 +19,6 @@ import (
2019
"xorm.io/builder"
2120
)
2221

23-
var (
24-
// ErrTeamNotExist team does not exist
25-
ErrTeamNotExist = errors.New("Team does not exist")
26-
)
27-
2822
// IsOwnedBy returns true if given user is in the owner team.
2923
func (org *User) IsOwnedBy(uid int64) (bool, error) {
3024
return IsOrganizationOwner(org.ID, uid)
@@ -304,7 +298,7 @@ type OrgUser struct {
304298
func isOrganizationOwner(e Engine, orgID, uid int64) (bool, error) {
305299
ownerTeam, err := getOwnerTeam(e, orgID)
306300
if err != nil {
307-
if err == ErrTeamNotExist {
301+
if IsErrTeamNotExist(err) {
308302
log.Error("Organization does not have owner team: %d", orgID)
309303
return false, nil
310304
}

models/org_team.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ func getTeam(e Engine, orgID int64, name string) (*Team, error) {
352352
if err != nil {
353353
return nil, err
354354
} else if !has {
355-
return nil, ErrTeamNotExist
355+
return nil, ErrTeamNotExist{orgID, 0, name}
356356
}
357357
return t, nil
358358
}
@@ -373,7 +373,7 @@ func getTeamByID(e Engine, teamID int64) (*Team, error) {
373373
if err != nil {
374374
return nil, err
375375
} else if !has {
376-
return nil, ErrTeamNotExist
376+
return nil, ErrTeamNotExist{0, teamID, ""}
377377
}
378378
return t, nil
379379
}

models/org_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ func TestUser_GetTeam(t *testing.T) {
6464
assert.Equal(t, "team1", team.LowerName)
6565

6666
_, err = org.GetTeam("does not exist")
67-
assert.Equal(t, ErrTeamNotExist, err)
67+
assert.True(t, IsErrTeamNotExist(err))
6868

6969
nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
7070
_, err = nonOrg.GetTeam("team")
71-
assert.Equal(t, ErrTeamNotExist, err)
71+
assert.True(t, IsErrTeamNotExist(err))
7272
}
7373

7474
func TestUser_GetOwnerTeam(t *testing.T) {
@@ -80,7 +80,7 @@ func TestUser_GetOwnerTeam(t *testing.T) {
8080

8181
nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
8282
_, err = nonOrg.GetOwnerTeam()
83-
assert.Equal(t, ErrTeamNotExist, err)
83+
assert.True(t, IsErrTeamNotExist(err))
8484
}
8585

8686
func TestUser_GetTeams(t *testing.T) {

models/repo_collaboration.go

+14-14
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,6 @@ type Collaboration struct {
1616
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"`
1717
}
1818

19-
// ModeI18nKey returns the collaboration mode I18n Key
20-
func (c *Collaboration) ModeI18nKey() string {
21-
switch c.Mode {
22-
case AccessModeRead:
23-
return "repo.settings.collaboration.read"
24-
case AccessModeWrite:
25-
return "repo.settings.collaboration.write"
26-
case AccessModeAdmin:
27-
return "repo.settings.collaboration.admin"
28-
default:
29-
return "repo.settings.collaboration.undefined"
30-
}
31-
}
32-
3319
// AddCollaborator adds new collaboration to a repository with default access mode.
3420
func (repo *Repository) AddCollaborator(u *User) error {
3521
collaboration := &Collaboration{
@@ -183,3 +169,17 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) {
183169

184170
return sess.Commit()
185171
}
172+
173+
func (repo *Repository) getRepoTeams(e Engine) (teams []*Team, err error) {
174+
return teams, e.
175+
Join("INNER", "team_repo", "team_repo.team_id = team.id").
176+
Where("team.org_id = ?", repo.OwnerID).
177+
And("team_repo.repo_id=?", repo.ID).
178+
OrderBy("CASE WHEN name LIKE '" + ownerTeamName + "' THEN '' ELSE name END").
179+
Find(&teams)
180+
}
181+
182+
// GetRepoTeams gets the list of teams that has access to the repository
183+
func (repo *Repository) GetRepoTeams() ([]*Team, error) {
184+
return repo.getRepoTeams(x)
185+
}

models/repo_collaboration_test.go

-11
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,6 @@ import (
1010
"github.com/stretchr/testify/assert"
1111
)
1212

13-
func TestCollaboration_ModeI18nKey(t *testing.T) {
14-
assert.Equal(t, "repo.settings.collaboration.read",
15-
(&Collaboration{Mode: AccessModeRead}).ModeI18nKey())
16-
assert.Equal(t, "repo.settings.collaboration.write",
17-
(&Collaboration{Mode: AccessModeWrite}).ModeI18nKey())
18-
assert.Equal(t, "repo.settings.collaboration.admin",
19-
(&Collaboration{Mode: AccessModeAdmin}).ModeI18nKey())
20-
assert.Equal(t, "repo.settings.collaboration.undefined",
21-
(&Collaboration{Mode: AccessModeNone}).ModeI18nKey())
22-
}
23-
2413
func TestRepository_AddCollaborator(t *testing.T) {
2514
assert.NoError(t, PrepareTestDatabase())
2615

models/user.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,13 @@ type User struct {
147147
NumRepos int
148148

149149
// For organization
150-
NumTeams int
151-
NumMembers int
152-
Teams []*Team `xorm:"-"`
153-
Members UserList `xorm:"-"`
154-
MembersIsPublic map[int64]bool `xorm:"-"`
155-
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
150+
NumTeams int
151+
NumMembers int
152+
Teams []*Team `xorm:"-"`
153+
Members UserList `xorm:"-"`
154+
MembersIsPublic map[int64]bool `xorm:"-"`
155+
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
156+
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
156157

157158
// Preferences
158159
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`

models/user_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ func TestSearchUsers(t *testing.T) {
140140
testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 3, PageSize: 2},
141141
[]int64{19, 25})
142142

143-
testOrgSuccess(&SearchUserOptions{Page: 4, PageSize: 2},
143+
testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 4, PageSize: 2},
144+
[]int64{26})
145+
146+
testOrgSuccess(&SearchUserOptions{Page: 5, PageSize: 2},
144147
[]int64{})
145148

146149
// test users

models/userlist.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (users UserList) loadOrganizationOwners(e Engine, orgID int64) (map[int64]*
4343
}
4444
ownerTeam, err := getOwnerTeam(e, orgID)
4545
if err != nil {
46-
if err == ErrTeamNotExist {
46+
if IsErrTeamNotExist(err) {
4747
log.Error("Organization does not have owner team: %d", orgID)
4848
return nil, nil
4949
}

modules/auth/org.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@ func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) bind
3333

3434
// UpdateOrgSettingForm form for updating organization settings
3535
type UpdateOrgSettingForm struct {
36-
Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
37-
FullName string `binding:"MaxSize(100)"`
38-
Description string `binding:"MaxSize(255)"`
39-
Website string `binding:"ValidUrl;MaxSize(255)"`
40-
Location string `binding:"MaxSize(50)"`
41-
Visibility structs.VisibleType
42-
MaxRepoCreation int
36+
Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
37+
FullName string `binding:"MaxSize(100)"`
38+
Description string `binding:"MaxSize(255)"`
39+
Website string `binding:"ValidUrl;MaxSize(255)"`
40+
Location string `binding:"MaxSize(50)"`
41+
Visibility structs.VisibleType
42+
MaxRepoCreation int
43+
RepoAdminChangeTeamAccess bool
4344
}
4445

4546
// Validate validates the fields

modules/structs/org.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ package structs
66

77
// Organization represents an organization
88
type Organization struct {
9-
ID int64 `json:"id"`
10-
UserName string `json:"username"`
11-
FullName string `json:"full_name"`
12-
AvatarURL string `json:"avatar_url"`
13-
Description string `json:"description"`
14-
Website string `json:"website"`
15-
Location string `json:"location"`
16-
Visibility string `json:"visibility"`
9+
ID int64 `json:"id"`
10+
UserName string `json:"username"`
11+
FullName string `json:"full_name"`
12+
AvatarURL string `json:"avatar_url"`
13+
Description string `json:"description"`
14+
Website string `json:"website"`
15+
Location string `json:"location"`
16+
Visibility string `json:"visibility"`
17+
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
1718
}
1819

1920
// CreateOrgOption options for creating an organization
@@ -26,7 +27,8 @@ type CreateOrgOption struct {
2627
Location string `json:"location"`
2728
// possible values are `public` (default), `limited` or `private`
2829
// enum: public,limited,private
29-
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
30+
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
31+
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
3032
}
3133

3234
// EditOrgOption options for editing an organization
@@ -37,5 +39,6 @@ type EditOrgOption struct {
3739
Location string `json:"location"`
3840
// possible values are `public`, `limited` or `private`
3941
// enum: public,limited,private
40-
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
42+
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
43+
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
4144
}

options/locale/locale_en-US.ini

+9
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ enterred_invalid_repo_name = The repository name you entered is incorrect.
319319
enterred_invalid_owner_name = The new owner name is not valid.
320320
enterred_invalid_password = The password you entered is incorrect.
321321
user_not_exist = The user does not exist.
322+
team_not_exist = The team does not exist.
322323
last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner in any given team.
323324
cannot_add_org_to_team = An organization cannot be added as a team member.
324325

@@ -1136,6 +1137,7 @@ settings.collaboration = Collaborators
11361137
settings.collaboration.admin = Administrator
11371138
settings.collaboration.write = Write
11381139
settings.collaboration.read = Read
1140+
settings.collaboration.owner = Owner
11391141
settings.collaboration.undefined = Undefined
11401142
settings.hooks = Webhooks
11411143
settings.githooks = Git Hooks
@@ -1217,6 +1219,11 @@ settings.collaborator_deletion_desc = Removing a collaborator will revoke their
12171219
settings.remove_collaborator_success = The collaborator has been removed.
12181220
settings.search_user_placeholder = Search user…
12191221
settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator.
1222+
settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner
1223+
settings.team_not_in_organization = The team is not in the same organization as the repository
1224+
settings.add_team_duplicate = Team already has the repository
1225+
settings.add_team_success = The team now have access to the repository.
1226+
settings.remove_team_success = The team's access to the repository has been removed.
12201227
settings.add_webhook = Add Webhook
12211228
settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character.
12221229
settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Read more in the <a target="_blank" rel="noopener noreferrer" href="%s">webhooks guide</a>.
@@ -1475,6 +1482,8 @@ settings.options = Organization
14751482
settings.full_name = Full Name
14761483
settings.website = Website
14771484
settings.location = Location
1485+
settings.permission = Permissions
1486+
settings.repoadminchangeteam = Repository admin can add and remove access for teams
14781487
settings.visibility = Visibility
14791488
settings.visibility.public = Public
14801489
settings.visibility.limited = Limited (Visible to logged in users only)

public/css/index.css

+3
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,8 @@ footer .ui.left,footer .ui.right{line-height:40px}
747747
.repository.settings.collaboration .collaborator.list>.item:not(:last-child){border-bottom:1px solid #ddd}
748748
.repository.settings.collaboration #repo-collab-form #search-user-box .results{left:7px}
749749
.repository.settings.collaboration #repo-collab-form .ui.button{margin-left:5px;margin-top:-3px}
750+
.repository.settings.collaboration #repo-collab-team-form #search-team-box .results{left:7px}
751+
.repository.settings.collaboration #repo-collab-team-form .ui.button{margin-left:5px;margin-top:-3px}
750752
.repository.settings.branches .protected-branches .selection.dropdown{width:300px}
751753
.repository.settings.branches .protected-branches .item{border:1px solid #eaeaea;padding:10px 15px}
752754
.repository.settings.branches .protected-branches .item:not(:last-child){border-bottom:0}
@@ -783,6 +785,7 @@ footer .ui.left,footer .ui.right{line-height:40px}
783785
.user-cards .list .item .meta{margin-top:5px}
784786
#search-repo-box .results .result .image,#search-user-box .results .result .image{float:left;margin-right:8px;width:2em;height:2em}
785787
#search-repo-box .results .result .content,#search-user-box .results .result .content{margin:6px 0}
788+
#search-team-box .results .result .content{margin:6px 0}
786789
#issue-filters.hide{display:none}
787790
#issue-actions{margin-top:-1rem!important}
788791
#issue-actions.hide{display:none}

0 commit comments

Comments
 (0)