summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--custom/conf/app.example.ini4
-rw-r--r--models/error.go16
-rw-r--r--models/repo.go27
-rw-r--r--models/repo_list.go2
-rw-r--r--models/user.go4
-rw-r--r--modules/git/repo_branch.go5
-rw-r--r--modules/repository/adopt.go272
-rw-r--r--modules/repository/create.go73
-rw-r--r--modules/repository/fork.go24
-rw-r--r--modules/repository/generate.go14
-rw-r--r--modules/repository/init.go89
-rw-r--r--modules/setting/repository.go5
-rw-r--r--options/locale/locale_en-US.ini15
-rw-r--r--routers/admin/repos.go95
-rw-r--r--routers/api/v1/admin/adopt.go164
-rw-r--r--routers/api/v1/api.go5
-rw-r--r--routers/api/v1/repo/migrate.go2
-rw-r--r--routers/repo/migrate.go14
-rw-r--r--routers/repo/repo.go12
-rw-r--r--routers/repo/setting.go12
-rw-r--r--routers/routes/routes.go2
-rw-r--r--routers/user/setting/adopt.go56
-rw-r--r--routers/user/setting/profile.go102
-rw-r--r--services/repository/generate.go2
-rw-r--r--services/repository/repository.go29
-rw-r--r--templates/admin/repo/list.tmpl3
-rw-r--r--templates/admin/repo/unadopted.tmpl98
-rw-r--r--templates/swagger/v1_json.tmpl113
-rw-r--r--templates/user/settings/repos.tmpl152
-rw-r--r--web_src/less/_admin.less10
-rw-r--r--web_src/less/_user.less9
31 files changed, 1335 insertions, 95 deletions
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
+ }
+
+ 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,
+ }
+ }
+
+ 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 {
- return fmt.Errorf("InitializeLabels: %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)
- return fmt.Errorf("CreateRepository(git update-server-info): %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, 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"),
+ }
+
+ if opts.Page <= 0 {
+ opts.Page = 1
}
- repos := ctxUser.Repos
+ start := (opts.Page - 1) * opts.PageSize
+ end := start + opts.PageSize
- for i := range repos {
- if repos[i].IsFork {
- err := repos[i].GetBaseRepo()
+ 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
}
- }
- ctx.Data["Owner"] = ctxUser
- ctx.Data["Repos"] = repos
+ 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
+ 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" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "admin.repos.repo_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}})
+ <div class="ui right">
+ <a class="ui blue tiny button" href="{{AppSubUrl}}/admin/repos/unadopted">{{.i18n.Tr "admin.repos.unadopted"}}</a>
+ </div>
</h4>
<div class="ui attached segment">
{{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" .}}
+<div class="admin user">
+ {{template "admin/navbar" .}}
+ <div class="ui container">
+ {{template "base/alert" .}}
+ <h4 class="ui top attached header">
+ {{.i18n.Tr "admin.repos.unadopted"}}
+ <div class="ui right">
+ <a class="ui blue tiny button" href="{{AppSubUrl}}/admin/repos">{{.i18n.Tr "admin.repos.repo_manage_panel"}}</a>
+ </div>
+ </h4>
+ <div class="ui attached segment">
+ <form class="ui form ignore-dirty">
+ <div class="ui fluid action input">
+ <input name="search" value="true" type="hidden">
+ <input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "repo.adopt_search"}}" autofocus>
+ <button class="ui blue button">{{.i18n.Tr "explore.search"}}</button>
+ </div>
+ </form>
+ </div>
+ {{if .search}}
+ <div class="ui attached segment settings">
+ {{if .Dirs}}
+ <div class="ui middle aligned divided list">
+ {{range $dirI, $dir := .Dirs}}
+ <div class="item">
+ <div class="content">
+ <span class="icon">{{svg "octicon-file-directory"}}</span>
+ <span class="name">{{$dir}}</span>
+ <div class="right floated content">
+ <button class="ui button submit tiny green adopt show-modal" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{$.i18n.Tr "repo.adopt_preexisting_label"}}</span></button>
+ <div class="ui basic modal" id="adopt-unadopted-modal-{{$dirI}}">
+ <i class="close icon"></i>
+ <div class="header">
+ <span class="label">{{$.i18n.Tr "repo.adopt_preexisting"}}</span>
+ </div>
+ <div class="content">
+ <p>{{$.i18n.Tr "repo.adopt_preexisting_content" $dir}}</p>
+ </div>
+ <form class="ui form" method="POST" action="{{AppSubUrl}}/admin/repos/unadopted">
+ {{$.CsrfTokenHtml}}
+ <input type="hidden" name="id" value="{{$dir}}">
+ <input type="hidden" name="action" value="adopt">
+ <div class="actions">
+ <div class="ui red basic inverted cancel button">
+ <i class="remove icon"></i>
+ {{$.i18n.Tr "modal.no"}}
+ </div>
+ <button class="ui green basic inverted ok button">
+ <i class="checkmark icon"></i>
+ {{$.i18n.Tr "modal.yes"}}
+ </button>
+ </div>
+ </form>
+ </div>
+ <button class="ui button submit tiny red delete show-modal" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{$.i18n.Tr "repo.delete_preexisting_label"}}</span></button>
+ <div class="ui basic modal" id="delete-unadopted-modal-{{$dirI}}">
+ <i class="close icon"></i>
+ <div class="header">
+ <span class="label">{{$.i18n.Tr "repo.delete_preexisting"}}</span>
+ </div>
+ <div class="content">
+ <p>{{$.i18n.Tr "repo.delete_preexisting_content" $dir}}</p>
+ </div>
+ <form class="ui form" method="POST" action="{{AppSubUrl}}/admin/repos/unadopted">
+ {{$.CsrfTokenHtml}}
+ <input type="hidden" name="id" value="{{$dir}}">
+ <input type="hidden" name="action" value="delete">
+ <div class="actions">
+ <div class="ui red basic inverted cancel button">
+ <i class="remove icon"></i>
+ {{$.i18n.Tr "modal.no"}}
+ </div>
+ <button class="ui green basic inverted ok button">
+ <i class="checkmark icon"></i>
+ {{$.i18n.Tr "modal.yes"}}
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ {{end}}
+ </div>
+ {{template "base/paginate" .}}
+ {{else}}
+ <div class="item">
+ {{.i18n.Tr "admin.repos.unadopted.no_more"}}
+ </div>
+ {{template "base/paginate" .}}
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+</div>
+
+{{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"}}
</h4>
<div class="ui attached segment">
- {{if .Repos}}
- <div class="ui middle aligned divided list">
- {{range .Repos}}
- <div class="item">
- <div class="content">
- {{if .IsPrivate}}
- <span class="text gold iconFloat">{{svg "octicon-lock"}}</span>
- {{else if .IsFork}}
- <span class="iconFloat">{{svg "octicon-repo-forked"}}</span>
- {{else if .IsMirror}}
- <span class="iconFloat">{{svg "octicon-mirror"}}</span>
- {{else}}
- <span class="iconFloat">{{svg "octicon-repo"}}</span>
- {{end}}
- <a class="name" href="{{AppSubUrl}}/{{$.Owner.Name}}/{{.Name}}">{{$.Owner.Name}}/{{.Name}}</a>
- <span>{{SizeFmt .Size}}</span>
- {{if .IsFork}}
- {{$.i18n.Tr "repo.forked_from"}}
- <span><a href="{{AppSubUrl}}/{{.BaseRepo.Owner.Name}}/{{.BaseRepo.Name}}">{{.BaseRepo.Owner.Name}}/{{.BaseRepo.Name}}</a></span>
- {{end}}
+ {{if or .allowAdopt .allowDelete}}
+ {{if .Dirs}}
+ <div class="ui middle aligned divided list">
+ {{range $dirI, $dir := .Dirs}}
+ {{ $repo := index $.ReposMap $dir}}
+ <div class="item">
+ <div class="content">
+ {{if $repo}}
+ {{if $repo.IsPrivate}}
+ <span class="text gold icon">{{svg "octicon-lock"}}</span>
+ {{else if $repo.IsFork}}
+ <span class="icon">{{svg "octicon-repo-forked"}}</span>
+ {{else if $repo.IsMirror}}
+ <span class="icon">{{svg "octicon-mirror"}}</span>
+ {{else if $repo.IsTemplate}}
+ <span class="icon">{{svg "octicon-repo-template"}}</span>
+ {{else}}
+ <span class="icon">{{svg "octicon-repo"}}</span>
+ {{end}}
+ <a class="name" href="{{AppSubUrl}}/{{$repo.OwnerName}}/{{$repo.Name}}">{{$repo.OwnerName}}/{{$repo.Name}}</a>
+ <span>{{SizeFmt $repo.Size}}</span>
+ {{if $repo.IsFork}}
+ {{$.i18n.Tr "repo.forked_from"}}
+ <span><a href="{{AppSubUrl}}/{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span>
+ {{end}}
+ {{else}}
+ <span class="icon">{{svg "octicon-file-directory"}}</span>
+ <span class="name">{{$.Owner.Name}}/{{$dir}}</span>
+ <div class="right floated content">
+ {{if $.allowAdopt}}
+ <button class="ui button submit tiny green adopt show-modal" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{$.i18n.Tr "repo.adopt_preexisting_label"}}</span></button>
+ <div class="ui basic modal" id="adopt-unadopted-modal-{{$dirI}}">
+ <i class="close icon"></i>
+ <div class="header">
+ <span class="label">{{$.i18n.Tr "repo.adopt_preexisting"}}</span>
+ </div>
+ <div class="content">
+ <p>{{$.i18n.Tr "repo.adopt_preexisting_content" $dir}}</p>
+ </div>
+ <form class="ui form" method="POST" action="{{AppSubUrl}}/user/settings/repos/unadopted">
+ {{$.CsrfTokenHtml}}
+ <input type="hidden" name="id" value="{{$dir}}">
+ <input type="hidden" name="action" value="adopt">
+ <div class="actions">
+ <div class="ui red basic inverted cancel button">
+ <i class="remove icon"></i>
+ {{$.i18n.Tr "modal.no"}}
+ </div>
+ <button class="ui green basic inverted ok button">
+ <i class="checkmark icon"></i>
+ {{$.i18n.Tr "modal.yes"}}
+ </button>
+ </div>
+ </form>
+ </div>
+ {{end}}
+ {{if $.allowDelete}}
+ <button class="ui button submit tiny red delete show-modal" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{$.i18n.Tr "repo.delete_preexisting_label"}}</span></button>
+ <div class="ui basic modal" id="delete-unadopted-modal-{{$dirI}}">
+ <i class="close icon"></i>
+ <div class="header">
+ <span class="label">{{$.i18n.Tr "repo.delete_preexisting"}}</span>
+ </div>
+ <div class="content">
+ <p>{{$.i18n.Tr "repo.delete_preexisting_content" $dir}}</p>
+ </div>
+ <form class="ui form" method="POST" action="{{AppSubUrl}}/user/settings/repos/unadopted">
+ {{$.CsrfTokenHtml}}
+ <input type="hidden" name="id" value="{{$dir}}">
+ <input type="hidden" name="action" value="delete">
+ <div class="actions">
+ <div class="ui red basic inverted cancel button">
+ <i class="remove icon"></i>
+ {{$.i18n.Tr "modal.no"}}
+ </div>
+ <button class="ui green basic inverted ok button">
+ <i class="checkmark icon"></i>
+ {{$.i18n.Tr "modal.yes"}}
+ </button>
+ </div>
+ </form>
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ </div>
</div>
- </div>
- {{end}}
- </div>
+ {{end}}
+ </div>
+ {{template "base/paginate" .}}
+ {{else}}
+ <div class="item">
+ {{.i18n.Tr "settings.repos_none"}}
+ </div>
+ {{end}}
{{else}}
- <div class="item">
- {{.i18n.Tr "settings.repos_none"}}
- </div>
+ {{if .Repos}}
+ <div class="ui middle aligned divided list">
+ {{range .Repos}}
+ <div class="item">
+ <div class="content">
+ {{if .IsPrivate}}
+ <span class="text gold iconFloat">{{svg "octicon-lock"}}</span>
+ {{else if .IsFork}}
+ <span class="iconFloat">{{svg "octicon-repo-forked"}}</span>
+ {{else if .IsMirror}}
+ <span class="iconFloat">{{svg "octicon-mirror"}}</span>
+ {{else if .IsTemplate}}
+ <span class="iconFloat">{{svg "octicon-repo-template"}}</span>
+ {{else}}
+ <span class="iconFloat">{{svg "octicon-repo"}}</span>
+ {{end}}
+ <a class="name" href="{{AppSubUrl}}/{{$.OwnerName}}/{{.Name}}">{{$.OwnerName}}/{{.Name}}</a>
+ <span>{{SizeFmt .Size}}</span>
+ {{if .IsFork}}
+ {{$.i18n.Tr "repo.forked_from"}}
+ <span><a href="{{AppSubUrl}}/{{.BaseRepo.OwnerName}}/{{.BaseRepo.Name}}">{{.BaseRepo.OwnerName}}/{{.BaseRepo.Name}}</a></span>
+ {{end}}
+ </div>
+ </div>
+ {{end}}
+ </div>
+ {{template "base/paginate" .}}
+ {{else}}
+ <div class="item">
+ {{.i18n.Tr "settings.repos_none"}}
+ </div>
+ {{end}}
{{end}}
</div>
</div>
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;