Compare commits

...

3 Commits

Author SHA1 Message Date
silverwind
d715a5ae75 test(api): cover null milestone due_on (#40172)
Removing a milestone deadline with `"due_on": null` was untested, only
`""` was. On release/v28 the null case returned HTTP 500
(https://github.com/go-gitea/gitea/pull/40171). main already handles it,
this adds the case to the existing test so it stays that way.
2026-11-08 20:53:59 +00:00
Lunny Xiao
1554b6c939 feat!: remove repository adopt feature (#40181)
Remove the "adopt unadopted repositories" feature. Adopting assumes a
fixed repository storage path on disk, which makes the storage path hard
to change. It is mostly used once for migrations, and the [Gitea
importer](https://gitea.com/gitea/importer) covers that after the
repositories are moved out of the repository root. The docs are updated
in https://gitea.com/gitea/docs/pulls/639.

- Remove the admin and user adopt pages, the `/admin/unadopted` API
endpoints and the adopt notifier hook
- Remove the `ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES` and
`ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES` settings
- The user "Repositories" settings page only lists the user's
repositories
- Repository files without a database record must now be removed from
disk by the administrator

Assisted-by: Codet:claude

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-08 18:38:23 +00:00
wxiaoguang
3699309edd feat: support jpeg-xl (jxl) (#40283)
Major browsers (Chrome/Firefox/Safari) all support it now,
https://developer.chrome.com/blog/jpeg-xl-in-chrome

* Fix #16629
2026-11-08 15:24:46 +00:00
43 changed files with 102 additions and 1521 deletions

View File

@@ -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

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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))
}

View File

@@ -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)}
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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,

View File

@@ -59,8 +59,6 @@ var (
DisableStars bool `ini:"DISABLE_STARS"`
DefaultBranch string
DefaultObjectFormat string
AllowAdoptionOfUnadoptedRepositories bool
AllowDeleteOfUnadoptedRepositories bool
DisableDownloadSourceArchives bool
AllowForkWithoutMaximumLimit bool
AllowForkIntoSameOwner bool

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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):

View File

@@ -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 {

View File

@@ -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))
}

View File

@@ -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):

View File

@@ -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):

View File

@@ -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)

View File

@@ -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:

View File

@@ -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")
}

View File

@@ -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)

View File

@@ -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)
})

View File

@@ -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)

View File

@@ -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))
}
}
}

View File

@@ -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,
) {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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) {
}

View File

@@ -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
}

View File

@@ -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
})
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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" .}}

View File

@@ -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" .}}

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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",

View File

@@ -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 {