Skip to content

Commit 8d99ee2

Browse files
mrsdizzielafriksguillep2k6543
authored
Add Organization Wide Labels (#10814)
* Add organization wide labels Implement organization wide labels similar to organization wide webhooks. This lets you create individual labels for organizations that can be used for all repos under that organization (so being able to reuse the same label across multiple repos). This makes it possible for small organizations with many repos to use labels effectively. Fixes #7406 * Add migration * remove comments * fix tests * Update options/locale/locale_en-US.ini Removed unused translation string * show org labels in issue search label filter * Use more clear var name * rename migration after merge from master * comment typo * update migration again after rebase with master * check for orgID <=0 per guillep2k review * fmt * Apply suggestions from code review Co-Authored-By: guillep2k <[email protected]> * remove unused code * Make sure RepoID is 0 when searching orgID per code review * more changes/code review requests * More descriptive translation var per code review * func description/delete comment when issue label deleted instead of hiding it * remove comment * only use issues in that repo when calculating number of open issues for org label on repo label page * Add integration test for IssuesSearch API with labels * remove unused function * Update models/issue_label.go Co-Authored-By: guillep2k <[email protected]> * Use subquery in GetLabelIDsInReposByNames * Fix tests to use correct orgID * fix more tests * IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well * update comment for clarity * Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition * Don't sort repos by date in IssuesSearch API After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures: SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45 Returns different results for MySQL than other engines. However, the similar query: SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30 Returns the same results. This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function. * linter is back! * code review * remove now unused option * Fix newline at end of files * more unused code * update to master * check for matching ids before query * Update models/issue_label.go Co-Authored-By: 6543 <[email protected]> * Update models/issue_label.go * update comments * Update routers/org/setting.go Co-authored-by: Lauris BH <[email protected]> Co-authored-by: guillep2k <[email protected]> Co-authored-by: 6543 <[email protected]>
1 parent 36d9237 commit 8d99ee2

39 files changed

+1627
-315
lines changed

integrations/api_issue_label_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,74 @@ func TestAPIReplaceIssueLabels(t *testing.T) {
134134
models.AssertCount(t, &models.IssueLabel{IssueID: issue.ID}, 1)
135135
models.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
136136
}
137+
138+
func TestAPIModifyOrgLabels(t *testing.T) {
139+
assert.NoError(t, models.LoadFixtures())
140+
141+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)
142+
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
143+
user := "user1"
144+
session := loginUser(t, user)
145+
token := getTokenForLoggedInUser(t, session)
146+
urlStr := fmt.Sprintf("/api/v1/orgs/%s/labels?token=%s", owner.Name, token)
147+
148+
// CreateLabel
149+
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
150+
Name: "TestL 1",
151+
Color: "abcdef",
152+
Description: "test label",
153+
})
154+
resp := session.MakeRequest(t, req, http.StatusCreated)
155+
apiLabel := new(api.Label)
156+
DecodeJSON(t, resp, &apiLabel)
157+
dbLabel := models.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, OrgID: owner.ID}).(*models.Label)
158+
assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
159+
assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
160+
161+
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
162+
Name: "TestL 2",
163+
Color: "#123456",
164+
Description: "jet another test label",
165+
})
166+
session.MakeRequest(t, req, http.StatusCreated)
167+
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
168+
Name: "WrongTestL",
169+
Color: "#12345g",
170+
})
171+
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
172+
173+
//ListLabels
174+
req = NewRequest(t, "GET", urlStr)
175+
resp = session.MakeRequest(t, req, http.StatusOK)
176+
var apiLabels []*api.Label
177+
DecodeJSON(t, resp, &apiLabels)
178+
assert.Len(t, apiLabels, 4)
179+
180+
//GetLabel
181+
singleURLStr := fmt.Sprintf("/api/v1/orgs/%s/labels/%d?token=%s", owner.Name, dbLabel.ID, token)
182+
req = NewRequest(t, "GET", singleURLStr)
183+
resp = session.MakeRequest(t, req, http.StatusOK)
184+
DecodeJSON(t, resp, &apiLabel)
185+
assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
186+
187+
//EditLabel
188+
newName := "LabelNewName"
189+
newColor := "09876a"
190+
newColorWrong := "09g76a"
191+
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
192+
Name: &newName,
193+
Color: &newColor,
194+
})
195+
resp = session.MakeRequest(t, req, http.StatusOK)
196+
DecodeJSON(t, resp, &apiLabel)
197+
assert.EqualValues(t, newColor, apiLabel.Color)
198+
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
199+
Color: &newColorWrong,
200+
})
201+
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
202+
203+
//DeleteLabel
204+
req = NewRequest(t, "DELETE", singleURLStr)
205+
resp = session.MakeRequest(t, req, http.StatusNoContent)
206+
207+
}

integrations/api_issue_test.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func TestAPIEditIssue(t *testing.T) {
130130
assert.Equal(t, title, issueAfter.Title)
131131
}
132132

