Skip to content

Commit c42c31a

Browse files
authored
Correctly set the organization num repos (#11339)
* Correctly set the organization num repos Correctly set the organization num repos to the number of accessible repos for the user Fix #11194 Signed-off-by: Andrew Thornton <[email protected]> * as per @lunny Signed-off-by: Andrew Thornton <[email protected]> * attempt to fix mssql Signed-off-by: Andrew Thornton <[email protected]> * Update models/user.go * Explicit columns Signed-off-by: Andrew Thornton <[email protected]> * Add test and fix 0 counted orgs Signed-off-by: Andrew Thornton <[email protected]> * remove orgname from api Signed-off-by: Andrew Thornton <[email protected]>
1 parent 45968b9 commit c42c31a

File tree

4 files changed

+267
-10
lines changed

4 files changed

+267
-10
lines changed

integrations/api_helper_for_declarative_test.go

+83
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,86 @@ func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFil
266266
}
267267
}
268268
}
269+
270+
func doAPICreateOrganization(ctx APITestContext, options *api.CreateOrgOption, callback ...func(*testing.T, api.Organization)) func(t *testing.T) {
271+
return func(t *testing.T) {
272+
url := fmt.Sprintf("/api/v1/orgs?token=%s", ctx.Token)
273+
274+
req := NewRequestWithJSON(t, "POST", url, &options)
275+
if ctx.ExpectedCode != 0 {
276+
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
277+
return
278+
}
279+
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
280+
281+
var contents api.Organization
282+
DecodeJSON(t, resp, &contents)
283+
if len(callback) > 0 {
284+
callback[0](t, contents)
285+
}
286+
}
287+
}
288+
289+
func doAPICreateOrganizationRepository(ctx APITestContext, orgName string, options *api.CreateRepoOption, callback ...func(*testing.T, api.Repository)) func(t *testing.T) {
290+
return func(t *testing.T) {
291+
url := fmt.Sprintf("/api/v1/orgs/%s/repos?token=%s", orgName, ctx.Token)
292+
293+
req := NewRequestWithJSON(t, "POST", url, &options)
294+
if ctx.ExpectedCode != 0 {
295+
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
296+
return
297+
}
298+
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
299+
300+
var contents api.Repository
301+
DecodeJSON(t, resp, &contents)
302+
if len(callback) > 0 {
303+
callback[0](t, contents)
304+
}
305+
}
306+
}
307+
308+
func doAPICreateOrganizationTeam(ctx APITestContext, orgName string, options *api.CreateTeamOption, callback ...func(*testing.T, api.Team)) func(t *testing.T) {
309+
return func(t *testing.T) {
310+
url := fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", orgName, ctx.Token)
311+
312+
req := NewRequestWithJSON(t, "POST", url, &options)
313+
if ctx.ExpectedCode != 0 {
314+
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
315+
return
316+
}
317+
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
318+
319+
var contents api.Team
320+
DecodeJSON(t, resp, &contents)
321+
if len(callback) > 0 {
322+
callback[0](t, contents)
323+
}
324+
}
325+
}
326+
327+
func doAPIAddUserToOrganizationTeam(ctx APITestContext, teamID int64, username string) func(t *testing.T) {
328+
return func(t *testing.T) {
329+
url := fmt.Sprintf("/api/v1/teams/%d/members/%s?token=%s", teamID, username, ctx.Token)
330+
331+
req := NewRequest(t, "PUT", url)
332+
if ctx.ExpectedCode != 0 {
333+
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
334+
return
335+
}
336+
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
337+
}
338+
}
339+
340+
func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, repoName string) func(t *testing.T) {
341+
return func(t *testing.T) {
342+
url := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s?token=%s", teamID, orgName, repoName, ctx.Token)
343+
344+
req := NewRequest(t, "PUT", url)
345+
if ctx.ExpectedCode != 0 {
346+
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
347+
return
348+
}
349+
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
350+
}
351+
}

