Skip to content

Commit

Permalink
Add organization wide labels
Browse files Browse the repository at this point in the history
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 go-gitea#7406
  • Loading branch information
mrsdizzie committed Mar 24, 2020
1 parent dcaa564 commit 6548c80
Show file tree
Hide file tree
Showing 33 changed files with 1,404 additions and 239 deletions.
71 changes: 71 additions & 0 deletions integrations/api_issue_label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,74 @@ func TestAPIReplaceIssueLabels(t *testing.T) {
models.AssertCount(t, &models.IssueLabel{IssueID: issue.ID}, 1)
models.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
}

func TestAPIModifyOrgLabels(t *testing.T) {
assert.NoError(t, models.LoadFixtures())

repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
user := "user1"
session := loginUser(t, user)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/orgs/%s/labels?token=%s", owner.Name, token)

// CreateLabel
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
Name: "TestL 1",
Color: "abcdef",
Description: "test label",
})
resp := session.MakeRequest(t, req, http.StatusCreated)
apiLabel := new(api.Label)
DecodeJSON(t, resp, &apiLabel)
dbLabel := models.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, OrgID: owner.ID}).(*models.Label)
assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)

req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
Name: "TestL 2",
Color: "#123456",
Description: "jet another test label",
})
session.MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
Name: "WrongTestL",
Color: "#12345g",
})
session.MakeRequest(t, req, http.StatusUnprocessableEntity)

//ListLabels
req = NewRequest(t, "GET", urlStr)
resp = session.MakeRequest(t, req, http.StatusOK)
var apiLabels []*api.Label
DecodeJSON(t, resp, &apiLabels)
assert.Len(t, apiLabels, 2)

//GetLabel
singleURLStr := fmt.Sprintf("/api/v1/orgs/%s/labels/%d?token=%s", owner.Name, dbLabel.ID, token)
req = NewRequest(t, "GET", singleURLStr)
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiLabel)
assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)

//EditLabel
newName := "LabelNewName"
newColor := "09876a"
newColorWrong := "09g76a"
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
Name: &newName,
Color: &newColor,
})
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiLabel)
assert.EqualValues(t, newColor, apiLabel.Color)
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
Color: &newColorWrong,
})
session.MakeRequest(t, req, http.StatusUnprocessableEntity)

//DeleteLabel
req = NewRequest(t, "DELETE", singleURLStr)
resp = session.MakeRequest(t, req, http.StatusNoContent)

}
28 changes: 22 additions & 6 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -1549,22 +1549,38 @@ func (err ErrTrackedTimeNotExist) Error() string {
// |_______ (____ /___ /\___ >____/
// \/ \/ \/ \/

// ErrLabelNotExist represents a "LabelNotExist" kind of error.
type ErrLabelNotExist struct {
// ErrRepoLabelNotExist represents a "LabelNotExist" kind of error.
type ErrRepoLabelNotExist struct {
LabelID int64
RepoID int64
}

// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
func IsErrLabelNotExist(err error) bool {
_, ok := err.(ErrLabelNotExist)
// IsErrRepoLabelNotExist checks if an error is a ErrLabelNotExist.
func IsErrRepoLabelNotExist(err error) bool {
_, ok := err.(ErrRepoLabelNotExist)
return ok
}

func (err ErrLabelNotExist) Error() string {
func (err ErrRepoLabelNotExist) Error() string {
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
}

// ErrOrgLabelNotExist represents a "LabelNotExist" kind of error.
type ErrOrgLabelNotExist struct {
LabelID int64
OrgID int64
}

// IsErrOrgLabelNotExist checks if an error is a ErrLabelNotExist.
func IsErrOrgLabelNotExist(err error) bool {
_, ok := err.(ErrOrgLabelNotExist)
return ok
}

func (err ErrOrgLabelNotExist) Error() string {
return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
}

// _____ .__.__ __
// / \ |__| | ____ _______/ |_ ____ ____ ____
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
Expand Down
5 changes: 5 additions & 0 deletions models/fixtures/issue_label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@
id: 3
issue_id: 2
label_id: 1

-
id: 4
issue_id: 2
label_id: 4
19 changes: 19 additions & 0 deletions models/fixtures/label.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-
id: 1
repo_id: 1
org_id: 0
name: label1
color: '#abcdef'
num_issues: 2
Expand All @@ -9,7 +10,25 @@
-
id: 2
repo_id: 1
org_id: 0
name: label2
color: '#000000'
num_issues: 1
num_closed_issues: 1
-
id: 3
repo_id: 0
org_id: 1
name: orglabel3
color: '#abcdef'
num_issues: 0
num_closed_issues: 0

-
id: 4
repo_id: 0
org_id: 1
name: orglabel4
color: '#000000'
num_issues: 1
num_closed_issues: 0
4 changes: 2 additions & 2 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
return err
}
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
return ErrLabelNotExist{}
return ErrRepoLabelNotExist{}
}

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

for _, label := range labels {
// Silently drop invalid labels.
if label.RepoID != opts.Repo.ID {
if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.Owner.ID {
continue
}

Expand Down
Loading

0 comments on commit 6548c80

Please sign in to comment.