Compare commits

...

9 Commits

Author SHA1 Message Date
Giteabot 25416e9be7 fix: trace git command correctly (#39520) (#39524)
Backport #39520 by @wxiaoguang

Help  #39410

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-01 10:19:33 +00:00
Giteabot 1c4fff096f enhance(packages/npm): improve npm client compatibility (#39434) (#39522)
Backport #39434 by @silverwind

Aligns the npm registry with what npm, pnpm and yarn expect:

1. Raise the publish body cap from
https://github.com/go-gitea/gitea/pull/37890 to 256 MiB like npmjs,
larger bodies get 413
2. Pick the tarball attachment by name, `npm publish --provenance`
failed at random
3. Store and serve `libc`, so mismatched glibc/musl optional binaries
are skipped
4. Treat root `*.gyp` files as an install script, like npm does
5. Always serve a `latest` dist-tag, yarn and pnpm fail without it
6. Take top-level metadata from `latest` and drop the per-version readme
7. Serve tarballs at the npmjs path `/<name>/-/<file>`, former URLs keep
working
8. Add ETag revalidation for metadata, `npm ping` and `npm whoami`

Tested with npm 12.1, pnpm 12.4, yarn 1.22 and yarn 4.18.

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-01 08:09:21 +00:00
Giteabot d16daed041 fix: handle git branch name with special chars correctly (#39483) (#39515)
Backport #39483 

Fix the bugs:
* Commit graph page doesn't show
* PR command line instructions are wrong

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-01 14:07:00 +08:00
Giteabot 2d58c8c3df fix: add missing checks to several API and web handlers (#39501) (#39507)
Backport #39501 by @silverwind

Several handlers skipped checks that their sibling routes or settings
already enforce. This brings them in line.

- Push mirror API honors `DISABLE_NEW_PUSH` and checks the caller's
permission
- Media API serves small files with the usual content headers
- Issue attachment API ignores comment attachments
- Push-to-create respects `FORCE_PRIVATE`
- Profile feeds and follow actions respect `ENABLE_FEED` and owner
visibility
- Tag delete route refuses release tags
- Refresh token grant only accepts refresh tokens
- Gitea migrations bound the source's page size

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
2026-09-30 18:46:59 +00:00
Giteabot 1e28bb1bd7 fix(markup): don't escape ambiguous characters in MathML (#39493) (#39505)
Co-authored-by: Nico Schlömer <nschloe@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-30 16:56:23 +00:00
Giteabot f0e8c3c3d0 fix(markup): skip post-processing inside MathML (#39497) (#39502)
Co-authored-by: Nico Schlömer <nschloe@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-30 08:06:27 -07:00
Giteabot 7c58b73243 fix: copy new access token to clipboard (#39496) (#39499)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-09-30 13:45:37 +00:00
Giteabot 6546382f4e fix: npm route (#39488) (#39490)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-30 12:08:14 +00:00
Giteabot 0930bd71fe fix(actions): keep runs order after auto refresh (#39479) (#39481)
Co-authored-by: JerryLien <jerrylien0801@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-30 18:57:19 +08:00
55 changed files with 565 additions and 365 deletions
+2 -3
View File
@@ -295,9 +295,8 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
return &run, nil return &run, nil
} }
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) { func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) (runs []*ActionRun, err error) {
var runs []*ActionRun err = db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).OrderBy("id").Find(&runs)
err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs)
return runs, err return runs, err
} }
+15
View File
@@ -407,6 +407,21 @@ func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefN
return git.RefNameFromPullIndex(pr.Index).String() return git.RefNameFromPullIndex(pr.Index).String()
} }
func (pr *PullRequest) GetInstructionsCliArgs() (ret struct {
BaseBranchArg string
HeadBranchArg string
LocalBranchArg string
},
) {
ret.BaseBranchArg = util.ShellEscape(pr.BaseBranch)
ret.HeadBranchArg = util.ShellEscape(pr.HeadBranch)
ret.LocalBranchArg = ret.HeadBranchArg
if pr.HeadRepo != nil && pr.HeadRepoID != pr.BaseRepoID {
ret.LocalBranchArg = util.ShellEscape(pr.HeadRepo.OwnerName) + "-" + ret.HeadBranchArg
}
return ret
}
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR) // GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int { func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
opts := FindCommentsOptions{ opts := FindCommentsOptions{
+40 -1
View File
@@ -8,11 +8,13 @@ import (
"fmt" "fmt"
"html" "html"
"io" "io"
"strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/translation" "gitea.dev/modules/translation"
"gitea.dev/modules/util"
) )
type htmlChunkReader struct { type htmlChunkReader struct {
@@ -30,6 +32,10 @@ type escapeStreamer struct {
ambiguousTables []*AmbiguousTable ambiguousTables []*AmbiguousTable
allowed map[rune]bool allowed map[rune]bool
tagPartial []byte // partial tag content, used to detect if we are in some tags
inTagMath bool // MathML operators like U+2212 are intended and wrapping them breaks the math layout
out io.Writer out io.Writer
} }
@@ -62,6 +68,7 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
for i, part := range parts { for i, part := range parts {
if partInTag[i] { if partInTag[i] {
lastIsTag = true lastIsTag = true
es.trackHtmlTag(part)
if _, err := out.Write(part); err != nil { if _, err := out.Write(part); err != nil {
return nil, err return nil, err
} }
@@ -75,7 +82,11 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
return nil, err return nil, err
} }
} }
if err = es.detectAndWriteRunes(part); err != nil { if es.inTagMath {
if _, err := out.Write(part); err != nil {
return nil, err
}
} else if err = es.detectAndWriteRunes(part); err != nil {
return nil, err return nil, err
} }
} }
@@ -83,6 +94,34 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
} }
} }
// trackHtmlTag receives tag parts, a tag might be split into multiple parts
func (e *escapeStreamer) trackHtmlTag(part []byte) {
const maxHeadLen = 100 // only read the first N bytes of the tag for detection purpose
if part[0] == '<' {
// start a new tag
e.tagPartial = e.tagPartial[:0]
}
if len(e.tagPartial) >= maxHeadLen {
return
}
e.tagPartial = append(e.tagPartial, part[:min(len(part), maxHeadLen-len(e.tagPartial))]...)
isTag := func(prefix string) bool {
if len(e.tagPartial) < len(prefix)+1 {
return false
}
if !util.AsciiEqualFold(e.tagPartial[:len(prefix)], []byte(prefix)) {
return false
}
return strings.IndexByte(" \t\n\r\f>", e.tagPartial[len(prefix)]) != -1
}
if isTag("<math") {
e.inTagMath = true
} else if isTag("</math") {
e.inTagMath = false
}
}
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) { func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom) remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
if ok { if ok {
+24
View File
@@ -141,6 +141,12 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`, result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
status: EscapeStatus{Escaped: true, HasAmbiguous: true}, status: EscapeStatus{Escaped: true, HasAmbiguous: true},
}, },
{
name: "ambiguous in math",
text: "<math><mo>−</mo><mi>b</mi></math> −",
result: `<math><mo>−</mo><mi>b</mi></math> <span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:− [U+2212],- [U+002D]"><span class="char">−</span></span>`,
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
},
} }
func TestEscapeControlReader(t *testing.T) { func TestEscapeControlReader(t *testing.T) {
@@ -156,6 +162,24 @@ func TestEscapeControlReader(t *testing.T) {
} }
} }
func TestTrackHtmlTag(t *testing.T) {
e := &escapeStreamer{}
for _, tt := range []struct {
parts []string
inMath bool
}{
{[]string{"<ma", `TH display="block">`}, true},
{[]string{"<mo>"}, true},
{[]string{"</MA", "th>"}, false},
{[]string{"<mathx>"}, false},
} {
for _, part := range tt.parts {
e.trackHtmlTag([]byte(part))
}
assert.Equal(t, tt.inMath, e.inTagMath, "%v", tt.parts)
}
}
func TestSettingAmbiguousUnicodeDetection(t *testing.T) { func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)() defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
_, out := EscapeControlHTML("a test", &translation.MockLocale{}) _, out := EscapeControlHTML("a test", &translation.MockLocale{})
+10 -5
View File
@@ -49,8 +49,8 @@ type Command struct {
cmd *process.Cmd cmd *process.Cmd
cmdCtx context.Context cmdCtx context.Context
cmdCancel process.CancelCauseFunc cmdCtxCancel process.CancelCauseFunc
cmdFinished process.FinishedFunc cmdFinished func()
cmdStartTime time.Time cmdStartTime time.Time
pipelineFunc func(Context) error pipelineFunc func(Context) error
@@ -428,19 +428,24 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
if c.callerInfo == "" { if c.callerInfo == "" {
c.WithParentCallerInfo() c.WithParentCallerInfo()
} }
// these logs are for debugging purposes only, so no guarantee of correctness or stability // these logs are for debugging purposes only, so no guarantee of correctness or stability
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString) desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString)
log.Debug("git.Command: %s", desc) log.Debug("git.Command: %s", desc)
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun) _, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
defer span.End()
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo) span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString) span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
var cmdCtxFinished func()
if c.cmdTimeout <= 0 { if c.cmdTimeout <= 0 {
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContext(ctx, desc) c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContext(ctx, desc)
} else { } else {
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc) c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
}
c.cmdFinished = func() {
cmdCtxFinished()
span.End()
} }
c.cmdStartTime = time.Now() c.cmdStartTime = time.Now()
+1 -1
View File
@@ -27,6 +27,6 @@ func (c *cmdContext) CancelPipeline(err error) error {
// * context canceled by pipeline caller with/without error (normal cancellation) // * context canceled by pipeline caller with/without error (normal cancellation)
// * context canceled by parent context (still context.Canceled error) // * context canceled by parent context (still context.Canceled error)
// * other causes // * other causes
c.cmd.cmdCancel(pipelineError{err}) c.cmd.cmdCtxCancel(pipelineError{err})
return err return err
} }
+2
View File
@@ -122,6 +122,8 @@ func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *
ts.parent = parentSpan ts.parent = parentSpan
} }
// FIXME: this ctx handling is not right. The returned ctx should inherit the ctx passed in, but not from span's internal contexts
// The returned ctx only needs to inherit the values of the internal contexts of spans
parentCtx := ctx parentCtx := ctx
for internalSpanIdx, tsp := range starters { for internalSpanIdx, tsp := range starters {
var internalSpan traceSpanInternal var internalSpan traceSpanInternal
+7 -4
View File
@@ -6,14 +6,17 @@ package gtprof
// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv // Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
const ( const (
TraceSpanContext = "context"
TraceSpanHTTP = "http" TraceSpanHTTP = "http"
TraceSpanGitRun = "git-run" TraceSpanGitRun = "git-run"
TraceSpanDatabase = "database" TraceSpanDatabase = "database"
) )
const ( const (
TraceAttrFuncCaller = "func.caller" TraceAttrGeneralName = "general.name"
TraceAttrDbSQL = "db.sql" TraceAttrGeneralDesc = "general.desc"
TraceAttrGitCommand = "git.command" TraceAttrFuncCaller = "func.caller"
TraceAttrHTTPRoute = "http.route" TraceAttrDbSQL = "db.sql"
TraceAttrGitCommand = "git.command"
TraceAttrHTTPRoute = "http.route"
) )
+4
View File
@@ -45,6 +45,10 @@ func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions) return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
} }
func MarshalDeterministic(v any) ([]byte, error) {
return jsonv2.Marshal(v, jsonV2.marshalOptions, jsonv2.Deterministic(true))
}
func (j *JSONv2) Marshal(v any) ([]byte, error) { func (j *JSONv2) Marshal(v any) ([]byte, error) {
return jsonv2.Marshal(v, j.marshalOptions) return jsonv2.Marshal(v, j.marshalOptions)
} }
+2 -2
View File
@@ -349,8 +349,8 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span" // TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
// if we don't stop it, it will go into the TextNode again and create an infinite recursion // if we don't stop it, it will go into the TextNode again and create an infinite recursion
return node.NextSibling return node.NextSibling
} else if node.Data == "code" || node.Data == "pre" { } else if node.Data == "code" || node.Data == "pre" || node.Data == "math" {
return node.NextSibling // ignore code and pre nodes return node.NextSibling // ignore code, pre and math nodes
} else if node.Data == "img" { } else if node.Data == "img" {
return visitNodeImg(ctx, node) return visitNodeImg(ctx, node)
} else if node.Data == "video" { } else if node.Data == "video" {
+3
View File
@@ -543,6 +543,9 @@ func TestPostProcess(t *testing.T) {
`Some text with <span class="emoji" data-alias="smile">😄</span> in the middle`) `Some text with <span class="emoji" data-alias="smile">😄</span> in the middle`)
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234", test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`) `<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
test(
"<math><mtext>:gitea: go-gitea/gitea#12345</mtext></math>",
"<math><mtext>:gitea: go-gitea/gitea#12345</mtext></math>")
// special tags, GitHub's behavior, and for unclosed tags, output as text content as much as possible // special tags, GitHub's behavior, and for unclosed tags, output as text content as much as possible
test("<script>a", `&lt;script&gt;a`) test("<script>a", `&lt;script&gt;a`)
+23 -51
View File
@@ -121,6 +121,7 @@ type PackageMetadataVersion struct {
Engines map[string]string `json:"engines,omitempty"` Engines map[string]string `json:"engines,omitempty"`
CPU []string `json:"cpu,omitempty"` CPU []string `json:"cpu,omitempty"`
OS []string `json:"os,omitempty"` OS []string `json:"os,omitempty"`
Libc []string `json:"libc,omitempty"`
Directories map[string]string `json:"directories,omitempty"` Directories map[string]string `json:"directories,omitempty"`
Funding any `json:"funding,omitempty"` Funding any `json:"funding,omitempty"`
AcceptDependencies map[string]string `json:"acceptDependencies,omitempty"` AcceptDependencies map[string]string `json:"acceptDependencies,omitempty"`
@@ -129,12 +130,9 @@ type PackageMetadataVersion struct {
// PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version // PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
type PackageDistribution struct { type PackageDistribution struct {
Integrity string `json:"integrity"` Integrity string `json:"integrity"`
Shasum string `json:"shasum"` Shasum string `json:"shasum"`
Tarball string `json:"tarball"` Tarball string `json:"tarball"`
FileCount int `json:"fileCount,omitempty"`
UnpackedSize int `json:"unpackedSize,omitempty"`
NpmSignature string `json:"npm-signature,omitempty"`
} }
type PackageSearch struct { type PackageSearch struct {
@@ -226,7 +224,7 @@ func (r *Repository) UnmarshalJSON(data []byte) error {
} }
// Bin maps command names to executable files. npm also allows a single string, // Bin maps command names to executable files. npm also allows a single string,
// in which case the command is named after the package (resolved in ParsePackage). // in which case the command is named after the package (resolved in parseUploadPackage).
type Bin map[string]string type Bin map[string]string
// UnmarshalJSON is needed because the bin field can be a string or an object. // UnmarshalJSON is needed because the bin field can be a string or an object.
@@ -264,7 +262,7 @@ type packageUpload struct {
// is non-nil on success; a body without `_attachments` is a deprecate request, // is non-nil on success; a body without `_attachments` is a deprecate request,
// otherwise it is a "publish". // otherwise it is a "publish".
func ParseUpload(r io.Reader) (*Package, *PackageDeprecation, error) { func ParseUpload(r io.Reader) (*Package, *PackageDeprecation, error) {
body, err := io.ReadAll(io.LimitReader(r, 10*1024*1024)) body, err := io.ReadAll(r)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -280,16 +278,6 @@ func ParseUpload(r io.Reader) (*Package, *PackageDeprecation, error) {
return p, nil, err return p, nil, err
} }
// ParsePackage parses a npm publish PUT body. Bodies without `_attachments`
// surface as ErrInvalidAttachment once name/version validation has passed.
func ParsePackage(r io.Reader) (*Package, error) {
var upload packageUpload
if err := json.NewDecoder(r).Decode(&upload); err != nil {
return nil, err
}
return parseUploadPackage(&upload)
}
// parseUploadPackage builds a Package from a decoded publish body. // parseUploadPackage builds a Package from a decoded publish body.
func parseUploadPackage(upload *packageUpload) (*Package, error) { func parseUploadPackage(upload *packageUpload) (*Package, error) {
for _, meta := range upload.Versions { for _, meta := range upload.Versions {
@@ -343,6 +331,7 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
Engines: meta.Engines, Engines: meta.Engines,
CPU: meta.CPU, CPU: meta.CPU,
OS: meta.OS, OS: meta.OS,
Libc: meta.Libc,
Directories: meta.Directories, Directories: meta.Directories,
Funding: meta.Funding, Funding: meta.Funding,
AcceptDependencies: meta.AcceptDependencies, AcceptDependencies: meta.AcceptDependencies,
@@ -356,12 +345,12 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version)) p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version))
attachment := func() *PackageAttachment { attachment := upload.Attachments[meta.Name+"-"+meta.Version+".tgz"] // not the sigstore bundle of `npm publish --provenance`
if attachment == nil && len(upload.Attachments) == 1 {
for _, a := range upload.Attachments { for _, a := range upload.Attachments {
return a attachment = a
} }
return nil }
}()
if attachment == nil || len(attachment.Data) == 0 { if attachment == nil || len(attachment.Data) == 0 {
return nil, ErrInvalidAttachment return nil, ErrInvalidAttachment
} }
@@ -393,8 +382,6 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
return nil, ErrInvalidIntegrity return nil, ErrInvalidIntegrity
} }
// Derive _hasShrinkwrap and hasInstallScript from the tarball; the
// packument can lie about either.
p.Metadata.HasShrinkwrap, p.Metadata.HasInstallScript = inspectTarball(data) p.Metadata.HasShrinkwrap, p.Metadata.HasInstallScript = inspectTarball(data)
return p, nil return p, nil
@@ -410,11 +397,7 @@ const maxNpmTarballScanBytes = int64(32 * 1024 * 1024) // 32 MiB
// maxNpmPackageJSONBytes caps the package.json bytes decoded from the tarball. // maxNpmPackageJSONBytes caps the package.json bytes decoded from the tarball.
const maxNpmPackageJSONBytes = int64(1 * 1024 * 1024) // 1 MiB const maxNpmPackageJSONBytes = int64(1 * 1024 * 1024) // 1 MiB
// inspectTarball reports hasShrinkwrap (presence of package/npm-shrinkwrap.json) // inspectTarball trusts the tarball over the client's packument, read errors yield zero values to not block publishing
// and hasInstallScript (package/package.json declares any of preinstall,
// install, postinstall). Both must be derived server-side because the client
// can lie in the packument. Any read/decode error yields (false, false) so a
// malformed archive does not block publishing.
func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) { func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
gr, err := gzip.NewReader(bytes.NewReader(data)) gr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil { if err != nil {
@@ -422,11 +405,16 @@ func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
} }
defer gr.Close() defer gr.Close()
var hasGypFile bool
var pkg struct {
Scripts map[string]string `json:"scripts"`
Gypfile any `json:"gypfile"`
}
tr := tar.NewReader(io.LimitReader(gr, maxNpmTarballScanBytes)) tr := tar.NewReader(io.LimitReader(gr, maxNpmTarballScanBytes))
for { for {
hdr, err := tr.Next() hdr, err := tr.Next()
if err != nil { if err != nil {
return hasShrinkwrap, hasInstallScript break
} }
// npm pack puts files under a single root directory (usually "package/"). // npm pack puts files under a single root directory (usually "package/").
name := strings.TrimPrefix(hdr.Name, "./") name := strings.TrimPrefix(hdr.Name, "./")
@@ -436,30 +424,14 @@ func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
switch { switch {
case strings.HasSuffix(name, "/npm-shrinkwrap.json"): case strings.HasSuffix(name, "/npm-shrinkwrap.json"):
hasShrinkwrap = true hasShrinkwrap = true
case strings.HasSuffix(name, ".gyp"):
hasGypFile = true
case strings.HasSuffix(name, "/package.json"): case strings.HasSuffix(name, "/package.json"):
hasInstallScript = tarballDeclaresInstallScript(tr) _ = json.NewDecoder(io.LimitReader(tr, maxNpmPackageJSONBytes)).Decode(&pkg)
}
if hasShrinkwrap && hasInstallScript {
return hasShrinkwrap, hasInstallScript
} }
} }
} // npm publish adds "install": "node-gyp rebuild" for a root *.gyp file to the manifest, but not to the tarball
return hasShrinkwrap, strings.TrimSpace(pkg.Scripts["preinstall"]+pkg.Scripts["install"]+pkg.Scripts["postinstall"]) != "" || hasGypFile && pkg.Gypfile != false
// tarballDeclaresInstallScript reports whether a package.json declares any
// of preinstall, install, postinstall.
func tarballDeclaresInstallScript(r io.Reader) bool {
var pkg struct {
Scripts map[string]string `json:"scripts"`
}
if err := json.NewDecoder(io.LimitReader(r, maxNpmPackageJSONBytes)).Decode(&pkg); err != nil {
return false
}
for _, name := range []string{"preinstall", "install", "postinstall"} {
if strings.TrimSpace(pkg.Scripts[name]) != "" {
return true
}
}
return false
} }
func validateName(name string) bool { func validateName(name string) bool {
+24 -54
View File
@@ -41,21 +41,20 @@ func TestParsePackage(t *testing.T) {
integrity := "sha512-" + base64Sha512(dataBytes) integrity := "sha512-" + base64Sha512(dataBytes)
t.Run("InvalidUpload", func(t *testing.T) { t.Run("InvalidUpload", func(t *testing.T) {
p, err := ParsePackage(bytes.NewReader([]byte{0})) p, _, err := ParseUpload(bytes.NewReader([]byte{0}))
assert.Nil(t, p) assert.Nil(t, p)
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("InvalidUploadNoData", func(t *testing.T) { t.Run("InvalidUploadNoData", func(t *testing.T) {
b, _ := json.Marshal(packageUpload{}) p, err := parseUploadPackage(&packageUpload{})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackage) assert.ErrorIs(t, err, ErrInvalidPackage)
}) })
t.Run("InvalidPackageName", func(t *testing.T) { t.Run("InvalidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) { test := func(t *testing.T, name string) {
b, _ := json.Marshal(packageUpload{ p, err := parseUploadPackage(&packageUpload{
PackageMetadata: PackageMetadata{ PackageMetadata: PackageMetadata{
ID: name, ID: name,
Name: name, Name: name,
@@ -66,8 +65,6 @@ func TestParsePackage(t *testing.T) {
}, },
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageName) assert.ErrorIs(t, err, ErrInvalidPackageName)
} }
@@ -94,7 +91,7 @@ func TestParsePackage(t *testing.T) {
t.Run("ValidPackageName", func(t *testing.T) { t.Run("ValidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) { test := func(t *testing.T, name string) {
b, _ := json.Marshal(packageUpload{ p, err := parseUploadPackage(&packageUpload{
PackageMetadata: PackageMetadata{ PackageMetadata: PackageMetadata{
ID: name, ID: name,
Name: name, Name: name,
@@ -105,8 +102,6 @@ func TestParsePackage(t *testing.T) {
}, },
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageVersion) assert.ErrorIs(t, err, ErrInvalidPackageVersion)
} }
@@ -125,7 +120,7 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidPackageVersion", func(t *testing.T) { t.Run("InvalidPackageVersion", func(t *testing.T) {
version := "first-version" version := "first-version"
b, _ := json.Marshal(packageUpload{ p, err := parseUploadPackage(&packageUpload{
PackageMetadata: PackageMetadata{ PackageMetadata: PackageMetadata{
ID: packageFullName, ID: packageFullName,
Name: packageFullName, Name: packageFullName,
@@ -137,8 +132,6 @@ func TestParsePackage(t *testing.T) {
}, },
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageVersion) assert.ErrorIs(t, err, ErrInvalidPackageVersion)
}) })
@@ -160,7 +153,7 @@ func TestParsePackage(t *testing.T) {
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b)) p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidAttachment) assert.ErrorIs(t, err, ErrInvalidAttachment)
}) })
@@ -185,7 +178,7 @@ func TestParsePackage(t *testing.T) {
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b)) p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidAttachment) assert.ErrorIs(t, err, ErrInvalidAttachment)
}) })
@@ -213,7 +206,7 @@ func TestParsePackage(t *testing.T) {
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b)) p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidIntegrity) assert.ErrorIs(t, err, ErrInvalidIntegrity)
}) })
@@ -241,7 +234,7 @@ func TestParsePackage(t *testing.T) {
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b)) p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p) assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidIntegrity) assert.ErrorIs(t, err, ErrInvalidIntegrity)
}) })
@@ -281,10 +274,13 @@ func TestParsePackage(t *testing.T) {
filename: { filename: {
Data: data, Data: data,
}, },
packageFullName + "-" + packageVersion + ".sigstore": {
Data: "{}",
},
}, },
}) })
p, err := ParsePackage(bytes.NewReader(b)) p, _, err := ParseUpload(bytes.NewReader(b))
assert.NotNil(t, p) assert.NotNil(t, p)
assert.NoError(t, err) assert.NoError(t, err)
@@ -329,7 +325,7 @@ func TestParsePackage(t *testing.T) {
} }
} }
}` }`
p, err := ParsePackage(strings.NewReader(packageJSON)) p, _, err := ParseUpload(strings.NewReader(packageJSON))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "MIT", string(p.Metadata.License)) require.Equal(t, "MIT", string(p.Metadata.License))
}) })
@@ -354,7 +350,7 @@ func TestParsePackage(t *testing.T) {
} }
} }
}` }`
p, err := ParsePackage(strings.NewReader(packageJSON)) p, _, err := ParseUpload(strings.NewReader(packageJSON))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL) require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL)
// a string bin is named after the package // a string bin is named after the package
@@ -426,6 +422,15 @@ func TestInspectTarball(t *testing.T) {
// npm pack sometimes emits "./package/..." entries. // npm pack sometimes emits "./package/..." entries.
wantShrinkwrap: true, wantShrinkwrap: true,
}, },
{
name: "gyp file implies node-gyp install",
files: map[string]string{"package/binding.gyp": "{}"},
wantInstaller: true,
},
{
name: "gypfile false disables gyp install",
files: map[string]string{"package/binding.gyp": "{}", "package/package.json": `{"gypfile":false}`},
},
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
@@ -460,41 +465,6 @@ func TestParseUpload(t *testing.T) {
require.NotNil(t, dep) require.NotNil(t, dep)
assert.Equal(t, map[string]string{"1.0.0": "gone", "1.0.1": ""}, dep.Versions) assert.Equal(t, map[string]string{"1.0.0": "gone", "1.0.1": ""}, dep.Versions)
}) })
t.Run("dispatches publish when _attachments present", func(t *testing.T) {
// Reuse a minimal tarball with a package.json.
data := buildTarball(map[string]string{"package/package.json": `{}`})
integrity := "sha512-" + base64Sha512(data)
body := fmt.Sprintf(
`{"name":%q,"versions":{"1.0.0":{"name":%q,"version":"1.0.0","dist":{"integrity":%q}}},"_attachments":{"x.tgz":{"data":%q}}}`,
pkg, pkg, integrity, base64.StdEncoding.EncodeToString(data),
)
p, dep, err := ParseUpload(strings.NewReader(body))
require.NoError(t, err)
assert.Nil(t, dep)
require.NotNil(t, p)
assert.Equal(t, pkg, p.Name)
})
t.Run("publish whose readme mentions deprecated is not misrouted", func(t *testing.T) {
// The old fast-path used a substring check for "deprecated"; make sure
// the new dispatch keys off _attachments only.
data := buildTarball(map[string]string{"package/package.json": `{}`})
integrity := "sha512-" + base64Sha512(data)
body := fmt.Sprintf(
`{"name":%q,"versions":{"1.0.0":{"name":%q,"version":"1.0.0","readme":"this package is deprecated!","dist":{"integrity":%q}}},"_attachments":{"x.tgz":{"data":%q}}}`,
pkg, pkg, integrity, base64.StdEncoding.EncodeToString(data),
)
p, dep, err := ParseUpload(strings.NewReader(body))
require.NoError(t, err)
assert.Nil(t, dep)
require.NotNil(t, p)
})
t.Run("invalid json errors out", func(t *testing.T) {
_, _, err := ParseUpload(strings.NewReader("not json"))
assert.Error(t, err)
})
} }
func base64Sha512(data []byte) string { func base64Sha512(data []byte) string {
+1
View File
@@ -29,6 +29,7 @@ type Metadata struct {
Engines map[string]string `json:"engines,omitempty"` Engines map[string]string `json:"engines,omitempty"`
CPU []string `json:"cpu,omitempty"` CPU []string `json:"cpu,omitempty"`
OS []string `json:"os,omitempty"` OS []string `json:"os,omitempty"`
Libc []string `json:"libc,omitempty"`
Directories map[string]string `json:"directories,omitempty"` Directories map[string]string `json:"directories,omitempty"`
Funding any `json:"funding,omitempty"` Funding any `json:"funding,omitempty"`
AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"` AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"`
+2 -1
View File
@@ -36,6 +36,7 @@ import "strings"
const ( const (
tildePrefix = '~' tildePrefix = '~'
commentPrefix = '#'
needsEscape = " \t\n|&;()<>${}[]*?!\"'`\\" needsEscape = " \t\n|&;()<>${}[]*?!\"'`\\"
needsSingleQuote = "!\n" needsSingleQuote = "!\n"
) )
@@ -74,7 +75,7 @@ func ShellEscape(toEscape string) string {
} }
// Now for simplicity we'll look at the rest of the string // Now for simplicity we'll look at the rest of the string
if !strings.ContainsAny(toEscape[start:], needsEscape) { if !strings.ContainsAny(toEscape[start:], needsEscape) && toEscape[0] != commentPrefix {
return toEscape return toEscape
} }
+4
View File
@@ -75,6 +75,10 @@ func TestShellEscape(t *testing.T) {
"Double quote and escape `...", "Double quote and escape `...",
"~/gitea`", "~/gitea`",
"~/\"gitea\\`\"", "~/\"gitea\\`\"",
}, {
"Double quote leading #",
"#123",
`"#123"`,
}, { }, {
"Double quotes can handle a number of things without having to escape them but not everything ...", "Double quotes can handle a number of things without having to escape them but not everything ...",
"~/<gitea> ${gitea} `gitea` [gitea] (gitea) \"gitea\" \\gitea\\ 'gitea'", "~/<gitea> ${gitea} `gitea` [gitea] (gitea) \"gitea\" \\gitea\\ 'gitea'",
+1 -1
View File
@@ -121,7 +121,7 @@ func asciiLower(b byte) byte {
// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go // AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold] // ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
func AsciiEqualFold(s, t string) bool { func AsciiEqualFold[T string | []byte](s, t T) bool {
if len(s) != len(t) { if len(s) != len(t) {
return false return false
} }
+14 -2
View File
@@ -5,6 +5,7 @@ package web
import ( import (
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"slices" "slices"
"strings" "strings"
@@ -19,13 +20,17 @@ type RouterPathGroup struct {
r *Router r *Router
pathParam string pathParam string
matchers []*routerPathMatcher matchers []*routerPathMatcher
unescape bool
} }
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) { func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
chiCtx := chi.RouteContext(req.Context()) chiCtx := chi.RouteContext(req.Context())
path := chiCtx.URLParam(g.pathParam) path := chiCtx.URLParam(g.pathParam)
if g.unescape {
path, _ = url.PathUnescape(path)
}
for _, m := range g.matchers { for _, m := range g.matchers {
if m.matchPath(chiCtx, path) { if m.matchPath(chiCtx, path, g.unescape) {
chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern) chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern)
executeMiddlewaresHandler(resp, req, m.middlewares, m.handlerFunc) executeMiddlewaresHandler(resp, req, m.middlewares, m.handlerFunc)
return return
@@ -53,6 +58,10 @@ func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupP
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
} }
func (g *RouterPathGroup) UseUnescapedPath() {
g.unescape = true
}
type routerPathParam struct { type routerPathParam struct {
name string name string
pathSepEnd bool pathSepEnd bool
@@ -68,7 +77,7 @@ type routerPathMatcher struct {
handlerFunc http.HandlerFunc handlerFunc http.HandlerFunc
} }
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool { func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string, unescaped bool) bool {
if !p.methods.Contains(chiCtx.RouteMethod) { if !p.methods.Contains(chiCtx.RouteMethod) {
return false return false
} }
@@ -102,6 +111,9 @@ func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
if p.params[i].pathSepEnd { if p.params[i].pathSepEnd {
val = strings.TrimSuffix(val, "/") val = strings.TrimSuffix(val, "/")
} }
if unescaped {
val = url.PathEscape(val)
}
chiCtx.URLParams.Add(p.params[i].name, val) chiCtx.URLParams.Add(p.params[i].name, val)
} }
return true return true
+9 -1
View File
@@ -7,6 +7,7 @@ import (
"bytes" "bytes"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"strings" "strings"
"testing" "testing"
@@ -97,12 +98,16 @@ func (r *testRecorder) test(t *testing.T, rt *Router, methodPath string, expecte
} }
func TestPathProcessor(t *testing.T) { func TestPathProcessor(t *testing.T) {
unescape := false
testProcess := func(pattern, uri string, expectedPathParams map[string]string) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.RouteMethod = "GET" chiCtx.RouteMethod = "GET"
p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound) p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound)
shouldProcess := expectedPathParams != nil shouldProcess := expectedPathParams != nil
assert.Equal(t, shouldProcess, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) if unescape {
uri, _ = url.PathUnescape(uri)
}
assert.Equal(t, shouldProcess, p.matchPath(chiCtx, uri, unescape), "use pattern %s to process uri %s", pattern, uri)
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
} }
@@ -119,6 +124,9 @@ func TestPathProcessor(t *testing.T) {
testProcess("/<p1:*>/part/<p2>", "/part/c", map[string]string{"p1": "", "p2": "c"}) testProcess("/<p1:*>/part/<p2>", "/part/c", map[string]string{"p1": "", "p2": "c"})
testProcess("/<p1:*>/part/<p2>", "/a/other-part/c", nil) testProcess("/<p1:*>/part/<p2>", "/a/other-part/c", nil)
testProcess("/<p1:*>-part/<p2>", "/a-other-part/c", map[string]string{"p1": "a-other", "p2": "c"}) testProcess("/<p1:*>-part/<p2>", "/a-other-part/c", map[string]string{"p1": "a-other", "p2": "c"})
unescape = true
testProcess("/<p1:@/x>", "/%40%2fx", map[string]string{"p1": "@%2Fx"})
} }
func TestRouter(t *testing.T) { func TestRouter(t *testing.T) {
+20 -30
View File
@@ -405,37 +405,27 @@ func CommonRoutes() *web.Router {
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
}) })
r.Group("/npm", func() { r.Group("/npm", func() {
// HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details r.Get("/-/v1/search", npm.PackageSearch)
scopeRegexp := `^@` + npm_module.RegexpNamePart + `$` r.Get("/-/ping", npm.Ping)
idRegexp := `^(@` + npm_module.RegexpNamePart + `%2[fF])?` + npm_module.RegexpNamePart + `$` r.Get("/-/whoami", npm.Whoami)
addPackageHandlers := func() { r.PathGroup("/*", func(g *web.RouterPathGroup) {
r.Get("", npm.PackageMetadata) // HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage) packageId := `/<id:(@` + npm_module.RegexpNamePart + `/)?` + npm_module.RegexpNamePart + ">"
r.Get("/{version}", npm.PackageVersionMetadata) g.UseUnescapedPath()
r.Group("/-/{version}/{filename}", func() { g.MatchPath("DELETE", packageId+"/-/<version>/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
r.Get("", npm.DownloadPackageFile) g.MatchPath("DELETE", packageId+"/-/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFileByName) // former tarball URL, still in lockfiles
}) g.MatchPath("GET", packageId+"/-/<filename>", npm.DownloadPackageFileByName)
r.Get("/-/{filename}", npm.DownloadPackageFileByName) g.MatchPath("DELETE", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage)
r.Group("/-rev/{revision}", func() { g.MatchPath("PUT", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview)
r.Delete("", npm.DeletePackage) g.MatchPath("GET", packageId+"/<version>", npm.PackageVersionMetadata)
r.Put("", npm.DeletePreview) g.MatchPath("GET", packageId, npm.PackageMetadata)
}, reqPackageAccess(perm.AccessModeWrite)) g.MatchPath("PUT", packageId, reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
}
r.Group("/{scope:"+scopeRegexp+"}/{id:"+idRegexp+"}", addPackageHandlers)
r.Group("/{id:"+idRegexp+"}", addPackageHandlers)
addPackageDistTagsHandlers := func() { packageDistTags := "/-/package" + packageId + "/dist-tags"
r.Get("", npm.ListPackageTags) g.MatchPath("GET", packageDistTags, npm.ListPackageTags)
r.Group("/{tag}", func() { g.MatchPath("PUT", packageDistTags+"/<tag>", reqPackageAccess(perm.AccessModeWrite), npm.AddPackageTag)
r.Put("", npm.AddPackageTag) g.MatchPath("DELETE", packageDistTags+"/<tag>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageTag)
r.Delete("", npm.DeletePackageTag)
}, reqPackageAccess(perm.AccessModeWrite))
}
r.Group("/-/package/{scope:"+scopeRegexp+"}/{id:"+idRegexp+"}/dist-tags", addPackageDistTagsHandlers)
r.Group("/-/package/{id:"+idRegexp+"}/dist-tags", addPackageDistTagsHandlers)
r.Group("/-/v1/search", func() {
r.Get("", npm.PackageSearch)
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/pub", func() { r.Group("/pub", func() {
+17 -4
View File
@@ -8,7 +8,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/url" "slices"
"sort" "sort"
"time" "time"
@@ -25,6 +25,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
distTags := make(map[string]string) distTags := make(map[string]string)
times := make(map[string]time.Time) times := make(map[string]time.Time)
firstPublished, lastPublished := pds[0].Version.CreatedUnix, pds[0].Version.CreatedUnix firstPublished, lastPublished := pds[0].Version.CreatedUnix, pds[0].Version.CreatedUnix
var latest *packages_model.PackageDescriptor
for _, pd := range pds { for _, pd := range pds {
semVer := pd.SemVer.String() semVer := pd.SemVer.String()
versions[semVer] = createPackageMetadataVersion(registryURL, pd) versions[semVer] = createPackageMetadataVersion(registryURL, pd)
@@ -35,6 +36,9 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
for _, pvp := range pd.VersionProperties { for _, pvp := range pd.VersionProperties {
if pvp.Name == npm_module.TagProperty { if pvp.Name == npm_module.TagProperty {
distTags[pvp.Value] = pd.Version.Version distTags[pvp.Value] = pd.Version.Version
if pvp.Value == "latest" {
latest = pd
}
} }
} }
} }
@@ -43,7 +47,16 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
times["created"] = firstPublished.AsTimeInLocation(time.UTC) times["created"] = firstPublished.AsTimeInLocation(time.UTC)
times["modified"] = lastPublished.AsTimeInLocation(time.UTC) times["modified"] = lastPublished.AsTimeInLocation(time.UTC)
latest := pds[len(pds)-1] if latest == nil { // yarn and pnpm fail without it, e.g. after its version got deleted
latest = pds[len(pds)-1]
for _, pd := range slices.Backward(pds) {
if pd.SemVer.Prerelease() == "" {
latest = pd
break
}
}
distTags["latest"] = latest.Version.Version
}
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](latest) metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](latest)
@@ -86,13 +99,13 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
PeerDependencies: metadata.PeerDependencies, PeerDependencies: metadata.PeerDependencies,
PeerDependenciesMeta: metadata.PeerDependenciesMeta, PeerDependenciesMeta: metadata.PeerDependenciesMeta,
OptionalDependencies: metadata.OptionalDependencies, OptionalDependencies: metadata.OptionalDependencies,
Readme: metadata.Readme,
Bin: metadata.Bin, Bin: metadata.Bin,
HasInstallScript: metadata.HasInstallScript, HasInstallScript: metadata.HasInstallScript,
HasShrinkwrap: metadata.HasShrinkwrap, HasShrinkwrap: metadata.HasShrinkwrap,
Engines: metadata.Engines, Engines: metadata.Engines,
CPU: metadata.CPU, CPU: metadata.CPU,
OS: metadata.OS, OS: metadata.OS,
Libc: metadata.Libc,
Directories: metadata.Directories, Directories: metadata.Directories,
Funding: metadata.Funding, Funding: metadata.Funding,
AcceptDependencies: metadata.AcceptDependencies, AcceptDependencies: metadata.AcceptDependencies,
@@ -100,7 +113,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
Dist: npm_module.PackageDistribution{ Dist: npm_module.PackageDistribution{
Shasum: pd.Files[0].Blob.HashSHA1, Shasum: pd.Files[0].Blob.HashSHA1,
Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes), Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes),
Tarball: fmt.Sprintf("%s/%s/-/%s/%s", registryURL, url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)), Tarball: fmt.Sprintf("%s/%s/-/%s", registryURL, pd.Package.Name, pd.Files[0].File.LowerName), // npmjs shape, which npm parses for allowScripts and yarn keeps registry-relative
}, },
} }
} }
+11 -6
View File
@@ -25,7 +25,7 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
Owner: &user_model.User{Name: "alice"}, Owner: &user_model.User{Name: "alice"},
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)}, Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
SemVer: version.Must(version.NewVersion(v)), SemVer: version.Must(version.NewVersion(v)),
Metadata: &npm_module.Metadata{Keywords: []string{"gitea"}, Repository: repo}, Metadata: &npm_module.Metadata{Readme: v, Keywords: []string{"gitea"}, Repository: repo},
Files: []*packages_model.PackageFileDescriptor{{ Files: []*packages_model.PackageFileDescriptor{{
File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"}, File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"},
Blob: &packages_model.PackageBlob{}, Blob: &packages_model.PackageBlob{},
@@ -35,21 +35,26 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{ result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{
descriptor("1.1.0", 1000, npm_module.Repository{}), descriptor("1.1.0", 1000, npm_module.Repository{}),
descriptor("2.0.0-rc.1", 1500, repository),
descriptor("1.0.0", 2000, repository), descriptor("1.0.0", 2000, repository),
}) })
assert.Equal(t, map[string]time.Time{ assert.Equal(t, map[string]time.Time{
"1.0.0": time.Unix(2000, 0).UTC(), "1.0.0": time.Unix(2000, 0).UTC(),
"1.1.0": time.Unix(1000, 0).UTC(), "1.1.0": time.Unix(1000, 0).UTC(),
"created": time.Unix(1000, 0).UTC(), "2.0.0-rc.1": time.Unix(1500, 0).UTC(),
"modified": time.Unix(2000, 0).UTC(), "created": time.Unix(1000, 0).UTC(),
"modified": time.Unix(2000, 0).UTC(),
}, result.Time) }, result.Time)
assert.Equal(t, map[string]string{"latest": "1.1.0"}, result.DistTags)
assert.Equal(t, "1.1.0", result.Readme)
assert.Empty(t, result.Versions["1.1.0"].Readme)
assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Maintainers) assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Maintainers)
assert.Equal(t, []string{"gitea"}, result.Keywords) assert.Equal(t, []string{"gitea"}, result.Keywords)
assert.Equal(t, []string{"gitea"}, result.Versions["1.0.0"].Keywords) assert.Equal(t, []string{"gitea"}, result.Versions["1.0.0"].Keywords)
assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Versions["1.0.0"].Maintainers) assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Versions["1.0.0"].Maintainers)
assert.Equal(t, assert.Equal(t,
"https://gitea.dev/api/packages/alice/npm/@scope%2Ftest/-/1.0.0/test-1.0.0.tgz", "https://gitea.dev/api/packages/alice/npm/@scope/test/-/test-1.0.0.tgz",
result.Versions["1.0.0"].Dist.Tarball, result.Versions["1.0.0"].Dist.Tarball,
) )
assert.Equal(t, repository, result.Versions["1.0.0"].Repository) assert.Equal(t, repository, result.Versions["1.0.0"].Repository)
+90 -104
View File
@@ -6,6 +6,7 @@ package npm
import ( import (
"bytes" "bytes"
std_ctx "context" std_ctx "context"
"crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -44,49 +45,53 @@ func apiError(ctx *context.Context, status int, obj any) {
// packageNameFromParams gets the package name from the url parameters // packageNameFromParams gets the package name from the url parameters
func packageNameFromParams(ctx *context.Context) string { func packageNameFromParams(ctx *context.Context) string {
// Real examples: these 2 both should work: // HINT: NPM-ROUTE-PATH-PATTERN: real examples: these cases all should work:
// * "https://registry.npmjs.org/@angular/core" // * "https://registry.npmjs.org/@angular/core"
// * "https://registry.npmjs.org/@angular%2Fcore" // * "https://registry.npmjs.org/@angular%2Fcore"
// * "https://registry.npmjs.org/%40angular%2Fcore"
// //
// HINT: NPM-ROUTE-PATH-PATTERN: The cases for the path parameters: return ctx.PathParam("id") // id is the full package name, e.g.: "@angular/core" or "lodash"
// * ".../TheName/...": id="TheName"
// * ".../@TheScope/TheName/...": scope="@TheScope", id="TheName"
// * ".../@TheScope%2FTheName/...": id="@TheScope/TheName"
scope := ctx.PathParam("scope")
fullOrSub := ctx.PathParam("id") // may be a full name or a subpath of the full package name
if scope != "" {
// now id is the subpath of the full package name, e.g. "core" in "@angular/core"
return fmt.Sprintf("%s/%s", scope, fullOrSub)
}
return fullOrSub // id is the full package name, e.g.: "@angular/core" or "lodash"
} }
func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string { func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string {
return httplib.GuessCurrentAppURL(ctx) + "api/packages/" + url.PathEscape(owner.Name) + "/npm" return httplib.GuessCurrentAppURL(ctx) + "api/packages/" + url.PathEscape(owner.Name) + "/npm"
} }
// PackageMetadata returns the metadata for a single package func packageMetadata(ctx *context.Context) *npm_module.PackageMetadata {
func PackageMetadata(ctx *context.Context) { pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageNameFromParams(ctx))
packageName := packageNameFromParams(ctx)
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return nil
} }
if len(pvs) == 0 { if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, err) apiError(ctx, http.StatusNotFound, err)
return return nil
} }
pds, err := packages_model.GetPackageDescriptors(ctx, pvs) pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return nil
} }
resp := createPackageMetadataResponse(buildNpmRegistryURL(ctx, ctx.Package.Owner), pds) return createPackageMetadataResponse(buildNpmRegistryURL(ctx, ctx.Package.Owner), pds)
ctx.JSON(http.StatusOK, resp) }
// PackageMetadata returns the metadata for a single package
func PackageMetadata(ctx *context.Context) {
if metadata := packageMetadata(ctx); metadata != nil {
serveMetadata(ctx, metadata)
}
}
func serveMetadata(ctx *context.Context, obj any) {
body, err := json.MarshalDeterministic(obj)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Resp.Header().Set("ETag", fmt.Sprintf(`W/"%x"`, sha256.Sum256(body)))
ctx.ServeContent(bytes.NewReader(body), context.ServeHeaderOptions{ContentType: "application/json;charset=utf-8"})
} }
// PackageVersionMetadata returns the metadata for a single version or dist-tag // PackageVersionMetadata returns the metadata for a single version or dist-tag
@@ -110,7 +115,11 @@ func PackageVersionMetadata(ctx *context.Context) {
return return
} }
if len(pvs) == 0 { if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, "version not found: "+versionOrTag) if versionOrTag != "latest" {
apiError(ctx, http.StatusNotFound, "version not found: "+versionOrTag)
} else if metadata := packageMetadata(ctx); metadata != nil { // unset, so serve the packument's fallback
serveMetadata(ctx, metadata.Versions[metadata.DistTags["latest"]])
}
return return
} }
@@ -120,25 +129,40 @@ func PackageVersionMetadata(ctx *context.Context) {
return return
} }
ctx.JSON(http.StatusOK, createPackageMetadataVersion(buildNpmRegistryURL(ctx, ctx.Package.Owner), pd)) serveMetadata(ctx, createPackageMetadataVersion(buildNpmRegistryURL(ctx, ctx.Package.Owner), pd))
} }
// DownloadPackageFile serves the content of a package func packageVersionByFilename(ctx *context.Context) *packages_model.PackageVersion {
func DownloadPackageFile(ctx *context.Context) { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
packageName := packageNameFromParams(ctx) OwnerID: ctx.Package.Owner.ID,
packageVersion := ctx.PathParam("version") Type: packages_model.TypeNpm,
filename := ctx.PathParam("filename") Name: packages_model.SearchValue{ExactMatch: true, Value: packageNameFromParams(ctx)},
HasFileWithName: ctx.PathParam("filename"),
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return nil
}
if len(pvs) != 1 {
apiError(ctx, http.StatusNotFound, nil)
return nil
}
return pvs[0]
}
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( // DownloadPackageFileByName finds the version and serves the contents of a package
func DownloadPackageFileByName(ctx *context.Context) {
pv := packageVersionByFilename(ctx)
if pv == nil {
return
}
s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx, ctx,
&packages_service.PackageInfo{ pv,
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeNpm,
Name: packageName,
Version: packageVersion,
},
&packages_service.PackageFileInfo{ &packages_service.PackageFileInfo{
Filename: filename, Filename: ctx.PathParam("filename"),
}, },
ctx.Req.Method, ctx.Req.Method,
) )
@@ -150,54 +174,14 @@ func DownloadPackageFile(ctx *context.Context) {
helper.ServePackageFile(ctx, s, u, pf) helper.ServePackageFile(ctx, s, u, pf)
} }
// DownloadPackageFileByName finds the version and serves the contents of a package
func DownloadPackageFileByName(ctx *context.Context) {
filename := ctx.PathParam("filename")
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNpm,
Name: packages_model.SearchValue{
ExactMatch: true,
Value: packageNameFromParams(ctx),
},
HasFileWithName: filename,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pvs) != 1 {
apiError(ctx, http.StatusNotFound, nil)
return
}
s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pvs[0],
&packages_service.PackageFileInfo{
Filename: filename,
},
ctx.Req.Method,
)
if err != nil {
if errors.Is(err, packages_model.ErrPackageFileNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
helper.ServePackageFile(ctx, s, u, pf)
}
// UploadPackage creates a new package // UploadPackage creates a new package
func UploadPackage(ctx *context.Context) { func UploadPackage(ctx *context.Context) {
npmPackage, deprecation, err := npm_module.ParseUpload(ctx.Req.Body) // about the npmjs and GitHub Packages limit, fits base64 tarballs up to ~200 MB
npmPackage, deprecation, err := npm_module.ParseUpload(http.MaxBytesReader(ctx.Resp, ctx.Req.Body, 256*1024*1024))
if err != nil { if err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if _, ok := errors.AsType[*http.MaxBytesError](err); ok {
apiError(ctx, http.StatusRequestEntityTooLarge, err)
} else if errors.Is(err, util.ErrInvalidArgument) {
apiError(ctx, http.StatusBadRequest, err) apiError(ctx, http.StatusBadRequest, err)
} else { } else {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
@@ -350,26 +334,14 @@ func deprecatePackage(ctx *context.Context, dep *npm_module.PackageDeprecation)
ctx.Status(http.StatusOK) ctx.Status(http.StatusOK)
} }
// DeletePackageVersion deletes the package version // DeletePackageVersion deletes the package version, which `npm unpublish` addresses by its tarball
func DeletePackageVersion(ctx *context.Context) { func DeletePackageVersion(ctx *context.Context) {
packageName := packageNameFromParams(ctx) pv := packageVersionByFilename(ctx)
packageVersion := ctx.PathParam("version") if pv == nil {
return
}
err := packages_service.RemovePackageVersionByNameAndVersion( if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
ctx,
ctx.Doer,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeNpm,
Name: packageName,
Version: packageVersion,
},
)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return
} }
@@ -404,9 +376,7 @@ func DeletePackage(ctx *context.Context) {
// ListPackageTags returns all tags for a package // ListPackageTags returns all tags for a package
func ListPackageTags(ctx *context.Context) { func ListPackageTags(ctx *context.Context) {
packageName := packageNameFromParams(ctx) pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageNameFromParams(ctx))
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return
@@ -424,7 +394,11 @@ func ListPackageTags(ctx *context.Context) {
} }
} }
ctx.JSON(http.StatusOK, tags) if _, ok := tags["latest"]; ok {
ctx.JSON(http.StatusOK, tags)
} else if metadata := packageMetadata(ctx); metadata != nil { // unset, so list the packument's fallback
ctx.JSON(http.StatusOK, metadata.DistTags)
}
} }
// AddPackageTag adds a tag to the package // AddPackageTag adds a tag to the package
@@ -534,6 +508,18 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
}) })
} }
func Ping(ctx *context.Context) {
ctx.JSON(http.StatusOK, map[string]any{})
}
func Whoami(ctx *context.Context) {
if ctx.Doer == nil {
apiError(ctx, http.StatusUnauthorized, "Unauthorized")
return
}
ctx.JSON(http.StatusOK, map[string]string{"username": ctx.Doer.Name})
}
func PackageSearch(ctx *context.Context) { func PackageSearch(ctx *context.Context) {
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID, OwnerID: ctx.Package.Owner.ID,
+3 -3
View File
@@ -162,7 +162,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// if it's not a pointer, just serve the data directly // if it's not a pointer, just serve the data directly
if !pointer.IsValid() { if !pointer.IsValid() {
_, _ = ctx.Resp.Write(lfsPointerBuf) httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, int64(len(lfsPointerBuf)), bytes.NewReader(lfsPointerBuf), httplib.ServeHeaderOptions{Filename: blob.Name()})
return return
} }
@@ -171,7 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// If there isn't one, just serve the data directly // If there isn't one, just serve the data directly
if errors.Is(err, git_model.ErrLFSObjectNotExist) { if errors.Is(err, git_model.ErrLFSObjectNotExist) {
_, _ = ctx.Resp.Write(lfsPointerBuf) httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, int64(len(lfsPointerBuf)), bytes.NewReader(lfsPointerBuf), httplib.ServeHeaderOptions{Filename: blob.Name()})
return return
} else if err != nil { } else if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -198,7 +198,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
return return
} }
defer lfsDataFile.Close() defer lfsDataFile.Close()
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath}) httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: blob.Name()})
} }
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) { func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
+2 -2
View File
@@ -386,8 +386,8 @@ func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_mo
ctx.APIErrorNotFound("no such attachment in repo") ctx.APIErrorNotFound("no such attachment in repo")
return false return false
} }
if attachment.IssueID == 0 { if attachment.IssueID == 0 || attachment.CommentID != 0 {
log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID) log.Debug("Requested attachment[%d] is not an issue attachment.", attachment.ID)
ctx.APIErrorNotFound("no such attachment in issue") ctx.APIErrorNotFound("no such attachment in issue")
return false return false
} else if issue != nil && attachment.IssueID != issue.ID { } else if issue != nil && attachment.IssueID != issue.ID {
+6 -1
View File
@@ -291,6 +291,11 @@ func AddPushMirror(ctx *context.APIContext) {
return return
} }
if setting.Mirror.DisableNewPush {
ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new push mirrors")
return
}
pushMirror := web.GetForm[*api.CreatePushMirrorOption](ctx) pushMirror := web.GetForm[*api.CreatePushMirrorOption](ctx)
CreatePushMirror(ctx, pushMirror) CreatePushMirror(ctx, pushMirror)
} }
@@ -356,7 +361,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword) address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
if err == nil { if err == nil {
err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser) err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
} }
if err != nil { if err != nil {
HandleRemoteAddressError(ctx, err) HandleRemoteAddressError(ctx, err)
+23
View File
@@ -10,13 +10,36 @@ import (
"gitea.dev/models/db" "gitea.dev/models/db"
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test" "gitea.dev/modules/test"
"gitea.dev/services/contexttest" "gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestCreatePushMirrorUsesCallerPermission(t *testing.T) {
defer test.MockVariableValue(&setting.ImportLocalPaths, true)()
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
ctx.Doer = &user_model.User{}
ctx.ContextUser = &user_model.User{AllowImportLocal: true}
CreatePushMirror(ctx, &api.CreatePushMirrorOption{RemoteAddress: "local-mirror", Interval: "0"})
assert.Equal(t, http.StatusUnauthorized, resp.Code)
}
func TestAddPushMirrorDisabled(t *testing.T) {
defer test.MockVariableValue(&setting.Mirror.DisableNewPush, true)()
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
AddPushMirror(ctx)
assert.Equal(t, http.StatusForbidden, resp.Code)
assert.Contains(t, resp.Body.String(), "the site administrator has disabled the creation of new push mirrors")
}
// TestPushMirrorSync verifies the endpoint attempts every push mirror instead // TestPushMirrorSync verifies the endpoint attempts every push mirror instead
// of aborting on the first failure, reporting all failed remotes with a 422. // of aborting on the first failure, reporting all failed remotes with a 422.
// Each remote name is not a configured git remote, so SyncPushMirror fails fast // Each remote name is not a configured git remote, so SyncPushMirror fails fast
+1 -1
View File
@@ -1032,7 +1032,7 @@ func MergePullRequest(ctx *context.APIContext) {
} }
} }
if err := pull_service.Merge(pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { if err := pull_service.Merge(ctx, pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) { if pull_service.IsErrInvalidMergeStyle(err) {
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
} else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok { } else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok {
+2 -1
View File
@@ -139,7 +139,8 @@ func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts
// The repo is empty and being initialized by this push, so there is no // The repo is empty and being initialized by this push, so there is no
// dependent state (webhooks, notifications, visibility fan-out) to reconcile // dependent state (webhooks, notifications, visibility fan-out) to reconcile
// yet; setting the flags directly is sufficient in this push-to-create case. // yet; setting the flags directly is sufficient in this push-to-create case.
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() { if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() &&
(isPrivate.Value() || !setting.Repository.ForcePrivate || ctx.Doer.IsAdmin) {
repo.IsPrivate = isPrivate.Value() repo.IsPrivate = isPrivate.Value()
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
log.Error("failed to update repo is_private: %v", err) log.Error("failed to update repo is_private: %v", err)
+1 -1
View File
@@ -576,7 +576,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
} }
token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey) token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey)
if err != nil { if err != nil || token.Kind != oauth2_provider.KindRefreshToken {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient, ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "unable to parse refresh token", ErrorDescription: "unable to parse refresh token",
+10 -1
View File
@@ -9,7 +9,9 @@ import (
activities_model "gitea.dev/models/activities" activities_model "gitea.dev/models/activities"
"gitea.dev/models/organization" "gitea.dev/models/organization"
"gitea.dev/models/renderhelper" "gitea.dev/models/renderhelper"
user_model "gitea.dev/models/user"
"gitea.dev/modules/markup/markdown" "gitea.dev/modules/markup/markdown"
"gitea.dev/modules/setting"
"gitea.dev/services/context" "gitea.dev/services/context"
feed_service "gitea.dev/services/feed" feed_service "gitea.dev/services/feed"
@@ -28,8 +30,15 @@ func ShowUserFeedAtom(ctx *context.Context) {
// showUserFeed show user activity as RSS / Atom feed // showUserFeed show user activity as RSS / Atom feed
func showUserFeed(ctx *context.Context, formatType string) { func showUserFeed(ctx *context.Context, formatType string) {
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
isOrganisation := ctx.ContextUser.IsOrganization() isOrganisation := ctx.ContextUser.IsOrganization()
if !setting.Other.EnableFeed ||
isOrganisation && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
!isOrganisation && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(nil)
return
}
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
if ctx.IsSigned && isOrganisation && !includePrivate { if ctx.IsSigned && isOrganisation && !includePrivate {
// When feed is requested by a member of the organization, // When feed is requested by a member of the organization,
// include the private repo's the member has access to. // include the private repo's the member has access to.
+9 -1
View File
@@ -642,7 +642,15 @@ func (data *actionRunListData) preparePartialRefreshRuns(ctx *context.Context) b
ctx.ServerError("GetRunsByRepoAndID", err) ctx.ServerError("GetRunsByRepoAndID", err)
return false return false
} }
data.ActionRuns = runs runsMap := make(map[int64]*actions_model.ActionRun, len(runs))
for _, run := range runs {
runsMap[run.ID] = run
}
for _, id := range data.refreshRunIDs {
if run, ok := runsMap[id]; ok {
data.ActionRuns = append(data.ActionRuns, run)
}
}
return true return true
} }
+15
View File
@@ -15,6 +15,7 @@ import (
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/test" "gitea.dev/modules/test"
web_context "gitea.dev/services/context" web_context "gitea.dev/services/context"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -74,3 +75,17 @@ func newWorkflowBadgeTestContext(t *testing.T) *web_context.Context {
} }
return ctx return ctx
} }
func TestActionRunListData(t *testing.T) {
unittest.PrepareTestEnv(t)
t.Run("preparePartialRefreshRuns", func(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user5/repo4/actions")
contexttest.LoadRepo(t, ctx, 4)
d := &actionRunListData{refreshRunIDs: []int64{791, 792}}
d.preparePartialRefreshRuns(ctx)
assert.Equal(t, []int64{791, 792}, []int64{d.ActionRuns[0].ID, d.ActionRuns[1].ID})
d = &actionRunListData{refreshRunIDs: []int64{792, 791}}
d.preparePartialRefreshRuns(ctx)
assert.Equal(t, []int64{792, 791}, []int64{d.ActionRuns[0].ID, d.ActionRuns[1].ID})
})
}
+1 -1
View File
@@ -1145,7 +1145,7 @@ func MergePullRequest(ctx *context.Context) {
} }
} }
if err := pull_service.Merge(pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { if err := pull_service.Merge(ctx, pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) { if pull_service.IsErrInvalidMergeStyle(err) {
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
} else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok { } else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok {
+5
View File
@@ -660,6 +660,11 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
return return
} }
if isDelTag && !rel.IsTag {
ctx.HTTPError(http.StatusConflict, "a tag attached to a release cannot be deleted directly")
return
}
if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil { if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil {
if release_service.IsErrProtectedTagName(err) { if release_service.IsErrProtectedTagName(err) {
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
+16
View File
@@ -4,6 +4,7 @@
package repo package repo
import ( import (
"net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@@ -21,6 +22,21 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDeleteTagRetainsReleaseAndAttachments(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, resp := contexttest.MockContext(t, "POST user2/repo1/tags/delete?id=1")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1})
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9, ReleaseID: 1})
DeleteTag(ctx)
assert.Equal(t, http.StatusConflict, resp.Code)
assert.Equal(t, release, unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1}))
assert.Equal(t, attachment, unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9}))
}
func TestNewReleasePost(t *testing.T) { func TestNewReleasePost(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
-8
View File
@@ -734,18 +734,10 @@ func UsernameSubRoute(ctx *context.Context) {
ShowGPGKeys(ctx) ShowGPGKeys(ctx)
} }
case strings.HasSuffix(username, ".rss"): case strings.HasSuffix(username, ".rss"):
if !setting.Other.EnableFeed {
ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".rss") { if reloadParam(".rss") {
feed.ShowUserFeedRSS(ctx) feed.ShowUserFeedRSS(ctx)
} }
case strings.HasSuffix(username, ".atom"): case strings.HasSuffix(username, ".atom"):
if !setting.Other.EnableFeed {
ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".atom") { if reloadParam(".atom") {
feed.ShowUserFeedAtom(ctx) feed.ShowUserFeedAtom(ctx)
} }
+7
View File
@@ -322,6 +322,13 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
// ActionUserFollow is for follow/unfollow user request // ActionUserFollow is for follow/unfollow user request
func ActionUserFollow(ctx *context.Context) { func ActionUserFollow(ctx *context.Context) {
isOrg := ctx.ContextUser.IsOrganization()
if isOrg && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
!isOrg && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(nil)
return
}
var err error var err error
switch ctx.FormString("action") { switch ctx.FormString("action") {
case "follow": case "follow":
+1 -1
View File
@@ -224,7 +224,7 @@ func handlePullRequestAutoMerge(ctx context.Context, pr *issues_model.PullReques
// although expectedHeadCommitID is checked before, we should pass it to the Merge function to // although expectedHeadCommitID is checked before, we should pass it to the Merge function to
// make it be checked again in case the head commit id changed after the previous check. // make it be checked again in case the head commit id changed after the previous check.
if err := pull_service.Merge(pr.ID, doer, scheduledPRM.MergeStyle, expectedHeadCommitID, scheduledPRM.Message, true); err != nil { if err := pull_service.Merge(ctx, pr.ID, doer, scheduledPRM.MergeStyle, expectedHeadCommitID, scheduledPRM.Message, true); err != nil {
if pull_service.IsErrSHADoesNotMatch(err) { if pull_service.IsErrSHADoesNotMatch(err) {
return errors.Join(errSkipAutoMerge, err) return errors.Join(errSkipAutoMerge, err)
} }
+2 -3
View File
@@ -107,9 +107,8 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
if err != nil { if err != nil {
log.Info("Unable to get global API settings. Ignoring these.") log.Info("Unable to get global API settings. Ignoring these.")
log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err) log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err)
} } else if apiConf != nil && apiConf.MaxResponseItems > 0 {
if apiConf != nil { maxPerPage = min(apiConf.MaxResponseItems, 100)
maxPerPage = apiConf.MaxResponseItems
} }
return &GiteaDownloader{ return &GiteaDownloader{
+10 -7
View File
@@ -5,6 +5,7 @@ package migrations
import ( import (
"fmt" "fmt"
"math"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@@ -317,15 +318,16 @@ func TestGiteaDownloadRepo(t *testing.T) {
func TestGiteaDownloadCommentsPaging(t *testing.T) { func TestGiteaDownloadCommentsPaging(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
maxResponseItems, commentCount, requests int maxResponseItems, pageSize, commentCount, requests int
paginated bool paginated bool
}{ }{
{maxResponseItems: 2, commentCount: 2, requests: 2}, {maxResponseItems: 2, pageSize: 2, commentCount: 2, requests: 2},
{maxResponseItems: 2, commentCount: 3, requests: 1}, {maxResponseItems: 2, pageSize: 2, commentCount: 3, requests: 1},
{maxResponseItems: 2, commentCount: 4, requests: 3, paginated: true}, {maxResponseItems: 2, pageSize: 2, commentCount: 4, requests: 3, paginated: true},
{maxResponseItems: 0, commentCount: 0, requests: 1}, {maxResponseItems: 0, pageSize: 10, commentCount: 0, requests: 1},
{maxResponseItems: math.MaxInt, pageSize: 100, commentCount: 0, requests: 1},
} { } {
t.Run(strconv.Itoa(tc.commentCount), func(t *testing.T) { t.Run(fmt.Sprintf("maxResponseItems=%d/comments=%d", tc.maxResponseItems, tc.commentCount), func(t *testing.T) {
commentRequests := 0 commentRequests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
@@ -352,6 +354,7 @@ func TestGiteaDownloadCommentsPaging(t *testing.T) {
downloader, err := NewGiteaDownloader(t.Context(), server.URL, "o/r", "", "", "") downloader, err := NewGiteaDownloader(t.Context(), server.URL, "o/r", "", "", "")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tc.pageSize, downloader.maxPerPage)
comments, _, err := downloader.GetComments(t.Context(), &base.Issue{Number: 1}) comments, _, err := downloader.GetComments(t.Context(), &base.Issue{Number: 1})
require.NoError(t, err) require.NoError(t, err)
+16 -1
View File
@@ -14,6 +14,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"unicode" "unicode"
"uuid"
"gitea.dev/models/db" "gitea.dev/models/db"
git_model "gitea.dev/models/git" git_model "gitea.dev/models/git"
@@ -27,6 +28,7 @@ import (
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/globallock" "gitea.dev/modules/globallock"
"gitea.dev/modules/graceful" "gitea.dev/modules/graceful"
"gitea.dev/modules/gtprof"
"gitea.dev/modules/httplib" "gitea.dev/modules/httplib"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/references" "gitea.dev/modules/references"
@@ -289,9 +291,22 @@ func hasPullRequestCommitBeenMerged(ctx context.Context, pr *issues_model.PullRe
// Merge merges pull request to base repository. // Merge merges pull request to base repository.
// Caller should check PR is ready to be merged (review and status checks) // Caller should check PR is ready to be merged (review and status checks)
func Merge(prID int64, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error { func Merge(outerCtx context.Context, prID int64, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
outerCtxId := uuid.NewV4().String()
_, outerSpan := gtprof.GetTracer().Start(outerCtx, gtprof.TraceSpanContext)
outerSpan.SetAttributeString("context.trace-id", outerCtxId) // this attribute is only used internally for debugging purpose
defer outerSpan.End()
// TODO: in the future, the contexts from graceful.GetManager() should be wrapped with gtprof tracing, refactor the code to framework-level support
ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled
ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanContext)
span.SetAttributeString(gtprof.TraceAttrGeneralName, "merge-pull-request")
span.SetAttributeString(gtprof.TraceAttrGeneralDesc, fmt.Sprintf("merge pull request %d with merge style %s", prID, mergeStyle))
span.SetAttributeString("context.trace-id-outer", outerCtxId) // this attribute is only used internally for debugging purpose
defer span.End()
err := globallock.LockAndDo(ctx, getPullWorkingLockKey(prID), func(ctx context.Context) error { err := globallock.LockAndDo(ctx, getPullWorkingLockKey(prID), func(ctx context.Context) error {
pr, err := issues_model.GetPullRequestByID(ctx, prID) pr, err := issues_model.GetPullRequestByID(ctx, prID)
if err != nil { if err != nil {
+12 -6
View File
@@ -7,20 +7,26 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"strings"
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
) )
const gitLogGraphFormatSep = "^" // disallowed char in git ref names
// GetCommitGraph return a list of commit (GraphItems) from all branches // GetCommitGraph return a list of commit (GraphItems) from all branches
func GetCommitGraph(ctx context.Context, gitRepo *git.Repository, page, maxAllowedColors int, hidePRRefs bool, refs, files []string) (*Graph, error) { func GetCommitGraph(ctx context.Context, gitRepo *git.Repository, page, maxAllowedColors int, hidePRRefs bool, refs, files []string) (*Graph, error) {
format := "DATA:%D|%H|%ad|%h|%s" format := "DATA:" + strings.Join([]string{
"%D", // ref names without the " (", ")" wrapping.
if page == 0 { "%H", // commit hash
page = 1 "%ad", // author date (format respects --date= option)
} "%h", // abbreviated commit hash
"%s", // subject
}, gitLogGraphFormatSep)
page = max(page, 1)
graphCmd := gitcmd.NewCommand("log", "--graph", "--date-order", "--decorate=full") graphCmd := gitcmd.NewCommand("log", "--graph", "--date-order", "--decorate=full")
if hidePRRefs { if hidePRRefs {
@@ -31,7 +37,7 @@ func GetCommitGraph(ctx context.Context, gitRepo *git.Repository, page, maxAllow
graphCmd.AddArguments("--tags", "--branches") graphCmd.AddArguments("--tags", "--branches")
} }
graphCmd.AddArguments("-C", "-M", "--date=iso-strict"). graphCmd.AddArguments("--find-copies", "--find-renames", "--date=iso-strict").
AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page). AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page).
AddOptionFormat("--pretty=format:%s", format) AddOptionFormat("--pretty=format:%s", format)
+1 -1
View File
@@ -216,7 +216,7 @@ func parseGitTime(timeStr string) time.Time {
// NewCommit creates a new commit from a provided line // NewCommit creates a new commit from a provided line
func NewCommit(row, column int, line []byte) (*Commit, error) { func NewCommit(row, column int, line []byte) (*Commit, error) {
data := bytes.SplitN(line, []byte("|"), 5) data := bytes.SplitN(line, []byte(gitLogGraphFormatSep), 5)
if len(data) < 5 { if len(data) < 5 {
return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line)) return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line))
} }
+3 -3
View File
@@ -35,7 +35,7 @@ func BenchmarkGetCommitGraph(b *testing.B) {
} }
func BenchmarkParseCommitString(b *testing.B) { func BenchmarkParseCommitString(b *testing.B) {
testString := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|Add route for graph" testString := "* DATA:^4e61bacab44e9b4730e44a6615d04098dd3a8eaf^2016-12-20 21:10:41 +0100^4e61bac^Add route for graph"
parser := &Parser{} parser := &Parser{}
parser.Reset() parser.Reset()
@@ -224,14 +224,14 @@ func TestParseGlyphs(t *testing.T) {
} }
func TestCommitStringParsing(t *testing.T) { func TestCommitStringParsing(t *testing.T) {
dataFirstPart := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|" dataFirstPart := "* DATA:^4e61bacab44e9b4730e44a6615d04098dd3a8eaf^2016-12-20 21:10:41 +0100^4e61bac^"
tests := []struct { tests := []struct {
shouldPass bool shouldPass bool
testName string testName string
commitMessage string commitMessage string
}{ }{
{true, "normal", "not a fancy message"}, {true, "normal", "not a fancy message"},
{true, "extra pipe", "An extra pipe: |"}, {true, "extra sep", "An extra sep"},
{true, "extra 'Data:'", "DATA: might be trouble"}, {true, "extra 'Data:'", "DATA: might be trouble"},
} }
@@ -9,18 +9,15 @@
<h3 class="tw-m-0">{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3> <h3 class="tw-m-0">{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>
{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}} {{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}
</div> </div>
{{$localBranch := $pull.HeadBranch}} {{$args := $pull.GetInstructionsCliArgs}}
{{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}
{{$localBranch = print $pull.HeadRepo.OwnerName "-" $pull.HeadBranch}}
{{end}}
<div class="ui secondary segment tw-font-mono"> <div class="ui secondary segment tw-font-mono">
{{$gitRemoteName := ctx.RootData.SystemConfig.Repository.GitGuideRemoteName.Value ctx}} {{$gitRemoteName := ctx.RootData.SystemConfig.Repository.GitGuideRemoteName.Value ctx}}
{{if eq $pull.Flow 0}} {{if eq $pull.Flow 0}}
<div>git fetch -u {{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}{{$pull.HeadRepo.HTMLURL ctx}}{{else}}{{$gitRemoteName}}{{end}} {{$pull.HeadBranch}}:{{$localBranch}}</div> <div>git fetch -u {{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}{{$pull.HeadRepo.HTMLURL ctx}}{{else}}{{$gitRemoteName}}{{end}} {{$args.HeadBranchArg}}:{{$args.LocalBranchArg}}</div>
{{else}} {{else}}
<div>git fetch -u {{$gitRemoteName}} {{$pull.GetGitHeadRefName}}:{{$localBranch}}</div> <div>git fetch -u {{$gitRemoteName}} {{$pull.GetGitHeadRefName}}:{{$args.LocalBranchArg}}</div>
{{end}} {{end}}
<div>git checkout {{$localBranch}}</div> <div>git checkout {{$args.LocalBranchArg}}</div>
</div> </div>
{{if $data.ShowMergeInstructions}} {{if $data.ShowMergeInstructions}}
<div> <div>
@@ -32,32 +29,32 @@
</div> </div>
<div class="ui secondary segment tw-font-mono"> <div class="ui secondary segment tw-font-mono">
<div data-pull-merge-style="merge"> <div data-pull-merge-style="merge">
<div>git checkout {{$pull.BaseBranch}}</div> <div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --no-ff {{$localBranch}}</div> <div>git merge --no-ff {{$args.LocalBranchArg}}</div>
</div> </div>
<div class="tw-hidden" data-pull-merge-style="rebase"> <div class="tw-hidden" data-pull-merge-style="rebase">
<div>git checkout {{$pull.BaseBranch}}</div> <div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --ff-only {{$localBranch}}</div> <div>git merge --ff-only {{$args.LocalBranchArg}}</div>
</div> </div>
<div class="tw-hidden" data-pull-merge-style="rebase-merge"> <div class="tw-hidden" data-pull-merge-style="rebase-merge">
<div>git checkout {{$localBranch}}</div> <div>git checkout {{$args.LocalBranchArg}}</div>
<div>git rebase {{$pull.BaseBranch}}</div> <div>git rebase {{$args.BaseBranchArg}}</div>
<div>git checkout {{$pull.BaseBranch}}</div> <div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --no-ff {{$localBranch}}</div> <div>git merge --no-ff {{$args.LocalBranchArg}}</div>
</div> </div>
<div class="tw-hidden" data-pull-merge-style="squash"> <div class="tw-hidden" data-pull-merge-style="squash">
<div>git checkout {{$pull.BaseBranch}}</div> <div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --squash {{$localBranch}}</div> <div>git merge --squash {{$args.LocalBranchArg}}</div>
</div> </div>
<div class="tw-hidden" data-pull-merge-style="fast-forward-only"> <div class="tw-hidden" data-pull-merge-style="fast-forward-only">
<div>git checkout {{$pull.BaseBranch}}</div> <div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --ff-only {{$localBranch}}</div> <div>git merge --ff-only {{$args.LocalBranchArg}}</div>
</div> </div>
<div class="tw-hidden" data-pull-merge-style="manually-merged"> <div class="tw-hidden" data-pull-merge-style="manually-merged">
<div>git checkout {{$pull.BaseBranch}}</div> <div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge {{$localBranch}}</div> <div>git merge {{$args.LocalBranchArg}}</div>
</div> </div>
<div>git push {{$gitRemoteName}} {{$pull.BaseBranch}}</div> <div>git push {{$gitRemoteName}} {{$args.BaseBranchArg}}</div>
</div> </div>
{{end}} {{end}}
</div> </div>
+2
View File
@@ -17,6 +17,8 @@ test('create a bot and manage its access token', async ({page, request}) => {
await page.getByRole('row', {name: /^user /}).getByRole('radio', {name: 'Read', exact: true}).check(); await page.getByRole('row', {name: /^user /}).getByRole('radio', {name: 'Read', exact: true}).check();
await page.getByRole('button', {name: 'Generate Token'}).click(); await page.getByRole('button', {name: 'Generate Token'}).click();
const token = await page.getByRole('code').textContent(); const token = await page.getByRole('code').textContent();
await page.getByRole('button', {name: 'Copy', exact: true}).click();
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(token);
const response = await request.get('/api/v1/user', {headers: {Authorization: `token ${token}`}}); const response = await request.get('/api/v1/user', {headers: {Authorization: `token ${token}`}});
expect(await response.json()).toMatchObject({login: botName, type: 'Bot'}); expect(await response.json()).toMatchObject({login: botName, type: 'Bot'});
@@ -38,6 +38,11 @@ func TestAPIGetIssueAttachment(t *testing.T) {
apiAttachment := DecodeJSON(t, resp, &api.Attachment{}) apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID}) unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
commentAttachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 3, RepoID: repo.ID})
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: commentAttachment.IssueID}).Index, commentAttachment.ID)).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusNotFound)
} }
func TestAPIListIssueAttachments(t *testing.T) { func TestAPIListIssueAttachments(t *testing.T) {
+27 -21
View File
@@ -104,6 +104,7 @@ func TestPackageNpm(t *testing.T) {
}, },
"cpu": ["x64", "arm64"], "cpu": ["x64", "arm64"],
"os": ["linux", "darwin"], "os": ["linux", "darwin"],
"libc": ["glibc"],
"directories": { "directories": {
"doc": "./doc", "doc": "./doc",
"man": "./man" "man": "./man"
@@ -170,8 +171,9 @@ func TestPackageNpm(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
rootPaths := []string{ rootPaths := []string{
fmt.Sprintf("/api/packages/%s/npm/@scope/test-package", user.Name), "/api/packages/user2/npm/@scope/test-package",
fmt.Sprintf("/api/packages/%s/npm/@scope%%2ftest-package", user.Name), "/api/packages/user2/npm/@scope%2Ftest-package",
"/api/packages/user2/npm/%40scope%2ftest-package",
} }
for _, root := range rootPaths { for _, root := range rootPaths {
req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename)).AddTokenAuth(token) req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename)).AddTokenAuth(token)
@@ -186,7 +188,7 @@ func TestPackageNpm(t *testing.T) {
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm) pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, pvs, 1) assert.Len(t, pvs, 1)
assert.Equal(t, int64(4), pvs[0].DownloadCount) assert.EqualValues(t, 6, pvs[0].DownloadCount)
}) })
t.Run("PackageMetadata", func(t *testing.T) { t.Run("PackageMetadata", func(t *testing.T) {
@@ -217,7 +219,7 @@ func TestPackageNpm(t *testing.T) {
assert.Equal(t, packageBinPath, pmv.Bin[packageBinName]) assert.Equal(t, packageBinPath, pmv.Bin[packageBinName])
assert.Equal(t, integrity, pmv.Dist.Integrity) assert.Equal(t, integrity, pmv.Dist.Integrity)
assert.Equal(t, sha1SumHex, pmv.Dist.Shasum) assert.Equal(t, sha1SumHex, pmv.Dist.Shasum)
assert.Equal(t, fmt.Sprintf("%s%s/-/%s/%s", setting.AppURL, root[1:], packageVersion, filename), pmv.Dist.Tarball) assert.Equal(t, fmt.Sprintf("%sapi/packages/%s/npm/%s/-/%s", setting.AppURL, user.Name, packageName, filename), pmv.Dist.Tarball)
assert.Equal(t, repoType, result.Repository.Type) assert.Equal(t, repoType, result.Repository.Type)
assert.Equal(t, repoURL, result.Repository.URL) assert.Equal(t, repoURL, result.Repository.URL)
assert.Equal(t, map[string]string{"tea": "2.x", "soy-milk": "1.2"}, pmv.PeerDependencies) assert.Equal(t, map[string]string{"tea": "2.x", "soy-milk": "1.2"}, pmv.PeerDependencies)
@@ -227,10 +229,24 @@ func TestPackageNpm(t *testing.T) {
assert.Equal(t, map[string]string{"node": ">=22.7.0", "npm": ">=10.8.2"}, pmv.Engines) assert.Equal(t, map[string]string{"node": ">=22.7.0", "npm": ">=10.8.2"}, pmv.Engines)
assert.Equal(t, []string{"x64", "arm64"}, pmv.CPU) assert.Equal(t, []string{"x64", "arm64"}, pmv.CPU)
assert.Equal(t, []string{"linux", "darwin"}, pmv.OS) assert.Equal(t, []string{"linux", "darwin"}, pmv.OS)
assert.Equal(t, []string{"glibc"}, pmv.Libc)
assert.Equal(t, map[string]string{"doc": "./doc", "man": "./man"}, pmv.Directories) assert.Equal(t, map[string]string{"doc": "./doc", "man": "./man"}, pmv.Directories)
assert.Equal(t, "https://example.com/fund", pmv.Funding) assert.Equal(t, "https://example.com/fund", pmv.Funding)
assert.Equal(t, map[string]string{"left-pad": "1.x"}, pmv.AcceptDependencies) assert.Equal(t, map[string]string{"left-pad": "1.x"}, pmv.AcceptDependencies)
assert.Empty(t, pmv.Deprecated) assert.Empty(t, pmv.Deprecated)
req = NewRequest(t, "GET", root).AddTokenAuth(token).SetHeader("If-None-Match", resp.Header().Get("ETag"))
MakeRequest(t, req, http.StatusNotModified)
})
t.Run("PingWhoami", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
registry := fmt.Sprintf("/api/packages/%s/npm/-/", user.Name)
MakeRequest(t, NewRequest(t, "GET", registry+"ping"), http.StatusOK)
MakeRequest(t, NewRequest(t, "GET", registry+"whoami"), http.StatusUnauthorized)
resp := MakeRequest(t, NewRequest(t, "GET", registry+"whoami").AddTokenAuth(token), http.StatusOK)
assert.JSONEq(t, `{"username":"`+user.Name+`"}`, resp.Body.String())
}) })
t.Run("PackageVersionMetadata", func(t *testing.T) { t.Run("PackageVersionMetadata", func(t *testing.T) {
@@ -289,22 +305,6 @@ func TestPackageNpm(t *testing.T) {
assert.Equal(t, packageVersion, result[packageTag2]) assert.Equal(t, packageVersion, result[packageTag2])
}) })
t.Run("PackageMetadataDistTags", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", root).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
result := DecodeJSON(t, resp, &npm.PackageMetadata{})
assert.Len(t, result.DistTags, 2)
assert.Contains(t, result.DistTags, packageTag)
assert.Equal(t, packageVersion, result.DistTags[packageTag])
assert.Contains(t, result.DistTags, packageTag2)
assert.Equal(t, packageVersion, result.DistTags[packageTag2])
})
t.Run("DeleteTag", func(t *testing.T) { t.Run("DeleteTag", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
@@ -318,6 +318,12 @@ func TestPackageNpm(t *testing.T) {
test(t, http.StatusBadRequest, "1.0") test(t, http.StatusBadRequest, "1.0")
test(t, http.StatusOK, "dummy") test(t, http.StatusOK, "dummy")
test(t, http.StatusOK, packageTag2) test(t, http.StatusOK, packageTag2)
test(t, http.StatusOK, packageTag)
resp := MakeRequest(t, NewRequest(t, "GET", tagsRoot).AddTokenAuth(token), http.StatusOK)
assert.Equal(t, map[string]string{packageTag: packageVersion}, DecodeJSON(t, resp, map[string]string{}))
resp = MakeRequest(t, NewRequest(t, "GET", root+"/"+packageTag).AddTokenAuth(token), http.StatusOK)
assert.Equal(t, packageVersion, DecodeJSON(t, resp, &npm.PackageMetadataVersion{}).Version)
}) })
t.Run("Search", func(t *testing.T) { t.Run("Search", func(t *testing.T) {
@@ -522,7 +528,7 @@ func TestPackageNpm(t *testing.T) {
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename)) req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
MakeRequest(t, req, http.StatusUnauthorized) MakeRequest(t, req, http.StatusUnauthorized)
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename)). req = NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/-rev/dummy", root, filename)).
AddTokenAuth(token) AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
@@ -22,6 +22,10 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo2/media/test.xml").AddTokenAuth(getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository))
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
// Test with LFS // Test with LFS
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
createLFSTestRepository(t, "repo-lfs-test") createLFSTestRepository(t, "repo-lfs-test")
+10
View File
@@ -16,6 +16,8 @@ import (
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
repo_service "gitea.dev/services/repository" repo_service "gitea.dev/services/repository"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -175,6 +177,14 @@ func TestGitPushVisibilityOption(t *testing.T) {
doGitPushTestRepository(gitPath, "origin", "branch2", "-o", "repo.private=false")(t) doGitPushTestRepository(gitPath, "origin", "branch2", "-o", "repo.private=false")(t)
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
assert.True(t, repo.IsPrivate, "repo.private option must be ignored on an existing repository") assert.True(t, repo.IsPrivate, "repo.private option must be ignored on an existing repository")
defer test.MockVariableValue(&setting.Repository.ForcePrivate, true)()
forcedRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{Name: "repo-visibility-forced", DefaultBranch: "master", IsPrivate: true})
require.NoError(t, err)
u.Path = forcedRepo.FullName() + ".git"
doGitAddRemote(gitPath, "forced", u)(t)
doGitPushTestRepository(gitPath, "forced", "master", "-o", "repo.private=false")(t)
assert.True(t, unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: forcedRepo.ID}).IsPrivate)
}) })
} }
+9
View File
@@ -571,6 +571,15 @@ func testRefreshTokenInvalidation(t *testing.T) {
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription) assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "https://example.com",
"refresh_token": parsed.AccessToken,
})
MakeRequest(t, req, http.StatusBadRequest)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token", "grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+6 -6
View File
@@ -378,11 +378,11 @@ func TestCantMergeConflict(t *testing.T) {
BaseBranch: "base", BaseBranch: "base",
}) })
err := pull_service.Merge(pr.ID, user1, repo_model.MergeStyleMerge, "", "CONFLICT", false) err := pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleMerge, "", "CONFLICT", false)
assert.Error(t, err, "Merge should return an error due to conflict") assert.Error(t, err, "Merge should return an error due to conflict")
assert.True(t, pull_service.IsErrMergeConflicts(err), "Merge error is not a conflict error") assert.True(t, pull_service.IsErrMergeConflicts(err), "Merge error is not a conflict error")
err = pull_service.Merge(pr.ID, user1, repo_model.MergeStyleRebase, "", "CONFLICT", false) err = pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleRebase, "", "CONFLICT", false)
assert.Error(t, err, "Merge should return an error due to conflict") assert.Error(t, err, "Merge should return an error due to conflict")
assert.True(t, pull_service.IsErrRebaseConflicts(err), "Merge error is not a conflict error") assert.True(t, pull_service.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
}) })
@@ -473,7 +473,7 @@ func TestCantMergeUnrelated(t *testing.T) {
BaseBranch: "base", BaseBranch: "base",
}) })
err = pull_service.Merge(pr.ID, user1, repo_model.MergeStyleMerge, "", "UNRELATED", false) err = pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleMerge, "", "UNRELATED", false)
assert.Error(t, err, "Merge should return an error due to unrelated") assert.Error(t, err, "Merge should return an error due to unrelated")
assert.True(t, pull_service.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error") assert.True(t, pull_service.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
}) })
@@ -509,7 +509,7 @@ func TestFastForwardOnlyMerge(t *testing.T) {
BaseBranch: "master", BaseBranch: "master",
}) })
err := pull_service.Merge(pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false) err := pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
assert.NoError(t, err) assert.NoError(t, err)
}) })
} }
@@ -596,7 +596,7 @@ func TestFastForwardOnlyMergeWithRequiredSignedCommits(t *testing.T) {
pb.RequireSignedCommits = false pb.RequireSignedCommits = false
require.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, pb, git_model.WhitelistOptions{})) require.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, pb, git_model.WhitelistOptions{}))
require.NoError(t, pull_service.Merge(pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)) require.NoError(t, pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false))
}) })
} }
@@ -631,7 +631,7 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
BaseBranch: "master", BaseBranch: "master",
}) })
err := pull_service.Merge(pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false) err := pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
assert.Error(t, err, "Merge should return an error due to being for a diverging branch") assert.Error(t, err, "Merge should return an error due to being for a diverging branch")
assert.True(t, pull_service.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error") assert.True(t, pull_service.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
}) })
+9
View File
@@ -84,6 +84,7 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22}) org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
req := NewRequest(t, "GET", "/"+org22.Name) req := NewRequest(t, "GET", "/"+org22.Name)
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", "/"+org22.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
session := loginUser(t, "user1") session := loginUser(t, "user1")
oldName := org22.Name oldName := org22.Name
@@ -106,6 +107,8 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23}) org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
req = NewRequest(t, "GET", "/"+org23.Name) req = NewRequest(t, "GET", "/"+org23.Name)
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
strangerSession := loginUser(t, "user4")
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+org23.Name+"?action=follow"), http.StatusNotFound)
oldName = org23.Name oldName = org23.Name
newName = "org23_renamed" newName = "org23_renamed"
@@ -127,6 +130,8 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31}) user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
req = NewRequest(t, "GET", "/"+user31.Name) req = NewRequest(t, "GET", "/"+user31.Name)
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", "/"+user31.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+user31.Name+"?action=follow"), http.StatusNotFound)
oldName = user31.Name oldName = user31.Name
newName = "user31_renamed" newName = "user31_renamed"
@@ -330,6 +335,10 @@ func testGetUserRss(t *testing.T) {
session := loginUser(t, "user2") session := loginUser(t, "user2")
req = NewRequestf(t, "GET", "/non-existent-user.rss") req = NewRequestf(t, "GET", "/non-existent-user.rss")
session.MakeRequest(t, req, http.StatusNotFound) session.MakeRequest(t, req, http.StatusNotFound)
defer test.MockVariableValue(&setting.Other.EnableFeed, false)()
MakeRequest(t, NewRequestf(t, "GET", "/%s.rss", user34), http.StatusNotFound)
MakeRequest(t, NewRequestf(t, "GET", "/%s", user34).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
} }
func testUserListStopWatches(t *testing.T) { func testUserListStopWatches(t *testing.T) {
+2 -4
View File
@@ -64,7 +64,7 @@ function replaceWithFeedbackSvg(origSvg: SVGElement, success: boolean): () => vo
// Enable clipboard copy from HTML attributes. These properties are supported: // Enable clipboard copy from HTML attributes. These properties are supported:
// - data-clipboard-text: Direct text to copy // - data-clipboard-text: Direct text to copy
// - data-clipboard-target: Holds a selector for an element. "value" of <input> or <textarea>, or "textContent" of <div> will be copied // - data-clipboard-target: Holds a selector for an element. "value" of <input> or <textarea>, or "textContent" of other elements will be copied
export function initGlobalCopyToClipboardListener() { export function initGlobalCopyToClipboardListener() {
document.addEventListener('click', async (e) => { document.addEventListener('click', async (e) => {
const target = (e.target as HTMLElement).closest<HTMLElement>('[data-clipboard-text], [data-clipboard-target]'); const target = (e.target as HTMLElement).closest<HTMLElement>('[data-clipboard-text], [data-clipboard-target]');
@@ -78,10 +78,8 @@ export function initGlobalCopyToClipboardListener() {
const textTarget = document.querySelector(textSelector)!; const textTarget = document.querySelector(textSelector)!;
if (textTarget.nodeName === 'INPUT' || textTarget.nodeName === 'TEXTAREA') { if (textTarget.nodeName === 'INPUT' || textTarget.nodeName === 'TEXTAREA') {
text = (textTarget as HTMLInputElement | HTMLTextAreaElement).value; text = (textTarget as HTMLInputElement | HTMLTextAreaElement).value;
} else if (textTarget.nodeName === 'DIV') {
text = textTarget.textContent;
} else { } else {
throw new Error(`Unsupported element for clipboard target: ${textSelector}`); text = textTarget.textContent;
} }
} }
// now, text can not be null // now, text can not be null