aboutsummaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2021-11-16 21:30:11 +0800
committerGitHub <noreply@github.com>2021-11-16 21:30:11 +0800
commit48ccd325a1b81a58ac6d1d5d94fc4e90974599ea (patch)
treee7bb31d69343dd9845a1b0df013b093c8720f14b /services
parent447428f44659cee0e94e1c444f6291defac2bda2 (diff)
downloadgitea-48ccd325a1b81a58ac6d1d5d94fc4e90974599ea.tar.gz
gitea-48ccd325a1b81a58ac6d1d5d94fc4e90974599ea.zip
Move some functions into services/repository (#17660)
Diffstat (limited to 'services')
-rw-r--r--services/cron/cron.go94
-rw-r--r--services/cron/setting.go87
-rw-r--r--services/cron/tasks.go169
-rw-r--r--services/cron/tasks_basic.go141
-rw-r--r--services/cron/tasks_extended.go166
-rw-r--r--services/repository/adopt.go386
-rw-r--r--services/repository/archive.go20
-rw-r--r--services/repository/fork.go175
-rw-r--r--services/repository/fork_test.go30
-rw-r--r--services/repository/repository.go30
10 files changed, 1268 insertions, 30 deletions
diff --git a/services/cron/cron.go b/services/cron/cron.go
new file mode 100644
index 0000000000..ae309bd866
--- /dev/null
+++ b/services/cron/cron.go
@@ -0,0 +1,94 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 cron
+
+import (
+ "context"
+ "time"
+
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/sync"
+
+ "github.com/gogs/cron"
+)
+
+var c = cron.New()
+
+// Prevent duplicate running tasks.
+var taskStatusTable = sync.NewStatusTable()
+
+// NewContext begins cron tasks
+// Each cron task is run within the shutdown context as a running server
+// AtShutdown the cron server is stopped
+func NewContext() {
+ initBasicTasks()
+ initExtendedTasks()
+
+ lock.Lock()
+ for _, task := range tasks {
+ if task.IsEnabled() && task.DoRunAtStart() {
+ go task.Run()
+ }
+ }
+
+ c.Start()
+ started = true
+ lock.Unlock()
+ graceful.GetManager().RunAtShutdown(context.Background(), func() {
+ c.Stop()
+ lock.Lock()
+ started = false
+ lock.Unlock()
+ })
+
+}
+
+// TaskTableRow represents a task row in the tasks table
+type TaskTableRow struct {
+ Name string
+ Spec string
+ Next time.Time
+ Prev time.Time
+ ExecTimes int64
+}
+
+// TaskTable represents a table of tasks
+type TaskTable []*TaskTableRow
+
+// ListTasks returns all running cron tasks.
+func ListTasks() TaskTable {
+ entries := c.Entries()
+ eMap := map[string]*cron.Entry{}
+ for _, e := range entries {
+ eMap[e.Description] = e
+ }
+ lock.Lock()
+ defer lock.Unlock()
+ tTable := make([]*TaskTableRow, 0, len(tasks))
+ for _, task := range tasks {
+ spec := "-"
+ var (
+ next time.Time
+ prev time.Time
+ )
+ if e, ok := eMap[task.Name]; ok {
+ spec = e.Spec
+ next = e.Next
+ prev = e.Prev
+ }
+ task.lock.Lock()
+ tTable = append(tTable, &TaskTableRow{
+ Name: task.Name,
+ Spec: spec,
+ Next: next,
+ Prev: prev,
+ ExecTimes: task.ExecTimes,
+ })
+ task.lock.Unlock()
+ }
+
+ return tTable
+}
diff --git a/services/cron/setting.go b/services/cron/setting.go
new file mode 100644
index 0000000000..d55e5b60ad
--- /dev/null
+++ b/services/cron/setting.go
@@ -0,0 +1,87 @@
+// 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 cron
+
+import (
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "github.com/unknwon/i18n"
+)
+
+// Config represents a basic configuration interface that cron task
+type Config interface {
+ IsEnabled() bool
+ DoRunAtStart() bool
+ GetSchedule() string
+ FormatMessage(name, status string, doer *models.User, args ...interface{}) string
+ DoNoticeOnSuccess() bool
+}
+
+// BaseConfig represents the basic config for a Cron task
+type BaseConfig struct {
+ Enabled bool
+ RunAtStart bool
+ Schedule string
+ NoSuccessNotice bool
+}
+
+// OlderThanConfig represents a cron task with OlderThan setting
+type OlderThanConfig struct {
+ BaseConfig
+ OlderThan time.Duration
+}
+
+// UpdateExistingConfig represents a cron task with UpdateExisting setting
+type UpdateExistingConfig struct {
+ BaseConfig
+ UpdateExisting bool
+}
+
+// CleanupHookTaskConfig represents a cron task with settings to cleanup hook_task
+type CleanupHookTaskConfig struct {
+ BaseConfig
+ CleanupType string
+ OlderThan time.Duration
+ NumberToKeep int
+}
+
+// GetSchedule returns the schedule for the base config
+func (b *BaseConfig) GetSchedule() string {
+ return b.Schedule
+}
+
+// IsEnabled returns the enabled status for the config
+func (b *BaseConfig) IsEnabled() bool {
+ return b.Enabled
+}
+
+// DoRunAtStart returns whether the task should be run at the start
+func (b *BaseConfig) DoRunAtStart() bool {
+ return b.RunAtStart
+}
+
+// DoNoticeOnSuccess returns whether a success notice should be posted
+func (b *BaseConfig) DoNoticeOnSuccess() bool {
+ return !b.NoSuccessNotice
+}
+
+// FormatMessage returns a message for the task
+func (b *BaseConfig) FormatMessage(name, status string, doer *models.User, args ...interface{}) string {
+ realArgs := make([]interface{}, 0, len(args)+2)
+ realArgs = append(realArgs, i18n.Tr("en-US", "admin.dashboard."+name))
+ if doer == nil {
+ realArgs = append(realArgs, "(Cron)")
+ } else {
+ realArgs = append(realArgs, doer.Name)
+ }
+ if len(args) > 0 {
+ realArgs = append(realArgs, args...)
+ }
+ if doer == nil || (doer.ID == -1 && doer.Name == "(Cron)") {
+ return i18n.Tr("en-US", "admin.dashboard.cron."+status, realArgs...)
+ }
+ return i18n.Tr("en-US", "admin.dashboard.task."+status, realArgs...)
+}
diff --git a/services/cron/tasks.go b/services/cron/tasks.go
new file mode 100644
index 0000000000..56c363e0b8
--- /dev/null
+++ b/services/cron/tasks.go
@@ -0,0 +1,169 @@
+// 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 cron
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "sync"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+var lock = sync.Mutex{}
+var started = false
+var tasks = []*Task{}
+var tasksMap = map[string]*Task{}
+
+// Task represents a Cron task
+type Task struct {
+ lock sync.Mutex
+ Name string
+ config Config
+ fun func(context.Context, *models.User, Config) error
+ ExecTimes int64
+}
+
+// DoRunAtStart returns if this task should run at the start
+func (t *Task) DoRunAtStart() bool {
+ return t.config.DoRunAtStart()
+}
+
+// IsEnabled returns if this task is enabled as cron task
+func (t *Task) IsEnabled() bool {
+ return t.config.IsEnabled()
+}
+
+// GetConfig will return a copy of the task's config
+func (t *Task) GetConfig() Config {
+ if reflect.TypeOf(t.config).Kind() == reflect.Ptr {
+ // Pointer:
+ return reflect.New(reflect.ValueOf(t.config).Elem().Type()).Interface().(Config)
+ }
+ // Not pointer:
+ return reflect.New(reflect.TypeOf(t.config)).Elem().Interface().(Config)
+}
+
+// Run will run the task incrementing the cron counter with no user defined
+func (t *Task) Run() {
+ t.RunWithUser(&models.User{
+ ID: -1,
+ Name: "(Cron)",
+ LowerName: "(cron)",
+ }, t.config)
+}
+
+// RunWithUser will run the task incrementing the cron counter at the time with User
+func (t *Task) RunWithUser(doer *models.User, config Config) {
+ if !taskStatusTable.StartIfNotRunning(t.Name) {
+ return
+ }
+ t.lock.Lock()
+ if config == nil {
+ config = t.config
+ }
+ t.ExecTimes++
+ t.lock.Unlock()
+ defer func() {
+ taskStatusTable.Stop(t.Name)
+ if err := recover(); err != nil {
+ // Recover a panic within the
+ combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2))
+ log.Error("PANIC whilst running task: %s Value: %v", t.Name, combinedErr)
+ }
+ }()
+ graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
+ ctx, cancel := context.WithCancel(baseCtx)
+ defer cancel()
+ pm := process.GetManager()
+ pid := pm.Add(config.FormatMessage(t.Name, "process", doer), cancel)
+ defer pm.Remove(pid)
+ if err := t.fun(ctx, doer, config); err != nil {
+ if db.IsErrCancelled(err) {
+ message := err.(db.ErrCancelled).Message
+ if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "aborted", doer, message)); err != nil {
+ log.Error("CreateNotice: %v", err)
+ }
+ return
+ }
+ if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "error", doer, err)); err != nil {
+ log.Error("CreateNotice: %v", err)
+ }
+ return
+ }
+ if config.DoNoticeOnSuccess() {
+ if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "finished", doer)); err != nil {
+ log.Error("CreateNotice: %v", err)
+ }
+ }
+ })
+}
+
+// GetTask gets the named task
+func GetTask(name string) *Task {
+ lock.Lock()
+ defer lock.Unlock()
+ log.Info("Getting %s in %v", name, tasksMap[name])
+
+ return tasksMap[name]
+}
+
+// RegisterTask allows a task to be registered with the cron service
+func RegisterTask(name string, config Config, fun func(context.Context, *models.User, Config) error) error {
+ log.Debug("Registering task: %s", name)
+ _, err := setting.GetCronSettings(name, config)
+ if err != nil {
+ log.Error("Unable to register cron task with name: %s Error: %v", name, err)
+ return err
+ }
+
+ task := &Task{
+ Name: name,
+ config: config,
+ fun: fun,
+ }
+ lock.Lock()
+ locked := true
+ defer func() {
+ if locked {
+ lock.Unlock()
+ }
+ }()
+ if _, has := tasksMap[task.Name]; has {
+ log.Error("A task with this name: %s has already been registered", name)
+ return fmt.Errorf("duplicate task with name: %s", task.Name)
+ }
+
+ if config.IsEnabled() {
+ // We cannot use the entry return as there is no way to lock it
+ if _, err = c.AddJob(name, config.GetSchedule(), task); err != nil {
+ log.Error("Unable to register cron task with name: %s Error: %v", name, err)
+ return err
+ }
+ }
+
+ tasks = append(tasks, task)
+ tasksMap[task.Name] = task
+ if started && config.IsEnabled() && config.DoRunAtStart() {
+ lock.Unlock()
+ locked = false
+ task.Run()
+ }
+
+ return nil
+}
+
+// RegisterTaskFatal will register a task but if there is an error log.Fatal
+func RegisterTaskFatal(name string, config Config, fun func(context.Context, *models.User, Config) error) {
+ if err := RegisterTask(name, config, fun); err != nil {
+ log.Fatal("Unable to register cron task %s Error: %v", name, err)
+ }
+}
diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go
new file mode 100644
index 0000000000..a42c031b36
--- /dev/null
+++ b/services/cron/tasks_basic.go
@@ -0,0 +1,141 @@
+// 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 cron
+
+import (
+ "context"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/migrations"
+ repository_service "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/auth"
+ mirror_service "code.gitea.io/gitea/services/mirror"
+)
+
+func registerUpdateMirrorTask() {
+ RegisterTaskFatal("update_mirrors", &BaseConfig{
+ Enabled: true,
+ RunAtStart: false,
+ Schedule: "@every 10m",
+ NoSuccessNotice: true,
+ }, func(ctx context.Context, _ *models.User, _ Config) error {
+ return mirror_service.Update(ctx)
+ })
+}
+
+func registerRepoHealthCheck() {
+ type RepoHealthCheckConfig struct {
+ BaseConfig
+ Timeout time.Duration
+ Args []string `delim:" "`
+ }
+ RegisterTaskFatal("repo_health_check", &RepoHealthCheckConfig{
+ BaseConfig: BaseConfig{
+ Enabled: true,
+ RunAtStart: false,
+ Schedule: "@midnight",
+ },
+ Timeout: 60 * time.Second,
+ Args: []string{},
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ rhcConfig := config.(*RepoHealthCheckConfig)
+ return repository_service.GitFsck(ctx, rhcConfig.Timeout, rhcConfig.Args)
+ })
+}
+
+func registerCheckRepoStats() {
+ RegisterTaskFatal("check_repo_stats", &BaseConfig{
+ Enabled: true,
+ RunAtStart: true,
+ Schedule: "@midnight",
+ }, func(ctx context.Context, _ *models.User, _ Config) error {
+ return models.CheckRepoStats(ctx)
+ })
+}
+
+func registerArchiveCleanup() {
+ RegisterTaskFatal("archive_cleanup", &OlderThanConfig{
+ BaseConfig: BaseConfig{
+ Enabled: true,
+ RunAtStart: true,
+ Schedule: "@midnight",
+ },
+ OlderThan: 24 * time.Hour,
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ acConfig := config.(*OlderThanConfig)
+ return models.DeleteOldRepositoryArchives(ctx, acConfig.OlderThan)
+ })
+}
+
+func registerSyncExternalUsers() {
+ RegisterTaskFatal("sync_external_users", &UpdateExistingConfig{
+ BaseConfig: BaseConfig{
+ Enabled: true,
+ RunAtStart: false,
+ Schedule: "@midnight",
+ },
+ UpdateExisting: true,
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ realConfig := config.(*UpdateExistingConfig)
+ return auth.SyncExternalUsers(ctx, realConfig.UpdateExisting)
+ })
+}
+
+func registerDeletedBranchesCleanup() {
+ RegisterTaskFatal("deleted_branches_cleanup", &OlderThanConfig{
+ BaseConfig: BaseConfig{
+ Enabled: true,
+ RunAtStart: true,
+ Schedule: "@midnight",
+ },
+ OlderThan: 24 * time.Hour,
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ realConfig := config.(*OlderThanConfig)
+ models.RemoveOldDeletedBranches(ctx, realConfig.OlderThan)
+ return nil
+ })
+}
+
+func registerUpdateMigrationPosterID() {
+ RegisterTaskFatal("update_migration_poster_id", &BaseConfig{
+ Enabled: true,
+ RunAtStart: true,
+ Schedule: "@midnight",
+ }, func(ctx context.Context, _ *models.User, _ Config) error {
+ return migrations.UpdateMigrationPosterID(ctx)
+ })
+}
+
+func registerCleanupHookTaskTable() {
+ RegisterTaskFatal("cleanup_hook_task_table", &CleanupHookTaskConfig{
+ BaseConfig: BaseConfig{
+ Enabled: true,
+ RunAtStart: false,
+ Schedule: "@midnight",
+ },
+ CleanupType: "OlderThan",
+ OlderThan: 168 * time.Hour,
+ NumberToKeep: 10,
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ realConfig := config.(*CleanupHookTaskConfig)
+ return webhook.CleanupHookTaskTable(ctx, webhook.ToHookTaskCleanupType(realConfig.CleanupType), realConfig.OlderThan, realConfig.NumberToKeep)
+ })
+}
+
+func initBasicTasks() {
+ registerUpdateMirrorTask()
+ registerRepoHealthCheck()
+ registerCheckRepoStats()
+ registerArchiveCleanup()
+ registerSyncExternalUsers()
+ registerDeletedBranchesCleanup()
+ if !setting.Repository.DisableMigrations {
+ registerUpdateMigrationPosterID()
+ }
+ registerCleanupHookTaskTable()
+}
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
new file mode 100644
index 0000000000..4ddcd44537
--- /dev/null
+++ b/services/cron/tasks_extended.go
@@ -0,0 +1,166 @@
+// 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 cron
+
+import (
+ "context"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/updatechecker"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+func registerDeleteInactiveUsers() {
+ RegisterTaskFatal("delete_inactive_accounts", &OlderThanConfig{
+ BaseConfig: BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@annually",
+ },
+ OlderThan: 0 * time.Second,
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ olderThanConfig := config.(*OlderThanConfig)
+ return models.DeleteInactiveUsers(ctx, olderThanConfig.OlderThan)
+ })
+}
+
+func registerDeleteRepositoryArchives() {
+ RegisterTaskFatal("delete_repo_archives", &BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@annually",
+ }, func(ctx context.Context, _ *models.User, _ Config) error {
+ return repo_service.DeleteRepositoryArchives(ctx)
+ })
+}
+
+func registerGarbageCollectRepositories() {
+ type RepoHealthCheckConfig struct {
+ BaseConfig
+ Timeout time.Duration
+ Args []string `delim:" "`
+ }
+ RegisterTaskFatal("git_gc_repos", &RepoHealthCheckConfig{
+ BaseConfig: BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 72h",
+ },
+ Timeout: time.Duration(setting.Git.Timeout.GC) * time.Second,
+ Args: setting.Git.GCArgs,
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ rhcConfig := config.(*RepoHealthCheckConfig)
+ return repo_module.GitGcRepos(ctx, rhcConfig.Timeout, rhcConfig.Args...)
+ })
+}
+
+func registerRewriteAllPublicKeys() {
+ RegisterTaskFatal("resync_all_sshkeys", &BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 72h",
+ }, func(_ context.Context, _ *models.User, _ Config) error {
+ return models.RewriteAllPublicKeys()
+ })
+}
+
+func registerRewriteAllPrincipalKeys() {
+ RegisterTaskFatal("resync_all_sshprincipals", &BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 72h",
+ }, func(_ context.Context, _ *models.User, _ Config) error {
+ return models.RewriteAllPrincipalKeys()
+ })
+}
+
+func registerRepositoryUpdateHook() {
+ RegisterTaskFatal("resync_all_hooks", &BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 72h",
+ }, func(ctx context.Context, _ *models.User, _ Config) error {
+ return repo_module.SyncRepositoryHooks(ctx)
+ })
+}
+
+func registerReinitMissingRepositories() {
+ RegisterTaskFatal("reinit_missing_repos", &BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 72h",
+ }, func(ctx context.Context, _ *models.User, _ Config) error {
+ return repo_module.ReinitMissingRepositories(ctx)
+ })
+}
+
+func registerDeleteMissingRepositories() {
+ RegisterTaskFatal("delete_missing_repos", &BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 72h",
+ }, func(ctx context.Context, user *models.User, _ Config) error {
+ return repo_module.DeleteMissingRepositories(ctx, user)
+ })
+}
+
+func registerRemoveRandomAvatars() {
+ RegisterTaskFatal("delete_generated_repository_avatars", &BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 72h",
+ }, func(ctx context.Context, _ *models.User, _ Config) error {
+ return models.RemoveRandomAvatars(ctx)
+ })
+}
+
+func registerDeleteOldActions() {
+ RegisterTaskFatal("delete_old_actions", &OlderThanConfig{
+ BaseConfig: BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 168h",
+ },
+ OlderThan: 365 * 24 * time.Hour,
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ olderThanConfig := config.(*OlderThanConfig)
+ return models.DeleteOldActions(olderThanConfig.OlderThan)
+ })
+}
+
+func registerUpdateGiteaChecker() {
+ type UpdateCheckerConfig struct {
+ BaseConfig
+ HTTPEndpoint string
+ }
+ RegisterTaskFatal("update_checker", &UpdateCheckerConfig{
+ BaseConfig: BaseConfig{
+ Enabled: true,
+ RunAtStart: false,
+ Schedule: "@every 168h",
+ },
+ HTTPEndpoint: "https://dl.gitea.io/gitea/version.json",
+ }, func(ctx context.Context, _ *models.User, config Config) error {
+ updateCheckerConfig := config.(*UpdateCheckerConfig)
+ return updatechecker.GiteaUpdateChecker(updateCheckerConfig.HTTPEndpoint)
+ })
+}
+
+func initExtendedTasks() {
+ registerDeleteInactiveUsers()
+ registerDeleteRepositoryArchives()
+ registerGarbageCollectRepositories()
+ registerRewriteAllPublicKeys()
+ registerRewriteAllPrincipalKeys()
+ registerRepositoryUpdateHook()
+ registerReinitMissingRepositories()
+ registerDeleteMissingRepositories()
+ registerRemoveRandomAvatars()
+ registerDeleteOldActions()
+ registerUpdateGiteaChecker()
+}
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
new file mode 100644
index 0000000000..d48411fbb4
--- /dev/null
+++ b/services/repository/adopt.go
@@ -0,0 +1,386 @@
+// 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 repository
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/notification"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/gobwas/glob"
+)
+
+// AdoptRepository adopts pre-existing repository files for the user/organization.
+func AdoptRepository(doer, u *models.User, opts models.CreateRepoOptions) (*models.Repository, error) {
+ if !doer.IsAdmin && !u.CanCreateRepo() {
+ return nil, models.ErrReachLimitOfRepo{
+ Limit: u.MaxRepoCreation,
+ }
+ }
+
+ if len(opts.DefaultBranch) == 0 {
+ opts.DefaultBranch = setting.Repository.DefaultBranch
+ }
+
+ repo := &models.Repository{
+ OwnerID: u.ID,
+ Owner: u,
+ OwnerName: u.Name,
+ Name: opts.Name,
+ LowerName: strings.ToLower(opts.Name),
+ Description: opts.Description,
+ OriginalURL: opts.OriginalURL,
+ OriginalServiceType: opts.GitServiceType,
+ IsPrivate: opts.IsPrivate,
+ IsFsckEnabled: !opts.IsMirror,
+ CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
+ Status: opts.Status,
+ IsEmpty: !opts.AutoInit,
+ }
+
+ if err := db.WithTx(func(ctx context.Context) error {
+ repoPath := models.RepoPath(u.Name, repo.Name)
+ isExist, err := util.IsExist(repoPath)
+ if err != nil {
+ log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
+ return err
+ }
+ if !isExist {
+ return models.ErrRepoNotExist{
+ OwnerName: u.Name,
+ Name: repo.Name,
+ }
+ }
+
+ if err := models.CreateRepository(ctx, doer, u, repo, true); err != nil {
+ return err
+ }
+ if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil {
+ return fmt.Errorf("createDelegateHooks: %v", err)
+ }
+ if err := repo.CheckDaemonExportOK(ctx); err != nil {
+ return fmt.Errorf("checkDaemonExportOK: %v", err)
+ }
+
+ // Initialize Issue Labels if selected
+ if len(opts.IssueLabels) > 0 {
+ if err := models.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
+ return fmt.Errorf("InitializeLabels: %v", err)
+ }
+ }
+
+ if stdout, err := git.NewCommand("update-server-info").
+ SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
+ RunInDir(repoPath); err != nil {
+ log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+ return fmt.Errorf("CreateRepository(git update-server-info): %v", err)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ notification.NotifyCreateRepository(doer, u, repo)
+
+ return repo, nil
+}
+
+func adoptRepository(ctx context.Context, repoPath string, u *models.User, repo *models.Repository, opts models.CreateRepoOptions) (err error) {
+ isExist, err := util.IsExist(repoPath)
+ if err != nil {
+ log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
+ return err
+ }
+ if !isExist {
+ return fmt.Errorf("adoptRepository: path does not already exist: %s", repoPath)
+ }
+
+ if err := repo_module.CreateDelegateHooks(repoPath); err != nil {
+ return fmt.Errorf("createDelegateHooks: %v", err)
+ }
+
+ // Re-fetch the repository from database before updating it (else it would
+ // override changes that were done earlier with sql)
+ if repo, err = models.GetRepositoryByIDCtx(ctx, repo.ID); err != nil {
+ return fmt.Errorf("getRepositoryByID: %v", err)
+ }
+
+ repo.IsEmpty = false
+ gitRepo, err := git.OpenRepository(repo.RepoPath())
+ if err != nil {
+ return fmt.Errorf("openRepository: %v", err)
+ }
+ defer gitRepo.Close()
+ if len(opts.DefaultBranch) > 0 {
+ repo.DefaultBranch = opts.DefaultBranch
+
+ if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ return fmt.Errorf("setDefaultBranch: %v", err)
+ }
+ } else {
+ repo.DefaultBranch, err = gitRepo.GetDefaultBranch()
+ if err != nil {
+ repo.DefaultBranch = setting.Repository.DefaultBranch
+ if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ return fmt.Errorf("setDefaultBranch: %v", err)
+ }
+ }
+
+ repo.DefaultBranch = strings.TrimPrefix(repo.DefaultBranch, git.BranchPrefix)
+ }
+ branches, _, _ := gitRepo.GetBranches(0, 0)
+ found := false
+ hasDefault := false
+ hasMaster := false
+ hasMain := false
+ for _, branch := range branches {
+ if branch == repo.DefaultBranch {
+ found = true
+ break
+ } else if branch == setting.Repository.DefaultBranch {
+ hasDefault = true
+ } else if branch == "master" {
+ hasMaster = true
+ } else if branch == "main" {
+ hasMain = true
+ }
+ }
+ if !found {
+ if hasDefault {
+ repo.DefaultBranch = setting.Repository.DefaultBranch
+ } else if hasMaster {
+ repo.DefaultBranch = "master"
+ } else if hasMain {
+ repo.DefaultBranch = "main"
+ } else if len(branches) > 0 {
+ repo.DefaultBranch = branches[0]
+ } else {
+ repo.IsEmpty = true
+ repo.DefaultBranch = setting.Repository.DefaultBranch
+ }
+
+ if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ return fmt.Errorf("setDefaultBranch: %v", err)
+ }
+ }
+
+ if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil {
+ return fmt.Errorf("updateRepository: %v", err)
+ }
+
+ return nil
+}
+
+// DeleteUnadoptedRepository deletes unadopted repository files from the filesystem
+func DeleteUnadoptedRepository(doer, u *models.User, repoName string) error {
+ if err := models.IsUsableRepoName(repoName); err != nil {
+ return err
+ }
+
+ repoPath := models.RepoPath(u.Name, repoName)
+ isExist, err := util.IsExist(repoPath)
+ if err != nil {
+ log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
+ return err
+ }
+ if !isExist {
+ return models.ErrRepoNotExist{
+ OwnerName: u.Name,
+ Name: repoName,
+ }
+ }
+
+ if exist, err := models.IsRepositoryExist(u, repoName); err != nil {
+ return err
+ } else if exist {
+ return models.ErrRepoAlreadyExist{
+ Uname: u.Name,
+ Name: repoName,
+ }
+ }
+
+ return util.RemoveAll(repoPath)
+}
+
+// ListUnadoptedRepositories lists all the unadopted repositories that match the provided query
+func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, int, error) {
+ globUser, _ := glob.Compile("*")
+ globRepo, _ := glob.Compile("*")
+
+ qsplit := strings.SplitN(query, "/", 2)
+ if len(qsplit) > 0 && len(query) > 0 {
+ var err error
+ globUser, err = glob.Compile(qsplit[0])
+ if err != nil {
+ log.Info("Invalid glob expression '%s' (skipped): %v", qsplit[0], err)
+ }
+ if len(qsplit) > 1 {
+ globRepo, err = glob.Compile(qsplit[1])
+ if err != nil {
+ log.Info("Invalid glob expression '%s' (skipped): %v", qsplit[1], err)
+ }
+ }
+ }
+ start := (opts.Page - 1) * opts.PageSize
+ end := start + opts.PageSize
+
+ repoNamesToCheck := make([]string, 0, opts.PageSize)
+
+ repoNames := make([]string, 0, opts.PageSize)
+ var ctxUser *models.User
+
+ count := 0
+
+ // We're going to iterate by pagesize.
+ root := filepath.Clean(setting.RepoRootPath)
+ if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() || path == root {
+ return nil
+ }
+
+ if !strings.ContainsRune(path[len(root)+1:], filepath.Separator) {
+ // Got a new user
+
+ // Clean up old repoNamesToCheck
+ if len(repoNamesToCheck) > 0 {
+ repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{
+ Actor: ctxUser,
+ Private: true,
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: opts.PageSize,
+ }, LowerNames: repoNamesToCheck})
+ if err != nil {
+ return err
+ }
+ for _, name := range repoNamesToCheck {
+ found := false
+ repoLoopCatchup:
+ for i, repo := range repos {
+ if repo.LowerName == name {
+ found = true
+ repos = append(repos[:i], repos[i+1:]...)
+ break repoLoopCatchup
+ }
+ }
+ if !found {
+ if count >= start && count < end {
+ repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name))
+ }
+ count++
+ }
+ }
+ repoNamesToCheck = repoNamesToCheck[:0]
+ }
+
+ if !globUser.Match(info.Name()) {
+ return filepath.SkipDir
+ }
+
+ ctxUser, err = models.GetUserByName(info.Name())
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ log.Debug("Missing user: %s", info.Name())
+ return filepath.SkipDir
+ }
+ return err
+ }
+ return nil
+ }
+
+ name := info.Name()
+
+ if !strings.HasSuffix(name, ".git") {
+ return filepath.SkipDir
+ }
+ name = name[:len(name)-4]
+ if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name || !globRepo.Match(name) {
+ return filepath.SkipDir
+ }
+ if count < end {
+ repoNamesToCheck = append(repoNamesToCheck, name)
+ if len(repoNamesToCheck) >= opts.PageSize {
+ repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{
+ Actor: ctxUser,
+ Private: true,
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: opts.PageSize,
+ }, LowerNames: repoNamesToCheck})
+ if err != nil {
+ return err
+ }
+ for _, name := range repoNamesToCheck {
+ found := false
+ repoLoop:
+ for i, repo := range repos {
+ if repo.LowerName == name {
+ found = true
+ repos = append(repos[:i], repos[i+1:]...)
+ break repoLoop
+ }
+ }
+ if !found {
+ if count >= start && count < end {
+ repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name))
+ }
+ count++
+ }
+ }
+ repoNamesToCheck = repoNamesToCheck[:0]
+ }
+ return filepath.SkipDir
+ }
+ count++
+ return filepath.SkipDir
+ }); err != nil {
+ return nil, 0, err
+ }
+
+ if len(repoNamesToCheck) > 0 {
+ repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{
+ Actor: ctxUser,
+ Private: true,
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: opts.PageSize,
+ }, LowerNames: repoNamesToCheck})
+ if err != nil {
+ return nil, 0, err
+ }
+ for _, name := range repoNamesToCheck {
+ found := false
+ repoLoop:
+ for i, repo := range repos {
+ if repo.LowerName == name {
+ found = true
+ repos = append(repos[:i], repos[i+1:]...)
+ break repoLoop
+ }
+ }
+ if !found {
+ if count >= start && count < end {
+ repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name))
+ }
+ count++
+ }
+ }
+ }
+ return repoNames, count, nil
+}
diff --git a/services/repository/archive.go b/services/repository/archive.go
new file mode 100644
index 0000000000..bea636c579
--- /dev/null
+++ b/services/repository/archive.go
@@ -0,0 +1,20 @@
+// 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 repository
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/storage"
+)
+
+// DeleteRepositoryArchives deletes all repositories' archives.
+func DeleteRepositoryArchives(ctx context.Context) error {
+ if err := models.DeleteAllRepoArchives(); err != nil {
+ return err
+ }
+ return storage.Clean(storage.RepoArchives)
+}
diff --git a/services/repository/fork.go b/services/repository/fork.go
new file mode 100644
index 0000000000..f052d18a75
--- /dev/null
+++ b/services/repository/fork.go
@@ -0,0 +1,175 @@
+// Copyright 2019 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 repository
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/notification"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// ForkRepository forks a repository
+func ForkRepository(doer, owner *models.User, opts models.ForkRepoOptions) (_ *models.Repository, err error) {
+ forkedRepo, err := opts.BaseRepo.GetUserFork(owner.ID)
+ if err != nil {
+ return nil, err
+ }
+ if forkedRepo != nil {
+ return nil, models.ErrForkAlreadyExist{
+ Uname: owner.Name,
+ RepoName: opts.BaseRepo.FullName(),
+ ForkName: forkedRepo.FullName(),
+ }
+ }
+
+ repo := &models.Repository{
+ OwnerID: owner.ID,
+ Owner: owner,
+ OwnerName: owner.Name,
+ Name: opts.Name,
+ LowerName: strings.ToLower(opts.Name),
+ Description: opts.Description,
+ DefaultBranch: opts.BaseRepo.DefaultBranch,
+ IsPrivate: opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate,
+ IsEmpty: opts.BaseRepo.IsEmpty,
+ IsFork: true,
+ ForkID: opts.BaseRepo.ID,
+ }
+
+ oldRepoPath := opts.BaseRepo.RepoPath()
+
+ needsRollback := false
+ rollbackFn := func() {
+ if !needsRollback {
+ return
+ }
+
+ repoPath := models.RepoPath(owner.Name, repo.Name)
+
+ if exists, _ := util.IsExist(repoPath); !exists {
+ return
+ }
+
+ // As the transaction will be failed and hence database changes will be destroyed we only need
+ // to delete the related repository on the filesystem
+ if errDelete := util.RemoveAll(repoPath); errDelete != nil {
+ log.Error("Failed to remove fork repo")
+ }
+ }
+
+ needsRollbackInPanic := true
+ defer func() {
+ panicErr := recover()
+ if panicErr == nil {
+ return
+ }
+
+ if needsRollbackInPanic {
+ rollbackFn()
+ }
+ panic(panicErr)
+ }()
+
+ err = db.WithTx(func(ctx context.Context) error {
+ if err = models.CreateRepository(ctx, doer, owner, repo, false); err != nil {
+ return err
+ }
+
+ if err = models.IncrementRepoForkNum(ctx, opts.BaseRepo.ID); err != nil {
+ return err
+ }
+
+ // copy lfs files failure should not be ignored
+ if err = models.CopyLFS(ctx, repo, opts.BaseRepo); err != nil {
+ return err
+ }
+
+ needsRollback = true
+
+ repoPath := models.RepoPath(owner.Name, repo.Name)
+ if stdout, err := git.NewCommandContext(ctx,
+ "clone", "--bare", oldRepoPath, repoPath).
+ SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())).
+ RunInDirTimeout(10*time.Minute, ""); err != nil {
+ log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)
+ return fmt.Errorf("git clone: %v", err)
+ }
+
+ if err := repo.CheckDaemonExportOK(ctx); err != nil {
+ return fmt.Errorf("checkDaemonExportOK: %v", err)
+ }
+
+ if stdout, err := git.NewCommandContext(ctx, "update-server-info").
+ SetDescription(fmt.Sprintf("ForkRepository(git update-server-info): %s", repo.FullName())).
+ RunInDir(repoPath); err != nil {
+ log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err)
+ return fmt.Errorf("git update-server-info: %v", err)
+ }
+
+ if err = repo_module.CreateDelegateHooks(repoPath); err != nil {
+ return fmt.Errorf("createDelegateHooks: %v", err)
+ }
+ return nil
+ })
+ needsRollbackInPanic = false
+ if err != nil {
+ rollbackFn()
+ return nil, err
+ }
+
+ // even if below operations failed, it could be ignored. And they will be retried
+ ctx := db.DefaultContext
+ if err := repo.UpdateSize(ctx); err != nil {
+ log.Error("Failed to update size for repository: %v", err)
+ }
+ if err := models.CopyLanguageStat(opts.BaseRepo, repo); err != nil {
+ log.Error("Copy language stat from oldRepo failed")
+ }
+
+ notification.NotifyForkRepository(doer, opts.BaseRepo, repo)
+
+ return repo, nil
+}
+
+// ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo
+func ConvertForkToNormalRepository(repo *models.Repository) error {
+ err := db.WithTx(func(ctx context.Context) error {
+ repo, err := models.GetRepositoryByIDCtx(ctx, repo.ID)
+ if err != nil {
+ return err
+ }
+
+ if !repo.IsFork {
+ return nil
+ }
+
+ if err := models.DecrementRepoForkNum(ctx, repo.ForkID); err != nil {
+ log.Error("Unable to decrement repo fork num for old root repo %d of repository %-v whilst converting from fork. Error: %v", repo.ForkID, repo, err)
+ return err
+ }
+
+ repo.IsFork = false
+ repo.ForkID = 0
+
+ if err := models.UpdateRepositoryCtx(ctx, repo, false); err != nil {
+ log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err)
+ return err
+ }
+
+ return nil
+ })
+
+ return err
+}
diff --git a/services/repository/fork_test.go b/services/repository/fork_test.go
new file mode 100644
index 0000000000..197d76b056
--- /dev/null
+++ b/services/repository/fork_test.go
@@ -0,0 +1,30 @@
+// 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 repository
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/unittest"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestForkRepository(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ // user 13 has already forked repo10
+ user := unittest.AssertExistsAndLoadBean(t, &models.User{ID: 13}).(*models.User)
+ repo := unittest.AssertExistsAndLoadBean(t, &models.Repository{ID: 10}).(*models.Repository)
+
+ fork, err := ForkRepository(user, user, models.ForkRepoOptions{
+ BaseRepo: repo,
+ Name: "test",
+ Description: "test",
+ })
+ assert.Nil(t, fork)
+ assert.Error(t, err)
+ assert.True(t, models.IsErrForkAlreadyExist(err))
+}
diff --git a/services/repository/repository.go b/services/repository/repository.go
index cde4af2afa..98d160c223 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -28,36 +28,6 @@ func CreateRepository(doer, owner *models.User, opts models.CreateRepoOptions) (
return repo, nil
}
-// AdoptRepository adopts pre-existing repository files for the user/organization.
-func AdoptRepository(doer, owner *models.User, opts models.CreateRepoOptions) (*models.Repository, error) {
- repo, err := repo_module.AdoptRepository(doer, owner, opts)
- if err != nil {
- // No need to rollback here we should do this in AdoptRepository...
- return nil, err
- }
-
- notification.NotifyCreateRepository(doer, owner, repo)
-
- return repo, nil
-}
-
-// DeleteUnadoptedRepository adopts pre-existing repository files for the user/organization.
-func DeleteUnadoptedRepository(doer, owner *models.User, name string) error {
- return repo_module.DeleteUnadoptedRepository(doer, owner, name)
-}
-
-// ForkRepository forks a repository
-func ForkRepository(doer, u *models.User, opts models.ForkRepoOptions) (*models.Repository, error) {
- repo, err := repo_module.ForkRepository(doer, u, opts)
- if err != nil {
- return nil, err
- }
-
- notification.NotifyForkRepository(doer, opts.BaseRepo, repo)
-
- return repo, nil
-}
-
// DeleteRepository deletes a repository for a user or organization.
func DeleteRepository(doer *models.User, repo *models.Repository) error {
if err := pull_service.CloseRepoBranchesPulls(doer, repo); err != nil {