diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2021-12-10 09:27:50 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-10 09:27:50 +0800 |
commit | 719bddcd76610a63dadc8555760072957a11cf30 (patch) | |
tree | 0df26092fba7e3e21444fe493e6b349473b6b0cb /models/repo | |
parent | fb8166c6c6b652a0e6fa98681780a6a71090faf3 (diff) | |
download | gitea-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.go | 94 | ||||
-rw-r--r-- | models/repo/git.go | 31 | ||||
-rw-r--r-- | models/repo/issue.go | 67 | ||||
-rw-r--r-- | models/repo/language_stats.go | 215 | ||||
-rw-r--r-- | models/repo/main_test.go | 3 | ||||
-rw-r--r-- | models/repo/mirror.go | 177 | ||||
-rw-r--r-- | models/repo/pushmirror.go | 112 | ||||
-rw-r--r-- | models/repo/pushmirror_test.go | 50 | ||||
-rw-r--r-- | models/repo/repo.go | 736 | ||||
-rw-r--r-- | models/repo/repo_indexer.go | 125 | ||||
-rw-r--r-- | models/repo/repo_test.go | 44 | ||||
-rw-r--r-- | models/repo/repo_unit.go | 244 | ||||
-rw-r--r-- | models/repo/wiki.go | 39 | ||||
-rw-r--r-- | models/repo/wiki_test.go | 45 |
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()) +} |