diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index ffc4e40678..25c3d5a59c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -64,6 +64,10 @@ PREFIX_ARCHIVE_FILES = true DISABLE_MIRRORS = false ; The default branch name of new repositories DEFAULT_BRANCH=master +; Allow adoption of unadopted repositories +ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES=false +; Allow deletion of unadopted repositories +ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES=false [repository.editor] ; List of file extensions for which lines should be wrapped in the Monaco editor diff --git a/models/error.go b/models/error.go index 13391e5d87..1cab19aafd 100644 --- a/models/error.go +++ b/models/error.go @@ -743,6 +743,22 @@ func (err ErrRepoAlreadyExist) Error() string { return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name) } +// ErrRepoFilesAlreadyExist represents a "RepoFilesAlreadyExist" kind of error. +type ErrRepoFilesAlreadyExist struct { + Uname string + Name string +} + +// IsErrRepoFilesAlreadyExist checks if an error is a ErrRepoAlreadyExist. +func IsErrRepoFilesAlreadyExist(err error) bool { + _, ok := err.(ErrRepoFilesAlreadyExist) + return ok +} + +func (err ErrRepoFilesAlreadyExist) Error() string { + return fmt.Sprintf("repository files already exist [uname: %s, name: %s]", err.Uname, err.Name) +} + // ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error. type ErrForkAlreadyExist struct { Uname string diff --git a/models/repo.go b/models/repo.go index 25fe3f63d3..96b359bca4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -278,7 +278,7 @@ func (repo *Repository) IsBeingCreated() bool { func (repo *Repository) AfterLoad() { // FIXME: use models migration to solve all at once. if len(repo.DefaultBranch) == 0 { - repo.DefaultBranch = "master" + repo.DefaultBranch = setting.Repository.DefaultBranch } repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues @@ -1048,7 +1048,7 @@ func (repo *Repository) CloneLink() (cl *CloneLink) { } // CheckCreateRepository check if could created a repository -func CheckCreateRepository(doer, u *User, name string) error { +func CheckCreateRepository(doer, u *User, name string, overwriteOrAdopt bool) error { if !doer.CanCreateRepo() { return ErrReachLimitOfRepo{u.MaxRepoCreation} } @@ -1063,6 +1063,10 @@ func CheckCreateRepository(doer, u *User, name string) error { } else if has { return ErrRepoAlreadyExist{u.Name, name} } + + if !overwriteOrAdopt && com.IsExist(RepoPath(u.Name, name)) { + return ErrRepoFilesAlreadyExist{u.Name, name} + } return nil } @@ -1116,11 +1120,15 @@ var ( // IsUsableRepoName returns true when repository is usable func IsUsableRepoName(name string) error { + if alphaDashDotPattern.MatchString(name) { + // Note: usually this error is normally caught up earlier in the UI + return ErrNameCharsNotAllowed{Name: name} + } return isUsableName(reservedRepoNames, reservedRepoPatterns, name) } // CreateRepository creates a repository for the user/organization. -func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error) { +func CreateRepository(ctx DBContext, doer, u *User, repo *Repository, overwriteOrAdopt bool) (err error) { if err = IsUsableRepoName(repo.Name); err != nil { return err } @@ -1132,6 +1140,15 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error return ErrRepoAlreadyExist{u.Name, repo.Name} } + repoPath := RepoPath(u.Name, repo.Name) + if !overwriteOrAdopt && com.IsExist(repoPath) { + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + if _, err = ctx.e.Insert(repo); err != nil { return err } @@ -1856,6 +1873,10 @@ func GetUserRepositories(opts *SearchRepoOptions) ([]*Repository, int64, error) cond = cond.And(builder.Eq{"is_private": false}) } + if opts.LowerNames != nil && len(opts.LowerNames) > 0 { + cond = cond.And(builder.In("lower_name", opts.LowerNames)) + } + sess := x.NewSession() defer sess.Close() diff --git a/models/repo_list.go b/models/repo_list.go index dea88d8816..355b801a7e 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -175,6 +175,8 @@ type SearchRepoOptions struct { // True -> include just has milestones // False -> include just has no milestone HasMilestones util.OptionalBool + // LowerNames represents valid lower names to restrict to + LowerNames []string } //SearchOrderBy is used to sort the result diff --git a/models/user.go b/models/user.go index c7b3f0981e..650d5a803a 100644 --- a/models/user.go +++ b/models/user.go @@ -646,8 +646,8 @@ func (u *User) GetOrganizationCount() (int64, error) { } // GetRepositories returns repositories that user owns, including private repositories. -func (u *User) GetRepositories(listOpts ListOptions) (err error) { - u.Repos, _, err = GetUserRepositories(&SearchRepoOptions{Actor: u, Private: true, ListOptions: listOpts}) +func (u *User) GetRepositories(listOpts ListOptions, names ...string) (err error) { + u.Repos, _, err = GetUserRepositories(&SearchRepoOptions{Actor: u, Private: true, ListOptions: listOpts, LowerNames: names}) return err } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 8f9c802e01..cd30c191ea 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -74,6 +74,11 @@ func (repo *Repository) SetDefaultBranch(name string) error { return err } +// GetDefaultBranch gets default branch of repository. +func (repo *Repository) GetDefaultBranch() (string, error) { + return NewCommand("symbolic-ref", "HEAD").RunInDir(repo.Path) +} + // GetBranches returns all branches of the repository. func (repo *Repository) GetBranches() ([]string, error) { var branchNames []string diff --git a/modules/repository/adopt.go b/modules/repository/adopt.go new file mode 100644 index 0000000000..22cd6dd91f --- /dev/null +++ b/modules/repository/adopt.go @@ -0,0 +1,272 @@ +// 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 +} diff --git a/modules/repository/create.go b/modules/repository/create.go index c180b9b948..e6a3e7081d 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -13,10 +13,12 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + + "github.com/unknwon/com" ) // CreateRepository creates a repository for the user/organization. -func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *models.Repository, err error) { +func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (*models.Repository, error) { if !doer.IsAdmin && !u.CanCreateRepo() { return nil, models.ErrReachLimitOfRepo{ Limit: u.MaxRepoCreation, @@ -44,39 +46,64 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m TrustModel: opts.TrustModel, } - err = models.WithTx(func(ctx models.DBContext) error { - if err = models.CreateRepository(ctx, doer, u, repo); err != nil { + if err := models.WithTx(func(ctx models.DBContext) error { + if err := models.CreateRepository(ctx, doer, u, repo, false); err != nil { return err } // No need for init mirror. - if !opts.IsMirror { - repoPath := models.RepoPath(u.Name, repo.Name) - if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { - if err2 := util.RemoveAll(repoPath); err2 != nil { - log.Error("initRepository: %v", err) - return fmt.Errorf( - "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) - } - return fmt.Errorf("initRepository: %v", err) - } + if opts.IsMirror { + return nil + } - // 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) + repoPath := models.RepoPath(u.Name, repo.Name) + if com.IsExist(repoPath) { + // repo already exists - We have two or three options. + // 1. We fail stating that the directory exists + // 2. We create the db repository to go with this data and adopt the git repo + // 3. We delete it and start afresh + // + // Previously Gitea would just delete and start afresh - this was naughty. + // So we will now fail and delegate to other functionality to adopt or delete + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return models.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, } } - return nil - }) - return repo, err + if err := initRepository(ctx, repoPath, doer, repo, opts); err != nil { + if err2 := util.RemoveAll(repoPath); err2 != nil { + log.Error("initRepository: %v", err) + return fmt.Errorf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) + } + return fmt.Errorf("initRepository: %v", err) + } + + // Initialize Issue Labels if selected + if len(opts.IssueLabels) > 0 { + if err := models.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { + if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + 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) + if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + return fmt.Errorf("CreateRepository(git update-server-info): %v", err) + } + return nil + }); err != nil { + return nil, err + } + + return repo, nil } diff --git a/modules/repository/fork.go b/modules/repository/fork.go index 169c391edd..cdd08e3d3c 100644 --- a/modules/repository/fork.go +++ b/modules/repository/fork.go @@ -46,11 +46,21 @@ func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name, oldRepoPath := oldRepo.RepoPath() err = models.WithTx(func(ctx models.DBContext) error { - if err = models.CreateRepository(ctx, doer, owner, repo); err != nil { + if err = models.CreateRepository(ctx, doer, owner, repo, false); err != nil { return err } + rollbackRemoveFn := func() { + if repo.ID == 0 { + return + } + if errDelete := models.DeleteRepository(doer, owner.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + } + if err = models.IncrementRepoForkNum(ctx, oldRepo.ID); err != nil { + rollbackRemoveFn() return err } @@ -60,6 +70,7 @@ func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name, SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", oldRepo.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, oldRepo, stdout, err) + rollbackRemoveFn() return fmt.Errorf("git clone: %v", err) } @@ -67,10 +78,12 @@ func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name, 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) + rollbackRemoveFn() return fmt.Errorf("git update-server-info: %v", err) } if err = createDelegateHooks(repoPath); err != nil { + rollbackRemoveFn() return fmt.Errorf("createDelegateHooks: %v", err) } return nil @@ -86,5 +99,12 @@ func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name, if err := models.CopyLanguageStat(oldRepo, repo); err != nil { log.Error("Copy language stat from oldRepo failed") } - return repo, models.CopyLFS(ctx, repo, oldRepo) + + if err := models.CopyLFS(ctx, repo, oldRepo); err != nil { + if errDelete := models.DeleteRepository(doer, owner.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + return nil, err + } + return repo, nil } diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 1314464a6e..5d1ef72b6c 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/huandu/xstrings" + "github.com/unknwon/com" ) type transformer struct { @@ -246,12 +247,19 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template TrustModel: templateRepo.TrustModel, } - if err = models.CreateRepository(ctx, doer, owner, generateRepo); err != nil { + if err = models.CreateRepository(ctx, doer, owner, generateRepo, false); err != nil { return nil, err } - repoPath := models.RepoPath(owner.Name, generateRepo.Name) - if err = checkInitRepository(repoPath); err != nil { + repoPath := generateRepo.RepoPath() + if com.IsExist(repoPath) { + return nil, models.ErrRepoFilesAlreadyExist{ + Uname: generateRepo.OwnerName, + Name: generateRepo.Name, + } + } + + if err = checkInitRepository(owner.Name, generateRepo.Name); err != nil { return generateRepo, err } diff --git a/modules/repository/init.go b/modules/repository/init.go index d066544a85..707f8f5250 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -172,10 +172,14 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def return nil } -func checkInitRepository(repoPath string) (err error) { +func checkInitRepository(owner, name string) (err error) { // Somehow the directory could exist. + repoPath := models.RepoPath(owner, name) if com.IsExist(repoPath) { - return fmt.Errorf("checkInitRepository: path already exists: %s", repoPath) + return models.ErrRepoFilesAlreadyExist{ + Uname: owner, + Name: name, + } } // Init git bare new repository. @@ -187,9 +191,85 @@ func checkInitRepository(repoPath string) (err error) { return nil } +func adoptRepository(ctx models.DBContext, repoPath string, u *models.User, repo *models.Repository, opts models.CreateRepoOptions) (err error) { + if !com.IsExist(repoPath) { + return fmt.Errorf("adoptRepository: path does not already exist: %s", repoPath) + } + + if err := 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() + found := false + hasDefault := false + hasMaster := 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 + } + } + if !found { + if hasDefault { + repo.DefaultBranch = setting.Repository.DefaultBranch + } else if hasMaster { + repo.DefaultBranch = "master" + } 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 +} + // InitRepository initializes README and .gitignore if needed. func initRepository(ctx models.DBContext, repoPath string, u *models.User, repo *models.Repository, opts models.CreateRepoOptions) (err error) { - if err = checkInitRepository(repoPath); err != nil { + if err = checkInitRepository(repo.OwnerName, repo.Name); err != nil { return err } @@ -225,7 +305,8 @@ func initRepository(ctx models.DBContext, repoPath string, u *models.User, repo repo.IsEmpty = true } - repo.DefaultBranch = "master" + repo.DefaultBranch = setting.Repository.DefaultBranch + if len(opts.DefaultBranch) > 0 { repo.DefaultBranch = opts.DefaultBranch gitRepo, err := git.OpenRepository(repo.RepoPath()) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 67dd805353..b5764f7fc4 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -44,6 +44,8 @@ var ( PrefixArchiveFiles bool DisableMirrors bool DefaultBranch string + AllowAdoptionOfUnadoptedRepositories bool + AllowDeleteOfUnadoptedRepositories bool // Repository editor settings Editor struct { @@ -146,6 +148,7 @@ var ( DefaultRepoUnits: []string{}, PrefixArchiveFiles: true, DisableMirrors: false, + DefaultBranch: "master", // Repository editor settings Editor: struct { @@ -245,7 +248,7 @@ func newRepository() { Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool() Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool() Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) - Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString("master") + Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch) RepoRootPath = sec.Key("ROOT").MustString(path.Join(homeDir, "gitea-repositories")) forcePathSeparator(RepoRootPath) if !filepath.IsAbs(RepoRootPath) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ea76d4cb6a..37d8d7272a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -356,6 +356,10 @@ lang_select_error = Select a language from the list. username_been_taken = The username is already taken. repo_name_been_taken = The repository name is already used. +repository_files_already_exist = Files already exist for this repository. Contact the system administrator. +repository_files_already_exist.adopt = Files already exist for this repository and can only be Adopted. +repository_files_already_exist.delete = Files already exist for this repository. You must delete them. +repository_files_already_exist.adopt_or_delete = Files already exist for this repository. Either adopt them or delete them. visit_rate_limit = Remote visit addressed rate limitation. 2fa_auth_required = Remote visit required two factors authentication. org_name_been_taken = The organization name is already taken. @@ -682,6 +686,15 @@ pick_reaction = Pick your reaction reactions_more = and %d more unit_disabled = The site administrator has disabled this repository section. language_other = Other +adopt_search = Enter username to search for unadopted repositories... (leave blank to find all) +adopt_preexisting_label = Adopt Files +adopt_preexisting = Adopt pre-existing files +adopt_preexisting_content = Create repository from %s +adopt_preexisting_success = Adopted files and created repository from %s +delete_preexisting_label = Delete +delete_preexisting = Delete pre-existing files +delete_preexisting_content = Delete files in %s +delete_preexisting_success = Deleted unadopted files in %s desc.private = Private desc.public = Public @@ -2069,6 +2082,8 @@ orgs.members = Members orgs.new_orga = New Organization repos.repo_manage_panel = Repository Management +repos.unadopted = Unadopted Repositories +repos.unadopted.no_more = No more unadopted repositories found repos.owner = Owner repos.name = Name repos.private = Private diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 39a1d7596c..10abaf9547 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -5,17 +5,22 @@ package admin import ( + "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" repo_service "code.gitea.io/gitea/services/repository" + "github.com/unknwon/com" ) const ( - tplRepos base.TplName = "admin/repo/list" + tplRepos base.TplName = "admin/repo/list" + tplUnadoptedRepos base.TplName = "admin/repo/unadopted" ) // Repos show all the repositories @@ -50,3 +55,91 @@ func DeleteRepo(ctx *context.Context) { "redirect": setting.AppSubURL + "/admin/repos?page=" + ctx.Query("page") + "&sort=" + ctx.Query("sort"), }) } + +// UnadoptedRepos lists the unadopted repositories +func UnadoptedRepos(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.repositories") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminRepositories"] = true + + opts := models.ListOptions{ + PageSize: setting.UI.Admin.UserPagingNum, + Page: ctx.QueryInt("page"), + } + + if opts.Page <= 0 { + opts.Page = 1 + } + + doSearch := ctx.QueryBool("search") + + ctx.Data["search"] = doSearch + q := ctx.Query("q") + + if !doSearch { + pager := context.NewPagination(0, opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + ctx.HTML(200, tplUnadoptedRepos) + return + } + + ctx.Data["Keyword"] = q + repoNames, count, err := repository.ListUnadoptedRepositories(q, &opts) + if err != nil { + ctx.ServerError("ListUnadoptedRepositories", err) + } + ctx.Data["Dirs"] = repoNames + pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + ctx.HTML(200, tplUnadoptedRepos) +} + +// AdoptOrDeleteRepository adopts or deletes a repository +func AdoptOrDeleteRepository(ctx *context.Context) { + dir := ctx.Query("id") + action := ctx.Query("action") + dirSplit := strings.SplitN(dir, "/", 2) + if len(dirSplit) != 2 { + ctx.Redirect(setting.AppSubURL + "/admin/repos") + return + } + + ctxUser, err := models.GetUserByName(dirSplit[0]) + if err != nil { + if models.IsErrUserNotExist(err) { + log.Debug("User does not exist: %s", dirSplit[0]) + ctx.Redirect(setting.AppSubURL + "/admin/repos") + return + } + ctx.ServerError("GetUserByName", err) + return + } + + repoName := dirSplit[1] + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, repoName); err != nil { + ctx.ServerError("IsRepositoryExist", err) + return + } else if has || !com.IsDir(models.RepoPath(ctxUser.Name, repoName)) { + // Fallthrough to failure mode + } else if action == "adopt" { + if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{ + Name: dirSplit[1], + IsPrivate: true, + }); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) + } else if action == "delete" { + if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, dirSplit[1]); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir)) + } + ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted") +} diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go new file mode 100644 index 0000000000..1a7a62a55c --- /dev/null +++ b/routers/api/v1/admin/adopt.go @@ -0,0 +1,164 @@ +// 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 admin + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/routers/api/v1/utils" + "github.com/unknwon/com" +) + +// ListUnadoptedRepositories lists the unadopted repositories that match the provided names +func ListUnadoptedRepositories(ctx *context.APIContext) { + // swagger:operation GET /admin/unadopted admin adminUnadoptedList + // --- + // summary: List unadopted repositories + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // - name: pattern + // in: query + // description: pattern of repositories to search for + // type: string + // responses: + // "200": + // "$ref": "#/responses/StringSlice" + // "403": + // "$ref": "#/responses/forbidden" + + listOptions := utils.GetListOptions(ctx) + repoNames, count, err := repository.ListUnadoptedRepositories(ctx.Query("query"), &listOptions) + if err != nil { + ctx.InternalServerError(err) + } + + ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count)) + ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count") + + ctx.JSON(http.StatusOK, repoNames) +} + +// AdoptRepository will adopt an unadopted repository +func AdoptRepository(ctx *context.APIContext) { + // swagger:operation POST /admin/unadopted/{owner}/{repo} admin adminAdoptRepository + // --- + // summary: Adopt unadopted files as a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" + ownerName := ctx.Params(":username") + repoName := ctx.Params(":reponame") + + ctxUser, err := models.GetUserByName(ownerName) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.NotFound() + return + } + ctx.InternalServerError(err) + return + } + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, repoName); err != nil { + ctx.InternalServerError(err) + return + } else if has || !com.IsDir(models.RepoPath(ctxUser.Name, repoName)) { + ctx.NotFound() + return + } + if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{ + Name: repoName, + IsPrivate: true, + }); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteUnadoptedRepository will delete an unadopted repository +func DeleteUnadoptedRepository(ctx *context.APIContext) { + // swagger:operation DELETE /admin/unadopted/{owner}/{repo} admin adminDeleteUnadoptedRepository + // --- + // summary: Delete unadopted files + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + ownerName := ctx.Params(":username") + repoName := ctx.Params(":reponame") + + ctxUser, err := models.GetUserByName(ownerName) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.NotFound() + return + } + ctx.InternalServerError(err) + return + } + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, repoName); err != nil { + ctx.InternalServerError(err) + return + } else if has || !com.IsDir(models.RepoPath(ctxUser.Name, repoName)) { + ctx.NotFound() + return + } + + if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, repoName); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8b3a7545c6..3b6f8dbba3 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -957,6 +957,11 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo) }) }) + m.Group("/unadopted", func() { + m.Get("", admin.ListUnadoptedRepositories) + m.Post("/:username/:reponame", admin.AdoptRepository) + m.Delete("/:username/:reponame", admin.DeleteUnadoptedRepository) + }) }, reqToken(), reqSiteAdmin()) m.Group("/topics", func() { diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 019d82031c..f9cddbb7cd 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -198,6 +198,8 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteA switch { case models.IsErrRepoAlreadyExist(err): ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") + case models.IsErrRepoFilesAlreadyExist(err): + ctx.Error(http.StatusConflict, "", "Files already exist for this repository. Adopt them or delete them.") case migrations.IsRateLimitError(err): ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit addressed rate limitation.") case migrations.IsTwoFactorAuthError(err): diff --git a/routers/repo/migrate.go b/routers/repo/migrate.go index 19dbfbab40..9b10970bf0 100644 --- a/routers/repo/migrate.go +++ b/routers/repo/migrate.go @@ -67,6 +67,18 @@ func handleMigrateError(ctx *context.Context, owner *models.User, err error, nam case models.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrRepoFilesAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + switch { + case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) + case setting.Repository.AllowAdoptionOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) + case setting.Repository.AllowDeleteOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) + default: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) + } case models.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) @@ -159,7 +171,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { opts.Releases = false } - err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, false) if err != nil { handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, &form) return diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 4a088ff9cd..12434747eb 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -160,6 +160,18 @@ func handleCreateError(ctx *context.Context, owner *models.User, err error, name case models.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrRepoFilesAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + switch { + case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) + case setting.Repository.AllowAdoptionOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) + case setting.Repository.AllowDeleteOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) + default: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) + } case models.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) diff --git a/routers/repo/setting.go b/routers/repo/setting.go index d2c20fb03a..865dea9bca 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -88,6 +88,18 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) case models.IsErrNameReserved(err): ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &form) + case models.IsErrRepoFilesAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + switch { + case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) + case setting.Repository.AllowAdoptionOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) + case setting.Repository.AllowDeleteOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) + default: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) + } case models.IsErrNamePatternNotAllowed(err): ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) default: diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 5345a10171..f60af5dad0 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -402,6 +402,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/keys/delete", userSetting.DeleteKey) m.Get("/organization", userSetting.Organization) m.Get("/repos", userSetting.Repos) + m.Post("/repos/unadopted", userSetting.AdoptOrDeleteRepository) }, reqSignIn, func(ctx *context.Context) { ctx.Data["PageIsUserSettings"] = true ctx.Data["AllThemes"] = setting.UI.Themes @@ -461,6 +462,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/repos", func() { m.Get("", admin.Repos) + m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository) m.Post("/delete", admin.DeleteRepo) }) diff --git a/routers/user/setting/adopt.go b/routers/user/setting/adopt.go new file mode 100644 index 0000000000..6ff07d6daa --- /dev/null +++ b/routers/user/setting/adopt.go @@ -0,0 +1,56 @@ +// 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 setting + +import ( + "path/filepath" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "github.com/unknwon/com" +) + +// AdoptOrDeleteRepository adopts or deletes a repository +func AdoptOrDeleteRepository(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsRepos"] = true + allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories + ctx.Data["allowAdopt"] = allowAdopt + allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories + ctx.Data["allowDelete"] = allowDelete + + dir := ctx.Query("id") + action := ctx.Query("action") + + ctxUser := ctx.User + root := filepath.Join(models.UserPath(ctxUser.LowerName)) + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, dir); err != nil { + ctx.ServerError("IsRepositoryExist", err) + return + } else if has || !com.IsDir(filepath.Join(root, dir+".git")) { + // Fallthrough to failure mode + } else if action == "adopt" && allowAdopt { + if _, err := repository.AdoptRepository(ctxUser, ctxUser, models.CreateRepoOptions{ + Name: dir, + IsPrivate: true, + }); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) + } else if action == "delete" && allowDelete { + if err := repository.DeleteUnadoptedRepository(ctxUser, ctxUser, dir); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir)) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings/repos") +} diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go index ba9ba2b257..fe0506946a 100644 --- a/routers/user/setting/profile.go +++ b/routers/user/setting/profile.go @@ -9,6 +9,8 @@ import ( "errors" "fmt" "io/ioutil" + "os" + "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -197,32 +199,96 @@ func Organization(ctx *context.Context) { func Repos(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsRepos"] = true - ctxUser := ctx.User + ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories + ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories - var err error - if err = ctxUser.GetRepositories(models.ListOptions{Page: 1, PageSize: setting.UI.User.RepoPagingNum}); err != nil { - ctx.ServerError("GetRepositories", err) - return + opts := models.ListOptions{ + PageSize: setting.UI.Admin.UserPagingNum, + Page: ctx.QueryInt("page"), } - repos := ctxUser.Repos - for i := range repos { - if repos[i].IsFork { - err := repos[i].GetBaseRepo() + if opts.Page <= 0 { + opts.Page = 1 + } + start := (opts.Page - 1) * opts.PageSize + end := start + opts.PageSize + + adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories) + + ctxUser := ctx.User + count := 0 + + if adoptOrDelete { + repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum) + repos := map[string]*models.Repository{} + // We're going to iterate by pagesize. + root := filepath.Join(models.UserPath(ctxUser.Name)) + if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { - ctx.ServerError("GetBaseRepo", err) - return + return err } - err = repos[i].BaseRepo.GetOwner() - if err != nil { - ctx.ServerError("GetOwner", err) - return + if !info.IsDir() || path == root { + 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 { + return filepath.SkipDir + } + if count >= start && count < end { + repoNames = append(repoNames, name) + } + count++ + return filepath.SkipDir + }); err != nil { + ctx.ServerError("filepath.Walk", err) + return + } + + if err := ctxUser.GetRepositories(models.ListOptions{Page: 1, PageSize: setting.UI.Admin.UserPagingNum}, repoNames...); err != nil { + ctx.ServerError("GetRepositories", err) + return + } + for _, repo := range ctxUser.Repos { + if repo.IsFork { + if err := repo.GetBaseRepo(); err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } + } + repos[repo.LowerName] = repo + } + ctx.Data["Dirs"] = repoNames + ctx.Data["ReposMap"] = repos + } else { + var err error + var count64 int64 + ctxUser.Repos, count64, err = models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) + + if err != nil { + ctx.ServerError("GetRepositories", err) + return + } + count = int(count64) + repos := ctxUser.Repos + + for i := range repos { + if repos[i].IsFork { + if err := repos[i].GetBaseRepo(); err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } } } + + ctx.Data["Repos"] = repos } - ctx.Data["Owner"] = ctxUser - ctx.Data["Repos"] = repos - + pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager ctx.HTML(200, tplSettingsRepositories) } diff --git a/services/repository/generate.go b/services/repository/generate.go index 95e5cdc6c2..067f8f61d0 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -64,7 +64,7 @@ func GenerateRepository(doer, owner *models.User, templateRepo *models.Repositor return nil }); err != nil { - if generateRepo != nil { + if generateRepo != nil && generateRepo.ID > 0 { if errDelete := models.DeleteRepository(doer, owner.ID, generateRepo.ID); errDelete != nil { log.Error("Rollback deleteRepository: %v", errDelete) } diff --git a/services/repository/repository.go b/services/repository/repository.go index 77c8728d94..c6768f3f00 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -18,11 +18,7 @@ import ( func CreateRepository(doer, owner *models.User, opts models.CreateRepoOptions) (*models.Repository, error) { repo, err := repo_module.CreateRepository(doer, owner, opts) if err != nil { - if repo != nil { - if errDelete := models.DeleteRepository(doer, owner.ID, repo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } - } + // No need to rollback here we should do this in CreateRepository... return nil, err } @@ -31,15 +27,28 @@ 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, oldRepo *models.Repository, name, desc string) (*models.Repository, error) { repo, err := repo_module.ForkRepository(doer, u, oldRepo, name, desc) if err != nil { - if repo != nil { - if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } - } return nil, err } diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 4c3b77dcfb..51e329e038 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -5,6 +5,9 @@ {{template "base/alert" .}}

