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>tokarchuk/v1.17
parent
6fa19a8458
commit
7a7f56044a
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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") |
||||
} |
@ -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" .}} |
Loading…
Reference in new issue