integrations/org_count_test.go

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2020 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 integrations
6+
7+
import (
8+
"net/url"
9+
"strings"
10+
"testing"
11+
12+
"code.gitea.io/gitea/models"
13+
api "code.gitea.io/gitea/modules/structs"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestOrgCounts(t *testing.T) {
18+
onGiteaRun(t, testOrgCounts)
19+
}
20+
21+
func testOrgCounts(t *testing.T, u *url.URL) {
22+
orgOwner := "user2"
23+
orgName := "testOrg"
24+
orgCollaborator := "user4"
25+
ctx := NewAPITestContext(t, orgOwner, "repo1")
26+
27+
var ownerCountRepos map[string]int
28+
var collabCountRepos map[string]int
29+
30+
t.Run("GetTheOwnersNumRepos", doCheckOrgCounts(orgOwner, map[string]int{},
31+
false,
32+
func(_ *testing.T, calcOrgCounts map[string]int) {
33+
ownerCountRepos = calcOrgCounts
34+
},
35+
))
36+
t.Run("GetTheCollaboratorsNumRepos", doCheckOrgCounts(orgCollaborator, map[string]int{},
37+
false,
38+
func(_ *testing.T, calcOrgCounts map[string]int) {
39+
collabCountRepos = calcOrgCounts
40+
},
41+
))
42+
43+
t.Run("CreatePublicTestOrganization", doAPICreateOrganization(ctx, &api.CreateOrgOption{
44+
UserName: orgName,
45+
Visibility: "public",
46+
}))
47+
48+
// Following the creation of the organization, the orgName must appear in the counts with 0 repos
49+
ownerCountRepos[orgName] = 0
50+
51+
t.Run("AssertNumRepos0ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
52+
53+
// the collaborator is not a collaborator yet
54+
t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))
55+
56+
t.Run("CreateOrganizationPrivateRepo", doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{
57+
Name: "privateTestRepo",
58+
AutoInit: true,
59+
Private: true,
60+
}))
61+
62+
ownerCountRepos[orgName] = 1
63+
t.Run("AssertNumRepos1ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
64+
65+
t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))
66+
67+
var testTeam api.Team
68+
69+
t.Run("CreateTeamForPublicTestOrganization", doAPICreateOrganizationTeam(ctx, orgName, &api.CreateTeamOption{
70+
Name: "test",
71+
Permission: "read",
72+
Units: []string{"repo.code", "repo.issues", "repo.wiki", "repo.pulls", "repo.releases"},
73+
CanCreateOrgRepo: true,
74+
}, func(_ *testing.T, team api.Team) {
75+
testTeam = team
76+
}))
77+
78+
t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))
79+
80+
t.Run("AddCollboratorToTeam", doAPIAddUserToOrganizationTeam(ctx, testTeam.ID, orgCollaborator))
81+
82+
collabCountRepos[orgName] = 0
83+
t.Run("AssertNumRepos0ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
84+
85+
// Now create a Public Repo
86+
t.Run("CreateOrganizationPublicRepo", doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{
87+
Name: "publicTestRepo",
88+
AutoInit: true,
89+
}))
90+
91+
ownerCountRepos[orgName] = 2
92+
t.Run("AssertNumRepos2ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
93+
collabCountRepos[orgName] = 1
94+
t.Run("AssertNumRepos1ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
95+
96+
// Now add the testTeam to the privateRepo
97+
t.Run("AddTestTeamToPrivateRepo", doAPIAddRepoToOrganizationTeam(ctx, testTeam.ID, orgName, "privateTestRepo"))
98+
99+
t.Run("AssertNumRepos2ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
100+
collabCountRepos[orgName] = 2
101+
t.Run("AssertNumRepos2ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
102+
}
103+
104+
func doCheckOrgCounts(username string, orgCounts map[string]int, strict bool, callback ...func(*testing.T, map[string]int)) func(t *testing.T) {
105+
canonicalCounts := make(map[string]int, len(orgCounts))
106+
107+
for key, value := range orgCounts {
108+
newKey := strings.TrimSpace(strings.ToLower(key))
109+
canonicalCounts[newKey] = value
110+
}
111+
112+
return func(t *testing.T) {
113+
user := models.AssertExistsAndLoadBean(t, &models.User{
114+
Name: username,
115+
}).(*models.User)
116+
117+
user.GetOrganizations(&models.SearchOrganizationsOptions{All: true})
118+
119+
calcOrgCounts := map[string]int{}
120+
121+
for _, org := range user.Orgs {
122+
calcOrgCounts[org.LowerName] = org.NumRepos
123+
count, ok := canonicalCounts[org.LowerName]
124+
if ok {
125+
assert.True(t, count == org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count)
126+
} else {
127+
assert.False(t, strict, "Did not expect to see %s with count %d", org.Name, org.NumRepos)
128+
}
129+
}
130+
131+
for key, value := range orgCounts {
132+
_, seen := calcOrgCounts[strings.TrimSpace(strings.ToLower(key))]
133+
assert.True(t, seen, "Expected to see %s with %d but did not", key, value)
134+
}
135+
136+
if len(callback) > 0 {
137+
callback[0](t, calcOrgCounts)
138+
}
139+
}
140+
}

models/user.go

+41-7
Original file line numberDiff line numberDiff line change
@@ -712,18 +712,52 @@ func (u *User) GetOwnedOrganizations() (err error) {
712712

713713
// GetOrganizations returns paginated organizations that user belongs to.
714714
func (u *User) GetOrganizations(opts *SearchOrganizationsOptions) error {
715-
ous, err := GetOrgUsersByUserID(u.ID, opts)
715+
sess := x.NewSession()
716+
defer sess.Close()
717+
718+
schema, err := x.TableInfo(new(User))
716719
if err != nil {
717720
return err
718721
}
722+
groupByCols := &strings.Builder{}
723+
for _, col := range schema.Columns() {
724+
fmt.Fprintf(groupByCols, "`%s`.%s,", schema.Name, col.Name)
725+
}
726+
groupByStr := groupByCols.String()
727+
groupByStr = groupByStr[0 : len(groupByStr)-1]
719728

720-
u.Orgs = make([]*User, len(ous))
721-
for i, ou := range ous {
722-
u.Orgs[i], err = GetUserByID(ou.OrgID)
723-
if err != nil {
724-
return err
725-
}
729+
sess.Select("`user`.*, count(repo_id) as org_count").
730+
Table("user").
731+
Join("INNER", "org_user", "`org_user`.org_id=`user`.id").
732+
Join("LEFT", builder.
733+
Select("id as repo_id, owner_id as repo_owner_id").
734+
From("repository").
735+
Where(accessibleRepositoryCondition(u)), "`repository`.repo_owner_id = `org_user`.org_id").
736+
And("`org_user`.uid=?", u.ID).
737+
GroupBy(groupByStr)
738+
if opts.PageSize != 0 {
739+
sess = opts.setSessionPagination(sess)
726740
}
741+
type OrgCount struct {
742+
User `xorm:"extends"`
743+
OrgCount int
744+
}
745+
orgCounts := make([]*OrgCount, 0, 10)
746+
747+
if err := sess.
748+
Asc("`user`.name").
749+
Find(&orgCounts); err != nil {
750+
return err
751+
}
752+
753+
orgs := make([]*User, len(orgCounts))
754+
for i, orgCount := range orgCounts {
755+
orgCount.User.NumRepos = orgCount.OrgCount
756+
orgs[i] = &orgCount.User
757+
}
758+
759+
u.Orgs = orgs
760+
727761
return nil
728762
}
729763

routers/api/v1/api.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ func orgAssignment(args ...bool) macaron.Handler {
383383

384384
var err error
385385
if assignOrg {
386-
ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":orgname"))
386+
ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":org"))
387387
if err != nil {
388388
if models.IsErrOrgNotExist(err) {
389389
ctx.NotFound()
@@ -857,7 +857,7 @@ func RegisterRoutes(m *macaron.Macaron) {
857857
m.Get("/users/:username/orgs", org.ListUserOrgs)
858858
m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create)
859859
m.Get("/orgs", org.GetAll)
860-
m.Group("/orgs/:orgname", func() {
860+
m.Group("/orgs/:org", func() {
861861
m.Combo("").Get(org.Get).
862862
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
863863
Delete(reqToken(), reqOrgOwnership(), org.Delete)
@@ -907,7 +907,7 @@ func RegisterRoutes(m *macaron.Macaron) {
907907
})
908908
m.Group("/repos", func() {
909909
m.Get("", org.GetTeamRepos)
910-
m.Combo("/:orgname/:reponame").
910+
m.Combo("/:org/:reponame").
911911
Put(org.AddTeamRepository).
912912
Delete(org.RemoveTeamRepository)
913913
})

0 commit comments

Comments
 (0)