mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-08-28 21:36:05 +02:00
Adopt repositories (#12920)
* Don't automatically delete repository files if they are present Prior to this PR Gitea would delete any repository files if they are present during creation or migration. This can in certain circumstances lead to data-loss and is slightly unpleasant. This PR provides a mechanism for Gitea to adopt repositories on creation and otherwise requires an explicit flag for deletion. PushCreate is slightly different - the create will cause adoption if that is allowed otherwise it will delete the data if that is allowed. Signed-off-by: Andrew Thornton <art27@cantab.net> * Update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix tests and migrate overwrite Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @lunny Only offer to adopt or overwrite if the user can do that. Allow the site administrator to adopt or overwrite in all circumstances Signed-off-by: Andrew Thornton <art27@cantab.net> * Use setting.Repository.DefaultBranch for the default branch Signed-off-by: Andrew Thornton <art27@cantab.net> * Always set setting.Repository.DefaultBranch Signed-off-by: Andrew Thornton <art27@cantab.net> * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * update templates Signed-off-by: Andrew Thornton <art27@cantab.net> * ensure repo closed Signed-off-by: Andrew Thornton <art27@cantab.net> * Rewrite of adoption as per @6543 and @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * Apply suggestions from code review * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * missing not Signed-off-by: Andrew Thornton <art27@cantab.net> * add modals and flash reporting Signed-off-by: Andrew Thornton <art27@cantab.net> * Make the unadopted page searchable Signed-off-by: Andrew Thornton <art27@cantab.net> * Add API Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Handle empty and non-master branched repositories Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * remove commented out code Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
6fa19a8458
commit
7a7f56044a
31 changed files with 1340 additions and 100 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
272
modules/repository/adopt.go
Normal file
272
modules/repository/adopt.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
164
routers/api/v1/admin/adopt.go
Normal file
164
routers/api/v1/admin/adopt.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
56
routers/user/setting/adopt.go
Normal file
56
routers/user/setting/adopt.go
Normal file
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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" .}}
|
||||
|
|
98
templates/admin/repo/unadopted.tmpl
Normal file
98
templates/admin/repo/unadopted.tmpl
Normal file
|
@ -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" .}}
|
|
@ -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": [
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue