mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Additional API support for labels (#3290)
* Add API support for labels. * Error handling for adding/replacing multiple issue labels * Revisions to function names and error handling. Use issue.ClearLabels in replace/clear functions * Additional code cleanup
This commit is contained in:
		| @@ -5,6 +5,7 @@ | |||||||
| package models | package models | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -34,6 +35,30 @@ func (err ErrNamePatternNotAllowed) Error() string { | |||||||
| 	return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern) | 	return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type ErrMultipleErrors struct { | ||||||
|  | 	Errors []error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsErrMultipleErrors(err error) bool { | ||||||
|  | 	_, ok := err.(ErrMultipleErrors) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (err ErrMultipleErrors) Error() string { | ||||||
|  | 	var message bytes.Buffer | ||||||
|  |  | ||||||
|  | 	message.WriteString("Multiple errors encountered: ") | ||||||
|  |  | ||||||
|  | 	for i := range err.Errors { | ||||||
|  | 		message.WriteString(err.Errors[i].Error()) | ||||||
|  | 		if i < len(err.Errors)-1 { | ||||||
|  | 			message.WriteString("; ") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return message.String() | ||||||
|  | } | ||||||
|  |  | ||||||
| //  ____ ___ | //  ____ ___ | ||||||
| // |    |   \______ ___________ | // |    |   \______ ___________ | ||||||
| // |    |   /  ___// __ \_  __ \ | // |    |   /  ___// __ \_  __ \ | ||||||
| @@ -545,6 +570,20 @@ func (err ErrLabelNotExist) Error() string { | |||||||
| 	return fmt.Sprintf("label does not exist [id: %d]", err.ID) | 	return fmt.Sprintf("label does not exist [id: %d]", err.ID) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type ErrLabelNotValidForRepository struct { | ||||||
|  | 	ID     int64 | ||||||
|  | 	RepoID int64 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsErrLabelNotValidForRepository(err error) bool { | ||||||
|  | 	_, ok := err.(ErrLabelNotValidForRepository) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (err ErrLabelNotValidForRepository) Error() string { | ||||||
|  | 	return fmt.Sprintf("label is not valid for repository [label_id: %d, repo_id: %d]", err.ID, err.RepoID) | ||||||
|  | } | ||||||
|  |  | ||||||
| //    _____  .__.__                   __ | //    _____  .__.__                   __ | ||||||
| //   /     \ |__|  |   ____   _______/  |_  ____   ____   ____ | //   /     \ |__|  |   ____   _______/  |_  ____   ____   ____ | ||||||
| //  /  \ /  \|  |  | _/ __ \ /  ___/\   __\/  _ \ /    \_/ __ \ | //  /  \ /  \|  |  | _/ __ \ /  ___/\   __\/  _ \ /    \_/ __ \ | ||||||
|   | |||||||
| @@ -241,7 +241,23 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||||
| 				}) | 				}) | ||||||
| 				m.Group("/issues", func() { | 				m.Group("/issues", func() { | ||||||
| 					m.Combo("").Get(repo.ListIssues).Post(bind(api.CreateIssueOption{}), repo.CreateIssue) | 					m.Combo("").Get(repo.ListIssues).Post(bind(api.CreateIssueOption{}), repo.CreateIssue) | ||||||
| 					m.Combo("/:index").Get(repo.GetIssue).Patch(bind(api.EditIssueOption{}), repo.EditIssue) | 					m.Group("/:index", func() { | ||||||
|  | 						m.Combo("").Get(repo.GetIssue).Patch(bind(api.EditIssueOption{}), repo.EditIssue) | ||||||
|  | 						m.Group("/labels", func() { | ||||||
|  | 							m.Combo("").Get(repo.GetIssueLabels). | ||||||
|  | 								Post(bind(api.IssueLabelsOption{}), repo.AddIssueLabels). | ||||||
|  | 								Put(bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels). | ||||||
|  | 								Delete(repo.ClearIssueLabels) | ||||||
|  | 							m.Delete("/:id", repo.DeleteIssueLabel) | ||||||
|  | 						}) | ||||||
|  |  | ||||||
|  | 					}) | ||||||
|  | 				}) | ||||||
|  | 				m.Group("/labels", func() { | ||||||
|  | 					m.Combo("").Get(repo.ListLabels). | ||||||
|  | 						Post(bind(api.LabelOption{}), repo.CreateLabel) | ||||||
|  | 					m.Combo("/:id").Get(repo.GetLabel).Patch(bind(api.LabelOption{}), repo.EditLabel). | ||||||
|  | 						Delete(repo.DeleteLabel) | ||||||
| 				}) | 				}) | ||||||
| 			}, RepoAssignment()) | 			}, RepoAssignment()) | ||||||
| 		}, ReqToken()) | 		}, ReqToken()) | ||||||
|   | |||||||
| @@ -129,6 +129,7 @@ func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey { | |||||||
|  |  | ||||||
| func ToLabel(label *models.Label) *api.Label { | func ToLabel(label *models.Label) *api.Label { | ||||||
| 	return &api.Label{ | 	return &api.Label{ | ||||||
|  | 		ID:    label.ID, | ||||||
| 		Name:  label.Name, | 		Name:  label.Name, | ||||||
| 		Color: label.Color, | 		Color: label.Color, | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										219
									
								
								routers/api/v1/repo/issue_label.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								routers/api/v1/repo/issue_label.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | |||||||
|  | // Copyright 2016 The Gogs 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 repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	api "github.com/gogits/go-gogs-client" | ||||||
|  |  | ||||||
|  | 	"github.com/gogits/gogs/models" | ||||||
|  | 	"github.com/gogits/gogs/modules/context" | ||||||
|  | 	"github.com/gogits/gogs/routers/api/v1/convert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func GetIssueLabels(ctx *context.APIContext) { | ||||||
|  | 	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	apiLabels := make([]*api.Label, len(issue.Labels)) | ||||||
|  | 	for i := range issue.Labels { | ||||||
|  | 		apiLabels[i] = convert.ToLabel(issue.Labels[i]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(200, &apiLabels) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func AddIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) { | ||||||
|  | 	if !ctx.Repo.IsWriter() { | ||||||
|  | 		ctx.Status(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var labels []*models.Label | ||||||
|  | 	if labels, err = filterLabelsByRepoID(form.Labels, issue.RepoID); err != nil { | ||||||
|  | 		ctx.Error(400, "filterLabelsByRepoID", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i := range labels { | ||||||
|  | 		if !models.HasIssueLabel(issue.ID, labels[i].ID) { | ||||||
|  | 			if err := models.NewIssueLabel(issue, labels[i]); err != nil { | ||||||
|  | 				ctx.Error(500, "NewIssueLabel", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Refresh issue to get the updated list of labels from the DB | ||||||
|  | 	issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	apiLabels := make([]*api.Label, len(issue.Labels)) | ||||||
|  | 	for i := range issue.Labels { | ||||||
|  | 		apiLabels[i] = convert.ToLabel(issue.Labels[i]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(200, &apiLabels) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ReplaceIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) { | ||||||
|  | 	if !ctx.Repo.IsWriter() { | ||||||
|  | 		ctx.Status(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var labels []*models.Label | ||||||
|  | 	if labels, err = filterLabelsByRepoID(form.Labels, issue.RepoID); err != nil { | ||||||
|  | 		ctx.Error(400, "filterLabelsByRepoID", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := issue.ClearLabels(); err != nil { | ||||||
|  | 		ctx.Error(500, "ClearLabels", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i := range labels { | ||||||
|  | 		if err := models.NewIssueLabel(issue, labels[i]); err != nil { | ||||||
|  | 			ctx.Error(500, "NewIssueLabel", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Refresh issue to get the updated list of labels from the DB | ||||||
|  | 	issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	apiLabels := make([]*api.Label, len(issue.Labels)) | ||||||
|  | 	for i := range issue.Labels { | ||||||
|  | 		apiLabels[i] = convert.ToLabel(issue.Labels[i]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(200, &apiLabels) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func DeleteIssueLabel(ctx *context.APIContext) { | ||||||
|  | 	if !ctx.Repo.IsWriter() { | ||||||
|  | 		ctx.Status(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	label, err := models.GetLabelByID(ctx.ParamsInt64(":id")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrLabelNotExist(err) { | ||||||
|  | 			ctx.Status(400) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetLabelByID", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := models.DeleteIssueLabel(issue, label); err != nil { | ||||||
|  | 		ctx.Error(500, "DeleteIssueLabel", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Status(204) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ClearIssueLabels(ctx *context.APIContext) { | ||||||
|  | 	if !ctx.Repo.IsWriter() { | ||||||
|  | 		ctx.Status(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := issue.ClearLabels(); err != nil { | ||||||
|  | 		ctx.Error(500, "ClearLabels", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Status(204) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func filterLabelsByRepoID(labelIDs []int64, repoID int64) ([]*models.Label, error) { | ||||||
|  | 	labels := make([]*models.Label, 0, len(labelIDs)) | ||||||
|  | 	errors := make([]error, 0, len(labelIDs)) | ||||||
|  |  | ||||||
|  | 	for i := range labelIDs { | ||||||
|  | 		label, err := models.GetLabelByID(labelIDs[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			errors = append(errors, err) | ||||||
|  | 		} else if label.RepoID != repoID { | ||||||
|  | 			errors = append(errors, models.ErrLabelNotValidForRepository{label.ID, repoID}) | ||||||
|  | 		} else { | ||||||
|  | 			labels = append(labels, label) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	errorCount := len(errors) | ||||||
|  |  | ||||||
|  | 	if errorCount == 1 { | ||||||
|  | 		return labels, errors[0] | ||||||
|  | 	} else if errorCount > 1 { | ||||||
|  | 		return labels, models.ErrMultipleErrors{errors} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return labels, nil | ||||||
|  | } | ||||||
							
								
								
									
										123
									
								
								routers/api/v1/repo/label.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								routers/api/v1/repo/label.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | |||||||
|  | // Copyright 2016 The Gogs 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 repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	api "github.com/gogits/go-gogs-client" | ||||||
|  |  | ||||||
|  | 	"github.com/gogits/gogs/models" | ||||||
|  | 	"github.com/gogits/gogs/modules/context" | ||||||
|  | 	"github.com/gogits/gogs/modules/log" | ||||||
|  | 	"github.com/gogits/gogs/routers/api/v1/convert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func ListLabels(ctx *context.APIContext) { | ||||||
|  | 	labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.Error(500, "Labels", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	apiLabels := make([]*api.Label, len(labels)) | ||||||
|  | 	for i := range labels { | ||||||
|  | 		apiLabels[i] = convert.ToLabel(labels[i]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(200, &apiLabels) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetLabel(ctx *context.APIContext) { | ||||||
|  | 	label, err := models.GetLabelByID(ctx.ParamsInt64(":id")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrLabelNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetLabelByID", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(200, convert.ToLabel(label)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func CreateLabel(ctx *context.APIContext, form api.LabelOption) { | ||||||
|  | 	if !ctx.Repo.IsWriter() { | ||||||
|  | 		ctx.Status(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	label := &models.Label{ | ||||||
|  | 		Name:   form.Name, | ||||||
|  | 		Color:  form.Color, | ||||||
|  | 		RepoID: ctx.Repo.Repository.ID, | ||||||
|  | 	} | ||||||
|  | 	err := models.NewLabel(label) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.Error(500, "NewLabel", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	label, err = models.GetLabelByID(label.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.Error(500, "GetLabelByID", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx.JSON(201, convert.ToLabel(label)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func EditLabel(ctx *context.APIContext, form api.LabelOption) { | ||||||
|  | 	if !ctx.Repo.IsWriter() { | ||||||
|  | 		ctx.Status(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	label, err := models.GetLabelByID(ctx.ParamsInt64(":id")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrLabelNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetLabelByID", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(form.Name) > 0 { | ||||||
|  | 		label.Name = form.Name | ||||||
|  | 	} | ||||||
|  | 	if len(form.Color) > 0 { | ||||||
|  | 		label.Color = form.Color | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := models.UpdateLabel(label); err != nil { | ||||||
|  | 		ctx.Handle(500, "UpdateLabel", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx.JSON(200, convert.ToLabel(label)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func DeleteLabel(ctx *context.APIContext) { | ||||||
|  | 	if !ctx.Repo.IsWriter() { | ||||||
|  | 		ctx.Status(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	label, err := models.GetLabelByID(ctx.ParamsInt64(":id")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrLabelNotExist(err) { | ||||||
|  | 			ctx.Status(404) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(500, "GetLabelByID", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil { | ||||||
|  | 		ctx.Error(500, "DeleteLabel", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Trace("Label deleted: %s %s", label.ID, label.Name) | ||||||
|  | 	ctx.Status(204) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 lstahlman
					lstahlman