{{.i18n.Tr "admin.repos.repo_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}}) +
+ {{.i18n.Tr "admin.repos.unadopted"}} +

{{template "admin/repo/search" .}} diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl new file mode 100644 index 0000000000..7a046c6026 --- /dev/null +++ b/templates/admin/repo/unadopted.tmpl @@ -0,0 +1,98 @@ +{{template "base/head" .}} +
+ {{template "admin/navbar" .}} +
+ {{template "base/alert" .}} +

+ {{.i18n.Tr "admin.repos.unadopted"}} + +

+
+
+
+ + + +
+
+
+ {{if .search}} +
+ {{if .Dirs}} +
+ {{range $dirI, $dir := .Dirs}} +
+
+ {{svg "octicon-file-directory"}} + {{$dir}} +
+ + + + +
+
+
+ {{end}} +
+ {{template "base/paginate" .}} + {{else}} +
+ {{.i18n.Tr "admin.repos.unadopted.no_more"}} +
+ {{template "base/paginate" .}} + {{end}} +
+ {{end}} +
+
+ +{{template "base/footer" .}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c1847f0440..e383448933 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -120,6 +120,119 @@ } } }, + "/admin/unadopted": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List unadopted repositories", + "operationId": "adminUnadoptedList", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "pattern of repositories to search for", + "name": "pattern", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/StringSlice" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, + "/admin/unadopted/{owner}/{repo}": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Adopt unadopted files as a repository", + "operationId": "adminAdoptRepository", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete unadopted files", + "operationId": "adminDeleteUnadoptedRepository", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/admin/users": { "get": { "produces": [ diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl index 8d9065ec61..456647d9be 100644 --- a/templates/user/settings/repos.tmpl +++ b/templates/user/settings/repos.tmpl @@ -7,34 +7,134 @@ {{.i18n.Tr "settings.repos"}}
- {{if .Repos}} -
- {{range .Repos}} -
-
- {{if .IsPrivate}} - {{svg "octicon-lock"}} - {{else if .IsFork}} - {{svg "octicon-repo-forked"}} - {{else if .IsMirror}} - {{svg "octicon-mirror"}} - {{else}} - {{svg "octicon-repo"}} - {{end}} - {{$.Owner.Name}}/{{.Name}} - {{SizeFmt .Size}} - {{if .IsFork}} - {{$.i18n.Tr "repo.forked_from"}} - {{.BaseRepo.Owner.Name}}/{{.BaseRepo.Name}} - {{end}} + {{if or .allowAdopt .allowDelete}} + {{if .Dirs}} +
+ {{range $dirI, $dir := .Dirs}} + {{ $repo := index $.ReposMap $dir}} +
+
+ {{if $repo}} + {{if $repo.IsPrivate}} + {{svg "octicon-lock"}} + {{else if $repo.IsFork}} + {{svg "octicon-repo-forked"}} + {{else if $repo.IsMirror}} + {{svg "octicon-mirror"}} + {{else if $repo.IsTemplate}} + {{svg "octicon-repo-template"}} + {{else}} + {{svg "octicon-repo"}} + {{end}} + {{$repo.OwnerName}}/{{$repo.Name}} + {{SizeFmt $repo.Size}} + {{if $repo.IsFork}} + {{$.i18n.Tr "repo.forked_from"}} + {{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}} + {{end}} + {{else}} + {{svg "octicon-file-directory"}} + {{$.Owner.Name}}/{{$dir}} +
+ {{if $.allowAdopt}} + + + {{end}} + {{if $.allowDelete}} + + + {{end}} +
+ {{end}} +
-
- {{end}} -
+ {{end}} +
+ {{template "base/paginate" .}} + {{else}} +
+ {{.i18n.Tr "settings.repos_none"}} +
+ {{end}} {{else}} -
- {{.i18n.Tr "settings.repos_none"}} -
+ {{if .Repos}} +
+ {{range .Repos}} +
+
+ {{if .IsPrivate}} + {{svg "octicon-lock"}} + {{else if .IsFork}} + {{svg "octicon-repo-forked"}} + {{else if .IsMirror}} + {{svg "octicon-mirror"}} + {{else if .IsTemplate}} + {{svg "octicon-repo-template"}} + {{else}} + {{svg "octicon-repo"}} + {{end}} + {{$.OwnerName}}/{{.Name}} + {{SizeFmt .Size}} + {{if .IsFork}} + {{$.i18n.Tr "repo.forked_from"}} + {{.BaseRepo.OwnerName}}/{{.BaseRepo.Name}} + {{end}} +
+
+ {{end}} +
+ {{template "base/paginate" .}} + {{else}} +
+ {{.i18n.Tr "settings.repos_none"}} +
+ {{end}} {{end}}
diff --git a/web_src/less/_admin.less b/web_src/less/_admin.less index 29afe96b06..052c29dd62 100644 --- a/web_src/less/_admin.less +++ b/web_src/less/_admin.less @@ -30,6 +30,16 @@ form tbody button[type='submit'] { padding: 5px 8px; } + + } + + .settings .button.adopt, + .settings .button.delete { + margin-top: -15px; + margin-bottom: -15px; + .label { + vertical-align: middle; + } } .ui.header, diff --git a/web_src/less/_user.less b/web_src/less/_user.less index 0b983a382c..fcc5c0290f 100644 --- a/web_src/less/_user.less +++ b/web_src/less/_user.less @@ -135,6 +135,15 @@ } } + .button.adopt, + .button.delete { + margin-top: -15px; + margin-bottom: -15px; + .label { + vertical-align: middle; + } + } + &.link-account:not(.icon) { padding-top: 15px; padding-bottom: 5px;