133-
func TestAPISearchIssue(t *testing.T) {
133+
func TestAPISearchIssues(t *testing.T) {
134134
defer prepareTestEnv(t)()
135135

136136
session := loginUser(t, "user2")
@@ -173,3 +173,66 @@ func TestAPISearchIssue(t *testing.T) {
173173
DecodeJSON(t, resp, &apiIssues)
174174
assert.Len(t, apiIssues, 1)
175175
}
176+
177+
func TestAPISearchIssuesWithLabels(t *testing.T) {
178+
defer prepareTestEnv(t)()
179+
180+
session := loginUser(t, "user1")
181+
token := getTokenForLoggedInUser(t, session)
182+
183+
link, _ := url.Parse("/api/v1/repos/issues/search")
184+
req := NewRequest(t, "GET", link.String())
185+
resp := session.MakeRequest(t, req, http.StatusOK)
186+
var apiIssues []*api.Issue
187+
DecodeJSON(t, resp, &apiIssues)
188+
189+
assert.Len(t, apiIssues, 9)
190+
191+
query := url.Values{}
192+
query.Add("token", token)
193+
link.RawQuery = query.Encode()
194+
req = NewRequest(t, "GET", link.String())
195+
resp = session.MakeRequest(t, req, http.StatusOK)
196+
DecodeJSON(t, resp, &apiIssues)
197+
assert.Len(t, apiIssues, 9)
198+
199+
query.Add("labels", "label1")
200+
link.RawQuery = query.Encode()
201+
req = NewRequest(t, "GET", link.String())
202+
resp = session.MakeRequest(t, req, http.StatusOK)
203+
DecodeJSON(t, resp, &apiIssues)
204+
assert.Len(t, apiIssues, 2)
205+
206+
// multiple labels
207+
query.Set("labels", "label1,label2")
208+
link.RawQuery = query.Encode()
209+
req = NewRequest(t, "GET", link.String())
210+
resp = session.MakeRequest(t, req, http.StatusOK)
211+
DecodeJSON(t, resp, &apiIssues)
212+
assert.Len(t, apiIssues, 2)
213+
214+
// an org label
215+
query.Set("labels", "orglabel4")
216+
link.RawQuery = query.Encode()
217+
req = NewRequest(t, "GET", link.String())
218+
resp = session.MakeRequest(t, req, http.StatusOK)
219+
DecodeJSON(t, resp, &apiIssues)
220+
assert.Len(t, apiIssues, 1)
221+
222+
// org and repo label
223+
query.Set("labels", "label2,orglabel4")
224+
query.Add("state", "all")
225+
link.RawQuery = query.Encode()
226+
req = NewRequest(t, "GET", link.String())
227+
resp = session.MakeRequest(t, req, http.StatusOK)
228+
DecodeJSON(t, resp, &apiIssues)
229+
assert.Len(t, apiIssues, 2)
230+
231+
// org and repo label which share the same issue
232+
query.Set("labels", "label1,orglabel4")
233+
link.RawQuery = query.Encode()
234+
req = NewRequest(t, "GET", link.String())
235+
resp = session.MakeRequest(t, req, http.StatusOK)
236+
DecodeJSON(t, resp, &apiIssues)
237+
assert.Len(t, apiIssues, 2)
238+
}

models/error.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,10 +1502,41 @@ func (err ErrTrackedTimeNotExist) Error() string {
15021502
// |_______ (____ /___ /\___ >____/
15031503
// \/ \/ \/ \/
15041504

1505+
// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
1506+
type ErrRepoLabelNotExist struct {
1507+
LabelID int64
1508+
RepoID int64
1509+
}
1510+
1511+
// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
1512+
func IsErrRepoLabelNotExist(err error) bool {
1513+
_, ok := err.(ErrRepoLabelNotExist)
1514+
return ok
1515+
}
1516+
1517+
func (err ErrRepoLabelNotExist) Error() string {
1518+
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
1519+
}
1520+
1521+
// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
1522+
type ErrOrgLabelNotExist struct {
1523+
LabelID int64
1524+
OrgID int64
1525+
}
1526+
1527+
// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
1528+
func IsErrOrgLabelNotExist(err error) bool {
1529+
_, ok := err.(ErrOrgLabelNotExist)
1530+
return ok
1531+
}
1532+
1533+
func (err ErrOrgLabelNotExist) Error() string {
1534+
return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
1535+
}
1536+
15051537
// ErrLabelNotExist represents a "LabelNotExist" kind of error.
15061538
type ErrLabelNotExist struct {
15071539
LabelID int64
1508-
RepoID int64
15091540
}
15101541

15111542
// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
@@ -1515,7 +1546,7 @@ func IsErrLabelNotExist(err error) bool {
15151546
}
15161547

15171548
func (err ErrLabelNotExist) Error() string {
1518-
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
1549+
return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
15191550
}
15201551

15211552
// _____ .__.__ __

models/fixtures/issue_label.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@
1212
id: 3
1313
issue_id: 2
1414
label_id: 1
15+
16+
-
17+
id: 4
18+
issue_id: 2
19+
label_id: 4

models/fixtures/label.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
-
22
id: 1
33
repo_id: 1
4+
org_id: 0
45
name: label1
56
color: '#abcdef'
67
num_issues: 2
@@ -9,7 +10,26 @@
910
-
1011
id: 2
1112
repo_id: 1
13+
org_id: 0
1214
name: label2
1315
color: '#000000'
1416
num_issues: 1
1517
num_closed_issues: 1
18+
-
19+
id: 3
20+
repo_id: 0
21+
org_id: 3
22+
name: orglabel3
23+
color: '#abcdef'
24+
num_issues: 0
25+
num_closed_issues: 0
26+
27+
-
28+
id: 4
29+
repo_id: 0
30+
org_id: 3
31+
name: orglabel4
32+
color: '#000000'
33+
num_issues: 1
34+
num_closed_issues: 0
35+

models/issue.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
459459
return err
460460
}
461461
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
462-
return ErrLabelNotExist{}
462+
return ErrRepoLabelNotExist{}
463463
}
464464

465465
if err = issue.clearLabels(sess, doer); err != nil {
@@ -894,7 +894,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
894894

895895
for _, label := range labels {
896896
// Silently drop invalid labels.
897-
if label.RepoID != opts.Repo.ID {
897+
if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID {
898898
continue
899899
}
900900

0 commit comments

Comments
 (0)