Add units concept for modulable functions of a repository (#742)

* Add units concept for modulable functions of a repository

* remove unused comment codes & fix lints and tests

* remove unused comment codes

* use struct config instead of map

* fix lint

* rm wrong files

* fix tests
This commit is contained in:
Lunny Xiao 2017-02-04 23:53:46 +08:00 committed by GitHub
parent 49fa03bf42
commit 8a421b1fd7
16 changed files with 668 additions and 89 deletions

View file

@ -76,10 +76,12 @@ var migrations = []Migration{
// v13 -> v14:v0.9.87
NewMigration("set comment updated with created", setCommentUpdatedWithCreated),
// v14
// v14 -> v15
NewMigration("create user column diff view style", createUserColumnDiffViewStyle),
// v15
// v15 -> v16
NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn),
// V16 -> v17
NewMigration("create repo unit table and add units for all repos", addUnitsToTables),
}
// Migrate database to current version

117
models/migrations/v16.go Normal file
View file

@ -0,0 +1,117 @@
package migrations
import (
"fmt"
"time"
"code.gitea.io/gitea/modules/markdown"
"github.com/go-xorm/xorm"
)
// RepoUnit describes all units of a repository
type RepoUnit struct {
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type int `xorm:"INDEX(s)"`
Index int
Config map[string]string `xorm:"JSON"`
CreatedUnix int64 `xorm:"INDEX CREATED"`
Created time.Time `xorm:"-"`
}
// Enumerate all the unit types
const (
UnitTypeCode = iota + 1 // 1 code
UnitTypeIssues // 2 issues
UnitTypePRs // 3 PRs
UnitTypeCommits // 4 Commits
UnitTypeReleases // 5 Releases
UnitTypeWiki // 6 Wiki
UnitTypeSettings // 7 Settings
UnitTypeExternalWiki // 8 ExternalWiki
UnitTypeExternalTracker // 9 ExternalTracker
)
// Repo describes a repository
type Repo struct {
ID int64
EnableWiki, EnableExternalWiki, EnableIssues, EnableExternalTracker, EnablePulls bool
ExternalWikiURL, ExternalTrackerURL, ExternalTrackerFormat, ExternalTrackerStyle string
}
func addUnitsToTables(x *xorm.Engine) error {
var repos []Repo
err := x.Table("repository").Find(&repos)
if err != nil {
return fmt.Errorf("Query repositories: %v", err)
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
var repoUnit RepoUnit
if err := sess.CreateTable(&repoUnit); err != nil {
return fmt.Errorf("CreateTable RepoUnit: %v", err)
}
if err := sess.CreateUniques(&repoUnit); err != nil {
return fmt.Errorf("CreateUniques RepoUnit: %v", err)
}
if err := sess.CreateIndexes(&repoUnit); err != nil {
return fmt.Errorf("CreateIndexes RepoUnit: %v", err)
}
for _, repo := range repos {
for i := 1; i <= 9; i++ {
if (i == UnitTypeWiki || i == UnitTypeExternalWiki) && !repo.EnableWiki {
continue
}
if i == UnitTypeExternalWiki && !repo.EnableExternalWiki {
continue
}
if i == UnitTypePRs && !repo.EnablePulls {
continue
}
if (i == UnitTypeIssues || i == UnitTypeExternalTracker) && !repo.EnableIssues {
continue
}
if i == UnitTypeExternalTracker && !repo.EnableExternalTracker {
continue
}
var config = make(map[string]string)
switch i {
case UnitTypeExternalTracker:
config["ExternalTrackerURL"] = repo.ExternalTrackerURL
config["ExternalTrackerFormat"] = repo.ExternalTrackerFormat
if len(repo.ExternalTrackerStyle) == 0 {
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric
}
config["ExternalTrackerStyle"] = repo.ExternalTrackerStyle
case UnitTypeExternalWiki:
config["ExternalWikiURL"] = repo.ExternalWikiURL
}
if _, err = sess.Insert(&RepoUnit{
RepoID: repo.ID,
Type: i,
Index: i,
Config: config,
}); err != nil {
return fmt.Errorf("Insert repo unit: %v", err)
}
}
}
if err := sess.Commit(); err != nil {
return err
}
return nil
}

View file

@ -106,6 +106,7 @@ func init() {
new(IssueUser),
new(LFSMetaObject),
new(TwoFactor),
new(RepoUnit),
)
gonicNames := []string{"SSL", "UID"}

View file

@ -200,17 +200,8 @@ type Repository struct {
IsMirror bool `xorm:"INDEX"`
*Mirror `xorm:"-"`
// Advanced settings
EnableWiki bool `xorm:"NOT NULL DEFAULT true"`
EnableExternalWiki bool
ExternalWikiURL string
EnableIssues bool `xorm:"NOT NULL DEFAULT true"`
EnableExternalTracker bool
ExternalTrackerURL string
ExternalTrackerFormat string
ExternalTrackerStyle string
ExternalMetas map[string]string `xorm:"-"`
EnablePulls bool `xorm:"NOT NULL DEFAULT true"`
ExternalMetas map[string]string `xorm:"-"`
Units []*RepoUnit `xorm:"-"`
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
ForkID int64 `xorm:"INDEX"`
@ -247,10 +238,6 @@ func (repo *Repository) AfterSet(colName string, _ xorm.Cell) {
repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
case "num_closed_milestones":
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
case "external_tracker_style":
if len(repo.ExternalTrackerStyle) == 0 {
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric
}
case "created_unix":
repo.Created = time.Unix(repo.CreatedUnix, 0).Local()
case "updated_unix":
@ -307,6 +294,72 @@ func (repo *Repository) APIFormat(mode AccessMode) *api.Repository {
}
}
func (repo *Repository) getUnits(e Engine) (err error) {
if repo.Units != nil {
return nil
}
repo.Units, err = getUnitsByRepoID(e, repo.ID)
return err
}
func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) {
return units, e.Where("repo_id = ?", repoID).Find(&units)
}
// EnableUnit if this repository enabled some unit
func (repo *Repository) EnableUnit(tp UnitType) bool {
repo.getUnits(x)
for _, unit := range repo.Units {
if unit.Type == tp {
return true
}
}
return false
}
var (
// ErrUnitNotExist organization does not exist
ErrUnitNotExist = errors.New("Unit does not exist")
)
// MustGetUnit always returns a RepoUnit object
func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit {
ru, err := repo.GetUnit(tp)
if err == nil {
return ru
}
if tp == UnitTypeExternalWiki {
return &RepoUnit{
Type: tp,
Config: new(ExternalWikiConfig),
}
} else if tp == UnitTypeExternalTracker {
return &RepoUnit{
Type: tp,
Config: new(ExternalTrackerConfig),
}
}
return &RepoUnit{
Type: tp,
Config: new(UnitConfig),
}
}
// GetUnit returns a RepoUnit object
func (repo *Repository) GetUnit(tp UnitType) (*RepoUnit, error) {
if err := repo.getUnits(x); err != nil {
return nil, err
}
for _, unit := range repo.Units {
if unit.Type == tp {
return unit, nil
}
}
return nil, ErrUnitNotExist
}
func (repo *Repository) getOwner(e Engine) (err error) {
if repo.Owner != nil {
return nil
@ -334,15 +387,18 @@ func (repo *Repository) mustOwner(e Engine) *User {
// ComposeMetas composes a map of metas for rendering external issue tracker URL.
func (repo *Repository) ComposeMetas() map[string]string {
if !repo.EnableExternalTracker {
unit, err := repo.GetUnit(UnitTypeExternalTracker)
if err != nil {
return nil
} else if repo.ExternalMetas == nil {
}
if repo.ExternalMetas == nil {
repo.ExternalMetas = map[string]string{
"format": repo.ExternalTrackerFormat,
"format": unit.ExternalTrackerConfig().ExternalTrackerFormat,
"user": repo.MustOwner().Name,
"repo": repo.Name,
}
switch repo.ExternalTrackerStyle {
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
case markdown.IssueNameStyleAlphanumeric:
repo.ExternalMetas["style"] = markdown.IssueNameStyleAlphanumeric
default:
@ -359,6 +415,8 @@ func (repo *Repository) DeleteWiki() {
for _, wikiPath := range wikiPaths {
RemoveAllWithNotice("Delete repository wiki", wikiPath)
}
x.Where("repo_id = ?", repo.ID).And("type = ?", UnitTypeWiki).Delete(new(RepoUnit))
}
func (repo *Repository) getAssignees(e Engine) (_ []*User, err error) {
@ -482,7 +540,7 @@ func (repo *Repository) CanEnablePulls() bool {
// AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled.
func (repo *Repository) AllowsPulls() bool {
return repo.CanEnablePulls() && repo.EnablePulls
return repo.CanEnablePulls() && repo.EnableUnit(UnitTypePullRequests)
}
// CanEnableEditor returns true if repository meets the requirements of web editor.
@ -997,6 +1055,20 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) {
return err
}
// insert units for repo
var units = make([]RepoUnit, 0, len(defaultRepoUnits))
for i, tp := range defaultRepoUnits {
units = append(units, RepoUnit{
RepoID: repo.ID,
Type: tp,
Index: i,
})
}
if _, err = e.Insert(&units); err != nil {
return err
}
u.NumRepos++
// Remember visibility preference.
u.LastRepoVisibility = repo.IsPrivate
@ -1035,15 +1107,12 @@ func CreateRepository(u *User, opts CreateRepoOptions) (_ *Repository, err error
}
repo := &Repository{
OwnerID: u.ID,
Owner: u,
Name: opts.Name,
LowerName: strings.ToLower(opts.Name),
Description: opts.Description,
IsPrivate: opts.IsPrivate,
EnableWiki: true,
EnableIssues: true,
EnablePulls: true,
OwnerID: u.ID,
Owner: u,
Name: opts.Name,
LowerName: strings.ToLower(opts.Name),
Description: opts.Description,
IsPrivate: opts.IsPrivate,
}
sess := x.NewSession()
@ -1380,6 +1449,25 @@ func UpdateRepository(repo *Repository, visibilityChanged bool) (err error) {
return sess.Commit()
}
// UpdateRepositoryUnits updates a repository's units
func UpdateRepositoryUnits(repo *Repository, units []RepoUnit) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Where("repo_id = ?", repo.ID).Delete(new(RepoUnit)); err != nil {
return err
}
if _, err = sess.Insert(units); err != nil {
return err
}
return sess.Commit()
}
// DeleteRepository deletes a repository for a user or organization.
func DeleteRepository(uid, repoID int64) error {
repo := &Repository{ID: repoID, OwnerID: uid}
@ -1467,6 +1555,10 @@ func DeleteRepository(uid, repoID int64) error {
return err
}
if _, err = sess.Where("repo_id = ?", repoID).Delete(new(RepoUnit)); err != nil {
return err
}
if repo.IsFork {
if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil {
return fmt.Errorf("decrease fork count: %v", err)

View file

@ -14,34 +14,42 @@ func TestRepo(t *testing.T) {
repo.Name = "testrepo"
repo.Owner = new(User)
repo.Owner.Name = "testuser"
repo.ExternalTrackerFormat = "https://someurl.com/{user}/{repo}/{issue}"
externalTracker := RepoUnit{
Type: UnitTypeExternalTracker,
Config: &ExternalTrackerConfig{
ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}",
},
}
repo.Units = []*RepoUnit{
&externalTracker,
}
Convey("When no external tracker is configured", func() {
Convey("It should be nil", func() {
repo.EnableExternalTracker = false
repo.Units = nil
So(repo.ComposeMetas(), ShouldEqual, map[string]string(nil))
})
Convey("It should be nil even if other settings are present", func() {
repo.EnableExternalTracker = false
repo.ExternalTrackerFormat = "http://someurl.com/{user}/{repo}/{issue}"
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric
repo.Units = nil
So(repo.ComposeMetas(), ShouldEqual, map[string]string(nil))
})
})
Convey("When an external issue tracker is configured", func() {
repo.EnableExternalTracker = true
repo.Units = []*RepoUnit{
&externalTracker,
}
Convey("It should default to numeric issue style", func() {
metas := repo.ComposeMetas()
So(metas["style"], ShouldEqual, markdown.IssueNameStyleNumeric)
})
Convey("It should pass through numeric issue style setting", func() {
repo.ExternalTrackerStyle = markdown.IssueNameStyleNumeric
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markdown.IssueNameStyleNumeric
metas := repo.ComposeMetas()
So(metas["style"], ShouldEqual, markdown.IssueNameStyleNumeric)
})
Convey("It should pass through alphanumeric issue style setting", func() {
repo.ExternalTrackerStyle = markdown.IssueNameStyleAlphanumeric
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markdown.IssueNameStyleAlphanumeric
metas := repo.ComposeMetas()
So(metas["style"], ShouldEqual, markdown.IssueNameStyleAlphanumeric)
})

137
models/repo_unit.go Normal file
View file

@ -0,0 +1,137 @@
// Copyright 2017 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 models
import (
"encoding/json"
"time"
"github.com/Unknwon/com"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
)
// RepoUnit describes all units of a repository
type RepoUnit struct {
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type UnitType `xorm:"INDEX(s)"`
Index int
Config core.Conversion `xorm:"TEXT"`
CreatedUnix int64 `xorm:"INDEX CREATED"`
Created time.Time `xorm:"-"`
}
// UnitConfig describes common unit config
type UnitConfig struct {
}
// FromDB fills up a UnitConfig from serialized format.
func (cfg *UnitConfig) FromDB(bs []byte) error {
return json.Unmarshal(bs, &cfg)
}
// ToDB exports a UnitConfig to a serialized format.
func (cfg *UnitConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// ExternalWikiConfig describes external wiki config
type ExternalWikiConfig struct {
ExternalWikiURL string
}
// FromDB fills up a ExternalWikiConfig from serialized format.
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
return json.Unmarshal(bs, &cfg)
}
// ToDB exports a ExternalWikiConfig to a serialized format.
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// ExternalTrackerConfig describes external tracker config
type ExternalTrackerConfig struct {
ExternalTrackerURL string
ExternalTrackerFormat string
ExternalTrackerStyle string
}
// FromDB fills up a ExternalTrackerConfig from serialized format.
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
return json.Unmarshal(bs, &cfg)
}
// ToDB exports a ExternalTrackerConfig to a serialized format.
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName {
case "type":
switch UnitType(Cell2Int64(val)) {
case UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, UnitTypeCommits, UnitTypeReleases,
UnitTypeWiki, UnitTypeSettings:
r.Config = new(UnitConfig)
case UnitTypeExternalWiki:
r.Config = new(ExternalWikiConfig)
case UnitTypeExternalTracker:
r.Config = new(ExternalTrackerConfig)
default:
panic("unrecognized repo unit type: " + com.ToStr(*val))
}
}
}
// AfterSet is invoked from XORM after setting the value of a field of this object.
func (r *RepoUnit) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "created_unix":
r.Created = time.Unix(r.CreatedUnix, 0).Local()
}
}
// Unit returns Unit
func (r *RepoUnit) Unit() Unit {
return Units[r.Type]
}
// CodeConfig returns config for UnitTypeCode
func (r *RepoUnit) CodeConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
// IssuesConfig returns config for UnitTypeIssues
func (r *RepoUnit) IssuesConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
// PullRequestsConfig returns config for UnitTypePullRequests
func (r *RepoUnit) PullRequestsConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
// CommitsConfig returns config for UnitTypeCommits
func (r *RepoUnit) CommitsConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
// ReleasesConfig returns config for UnitTypeReleases
func (r *RepoUnit) ReleasesConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
// ExternalWikiConfig returns config for UnitTypeExternalWiki
func (r *RepoUnit) ExternalWikiConfig() *ExternalWikiConfig {
return r.Config.(*ExternalWikiConfig)
}
// ExternalTrackerConfig returns config for UnitTypeExternalTracker
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
return r.Config.(*ExternalTrackerConfig)
}

137
models/unit.go Normal file
View file

@ -0,0 +1,137 @@
// Copyright 2017 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 models
// UnitType is Unit's Type
type UnitType int
// Enumerate all the unit types
const (
UnitTypeCode UnitType = iota + 1 // 1 code
UnitTypeIssues // 2 issues
UnitTypePullRequests // 3 PRs
UnitTypeCommits // 4 Commits
UnitTypeReleases // 5 Releases
UnitTypeWiki // 6 Wiki
UnitTypeSettings // 7 Settings
UnitTypeExternalWiki // 8 ExternalWiki
UnitTypeExternalTracker // 9 ExternalTracker
)
// Unit is a tab page of one repository
type Unit struct {
Type UnitType
NameKey string
URI string
DescKey string
Idx int
}
// Enumerate all the units
var (
UnitCode = Unit{
UnitTypeCode,
"repo.code",
"/",
"repo.code_desc",
0,
}
UnitIssues = Unit{
UnitTypeIssues,
"repo.issues",
"/issues",
"repo.issues_desc",
1,
}
UnitExternalTracker = Unit{
UnitTypeExternalTracker,
"repo.issues",
"/issues",
"repo.issues_desc",
1,
}
UnitPullRequests = Unit{
UnitTypePullRequests,
"repo.pulls",
"/pulls",
"repo.pulls_desc",
2,
}
UnitCommits = Unit{
UnitTypeCommits,
"repo.commits",
"/commits/master",
"repo.commits_desc",
3,
}
UnitReleases = Unit{
UnitTypeReleases,
"repo.releases",
"/releases",
"repo.releases_desc",
4,
}
UnitWiki = Unit{
UnitTypeWiki,
"repo.wiki",
"/wiki",
"repo.wiki_desc",
5,
}
UnitExternalWiki = Unit{
UnitTypeExternalWiki,
"repo.wiki",
"/wiki",
"repo.wiki_desc",
5,
}
UnitSettings = Unit{
UnitTypeSettings,
"repo.settings",
"/settings",
"repo.settings_desc",
6,
}
// defaultRepoUnits contains all the default unit types
defaultRepoUnits = []UnitType{
UnitTypeCode,
UnitTypeIssues,
UnitTypePullRequests,
UnitTypeCommits,
UnitTypeReleases,
UnitTypeWiki,
UnitTypeSettings,
}
// MustRepoUnits contains the units could be disabled currently
MustRepoUnits = []UnitType{
UnitTypeCode,
UnitTypeCommits,
UnitTypeReleases,
UnitTypeSettings,
}
// Units contains all the units
Units = map[UnitType]Unit{
UnitTypeCode: UnitCode,
UnitTypeIssues: UnitIssues,
UnitTypeExternalTracker: UnitExternalTracker,
UnitTypePullRequests: UnitPullRequests,
UnitTypeCommits: UnitCommits,
UnitTypeReleases: UnitReleases,
UnitTypeWiki: UnitWiki,
UnitTypeExternalWiki: UnitExternalWiki,
UnitTypeSettings: UnitSettings,
}
)