gitea/modules/repository/adopt.go
zeripath 7a7f56044a
Adopt repositories (#12920)
* Don't automatically delete repository files if they are present

Prior to this PR Gitea would delete any repository files if they are
present during creation or migration. This can in certain circumstances
lead to data-loss and is slightly unpleasant.

This PR provides a mechanism for Gitea to adopt repositories on creation
and otherwise requires an explicit flag for deletion.

PushCreate is slightly different - the create will cause adoption if
that is allowed otherwise it will delete the data if that is allowed.

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Update swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Fix tests and migrate overwrite

Signed-off-by: Andrew Thornton <art27@cantab.net>

* as per @lunny

Only offer to adopt or overwrite if the user can do that.

Allow the site administrator to adopt or overwrite in all
circumstances

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Use setting.Repository.DefaultBranch for the default branch

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Always set setting.Repository.DefaultBranch

Signed-off-by: Andrew Thornton <art27@cantab.net>

* update swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* update templates

Signed-off-by: Andrew Thornton <art27@cantab.net>

* ensure repo closed

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Rewrite of adoption as per @6543 and @lunny

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Apply suggestions from code review

* update swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* missing not

Signed-off-by: Andrew Thornton <art27@cantab.net>

* add modals and flash reporting

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Make the unadopted page searchable

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add API

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Fix swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* fix swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Handle empty and non-master branched repositories

Signed-off-by: Andrew Thornton <art27@cantab.net>

* placate lint

Signed-off-by: Andrew Thornton <art27@cantab.net>

* remove commented out code

Signed-off-by: Andrew Thornton <art27@cantab.net>

Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2020-09-25 07:09:23 +03:00

272 lines
7.4 KiB
Go

// 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 (
"fmt"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
"github.com/unknwon/com"
)
// AdoptRepository adopts a repository 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 := models.WithTx(func(ctx models.DBContext) error {
repoPath := models.RepoPath(u.Name, repo.Name)
if !com.IsExist(repoPath) {
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)
}
// 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
}
return repo, 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)
if !com.IsExist(repoPath) {
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 *models.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 expresion '%s' (skipped): %v", qsplit[0], err)
}
if len(qsplit) > 1 {
globRepo, err = glob.Compile(qsplit[1])
if err != nil {
log.Info("Invalid glob expresion '%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.Join(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: models.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: models.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.Name == 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: models.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
}