summaryrefslogtreecommitdiffstats
path: root/models/repo
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2021-12-10 09:27:50 +0800
committerGitHub <noreply@github.com>2021-12-10 09:27:50 +0800
commit719bddcd76610a63dadc8555760072957a11cf30 (patch)
tree0df26092fba7e3e21444fe493e6b349473b6b0cb /models/repo
parentfb8166c6c6b652a0e6fa98681780a6a71090faf3 (diff)
downloadgitea-719bddcd76610a63dadc8555760072957a11cf30.tar.gz
gitea-719bddcd76610a63dadc8555760072957a11cf30.zip
Move repository model into models/repo (#17933)
* Some refactors related repository model * Move more methods out of repository * Move repository into models/repo * Fix test * Fix test * some improvements * Remove unnecessary function
Diffstat (limited to 'models/repo')
-rw-r--r--models/repo/avatar.go94
-rw-r--r--models/repo/git.go31
-rw-r--r--models/repo/issue.go67
-rw-r--r--models/repo/language_stats.go215
-rw-r--r--models/repo/main_test.go3
-rw-r--r--models/repo/mirror.go177
-rw-r--r--models/repo/pushmirror.go112
-rw-r--r--models/repo/pushmirror_test.go50
-rw-r--r--models/repo/repo.go736
-rw-r--r--models/repo/repo_indexer.go125
-rw-r--r--models/repo/repo_test.go44
-rw-r--r--models/repo/repo_unit.go244
-rw-r--r--models/repo/wiki.go39
-rw-r--r--models/repo/wiki_test.go45
14 files changed, 1982 insertions, 0 deletions
diff --git a/models/repo/avatar.go b/models/repo/avatar.go
new file mode 100644
index 0000000000..f11f868d63
--- /dev/null
+++ b/models/repo/avatar.go
@@ -0,0 +1,94 @@
+// Copyright 2021 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 repo
+
+import (
+ "fmt"
+ "image/png"
+ "io"
+ "net/url"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/avatar"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
+)
+
+// CustomAvatarRelativePath returns repository custom avatar file path.
+func (repo *Repository) CustomAvatarRelativePath() string {
+ return repo.Avatar
+}
+
+// RelAvatarLink returns a relative link to the repository's avatar.
+func (repo *Repository) RelAvatarLink() string {
+ return repo.relAvatarLink(db.GetEngine(db.DefaultContext))
+}
+
+// generateRandomAvatar generates a random avatar for repository.
+func generateRandomAvatar(e db.Engine, repo *Repository) error {
+ idToString := fmt.Sprintf("%d", repo.ID)
+
+ seed := idToString
+ img, err := avatar.RandomImage([]byte(seed))
+ if err != nil {
+ return fmt.Errorf("RandomImage: %v", err)
+ }
+
+ repo.Avatar = idToString
+
+ if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error {
+ if err := png.Encode(w, img); err != nil {
+ log.Error("Encode: %v", err)
+ }
+ return err
+ }); err != nil {
+ return fmt.Errorf("Failed to create dir %s: %v", repo.CustomAvatarRelativePath(), err)
+ }
+
+ log.Info("New random avatar created for repository: %d", repo.ID)
+
+ if _, err := e.ID(repo.ID).Cols("avatar").NoAutoTime().Update(repo); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (repo *Repository) relAvatarLink(e db.Engine) string {
+ // If no avatar - path is empty
+ avatarPath := repo.CustomAvatarRelativePath()
+ if len(avatarPath) == 0 {
+ switch mode := setting.RepoAvatar.Fallback; mode {
+ case "image":
+ return setting.RepoAvatar.FallbackImage
+ case "random":
+ if err := generateRandomAvatar(e, repo); err != nil {
+ log.Error("generateRandomAvatar: %v", err)
+ }
+ default:
+ // default behaviour: do not display avatar
+ return ""
+ }
+ }
+ return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
+}
+
+// AvatarLink returns a link to the repository's avatar.
+func (repo *Repository) AvatarLink() string {
+ return repo.avatarLink(db.GetEngine(db.DefaultContext))
+}
+
+// avatarLink returns user avatar absolute link.
+func (repo *Repository) avatarLink(e db.Engine) string {
+ link := repo.relAvatarLink(e)
+ // we only prepend our AppURL to our known (relative, internal) avatar link to get an absolute URL
+ if strings.HasPrefix(link, "/") && !strings.HasPrefix(link, "//") {
+ return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
+ }
+ // otherwise, return the link as it is
+ return link
+}
diff --git a/models/repo/git.go b/models/repo/git.go
new file mode 100644
index 0000000000..509020565a
--- /dev/null
+++ b/models/repo/git.go
@@ -0,0 +1,31 @@
+// Copyright 2021 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 repo
+
+import "code.gitea.io/gitea/models/db"
+
+// MergeStyle represents the approach to merge commits into base branch.
+type MergeStyle string
+
+const (
+ // MergeStyleMerge create merge commit
+ MergeStyleMerge MergeStyle = "merge"
+ // MergeStyleRebase rebase before merging
+ MergeStyleRebase MergeStyle = "rebase"
+ // MergeStyleRebaseMerge rebase before merging with merge commit (--no-ff)
+ MergeStyleRebaseMerge MergeStyle = "rebase-merge"
+ // MergeStyleSquash squash commits into single commit before merging
+ MergeStyleSquash MergeStyle = "squash"
+ // MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
+ MergeStyleManuallyMerged MergeStyle = "manually-merged"
+ // MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase
+ MergeStyleRebaseUpdate MergeStyle = "rebase-update-only"
+)
+
+// UpdateDefaultBranch updates the default branch
+func UpdateDefaultBranch(repo *Repository) error {
+ _, err := db.GetEngine(db.DefaultContext).ID(repo.ID).Cols("default_branch").Update(repo)
+ return err
+}
diff --git a/models/repo/issue.go b/models/repo/issue.go
new file mode 100644
index 0000000000..3edcc7b5a0
--- /dev/null
+++ b/models/repo/issue.go
@@ -0,0 +1,67 @@
+// 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 repo
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// ___________.__ ___________ __
+// \__ ___/|__| _____ ___\__ ___/___________ ____ | | __ ___________
+// | | | |/ \_/ __ \| | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
+// | | | | Y Y \ ___/| | | | \// __ \\ \___| <\ ___/| | \/
+// |____| |__|__|_| /\___ >____| |__| (____ /\___ >__|_ \\___ >__|
+// \/ \/ \/ \/ \/ \/
+
+// CanEnableTimetracker returns true when the server admin enabled time tracking
+// This overrules IsTimetrackerEnabled
+func (repo *Repository) CanEnableTimetracker() bool {
+ return setting.Service.EnableTimetracking
+}
+
+// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs.
+func (repo *Repository) IsTimetrackerEnabled() bool {
+ if !setting.Service.EnableTimetracking {
+ return false
+ }
+
+ var u *RepoUnit
+ var err error
+ if u, err = repo.GetUnit(unit.TypeIssues); err != nil {
+ return setting.Service.DefaultEnableTimetracking
+ }
+ return u.IssuesConfig().EnableTimetracker
+}
+
+// AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value
+func (repo *Repository) AllowOnlyContributorsToTrackTime() bool {
+ var u *RepoUnit
+ var err error
+ if u, err = repo.GetUnit(unit.TypeIssues); err != nil {
+ return setting.Service.DefaultAllowOnlyContributorsToTrackTime
+ }
+ return u.IssuesConfig().AllowOnlyContributorsToTrackTime
+}
+
+// IsDependenciesEnabled returns if dependencies are enabled and returns the default setting if not set.
+func (repo *Repository) IsDependenciesEnabled() bool {
+ return repo.IsDependenciesEnabledCtx(db.DefaultContext)
+}
+
+// IsDependenciesEnabledCtx returns if dependencies are enabled and returns the default setting if not set.
+func (repo *Repository) IsDependenciesEnabledCtx(ctx context.Context) bool {
+ var u *RepoUnit
+ var err error
+ if u, err = repo.getUnit(ctx, unit.TypeIssues); err != nil {
+ log.Trace("%s", err)
+ return setting.Service.DefaultEnableDependencies
+ }
+ return u.IssuesConfig().EnableDependencies
+}
diff --git a/models/repo/language_stats.go b/models/repo/language_stats.go
new file mode 100644
index 0000000000..3b0888b6bd
--- /dev/null
+++ b/models/repo/language_stats.go
@@ -0,0 +1,215 @@
+// 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 repo
+
+import (
+ "math"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/go-enry/go-enry/v2"
+)
+
+// LanguageStat describes language statistics of a repository
+type LanguageStat struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
+ CommitID string
+ IsPrimary bool
+ Language string `xorm:"VARCHAR(50) UNIQUE(s) INDEX NOT NULL"`
+ Percentage float32 `xorm:"-"`
+ Size int64 `xorm:"NOT NULL DEFAULT 0"`
+ Color string `xorm:"-"`
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
+}
+
+func init() {
+ db.RegisterModel(new(LanguageStat))
+}
+
+// LanguageStatList defines a list of language statistics
+type LanguageStatList []*LanguageStat
+
+// LoadAttributes loads attributes
+func (stats LanguageStatList) LoadAttributes() {
+ for i := range stats {
+ stats[i].Color = enry.GetColor(stats[i].Language)
+ }
+}
+
+func (stats LanguageStatList) getLanguagePercentages() map[string]float32 {
+ langPerc := make(map[string]float32)
+ var otherPerc float32 = 100
+ var total int64
+
+ for _, stat := range stats {
+ total += stat.Size
+ }
+ if total > 0 {
+ for _, stat := range stats {
+ perc := float32(math.Round(float64(stat.Size)/float64(total)*1000) / 10)
+ if perc <= 0.1 {
+ continue
+ }
+ otherPerc -= perc
+ langPerc[stat.Language] = perc
+ }
+ otherPerc = float32(math.Round(float64(otherPerc)*10) / 10)
+ }
+ if otherPerc > 0 {
+ langPerc["other"] = otherPerc
+ }
+ return langPerc
+}
+
+func getLanguageStats(e db.Engine, repo *Repository) (LanguageStatList, error) {
+ stats := make(LanguageStatList, 0, 6)
+ if err := e.Where("`repo_id` = ?", repo.ID).Desc("`size`").Find(&stats); err != nil {
+ return nil, err
+ }
+ return stats, nil
+}
+
+// GetLanguageStats returns the language statistics for a repository
+func GetLanguageStats(repo *Repository) (LanguageStatList, error) {
+ return getLanguageStats(db.GetEngine(db.DefaultContext), repo)
+}
+
+// GetTopLanguageStats returns the top language statistics for a repository
+func GetTopLanguageStats(repo *Repository, limit int) (LanguageStatList, error) {
+ stats, err := getLanguageStats(db.GetEngine(db.DefaultContext), repo)
+ if err != nil {
+ return nil, err
+ }
+ perc := stats.getLanguagePercentages()
+ topstats := make(LanguageStatList, 0, limit)
+ var other float32
+ for i := range stats {
+ if _, ok := perc[stats[i].Language]; !ok {
+ continue
+ }
+ if stats[i].Language == "other" || len(topstats) >= limit {
+ other += perc[stats[i].Language]
+ continue
+ }
+ stats[i].Percentage = perc[stats[i].Language]
+ topstats = append(topstats, stats[i])
+ }
+ if other > 0 {
+ topstats = append(topstats, &LanguageStat{
+ RepoID: repo.ID,
+ Language: "other",
+ Color: "#cccccc",
+ Percentage: float32(math.Round(float64(other)*10) / 10),
+ })
+ }
+ topstats.LoadAttributes()
+ return topstats, nil
+}
+
+// UpdateLanguageStats updates the language statistics for repository
+func UpdateLanguageStats(repo *Repository, commitID string, stats map[string]int64) error {
+ ctx, committer, err := db.TxContext()
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+ sess := db.GetEngine(ctx)
+
+ oldstats, err := getLanguageStats(sess, repo)
+ if err != nil {
+ return err
+ }
+ var topLang string
+ var s int64
+ for lang, size := range stats {
+ if size > s {
+ s = size
+ topLang = strings.ToLower(lang)
+ }
+ }
+
+ for lang, size := range stats {
+ upd := false
+ llang := strings.ToLower(lang)
+ for _, s := range oldstats {
+ // Update already existing language
+ if strings.ToLower(s.Language) == llang {
+ s.CommitID = commitID
+ s.IsPrimary = llang == topLang
+ s.Size = size
+ if _, err := sess.ID(s.ID).Cols("`commit_id`", "`size`", "`is_primary`").Update(s); err != nil {
+ return err
+ }
+ upd = true
+ break
+ }
+ }
+ // Insert new language
+ if !upd {
+ if _, err := sess.Insert(&LanguageStat{
+ RepoID: repo.ID,
+ CommitID: commitID,
+ IsPrimary: llang == topLang,
+ Language: lang,
+ Size: size,
+ }); err != nil {
+ return err
+ }
+ }
+ }
+ // Delete old languages
+ statsToDelete := make([]int64, 0, len(oldstats))
+ for _, s := range oldstats {
+ if s.CommitID != commitID {
+ statsToDelete = append(statsToDelete, s.ID)
+ }
+ }
+ if len(statsToDelete) > 0 {
+ if _, err := sess.In("`id`", statsToDelete).Delete(&LanguageStat{}); err != nil {
+ return err
+ }
+ }
+
+ // Update indexer status
+ if err = updateIndexerStatus(sess, repo, RepoIndexerTypeStats, commitID); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+// CopyLanguageStat Copy originalRepo language stat information to destRepo (use for forked repo)
+func CopyLanguageStat(originalRepo, destRepo *Repository) error {
+ ctx, committer, err := db.TxContext()
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+ sess := db.GetEngine(ctx)
+
+ RepoLang := make(LanguageStatList, 0, 6)
+ if err := sess.Where("`repo_id` = ?", originalRepo.ID).Desc("`size`").Find(&RepoLang); err != nil {
+ return err
+ }
+ if len(RepoLang) > 0 {
+ for i := range RepoLang {
+ RepoLang[i].ID = 0
+ RepoLang[i].RepoID = destRepo.ID
+ RepoLang[i].CreatedUnix = timeutil.TimeStampNow()
+ }
+ // update destRepo's indexer status
+ tmpCommitID := RepoLang[0].CommitID
+ if err := updateIndexerStatus(sess, destRepo, RepoIndexerTypeStats, tmpCommitID); err != nil {
+ return err
+ }
+ if _, err := sess.Insert(&RepoLang); err != nil {
+ return err
+ }
+ }
+ return committer.Commit()
+}
diff --git a/models/repo/main_test.go b/models/repo/main_test.go
index aa960bf132..f40a976281 100644
--- a/models/repo/main_test.go
+++ b/models/repo/main_test.go
@@ -15,5 +15,8 @@ func TestMain(m *testing.M) {
unittest.MainTest(m, filepath.Join("..", ".."),
"attachment.yml",
"repo_archiver.yml",
+ "repository.yml",
+ "repo_unit.yml",
+ "repo_indexer_status.yml",
)
}
diff --git a/models/repo/mirror.go b/models/repo/mirror.go
new file mode 100644
index 0000000000..bdb449af3a
--- /dev/null
+++ b/models/repo/mirror.go
@@ -0,0 +1,177 @@
+// Copyright 2016 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 repo
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+)
+
+var (
+ // ErrMirrorNotExist mirror does not exist error
+ ErrMirrorNotExist = errors.New("Mirror does not exist")
+)
+
+// RemoteMirrorer defines base methods for pull/push mirrors.
+type RemoteMirrorer interface {
+ GetRepository() *Repository
+ GetRemoteName() string
+}
+
+// Mirror represents mirror information of a repository.
+type Mirror struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX"`
+ Repo *Repository `xorm:"-"`
+ Interval time.Duration
+ EnablePrune bool `xorm:"NOT NULL DEFAULT true"`
+
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX"`
+ NextUpdateUnix timeutil.TimeStamp `xorm:"INDEX"`
+
+ LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"`
+ LFSEndpoint string `xorm:"lfs_endpoint TEXT"`
+
+ Address string `xorm:"-"`
+}
+
+func init() {
+ db.RegisterModel(new(Mirror))
+}
+
+// BeforeInsert will be invoked by XORM before inserting a record
+func (m *Mirror) BeforeInsert() {
+ if m != nil {
+ m.UpdatedUnix = timeutil.TimeStampNow()
+ m.NextUpdateUnix = timeutil.TimeStampNow()
+ }
+}
+
+// AfterLoad is invoked from XORM after setting the values of all fields of this object.
+func (m *Mirror) AfterLoad(session *xorm.Session) {
+ if m == nil {
+ return
+ }
+
+ var err error
+ m.Repo, err = getRepositoryByID(session, m.RepoID)
+ if err != nil {
+ log.Error("getRepositoryByID[%d]: %v", m.ID, err)
+ }
+}
+
+// GetRepository returns the repository.
+func (m *Mirror) GetRepository() *Repository {
+ return m.Repo
+}
+
+// GetRemoteName returns the name of the remote.
+func (m *Mirror) GetRemoteName() string {
+ return "origin"
+}
+
+// ScheduleNextUpdate calculates and sets next update time.
+func (m *Mirror) ScheduleNextUpdate() {
+ if m.Interval != 0 {
+ m.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(m.Interval)
+ } else {
+ m.NextUpdateUnix = 0
+ }
+}
+
+func getMirrorByRepoID(e db.Engine, repoID int64) (*Mirror, error) {
+ m := &Mirror{RepoID: repoID}
+ has, err := e.Get(m)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrMirrorNotExist
+ }
+ return m, nil
+}
+
+// GetMirrorByRepoID returns mirror information of a repository.
+func GetMirrorByRepoID(repoID int64) (*Mirror, error) {
+ return getMirrorByRepoID(db.GetEngine(db.DefaultContext), repoID)
+}
+
+func updateMirror(e db.Engine, m *Mirror) error {
+ _, err := e.ID(m.ID).AllCols().Update(m)
+ return err
+}
+
+// UpdateMirror updates the mirror
+func UpdateMirror(m *Mirror) error {
+ return updateMirror(db.GetEngine(db.DefaultContext), m)
+}
+
+// DeleteMirrorByRepoID deletes a mirror by repoID
+func DeleteMirrorByRepoID(repoID int64) error {
+ _, err := db.GetEngine(db.DefaultContext).Delete(&Mirror{RepoID: repoID})
+ return err
+}
+
+// MirrorsIterate iterates all mirror repositories.
+func MirrorsIterate(f func(idx int, bean interface{}) error) error {
+ return db.GetEngine(db.DefaultContext).
+ Where("next_update_unix<=?", time.Now().Unix()).
+ And("next_update_unix!=0").
+ OrderBy("updated_unix ASC").
+ Iterate(new(Mirror), f)
+}
+
+// InsertMirror inserts a mirror to database
+func InsertMirror(mirror *Mirror) error {
+ _, err := db.GetEngine(db.DefaultContext).Insert(mirror)
+ return err
+}
+
+// MirrorRepositoryList contains the mirror repositories
+type MirrorRepositoryList []*Repository
+
+func (repos MirrorRepositoryList) loadAttributes(e db.Engine) error {
+ if len(repos) == 0 {
+ return nil
+ }
+
+ // Load mirrors.
+ repoIDs := make([]int64, 0, len(repos))
+ for i := range repos {
+ if !repos[i].IsMirror {
+ continue
+ }
+
+ repoIDs = append(repoIDs, repos[i].ID)
+ }
+ mirrors := make([]*Mirror, 0, len(repoIDs))
+ if err := e.
+ Where("id > 0").
+ In("repo_id", repoIDs).
+ Find(&mirrors); err != nil {
+ return fmt.Errorf("find mirrors: %v", err)
+ }
+
+ set := make(map[int64]*Mirror)
+ for i := range mirrors {
+ set[mirrors[i].RepoID] = mirrors[i]
+ }
+ for i := range repos {
+ repos[i].Mirror = set[repos[i].ID]
+ }
+ return nil
+}
+
+// LoadAttributes loads the attributes for the given MirrorRepositoryList
+func (repos MirrorRepositoryList) LoadAttributes() error {
+ return repos.loadAttributes(db.GetEngine(db.DefaultContext))
+}
diff --git a/models/repo/pushmirror.go b/models/repo/pushmirror.go
new file mode 100644
index 0000000000..0b62161641
--- /dev/null
+++ b/models/repo/pushmirror.go
@@ -0,0 +1,112 @@
+// Copyright 2021 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 repo
+
+import (
+ "errors"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+)
+
+var (
+ // ErrPushMirrorNotExist mirror does not exist error
+ ErrPushMirrorNotExist = errors.New("PushMirror does not exist")
+)
+
+// PushMirror represents mirror information of a repository.
+type PushMirror struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX"`
+ Repo *Repository `xorm:"-"`
+ RemoteName string
+
+ Interval time.Duration
+ CreatedUnix timeutil.TimeStamp `xorm:"created"`
+ LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
+ LastError string `xorm:"text"`
+}
+
+func init() {
+ db.RegisterModel(new(PushMirror))
+}
+
+// AfterLoad is invoked from XORM after setting the values of all fields of this object.
+func (m *PushMirror) AfterLoad(session *xorm.Session) {
+ if m == nil {
+ return
+ }
+
+ var err error
+ m.Repo, err = getRepositoryByID(session, m.RepoID)
+ if err != nil {
+ log.Error("getRepositoryByID[%d]: %v", m.ID, err)
+ }
+}
+
+// GetRepository returns the path of the repository.
+func (m *PushMirror) GetRepository() *Repository {
+ return m.Repo
+}
+
+// GetRemoteName returns the name of the remote.
+func (m *PushMirror) GetRemoteName() string {
+ return m.RemoteName
+}
+
+// InsertPushMirror inserts a push-mirror to database
+func InsertPushMirror(m *PushMirror) error {
+ _, err := db.GetEngine(db.DefaultContext).Insert(m)
+ return err
+}
+
+// UpdatePushMirror updates the push-mirror
+func UpdatePushMirror(m *PushMirror) error {
+ _, err := db.GetEngine(db.DefaultContext).ID(m.ID).AllCols().Update(m)
+ return err
+}
+
+// DeletePushMirrorByID deletes a push-mirrors by ID
+func DeletePushMirrorByID(ID int64) error {
+ _, err := db.GetEngine(db.DefaultContext).ID(ID).Delete(&PushMirror{})
+ return err
+}
+
+// DeletePushMirrorsByRepoID deletes all push-mirrors by repoID
+func DeletePushMirrorsByRepoID(repoID int64) error {
+ _, err := db.GetEngine(db.DefaultContext).Delete(&PushMirror{RepoID: repoID})
+ return err
+}
+
+// GetPushMirrorByID returns push-mirror information.
+func GetPushMirrorByID(ID int64) (*PushMirror, error) {
+ m := &PushMirror{}
+ has, err := db.GetEngine(db.DefaultContext).ID(ID).Get(m)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrPushMirrorNotExist
+ }
+ return m, nil
+}
+
+// GetPushMirrorsByRepoID returns push-mirror information of a repository.
+func GetPushMirrorsByRepoID(repoID int64) ([]*PushMirror, error) {
+ mirrors := make([]*PushMirror, 0, 10)
+ return mirrors, db.GetEngine(db.DefaultContext).Where("repo_id=?", repoID).Find(&mirrors)
+}
+
+// PushMirrorsIterate iterates all push-mirror repositories.
+func PushMirrorsIterate(f func(idx int, bean interface{}) error) error {
+ return db.GetEngine(db.DefaultContext).
+ Where("last_update + (`interval` / ?) <= ?", time.Second, time.Now().Unix()).
+ And("`interval` != 0").
+ OrderBy("last_update ASC").
+ Iterate(new(PushMirror), f)
+}
diff --git a/models/repo/pushmirror_test.go b/models/repo/pushmirror_test.go
new file mode 100644
index 0000000000..eff31fbac2
--- /dev/null
+++ b/models/repo/pushmirror_test.go
@@ -0,0 +1,50 @@
+// Copyright 2021 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 repo
+
+import (
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPushMirrorsIterate(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ now := timeutil.TimeStampNow()
+
+ InsertPushMirror(&PushMirror{
+ RemoteName: "test-1",
+ LastUpdateUnix: now,
+ Interval: 1,
+ })
+
+ long, _ := time.ParseDuration("24h")
+ InsertPushMirror(&PushMirror{
+ RemoteName: "test-2",
+ LastUpdateUnix: now,
+ Interval: long,
+ })
+
+ InsertPushMirror(&PushMirror{
+ RemoteName: "test-3",
+ LastUpdateUnix: now,
+ Interval: 0,
+ })
+
+ time.Sleep(1 * time.Millisecond)
+
+ PushMirrorsIterate(func(idx int, bean interface{}) error {
+ m, ok := bean.(*PushMirror)
+ assert.True(t, ok)
+ assert.Equal(t, "test-1", m.RemoteName)
+ assert.Equal(t, m.RemoteName, m.GetRemoteName())
+ return nil
+ })
+}
diff --git a/models/repo/repo.go b/models/repo/repo.go
new file mode 100644
index 0000000000..9353e813bc
--- /dev/null
+++ b/models/repo/repo.go
@@ -0,0 +1,736 @@
+// Copyright 2021 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 repo
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "net"
+ "net/url"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// TrustModelType defines the types of trust model for this repository
+type TrustModelType int
+
+// kinds of TrustModel
+const (
+ DefaultTrustModel TrustModelType = iota // default trust model
+ CommitterTrustModel
+ CollaboratorTrustModel
+ CollaboratorCommitterTrustModel
+)
+
+// String converts a TrustModelType to a string
+func (t TrustModelType) String() string {
+ switch t {
+ case DefaultTrustModel:
+ return "default"
+ case CommitterTrustModel:
+ return "committer"
+ case CollaboratorTrustModel:
+ return "collaborator"
+ case CollaboratorCommitterTrustModel:
+ return "collaboratorcommitter"
+ }
+ return "default"
+}
+
+// ToTrustModel converts a string to a TrustModelType
+func ToTrustModel(model string) TrustModelType {
+ switch strings.ToLower(strings.TrimSpace(model)) {
+ case "default":
+ return DefaultTrustModel
+ case "collaborator":
+ return CollaboratorTrustModel
+ case "committer":
+ return CommitterTrustModel
+ case "collaboratorcommitter":
+ return CollaboratorCommitterTrustModel
+ }
+ return DefaultTrustModel
+}
+
+// RepositoryStatus defines the status of repository
+type RepositoryStatus int
+
+// all kinds of RepositoryStatus
+const (
+ RepositoryReady RepositoryStatus = iota // a normal repository
+ RepositoryBeingMigrated // repository is migrating
+ RepositoryPendingTransfer // repository pending in ownership transfer state
+ RepositoryBroken // repository is in a permanently broken state
+)
+
+// Repository represents a git repository.
+type Repository struct {
+ ID int64 `xorm:"pk autoincr"`
+ OwnerID int64 `xorm:"UNIQUE(s) index"`
+ OwnerName string
+ Owner *user_model.User `xorm:"-"`
+ LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
+ Name string `xorm:"INDEX NOT NULL"`
+ Description string `xorm:"TEXT"`
+ Website string `xorm:"VARCHAR(2048)"`
+ OriginalServiceType api.GitServiceType `xorm:"index"`
+ OriginalURL string `xorm:"VARCHAR(2048)"`
+ DefaultBranch string
+
+ NumWatches int
+ NumStars int
+ NumForks int
+ NumIssues int
+ NumClosedIssues int
+ NumOpenIssues int `xorm:"-"`
+ NumPulls int
+ NumClosedPulls int
+ NumOpenPulls int `xorm:"-"`
+ NumMilestones int `xorm:"NOT NULL DEFAULT 0"`
+ NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"`
+ NumOpenMilestones int `xorm:"-"`
+ NumProjects int `xorm:"NOT NULL DEFAULT 0"`
+ NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"`
+ NumOpenProjects int `xorm:"-"`
+
+ IsPrivate bool `xorm:"INDEX"`
+ IsEmpty bool `xorm:"INDEX"`
+ IsArchived bool `xorm:"INDEX"`
+ IsMirror bool `xorm:"INDEX"`
+ *Mirror `xorm:"-"`
+ Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
+
+ RenderingMetas map[string]string `xorm:"-"`
+ DocumentRenderingMetas map[string]string `xorm:"-"`
+ Units []*RepoUnit `xorm:"-"`
+ PrimaryLanguage *LanguageStat `xorm:"-"`
+
+ IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
+ ForkID int64 `xorm:"INDEX"`
+ BaseRepo *Repository `xorm:"-"`
+ IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"`
+ TemplateID int64 `xorm:"INDEX"`
+ Size int64 `xorm:"NOT NULL DEFAULT 0"`
+ CodeIndexerStatus *RepoIndexerStatus `xorm:"-"`
+ StatsIndexerStatus *RepoIndexerStatus `xorm:"-"`
+ IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
+ CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
+ Topics []string `xorm:"TEXT JSON"`
+
+ TrustModel TrustModelType
+
+ // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
+ Avatar string `xorm:"VARCHAR(64)"`
+
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+}
+
+func init() {
+ db.RegisterModel(new(Repository))
+}
+
+// SanitizedOriginalURL returns a sanitized OriginalURL
+func (repo *Repository) SanitizedOriginalURL() string {
+ if repo.OriginalURL == "" {
+ return ""
+ }
+ u, err := url.Parse(repo.OriginalURL)
+ if err != nil {
+ return ""
+ }
+ u.User = nil
+ return u.String()
+}
+
+// ColorFormat returns a colored string to represent this repo
+func (repo *Repository) ColorFormat(s fmt.State) {
+ log.ColorFprintf(s, "%d:%s/%s",
+ log.NewColoredIDValue(repo.ID),
+ repo.OwnerName,
+ repo.Name)
+}
+
+// IsBeingMigrated indicates that repository is being migrated
+func (repo *Repository) IsBeingMigrated() bool {
+ return repo.Status == RepositoryBeingMigrated
+}
+
+// IsBeingCreated indicates that repository is being migrated or forked
+func (repo *Repository) IsBeingCreated() bool {
+ return repo.IsBeingMigrated()
+}
+
+// IsBroken indicates that repository is broken
+func (repo *Repository) IsBroken() bool {
+ return repo.Status == RepositoryBroken
+}
+
+// AfterLoad is invoked from XORM after setting the values of all fields of this object.
+func (repo *Repository) AfterLoad() {
+ // FIXME: use models migration to solve all at once.
+ if len(repo.DefaultBranch) == 0 {
+ repo.DefaultBranch = setting.Repository.DefaultBranch
+ }
+
+ repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
+ repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
+ repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
+ repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
+}
+
+// MustOwner always returns a valid *user_model.User object to avoid
+// conceptually impossible error handling.
+// It creates a fake object that contains error details
+// when error occurs.
+func (repo *Repository) MustOwner() *user_model.User {
+ return repo.mustOwner(db.DefaultContext)
+}
+
+// FullName returns the repository full name
+func (repo *Repository) FullName() string {
+ return repo.OwnerName + "/" + repo.Name
+}
+
+// HTMLURL returns the repository HTML URL
+func (repo *Repository) HTMLURL() string {
+ return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
+}
+
+// CommitLink make link to by commit full ID
+// note: won't check whether it's an right id
+func (repo *Repository) CommitLink(commitID string) (result string) {
+ if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
+ result = ""
+ } else {
+ result = repo.HTMLURL() + "/commit/" + url.PathEscape(commitID)
+ }
+ return
+}
+
+// APIURL returns the repository API URL
+func (repo *Repository) APIURL() string {
+ return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
+}
+
+// GetCommitsCountCacheKey returns cache key used for commits count caching.
+func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
+ var prefix string
+ if isRef {
+ prefix = "ref"
+ } else {
+ prefix = "commit"
+ }
+ return fmt.Sprintf("commits-count-%d-%s-%s", repo.ID, prefix, contextName)
+}
+
+// LoadUnits loads repo units into repo.Units
+func (repo *Repository) LoadUnits(ctx context.Context) (err error) {
+ if repo.Units != nil {
+ return nil
+ }
+
+ repo.Units, err = getUnitsByRepoID(db.GetEngine(ctx), repo.ID)
+ log.Trace("repo.Units: %-+v", repo.Units)
+ return err
+}
+
+// UnitEnabled if this repository has the given unit enabled
+func (repo *Repository) UnitEnabled(tp unit.Type) bool {
+ if err := repo.LoadUnits(db.DefaultContext); err != nil {
+ log.Warn("Error loading repository (ID: %d) units: %s", repo.ID, err.Error())
+ }
+ for _, unit := range repo.Units {
+ if unit.Type == tp {
+ return true
+ }
+ }
+ return false
+}
+
+// MustGetUnit always returns a RepoUnit object
+func (repo *Repository) MustGetUnit(tp unit.Type) *RepoUnit {
+ ru, err := repo.GetUnit(tp)
+ if err == nil {
+ return ru
+ }
+
+ if tp == unit.TypeExternalWiki {
+ return &RepoUnit{
+ Type: tp,
+ Config: new(ExternalWikiConfig),
+ }
+ } else if tp == unit.TypeExternalTracker {
+ return &RepoUnit{
+ Type: tp,
+ Config: new(ExternalTrackerConfig),
+ }
+ } else if tp == unit.TypePullRequests {
+ return &RepoUnit{
+ Type: tp,
+ Config: new(PullRequestsConfig),
+ }
+ } else if tp == unit.TypeIssues {
+ return &RepoUnit{
+ Type: tp,
+ Config: new(IssuesConfig),
+ }
+ }
+ return &RepoUnit{
+ Type: tp,
+ Config: new(UnitConfig),
+ }
+}
+
+// GetUnit returns a RepoUnit object
+func (repo *Repository) GetUnit(tp unit.Type) (*RepoUnit, error) {
+ return repo.getUnit(db.DefaultContext, tp)
+}
+
+func (repo *Repository) getUnit(ctx context.Context, tp unit.Type) (*RepoUnit, error) {
+ if err := repo.LoadUnits(ctx); err != nil {
+ return nil, err
+ }
+ for _, unit := range repo.Units {
+ if unit.Type == tp {
+ return unit, nil
+ }
+ }
+ return nil, ErrUnitTypeNotExist{tp}
+}
+
+// GetOwner returns the repository owner
+func (repo *Repository) GetOwner(ctx context.Context) (err error) {
+ if repo.Owner != nil {
+ return nil
+ }
+
+ repo.Owner, err = user_model.GetUserByIDEngine(db.GetEngine(ctx), repo.OwnerID)
+ return err
+}
+
+func (repo *Repository) mustOwner(ctx context.Context) *user_model.User {
+ if err := repo.GetOwner(ctx); err != nil {
+ return &user_model.User{
+ Name: "error",
+ FullName: err.Error(),
+ }
+ }
+
+ return repo.Owner
+}
+
+// ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers.
+func (repo *Repository) ComposeMetas() map[string]string {
+ if len(repo.RenderingMetas) == 0 {
+ metas := map[string]string{
+ "user": repo.OwnerName,
+ "repo": repo.Name,
+ "repoPath": repo.RepoPath(),
+ "mode": "comment",
+ }
+
+ unit, err := repo.GetUnit(unit.TypeExternalTracker)
+ if err == nil {
+ metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
+ switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
+ case markup.IssueNameStyleAlphanumeric:
+ metas["style"] = markup.IssueNameStyleAlphanumeric
+ default:
+ metas["style"] = markup.IssueNameStyleNumeric
+ }
+ }
+
+ repo.MustOwner()
+ if repo.Owner.IsOrganization() {
+ teams := make([]string, 0, 5)
+ _ = db.GetEngine(db.DefaultContext).Table("team_repo").
+ Join("INNER", "team", "team.id = team_repo.team_id").
+ Where("team_repo.repo_id = ?", repo.ID).
+ Select("team.lower_name").
+ OrderBy("team.lower_name").
+ Find(&teams)
+ metas["teams"] = "," + strings.Join(teams, ",") + ","
+ metas["org"] = strings.ToLower(repo.OwnerName)
+ }
+
+ repo.RenderingMetas = metas
+ }
+ return repo.RenderingMetas
+}
+
+// ComposeDocumentMetas composes a map of metas for properly rendering documents
+func (repo *Repository) ComposeDocumentMetas() map[string]string {
+ if len(repo.DocumentRenderingMetas) == 0 {
+ metas := map[string]string{}
+ for k, v := range repo.ComposeMetas() {
+ metas[k] = v
+ }
+ metas["mode"] = "document"
+ repo.DocumentRenderingMetas = metas
+ }
+ return repo.DocumentRenderingMetas
+}
+
+// GetBaseRepo populates repo.BaseRepo for a fork repository and
+// returns an error on failure (NOTE: no error is returned for
+// non-fork repositories, and BaseRepo will be left untouched)
+func (repo *Repository) GetBaseRepo() (err error) {
+ return repo.getBaseRepo(db.GetEngine(db.DefaultContext))
+}
+
+func (repo *Repository) getBaseRepo(e db.Engine) (err error) {
+ if !repo.IsFork {
+ return nil
+ }
+
+ repo.BaseRepo, err = getRepositoryByID(e, repo.ForkID)
+ return err
+}
+
+// IsGenerated returns whether _this_ repository was generated from a template
+func (repo *Repository) IsGenerated() bool {
+ return repo.TemplateID != 0
+}
+
+// RepoPath returns repository path by given user and repository name.
+func RepoPath(userName, repoName string) string { //revive:disable-line:exported
+ return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git")
+}
+
+// RepoPath returns the repository path
+func (repo *Repository) RepoPath() string {
+ return RepoPath(repo.OwnerName, repo.Name)
+}
+
+// GitConfigPath returns the path to a repository's git config/ directory
+func GitConfigPath(repoPath string) string {
+ return filepath.Join(repoPath, "config")
+}
+
+// GitConfigPath returns the repository git config path
+func (repo *Repository) GitConfigPath() string {
+ return GitConfigPath(repo.RepoPath())
+}
+
+// Link returns the repository link
+func (repo *Repository) Link() string {
+ return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
+}
+
+// ComposeCompareURL returns the repository comparison URL
+func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
+ return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID))
+}
+
+// IsOwnedBy returns true when user owns this repository
+func (repo *Repository) IsOwnedBy(userID int64) bool {
+ return repo.OwnerID == userID
+}
+
+// CanCreateBranch returns true if repository meets the requirements for creating new branches.
+func (repo *Repository) CanCreateBranch() bool {
+ return !repo.IsMirror
+}
+
+// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
+func (repo *Repository) CanEnablePulls() bool {
+ return !repo.IsMirror && !repo.IsEmpty
+}
+
+// AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled.
+func (repo *Repository) AllowsPulls() bool {
+ return repo.CanEnablePulls() && repo.UnitEnabled(unit.TypePullRequests)
+}
+
+// CanEnableEditor returns true if repository meets the requirements of web editor.
+func (repo *Repository) CanEnableEditor() bool {
+ return !repo.IsMirror
+}
+
+// DescriptionHTML does special handles to description and return HTML string.
+func (repo *Repository) DescriptionHTML() template.HTML {
+ desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
+ URLPrefix: repo.HTMLURL(),
+ Metas: repo.ComposeMetas(),
+ }, repo.Description)
+ if err != nil {
+ log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
+ return template.HTML(markup.Sanitize(repo.Description))
+ }
+ return template.HTML(markup.Sanitize(string(desc)))
+}
+
+// CloneLink represents different types of clone URLs of repository.
+type CloneLink struct {
+ SSH string
+ HTTPS string
+ Git string
+}
+
+// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name.
+func ComposeHTTPSCloneURL(owner, repo string) string {
+ return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo))
+}
+
+func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
+ repoName := repo.Name
+ if isWiki {
+ repoName += ".wiki"
+ }
+
+ sshUser := setting.RunUser
+ if setting.SSH.StartBuiltinServer {
+ sshUser = setting.SSH.BuiltinServerUser
+ }
+
+ cl := new(CloneLink)
+
+ // if we have a ipv6 literal we need to put brackets around it
+ // for the git cloning to work.
+ sshDomain := setting.SSH.Domain
+ ip := net.ParseIP(setting.SSH.Domain)
+ if ip != nil && ip.To4() == nil {
+ sshDomain = "[" + setting.SSH.Domain + "]"
+ }
+
+ if setting.SSH.Port != 22 {
+ cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
+ } else if setting.Repository.UseCompatSSHURI {
+ cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
+ } else {
+ cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
+ }
+ cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName)
+ return cl
+}
+
+// CloneLink returns clone URLs of repository.
+func (repo *Repository) CloneLink() (cl *CloneLink) {
+ return repo.cloneLink(false)
+}
+
+// GetOriginalURLHostname returns the hostname of a URL or the URL
+func (repo *Repository) GetOriginalURLHostname() string {
+ u, err := url.Parse(repo.OriginalURL)
+ if err != nil {
+ return repo.OriginalURL
+ }
+
+ return u.Host
+}
+
+// GetTrustModel will get the TrustModel for the repo or the default trust model
+func (repo *Repository) GetTrustModel() TrustModelType {
+ trustModel := repo.TrustModel
+ if trustModel == DefaultTrustModel {
+ trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel)
+ if trustModel == DefaultTrustModel {
+ return CollaboratorTrustModel
+ }
+ }
+ return trustModel
+}
+
+// GetRepositoryByOwnerAndName returns the repository by given ownername and reponame.
+func GetRepositoryByOwnerAndName(ownerName, repoName string) (*Repository, error) {
+ return GetRepositoryByOwnerAndNameCtx(db.DefaultContext, ownerName, repoName)
+}
+
+// __________ .__ __
+// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
+// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
+// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
+// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
+// \/ \/|__| \/ \/
+
+// ErrRepoNotExist represents a "RepoNotExist" kind of error.
+type ErrRepoNotExist struct {
+ ID int64
+ UID int64
+ OwnerName string
+ Name string
+}
+
+// IsErrRepoNotExist checks if an error is a ErrRepoNotExist.
+func IsErrRepoNotExist(err error) bool {
+ _, ok := err.(ErrRepoNotExist)
+ return ok
+}
+
+func (err ErrRepoNotExist) Error() string {
+ return fmt.Sprintf("repository does not exist [id: %d, uid: %d, owner_name: %s, name: %s]",
+ err.ID, err.UID, err.OwnerName, err.Name)
+}
+
+// GetRepositoryByOwnerAndNameCtx returns the repository by given owner name and repo name
+func GetRepositoryByOwnerAndNameCtx(ctx context.Context, ownerName, repoName string) (*Repository, error) {
+ var repo Repository
+ has, err := db.GetEngine(ctx).Table("repository").Select("repository.*").
+ Join("INNER", "`user`", "`user`.id = repository.owner_id").
+ Where("repository.lower_name = ?", strings.ToLower(repoName)).
+ And("`user`.lower_name = ?", strings.ToLower(ownerName)).
+ Get(&repo)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrRepoNotExist{0, 0, ownerName, repoName}
+ }
+ return &repo, nil
+}
+
+// GetRepositoryByName returns the repository by given name under user if exists.
+func GetRepositoryByName(ownerID int64, name string) (*Repository, error) {
+ repo := &Repository{
+ OwnerID: ownerID,
+ LowerName: strings.ToLower(name),
+ }
+ has, err := db.GetEngine(db.DefaultContext).Get(repo)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrRepoNotExist{0, ownerID, "", name}
+ }
+ return repo, err
+}
+
+func getRepositoryByID(e db.Engine, id int64) (*Repository, error) {
+ repo := new(Repository)
+ has, err := e.ID(id).Get(repo)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrRepoNotExist{id, 0, "", ""}
+ }
+ return repo, nil
+}
+
+// GetRepositoryByID returns the repository by given id if exists.
+func GetRepositoryByID(id int64) (*Repository, error) {
+ return getRepositoryByID(db.GetEngine(db.DefaultContext), id)
+}
+
+// GetRepositoryByIDCtx returns the repository by given id if exists.
+func GetRepositoryByIDCtx(ctx context.Context, id int64) (*Repository, error) {
+ return getRepositoryByID(db.GetEngine(ctx), id)
+}
+
+// GetRepositoriesMapByIDs returns the repositories by given id slice.
+func GetRepositoriesMapByIDs(ids []int64) (map[int64]*Repository, error) {
+ repos := make(map[int64]*Repository, len(ids))
+ return repos, db.GetEngine(db.DefaultContext).In("id", ids).Find(&repos)
+}
+
+// IsRepositoryExistCtx returns true if the repository with given name under user has already existed.
+func IsRepositoryExistCtx(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
+ has, err := db.GetEngine(ctx).Get(&Repository{
+ OwnerID: u.ID,
+ LowerName: strings.ToLower(repoName),
+ })
+ if err != nil {
+ return false, err
+ }
+ isDir, err := util.IsDir(RepoPath(u.Name, repoName))
+ return has && isDir, err
+}
+
+// IsRepositoryExist returns true if the repository with given name under user has already existed.
+func IsRepositoryExist(u *user_model.User, repoName string) (bool, error) {
+ return IsRepositoryExistCtx(db.DefaultContext, u, repoName)
+}
+
+// GetTemplateRepo populates repo.TemplateRepo for a generated repository and
+// returns an error on failure (NOTE: no error is returned for
+// non-generated repositories, and TemplateRepo will be left untouched)
+func GetTemplateRepo(repo *Repository) (*Repository, error) {
+ return getTemplateRepo(db.GetEngine(db.DefaultContext), repo)
+}
+
+func getTemplateRepo(e db.Engine, repo *Repository) (*Repository, error) {
+ if !repo.IsGenerated() {
+ return nil, nil
+ }
+
+ return getRepositoryByID(e, repo.TemplateID)
+}
+
+func countRepositories(userID int64, private bool) int64 {
+ sess := db.GetEngine(db.DefaultContext).Where("id > 0")
+
+ if userID > 0 {
+ sess.And("owner_id = ?", userID)
+ }
+ if !private {
+ sess.And("is_private=?", false)
+ }
+
+ count, err := sess.Count(new(Repository))
+ if err != nil {
+ log.Error("countRepositories: %v", err)
+ }
+ return count
+}
+
+// CountRepositories returns number of repositories.
+// Argument private only takes effect when it is false,
+// set it true to count all repositories.
+func CountRepositories(private bool) int64 {
+ return countRepositories(-1, private)
+}
+
+// CountUserRepositories returns number of repositories user owns.
+// Argument private only takes effect when it is false,
+// set it true to count all repositories.
+func CountUserRepositories(userID int64, private bool) int64 {
+ return countRepositories(userID, private)
+}
+
+// GetUserMirrorRepositories returns a list of mirror repositories of given user.
+func GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
+ repos := make([]*Repository, 0, 10)
+ return repos, db.GetEngine(db.DefaultContext).
+ Where("owner_id = ?", userID).
+ And("is_mirror = ?", true).
+ Find(&repos)
+}
+
+func getRepositoryCount(e db.Engine, ownerID int64) (int64, error) {
+ return e.Count(&Repository{OwnerID: ownerID})
+}
+
+func getPublicRepositoryCount(e db.Engine, u *user_model.User) (int64, error) {
+ return e.Where("is_private = ?", false).Count(&Repository{OwnerID: u.ID})
+}
+
+func getPrivateRepositoryCount(e db.Engine, u *user_model.User) (int64, error) {
+ return e.Where("is_private = ?", true).Count(&Repository{OwnerID: u.ID})
+}
+
+// GetRepositoryCount returns the total number of repositories of user.
+func GetRepositoryCount(ctx context.Context, ownerID int64) (int64, error) {
+ return getRepositoryCount(db.GetEngine(ctx), ownerID)
+}
+
+// GetPublicRepositoryCount returns the total number of public repositories of user.
+func GetPublicRepositoryCount(u *user_model.User) (int64, error) {
+ return getPublicRepositoryCount(db.GetEngine(db.DefaultContext), u)
+}
+
+// GetPrivateRepositoryCount returns the total number of private repositories of user.
+func GetPrivateRepositoryCount(u *user_model.User) (int64, error) {
+ return getPrivateRepositoryCount(db.GetEngine(db.DefaultContext), u)
+}
diff --git a/models/repo/repo_indexer.go b/models/repo/repo_indexer.go
new file mode 100644
index 0000000000..f442cad4d1
--- /dev/null
+++ b/models/repo/repo_indexer.go
@@ -0,0 +1,125 @@
+// 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 repo
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+
+ "xorm.io/builder"
+)
+
+// RepoIndexerType specifies the repository indexer type
+type RepoIndexerType int //revive:disable-line:exported
+
+const (
+ // RepoIndexerTypeCode code indexer
+ RepoIndexerTypeCode RepoIndexerType = iota // 0
+ // RepoIndexerTypeStats repository stats indexer
+ RepoIndexerTypeStats // 1
+)
+
+// RepoIndexerStatus status of a repo's entry in the repo indexer
+// For now, implicitly refers to default branch
+type RepoIndexerStatus struct { //revive:disable-line:exported
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX(s)"`
+ CommitSha string `xorm:"VARCHAR(40)"`
+ IndexerType RepoIndexerType `xorm:"INDEX(s) NOT NULL DEFAULT 0"`
+}
+
+func init() {
+ db.RegisterModel(new(RepoIndexerStatus))
+}
+
+// GetUnindexedRepos returns repos which do not have an indexer status
+func GetUnindexedRepos(indexerType RepoIndexerType, maxRepoID int64, page, pageSize int) ([]int64, error) {
+ ids := make([]int64, 0, 50)
+ cond := builder.Cond(builder.IsNull{
+ "repo_indexer_status.id",
+ }).And(builder.Eq{
+ "repository.is_empty": false,
+ })
+ sess := db.GetEngine(db.DefaultContext).Table("repository").Join("LEFT OUTER", "repo_indexer_status", "repository.id = repo_indexer_status.repo_id AND repo_indexer_status.indexer_type = ?", indexerType)
+ if maxRepoID > 0 {
+ cond = builder.And(cond, builder.Lte{
+ "repository.id": maxRepoID,
+ })
+ }
+ if page >= 0 && pageSize > 0 {
+ start := 0
+ if page > 0 {
+ start = (page - 1) * pageSize
+ }
+ sess.Limit(pageSize, start)
+ }
+
+ sess.Where(cond).Cols("repository.id").Desc("repository.id")
+ err := sess.Find(&ids)
+ return ids, err
+}
+
+// getIndexerStatus loads repo codes indxer status
+func getIndexerStatus(e db.Engine, repo *Repository, indexerType RepoIndexerType) (*RepoIndexerStatus, error) {
+ switch indexerType {
+ case RepoIndexerTypeCode:
+ if repo.CodeIndexerStatus != nil {
+ return repo.CodeIndexerStatus, nil
+ }
+ case RepoIndexerTypeStats:
+ if repo.StatsIndexerStatus != nil {
+ return repo.StatsIndexerStatus, nil
+ }
+ }
+ status := &RepoIndexerStatus{RepoID: repo.ID}
+ if has, err := e.Where("`indexer_type` = ?", indexerType).Get(status); err != nil {
+ return nil, err
+ } else if !has {
+ status.IndexerType = indexerType
+ status.CommitSha = ""
+ }
+ switch indexerType {
+ case RepoIndexerTypeCode:
+ repo.CodeIndexerStatus = status
+ case RepoIndexerTypeStats:
+ repo.StatsIndexerStatus = status
+ }
+ return status, nil
+}
+
+// GetIndexerStatus loads repo codes indxer status
+func GetIndexerStatus(repo *Repository, indexerType RepoIndexerType) (*RepoIndexerStatus, error) {
+ return getIndexerStatus(db.GetEngine(db.DefaultContext), repo, indexerType)
+}
+
+// updateIndexerStatus updates indexer status
+func updateIndexerStatus(e db.Engine, repo *Repository, indexerType RepoIndexerType, sha string) error {
+ status, err := getIndexerStatus(e, repo, indexerType)
+ if err != nil {
+ return fmt.Errorf("UpdateIndexerStatus: Unable to getIndexerStatus for repo: %s Error: %v", repo.FullName(), err)
+ }
+
+ if len(status.CommitSha) == 0 {
+ status.CommitSha = sha
+ _, err := e.Insert(status)
+ if err != nil {
+ return fmt.Errorf("UpdateIndexerStatus: Unable to insert repoIndexerStatus for repo: %s Sha: %s Error: %v", repo.FullName(), sha, err)
+ }
+ return nil
+ }
+ status.CommitSha = sha
+ _, err = e.ID(status.ID).Cols("commit_sha").
+ Update(status)
+ if err != nil {
+ return fmt.Errorf("UpdateIndexerStatus: Unable to update repoIndexerStatus for repo: %s Sha: %s Error: %v", repo.FullName(), sha, err)
+ }
+ return nil
+}
+
+// UpdateIndexerStatus updates indexer status
+func UpdateIndexerStatus(repo *Repository, indexerType RepoIndexerType, sha string) error {
+ return updateIndexerStatus(db.GetEngine(db.DefaultContext), repo, indexerType, sha)
+}
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
new file mode 100644
index 0000000000..6f48a22e49
--- /dev/null
+++ b/models/repo/repo_test.go
@@ -0,0 +1,44 @@
+// 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 repo
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetRepositoryCount(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ count, err1 := GetRepositoryCount(db.DefaultContext, 10)
+ privateCount, err2 := GetPrivateRepositoryCount(&user_model.User{ID: int64(10)})
+ publicCount, err3 := GetPublicRepositoryCount(&user_model.User{ID: int64(10)})
+ assert.NoError(t, err1)
+ assert.NoError(t, err2)
+ assert.NoError(t, err3)
+ assert.Equal(t, int64(3), count)
+ assert.Equal(t, privateCount+publicCount, count)
+}
+
+func TestGetPublicRepositoryCount(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ count, err := GetPublicRepositoryCount(&user_model.User{ID: int64(10)})
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1), count)
+}
+
+func TestGetPrivateRepositoryCount(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ count, err := GetPrivateRepositoryCount(&user_model.User{ID: int64(10)})
+ assert.NoError(t, err)
+ assert.Equal(t, int64(2), count)
+}
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
new file mode 100644
index 0000000000..5f6c43f02f
--- /dev/null
+++ b/models/repo/repo_unit.go
@@ -0,0 +1,244 @@
+// 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 repo
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/login"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+ "xorm.io/xorm/convert"
+)
+
+// ErrUnitTypeNotExist represents a "UnitTypeNotExist" kind of error.
+type ErrUnitTypeNotExist struct {
+ UT unit.Type
+}
+
+// IsErrUnitTypeNotExist checks if an error is a ErrUnitNotExist.
+func IsErrUnitTypeNotExist(err error) bool {
+ _, ok := err.(ErrUnitTypeNotExist)
+ return ok
+}
+
+func (err ErrUnitTypeNotExist) Error() string {
+ return fmt.Sprintf("Unit type does not exist: %s", err.UT.String())
+}
+
+// RepoUnit describes all units of a repository
+type RepoUnit struct { //revive:disable-line:exported
+ ID int64
+ RepoID int64 `xorm:"INDEX(s)"`
+ Type unit.Type `xorm:"INDEX(s)"`
+ Config convert.Conversion `xorm:"TEXT"`
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
+}
+
+func init() {
+ db.RegisterModel(new(RepoUnit))
+}
+
+// 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.UnmarshalHandleDoubleEncode(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.UnmarshalHandleDoubleEncode(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.UnmarshalHandleDoubleEncode(bs, &cfg)
+}
+
+// ToDB exports a ExternalTrackerConfig to a serialized format.
+func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
+ return json.Marshal(cfg)
+}
+
+// IssuesConfig describes issues config
+type IssuesConfig struct {
+ EnableTimetracker bool
+ AllowOnlyContributorsToTrackTime bool
+ EnableDependencies bool
+}
+
+// FromDB fills up a IssuesConfig from serialized format.
+func (cfg *IssuesConfig) FromDB(bs []byte) error {
+ return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+}
+
+// ToDB exports a IssuesConfig to a serialized format.
+func (cfg *IssuesConfig) ToDB() ([]byte, error) {
+ return json.Marshal(cfg)
+}
+
+// PullRequestsConfig describes pull requests config
+type PullRequestsConfig struct {
+ IgnoreWhitespaceConflicts bool
+ AllowMerge bool
+ AllowRebase bool
+ AllowRebaseMerge bool
+ AllowSquash bool
+ AllowManualMerge bool
+ AutodetectManualMerge bool
+ DefaultDeleteBranchAfterMerge bool
+ DefaultMergeStyle MergeStyle
+}
+
+// FromDB fills up a PullRequestsConfig from serialized format.
+func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
+ return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+}
+
+// ToDB exports a PullRequestsConfig to a serialized format.
+func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
+ return json.Marshal(cfg)
+}
+
+// IsMergeStyleAllowed returns if merge style is allowed
+func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
+ return mergeStyle == MergeStyleMerge && cfg.AllowMerge ||
+ mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
+ mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
+ mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
+ mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
+}
+
+// GetDefaultMergeStyle returns the default merge style for this pull request
+func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
+ if len(cfg.DefaultMergeStyle) != 0 {
+ return cfg.DefaultMergeStyle
+ }
+
+ return MergeStyleMerge
+}
+
+// AllowedMergeStyleCount returns the total count of allowed merge styles for the PullRequestsConfig
+func (cfg *PullRequestsConfig) AllowedMergeStyleCount() int {
+ count := 0
+ if cfg.AllowMerge {
+ count++
+ }
+ if cfg.AllowRebase {
+ count++
+ }
+ if cfg.AllowRebaseMerge {
+ count++
+ }
+ if cfg.AllowSquash {
+ count++
+ }
+ return count
+}
+
+// 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 unit.Type(login.Cell2Int64(val)) {
+ case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
+ r.Config = new(UnitConfig)
+ case unit.TypeExternalWiki:
+ r.Config = new(ExternalWikiConfig)
+ case unit.TypeExternalTracker:
+ r.Config = new(ExternalTrackerConfig)
+ case unit.TypePullRequests:
+ r.Config = new(PullRequestsConfig)
+ case unit.TypeIssues:
+ r.Config = new(IssuesConfig)
+ default:
+ panic(fmt.Sprintf("unrecognized repo unit type: %v", *val))
+ }
+ }
+}
+
+// Unit returns Unit
+func (r *RepoUnit) Unit() unit.Unit {
+ return unit.Units[r.Type]
+}
+
+// CodeConfig returns config for unit.TypeCode
+func (r *RepoUnit) CodeConfig() *UnitConfig {
+ return r.Config.(*UnitConfig)
+}
+
+// PullRequestsConfig returns config for unit.TypePullRequests
+func (r *RepoUnit) PullRequestsConfig() *PullRequestsConfig {
+ return r.Config.(*PullRequestsConfig)
+}
+
+// ReleasesConfig returns config for unit.TypeReleases
+func (r *RepoUnit) ReleasesConfig() *UnitConfig {
+ return r.Config.(*UnitConfig)
+}
+
+// ExternalWikiConfig returns config for unit.TypeExternalWiki
+func (r *RepoUnit) ExternalWikiConfig() *ExternalWikiConfig {
+ return r.Config.(*ExternalWikiConfig)
+}
+
+// IssuesConfig returns config for unit.TypeIssues
+func (r *RepoUnit) IssuesConfig() *IssuesConfig {
+ return r.Config.(*IssuesConfig)
+}
+
+// ExternalTrackerConfig returns config for unit.TypeExternalTracker
+func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
+ return r.Config.(*ExternalTrackerConfig)
+}
+
+func getUnitsByRepoID(e db.Engine, repoID int64) (units []*RepoUnit, err error) {
+ var tmpUnits []*RepoUnit
+ if err := e.Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
+ return nil, err
+ }
+
+ for _, u := range tmpUnits {
+ if !u.Type.UnitGlobalDisabled() {
+ units = append(units, u)
+ }
+ }
+
+ return units, nil
+}
+
+// UpdateRepoUnit updates the provided repo unit
+func UpdateRepoUnit(unit *RepoUnit) error {
+ _, err := db.GetEngine(db.DefaultContext).ID(unit.ID).Update(unit)
+ return err
+}
diff --git a/models/repo/wiki.go b/models/repo/wiki.go
new file mode 100644
index 0000000000..abf0155cad
--- /dev/null
+++ b/models/repo/wiki.go
@@ -0,0 +1,39 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// 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 repo
+
+import (
+ "path/filepath"
+ "strings"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// WikiCloneLink returns clone URLs of repository wiki.
+func (repo *Repository) WikiCloneLink() *CloneLink {
+ return repo.cloneLink(true)
+}
+
+// WikiPath returns wiki data path by given user and repository name.
+func WikiPath(userName, repoName string) string {
+ return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".wiki.git")
+}
+
+// WikiPath returns wiki data path for given repository.
+func (repo *Repository) WikiPath() string {
+ return WikiPath(repo.OwnerName, repo.Name)
+}
+
+// HasWiki returns true if repository has wiki.
+func (repo *Repository) HasWiki() bool {
+ isDir, err := util.IsDir(repo.WikiPath())
+ if err != nil {
+ log.Error("Unable to check if %s is a directory: %v", repo.WikiPath(), err)
+ }
+ return isDir
+}
diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go
new file mode 100644
index 0000000000..72f5280ce5
--- /dev/null
+++ b/models/repo/wiki_test.go
@@ -0,0 +1,45 @@
+// 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 repo
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepository_WikiCloneLink(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
+ cloneLink := repo.WikiCloneLink()
+ assert.Equal(t, "ssh://runuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH)
+ assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS)
+}
+
+func TestWikiPath(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git")
+ assert.Equal(t, expected, WikiPath("user2", "repo1"))
+}
+
+func TestRepository_WikiPath(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
+ expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git")
+ assert.Equal(t, expected, repo.WikiPath())
+}
+
+func TestRepository_HasWiki(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ repo1 := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
+ assert.True(t, repo1.HasWiki())
+ repo2 := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
+ assert.False(t, repo2.HasWiki())
+}