diff options
author | mrsdizzie <info@mrsdizzie.com> | 2020-04-01 00:14:46 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-01 01:14:46 -0300 |
commit | 8d99ee27734b15199e5f620f67e81857d4cddfca (patch) | |
tree | 7756300f3c974dbdf90958e4639120d9b6e4b9fb /models | |
parent | 36d9237e60441362684f9f297855662099bc5498 (diff) | |
download | gitea-8d99ee27734b15199e5f620f67e81857d4cddfca.tar.gz gitea-8d99ee27734b15199e5f620f67e81857d4cddfca.zip |
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>
Diffstat (limited to 'models')
-rw-r--r-- | models/error.go | 35 | ||||
-rw-r--r-- | models/fixtures/issue_label.yml | 5 | ||||
-rw-r--r-- | models/fixtures/label.yml | 20 | ||||
-rw-r--r-- | models/issue.go | 4 | ||||
-rw-r--r-- | models/issue_label.go | 372 | ||||
-rw-r--r-- | models/issue_label_test.go | 111 | ||||
-rw-r--r-- | models/issue_list_test.go | 1 | ||||
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v135.go | 22 |
9 files changed, 475 insertions, 97 deletions
diff --git a/models/error.go b/models/error.go index f53479fac8..f54df37330 100644 --- a/models/error.go +++ b/models/error.go @@ -1502,10 +1502,41 @@ func (err ErrTrackedTimeNotExist) Error() string { // |_______ (____ /___ /\___ >____/ // \/ \/ \/ \/ +// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error. +type ErrRepoLabelNotExist struct { + LabelID int64 + RepoID int64 +} + +// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist. +func IsErrRepoLabelNotExist(err error) bool { + _, ok := err.(ErrRepoLabelNotExist) + return ok +} + +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 "OrgLabelNotExist" kind of error. +type ErrOrgLabelNotExist struct { + LabelID int64 + OrgID int64 +} + +// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist. +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) +} + // ErrLabelNotExist represents a "LabelNotExist" kind of error. type ErrLabelNotExist struct { LabelID int64 - RepoID int64 } // IsErrLabelNotExist checks if an error is a ErrLabelNotExist. @@ -1515,7 +1546,7 @@ func IsErrLabelNotExist(err error) bool { } func (err ErrLabelNotExist) Error() string { - return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID) + return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID) } // _____ .__.__ __ diff --git a/models/fixtures/issue_label.yml b/models/fixtures/issue_label.yml index 49d5a95d02..f4ecb1f923 100644 --- a/models/fixtures/issue_label.yml +++ b/models/fixtures/issue_label.yml @@ -12,3 +12,8 @@ id: 3 issue_id: 2 label_id: 1 + +- + id: 4 + issue_id: 2 + label_id: 4 diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml index 5336342b1c..3ad82eebed 100644 --- a/models/fixtures/label.yml +++ b/models/fixtures/label.yml @@ -1,6 +1,7 @@ - id: 1 repo_id: 1 + org_id: 0 name: label1 color: '#abcdef' num_issues: 2 @@ -9,7 +10,26 @@ - 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: 3 + name: orglabel3 + color: '#abcdef' + num_issues: 0 + num_closed_issues: 0 + +- + id: 4 + repo_id: 0 + org_id: 3 + name: orglabel4 + color: '#000000' + num_issues: 1 + num_closed_issues: 0 + diff --git a/models/issue.go b/models/issue.go index 8aa02873a1..db8991095d 100644 --- a/models/issue.go +++ b/models/issue.go @@ -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 { @@ -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.OwnerID { continue } diff --git a/models/issue_label.go b/models/issue_label.go index 3b516a7aed..cb9c307e2b 100644 --- a/models/issue_label.go +++ b/models/issue_label.go @@ -21,18 +21,20 @@ var LabelColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") // Label represents a label of repository for issues. type Label struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX"` - Name string - Description string - Color string `xorm:"VARCHAR(7)"` - NumIssues int - NumClosedIssues int - NumOpenIssues int `xorm:"-"` - IsChecked bool `xorm:"-"` - QueryString string `xorm:"-"` - IsSelected bool `xorm:"-"` - IsExcluded bool `xorm:"-"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + NumIssues int + NumClosedIssues int + NumOpenIssues int `xorm:"-"` + NumOpenRepoIssues int64 `xorm:"-"` + IsChecked bool `xorm:"-"` + QueryString string `xorm:"-"` + IsSelected bool `xorm:"-"` + IsExcluded bool `xorm:"-"` } // GetLabelTemplateFile loads the label template file by given name, @@ -79,11 +81,26 @@ func GetLabelTemplateFile(name string) ([][3]string, error) { return list, nil } -// CalOpenIssues calculates the open issues of label. +// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. func (label *Label) CalOpenIssues() { label.NumOpenIssues = label.NumIssues - label.NumClosedIssues } +// CalOpenOrgIssues calculates the open issues of a label for a specific repo +func (label *Label) CalOpenOrgIssues(repoID, labelID int64) { + repoIDs := []int64{repoID} + labelIDs := []int64{labelID} + + counts, _ := CountIssuesByRepo(&IssuesOptions{ + RepoIDs: repoIDs, + LabelIDs: labelIDs, + }) + + for _, count := range counts { + label.NumOpenRepoIssues += count + } +} + // LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) { var labelQuerySlice []string @@ -106,6 +123,16 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) label.QueryString = strings.Join(labelQuerySlice, ",") } +// BelongsToOrg returns true if label is an organization label +func (label *Label) BelongsToOrg() bool { + return label.OrgID > 0 +} + +// BelongsToRepo returns true if label is a repository label +func (label *Label) BelongsToRepo() bool { + return label.RepoID > 0 +} + // ForegroundColor calculates the text color for labels based // on their background color. func (label *Label) ForegroundColor() template.CSS { @@ -126,6 +153,12 @@ func (label *Label) ForegroundColor() template.CSS { return template.CSS("#000") } +// .____ ___. .__ +// | | _____ \_ |__ ____ | | +// | | \__ \ | __ \_/ __ \| | +// | |___ / __ \| \_\ \ ___/| |__ +// >_______ (____ /___ /\___ >____/ + func loadLabels(labelTemplate string) ([]string, error) { list, err := GetLabelTemplateFile(labelTemplate) if err != nil { @@ -145,7 +178,7 @@ func LoadLabelsFormatted(labelTemplate string) (string, error) { return strings.Join(labels, ", "), err } -func initializeLabels(e Engine, repoID int64, labelTemplate string) error { +func initializeLabels(e Engine, id int64, labelTemplate string, isOrg bool) error { list, err := GetLabelTemplateFile(labelTemplate) if err != nil { return ErrIssueLabelTemplateLoad{labelTemplate, err} @@ -154,11 +187,15 @@ func initializeLabels(e Engine, repoID int64, labelTemplate string) error { labels := make([]*Label, len(list)) for i := 0; i < len(list); i++ { labels[i] = &Label{ - RepoID: repoID, Name: list[i][0], Description: list[i][2], Color: list[i][1], } + if isOrg { + labels[i].OrgID = id + } else { + labels[i].RepoID = id + } } for _, label := range labels { if err = newLabel(e, label); err != nil { @@ -169,8 +206,8 @@ func initializeLabels(e Engine, repoID int64, labelTemplate string) error { } // InitializeLabels adds a label set to a repository using a template -func InitializeLabels(ctx DBContext, repoID int64, labelTemplate string) error { - return initializeLabels(ctx.e, repoID, labelTemplate) +func InitializeLabels(ctx DBContext, repoID int64, labelTemplate string, isOrg bool) error { + return initializeLabels(ctx.e, repoID, labelTemplate, isOrg) } func newLabel(e Engine, label *Label) error { @@ -178,7 +215,7 @@ func newLabel(e Engine, label *Label) error { return err } -// NewLabel creates a new label for a repository +// NewLabel creates a new label func NewLabel(label *Label) error { if !LabelColorPattern.MatchString(label.Color) { return fmt.Errorf("bad color code: %s", label.Color) @@ -186,7 +223,7 @@ func NewLabel(label *Label) error { return newLabel(x, label) } -// NewLabels creates new labels for a repository. +// NewLabels creates new labels func NewLabels(labels ...*Label) error { sess := x.NewSession() defer sess.Close() @@ -204,12 +241,98 @@ func NewLabels(labels ...*Label) error { return sess.Commit() } +// UpdateLabel updates label information. +func UpdateLabel(l *Label) error { + if !LabelColorPattern.MatchString(l.Color) { + return fmt.Errorf("bad color code: %s", l.Color) + } + return updateLabel(x, l) +} + +// DeleteLabel delete a label +func DeleteLabel(id, labelID int64) error { + + label, err := GetLabelByID(labelID) + if err != nil { + if IsErrLabelNotExist(err) { + return nil + } + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if label.BelongsToOrg() && label.OrgID != id { + return nil + } + if label.BelongsToRepo() && label.RepoID != id { + return nil + } + + if _, err = sess.ID(labelID).Delete(new(Label)); err != nil { + return err + } else if _, err = sess. + Where("label_id = ?", labelID). + Delete(new(IssueLabel)); err != nil { + return err + } + + // delete comments about now deleted label_id + if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil { + return err + } + + return sess.Commit() +} + +// getLabelByID returns a label by label id +func getLabelByID(e Engine, labelID int64) (*Label, error) { + if labelID <= 0 { + return nil, ErrLabelNotExist{labelID} + } + + l := &Label{ + ID: labelID, + } + has, err := e.Get(l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrLabelNotExist{l.ID} + } + return l, nil +} + +// GetLabelByID returns a label by given ID. +func GetLabelByID(id int64) (*Label, error) { + return getLabelByID(x, id) +} + +// GetLabelsByIDs returns a list of labels by IDs +func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) { + labels := make([]*Label, 0, len(labelIDs)) + return labels, x.Table("label"). + In("id", labelIDs). + Asc("name"). + Cols("id"). + Find(&labels) +} + +// __________ .__ __ +// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. +// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | +// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | +// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| +// \/ \/|__| \/ \/ + // getLabelInRepoByName returns a label by Name in given repository. -// If pass repoID as 0, then ORM will ignore limitation of repository -// and can return arbitrary label with any valid ID. func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, error) { - if len(labelName) == 0 { - return nil, ErrLabelNotExist{0, repoID} + if len(labelName) == 0 || repoID <= 0 { + return nil, ErrRepoLabelNotExist{0, repoID} } l := &Label{ @@ -220,17 +343,15 @@ func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, err if err != nil { return nil, err } else if !has { - return nil, ErrLabelNotExist{0, l.RepoID} + return nil, ErrRepoLabelNotExist{0, l.RepoID} } return l, nil } // getLabelInRepoByID returns a label by ID in given repository. -// If pass repoID as 0, then ORM will ignore limitation of repository -// and can return arbitrary label with any valid ID. func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) { - if labelID <= 0 { - return nil, ErrLabelNotExist{labelID, repoID} + if labelID <= 0 || repoID <= 0 { + return nil, ErrRepoLabelNotExist{labelID, repoID} } l := &Label{ @@ -241,16 +362,11 @@ func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) { if err != nil { return nil, err } else if !has { - return nil, ErrLabelNotExist{l.ID, l.RepoID} + return nil, ErrRepoLabelNotExist{l.ID, l.RepoID} } return l, nil } -// GetLabelByID returns a label by given ID. -func GetLabelByID(id int64) (*Label, error) { - return getLabelInRepoByID(x, 0, id) -} - // GetLabelInRepoByName returns a label by name in given repository. func GetLabelInRepoByName(repoID int64, labelName string) (*Label, error) { return getLabelInRepoByName(x, repoID, labelName) @@ -280,19 +396,6 @@ func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { GroupBy("issue_label.issue_id") } -// GetLabelIDsInReposByNames returns a list of labelIDs by names in one of the given -// repositories. -// it silently ignores label names that do not belong to the repository. -func GetLabelIDsInReposByNames(repoIDs []int64, labelNames []string) ([]int64, error) { - labelIDs := make([]int64, 0, len(labelNames)) - return labelIDs, x.Table("label"). - In("repo_id", repoIDs). - In("name", labelNames). - Asc("name"). - Cols("id"). - Find(&labelIDs) -} - // GetLabelInRepoByID returns a label by ID in given repository. func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) { return getLabelInRepoByID(x, repoID, labelID) @@ -310,6 +413,9 @@ func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) { } func getLabelsByRepoID(e Engine, repoID int64, sortType string, listOptions ListOptions) ([]*Label, error) { + if repoID <= 0 { + return nil, ErrRepoLabelNotExist{0, repoID} + } labels := make([]*Label, 0, 10) sess := e.Where("repo_id = ?", repoID) @@ -336,6 +442,138 @@ func GetLabelsByRepoID(repoID int64, sortType string, listOptions ListOptions) ( return getLabelsByRepoID(x, repoID, sortType, listOptions) } +// ________ +// \_____ \_______ ____ +// / | \_ __ \/ ___\ +// / | \ | \/ /_/ > +// \_______ /__| \___ / +// \/ /_____/ + +// getLabelInOrgByName returns a label by Name in given organization +func getLabelInOrgByName(e Engine, orgID int64, labelName string) (*Label, error) { + if len(labelName) == 0 || orgID <= 0 { + return nil, ErrOrgLabelNotExist{0, orgID} + } + + l := &Label{ + Name: labelName, + OrgID: orgID, + } + has, err := e.Get(l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgLabelNotExist{0, l.OrgID} + } + return l, nil +} + +// getLabelInOrgByID returns a label by ID in given organization. +func getLabelInOrgByID(e Engine, orgID, labelID int64) (*Label, error) { + if labelID <= 0 || orgID <= 0 { + return nil, ErrOrgLabelNotExist{labelID, orgID} + } + + l := &Label{ + ID: labelID, + OrgID: orgID, + } + has, err := e.Get(l) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgLabelNotExist{l.ID, l.OrgID} + } + return l, nil +} + +// GetLabelInOrgByName returns a label by name in given organization. +func GetLabelInOrgByName(orgID int64, labelName string) (*Label, error) { + return getLabelInOrgByName(x, orgID, labelName) +} + +// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given +// organization. +func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) { + if orgID <= 0 { + return nil, ErrOrgLabelNotExist{0, orgID} + } + labelIDs := make([]int64, 0, len(labelNames)) + + return labelIDs, x.Table("label"). + Where("org_id = ?", orgID). + In("name", labelNames). + Asc("name"). + Cols("id"). + Find(&labelIDs) +} + +// GetLabelIDsInOrgsByNames returns a list of labelIDs by names in one of the given +// organization. +// it silently ignores label names that do not belong to the organization. +func GetLabelIDsInOrgsByNames(orgIDs []int64, labelNames []string) ([]int64, error) { + labelIDs := make([]int64, 0, len(labelNames)) + return labelIDs, x.Table("label"). + In("org_id", orgIDs). + In("name", labelNames). + Asc("name"). + Cols("id"). + Find(&labelIDs) +} + +// GetLabelInOrgByID returns a label by ID in given organization. +func GetLabelInOrgByID(orgID, labelID int64) (*Label, error) { + return getLabelInOrgByID(x, orgID, labelID) +} + +// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization, +// it silently ignores label IDs that do not belong to the organization. +func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) { + labels := make([]*Label, 0, len(labelIDs)) + return labels, x. + Where("org_id = ?", orgID). + In("id", labelIDs). + Asc("name"). + Find(&labels) +} + +func getLabelsByOrgID(e Engine, orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) { + if orgID <= 0 { + return nil, ErrOrgLabelNotExist{0, orgID} + } + labels := make([]*Label, 0, 10) + sess := e.Where("org_id = ?", orgID) + + switch sortType { + case "reversealphabetically": + sess.Desc("name") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("name") + } + + if listOptions.Page != 0 { + sess = listOptions.setSessionPagination(sess) + } + + return labels, sess.Find(&labels) +} + +// GetLabelsByOrgID returns all labels that belong to given organization by ID. +func GetLabelsByOrgID(orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) { + return getLabelsByOrgID(x, orgID, sortType, listOptions) +} + +// .___ +// | | ______ ________ __ ____ +// | |/ ___// ___/ | \_/ __ \ +// | |\___ \ \___ \| | /\ ___/ +// |___/____ >____ >____/ \___ | +// \/ \/ \/ + func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) { var labels []*Label return labels, e.Where("issue_label.issue_id = ?", issueID). @@ -367,46 +605,6 @@ func updateLabel(e Engine, l *Label) error { return err } -// UpdateLabel updates label information. -func UpdateLabel(l *Label) error { - if !LabelColorPattern.MatchString(l.Color) { - return fmt.Errorf("bad color code: %s", l.Color) - } - return updateLabel(x, l) -} - -// DeleteLabel delete a label of given repository. -func DeleteLabel(repoID, labelID int64) error { - _, err := GetLabelInRepoByID(repoID, labelID) - if err != nil { - if IsErrLabelNotExist(err) { - return nil - } - return err - } - - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if _, err = sess.ID(labelID).Delete(new(Label)); err != nil { - return err - } else if _, err = sess. - Where("label_id = ?", labelID). - Delete(new(IssueLabel)); err != nil { - return err - } - - // Clear label id in comment table - if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Update(&Comment{}); err != nil { - return err - } - - return sess.Commit() -} - // .___ .____ ___. .__ // | | ______ ________ __ ____ | | _____ \_ |__ ____ | | // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | diff --git a/models/issue_label_test.go b/models/issue_label_test.go index 6f51473fcb..8afba779e0 100644 --- a/models/issue_label_test.go +++ b/models/issue_label_test.go @@ -66,10 +66,10 @@ func TestGetLabelInRepoByName(t *testing.T) { assert.Equal(t, "label1", label.Name) _, err = GetLabelInRepoByName(1, "") - assert.True(t, IsErrLabelNotExist(err)) + assert.True(t, IsErrRepoLabelNotExist(err)) _, err = GetLabelInRepoByName(NonexistentID, "nonexistent") - assert.True(t, IsErrLabelNotExist(err)) + assert.True(t, IsErrRepoLabelNotExist(err)) } func TestGetLabelInRepoByNames(t *testing.T) { @@ -103,10 +103,10 @@ func TestGetLabelInRepoByID(t *testing.T) { assert.EqualValues(t, 1, label.ID) _, err = GetLabelInRepoByID(1, -1) - assert.True(t, IsErrLabelNotExist(err)) + assert.True(t, IsErrRepoLabelNotExist(err)) _, err = GetLabelInRepoByID(NonexistentID, NonexistentID) - assert.True(t, IsErrLabelNotExist(err)) + assert.True(t, IsErrRepoLabelNotExist(err)) } func TestGetLabelsInRepoByIDs(t *testing.T) { @@ -135,6 +135,107 @@ func TestGetLabelsByRepoID(t *testing.T) { testSuccess(1, "default", []int64{1, 2}) } +// Org vrsions + +func TestGetLabelInOrgByName(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + label, err := GetLabelInOrgByName(3, "orglabel3") + assert.NoError(t, err) + assert.EqualValues(t, 3, label.ID) + assert.Equal(t, "orglabel3", label.Name) + + _, err = GetLabelInOrgByName(3, "") + assert.True(t, IsErrOrgLabelNotExist(err)) + + _, err = GetLabelInOrgByName(0, "orglabel3") + assert.True(t, IsErrOrgLabelNotExist(err)) + + _, err = GetLabelInOrgByName(-1, "orglabel3") + assert.True(t, IsErrOrgLabelNotExist(err)) + + _, err = GetLabelInOrgByName(NonexistentID, "nonexistent") + assert.True(t, IsErrOrgLabelNotExist(err)) +} + +func TestGetLabelInOrgByNames(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"}) + assert.NoError(t, err) + + assert.Len(t, labelIDs, 2) + + assert.Equal(t, int64(3), labelIDs[0]) + assert.Equal(t, int64(4), labelIDs[1]) +} + +func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + // orglabel99 doesn't exists.. See labels.yml + labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"}) + assert.NoError(t, err) + + assert.Len(t, labelIDs, 2) + + assert.Equal(t, int64(3), labelIDs[0]) + assert.Equal(t, int64(4), labelIDs[1]) + assert.NoError(t, err) +} + +func TestGetLabelInOrgByID(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + label, err := GetLabelInOrgByID(3, 3) + assert.NoError(t, err) + assert.EqualValues(t, 3, label.ID) + + _, err = GetLabelInOrgByID(3, -1) + assert.True(t, IsErrOrgLabelNotExist(err)) + + _, err = GetLabelInOrgByID(0, 3) + assert.True(t, IsErrOrgLabelNotExist(err)) + + _, err = GetLabelInOrgByID(-1, 3) + assert.True(t, IsErrOrgLabelNotExist(err)) + + _, err = GetLabelInOrgByID(NonexistentID, NonexistentID) + assert.True(t, IsErrOrgLabelNotExist(err)) +} + +func TestGetLabelsInOrgByIDs(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + labels, err := GetLabelsInOrgByIDs(3, []int64{3, 4, NonexistentID}) + assert.NoError(t, err) + if assert.Len(t, labels, 2) { + assert.EqualValues(t, 3, labels[0].ID) + assert.EqualValues(t, 4, labels[1].ID) + } +} + +func TestGetLabelsByOrgID(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) { + labels, err := GetLabelsByOrgID(orgID, sortType, ListOptions{}) + assert.NoError(t, err) + assert.Len(t, labels, len(expectedIssueIDs)) + for i, label := range labels { + assert.EqualValues(t, expectedIssueIDs[i], label.ID) + } + } + testSuccess(3, "leastissues", []int64{3, 4}) + testSuccess(3, "mostissues", []int64{4, 3}) + testSuccess(3, "reversealphabetically", []int64{4, 3}) + testSuccess(3, "default", []int64{3, 4}) + + var err error + _, err = GetLabelsByOrgID(0, "leastissues", ListOptions{}) + assert.True(t, IsErrOrgLabelNotExist(err)) + + _, err = GetLabelsByOrgID(-1, "leastissues", ListOptions{}) + assert.True(t, IsErrOrgLabelNotExist(err)) + +} + +// + func TestGetLabelsByIssueID(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) labels, err := GetLabelsByIssueID(1) @@ -166,7 +267,7 @@ func TestDeleteLabel(t *testing.T) { AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID}) assert.NoError(t, DeleteLabel(label.RepoID, label.ID)) - AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID}) + AssertNotExistsBean(t, &Label{ID: label.ID}) assert.NoError(t, DeleteLabel(NonexistentID, NonexistentID)) CheckConsistencyFor(t, &Label{}, &Repository{}) diff --git a/models/issue_list_test.go b/models/issue_list_test.go index f5a91702f2..c9c39332c7 100644 --- a/models/issue_list_test.go +++ b/models/issue_list_test.go @@ -34,7 +34,6 @@ func TestIssueList_LoadAttributes(t *testing.T) { setting.Service.EnableTimetracking = true issueList := IssueList{ AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue), - AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue), AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue), } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 49b34861d6..847cd75d52 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -202,6 +202,8 @@ var migrations = []Migration{ NewMigration("Add EmailHash Table", addEmailHashTable), // v134 -> v135 NewMigration("Refix merge base for merged pull requests", refixMergeBase), + // v135 -> 136 + NewMigration("Add OrgID column to Labels table", addOrgIDLabelColumn), } // Migrate database to current version diff --git a/models/migrations/v135.go b/models/migrations/v135.go new file mode 100644 index 0000000000..8d859d42c0 --- /dev/null +++ b/models/migrations/v135.go @@ -0,0 +1,22 @@ +// 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 migrations + +import ( + "fmt" + + "xorm.io/xorm" +) + +func addOrgIDLabelColumn(x *xorm.Engine) error { + type Label struct { + OrgID int64 `xorm:"INDEX"` + } + + if err := x.Sync2(new(Label)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} |