mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
Compare commits
3 Commits
8cb6fb2001
...
d715a5ae75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d715a5ae75 | ||
|
|
1554b6c939 | ||
|
|
3699309edd |
@@ -1145,11 +1145,6 @@ LEVEL = Info
|
||||
;; The default Git object format of new repositories. Available values: sha1, sha256.
|
||||
;DEFAULT_OBJECT_FORMAT = sha1
|
||||
;;
|
||||
;; Allow adoption of unadopted repositories
|
||||
;ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES = false
|
||||
;;
|
||||
;; Allow deletion of unadopted repositories
|
||||
;ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES = false
|
||||
|
||||
;; Don't allow download source archive files from UI
|
||||
;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false
|
||||
@@ -2058,7 +2053,7 @@ LEVEL = Info
|
||||
;ENABLED = true
|
||||
;;
|
||||
;; Comma-separated list of allowed issue/pull-request attachment file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
|
||||
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
|
||||
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.jxl,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
|
||||
;;
|
||||
;; Max size of each issue/pull-request attachment file. Defaults to 100MB
|
||||
;MAX_SIZE = 100
|
||||
|
||||
@@ -217,11 +217,6 @@ func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch,
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
func DeleteRepoBranches(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Delete(new(Branch))
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
branches := make([]*Branch, 0, len(branchIDs))
|
||||
|
||||
@@ -578,8 +578,3 @@ func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DeleteRepoReleases(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(Release))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -204,8 +204,6 @@ type SearchRepoOptions struct {
|
||||
// True -> include just has milestones
|
||||
// False -> include just has no milestone
|
||||
HasMilestones optional.Option[bool]
|
||||
// LowerNames represents valid lower names to restrict to
|
||||
LowerNames []string
|
||||
// When specified true, apply some filters over the conditions:
|
||||
// - Don't show forks, when opts.Fork is OptionalBoolNone.
|
||||
// - Do not display repositories that don't have a description, an icon and topics.
|
||||
@@ -824,10 +822,6 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor
|
||||
cond = cond.And(builder.Eq{"is_private": false})
|
||||
}
|
||||
|
||||
if len(opts.LowerNames) > 0 {
|
||||
cond = cond.And(builder.In("lower_name", opts.LowerNames))
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
count, err := sess.Where(cond).Count(new(Repository))
|
||||
|
||||
@@ -18,7 +18,7 @@ func RepoWikiGitRepoRelativePath(ownerName, repoName string) string {
|
||||
}
|
||||
|
||||
// CodeRepoByName returns an unmanaged repository facade for the code repository of the given owner and repository name.
|
||||
// Usually it is used for migration fixes or repository adoption/creation/rename/transfer.
|
||||
// Usually it is used for migration fixes or repository creation/rename/transfer.
|
||||
func CodeRepoByName(ownerName, repoName string) RepositoryFacade {
|
||||
return RepositoryUnmanaged(RepoCodeGitRepoRelativePath(ownerName, repoName))
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (r *repositoryUnmanaged) GitRepoLocation() string {
|
||||
// RepositoryUnmanaged returns a RepositoryFacade for a repository that might not be managed by Gitea.
|
||||
// If the path is not absolute, then it is relative to setting.RepoRootPath
|
||||
// This function is mainly for maintaining the owner's repo when the repo is not managed yet.
|
||||
// e.g.: init, clone, transfer, rename, adopt, etc., and temp repo creation and modification.
|
||||
// e.g.: init, clone, transfer, rename, etc., and temp repo creation and modification.
|
||||
func RepositoryUnmanaged(s string) RepositoryFacade {
|
||||
return &repositoryUnmanaged{loc: filepath.Clean(s)}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func serveSetHeaderContentRelated(w http.ResponseWriter, contentType string) {
|
||||
func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) {
|
||||
header := w.Header()
|
||||
|
||||
skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp")
|
||||
skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".avif", ".png", ".jpeg", ".jpg", ".jxl", ".webp")
|
||||
if skipCompressionExts.Contains(strings.ToLower(path.Ext(opts.Filename))) {
|
||||
w.Header().Add(gzhttp.HeaderNoCompression, "1")
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// fast path: empty string, ignore
|
||||
case "":
|
||||
// leave image as false
|
||||
case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
|
||||
case ".avif", ".jpg", ".jpeg", ".jxl", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
|
||||
image = true
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ var wellKnownMimeTypesLower = sync.OnceValue(func() map[string]string {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".jxl": "image/jxl",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
|
||||
@@ -15,7 +15,7 @@ var Attachment AttachmentSettingType
|
||||
|
||||
func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
|
||||
Attachment = AttachmentSettingType{
|
||||
AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip",
|
||||
AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.jxl,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip",
|
||||
MaxSize: 100,
|
||||
MaxFiles: 5,
|
||||
Enabled: true,
|
||||
|
||||
@@ -59,8 +59,6 @@ var (
|
||||
DisableStars bool `ini:"DISABLE_STARS"`
|
||||
DefaultBranch string
|
||||
DefaultObjectFormat string
|
||||
AllowAdoptionOfUnadoptedRepositories bool
|
||||
AllowDeleteOfUnadoptedRepositories bool
|
||||
DisableDownloadSourceArchives bool
|
||||
AllowForkWithoutMaximumLimit bool
|
||||
AllowForkIntoSameOwner bool
|
||||
|
||||
@@ -18,6 +18,7 @@ const SniffContentSize = 1024
|
||||
const (
|
||||
MimeTypeImageSvg = "image/svg+xml"
|
||||
MimeTypeImageAvif = "image/avif"
|
||||
MimeTypeImageJxl = "image/jxl"
|
||||
|
||||
MimeTypeApplicationOctetStream = "application/octet-stream"
|
||||
)
|
||||
@@ -170,6 +171,12 @@ func DetectContentType(data []byte) SniffedType {
|
||||
ct = MimeTypeImageAvif
|
||||
}
|
||||
|
||||
// https://github.com/libjxl/libjxl/blob/main/plugins/mime/image-jxl.xml
|
||||
if bytes.HasPrefix(data, []byte("\xFF\x0A")) ||
|
||||
bytes.HasPrefix(data, []byte("\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A")) {
|
||||
ct = MimeTypeImageJxl
|
||||
}
|
||||
|
||||
if ct == "application/ogg" {
|
||||
dataHead := data
|
||||
if len(dataHead) > 256 {
|
||||
|
||||
@@ -150,10 +150,16 @@ func TestDetectFileTypeBox(t *testing.T) {
|
||||
assert.Equal(t, []string{"AAAA"}, brands)
|
||||
}
|
||||
|
||||
func TestDetectContentTypeAvif(t *testing.T) {
|
||||
buf := []byte("\x00\x00\x00\x20ftypavif.......................")
|
||||
st := DetectContentType(buf)
|
||||
assert.Equal(t, MimeTypeImageAvif, st.contentType)
|
||||
func TestDetectContentTypeImage(t *testing.T) {
|
||||
t.Run("avif", func(t *testing.T) {
|
||||
buf := []byte("\x00\x00\x00\x20ftypavif.......................")
|
||||
st := DetectContentType(buf)
|
||||
assert.Equal(t, MimeTypeImageAvif, st.contentType)
|
||||
})
|
||||
t.Run("jxl", func(t *testing.T) {
|
||||
assert.Equal(t, MimeTypeImageJxl, DetectContentType([]byte("\xFF\x0A....")).contentType)
|
||||
assert.Equal(t, MimeTypeImageJxl, DetectContentType([]byte("\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A...")).contentType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectContentTypeIncorrectFont(t *testing.T) {
|
||||
|
||||
@@ -547,9 +547,6 @@
|
||||
"form.repo_name_been_taken": "The repository name is already used.",
|
||||
"form.repository_force_private": "Force Private is enabled: private repositories cannot be made public.",
|
||||
"form.repository_files_already_exist": "Files already exist for this repository. Contact the system administrator.",
|
||||
"form.repository_files_already_exist.adopt": "Files already exist for this repository and can only be adopted.",
|
||||
"form.repository_files_already_exist.delete": "Files already exist for this repository. You must delete them.",
|
||||
"form.repository_files_already_exist.adopt_or_delete": "Files already exist for this repository. Either adopt them or delete them.",
|
||||
"form.org_name_been_taken": "The organization name is already taken.",
|
||||
"form.team_name_been_taken": "The team name is already taken.",
|
||||
"form.team_no_units_error": "Allow access to at least one repository section.",
|
||||
@@ -1014,15 +1011,6 @@
|
||||
"repo.reactions": "Reactions",
|
||||
"repo.unit_disabled": "The site administrator has disabled this repository section.",
|
||||
"repo.language_other": "Other",
|
||||
"repo.adopt_search": "Enter username to search for unadopted repositories… (leave blank to find all)",
|
||||
"repo.adopt_preexisting_label": "Adopt Files",
|
||||
"repo.adopt_preexisting": "Adopt pre-existing files",
|
||||
"repo.adopt_preexisting_content": "Create repository from %s",
|
||||
"repo.adopt_preexisting_success": "Adopted files and created repository from %s",
|
||||
"repo.delete_preexisting_label": "Delete",
|
||||
"repo.delete_preexisting": "Delete pre-existing files",
|
||||
"repo.delete_preexisting_content": "Delete files in %s",
|
||||
"repo.delete_preexisting_success": "Deleted unadopted files in %s",
|
||||
"repo.blame_prior": "View blame prior to this change",
|
||||
"repo.blame.ignore_revs": "Ignoring revisions in <a href=\"%s\">.git-blame-ignore-revs</a>. Click <a href=\"%s\">here to bypass</a> and see the normal blame view.",
|
||||
"repo.blame.ignore_revs.failed": "Failed to ignore revisions in <a href=\"%s\">.git-blame-ignore-revs</a>.",
|
||||
@@ -3148,8 +3136,6 @@
|
||||
"admin.orgs.members": "Members",
|
||||
"admin.orgs.new_orga": "New Organization",
|
||||
"admin.repos.repo_manage_panel": "Repository Management",
|
||||
"admin.repos.unadopted": "Unadopted Repositories",
|
||||
"admin.repos.unadopted.no_more": "No more unadopted repositories found",
|
||||
"admin.repos.owner": "Owner",
|
||||
"admin.repos.name": "Name",
|
||||
"admin.repos.private": "Private",
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// 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: query
|
||||
// in: query
|
||||
// description: glob pattern of repositories to search for, in the form owner or owner/repo
|
||||
// type: string
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/StringSlice"
|
||||
// default:
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
if listOptions.Page == 0 {
|
||||
listOptions.Page = 1
|
||||
}
|
||||
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, ctx.FormString("query"), &listOptions)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(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"
|
||||
// default:
|
||||
// "$ref": "#/responses/error"
|
||||
ownerName := ctx.PathParam("username")
|
||||
repoName := ctx.PathParam("reponame")
|
||||
|
||||
ctxUser, err := user_model.GetUserByName(ctx, ownerName)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// check not a repo
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
exist, err := git.IsRepositoryExist(ctx, gitrepo.CodeRepoByName(ctxUser.Name, repoName))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if has || !exist {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{
|
||||
Name: repoName,
|
||||
IsPrivate: true,
|
||||
}); err != nil {
|
||||
ctx.APIErrorInternal(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"
|
||||
// default:
|
||||
// "$ref": "#/responses/error"
|
||||
ownerName := ctx.PathParam("username")
|
||||
repoName := ctx.PathParam("reponame")
|
||||
|
||||
ctxUser, err := user_model.GetUserByName(ctx, ownerName)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// check not a repo
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
exist, err := git.IsRepositoryExist(ctx, gitrepo.CodeRepoByName(ctxUser.Name, repoName))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if has || !exist {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -1854,11 +1854,6 @@ func Routes() *web.Router {
|
||||
m.Get("", admin.GetAllEmails)
|
||||
m.Get("/search", admin.SearchEmail)
|
||||
})
|
||||
m.Group("/unadopted", func() {
|
||||
m.Get("", admin.ListUnadoptedRepositories)
|
||||
m.Post("/{username}/{reponame}", admin.AdoptRepository)
|
||||
m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository)
|
||||
})
|
||||
m.Group("/hooks", func() {
|
||||
m.Combo("").Get(admin.ListHooks).
|
||||
Post(bind(api.CreateHookOption{}), admin.CreateHook)
|
||||
|
||||
@@ -222,7 +222,7 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
|
||||
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
||||
ctx.APIError(http.StatusConflict, "Files already exist for this repository. Adopt them or delete them.")
|
||||
ctx.APIError(http.StatusConflict, "Files already exist for this repository.")
|
||||
case migrations.IsRateLimitError(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Remote visit addressed rate limitation.")
|
||||
case migrations.IsTwoFactorAuthError(err):
|
||||
|
||||
@@ -42,13 +42,6 @@ type swaggerResponseLicenseTemplateInfo struct {
|
||||
Body api.LicenseTemplateInfo `json:"body"`
|
||||
}
|
||||
|
||||
// StringSlice
|
||||
// swagger:response StringSlice
|
||||
type swaggerResponseStringSlice struct {
|
||||
// in:body
|
||||
Body []string `json:"body"`
|
||||
}
|
||||
|
||||
// LabelTemplateList
|
||||
// swagger:response LabelTemplateList
|
||||
type swaggerResponseLabelTemplateList struct {
|
||||
|
||||
@@ -4,15 +4,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
@@ -22,8 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
tplRepos templates.TplName = "admin/repo/list"
|
||||
tplUnadoptedRepos templates.TplName = "admin/repo/unadopted"
|
||||
tplRepos templates.TplName = "admin/repo/list"
|
||||
)
|
||||
|
||||
// Repos show all the repositories
|
||||
@@ -56,101 +47,3 @@ func DeleteRepo(ctx *context.Context) {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
// UnadoptedRepos lists the unadopted repositories
|
||||
func UnadoptedRepos(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.repositories")
|
||||
ctx.Data["PageIsAdminRepositories"] = true
|
||||
|
||||
opts := db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
Page: ctx.FormInt("page"),
|
||||
}
|
||||
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
||||
ctx.Data["CurrentPage"] = opts.Page
|
||||
|
||||
doSearch := ctx.FormBool("search")
|
||||
|
||||
ctx.Data["search"] = doSearch
|
||||
q := ctx.FormString("q")
|
||||
|
||||
if !doSearch {
|
||||
pager := context.NewPagerBuilder(ctx).TotalCount(0).PerPageLimit(opts.PageSize).CurPage(opts.Page).Build()
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Keyword"] = q
|
||||
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, q, &opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListUnadoptedRepositories", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Dirs"] = repoNames
|
||||
pager := context.NewPagerBuilder(ctx).TotalCount(count).PerPageLimit(opts.PageSize).CurPage(opts.Page).Build()
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
|
||||
}
|
||||
|
||||
// AdoptOrDeleteRepository adopts or deletes a repository
|
||||
func AdoptOrDeleteRepository(ctx *context.Context) {
|
||||
dir := ctx.FormString("id")
|
||||
action := ctx.FormString("action")
|
||||
page := ctx.FormString("page")
|
||||
q := ctx.FormString("q")
|
||||
|
||||
dirSplit := strings.SplitN(dir, "/", 2)
|
||||
if len(dirSplit) != 2 {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/repos")
|
||||
return
|
||||
}
|
||||
|
||||
ctxUser, err := user_model.GetUserByName(ctx, dirSplit[0])
|
||||
if err != nil {
|
||||
if user_model.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
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsRepositoryExist", err)
|
||||
return
|
||||
}
|
||||
exist, err := git.IsRepositoryExist(ctx, gitrepo.CodeRepoByName(ctxUser.Name, repoName))
|
||||
if err != nil {
|
||||
ctx.ServerError("IsDir", err)
|
||||
return
|
||||
}
|
||||
if has || !exist {
|
||||
// Fallthrough to failure mode
|
||||
} else if action == "adopt" {
|
||||
if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.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 := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, 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?search=true&q=" + url.QueryEscape(q) + "&page=" + url.QueryEscape(page))
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
@@ -212,16 +211,7 @@ func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_serv
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
|
||||
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
||||
switch {
|
||||
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
|
||||
ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"))
|
||||
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
||||
ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt"))
|
||||
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
||||
ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete"))
|
||||
default:
|
||||
ctx.JSONError(ctx.Tr("form.repository_files_already_exist"))
|
||||
}
|
||||
ctx.JSONError(ctx.Tr("form.repository_files_already_exist"))
|
||||
case errors.As(err, &errNameReserved):
|
||||
ctx.JSONError(ctx.Tr("repo.form.name_reserved", errNameReserved.Name))
|
||||
case errors.As(err, &errNamePatternNotAllowed):
|
||||
|
||||
@@ -81,16 +81,7 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error)
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.repo_name_been_taken"), "repo_name")
|
||||
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
||||
switch {
|
||||
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), "repo_name")
|
||||
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.repository_files_already_exist.adopt"), "repo_name")
|
||||
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.repository_files_already_exist.delete"), "repo_name")
|
||||
default:
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.repository_files_already_exist"), "repo_name")
|
||||
}
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.repository_files_already_exist"), "repo_name")
|
||||
case errors.As(err, &errNameReserved):
|
||||
ctx.JSONErrorWithField(ctx.Tr("repo.form.name_reserved", errNameReserved.Name), "repo_name")
|
||||
case errors.As(err, &errNamePatternNotAllowed):
|
||||
|
||||
@@ -177,16 +177,7 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error,
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, form)
|
||||
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
switch {
|
||||
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
|
||||
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
|
||||
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
|
||||
default:
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
||||
}
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
||||
case errors.As(err, &errNameReserved):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", errNameReserved.Name), tpl, form)
|
||||
|
||||
@@ -237,16 +237,7 @@ func handleSettingsPostUpdate(ctx *context.Context) {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", errNameReserved.Name), tplSettingsOptions, &form)
|
||||
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
switch {
|
||||
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
|
||||
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
|
||||
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
|
||||
default:
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
|
||||
}
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
|
||||
case errors.As(err, &errNamePatternNotAllowed):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", errNamePatternNotAllowed.Pattern), tplSettingsOptions, &form)
|
||||
default:
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// AdoptOrDeleteRepository adopts or deletes a repository
|
||||
func AdoptOrDeleteRepository(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.adopt")
|
||||
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.FormString("id")
|
||||
action := ctx.FormString("action")
|
||||
|
||||
ctxUser := ctx.Doer
|
||||
|
||||
// check not a repo
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, dir)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsRepositoryExist", err)
|
||||
return
|
||||
}
|
||||
|
||||
exist, err := git.IsRepositoryExist(ctx, gitrepo.CodeRepoByName(ctxUser.Name, dir))
|
||||
if err != nil {
|
||||
ctx.ServerError("IsDir", err)
|
||||
return
|
||||
}
|
||||
if has || !exist {
|
||||
// Fallthrough to failure mode
|
||||
} else if action == "adopt" && allowAdopt {
|
||||
if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_service.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 := repo_service.DeleteUnadoptedRepository(ctx, 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")
|
||||
}
|
||||
@@ -10,15 +10,11 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -207,9 +203,6 @@ func Organization(ctx *context.Context) {
|
||||
func Repos(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.repos")
|
||||
ctx.Data["PageIsSettingsRepos"] = true
|
||||
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
|
||||
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
|
||||
|
||||
opts := db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
Page: ctx.FormInt("page"),
|
||||
@@ -218,91 +211,21 @@ func Repos(ctx *context.Context) {
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
start := int64((opts.Page - 1) * opts.PageSize)
|
||||
end := start + int64(opts.PageSize)
|
||||
|
||||
adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories)
|
||||
|
||||
ctxUser := ctx.Doer
|
||||
var count int64
|
||||
|
||||
if adoptOrDelete {
|
||||
repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum)
|
||||
repos := map[string]*repo_model.Repository{}
|
||||
// We're going to iterate by pagesize.
|
||||
root := gitrepo.UserLocalPath(ctxUser.Name)
|
||||
if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() || path == root {
|
||||
return nil
|
||||
}
|
||||
name := d.Name()
|
||||
if !strings.HasSuffix(name, ".git") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
name = name[:len(name)-4]
|
||||
if repo_model.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.WalkDir", err)
|
||||
return
|
||||
}
|
||||
|
||||
userRepos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
OwnerID: ctxUser.ID,
|
||||
Private: true,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
LowerNames: repoNames,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepositories", err)
|
||||
return
|
||||
}
|
||||
for _, repo := range userRepos {
|
||||
if repo.IsFork {
|
||||
if err := repo.GetBaseRepo(ctx); err != nil {
|
||||
ctx.ServerError("GetBaseRepo", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
repos[repo.LowerName] = repo
|
||||
}
|
||||
ctx.Data["Dirs"] = repoNames
|
||||
ctx.Data["ReposMap"] = repos
|
||||
} else {
|
||||
repos, reposCount, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{OwnerID: ctxUser.ID, Private: true, ListOptions: opts})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepositories", err)
|
||||
return
|
||||
}
|
||||
count = reposCount
|
||||
|
||||
for i := range repos {
|
||||
if repos[i].IsFork {
|
||||
if err := repos[i].GetBaseRepo(ctx); err != nil {
|
||||
ctx.ServerError("GetBaseRepo", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Repos"] = repos
|
||||
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{OwnerID: ctx.Doer.ID, Private: true, ListOptions: opts})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepositories", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["ContextUser"] = ctxUser
|
||||
for i := range repos {
|
||||
if repos[i].IsFork {
|
||||
if err := repos[i].GetBaseRepo(ctx); err != nil {
|
||||
ctx.ServerError("GetBaseRepo", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Repos"] = repos
|
||||
pager := context.NewPagerBuilder(ctx).TotalCount(count).PerPageLimit(opts.PageSize).CurPage(opts.Page).Build()
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplSettingsRepositories)
|
||||
|
||||
@@ -702,7 +702,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
m.Get("/organization", user_setting.Organization)
|
||||
m.Get("/repos", user_setting.Repos)
|
||||
m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository)
|
||||
|
||||
m.Group("/hooks", func() {
|
||||
m.Get("", user_setting.Webhooks)
|
||||
@@ -818,7 +817,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
m.Group("/repos", func() {
|
||||
m.Get("", admin.Repos)
|
||||
m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository)
|
||||
m.Post("/delete", admin.DeleteRepo)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ package upload
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/reqctx"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/typesniffer"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
@@ -60,7 +60,7 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error {
|
||||
return nil // everything is allowed
|
||||
}
|
||||
|
||||
fullMimeType := http.DetectContentType(buf)
|
||||
fullMimeType := typesniffer.DetectContentType(buf).GetMimeType()
|
||||
mimeType, _, err := mime.ParseMediaType(fullMimeType)
|
||||
if err != nil {
|
||||
log.Warn("Detected attachment type could not be parsed %s", fullMimeType)
|
||||
|
||||
@@ -4,42 +4,38 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUpload(t *testing.T) {
|
||||
testContent := []byte(`This is a plain text file.`)
|
||||
var b bytes.Buffer
|
||||
w := gzip.NewWriter(&b)
|
||||
w.Write(testContent)
|
||||
w.Close()
|
||||
|
||||
kases := []struct {
|
||||
text := []byte(`This is a plain text file.`)
|
||||
gzip := []byte("\x1F\x8B\x08\x00\x00\x00\x00\x00\x00\xFF")
|
||||
jxl := []byte("\xFF\x0A....")
|
||||
cases := []struct {
|
||||
data []byte
|
||||
fileName string
|
||||
allowedTypes []string
|
||||
err error
|
||||
}{
|
||||
{testContent, "test.txt", []string{""}, nil},
|
||||
{testContent, "dir/test.txt", []string{""}, nil},
|
||||
{testContent, "../../../test.txt", []string{""}, nil},
|
||||
{testContent, "test.txt", []string{"", ",", "|", "*/*", "*/*,", "*/*|", "text/plain"}, nil},
|
||||
{testContent, "dir/test.txt", []string{"text/plain"}, nil},
|
||||
{testContent, "/dir.txt/test.js", []string{".js"}, nil},
|
||||
{testContent, "test.txt", []string{" text/plain ", ".txt", " .txt,.js", " .txt|.js"}, nil},
|
||||
{testContent, "../../test.txt", []string{" .txt|.js"}, nil},
|
||||
{testContent, "test.txt", []string{" .txt ,.js ", "text/plain, .txt", "text/*", "text/*,.js"}, nil},
|
||||
{testContent, "test.txt", []string{"text/**", "application/x-gzip", ".zip", ".zip,.txtx", ".zip|.txtx"}, ErrFileTypeForbidden{"text/plain; charset=utf-8"}},
|
||||
{b.Bytes(), "test.txt", []string{"application/x-gzip"}, nil},
|
||||
{text, "test.txt", []string{""}, nil},
|
||||
{text, "dir/test.txt", []string{""}, nil},
|
||||
{text, "../../../test.txt", []string{""}, nil},
|
||||
{text, "test.txt", []string{"", ",", "|", "*/*", "*/*,", "*/*|", "text/plain"}, nil},
|
||||
{text, "dir/test.txt", []string{"text/plain"}, nil},
|
||||
{text, "/dir.txt/test.js", []string{".js"}, nil},
|
||||
{text, "test.txt", []string{" text/plain ", ".txt", " .txt,.js", " .txt|.js"}, nil},
|
||||
{text, "../../test.txt", []string{" .txt|.js"}, nil},
|
||||
{text, "test.txt", []string{" .txt ,.js ", "text/plain, .txt", "text/*", "text/*,.js"}, nil},
|
||||
{text, "test.txt", []string{"text/**", "application/x-gzip", ".zip", ".zip,.txtx", ".zip|.txtx"}, ErrFileTypeForbidden{"text/plain"}},
|
||||
{gzip, "test.txt", []string{"application/x-gzip"}, nil},
|
||||
{jxl, "test.jxl", []string{"image/jxl"}, nil},
|
||||
}
|
||||
|
||||
for _, kase := range kases {
|
||||
for _, allowedTypes := range kase.allowedTypes {
|
||||
assert.Equal(t, kase.err, Verify(kase.data, kase.fileName, allowedTypes))
|
||||
for _, tc := range cases {
|
||||
for _, allowedTypes := range tc.allowedTypes {
|
||||
assert.Equal(t, tc.err, Verify(tc.data, tc.fileName, allowedTypes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,6 @@ func NewNotifier() notify_service.Notifier {
|
||||
return &indexerNotifier{}
|
||||
}
|
||||
|
||||
func (r *indexerNotifier) AdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
r.MigrateRepository(ctx, doer, u, repo)
|
||||
}
|
||||
|
||||
func (r *indexerNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
|
||||
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
|
||||
) {
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
type Notifier interface {
|
||||
Run()
|
||||
|
||||
AdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository)
|
||||
CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository)
|
||||
MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository)
|
||||
DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository)
|
||||
|
||||
@@ -364,15 +364,6 @@ func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_
|
||||
}
|
||||
}
|
||||
|
||||
// AdoptRepository notifies the adoption of a repository to notifiers
|
||||
func AdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
defer recoverPanic()
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
notifier.AdoptRepository(ctx, doer, u, repo)
|
||||
}
|
||||
}
|
||||
|
||||
// MigrateRepository notifies create repository to notifiers
|
||||
func MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
defer recoverPanic()
|
||||
|
||||
@@ -154,10 +154,6 @@ func (*NullNotifier) IssueChangeProjectColumn(ctx context.Context, doer *user_mo
|
||||
func (*NullNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
}
|
||||
|
||||
// AdoptRepository places a place holder function
|
||||
func (*NullNotifier) AdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
}
|
||||
|
||||
// DeleteRepository places a place holder function
|
||||
func (*NullNotifier) DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) {
|
||||
}
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
)
|
||||
|
||||
func deleteFailedAdoptRepository(repoID int64) error {
|
||||
return db.WithTx(graceful.GetManager().ShutdownContext(), func(ctx context.Context) error {
|
||||
if err := deleteDBRepository(ctx, repoID); err != nil {
|
||||
return fmt.Errorf("deleteDBRepository: %w", err)
|
||||
}
|
||||
if err := git_model.DeleteRepoBranches(ctx, repoID); err != nil {
|
||||
return fmt.Errorf("deleteRepoBranches: %w", err)
|
||||
}
|
||||
return repo_model.DeleteRepoReleases(ctx, repoID)
|
||||
})
|
||||
}
|
||||
|
||||
// AdoptRepository adopts pre-existing repository files for the user/organization.
|
||||
func AdoptRepository(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
|
||||
if !doer.CanCreateRepoIn(owner) {
|
||||
return nil, repo_model.ErrReachLimitOfRepo{
|
||||
Limit: owner.MaxRepoCreation,
|
||||
}
|
||||
}
|
||||
|
||||
repo := &repo_model.Repository{
|
||||
OwnerID: owner.ID,
|
||||
Owner: owner,
|
||||
OwnerName: owner.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: repo_model.RepositoryBeingMigrated,
|
||||
IsEmpty: !opts.AutoInit,
|
||||
}
|
||||
|
||||
// 1 - create the repository database operations first
|
||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
return createRepositoryInDB(ctx, doer, owner, repo, false)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// last - clean up if something goes wrong
|
||||
// WARNING: Don't override all later err with local variables
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// we can not use `ctx` because it may be canceled or timed out
|
||||
if errDel := deleteFailedAdoptRepository(repo.ID); errDel != nil {
|
||||
log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Re-fetch the repository from database before updating it (else it would
|
||||
// override changes that were done earlier with sql)
|
||||
if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
|
||||
return nil, fmt.Errorf("getRepositoryByID: %w", err)
|
||||
}
|
||||
|
||||
// 2 - adopt the repository from disk
|
||||
if err = adoptRepository(ctx, repo, opts.DefaultBranch); err != nil {
|
||||
return nil, fmt.Errorf("adoptRepository: %w", err)
|
||||
}
|
||||
|
||||
// 3 - Update the git repository
|
||||
if err = updateGitRepoAfterCreate(ctx, repo); err != nil {
|
||||
return nil, fmt.Errorf("updateGitRepoAfterCreate: %w", err)
|
||||
}
|
||||
|
||||
// 4 - update repository status
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
notify_service.AdoptRepository(ctx, doer, owner, repo)
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBranch string) (err error) {
|
||||
isExist, err := git.IsRepositoryExist(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", repo.FullName(), err)
|
||||
return err
|
||||
}
|
||||
if !isExist {
|
||||
return fmt.Errorf("adoptRepository: path does not already exist: %s", repo.FullName())
|
||||
}
|
||||
|
||||
if err := git.CreateDelegateHooks(ctx, repo); err != nil {
|
||||
return fmt.Errorf("createDelegateHooks: %w", err)
|
||||
}
|
||||
|
||||
repo.IsEmpty = false
|
||||
|
||||
if len(defaultBranch) > 0 {
|
||||
repo.DefaultBranch = defaultBranch
|
||||
|
||||
if err = git.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
|
||||
return fmt.Errorf("setDefaultBranch: %w", err)
|
||||
}
|
||||
} else {
|
||||
repo.DefaultBranch, err = git.GetDefaultBranch(ctx, repo)
|
||||
if err != nil {
|
||||
repo.DefaultBranch = setting.Repository.DefaultBranch
|
||||
if err = git.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
|
||||
return fmt.Errorf("setDefaultBranch: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't bother looking this repo in the context it won't be there
|
||||
gitRepo, err := git.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("openRepository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if _, _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil {
|
||||
return fmt.Errorf("SyncRepoBranchesWithRepo: %w", err)
|
||||
}
|
||||
|
||||
if _, err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
return fmt.Errorf("SyncReleasesWithTags: %w", err)
|
||||
}
|
||||
|
||||
branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
|
||||
RepoID: repo.ID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
})
|
||||
|
||||
found := false
|
||||
hasDefault := false
|
||||
hasMaster := false
|
||||
hasMain := 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
|
||||
} else if branch == "main" {
|
||||
hasMain = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if hasDefault {
|
||||
repo.DefaultBranch = setting.Repository.DefaultBranch
|
||||
} else if hasMaster {
|
||||
repo.DefaultBranch = "master"
|
||||
} else if hasMain {
|
||||
repo.DefaultBranch = "main"
|
||||
} else if len(branches) > 0 {
|
||||
repo.DefaultBranch = branches[0]
|
||||
} else {
|
||||
repo.IsEmpty = true
|
||||
repo.DefaultBranch = setting.Repository.DefaultBranch
|
||||
}
|
||||
|
||||
if err = git.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
|
||||
return fmt.Errorf("setDefaultBranch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_empty", "default_branch"); err != nil {
|
||||
return fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
|
||||
log.Error("Failed to update size for repository: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUnadoptedRepository deletes unadopted repository files from the filesystem
|
||||
func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, repoName string) error {
|
||||
if err := repo_model.IsUsableRepoName(repoName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codeRepo := gitrepo.CodeRepoByName(u.Name, repoName)
|
||||
exist, err := git.IsRepositoryExist(ctx, codeRepo)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if repo %s/%s exists. Error: %v", u.Name, repoName, err)
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return repo_model.ErrRepoNotExist{
|
||||
OwnerName: u.Name,
|
||||
Name: repoName,
|
||||
}
|
||||
}
|
||||
|
||||
if exist, err := repo_model.IsRepositoryModelExist(ctx, u, repoName); err != nil {
|
||||
return err
|
||||
} else if exist {
|
||||
return repo_model.ErrRepoAlreadyExist{
|
||||
Uname: u.Name,
|
||||
Name: repoName,
|
||||
}
|
||||
}
|
||||
|
||||
return git.DeleteRepository(ctx, codeRepo)
|
||||
}
|
||||
|
||||
type unadoptedRepositories struct {
|
||||
repositories []string
|
||||
count int64
|
||||
start, end int64
|
||||
}
|
||||
|
||||
func (unadopted *unadoptedRepositories) add(repository string) {
|
||||
if unadopted.count >= unadopted.start && unadopted.count < unadopted.end {
|
||||
unadopted.repositories = append(unadopted.repositories, repository)
|
||||
}
|
||||
unadopted.count++
|
||||
}
|
||||
|
||||
func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesToCheck []string, unadopted *unadoptedRepositories) error {
|
||||
if len(repoNamesToCheck) == 0 {
|
||||
return nil
|
||||
}
|
||||
ctxUser, err := user_model.GetUserByName(ctx, userName)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
log.Debug("Missing user: %s", userName)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
OwnerID: ctxUser.ID,
|
||||
Private: true,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: len(repoNamesToCheck),
|
||||
}, LowerNames: repoNamesToCheck,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(repos) == len(repoNamesToCheck) {
|
||||
return nil
|
||||
}
|
||||
repoNames := make(container.Set[string], len(repos))
|
||||
for _, repo := range repos {
|
||||
repoNames.Add(repo.LowerName)
|
||||
}
|
||||
for _, repoName := range repoNamesToCheck {
|
||||
if !repoNames.Contains(repoName) {
|
||||
unadopted.add(path.Join(userName, repoName)) // These are not used as filepaths - but as reponames - therefore use path.Join not filepath.Join
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUnadoptedRepositories lists all the unadopted repositories that match the provided query
|
||||
func ListUnadoptedRepositories(ctx context.Context, query string, opts *db.ListOptions) ([]string, int64, error) {
|
||||
opts.SetDefaultValues()
|
||||
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 expression '%s' (skipped): %v", qsplit[0], err)
|
||||
}
|
||||
if len(qsplit) > 1 {
|
||||
globRepo, err = glob.Compile(qsplit[1])
|
||||
if err != nil {
|
||||
log.Info("Invalid glob expression '%s' (skipped): %v", qsplit[1], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
var repoNamesToCheck []string
|
||||
|
||||
start := int64((opts.Page - 1) * opts.PageSize)
|
||||
unadopted := &unadoptedRepositories{
|
||||
repositories: make([]string, 0, opts.PageSize),
|
||||
start: start,
|
||||
end: start + int64(opts.PageSize),
|
||||
count: 0,
|
||||
}
|
||||
|
||||
var userName string
|
||||
|
||||
// We're going to iterate by pagesize.
|
||||
root := filepath.Clean(setting.RepoRootPath)
|
||||
if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() || path == root {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := d.Name()
|
||||
|
||||
if !strings.ContainsRune(path[len(root)+1:], filepath.Separator) {
|
||||
// Got a new user
|
||||
if err = checkUnadoptedRepositories(ctx, userName, repoNamesToCheck, unadopted); err != nil {
|
||||
return err
|
||||
}
|
||||
repoNamesToCheck = repoNamesToCheck[:0]
|
||||
|
||||
if !globUser.Match(name) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
userName = name
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(name, ".git") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
name = name[:len(name)-4]
|
||||
if repo_model.IsUsableRepoName(name) != nil || strings.ToLower(name) != name || !globRepo.Match(name) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
repoNamesToCheck = append(repoNamesToCheck, name)
|
||||
if len(repoNamesToCheck) >= setting.Database.IterateBufferSize {
|
||||
if err = checkUnadoptedRepositories(ctx, userName, repoNamesToCheck, unadopted); err != nil {
|
||||
return err
|
||||
}
|
||||
repoNamesToCheck = repoNamesToCheck[:0]
|
||||
}
|
||||
return filepath.SkipDir
|
||||
}); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := checkUnadoptedRepositories(ctx, userName, repoNamesToCheck, unadopted); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return unadopted.repositories, unadopted.count, nil
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckUnadoptedRepositories_Add(t *testing.T) {
|
||||
const start = 10
|
||||
const end = 20
|
||||
unadopted := &unadoptedRepositories{
|
||||
start: start,
|
||||
end: end,
|
||||
count: 0,
|
||||
}
|
||||
|
||||
const total = 30
|
||||
for range total {
|
||||
unadopted.add("something")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, total, unadopted.count)
|
||||
assert.Len(t, unadopted.repositories, end-start)
|
||||
}
|
||||
|
||||
func TestCheckUnadoptedRepositories(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
//
|
||||
// Non existent user
|
||||
//
|
||||
unadopted := &unadoptedRepositories{start: 0, end: 100}
|
||||
err := checkUnadoptedRepositories(t.Context(), "notauser", []string{"repo"}, unadopted)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, unadopted.repositories)
|
||||
//
|
||||
// Unadopted repository is returned
|
||||
// Existing (adopted) repository is not returned
|
||||
//
|
||||
userName := "user2"
|
||||
repoName := "repo2"
|
||||
unadoptedRepoName := "unadopted"
|
||||
unadopted = &unadoptedRepositories{start: 0, end: 100}
|
||||
err = checkUnadoptedRepositories(t.Context(), userName, []string{repoName, unadoptedRepoName}, unadopted)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{path.Join(userName, unadoptedRepoName)}, unadopted.repositories)
|
||||
//
|
||||
// Existing (adopted) repository is not returned
|
||||
//
|
||||
unadopted = &unadoptedRepositories{start: 0, end: 100}
|
||||
err = checkUnadoptedRepositories(t.Context(), userName, []string{repoName}, unadopted)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, unadopted.repositories)
|
||||
assert.Zero(t, unadopted.count)
|
||||
}
|
||||
|
||||
func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
username := "user2"
|
||||
unadoptedList := []string{path.Join(username, "unadopted1"), path.Join(username, "unadopted2")}
|
||||
for _, unadopted := range unadoptedList {
|
||||
_ = os.Mkdir(filepath.Join(setting.RepoRootPath, unadopted+".git"), 0o755)
|
||||
}
|
||||
|
||||
for i, unadopted := range unadoptedList {
|
||||
opts := db.ListOptions{Page: i + 1, PageSize: 1}
|
||||
repoNames, count, err := ListUnadoptedRepositories(t.Context(), "", &opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, count)
|
||||
assert.Equal(t, unadopted, repoNames[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdoptRepository(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testRepoName := "test-adopt"
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
destDir := filepath.Join(setting.RepoRootPath, user2.Name, testRepoName+".git")
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
// a successful adopt
|
||||
assert.NoError(t, unittest.SyncDirs(filepath.Join(setting.RepoRootPath, user2.Name, "repo1.git"), destDir))
|
||||
|
||||
adoptedRepo, err := AdoptRepository(t.Context(), user2, user2, CreateRepoOptions{Name: testRepoName})
|
||||
assert.NoError(t, err)
|
||||
repoTestAdopt := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: testRepoName})
|
||||
assert.Equal(t, "sha1", repoTestAdopt.ObjectFormatName)
|
||||
|
||||
// just delete the adopted repo's db records
|
||||
err = deleteFailedAdoptRepository(adoptedRepo.ID)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Failure", func(t *testing.T) {
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: testRepoName})
|
||||
// a failed adopt because some mock data
|
||||
// remove the hooks directory and create a file so that we cannot create the hooks successfully
|
||||
_ = os.RemoveAll(filepath.Join(destDir, "hooks", "update.d"))
|
||||
assert.NoError(t, os.WriteFile(filepath.Join(destDir, "hooks", "update.d"), []byte("dummy-content"), os.ModePerm))
|
||||
|
||||
adoptedRepo, err := AdoptRepository(t.Context(), user2, user2, CreateRepoOptions{Name: testRepoName})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, adoptedRepo)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: testRepoName})
|
||||
|
||||
exist, err := git.IsRepositoryExist(t.Context(), gitrepo.CodeRepoByName(user2.Name, testRepoName))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist) // the repository should be still in the disk
|
||||
})
|
||||
}
|
||||
@@ -278,7 +278,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
|
||||
return nil, err
|
||||
}
|
||||
if isExist {
|
||||
log.Error("Files already exist in %s and we are not going to adopt or delete.", repo.FullName())
|
||||
log.Error("Files already exist in %s", repo.FullName())
|
||||
// Don't return directly, we need err in defer to cleanupRepository
|
||||
err = repo_model.ErrRepoFilesAlreadyExist{
|
||||
Uname: repo.OwnerName,
|
||||
|
||||
@@ -137,7 +137,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
||||
return nil, err
|
||||
}
|
||||
if isExist {
|
||||
log.Error("Files already exist in %s and we are not going to adopt or delete.", repo.FullName())
|
||||
log.Error("Files already exist in %s", repo.FullName())
|
||||
// Don't return directly, we need err in defer to cleanupRepository
|
||||
err = repo_model.ErrRepoFilesAlreadyExist{
|
||||
Uname: repo.OwnerName,
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.repos.repo_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
|
||||
<div class="ui right">
|
||||
<a class="ui primary tiny button" href="{{AppSubUrl}}/-/admin/repos/unadopted">{{ctx.Locale.Tr "admin.repos.unadopted"}}</a>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{template "shared/repo/search" .}}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
{{template "admin/layout_head" (dict "pageClass" "admin")}}
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.repos.unadopted"}}
|
||||
<div class="ui right">
|
||||
<a class="ui primary tiny button" href="{{AppSubUrl}}/-/admin/repos">{{ctx.Locale.Tr "admin.repos.repo_manage_panel"}}</a>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form ignore-dirty">
|
||||
<div class="ui small fluid action input">
|
||||
<input name="search" value="true" type="hidden">
|
||||
<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.adopt_search"}}" autofocus>
|
||||
{{template "shared/search/button"}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{if .search}}
|
||||
<div class="ui attached segment settings">
|
||||
{{if .Dirs}}
|
||||
<div class="ui aligned divided list">
|
||||
{{range $dirI, $dir := .Dirs}}
|
||||
<div class="item flex-text-block">
|
||||
<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
|
||||
<div>
|
||||
<button type="button" class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
|
||||
<dialog class="modal-dialog" id="adopt-unadopted-modal-{{$dirI}}">
|
||||
<div class="header">
|
||||
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}</p>
|
||||
</div>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted">
|
||||
<input type="hidden" name="id" value="{{$dir}}">
|
||||
<input type="hidden" name="action" value="adopt">
|
||||
<input type="hidden" name="q" value="{{$.Keyword}}">
|
||||
<input type="hidden" name="page" value="{{$.CurrentPage}}">
|
||||
{{template "base/modal_actions_confirm"}}
|
||||
</form>
|
||||
</dialog>
|
||||
<button type="button" class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
|
||||
<dialog class="modal-dialog" id="delete-unadopted-modal-{{$dirI}}">
|
||||
<div class="header">
|
||||
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}</p>
|
||||
</div>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted">
|
||||
<input type="hidden" name="id" value="{{$dir}}">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="q" value="{{$.Keyword}}">
|
||||
<input type="hidden" name="page" value="{{$.CurrentPage}}">
|
||||
{{template "base/modal_actions_confirm"}}
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
{{else}}
|
||||
<div class="item">
|
||||
{{ctx.Locale.Tr "admin.repos.unadopted.no_more"}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "admin/layout_footer" .}}
|
||||
128
templates/swagger/v1-openapi3.generated.json
generated
128
templates/swagger/v1-openapi3.generated.json
generated
@@ -1375,19 +1375,6 @@
|
||||
},
|
||||
"description": "StopWatchList"
|
||||
},
|
||||
"StringSlice": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "StringSlice"
|
||||
},
|
||||
"Tag": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -12064,121 +12051,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/unadopted": {
|
||||
"get": {
|
||||
"operationId": "adminUnadoptedList",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "page number of results to return (1-based)",
|
||||
"in": "query",
|
||||
"name": "page",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "page size of results",
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "glob pattern of repositories to search for, in the form owner or owner/repo",
|
||||
"in": "query",
|
||||
"name": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/StringSlice"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/error"
|
||||
}
|
||||
},
|
||||
"summary": "List unadopted repositories",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/unadopted/{owner}/{repo}": {
|
||||
"delete": {
|
||||
"operationId": "adminDeleteUnadoptedRepository",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "owner of the repo",
|
||||
"in": "path",
|
||||
"name": "owner",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "name of the repo",
|
||||
"in": "path",
|
||||
"name": "repo",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/components/responses/empty"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/error"
|
||||
}
|
||||
},
|
||||
"summary": "Delete unadopted files",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "adminAdoptRepository",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "owner of the repo",
|
||||
"in": "path",
|
||||
"name": "owner",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "name of the repo",
|
||||
"in": "path",
|
||||
"name": "repo",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/components/responses/empty"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/error"
|
||||
}
|
||||
},
|
||||
"summary": "Adopt unadopted files as a repository",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/users": {
|
||||
"get": {
|
||||
"operationId": "adminSearchUsers",
|
||||
|
||||
119
templates/swagger/v1-swagger.generated.json
generated
119
templates/swagger/v1-swagger.generated.json
generated
@@ -676,116 +676,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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": "glob pattern of repositories to search for, in the form owner or owner/repo",
|
||||
"name": "query",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/StringSlice"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/users": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -34666,15 +34556,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringSlice": {
|
||||
"description": "StringSlice",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tag": {
|
||||
"description": "Tag",
|
||||
"schema": {
|
||||
|
||||
@@ -4,112 +4,37 @@
|
||||
{{ctx.Locale.Tr "settings.repos"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if or .allowAdopt .allowDelete}}
|
||||
{{if .Dirs}}
|
||||
<div class="ui list">
|
||||
{{range $dirI, $dir := .Dirs}}
|
||||
{{$repo := index $.ReposMap $dir}}
|
||||
<div class="item {{if not $repo}}tw-py-1{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}}
|
||||
<div class="content">
|
||||
{{if $repo}}
|
||||
{{if $repo.IsPrivate}}
|
||||
<span class="tw-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="muted name" href="{{$repo.Link}}">{{$repo.OwnerName}}/{{$repo.Name}}</a>
|
||||
<span class="tw-text-text-light-3" {{if not (eq $repo.Size 0)}} data-tooltip-content="{{$repo.SizeDetailsString}}"{{end}}>{{FormatByteSize $repo.Size}}</span>
|
||||
{{if $repo.IsFork}}
|
||||
{{ctx.Locale.Tr "repo.forked_from"}}
|
||||
<span><a href="{{$repo.BaseRepo.Link}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<span class="icon tw-inline-block tw-pt-2">{{svg "octicon-file-directory-fill"}}</span>
|
||||
<span class="name tw-inline-block tw-pt-2">{{$.ContextUser.Name}}/{{$dir}}</span>
|
||||
<div class="tw-float-right">
|
||||
{{if $.allowAdopt}}
|
||||
<button type="button" class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
|
||||
<dialog class="modal-dialog" id="adopt-unadopted-modal-{{$dirI}}">
|
||||
<div class="header">
|
||||
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}</p>
|
||||
</div>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/user/settings/repos/unadopted">
|
||||
<input type="hidden" name="id" value="{{$dir}}">
|
||||
<input type="hidden" name="action" value="adopt">
|
||||
{{template "base/modal_actions_confirm" $}}
|
||||
</form>
|
||||
</dialog>
|
||||
{{end}}
|
||||
{{if $.allowDelete}}
|
||||
<button type="button" class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button>
|
||||
<dialog class="modal-dialog" id="delete-unadopted-modal-{{$dirI}}">
|
||||
<div class="header">
|
||||
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}</p>
|
||||
</div>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/user/settings/repos/unadopted">
|
||||
<input type="hidden" name="id" value="{{$dir}}">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
{{template "base/modal_actions_confirm" $}}
|
||||
</form>
|
||||
</dialog>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .Repos}}
|
||||
<div class="ui list">
|
||||
{{range .Repos}}
|
||||
<div class="item">
|
||||
<div class="content flex-text-block">
|
||||
{{if .IsPrivate}}
|
||||
{{svg "octicon-lock" 16 "tw-text-gold"}}
|
||||
{{else if .IsFork}}
|
||||
{{svg "octicon-repo-forked"}}
|
||||
{{else if .IsMirror}}
|
||||
{{svg "octicon-mirror"}}
|
||||
{{else if .IsTemplate}}
|
||||
{{svg "octicon-repo-template"}}
|
||||
{{else}}
|
||||
{{svg "octicon-repo"}}
|
||||
{{end}}
|
||||
<a class="name" href="{{.Link}}">{{.OwnerName}}/{{.Name}}</a>
|
||||
<span>{{FormatByteSize .Size}}</span>
|
||||
{{if .IsFork}}
|
||||
{{ctx.Locale.Tr "repo.forked_from"}}
|
||||
<span><a href="{{.BaseRepo.Link}}">{{.BaseRepo.OwnerName}}/{{.BaseRepo.Name}}</a></span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
{{else}}
|
||||
<div class="item">
|
||||
{{ctx.Locale.Tr "settings.repos_none"}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
{{else}}
|
||||
{{if .Repos}}
|
||||
<div class="ui list">
|
||||
{{range .Repos}}
|
||||
<div class="item">
|
||||
<div class="content flex-text-block">
|
||||
{{if .IsPrivate}}
|
||||
{{svg "octicon-lock" 16 "tw-text-gold"}}
|
||||
{{else if .IsFork}}
|
||||
{{svg "octicon-repo-forked"}}
|
||||
{{else if .IsMirror}}
|
||||
{{svg "octicon-mirror"}}
|
||||
{{else if .IsTemplate}}
|
||||
{{svg "octicon-repo-template"}}
|
||||
{{else}}
|
||||
{{svg "octicon-repo"}}
|
||||
{{end}}
|
||||
<a class="name" href="{{.Link}}">{{.OwnerName}}/{{.Name}}</a>
|
||||
<span>{{FormatByteSize .Size}}</span>
|
||||
{{if .IsFork}}
|
||||
{{ctx.Locale.Tr "repo.forked_from"}}
|
||||
<span><a href="{{.BaseRepo.Link}}">{{.BaseRepo.OwnerName}}/{{.BaseRepo.Name}}</a></span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
{{else}}
|
||||
<div class="item">
|
||||
{{ctx.Locale.Tr "settings.repos_none"}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="item">
|
||||
{{ctx.Locale.Tr "settings.repos_none"}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,9 +49,11 @@ func TestAPIIssuesMilestone(t *testing.T) {
|
||||
apiMilestone2 := DecodeJSON(t, resp, &structs.Milestone{})
|
||||
assert.EqualValues(t, "closed", apiMilestone2.State)
|
||||
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, map[string]any{"due_on": ""}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Nil(t, DecodeJSON(t, resp, &structs.Milestone{}).Deadline)
|
||||
for _, dueOn := range []any{"", nil} {
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, map[string]any{"due_on": dueOn}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Nil(t, DecodeJSON(t, resp, &structs.Milestone{}).Deadline)
|
||||
}
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/milestones", owner.Name, repo.Name), structs.CreateMilestoneOption{
|
||||
Title: "wow",
|
||||
|
||||
@@ -180,7 +180,7 @@ export function sleep(ms: number): Promise<void> {
|
||||
}
|
||||
|
||||
export function isImageFile({name, type}: {name: string | null, type: string | null}): boolean {
|
||||
return Boolean(/\.(avif|jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/'));
|
||||
return Boolean(/\.(avif|jpeg|jpg|jxl|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/'));
|
||||
}
|
||||
|
||||
export function isVideoFile({name, type}: {name: string | null, type: string | null}): boolean {
|
||||
|
||||
Reference in New Issue
Block a user