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 <18600385+guillep2k@users.noreply.github.com>

* 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 <18600385+guillep2k@users.noreply.github.com>

* 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 <6543@obermui.de>

* Update models/issue_label.go

* update comments

* Update routers/org/setting.go

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
mrsdizzie 2020-04-01 00:14:46 -04:00 committed by GitHub
parent 36d9237e60
commit 8d99ee2773
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1627 additions and 315 deletions

View file

@ -861,6 +861,13 @@ func RegisterRoutes(m *macaron.Macaron) {
Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", org.SearchTeam)
}, reqOrgMembership())
m.Group("/labels", func() {
m.Get("", org.ListLabels)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
m.Combo("/:id").Get(org.GetLabel).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
})
m.Group("/hooks", func() {
m.Combo("").Get(org.ListHooks).
Post(bind(api.CreateHookOption{}), org.CreateHook)

237
routers/api/v1/org/label.go Normal file
View file

@ -0,0 +1,237 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package org
import (
"fmt"
"net/http"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
)
// ListLabels list all the labels of an organization
func ListLabels(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/labels organization orgListLabels
// ---
// summary: List an organization's labels
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results, maximum page size is 50
// type: integer
// responses:
// "200":
// "$ref": "#/responses/LabelList"
labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.Query("sort"), utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsByOrgID", err)
return
}
ctx.JSON(http.StatusOK, convert.ToLabelList(labels))
}
// CreateLabel create a label for a repository
func CreateLabel(ctx *context.APIContext, form api.CreateLabelOption) {
// swagger:operation POST /orgs/{org}/labels organization orgCreateLabel
// ---
// summary: Create a label for an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateLabelOption"
// responses:
// "201":
// "$ref": "#/responses/Label"
// "422":
// "$ref": "#/responses/validationError"
form.Color = strings.Trim(form.Color, " ")
if len(form.Color) == 6 {
form.Color = "#" + form.Color
}
if !models.LabelColorPattern.MatchString(form.Color) {
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color))
return
}
label := &models.Label{
Name: form.Name,
Color: form.Color,
OrgID: ctx.Org.Organization.ID,
Description: form.Description,
}
if err := models.NewLabel(label); err != nil {
ctx.Error(http.StatusInternalServerError, "NewLabel", err)
return
}
ctx.JSON(http.StatusCreated, convert.ToLabel(label))
}
// GetLabel get label by organization and label id
func GetLabel(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/labels/{id} organization orgGetLabel
// ---
// summary: Get a single label
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Label"
var (
label *models.Label
err error
)
strID := ctx.Params(":id")
if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
label, err = models.GetLabelInOrgByName(ctx.Org.Organization.ID, strID)
} else {
label, err = models.GetLabelInOrgByID(ctx.Org.Organization.ID, intID)
}
if err != nil {
if models.IsErrOrgLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByOrgID", err)
}
return
}
ctx.JSON(http.StatusOK, convert.ToLabel(label))
}
// EditLabel modify a label for an Organization
func EditLabel(ctx *context.APIContext, form api.EditLabelOption) {
// swagger:operation PATCH /orgs/{org}/labels/{id} organization orgEditLabel
// ---
// summary: Update a label
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditLabelOption"
// responses:
// "200":
// "$ref": "#/responses/Label"
// "422":
// "$ref": "#/responses/validationError"
label, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrOrgLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
}
return
}
if form.Name != nil {
label.Name = *form.Name
}
if form.Color != nil {
label.Color = strings.Trim(*form.Color, " ")
if len(label.Color) == 6 {
label.Color = "#" + label.Color
}
if !models.LabelColorPattern.MatchString(label.Color) {
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color))
return
}
}
if form.Description != nil {
label.Description = *form.Description
}
if err := models.UpdateLabel(label); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
ctx.JSON(http.StatusOK, convert.ToLabel(label))
}
// DeleteLabel delete a label for an organization
func DeleteLabel(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/labels/{id} organization orgDeleteLabel
// ---
// summary: Delete a label
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteLabel", err)
return
}
ctx.Status(http.StatusNoContent)
}

View file

