mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-19 16:25:05 +09:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b5c721971 | |||
| 2c7a442692 | |||
| e277be9e12 | |||
| 324be9a731 | |||
| e8e8bbdbb5 | |||
| bc11b8ac22 | |||
| a127bc1013 | |||
| c51da382d1 | |||
| e3d9b03f84 | |||
| f0b4a4142b |
@@ -1027,7 +1027,7 @@ LEVEL = Info
|
||||
;; If the charsets have equal confidence, tie-breaking will be done by order in this list
|
||||
;; with charsets earlier in the list chosen in preference to those later.
|
||||
;; Adding "defaults" will place the unused charsets at that position.
|
||||
;DETECTED_CHARSETS_ORDER = UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, UTF-32LE, ISO-8859, windows-1252, ISO-8859, windows-1250, ISO-8859, ISO-8859, ISO-8859, windows-1253, ISO-8859, windows-1255, ISO-8859, windows-1251, windows-1256, KOI8-R, ISO-8859, windows-1254, Shift_JIS, GB18030, EUC-JP, EUC-KR, Big5, ISO-2022, ISO-2022, ISO-2022, IBM424_rtl, IBM424_ltr, IBM420_rtl, IBM420_ltr
|
||||
;DETECTED_CHARSETS_ORDER = UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, UTF-32LE, ISO-8859, windows-1252, ISO-8859, windows-1250, ISO-8859, ISO-8859, ISO-8859, windows-1253, ISO-8859, windows-1255, ISO-8859, windows-1251, windows-1256, KOI8-R, ISO-8859, windows-1254, Shift_JIS, GB18030, EUC-JP, EUC-KR, Big5, ISO-2022, ISO-2022, ISO-2022
|
||||
;;
|
||||
;; Default ANSI charset to override non-UTF-8 charsets to
|
||||
;ANSI_CHARSET =
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
|
||||
"github.com/gogs/chardet"
|
||||
"golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/unicode/utf32"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
@@ -28,12 +30,26 @@ var globalVars = sync.OnceValue(func() (ret struct {
|
||||
invisibleRangeTable *unicode.RangeTable
|
||||
},
|
||||
) {
|
||||
ret.utf8Bom = []byte{'\xef', '\xbb', '\xbf'}
|
||||
ret.utf8Bom = []byte("\xef\xbb\xbf")
|
||||
ret.ambiguousTableMap = newAmbiguousTableMap()
|
||||
ret.invisibleRangeTable = newInvisibleRangeTable()
|
||||
return ret
|
||||
})
|
||||
|
||||
func Lookup(label string) (e encoding.Encoding, name string) {
|
||||
e, name = charset.Lookup(label)
|
||||
if e != nil {
|
||||
return e, name
|
||||
}
|
||||
switch {
|
||||
case strings.EqualFold(label, "UTF-32BE"):
|
||||
return utf32.UTF32(utf32.BigEndian, utf32.IgnoreBOM), "UTF-32BE"
|
||||
case strings.EqualFold(label, "UTF-32LE"):
|
||||
return utf32.UTF32(utf32.LittleEndian, utf32.IgnoreBOM), "UTF-32LE"
|
||||
}
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
type ConvertOpts struct {
|
||||
KeepBOM bool
|
||||
ErrorReplacement []byte
|
||||
@@ -57,7 +73,7 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
||||
return io.MultiReader(bytes.NewReader(maybeRemoveBOM(buf[:n], opts)), rd)
|
||||
}
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
encoding, _ := Lookup(charsetLabel)
|
||||
if encoding == nil {
|
||||
// unknown charset, don't do any processing
|
||||
return io.MultiReader(bytes.NewReader(buf[:n]), rd)
|
||||
@@ -86,7 +102,7 @@ func ToUTF8(content []byte, opts ConvertOpts) []byte {
|
||||
return maybeRemoveBOM(content, opts)
|
||||
}
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
encoding, _ := Lookup(charsetLabel)
|
||||
if encoding == nil {
|
||||
setting.PanicInDevOrTesting("unsupported detected charset %q, it shouldn't happen", charsetLabel)
|
||||
if opts.ErrorReturnOrigin {
|
||||
|
||||
@@ -245,3 +245,10 @@ func TestToUTF8WithFallbackReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetectedCharsetsOrder(t *testing.T) {
|
||||
for _, charsetName := range setting.DefaultDetectedCharsetsOrder() {
|
||||
e, _ := Lookup(charsetName)
|
||||
assert.NotNil(t, e, "charset %s is not registered", charsetName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,11 +171,15 @@ func MakeRepositoryWebLink(repoURL *RepositoryURL) string {
|
||||
case "http", "https":
|
||||
return strings.TrimSuffix(repoURL.GitURL.String(), ".git")
|
||||
case "ssh", "git+ssh":
|
||||
hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host)
|
||||
hostname = util.IfZero(hostname, repoURL.GitURL.Host)
|
||||
// only get the hostname part (with IPv6 square brackets)
|
||||
host, _, _ := net.SplitHostPort(repoURL.GitURL.Host)
|
||||
host = strings.TrimSuffix(net.JoinHostPort(host, "dummy-port"), ":dummy-port")
|
||||
// if failed to parse the host, use it as is
|
||||
host = util.IfZero(host, repoURL.GitURL.Host)
|
||||
|
||||
urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git")
|
||||
urlPath = strings.TrimPrefix(urlPath, "/")
|
||||
urlFull := fmt.Sprintf("https://%s/%s", hostname, urlPath)
|
||||
urlFull := fmt.Sprintf("https://%s/%s", host, urlPath)
|
||||
urlFull = strings.TrimSuffix(urlFull, "/")
|
||||
return urlFull
|
||||
}
|
||||
|
||||
@@ -264,4 +264,12 @@ func TestMakeRepositoryBaseLink(t *testing.T) {
|
||||
u, err = ParseRepositoryURL(t.Context(), "git+ssh://other:123/owner/repo.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://other/owner/repo", MakeRepositoryWebLink(u))
|
||||
|
||||
u, err = ParseRepositoryURL(t.Context(), "git+ssh://[::1]/owner/repo.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://[::1]/owner/repo", MakeRepositoryWebLink(u))
|
||||
|
||||
u, err = ParseRepositoryURL(t.Context(), "git+ssh://[::1]:2222/owner/repo.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://[::1]/owner/repo", MakeRepositoryWebLink(u))
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
"mi", "mn", "mo", "mtext", "mspace", "ms",
|
||||
// layout elements
|
||||
"mrow", "mfrac", "msqrt", "mroot", "mstyle", "merror", "mpadded", "mphantom",
|
||||
"maction", // although MDN says "maction" is deprecated, we still need to allow it, otherwise, if it is removed, the layout will be wrong
|
||||
// scripting elements
|
||||
"msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts", "mprescripts", "none",
|
||||
// tabular elements
|
||||
@@ -77,10 +78,11 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
// semantic annotations
|
||||
"semantics", "annotation", "annotation-xml",
|
||||
}
|
||||
policy.AllowNoAttrs().OnElements(mathMLElements...) // most MathML elements carry no attributes
|
||||
policy.AllowAttrs("display", "alttext").OnElements("math")
|
||||
policy.AllowAttrs(
|
||||
// global presentation attributes
|
||||
"dir", "displaystyle", "mathbackground", "mathcolor", "mathsize", "mathvariant", "scriptlevel",
|
||||
// global attributes
|
||||
"dir", "displaystyle", "mathbackground", "mathcolor", "mathsize", "mathvariant", "scriptlevel", "intent", "arg",
|
||||
// operator attributes
|
||||
"accent", "accentunder", "fence", "form", "largeop", "lspace", "maxsize", "minsize", "movablelimits", "rspace", "separator", "stretchy", "symmetric",
|
||||
// space and padding attributes
|
||||
@@ -90,7 +92,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
// table attributes
|
||||
"columnalign", "columnlines", "columnspacing", "frame", "framespacing", "rowalign", "rowlines", "rowspacing",
|
||||
// cell attributes
|
||||
"columnspan",
|
||||
"columnspan", "rowspan",
|
||||
// maction attributes
|
||||
"actiontype", "selection",
|
||||
// annotation attribute
|
||||
"encoding",
|
||||
).OnElements(mathMLElements...)
|
||||
|
||||
@@ -62,7 +62,10 @@ func TestSanitizer(t *testing.T) {
|
||||
`<picture><source media="a"><source media="b"><img alt="c" src="d"></picture>`, `<picture><source media="a"><source media="b"><img alt="c" src="d"></picture>`,
|
||||
|
||||
// MathML
|
||||
`<math display="display" class="foo"><mi mathcolor="c" class="bar"></mi></math>`, `<math display="display"><mi mathcolor="c"></mi></math>`,
|
||||
`<math display="block" class="foo"><mi mathcolor="c" class="bar"></mi></math>`, `<math display="block"><mi mathcolor="c"></mi></math>`,
|
||||
`<math><mfrac><mrow><mi>x</mi><mo>+</mo><mn>1</mn></mrow><msqrt><mn>2</mn></msqrt></mfrac></math>`, `<math><mfrac><mrow><mi>x</mi><mo>+</mo><mn>1</mn></mrow><msqrt><mn>2</mn></msqrt></mfrac></math>`,
|
||||
`<math><mtable><mtr><mtd rowspan="2" columnspan="2"><mn>1</mn></mtd></mtr></mtable></math>`, `<math><mtable><mtr><mtd rowspan="2" columnspan="2"><mn>1</mn></mtd></mtr></mtable></math>`,
|
||||
`<math><maction actiontype="toggle" selection="2"><mi intent="power($b,$e)" arg="b">x</mi><mn>2</mn></maction></math>`, `<math><maction actiontype="toggle" selection="2"><mi intent="power($b,$e)" arg="b">x</mi><mn>2</mn></maction></math>`,
|
||||
|
||||
// Disallow dangerous url schemes
|
||||
`<a href="javascript:alert('xss')">bad</a>`, `bad`,
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
@@ -47,6 +50,17 @@ func (s *ContentStore) Has(key BlobHash256Key) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ContentStore) OptionalSize(key BlobHash256Key) (sz optional.Option[int64], _ error) {
|
||||
st, err := s.store.Stat(KeyToRelativePath(key))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return sz, nil
|
||||
}
|
||||
if err != nil {
|
||||
return sz, err
|
||||
}
|
||||
return optional.Some(st.Size()), nil
|
||||
}
|
||||
|
||||
// Save stores a package blob
|
||||
func (s *ContentStore) Save(key BlobHash256Key, r io.Reader, size int64) error {
|
||||
_, err := s.store.Save(KeyToRelativePath(key), r, size)
|
||||
|
||||
@@ -126,41 +126,7 @@ var (
|
||||
TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
|
||||
} `ini:"repository.signing"`
|
||||
}{
|
||||
DetectedCharsetsOrder: []string{
|
||||
"UTF-8",
|
||||
"UTF-16BE",
|
||||
"UTF-16LE",
|
||||
"UTF-32BE",
|
||||
"UTF-32LE",
|
||||
"ISO-8859-1",
|
||||
"windows-1252",
|
||||
"ISO-8859-2",
|
||||
"windows-1250",
|
||||
"ISO-8859-5",
|
||||
"ISO-8859-6",
|
||||
"ISO-8859-7",
|
||||
"windows-1253",
|
||||
"ISO-8859-8-I",
|
||||
"windows-1255",
|
||||
"ISO-8859-8",
|
||||
"windows-1251",
|
||||
"windows-1256",
|
||||
"KOI8-R",
|
||||
"ISO-8859-9",
|
||||
"windows-1254",
|
||||
"Shift_JIS",
|
||||
"GB18030",
|
||||
"EUC-JP",
|
||||
"EUC-KR",
|
||||
"Big5",
|
||||
"ISO-2022-JP",
|
||||
"ISO-2022-KR",
|
||||
"ISO-2022-CN",
|
||||
"IBM424_rtl",
|
||||
"IBM424_ltr",
|
||||
"IBM420_rtl",
|
||||
"IBM420_ltr",
|
||||
},
|
||||
DetectedCharsetsOrder: DefaultDetectedCharsetsOrder(),
|
||||
DetectedCharsetScore: map[string]int{},
|
||||
AnsiCharset: "",
|
||||
ForcePrivate: false,
|
||||
@@ -294,6 +260,40 @@ var (
|
||||
ScriptType = "bash"
|
||||
)
|
||||
|
||||
func DefaultDetectedCharsetsOrder() []string {
|
||||
return []string{
|
||||
"UTF-8",
|
||||
"UTF-16BE",
|
||||
"UTF-16LE",
|
||||
"UTF-32BE",
|
||||
"UTF-32LE",
|
||||
"ISO-8859-1",
|
||||
"windows-1252",
|
||||
"ISO-8859-2",
|
||||
"windows-1250",
|
||||
"ISO-8859-5",
|
||||
"ISO-8859-6",
|
||||
"ISO-8859-7",
|
||||
"windows-1253",
|
||||
"ISO-8859-8-I",
|
||||
"windows-1255",
|
||||
"ISO-8859-8",
|
||||
"windows-1251",
|
||||
"windows-1256",
|
||||
"KOI8-R",
|
||||
"ISO-8859-9",
|
||||
"windows-1254",
|
||||
"Shift_JIS",
|
||||
"GB18030",
|
||||
"EUC-JP",
|
||||
"EUC-KR",
|
||||
"Big5",
|
||||
"ISO-2022-JP",
|
||||
"ISO-2022-KR",
|
||||
"ISO-2022-CN",
|
||||
}
|
||||
}
|
||||
|
||||
func loadRepositoryFrom(rootCfg ConfigProvider) {
|
||||
var err error
|
||||
// Determine and create root git repository path.
|
||||
|
||||
@@ -256,5 +256,5 @@ func PanicInDevOrTesting(msg string, a ...any) {
|
||||
if !IsProd || IsInTesting {
|
||||
panic(fmt.Sprintf(msg, a...))
|
||||
}
|
||||
log.Error(msg, a...)
|
||||
log.ErrorWithSkip(1, msg, a...)
|
||||
}
|
||||
|
||||
@@ -1514,10 +1514,10 @@ func Routes() *web.Router {
|
||||
m.Get("/signing-key.pub", misc.SigningKeySSH)
|
||||
m.Group("/topics", func() {
|
||||
m.Combo("").Get(repo.ListTopics).
|
||||
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
|
||||
Put(reqToken(), reqAdmin(), mustNotBeArchived, bind(api.RepoTopicOptions{}), repo.UpdateTopics)
|
||||
m.Group("/{topic}", func() {
|
||||
m.Combo("").Put(reqToken(), repo.AddTopic).
|
||||
Delete(reqToken(), repo.DeleteTopic)
|
||||
m.Combo("").Put(reqToken(), mustNotBeArchived, repo.AddTopic).
|
||||
Delete(reqToken(), mustNotBeArchived, repo.DeleteTopic)
|
||||
}, reqAdmin())
|
||||
}, reqAnyRepoReader())
|
||||
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||
|
||||
@@ -1617,7 +1617,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
|
||||
limit = max(limit, 0)
|
||||
|
||||
apiFiles := make([]*api.ChangedFile, 0, limit)
|
||||
for i := start; i < start+limit; i++ {
|
||||
for i := start; i < start+limit && i < len(diff.Files); i++ {
|
||||
// refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository.
|
||||
// The head repository might have been deleted, so we should not rely on it here.
|
||||
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
|
||||
|
||||
@@ -100,6 +100,8 @@ func UpdateTopics(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/invalidTopicsError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.RepoTopicOptions)
|
||||
topicNames := form.Topics
|
||||
@@ -161,6 +163,8 @@ func AddTopic(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/invalidTopicsError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))
|
||||
|
||||
@@ -228,6 +232,8 @@ func DeleteTopic(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/invalidTopicsError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ func Install(ctx *context.Context) {
|
||||
}
|
||||
form.RegisterConfirm = setting.Service.RegisterEmailConfirm
|
||||
form.MailNotify = setting.Service.EnableNotifyMail
|
||||
form.EnableUpdateChecker = setting.CfgProvider.Section("cron.update_checker").Key("ENABLED").MustBool(true)
|
||||
|
||||
form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
|
||||
form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
|
||||
|
||||
@@ -594,7 +594,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxDeleteBranch(ctx *context.Cont
|
||||
isPullBranchDeletable, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
|
||||
}
|
||||
|
||||
if isPullBranchDeletable && pull.HasMerged {
|
||||
if isPullBranchDeletable && prInfo.issue.IsClosed {
|
||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
|
||||
|
||||
@@ -360,6 +360,7 @@ func (prInfo *pullRequestViewInfo) prepareViewInfo(ctx *context.Context, issue *
|
||||
ctx.Data["BaseBranch"] = issue.PullRequest.BaseBranch
|
||||
ctx.Data["HeadBranch"] = issue.PullRequest.HeadBranch
|
||||
ctx.Data["HeadUserName"] = issue.PullRequest.MustHeadUserName(ctx)
|
||||
ctx.Data["BaseName"] = issue.PullRequest.BaseRepo.OwnerName
|
||||
|
||||
if issue.PullRequest.HasMerged {
|
||||
prInfo.prepareViewMergedPullInfo(ctx)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/htmlutil"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/public"
|
||||
"gitea.dev/modules/reqctx"
|
||||
@@ -139,5 +138,5 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
|
||||
if csp == "" {
|
||||
return ""
|
||||
}
|
||||
return htmlutil.HTMLFormat(`<meta http-equiv="Content-Security-Policy" content="%s">`, csp)
|
||||
return template.HTML(`<meta http-equiv="Content-Security-Policy" content="` + csp + `">`)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ import (
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
stdcharset "golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
@@ -889,7 +888,7 @@ parsingLoop:
|
||||
}
|
||||
charsetLabel, _ := charset.DetectEncoding(buffer.Bytes())
|
||||
if charsetLabel != "UTF-8" {
|
||||
charsetEncoding, _ := stdcharset.Lookup(charsetLabel)
|
||||
charsetEncoding, _ := charset.Lookup(charsetLabel)
|
||||
if charsetEncoding != nil {
|
||||
diffLineTypeDecoders[lineType] = charsetEncoding.NewDecoder()
|
||||
}
|
||||
|
||||
@@ -262,6 +262,42 @@ func NewPackageBlob(hsr packages_module.HashedSizeReader) *packages_model.Packag
|
||||
}
|
||||
}
|
||||
|
||||
func GetOrSavePackageBlob(ctx context.Context, contentStore *packages_module.ContentStore, blob *packages_model.PackageBlob, data packages_module.HashedSizeReader) (_ *packages_model.PackageBlob, _ bool, retErr error) {
|
||||
if blob.Size != data.Size() {
|
||||
return nil, false, fmt.Errorf("size mismatch: blob size %d, data size %d", blob.Size, data.Size())
|
||||
}
|
||||
pb, existsInDatabase, err := packages_model.GetOrInsertBlob(ctx, blob)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("unable to get or insert blob: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr != nil && !existsInDatabase {
|
||||
if errDelete := packages_model.DeleteBlobByID(ctx, pb.ID); errDelete != nil {
|
||||
log.Error("unable to delete blob from database after failed save in content store: %v", errDelete)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var objSize optional.Option[int64]
|
||||
storeKey := packages_module.BlobHash256Key(pb.HashSHA256)
|
||||
if existsInDatabase {
|
||||
// check if the blob file actually is valid in the content store
|
||||
objSize, err = contentStore.OptionalSize(storeKey)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("unable to check object size in content store: %w", err)
|
||||
}
|
||||
}
|
||||
if objSize.ValueOrDefault(-1) != blob.Size {
|
||||
if err := contentStore.Save(storeKey, data, data.Size()); err != nil {
|
||||
return nil, false, fmt.Errorf("unable to save object in content store: %w", err)
|
||||
}
|
||||
}
|
||||
// existsInDatabase controls the "roll back", if other errors happen later,
|
||||
// the "non-existing (newly created)" blob will be deleted from the content store, but not if it already existed in the database.
|
||||
return pb, existsInDatabase, nil
|
||||
}
|
||||
|
||||
func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
if err := CheckSizeQuotaExceeded(ctx, pfci.Creator, pvi.Owner, pvi.PackageType, pfci.Data.Size()); err != nil {
|
||||
return nil, nil, false, err
|
||||
@@ -273,18 +309,11 @@ func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVers
|
||||
func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename)
|
||||
|
||||
pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data))
|
||||
pb, exists, err := GetOrSavePackageBlob(ctx, packages_module.NewContentStore(), NewPackageBlob(pfci.Data), pfci.Data)
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
return nil, nil, false, err
|
||||
}
|
||||
if !exists {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), pfci.Data, pfci.Data.Size()); err != nil {
|
||||
log.Error("Error saving package blob in content store: %v", err)
|
||||
return nil, nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
if pfci.OverwriteExisting {
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfci.Filename, pfci.CompositeKey)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
|
||||
func TestCreatePackageAndAddFileRestoresMissingBlobFile(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
uploadPackage := func(t *testing.T, user *user_model.User, name, filename string, data []byte) (*packages_model.PackageFile, error) {
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
_, pf, err := CreatePackageAndAddFile(t.Context(),
|
||||
&PackageCreationInfo{
|
||||
PackageInfo: PackageInfo{
|
||||
Owner: user,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: name,
|
||||
Version: "1.0.0",
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: user,
|
||||
},
|
||||
&PackageFileCreationInfo{
|
||||
PackageFileInfo: PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
Creator: user,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
})
|
||||
return pf, err
|
||||
}
|
||||
|
||||
// This test data is from https://github.com/go-gitea/gitea/issues/39215, it doesn't really matter, actually.
|
||||
// The key point is that if the blob object is missing in the content storage, it must be restored when uploaded again.
|
||||
pkgData := test.WriteZipArchive(map[string]string{
|
||||
"package.nuspec": "<package><metadata><id>nuget.repro</id><version>1.0.0</version></metadata></package>",
|
||||
"lib/netstandard2.0/_._": "",
|
||||
}).Bytes()
|
||||
pkgDataSum := sha256.Sum256(pkgData)
|
||||
key := packages_module.BlobHash256Key(hex.EncodeToString(pkgDataSum[:]))
|
||||
contentStore := packages_module.NewContentStore()
|
||||
|
||||
// The initial upload writes the blob row and its file
|
||||
pf1, err := uploadPackage(t, user, "nuget.repro", "nuget.repro.1.0.0.nupkg", pkgData)
|
||||
require.NoError(t, err)
|
||||
sz, err := contentStore.OptionalSize(key)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, len(pkgData), sz.ValueOrDefault(-1))
|
||||
|
||||
// Simulate the storage inconsistency: the blob row survives but its file is missing
|
||||
require.NoError(t, contentStore.Delete(key))
|
||||
sz, err = contentStore.OptionalSize(key)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, -1, sz.ValueOrDefault(-1))
|
||||
|
||||
// Publishing a package with identical content must restore the blob file
|
||||
pf2, err := uploadPackage(t, user, "nuget.repro-copy", "nuget.repro-copy.1.0.0.nupkg", pkgData)
|
||||
require.NoError(t, err)
|
||||
sz, err = contentStore.OptionalSize(key)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, len(pkgData), sz.ValueOrDefault(-1))
|
||||
|
||||
// The blob file must be present and both packages must be downloadable
|
||||
for _, pf := range []*packages_model.PackageFile{pf1, pf2} {
|
||||
s, _, _, err := OpenFileForDownload(t.Context(), pf, http.MethodGet)
|
||||
require.NoError(t, err)
|
||||
respData, err := io.ReadAll(s)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, s.Close())
|
||||
assert.Equal(t, pkgData, respData)
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func isErrBlameNotFoundOrNotEnoughLines(err error) bool {
|
||||
return false
|
||||
}
|
||||
notFound := strings.HasPrefix(stdErr, "fatal: no such path")
|
||||
notEnoughLines := strings.HasPrefix(stdErr, "fatal: file ") && strings.Contains(stdErr, " has only ") && strings.Contains(stdErr, " lines?")
|
||||
notEnoughLines := strings.HasPrefix(stdErr, "fatal: file ") && strings.Contains(stdErr, " has only ") && strings.Contains(stdErr, " line")
|
||||
return notFound || notEnoughLines
|
||||
}
|
||||
|
||||
|
||||
@@ -55,10 +55,12 @@
|
||||
<tr>
|
||||
<td>{{.Version.ID}}</td>
|
||||
<td>
|
||||
{{if .Owner}}
|
||||
<a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>
|
||||
{{if .Owner.Visibility.IsPrivate}}
|
||||
<span class="tw-text-gold">{{svg "octicon-lock"}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{.Package.Type.Name}}</td>
|
||||
<td class="gt-ellipsis tw-max-w-48">{{.Package.Name}}</td>
|
||||
|
||||
@@ -47,10 +47,12 @@
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>
|
||||
{{if .Owner}}
|
||||
<a class="tw-break-anywhere" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>
|
||||
{{if .Owner.Visibility.IsPrivate}}
|
||||
<span class="tw-text-gold">{{svg "octicon-lock"}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<a class="tw-break-anywhere" href="{{.Link}}">{{.Name}}</a>
|
||||
@@ -59,7 +61,7 @@
|
||||
{{end}}
|
||||
{{if .IsPrivate}}
|
||||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
|
||||
{{else}}
|
||||
{{else if .Owner}}
|
||||
{{if .Owner.Visibility.IsPrivate}}
|
||||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.internal"}}</span>
|
||||
{{end}}
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "install.enable_update_checker"}}</label>
|
||||
<input name="enable_update_checker" type="checkbox">
|
||||
<input name="enable_update_checker" type="checkbox" {{if .enable_update_checker}}checked{{end}}>
|
||||
</div>
|
||||
<span class="help">{{ctx.Locale.Tr "install.enable_update_checker_helper"}}</span>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="item-main">
|
||||
<div class="item-title">
|
||||
{{$title := or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}}
|
||||
{{$title := or $run.Title (ctx.Locale.TrString "actions.runs.empty_commit_message")}}
|
||||
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $title $run.Link $.Repository}}
|
||||
</div>
|
||||
<div class="item-body">
|
||||
|
||||
Generated
+9
@@ -18058,6 +18058,9 @@
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/invalidTopicsError"
|
||||
},
|
||||
"423": {
|
||||
"$ref": "#/responses/repoArchivedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18104,6 +18107,9 @@
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/invalidTopicsError"
|
||||
},
|
||||
"423": {
|
||||
"$ref": "#/responses/repoArchivedError"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18148,6 +18154,9 @@
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/invalidTopicsError"
|
||||
},
|
||||
"423": {
|
||||
"$ref": "#/responses/repoArchivedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+9
@@ -30293,6 +30293,9 @@
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/components/responses/invalidTopicsError"
|
||||
},
|
||||
"423": {
|
||||
"$ref": "#/components/responses/repoArchivedError"
|
||||
}
|
||||
},
|
||||
"summary": "Replace list of topics for a repository",
|
||||
@@ -30342,6 +30345,9 @@
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/components/responses/invalidTopicsError"
|
||||
},
|
||||
"423": {
|
||||
"$ref": "#/components/responses/repoArchivedError"
|
||||
}
|
||||
},
|
||||
"summary": "Delete a topic from a repository",
|
||||
@@ -30389,6 +30395,9 @@
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/components/responses/invalidTopicsError"
|
||||
},
|
||||
"423": {
|
||||
"$ref": "#/components/responses/repoArchivedError"
|
||||
}
|
||||
},
|
||||
"summary": "Add a topic to a repository",
|
||||
|
||||
@@ -186,3 +186,30 @@ func TestAPIRepoTopic(t *testing.T) {
|
||||
AddTokenAuth(token4)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPIRepoTopicArchived(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 30}) // owner of the archived repo51
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 51})
|
||||
assert.True(t, repo.IsArchived)
|
||||
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// writing topics on an archived repo must be rejected, matching the web UI
|
||||
req := NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s", user.Name, repo.Name, "archivedtopic").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusLocked)
|
||||
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s", user.Name, repo.Name, "archivedtopic").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusLocked)
|
||||
|
||||
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/topics", user.Name, repo.Name),
|
||||
&api.RepoTopicOptions{Topics: []string{"archivedtopic"}}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusLocked)
|
||||
|
||||
// reading topics stays allowed on an archived repo
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/topics", user.Name, repo.Name)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -9,18 +9,24 @@ import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func assertLinkPageComplete(t *testing.T, session *TestSession, link string) {
|
||||
func assertLinkPageComplete(t *testing.T, session *TestSession, link string, containStrings ...string) {
|
||||
req := NewRequest(t, "GET", link)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()), "Page did not complete: "+link)
|
||||
for _, s := range containStrings {
|
||||
assert.Contains(t, resp.Body.String(), s, "Page does not contain expected string: "+s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinks(t *testing.T) {
|
||||
@@ -179,26 +185,30 @@ func testLinksAsUser(t *testing.T) {
|
||||
func testLinksRepoCommon(t *testing.T) {
|
||||
// repo1 has enabled almost features, so we can test most links
|
||||
repoLink := "/user2/repo1"
|
||||
links := []string{
|
||||
"/actions",
|
||||
"/packages",
|
||||
"/projects",
|
||||
|
||||
err := db.Insert(t.Context(), &actions.ActionRun{Title: "", RepoID: 1})
|
||||
require.NoError(t, err)
|
||||
|
||||
links := map[string][]string{
|
||||
"/actions": {"(empty commit message)"},
|
||||
"/packages": {},
|
||||
"/projects": {},
|
||||
}
|
||||
|
||||
// anonymous user
|
||||
for _, link := range links {
|
||||
assertLinkPageComplete(t, nil, repoLink+link)
|
||||
for link, strs := range links {
|
||||
assertLinkPageComplete(t, nil, repoLink+link, strs...)
|
||||
}
|
||||
|
||||
// admin/owner user
|
||||
session := loginUser(t, "user1")
|
||||
for _, link := range links {
|
||||
assertLinkPageComplete(t, session, repoLink+link)
|
||||
for link, strs := range links {
|
||||
assertLinkPageComplete(t, session, repoLink+link, strs...)
|
||||
}
|
||||
|
||||
// non-admin non-owner user
|
||||
session = loginUser(t, "user2")
|
||||
for _, link := range links {
|
||||
assertLinkPageComplete(t, session, repoLink+link)
|
||||
for link, strs := range links {
|
||||
assertLinkPageComplete(t, session, repoLink+link, strs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,20 +81,6 @@ func testPullMerge(t *testing.T, session *TestSession, user, repo, pullNum strin
|
||||
return resp
|
||||
}
|
||||
|
||||
func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", "/"+path.Join(user, repo, "pulls", pullnum))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Click the little button to create a pull
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
link, exists := htmlDoc.doc.Find(".timeline-item .delete-branch-after-merge").Attr("data-url")
|
||||
assert.True(t, exists, "The template has changed, can not find delete button url")
|
||||
req = NewRequest(t, "POST", link)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func preparePullMergeWebhook(t *testing.T, repoID int64) {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &webhook.Webhook{}, &webhook.HookTask{}))
|
||||
require.NoError(t, db.Insert(t.Context(), &webhook.Webhook{
|
||||
@@ -267,7 +253,7 @@ func TestPullSquashWithHeadCommitID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullCleanUpAfterMerge(t *testing.T) {
|
||||
func TestPullCleanUpAfterClose(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
session := loginUser(t, "user1") // FIXME: don't use admin user for testing
|
||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||
@@ -277,39 +263,60 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
|
||||
assert.Equal(t, 3, repo.NumPulls)
|
||||
assert.Equal(t, 3, repo.NumOpenPulls)
|
||||
|
||||
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
|
||||
getDeleteBranchLink := func(t *testing.T, session *TestSession, user, repo, pullnum string) string {
|
||||
req := NewRequest(t, "GET", "/"+path.Join(user, repo, "pulls", pullnum))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
return htmlDoc.doc.Find(".timeline-item .delete-branch-after-merge").AttrOr("data-url", "")
|
||||
}
|
||||
|
||||
elem := strings.Split(test.RedirectURL(resp), "/")
|
||||
assert.Equal(t, "pulls", elem[3])
|
||||
var closedPullNumStr string
|
||||
t.Run("CreateAndClosePR", func(t *testing.T) {
|
||||
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
|
||||
pullNumStr := path.Base(test.RedirectURL(resp))
|
||||
|
||||
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
|
||||
assert.Equal(t, 4, repo.NumPulls)
|
||||
assert.Equal(t, 4, repo.NumOpenPulls)
|
||||
testIssueClose(t, session, "user2", "repo1", pullNumStr)
|
||||
|
||||
testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{
|
||||
Style: repo_model.MergeStyleMerge,
|
||||
DeleteBranch: false,
|
||||
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
|
||||
assert.Equal(t, 4, repo.NumPulls)
|
||||
assert.Equal(t, 3, repo.NumOpenPulls)
|
||||
|
||||
closedPullNumStr = pullNumStr
|
||||
|
||||
// the closed but unmerged PR should have the "delete branch" button
|
||||
link := getDeleteBranchLink(t, session, "user2", "repo1", closedPullNumStr)
|
||||
assert.NotEmpty(t, link)
|
||||
})
|
||||
|
||||
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
|
||||
assert.Equal(t, 4, repo.NumPulls)
|
||||
assert.Equal(t, 3, repo.NumOpenPulls)
|
||||
t.Run("CreateAndMergePR", func(t *testing.T) {
|
||||
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
|
||||
pullNumStr := path.Base(test.RedirectURL(resp))
|
||||
|
||||
// Check PR branch deletion
|
||||
resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
|
||||
respJSON := test.ParseJSONRedirect(resp.Body.Bytes())
|
||||
require.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
|
||||
// the closed but unmerged PR should not have the "delete branch" button because there is a new PR for the same branch
|
||||
link := getDeleteBranchLink(t, session, "user2", "repo1", closedPullNumStr)
|
||||
assert.Empty(t, link)
|
||||
|
||||
elem = strings.Split(*respJSON.Redirect, "/")
|
||||
assert.Equal(t, "pulls", elem[3])
|
||||
testPullMerge(t, session, "user2", "repo1", pullNumStr, MergeOptions{
|
||||
Style: repo_model.MergeStyleMerge,
|
||||
DeleteBranch: false,
|
||||
})
|
||||
|
||||
// Check branch deletion result
|
||||
req := NewRequest(t, "GET", *respJSON.Redirect)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
// Check PR branch deletion
|
||||
link = getDeleteBranchLink(t, session, "user2", "repo1", pullNumStr)
|
||||
assert.NotEmpty(t, link)
|
||||
resp = session.MakeRequest(t, NewRequest(t, "POST", link), http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text())
|
||||
assert.Equal(t, `Branch "user1/repo1:feature/test" has been deleted.`, resultMsg)
|
||||
// Check branch deletion result
|
||||
req := NewRequest(t, "GET", test.RedirectURL(resp))
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text())
|
||||
assert.Equal(t, `Branch "user1/repo1:feature/test" has been deleted.`, resultMsg)
|
||||
|
||||
// the "delete branch" button should be gone since the PR has been merged
|
||||
link = getDeleteBranchLink(t, session, "user2", "repo1", pullNumStr)
|
||||
assert.Empty(t, link)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
|
||||
const file = {name: attachment.name, uuid: attachment.uuid, size: attachment.size};
|
||||
dzInst.emit('addedfile', file);
|
||||
dzInst.emit('complete', file);
|
||||
if (isImageFile(file.name)) {
|
||||
if (isImageFile(file)) {
|
||||
const imgSrc = `${attachmentBaseLinkUrl}/${file.uuid}`;
|
||||
dzInst.emit('thumbnail', file, imgSrc);
|
||||
}
|
||||
|
||||
@@ -66,9 +66,9 @@ function onModalBeforeHidden(this: any) {
|
||||
function onModalApproveDefault(this: any) {
|
||||
const $modal = $(this);
|
||||
const selectors = $modal.modal('setting', 'selector');
|
||||
const elModal = $modal[0];
|
||||
const elApprove = elModal.querySelector(selectors.approve);
|
||||
const elForm = elApprove?.closest('form');
|
||||
const elModal = $modal[0] as HTMLElement;
|
||||
const elApprove = elModal.querySelector<HTMLElement>(selectors.approve);
|
||||
const elForm = elApprove?.closest<HTMLFormElement>('form');
|
||||
if (!elForm) return true; // no form, just allow closing the modal
|
||||
|
||||
// "form-fetch-action" can handle network errors gracefully,
|
||||
@@ -78,6 +78,7 @@ function onModalApproveDefault(this: any) {
|
||||
// There is an abuse for the "modal" + "form" combination, the "Approve" button is a traditional form submit button in the form.
|
||||
// Then "approve" and "submit" occur at the same time, the modal will be closed immediately before the form is submitted.
|
||||
// So here we prevent the modal from closing automatically by returning false, add the "is-loading" class to the form element.
|
||||
if (!elForm.reportValidity()) return false;
|
||||
elForm.classList.add('is-loading');
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user