@ -81,8 +81,10 @@ func SearchIssues(ctx *context.APIContext) {
AllPublic: true,
TopicOnly: false,
Collaborate: util.OptionalBoolNone,
OrderBy: models.SearchOrderByRecentUpdated,
Actor: ctx.User,
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: models.SearchOrderByAlphabetically,
Actor: ctx.User,
}
if ctx.IsSigned {
opts.Private = true
@ -152,6 +154,7 @@ func SearchIssues(ctx *context.APIContext) {
Page: ctx.QueryInt("page"),
PageSize: setting.UI.IssuePagingNum,
},
RepoIDs: repoIDs,
IsClosed: isClosed,
IssueIDs: issueIDs,

View file

@ -171,12 +171,12 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
label, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
label, err := models.GetLabelByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrLabelNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelInRepoByID", err)
ctx.Error(http.StatusInternalServerError, "GetLabelByID", err)
}
return
}
@ -308,9 +308,9 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
return
}
labels, err = models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
labels, err = models.GetLabelsByIDs(form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
ctx.Error(http.StatusInternalServerError, "GetLabelsByIDs", err)
return
}

View file

@ -96,7 +96,7 @@ func GetLabel(ctx *context.APIContext) {
label, err = models.GetLabelInRepoByID(ctx.Repo.Repository.ID, intID)
}
if err != nil {
if models.IsErrLabelNotExist(err) {
if models.IsErrRepoLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@ -197,7 +197,7 @@ func EditLabel(ctx *context.APIContext, form api.EditLabelOption) {
label, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrLabelNotExist(err) {
if models.IsErrRepoLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)

106
routers/org/org_labels.go Normal file
View file

@ -0,0 +1,106 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package org
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/context"
)
// RetrieveLabels find all the labels of an organization
func RetrieveLabels(ctx *context.Context) {
labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
return
}
for _, l := range labels {
l.CalOpenIssues()
}
ctx.Data["Labels"] = labels
ctx.Data["NumLabels"] = len(labels)
ctx.Data["SortType"] = ctx.Query("sort")
}
// NewLabel create new label for organization
func NewLabel(ctx *context.Context, form auth.CreateLabelForm) {
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsLabels"] = true
if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
return
}
l := &models.Label{
OrgID: ctx.Org.Organization.ID,
Name: form.Title,
Description: form.Description,
Color: form.Color,
}
if err := models.NewLabel(l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context, form auth.CreateLabelForm) {
l, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, form.ID)
if err != nil {
switch {
case models.IsErrOrgLabelNotExist(err):
ctx.Error(404)
default:
ctx.ServerError("UpdateLabel", err)
}
return
}
l.Name = form.Title
l.Description = form.Description
l.Color = form.Color
if err := models.UpdateLabel(l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
}
// DeleteLabel delete a label
func DeleteLabel(ctx *context.Context) {
if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteLabel: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/settings/labels",
})
}
// InitializeLabels init labels for an organization
func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
if ctx.HasError() {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
return
}
if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Org.Organization.ID, form.TemplateName, true); err != nil {
if models.IsErrIssueLabelTemplateLoad(err) {
originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
return
}
ctx.ServerError("InitializeLabels", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
}

View file

@ -24,6 +24,8 @@ const (
tplSettingsDelete base.TplName = "org/settings/delete"
// tplSettingsHooks template path for render hook settings
tplSettingsHooks base.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
tplSettingsLabels base.TplName = "org/settings/labels"
)
// Settings render the main settings page
@ -177,3 +179,13 @@ func DeleteWebhook(ctx *context.Context) {
"redirect": ctx.Org.OrgLink + "/settings/hooks",
})
}
// Labels render organization labels page
func Labels(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsOrgSettingsLabels"] = true
ctx.Data["RequireMinicolors"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["LabelTemplates"] = models.LabelTemplates
ctx.HTML(200, tplSettingsLabels)
}

View file

@ -335,7 +335,6 @@ func PrepareCompareDiff(
} else {
title = headBranch
}
ctx.Data["title"] = title
ctx.Data["Username"] = headUser.Name
ctx.Data["Reponame"] = headRepo.Name

View file

@ -259,6 +259,18 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
ctx.ServerError("GetLabelsByRepoID", err)
return
}
if repo.Owner.IsOrganization() {
orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
}
ctx.Data["OrgLabels"] = orgLabels
labels = append(labels, orgLabels...)
}
for _, l := range labels {
l.LoadSelectedLabelsAfterClick(labelIDs)
}
@ -377,6 +389,15 @@ func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull boo
return nil
}
ctx.Data["Labels"] = labels
if repo.Owner.IsOrganization() {
orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
return nil
}
ctx.Data["OrgLabels"] = orgLabels
labels = append(labels, orgLabels...)
}
RetrieveRepoMilestonesAndAssignees(ctx, repo)
if ctx.Written() {
@ -593,6 +614,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
Content: form.Content,
Ref: form.Ref,
}
if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
if models.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(400, "UserDoesNotHaveAccessToRepo", err.Error())
@ -761,6 +783,19 @@ func ViewIssue(ctx *context.Context) {
ctx.ServerError("GetLabelsByRepoID", err)
return
}
ctx.Data["Labels"] = labels
if repo.Owner.IsOrganization() {
orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
}
ctx.Data["OrgLabels"] = orgLabels
labels = append(labels, orgLabels...)
}
hasSelected := false
for i := range labels {
if labelIDMark[labels[i].ID] {
@ -769,7 +804,6 @@ func ViewIssue(ctx *context.Context) {
}
}
ctx.Data["HasSelectedLabel"] = hasSelected
ctx.Data["Labels"] = labels
// Check milestone and assignee.
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {

View file

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
issue_service "code.gitea.io/gitea/services/issue"
)
@ -35,7 +36,7 @@ func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
return
}
if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName); err != nil {
if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName, false); err != nil {
if models.IsErrIssueLabelTemplateLoad(err) {
originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
@ -48,17 +49,47 @@ func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
// RetrieveLabels find all the labels of a repository
// RetrieveLabels find all the labels of a repository and organization
func RetrieveLabels(ctx *context.Context) {
labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
return
}
for _, l := range labels {
l.CalOpenIssues()
}
ctx.Data["Labels"] = labels
if ctx.Repo.Owner.IsOrganization() {
orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
}
for _, l := range orgLabels {
l.CalOpenOrgIssues(ctx.Repo.Repository.ID, l.ID)
}
ctx.Data["OrgLabels"] = orgLabels
org, err := models.GetOrgByName(ctx.Repo.Owner.LowerName)
if err != nil {
ctx.ServerError("GetOrgByName", err)
return
}
if ctx.User != nil {
ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID)
if err != nil {
ctx.ServerError("org.IsOwnedBy", err)
return
}
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + org.LowerName
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
ctx.Data["OrganizationLink"] = ctx.Org.OrgLink
}
}
ctx.Data["NumLabels"] = len(labels)
ctx.Data["SortType"] = ctx.Query("sort")
}
@ -89,10 +120,10 @@ func NewLabel(ctx *context.Context, form auth.CreateLabelForm) {
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context, form auth.CreateLabelForm) {
l, err := models.GetLabelByID(form.ID)
l, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, form.ID)
if err != nil {
switch {
case models.IsErrLabelNotExist(err):
case models.IsErrRepoLabelNotExist(err):
ctx.Error(404)
default:
ctx.ServerError("UpdateLabel", err)
@ -141,7 +172,7 @@ func UpdateIssueLabel(ctx *context.Context) {
case "attach", "detach", "toggle":
label, err := models.GetLabelByID(ctx.QueryInt64("id"))
if err != nil {
if models.IsErrLabelNotExist(err) {
if models.IsErrRepoLabelNotExist(err) {
ctx.Error(404, "GetLabelByID")
} else {
ctx.ServerError("GetLabelByID", err)

View file

@ -594,6 +594,14 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/feishu/:id", bindIgnErr(auth.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
})
m.Group("/labels", func() {
m.Get("", org.RetrieveLabels, org.Labels)
m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), org.NewLabel)
m.Post("/edit", bindIgnErr(auth.CreateLabelForm{}), org.UpdateLabel)
m.Post("/delete", org.DeleteLabel)
m.Post("/initialize", bindIgnErr(auth.InitializeLabelsForm{}), org.InitializeLabels)
})
m.Route("/delete", "GET,POST", org.SettingsDelete)
})
}, context.OrgAssignment(true, true))