mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-26 13:29:52 +09:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068355cabd | ||
|
|
3f93080f48 | ||
|
|
d2572c7342 | ||
|
|
b70f7aeda1 | ||
|
|
3af1ee88a5 | ||
|
|
6d348e57bc | ||
|
|
565e12d47b | ||
|
|
25fff4c043 | ||
|
|
6e2cf10687 | ||
|
|
1e86e1e45a | ||
|
|
0d946be8be | ||
|
|
3f01f06d3b | ||
|
|
9fe8653e45 | ||
|
|
1350cf8a38 | ||
|
|
f64ee9d1ed | ||
|
|
b5c6b80790 | ||
|
|
c9da6553a2 | ||
|
|
dbc17db0c2 | ||
|
|
bf7b6f8bd4 | ||
|
|
76c768edca | ||
|
|
320b44df58 | ||
|
|
078bb4fa07 | ||
|
|
883ed3b0bd | ||
|
|
eee37e0560 | ||
|
|
4e515cce99 | ||
|
|
d8e179f28f | ||
|
|
616dbddda4 | ||
|
|
6024280a23 | ||
|
|
96c651382f | ||
|
|
50be834d24 | ||
|
|
f4bff43d46 | ||
|
|
11de54dd41 | ||
|
|
1c3ae57f35 | ||
|
|
cbcf36cded | ||
|
|
5a665c0b0e | ||
|
|
0aa0ea17bc | ||
|
|
11cd8ff69a | ||
|
|
33498f828d |
@@ -51,7 +51,7 @@ jobs:
|
||||
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
|
||||
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
# clean branch name to get the folder name in the object storage
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
@@ -60,25 +60,11 @@ jobs:
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
|
||||
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
|
||||
- name: upload binaries to cloudflare r2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_REGION: auto
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
|
||||
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
# clean branch name to get the folder name in the object storage
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
@@ -61,25 +61,11 @@ jobs:
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
|
||||
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
|
||||
- name: upload binaries to cloudflare r2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_REGION: auto
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
|
||||
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
# clean branch name to get the folder name in the object storage
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
@@ -64,25 +64,11 @@ jobs:
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
|
||||
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
|
||||
- name: upload binaries to cloudflare r2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_REGION: auto
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
|
||||
@@ -5,60 +5,14 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func defaultLoggingFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "logger",
|
||||
Usage: `Logger name - will default to "default"`,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "writer",
|
||||
Usage: "Name of the log writer - will default to mode",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "level",
|
||||
Usage: "Logging level for the new logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "stacktrace-level",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Stacktrace logging level",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "flags",
|
||||
Aliases: []string{"F"},
|
||||
Usage: "Flags for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "expression",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Matching expression for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "prefix",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Prefix for the logger",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "color",
|
||||
Usage: "Use color in the logs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newLoggingCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "logging",
|
||||
@@ -91,92 +45,6 @@ func newLoggingCommand() *cli.Command {
|
||||
},
|
||||
},
|
||||
Action: runReleaseReopenLogging,
|
||||
}, {
|
||||
Name: "remove",
|
||||
Usage: "Remove a logger",
|
||||
ArgsUsage: "[name] Name of logger to remove",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
}, &cli.StringFlag{
|
||||
Name: "logger",
|
||||
Usage: `Logger name - will default to "default"`,
|
||||
},
|
||||
},
|
||||
Action: runRemoveLogger,
|
||||
}, {
|
||||
Name: "add",
|
||||
Usage: "Add a logger",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "file",
|
||||
Usage: "Add a file logger",
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "filename",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Filename for the logger - this must be set.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "rotate",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Rotate logs",
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "max-size",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Maximum size in bytes before rotation",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "daily",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Rotate logs daily",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-days",
|
||||
Aliases: []string{"D"},
|
||||
Usage: "Maximum number of daily logs to keep",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "compress",
|
||||
Aliases: []string{"z"},
|
||||
Usage: "Compress rotated logs",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "compression-level",
|
||||
Aliases: []string{"Z"},
|
||||
Usage: "Compression level to use",
|
||||
},
|
||||
}...),
|
||||
Action: runAddFileLogger,
|
||||
}, {
|
||||
Name: "conn",
|
||||
Usage: "Add a net conn logger",
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "reconnect-on-message",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Reconnect to host for every message",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "reconnect",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Reconnect to host when connection is dropped",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "protocol",
|
||||
Aliases: []string{"P"},
|
||||
Usage: "Set protocol to use: tcp, unix, or udp (defaults to tcp)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Host address and port to connect to (defaults to :7020)",
|
||||
},
|
||||
}...),
|
||||
Action: runAddConnLogger,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "log-sql",
|
||||
Usage: "Set LogSQL",
|
||||
@@ -195,107 +63,6 @@ func newLoggingCommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
logger := c.String("logger")
|
||||
if len(logger) == 0 {
|
||||
logger = log.DEFAULT
|
||||
}
|
||||
writer := c.Args().First()
|
||||
|
||||
extra := private.RemoveLogger(ctx, logger, writer)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "conn"
|
||||
vals["net"] = "tcp"
|
||||
if c.IsSet("protocol") {
|
||||
switch c.String("protocol") {
|
||||
case "udp":
|
||||
vals["net"] = "udp"
|
||||
case "unix":
|
||||
vals["net"] = "unix"
|
||||
}
|
||||
}
|
||||
if c.IsSet("address") {
|
||||
vals["address"] = c.String("address")
|
||||
} else {
|
||||
vals["address"] = ":7020"
|
||||
}
|
||||
if c.IsSet("reconnect") {
|
||||
vals["reconnect"] = c.Bool("reconnect")
|
||||
}
|
||||
if c.IsSet("reconnect-on-message") {
|
||||
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
|
||||
}
|
||||
return commonAddLogger(ctx, c, mode, vals)
|
||||
}
|
||||
|
||||
func runAddFileLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "file"
|
||||
if c.IsSet("filename") {
|
||||
vals["filename"] = c.String("filename")
|
||||
} else {
|
||||
return errors.New("filename must be set when creating a file logger")
|
||||
}
|
||||
if c.IsSet("rotate") {
|
||||
vals["rotate"] = c.Bool("rotate")
|
||||
}
|
||||
if c.IsSet("max-size") {
|
||||
vals["maxsize"] = c.Int64("max-size")
|
||||
}
|
||||
if c.IsSet("daily") {
|
||||
vals["daily"] = c.Bool("daily")
|
||||
}
|
||||
if c.IsSet("max-days") {
|
||||
vals["maxdays"] = c.Int("max-days")
|
||||
}
|
||||
if c.IsSet("compress") {
|
||||
vals["compress"] = c.Bool("compress")
|
||||
}
|
||||
if c.IsSet("compression-level") {
|
||||
vals["compressionLevel"] = c.Int("compression-level")
|
||||
}
|
||||
return commonAddLogger(ctx, c, mode, vals)
|
||||
}
|
||||
|
||||
func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
|
||||
if len(c.String("level")) > 0 {
|
||||
vals["level"] = log.LevelFromString(c.String("level")).String()
|
||||
}
|
||||
if len(c.String("stacktrace-level")) > 0 {
|
||||
vals["stacktraceLevel"] = log.LevelFromString(c.String("stacktrace-level")).String()
|
||||
}
|
||||
if len(c.String("expression")) > 0 {
|
||||
vals["expression"] = c.String("expression")
|
||||
}
|
||||
if len(c.String("prefix")) > 0 {
|
||||
vals["prefix"] = c.String("prefix")
|
||||
}
|
||||
if len(c.String("flags")) > 0 {
|
||||
vals["flags"] = log.FlagsFromString(c.String("flags"))
|
||||
}
|
||||
if c.IsSet("color") {
|
||||
vals["colorize"] = c.Bool("color")
|
||||
}
|
||||
logger := log.DEFAULT
|
||||
if c.IsSet("logger") {
|
||||
logger = c.String("logger")
|
||||
}
|
||||
writer := mode
|
||||
if c.IsSet("writer") {
|
||||
writer = c.String("writer")
|
||||
}
|
||||
|
||||
extra := private.AddLogger(ctx, logger, writer, mode, vals)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runPauseLogging(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
userMsg := private.PauseLogging(ctx)
|
||||
|
||||
@@ -606,7 +606,8 @@ ENABLED = true
|
||||
;; * https://github.com/hickford/git-credential-oauth
|
||||
;; * https://github.com/git-ecosystem/git-credential-manager
|
||||
;; * https://gitea.com/gitea/tea
|
||||
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea
|
||||
;; * Gitea App (the official Gitea mobile app)
|
||||
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea, gitea-app
|
||||
;;
|
||||
;; By default, OAuth2 applications can only use "http" and "https" as their redirect URI schemes.
|
||||
;; If you need to use other schemes (e.g. for desktop applications), you can specify them here as a comma-separated list.
|
||||
@@ -1122,7 +1123,6 @@ LEVEL = Info
|
||||
;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true
|
||||
|
||||
;; Allow to fork repositories into the same owner (user or organization)
|
||||
;; This feature is experimental, not fully tested, and may be changed in the future
|
||||
;ALLOW_FORK_INTO_SAME_OWNER = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -83,6 +83,11 @@ func BuiltinApplications() map[string]*BuiltinOAuth2Application {
|
||||
DisplayName: "tea",
|
||||
RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
|
||||
}
|
||||
m["b757811a-05c8-4c76-8d74-a5ee3d2073f2"] = &BuiltinOAuth2Application{
|
||||
ConfigName: "gitea-app",
|
||||
DisplayName: "Gitea App",
|
||||
RedirectURIs: []string{"com.gitea.app://oauth/callback"},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ func applyRepoConditions(sess db.Session, opts *IssuesOptions) {
|
||||
if opts.RepoCond == nil {
|
||||
opts.RepoCond = builder.NewCond()
|
||||
}
|
||||
opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"is_private": false})))
|
||||
opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(repo_model.PublicRepoUnderPublicOwnerCond())))
|
||||
}
|
||||
if opts.RepoCond != nil {
|
||||
sess.And(opts.RepoCond)
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
package issues
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -191,6 +193,42 @@ func (l *Label) ExclusiveScope() string {
|
||||
return l.Name[:lastIndex]
|
||||
}
|
||||
|
||||
// CompareLabelForDisplay compares labels for displaying them in dropdowns or lists.
|
||||
// Labels are grouped by their exclusive scope, and labels within the same scope
|
||||
// are sorted by their exclusive order, where unordered labels (order 0) come last.
|
||||
// Labels without a scope are listed first and everything else falls back to name order.
|
||||
func CompareLabelForDisplay(a, b *Label) int {
|
||||
scopeA, scopeB := a.ExclusiveScope(), b.ExclusiveScope()
|
||||
if scopeA != scopeB {
|
||||
if scopeA == "" {
|
||||
return -1
|
||||
}
|
||||
if scopeB == "" {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(scopeA, scopeB)
|
||||
}
|
||||
if scopeA != "" {
|
||||
orderA, orderB := a.ExclusiveOrder, b.ExclusiveOrder
|
||||
if orderA <= 0 {
|
||||
orderA = math.MaxInt
|
||||
}
|
||||
if orderB <= 0 {
|
||||
orderB = math.MaxInt
|
||||
}
|
||||
if orderA != orderB {
|
||||
return cmp.Compare(orderA, orderB)
|
||||
}
|
||||
}
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
|
||||
// SortLabelsForDisplay sorts labels in place for displaying them in dropdowns or lists,
|
||||
// grouping them by their exclusive scope and respecting the exclusive order within each scope.
|
||||
func SortLabelsForDisplay(labels []*Label) {
|
||||
slices.SortStableFunc(labels, CompareLabelForDisplay)
|
||||
}
|
||||
|
||||
// NewLabel creates a new label
|
||||
func NewLabel(ctx context.Context, l *Label) error {
|
||||
color, err := label.NormalizeColor(l.Color)
|
||||
|
||||
@@ -54,6 +54,47 @@ func TestLabel_ExclusiveScope(t *testing.T) {
|
||||
assert.Equal(t, "scope/subscope", label.ExclusiveScope())
|
||||
}
|
||||
|
||||
func TestSortLabelsForDisplay(t *testing.T) {
|
||||
labels := []*issues_model.Label{
|
||||
{Name: "priority/low", Exclusive: true, ExclusiveOrder: 4},
|
||||
{Name: "priority/critical", Exclusive: true, ExclusiveOrder: 1},
|
||||
{Name: "priority/medium", Exclusive: true, ExclusiveOrder: 3},
|
||||
{Name: "priority/high", Exclusive: true, ExclusiveOrder: 2},
|
||||
{Name: "bug"},
|
||||
{Name: "enhancement"},
|
||||
{Name: "kind/question", Exclusive: true},
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(labels)
|
||||
|
||||
names := make([]string, 0, len(labels))
|
||||
for _, l := range labels {
|
||||
names = append(names, l.Name)
|
||||
}
|
||||
assert.Equal(t, []string{
|
||||
"bug",
|
||||
"enhancement",
|
||||
"kind/question",
|
||||
"priority/critical",
|
||||
"priority/high",
|
||||
"priority/medium",
|
||||
"priority/low",
|
||||
}, names)
|
||||
|
||||
// labels without an exclusive order in the same scope are listed last, ordered by name
|
||||
labels = []*issues_model.Label{
|
||||
{Name: "scope/unordered-b", Exclusive: true},
|
||||
{Name: "scope/ordered", Exclusive: true, ExclusiveOrder: 1},
|
||||
{Name: "scope/unordered-a", Exclusive: true},
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(labels)
|
||||
|
||||
names = names[:0]
|
||||
for _, l := range labels {
|
||||
names = append(names, l.Name)
|
||||
}
|
||||
assert.Equal(t, []string{"scope/ordered", "scope/unordered-a", "scope/unordered-b"}, names)
|
||||
}
|
||||
|
||||
func TestNewLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labels := []*issues_model.Label{
|
||||
|
||||
+13
-9
@@ -1065,19 +1065,23 @@ func (r *Review) GetCodeCommentsCount(ctx context.Context) int {
|
||||
return int(count)
|
||||
}
|
||||
|
||||
// HTMLURL formats a URL-string to the related review issue-comment
|
||||
// HashTag returns unique hash tag for review.
|
||||
func (r *Review) HashTag() string {
|
||||
return fmt.Sprintf("pullrequestreview-%d", r.ID)
|
||||
}
|
||||
|
||||
// HTMLURL formats a URL-string to the review on the pull request page
|
||||
func (r *Review) HTMLURL(ctx context.Context) string {
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeReview,
|
||||
IssueID: r.IssueID,
|
||||
ReviewID: r.ID,
|
||||
if r.Type != ReviewTypeApprove && r.Type != ReviewTypeComment && r.Type != ReviewTypeReject {
|
||||
return "" // only submitted reviews get a timeline block carrying the anchor
|
||||
}
|
||||
comment := new(Comment)
|
||||
has, err := db.GetEngine(ctx).Where(opts.ToConds()).Get(comment)
|
||||
if err != nil || !has {
|
||||
if err := r.LoadIssue(ctx); err != nil {
|
||||
return ""
|
||||
}
|
||||
return comment.HTMLURL(ctx)
|
||||
if err := r.Issue.LoadRepo(ctx); err != nil {
|
||||
return ""
|
||||
}
|
||||
return r.Issue.HTMLURL(ctx) + "#" + r.HashTag()
|
||||
}
|
||||
|
||||
// RemapExternalUser ExternalUserRemappable interface
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -28,6 +29,15 @@ func TestGetReviewByID(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist")
|
||||
}
|
||||
|
||||
func TestReview_HTMLURL(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1})
|
||||
assert.Equal(t, setting.AppURL+"user2/repo1/pulls/2#pullrequestreview-1", review.HTMLURL(t.Context()))
|
||||
|
||||
pendingReview := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 4})
|
||||
assert.Empty(t, pendingReview.HTMLURL(t.Context()))
|
||||
}
|
||||
|
||||
func TestReview_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1})
|
||||
|
||||
@@ -303,7 +303,7 @@ func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.Us
|
||||
}
|
||||
}
|
||||
|
||||
if org.Visibility.IsPublic() {
|
||||
if ownerVisibilitySatisfiesDoer(org.AsUser(), doer) {
|
||||
return perm.AccessModeRead
|
||||
}
|
||||
|
||||
@@ -445,8 +445,7 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u
|
||||
And("team_user.org_id = ?", orgID).Find(&users)
|
||||
}
|
||||
|
||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
|
||||
func ownerVisibilitySatisfiesDoer(orgOrUser, user *user_model.User) bool {
|
||||
// If user is nil, it's an anonymous user/request.
|
||||
// The Ghost user is handled like an anonymous user.
|
||||
if user == nil || user.IsGhost() {
|
||||
@@ -461,10 +460,13 @@ func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User)
|
||||
return true
|
||||
}
|
||||
|
||||
if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return orgOrUser.Visibility != structs.VisibleTypePrivate && !user.IsRestricted
|
||||
}
|
||||
|
||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||
func HasOrgOrUserVisible(ctx context.Context, owner, doer *user_model.User) bool {
|
||||
return ownerVisibilitySatisfiesDoer(owner, doer) ||
|
||||
(doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID))
|
||||
}
|
||||
|
||||
// HasOrgsVisible tells if the given user can see at least one of the orgs provided
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -626,3 +628,10 @@ func TestCreateOrganization4(t *testing.T) {
|
||||
assert.True(t, db.IsErrNameReserved(err))
|
||||
unittest.CheckConsistencyFor(t, &organization.Organization{}, &organization.Team{})
|
||||
}
|
||||
|
||||
func TestOrAnyRepoUnitPermission(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
org := organization.Organization{Visibility: structs.VisibleTypeLimited}
|
||||
assert.Equal(t, perm.AccessModeNone, org.UnitPermission(t.Context(), nil, unit.TypeWiki))
|
||||
assert.Equal(t, perm.AccessModeRead, org.UnitPermission(t.Context(), &user_model.User{}, unit.TypeWiki))
|
||||
}
|
||||
|
||||
@@ -122,6 +122,18 @@ func TestGetActionsUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.False(t, perm.CanRead(unit.TypeCode))
|
||||
|
||||
// Reusable workflows use a separate authorization path and must enforce
|
||||
// the same fork-PR restriction.
|
||||
run := &actions_model.ActionRun{RepoID: repo2.ID, IsForkPullRequest: true}
|
||||
allowed, err := CanReadWorkflowCrossRepo(ctx, repo15, run)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, allowed)
|
||||
|
||||
run.IsForkPullRequest = false
|
||||
allowed, err = CanReadWorkflowCrossRepo(ctx, repo15, run)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, allowed)
|
||||
|
||||
// Restore state for subsequent subtests.
|
||||
task53.IsForkPullRequest = false
|
||||
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
||||
|
||||
@@ -675,7 +675,7 @@ func CanReadWorkflowCrossRepo(ctx context.Context, targetRepo *repo_model.Reposi
|
||||
// logs in a publicly visible run; requiring a private caller keeps private content flowing private -> private.
|
||||
// This is intentionally stricter than GitHub, which gates on the target repo's access setting (introduced in #32562):
|
||||
// https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-a-private-repository
|
||||
if run.Repo.IsPrivate {
|
||||
if run.Repo.IsPrivate && !run.IsForkPullRequest {
|
||||
if actionsUnit, err := targetRepo.GetUnit(ctx, unit.TypeActions); err == nil {
|
||||
if actionsUnit.ActionsConfig().IsCollaborativeOwner(run.Repo.OwnerID) {
|
||||
return true, nil
|
||||
|
||||
@@ -655,14 +655,12 @@ func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]i
|
||||
Find(&repoIDs)
|
||||
}
|
||||
|
||||
func userAllPublicRepoCond(cond builder.Cond, orgVisibilityLimit []structs.VisibleType) builder.Cond {
|
||||
func userAllPublicRepoCond(cond builder.Cond, ownerVisibilityLimit []structs.VisibleType) builder.Cond {
|
||||
return cond.Or(builder.And(
|
||||
builder.Eq{"`repository`.is_private": false},
|
||||
// Aren't in a private organisation or limited organisation if we're not logged in
|
||||
// Exclude owners who are not visible to the caller.
|
||||
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
|
||||
builder.And(
|
||||
builder.Eq{"type": user_model.UserTypeOrganization},
|
||||
builder.In("visibility", orgVisibilityLimit)),
|
||||
builder.In("visibility", ownerVisibilityLimit),
|
||||
))))
|
||||
}
|
||||
|
||||
@@ -771,6 +769,15 @@ func PublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||
)
|
||||
}
|
||||
|
||||
// NotPublicRepoUnderPublicOwnerCond complements PublicRepoUnderPublicOwnerCond. Spelled positively so
|
||||
// the owner subquery hashes the limited/private minority, not every public user.
|
||||
func NotPublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||
return builder.Or(
|
||||
builder.Eq{"`repository`.is_private": true},
|
||||
builder.In("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Neq{"visibility": structs.VisibleTypePublic})),
|
||||
)
|
||||
}
|
||||
|
||||
// UserActionsAccessibleOwnerRepoCond selects the repos owned by ownerID whose Actions `user` may read.
|
||||
// It is used to list an org/user's Actions runs and jobs (see the callers in routers/api/v1/shared).
|
||||
// - owner_id = ownerID: only that owner's repos.
|
||||
|
||||
@@ -271,6 +271,23 @@ func testSearchRepositoryRestricted(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchRepositoryExcludesHiddenIndividualOwners(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
hiddenOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
require.NoError(t, user_model.UpdateUserCols(t.Context(), &user_model.User{
|
||||
ID: hiddenOwner.ID,
|
||||
Visibility: structs.VisibleTypePrivate,
|
||||
}, "visibility"))
|
||||
|
||||
repos, _, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: 100},
|
||||
Keyword: "repo1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, repoIDs(repos), int64(1))
|
||||
}
|
||||
|
||||
func testSearchRepositoryPrivate(t *testing.T) {
|
||||
// test search private repository on explore page
|
||||
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// maxExpandedNodes bounds how many nodes alias expansion may create. go-yaml's own alias guard does
|
||||
// not cover us: it only counts while decoding into values, and a workflow is kept as raw yaml.Nodes.
|
||||
const maxExpandedNodes = 50000
|
||||
|
||||
var errTooManyYamlNodes = errors.New("maximum YAML nodes exceeded")
|
||||
|
||||
// ReadWorkflow decodes a workflow file with its aliases expanded. Callers inspect the workflow's
|
||||
// raw nodes by kind, and an alias is a kind none of them expect.
|
||||
func ReadWorkflow(content []byte) (*model.Workflow, error) {
|
||||
doc, err := resolveYamlAliases(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return readWorkflowDoc(doc)
|
||||
}
|
||||
|
||||
func readWorkflowDoc(doc *yaml.Node) (*model.Workflow, error) {
|
||||
if doc.Kind == 0 {
|
||||
return nil, io.EOF // what a yaml decoder reports for an empty file
|
||||
}
|
||||
w := new(model.Workflow)
|
||||
return w, doc.Decode(w)
|
||||
}
|
||||
|
||||
// decodeResolved is yaml.Unmarshal with aliases expanded first.
|
||||
func decodeResolved(content []byte, out any) error {
|
||||
doc, err := resolveYamlAliases(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decodeYamlDoc(doc, out)
|
||||
}
|
||||
|
||||
func decodeYamlDoc(doc *yaml.Node, out any) error {
|
||||
if doc.Kind == 0 {
|
||||
return nil // an empty document, as yaml.Unmarshal treats it
|
||||
}
|
||||
return doc.Decode(out)
|
||||
}
|
||||
|
||||
// resolveYamlAliases parses content and replaces every alias with a copy of the node its anchor names.
|
||||
func resolveYamlAliases(content []byte) (*yaml.Node, error) {
|
||||
doc := &yaml.Node{}
|
||||
if err := yaml.Unmarshal(content, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
budget := maxExpandedNodes
|
||||
return doc, expandAliases(doc, &budget)
|
||||
}
|
||||
|
||||
// expandAliases replaces node's alias descendants in place.
|
||||
func expandAliases(node *yaml.Node, budget *int) error {
|
||||
node.Anchor = "" // a name for a node, not part of the workflow: keep it out of the payloads
|
||||
if err := rejectMergeKeys(node); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, child := range node.Content {
|
||||
if child.Kind != yaml.AliasNode {
|
||||
if err := expandAliases(child, budget); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
copied, err := copyExpanded(child.Alias, budget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node.Content[i] = copied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyExpanded deep copies a node expandAliases already expanded and validated, since an anchor is
|
||||
// declared before the alias naming it. An anchor aliased from inside itself is the exception, and
|
||||
// recurses here until it exhausts budget.
|
||||
func copyExpanded(node *yaml.Node, budget *int) (*yaml.Node, error) {
|
||||
if *budget--; *budget < 0 {
|
||||
return nil, errTooManyYamlNodes
|
||||
}
|
||||
if node.Kind == yaml.AliasNode {
|
||||
return copyExpanded(node.Alias, budget)
|
||||
}
|
||||
|
||||
copied := *node
|
||||
copied.Content = make([]*yaml.Node, len(node.Content))
|
||||
for i, child := range node.Content {
|
||||
child, err := copyExpanded(child, budget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copied.Content[i] = child
|
||||
}
|
||||
return &copied, nil
|
||||
}
|
||||
|
||||
// rejectMergeKeys refuses `<<: *anchor`, same as GitHub does
|
||||
func rejectMergeKeys(node *yaml.Node) error {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < len(node.Content)-1; i += 2 {
|
||||
if node.Content[i].Tag == "!!merge" {
|
||||
return errors.New("merge keys (`<<`) are not supported, alias the whole value instead")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseResolvesAliases(t *testing.T) {
|
||||
got, err := Parse([]byte(`on: push
|
||||
env: &common_env
|
||||
SHARED: "1"
|
||||
jobs:
|
||||
a:
|
||||
runs-on: linux
|
||||
env: *common_env
|
||||
steps: &common_steps [{run: echo hi}]
|
||||
b:
|
||||
runs-on: linux
|
||||
env: *common_env
|
||||
steps: *common_steps
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
for _, workflow := range got {
|
||||
_, job := workflow.Job()
|
||||
var env map[string]string
|
||||
require.NoError(t, job.Env.Decode(&env))
|
||||
assert.Equal(t, map[string]string{"SHARED": "1"}, env)
|
||||
require.Len(t, job.Steps, 1)
|
||||
|
||||
payload, err := workflow.Marshal()
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(payload), "common_")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsAliases(t *testing.T) {
|
||||
job := func(body string) []byte {
|
||||
return []byte("on: push\njobs:\n a:\n runs-on: linux\n" + body)
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name, wantErr string
|
||||
content []byte
|
||||
}{
|
||||
{
|
||||
name: "nested aliases exceed the node limit",
|
||||
content: []byte(`on: push
|
||||
x0: &x0 [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
x1: &x1 [*x0, *x0, *x0, *x0, *x0, *x0, *x0, *x0, *x0]
|
||||
x2: &x2 [*x1, *x1, *x1, *x1, *x1, *x1, *x1, *x1, *x1]
|
||||
x3: &x3 [*x2, *x2, *x2, *x2, *x2, *x2, *x2, *x2, *x2]
|
||||
x4: &x4 [*x3, *x3, *x3, *x3, *x3, *x3, *x3, *x3, *x3]
|
||||
jobs: {a: {runs-on: linux, steps: [{run: echo}]}}
|
||||
`),
|
||||
wantErr: "maximum YAML nodes exceeded",
|
||||
},
|
||||
{
|
||||
name: "anchor aliased from inside itself",
|
||||
content: job(" steps: &s [{run: echo}, *s]\n"),
|
||||
wantErr: "maximum YAML nodes exceeded",
|
||||
},
|
||||
{
|
||||
name: "merge key",
|
||||
content: job(" env: &e {X: \"1\"}\n container:\n image: alpine\n env:\n <<: *e\n"),
|
||||
wantErr: "merge keys (`<<`) are not supported",
|
||||
},
|
||||
{
|
||||
name: "alias before its anchor",
|
||||
content: job(" env: *e\n container: {image: alpine, env: &e {X: \"1\"}}\n"),
|
||||
wantErr: "unknown anchor 'e' referenced",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := Parse(tt.content)
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -17,14 +16,21 @@ import (
|
||||
)
|
||||
|
||||
func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
||||
origin, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
// The workflow is split into one document per job below, which would strand an alias whose
|
||||
// anchor lands in another one.
|
||||
doc, err := resolveYamlAliases(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("model.ReadWorkflow: %w", err)
|
||||
return nil, fmt.Errorf("resolve aliases: %w", err)
|
||||
}
|
||||
|
||||
origin, err := readWorkflowDoc(doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read workflow: %w", err)
|
||||
}
|
||||
|
||||
workflow := &SingleWorkflow{}
|
||||
if err := yaml.Unmarshal(content, workflow); err != nil {
|
||||
return nil, fmt.Errorf("yaml.Unmarshal: %w", err)
|
||||
if err := decodeYamlDoc(doc, workflow); err != nil {
|
||||
return nil, fmt.Errorf("decode workflow: %w", err)
|
||||
}
|
||||
|
||||
pc := &parseContext{}
|
||||
@@ -125,6 +131,9 @@ type parseContext struct {
|
||||
type ParseOption func(c *parseContext)
|
||||
|
||||
func getMatrixes(job *model.Job) ([]map[string]any, error) {
|
||||
if err := validateMatrixFilters(job); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret, err := job.GetMatrixes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetMatrixes: %w", err)
|
||||
@@ -135,6 +144,32 @@ func getMatrixes(job *model.Job) ([]map[string]any, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// validateMatrixFilters rejects an `include`/`exclude` that is not a list of mappings, so that the
|
||||
// usual way to get there, an unevaluated ${{ }} expression that is still a scalar, is named as such
|
||||
// instead of panicking inside the expansion.
|
||||
func validateMatrixFilters(job *model.Job) error {
|
||||
if job.Strategy == nil || job.Strategy.RawMatrix.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
content := job.Strategy.RawMatrix.Content
|
||||
for i := 0; i+1 < len(content); i += 2 {
|
||||
name, value := content[i].Value, content[i+1]
|
||||
if name != "include" && name != "exclude" {
|
||||
continue
|
||||
}
|
||||
entries := []*yaml.Node{value}
|
||||
if value.Kind == yaml.SequenceNode {
|
||||
entries = value.Content
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("matrix %s must be a list of mappings", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeMatrix(matrix map[string]any) yaml.Node {
|
||||
if len(matrix) == 0 {
|
||||
return yaml.Node{}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -147,3 +148,47 @@ func TestParseInterpolatesRunName(t *testing.T) {
|
||||
require.Len(t, result, 1)
|
||||
assert.Empty(t, result[0].RunName)
|
||||
}
|
||||
|
||||
func TestRejectsUnevaluatedMatrixFilters(t *testing.T) {
|
||||
// An unevaluated ${{ }} expression is still a scalar, which act cannot apply as a filter: it used
|
||||
// to reach the expansion and panic there on an unchecked type assertion, taking down the file view
|
||||
// and the push_update queue.
|
||||
const workflow = `
|
||||
name: t
|
||||
on: push
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
m: ${{ steps.s.outputs.m }}
|
||||
steps: [{id: s, run: echo}]
|
||||
build:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
%s
|
||||
steps: [{run: echo}]
|
||||
`
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
matrix string
|
||||
}{
|
||||
{name: "include expression", matrix: "include: ${{ fromJson(needs.setup.outputs.m) }}"},
|
||||
{name: "exclude expression", matrix: "os: [a, b]\n exclude: ${{ fromJson(vars.MATRIX) }}"},
|
||||
{name: "include scalar", matrix: "include: whatever"},
|
||||
{name: "include list of scalars", matrix: "include: [a, b]"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.NotPanics(t, func() {
|
||||
_, err := Parse(fmt.Appendf(nil, workflow, tt.matrix))
|
||||
require.ErrorContains(t, err, "must be a list of mappings")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// a well-formed include/exclude keeps expanding
|
||||
planned, err := Parse(fmt.Appendf(nil, workflow, "os: [a, b]\n include:\n - os: c\n exclude:\n - os: b"))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, planned, 3) // setup, plus build for os a and c
|
||||
}
|
||||
|
||||
@@ -257,9 +257,11 @@ func (evt *Event) Inputs() []WorkflowDispatchInput {
|
||||
}
|
||||
|
||||
func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) {
|
||||
w := new(model.Workflow)
|
||||
err := yaml.NewDecoder(bytes.NewReader(content)).Decode(w)
|
||||
return w.RawConcurrency, err
|
||||
w, err := ReadWorkflow(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w.RawConcurrency, nil
|
||||
}
|
||||
|
||||
func EvaluateConcurrency(rc *model.RawConcurrency, jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (string, bool, error) {
|
||||
|
||||
@@ -62,7 +62,7 @@ func ParseWorkflowCallSpec(content []byte) (*WorkflowCallSpec, error) {
|
||||
var doc struct {
|
||||
On yaml.Node `yaml:"on"`
|
||||
}
|
||||
if err := yaml.Unmarshal(content, &doc); err != nil {
|
||||
if err := decodeResolved(content, &doc); err != nil {
|
||||
return nil, fmt.Errorf("parse workflow yaml: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
||||
// For example,
|
||||
// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
|
||||
// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
|
||||
// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
|
||||
// So its Stopped is the Started of postStep when there are no more steps to run.
|
||||
// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step2.
|
||||
// 4. preStep(Success) -> step1(Success) -> step2(Skipped) -> postStep(Success): lastHasRunStep is step1, because a skipped step never ran.
|
||||
// So its Stopped is the Started of postStep once no step is left to run.
|
||||
var lastHasRunStep *actions_model.ActionTaskStep
|
||||
|
||||
var logIndex int64
|
||||
@@ -71,8 +72,8 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
||||
Name: postStepName,
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
// If the lastHasRunStep is the last step, or it has failed, postStep has started.
|
||||
if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] {
|
||||
// If no step is left to run, or the lastHasRunStep has failed, postStep has started.
|
||||
if lastHasRunStep.Status.IsFailure() || task.Steps[len(task.Steps)-1].Status.IsDone() {
|
||||
postStep.LogIndex = logIndex
|
||||
postStep.LogLength = task.LogLength - postStep.LogIndex
|
||||
postStep.Started = lastHasRunStep.Stopped
|
||||
|
||||
@@ -156,6 +156,27 @@ func TestFullSteps(t *testing.T) {
|
||||
{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 10100},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "last step is skipped",
|
||||
task: &actions_model.ActionTask{
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 20, Started: 10010, Stopped: 10020},
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 30, LogLength: 60, Started: 10020, Stopped: 10090},
|
||||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
},
|
||||
Status: actions_model.StatusSuccess,
|
||||
Started: 10000,
|
||||
Stopped: 10100,
|
||||
LogLength: 100,
|
||||
},
|
||||
want: []*actions_model.ActionTaskStep{
|
||||
{Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010},
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 20, Started: 10010, Stopped: 10020},
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 30, LogLength: 60, Started: 10020, Stopped: 10090},
|
||||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 10100},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"slices"
|
||||
@@ -120,7 +119,7 @@ func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
||||
}
|
||||
|
||||
func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) {
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
workflow, err := jobparser.ReadWorkflow(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -171,16 +171,24 @@ func (c *Commit) AllAuthorIdentities() []*CommitIdentity {
|
||||
|
||||
addAuthor(c.Author.Name, c.Author.Email, commitIdentityRoleAuthor)
|
||||
for _, coAuthorValue := range trailerCoAuthors {
|
||||
addr, err := mail.ParseAddress(coAuthorValue)
|
||||
coAuthorName, coAuthorEmail := coAuthorValue, ""
|
||||
if err == nil {
|
||||
coAuthorName, coAuthorEmail = addr.Name, addr.Address
|
||||
}
|
||||
coAuthorName, coAuthorEmail := parseCommitIdentityValue(coAuthorValue)
|
||||
addAuthor(coAuthorName, coAuthorEmail, commitIdentityRoleCoAuthor)
|
||||
}
|
||||
return c.allAuthors
|
||||
}
|
||||
|
||||
// Git identities are not RFC 5322 addresses: net/mail rejects names like "dependabot[bot]", so fall back to the angle-addr.
|
||||
func parseCommitIdentityValue(value string) (name, email string) {
|
||||
if addr, err := mail.ParseAddress(value); err == nil {
|
||||
return addr.Name, addr.Address
|
||||
}
|
||||
begin, end := strings.LastIndex(value, "<"), strings.LastIndex(value, ">")
|
||||
if begin == -1 || end < begin {
|
||||
return value, ""
|
||||
}
|
||||
return strings.TrimSpace(value[:begin]), strings.TrimSpace(value[begin+1 : end])
|
||||
}
|
||||
|
||||
func (c *Commit) CoAuthorIdentities() (coAuthors []*CommitIdentity) {
|
||||
all := c.AllAuthorIdentities()
|
||||
if len(all) == 0 {
|
||||
|
||||
@@ -126,6 +126,17 @@ func TestCommitMessageParticipants(t *testing.T) {
|
||||
},
|
||||
[]*CommitIdentity{idt("b", "", roleCoAuthor), idt("c", "", roleCoAuthor)},
|
||||
},
|
||||
{
|
||||
"CoAuthorNameNotAnEmailAddress", // names net/mail rejects, e.g. bots and names with a comma
|
||||
&Commit{
|
||||
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
|
||||
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\nCo-authored-by: Smith, John <j@m.com>"},
|
||||
},
|
||||
[]*CommitIdentity{
|
||||
idt("dependabot[bot]", "49699333+dependabot[bot]@users.noreply.github.com", roleCoAuthor),
|
||||
idt("Smith, John", "j@m.com", roleCoAuthor),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.identities, c.commit.CoAuthorIdentities(), "case: %s", c.name)
|
||||
|
||||
@@ -118,15 +118,19 @@ func createDelegateHooks(hookDir string) (err error) {
|
||||
oldHookPath := filepath.Join(hookDir, hookName)
|
||||
newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("create hooks dir '%s': %w", filepath.Join(hookDir, hookName+".d"), err)
|
||||
hookDDir := filepath.Join(hookDir, hookName+".d")
|
||||
if err := os.MkdirAll(hookDDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create hooks dir '%s': %w", hookDDir, err)
|
||||
}
|
||||
if err := os.Chmod(hookDDir, 0o755); err != nil {
|
||||
return fmt.Errorf("chmod hooks dir '%s': %w", hookDDir, err)
|
||||
}
|
||||
|
||||
// WARNING: This will override all old server-side hooks
|
||||
if err = util.Remove(oldHookPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable to pre-remove old hook file '%s' prior to rewriting: %w ", oldHookPath, err)
|
||||
}
|
||||
if err = os.WriteFile(oldHookPath, []byte(hookTpls[i]), 0o777); err != nil {
|
||||
if err = os.WriteFile(oldHookPath, []byte(hookTpls[i]), 0o755); err != nil {
|
||||
return fmt.Errorf("write old hook file '%s': %w", oldHookPath, err)
|
||||
}
|
||||
|
||||
@@ -137,7 +141,7 @@ func createDelegateHooks(hookDir string) (err error) {
|
||||
if err = util.Remove(newHookPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable to pre-remove new hook file '%s' prior to rewriting: %w", newHookPath, err)
|
||||
}
|
||||
if err = os.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0o777); err != nil {
|
||||
if err = os.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0o755); err != nil {
|
||||
return fmt.Errorf("write new hook file '%s': %w", newHookPath, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitrepo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateDelegateHooksPermissions(t *testing.T) {
|
||||
hookDir := t.TempDir()
|
||||
existingHookDir := filepath.Join(hookDir, "post-receive.d")
|
||||
require.NoError(t, os.MkdirAll(existingHookDir, 0o777))
|
||||
require.NoError(t, os.Chmod(existingHookDir, 0o777))
|
||||
|
||||
require.NoError(t, createDelegateHooks(hookDir))
|
||||
|
||||
hookNames, _, _ := getHookTemplates()
|
||||
for _, hookName := range hookNames {
|
||||
for _, path := range []string{
|
||||
filepath.Join(hookDir, hookName),
|
||||
filepath.Join(hookDir, hookName+".d"),
|
||||
filepath.Join(hookDir, hookName+".d", "gitea"),
|
||||
} {
|
||||
info, err := os.Stat(path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, os.FileMode(0o755), info.Mode().Perm(), path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
analyzer_keyword "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/letter"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
||||
"github.com/blevesearch/bleve/v2/mapping"
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
@@ -69,9 +68,8 @@ func (d *RepoIndexerData) Type() string {
|
||||
const (
|
||||
repoIndexerAnalyzer = "repoIndexerAnalyzer"
|
||||
filenameIndexerAnalyzer = "filenameIndexerAnalyzer"
|
||||
filenameIndexerTokenizer = "filenameIndexerTokenizer"
|
||||
repoIndexerDocType = "repoIndexerDocType"
|
||||
repoIndexerLatestVersion = 9
|
||||
repoIndexerLatestVersion = 10
|
||||
)
|
||||
|
||||
// generateBleveIndexMapping generates a bleve index mapping for the repo indexer
|
||||
@@ -107,8 +105,8 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) {
|
||||
} else if err := mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]any{
|
||||
"type": analyzer_custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": letter.Name,
|
||||
"token_filters": []string{unicodeNormalizeName, lowercase.Name},
|
||||
"tokenizer": codeTokenizerName,
|
||||
"token_filters": []string{unicodeNormalizeName, codeTokenFilterName, lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/indexer/code/internal"
|
||||
inner_bleve "gitea.dev/modules/indexer/internal/bleve"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBleveIndexerTokenFilter(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
indexer := NewIndexer(dir)
|
||||
defer indexer.Close()
|
||||
|
||||
_, err := indexer.Init(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
batch := inner_bleve.NewFlushingBatch(indexer.inner.Indexer, maxBatchSize)
|
||||
batch.Index("2", &RepoIndexerData{RepoID: 2, Content: "mDNS.port2=12345", UpdatedAt: time.Now()})
|
||||
batch.Flush()
|
||||
|
||||
testCases := []struct {
|
||||
keyword string
|
||||
expectedIDs []int64
|
||||
}{
|
||||
{keyword: "12345", expectedIDs: []int64{2}},
|
||||
{keyword: "DNS", expectedIDs: []int64{}},
|
||||
{keyword: "mdns", expectedIDs: []int64{2}},
|
||||
{keyword: "port", expectedIDs: []int64{2}},
|
||||
{keyword: "port2", expectedIDs: []int64{2}},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.keyword, func(t *testing.T) {
|
||||
_, results, _, err := indexer.Search(t.Context(), &internal.SearchOptions{
|
||||
Paginator: &db.ListOptions{Page: 1, PageSize: 1},
|
||||
Keyword: testCase.keyword,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, testCase.expectedIDs, searchResultIDs(results))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func searchResultIDs(result []*internal.SearchResult) []int64 {
|
||||
ids := make([]int64, 0, len(result))
|
||||
for _, hit := range result {
|
||||
ids = append(ids, hit.RepoID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/character"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const codeTokenizerName = "codeTokenizer"
|
||||
|
||||
func codeTokenizerConstructor(_ map[string]any, _ *registry.Cache) (analysis.Tokenizer, error) {
|
||||
// Old code used "letter" tokenizer which doesn't support CJK.
|
||||
// Here it still doesn't support CJK, since there is no usable CJK tokenizer at the moment.
|
||||
return character.NewCharacterTokenizer(func(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||
}), nil
|
||||
}
|
||||
|
||||
const codeTokenFilterName = "codeTokenFilter"
|
||||
|
||||
type codeTokenFilter struct {
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func (c codeTokenFilter) Filter(stream analysis.TokenStream) (ret analysis.TokenStream) {
|
||||
// split one token to "letter" parts and "number" parts (to keep the old behavior).
|
||||
// e.g.: input token="port123", then the output tokens are "port123", "port", "123"
|
||||
for _, token := range stream {
|
||||
ret = append(ret, token)
|
||||
m := c.re.FindAllIndex(token.Term, -1)
|
||||
if len(m) > 1 {
|
||||
for _, it := range m {
|
||||
p1, p2 := it[0], it[1]
|
||||
t := &analysis.Token{
|
||||
Start: token.Start + p1,
|
||||
End: token.Start + p2,
|
||||
Term: token.Term[p1:p2],
|
||||
Position: token.Position,
|
||||
Type: analysis.AlphaNumeric,
|
||||
}
|
||||
ret = append(ret, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func codeTokenFilterConstructor(_ map[string]any, _ *registry.Cache) (analysis.TokenFilter, error) {
|
||||
return &codeTokenFilter{
|
||||
re: regexp.MustCompile("[a-zA-Z]+|[0-9]+"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
util.MustNoError(registry.RegisterTokenizer(codeTokenizerName, codeTokenizerConstructor))
|
||||
util.MustNoError(registry.RegisterTokenFilter(codeTokenFilterName, codeTokenFilterConstructor))
|
||||
}
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "gitea/path"
|
||||
)
|
||||
const Name = "gitea/path"
|
||||
|
||||
type TokenFilter struct{}
|
||||
|
||||
@@ -98,8 +98,5 @@ func generatePathTokens(input analysis.TokenStream, reversed bool) analysis.Toke
|
||||
|
||||
func init() {
|
||||
// FIXME: move it to the bleve's init function, but do not call it in global init
|
||||
err := registry.RegisterTokenFilter(Name, TokenFilterConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
util.MustNoError(registry.RegisterTokenFilter(Name, TokenFilterConstructor))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
||||
@@ -26,7 +25,7 @@ import (
|
||||
const (
|
||||
issueIndexerAnalyzer = "issueIndexer"
|
||||
issueIndexerDocType = "issueIndexerDocType"
|
||||
issueIndexerLatestVersion = 7
|
||||
issueIndexerLatestVersion = 8
|
||||
)
|
||||
|
||||
const unicodeNormalizeName = "unicodeNormalize"
|
||||
@@ -103,7 +102,7 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) {
|
||||
"type": custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name},
|
||||
"token_filters": []string{unicodeNormalizeName, camelCaseKeepWholeName, lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -77,6 +77,47 @@ func TestBleveIndexerNoAssignee(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBleveIndexerTokenFilter(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
indexer := NewIndexer(dir)
|
||||
defer indexer.Close()
|
||||
|
||||
_, err := indexer.Init(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, indexer.Index(t.Context(),
|
||||
&internal.IndexerData{ID: 1, Title: "fix(packages): SomeThing needs a rewrite (#12345)"},
|
||||
&internal.IndexerData{ID: 2, Title: "add support for mDNS discovery abc1234"},
|
||||
))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
keyword string
|
||||
expectedIDs []int64
|
||||
}{
|
||||
{name: "exact original case", keyword: "SomeThing", expectedIDs: []int64{1}},
|
||||
{name: "case matching original transitions", keyword: "someThing", expectedIDs: []int64{1}},
|
||||
{name: "all lower case", keyword: "something", expectedIDs: []int64{1}},
|
||||
{name: "all upper case", keyword: "SOMETHING", expectedIDs: []int64{1}},
|
||||
{name: "number match", keyword: "12345", expectedIDs: []int64{1}},
|
||||
{name: "number as part", keyword: "1234", expectedIDs: []int64{2}},
|
||||
{name: "sub-word search still works", keyword: "DNS", expectedIDs: []int64{2}},
|
||||
{name: "sub-word search, lower case", keyword: "mdns", expectedIDs: []int64{2}},
|
||||
{name: "keyword is camel case", keyword: "addSupport", expectedIDs: []int64{2}},
|
||||
{name: "keyword not match", keyword: "addsupport", expectedIDs: []int64{}},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
result, err := indexer.Search(t.Context(), &internal.SearchOptions{
|
||||
Keyword: testCase.keyword,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, testCase.expectedIDs, searchResultIDs(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func searchResultIDs(result *internal.SearchResult) []int64 {
|
||||
ids := make([]int64, 0, len(result.Hits))
|
||||
for _, hit := range result.Hits {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const camelCaseKeepWholeName = "camelCaseKeepWhole"
|
||||
|
||||
// camelCaseKeepWholeFilter behaves like bleve's built-in "camelCase" token filter,
|
||||
// it also uses the whole word for a token. For example: when indexing "someThing",
|
||||
// CamelCaseFilter only emits "some" and "thing", this filter also emits "something".
|
||||
// It is questionable why the "issue indexer" used the CamelCaseFilter, it just can't search "someThing".
|
||||
// To avoid breaking existing user experiences, this "whole token filter" is introduced to make the full word can be searched.
|
||||
type camelCaseKeepWholeFilter struct {
|
||||
inner *camelcase.CamelCaseFilter
|
||||
}
|
||||
|
||||
func (f *camelCaseKeepWholeFilter) Filter(input analysis.TokenStream) analysis.TokenStream {
|
||||
// First, do exactly what the stock camelCase filter does: split by "camelCase" tokens
|
||||
split := f.inner.Filter(input)
|
||||
|
||||
// Index the resulting position of the *first* sub-token produced for
|
||||
// each original token (matched by start offset), so the duplicated
|
||||
// whole-word token we add below lines up at the same position as the
|
||||
// sub-word it stands in for, instead of drifting out of sync for
|
||||
// fields with more than one original token.
|
||||
posByStart := make(map[int]int, len(split))
|
||||
for _, tok := range split {
|
||||
if _, ok := posByStart[tok.Start]; !ok {
|
||||
posByStart[tok.Start] = tok.Position
|
||||
}
|
||||
}
|
||||
|
||||
rv := make(analysis.TokenStream, 0, len(split)+len(input))
|
||||
rv = append(rv, split...)
|
||||
|
||||
// Then append one extra, un-split copy of every original token, so the
|
||||
// whole word survives as a standalone, independently searchable term.
|
||||
for _, token := range input {
|
||||
dup := *token
|
||||
dup.Term = append([]byte(nil), token.Term...)
|
||||
if pos, ok := posByStart[token.Start]; ok {
|
||||
dup.Position = pos
|
||||
}
|
||||
rv = append(rv, &dup)
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
func camelCaseKeepWholeFilterConstructor(_ map[string]any, _ *registry.Cache) (analysis.TokenFilter, error) {
|
||||
return &camelCaseKeepWholeFilter{inner: camelcase.NewCamelCaseFilter()}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
util.MustNoError(registry.RegisterTokenFilter(camelCaseKeepWholeName, camelCaseKeepWholeFilterConstructor))
|
||||
}
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing")
|
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||
ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing")
|
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||
ErrPackageInfoTooLarge = util.NewInvalidArgumentErrorf("PKGINFO contains too many entries")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,6 +37,8 @@ const (
|
||||
RepositoryVersion = "_repository"
|
||||
|
||||
NoArch = "noarch"
|
||||
|
||||
maxPackageInfoEntries = 1024
|
||||
)
|
||||
|
||||
// https://wiki.alpinelinux.org/wiki/Apk_spec
|
||||
@@ -185,10 +188,16 @@ func ParsePackageInfo(r io.Reader) (*Package, error) {
|
||||
p.FileMetadata.InstallIf = value
|
||||
case "provides":
|
||||
if value != "" {
|
||||
if len(p.FileMetadata.Provides)+len(p.FileMetadata.Dependencies) >= maxPackageInfoEntries {
|
||||
return nil, ErrPackageInfoTooLarge
|
||||
}
|
||||
p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
|
||||
}
|
||||
case "depend":
|
||||
if value != "" {
|
||||
if len(p.FileMetadata.Provides)+len(p.FileMetadata.Dependencies) >= maxPackageInfoEntries {
|
||||
return nil, ErrPackageInfoTooLarge
|
||||
}
|
||||
p.FileMetadata.Dependencies = append(p.FileMetadata.Dependencies, value)
|
||||
}
|
||||
case "provider_priority":
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -97,6 +98,15 @@ func TestParsePackage(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "Q1SRYURM5+uQDqfHSwTnNIOIuuDVQ=", p.FileMetadata.Checksum)
|
||||
})
|
||||
|
||||
t.Run("TooManyDependencyEntries", func(t *testing.T) {
|
||||
data := append(createPKGINFOContent(packageName, packageVersion), []byte("\ndepend = item")...)
|
||||
data = append(data, []byte(strings.Repeat("\ndepend = item", maxPackageInfoEntries))...)
|
||||
|
||||
p, err := ParsePackageInfo(bytes.NewReader(data))
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrPackageInfoTooLarge)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParsePackageInfo(t *testing.T) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
var (
|
||||
ErrMissingManifestFile = util.NewInvalidArgumentErrorf("Package.swift file is missing")
|
||||
ErrManifestFileTooLarge = util.NewInvalidArgumentErrorf("Package.swift file is too large")
|
||||
ErrManifestFilesTooLarge = util.NewInvalidArgumentErrorf("Package.swift files are too large")
|
||||
ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid")
|
||||
|
||||
manifestPattern = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
|
||||
@@ -29,6 +30,8 @@ var (
|
||||
|
||||
const (
|
||||
maxManifestFileSize = 128 * 1024
|
||||
maxManifestFiles = 64
|
||||
maxManifestSize = maxManifestFiles * maxManifestFileSize
|
||||
|
||||
PropertyScope = "swift.scope"
|
||||
PropertyName = "swift.name"
|
||||
@@ -140,6 +143,16 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
|
||||
manifestFiles = append(manifestFiles, file)
|
||||
}
|
||||
}
|
||||
if len(manifestFiles) > maxManifestFiles {
|
||||
return nil, ErrManifestFilesTooLarge
|
||||
}
|
||||
var manifestSize uint64
|
||||
for _, file := range manifestFiles {
|
||||
manifestSize += file.UncompressedSize64
|
||||
}
|
||||
if manifestSize > maxManifestSize {
|
||||
return nil, ErrManifestFilesTooLarge
|
||||
}
|
||||
|
||||
for _, file := range manifestFiles {
|
||||
manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name))
|
||||
|
||||
@@ -6,6 +6,7 @@ package swift
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -54,6 +55,19 @@ func TestParsePackage(t *testing.T) {
|
||||
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
|
||||
})
|
||||
|
||||
t.Run("TooManyManifestFiles", func(t *testing.T) {
|
||||
entries := make([][2]string, 0, maxManifestFiles+1)
|
||||
entries = append(entries, [2]string{"Package.swift", "// swift-tools-version:5.7"})
|
||||
for i := range maxManifestFiles {
|
||||
entries = append(entries, [2]string{fmt.Sprintf("Package@swift-5.%d.swift", i), "// swift-tools-version:5.7"})
|
||||
}
|
||||
|
||||
data := writeOrderedZipArchive(entries)
|
||||
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrManifestFilesTooLarge)
|
||||
})
|
||||
|
||||
t.Run("WithoutMetadata", func(t *testing.T) {
|
||||
content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
||||
content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
|
||||
|
||||
@@ -109,18 +109,6 @@ func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookO
|
||||
return requestJSONResp(req, &HookProcReceiveResult{})
|
||||
}
|
||||
|
||||
// SetDefaultBranch will set the default branch to the provided branch for the provided repository
|
||||
func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) ResponseExtra {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s",
|
||||
url.PathEscape(ownerName),
|
||||
url.PathEscape(repoName),
|
||||
url.PathEscape(branch),
|
||||
)
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra
|
||||
}
|
||||
|
||||
// SSHLog sends ssh error log response
|
||||
func SSHLog(ctx context.Context, isErr bool, msg string) error {
|
||||
reqURL := setting.LocalURL + "api/internal/ssh/log"
|
||||
|
||||
@@ -88,25 +88,6 @@ type LoggerOptions struct {
|
||||
Config map[string]any
|
||||
}
|
||||
|
||||
// AddLogger adds a logger
|
||||
func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/add-logger"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{
|
||||
Logger: logger,
|
||||
Writer: writer,
|
||||
Mode: mode,
|
||||
Config: config,
|
||||
})
|
||||
return requestJSONClientMsg(req, "Added")
|
||||
}
|
||||
|
||||
// RemoveLogger removes a logger
|
||||
func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer))
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Removed")
|
||||
}
|
||||
|
||||
// Processes return the current processes from this gitea instance
|
||||
func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel))
|
||||
|
||||
@@ -108,7 +108,7 @@ var OAuth2 = struct {
|
||||
JWTSigningAlgorithm: "RS256",
|
||||
JWTSigningPrivateKeyFile: "jwt/private.pem",
|
||||
MaxTokenLength: math.MaxInt16,
|
||||
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
|
||||
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea", "gitea-app"},
|
||||
}
|
||||
|
||||
func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestGetGeneralSigningSecretSave(t *testing.T) {
|
||||
func TestOauth2DefaultApplications(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(``)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea"}, OAuth2.DefaultApplications)
|
||||
assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea", "gitea-app"}, OAuth2.DefaultApplications)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`[oauth2]
|
||||
DEFAULT_APPLICATIONS = tea
|
||||
|
||||
@@ -11,7 +11,6 @@ type CreateUserOption struct {
|
||||
// The authentication source ID to associate with the user
|
||||
SourceID int64 `json:"source_id"`
|
||||
// identifier of the user, provided by the external authenticator (if configured)
|
||||
// default: empty
|
||||
LoginName string `json:"login_name"`
|
||||
// username of the user
|
||||
// required: true
|
||||
@@ -44,9 +43,7 @@ type EditUserOption struct {
|
||||
// The authentication source ID to associate with the user
|
||||
SourceID int64 `json:"source_id"`
|
||||
// identifier of the user, provided by the external authenticator (if configured)
|
||||
// default: empty
|
||||
// required: true
|
||||
LoginName string `json:"login_name" binding:"Required"`
|
||||
LoginName *string `json:"login_name"`
|
||||
// swagger:strfmt email
|
||||
// The email address of the user
|
||||
Email *string `json:"email" binding:"MaxSize(254)"`
|
||||
|
||||
@@ -154,7 +154,7 @@ type CreateRepoOption struct {
|
||||
// Whether the repository is template
|
||||
Template bool `json:"template"`
|
||||
// Gitignores to use
|
||||
Gitignores string `json:"gitignores"`
|
||||
Gitignores string `json:"gitignores" binding:"MaxSize(1024)"`
|
||||
// License to use
|
||||
License string `json:"license" binding:"MaxSize(100)"`
|
||||
// Readme of the repository to create
|
||||
|
||||
@@ -18,7 +18,6 @@ type User struct {
|
||||
// login of the user, same as `username`
|
||||
UserName string `json:"login"`
|
||||
// identifier of the user, provided by the external authenticator (if configured)
|
||||
// default: empty
|
||||
LoginName string `json:"login_name"`
|
||||
// The ID of the user's Authentication Source
|
||||
SourceID int64 `json:"source_id"`
|
||||
|
||||
@@ -415,7 +415,7 @@ func (ut *RenderUtils) participantNameLink(data *user_model.AvatarStackData, par
|
||||
if participant.GitIdentity.Email != "" {
|
||||
return htmlutil.HTMLFormat(`<a class="muted" href="mailto:%s">%s</a>`, participant.GitIdentity.Email, participant.GitIdentity.Name)
|
||||
}
|
||||
return template.HTML(template.HTMLEscapeString(participant.GitIdentity.Name))
|
||||
return htmlutil.HTMLFormat(`<span class="avatar-stack-name">%s</span>`, participant.GitIdentity.Name)
|
||||
}
|
||||
|
||||
func (ut *RenderUtils) participantPopupRow(data *user_model.AvatarStackData, participant *user_model.CommitParticipant) template.HTML {
|
||||
|
||||
@@ -316,3 +316,9 @@ func DiffSlice[T comparable](oldSlice, newSlice []T) (added, removed []T) {
|
||||
}
|
||||
return added, removed
|
||||
}
|
||||
|
||||
func MustNoError(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
packages_service "gitea.dev/services/packages"
|
||||
)
|
||||
|
||||
const maxChecksumSize = sha512.Size*2 + 1
|
||||
|
||||
const (
|
||||
mavenMetadataFile = "maven-metadata.xml"
|
||||
extensionMD5 = ".md5"
|
||||
@@ -260,12 +262,21 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
}
|
||||
defer releaser()
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
|
||||
ext := path.Ext(params.Filename)
|
||||
reader := io.Reader(ctx.Req.Body)
|
||||
if isChecksumExtension(ext) {
|
||||
reader = io.LimitReader(reader, maxChecksumSize+1)
|
||||
}
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(reader)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
if isChecksumExtension(ext) && !isChecksumSizeAllowed(buf.Size()) {
|
||||
apiError(ctx, http.StatusRequestEntityTooLarge, "checksum is too large")
|
||||
return
|
||||
}
|
||||
|
||||
pvci := &packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
@@ -291,8 +302,6 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
ext := path.Ext(params.Filename)
|
||||
|
||||
// Do not upload checksum files but compare the hashes.
|
||||
if isChecksumExtension(ext) {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
|
||||
@@ -404,6 +413,10 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
func isChecksumSizeAllowed(size int64) bool {
|
||||
return size <= maxChecksumSize
|
||||
}
|
||||
|
||||
func isChecksumExtension(ext string) bool {
|
||||
return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package maven
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChecksumSizeAllowed(t *testing.T) {
|
||||
assert.True(t, isChecksumSizeAllowed(maxChecksumSize))
|
||||
assert.False(t, isChecksumSizeAllowed(maxChecksumSize+1))
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
|
||||
authOpts := &user_service.UpdateAuthOptions{
|
||||
LoginSource: optional.FromNonDefault(form.SourceID),
|
||||
LoginName: optional.Some(form.LoginName),
|
||||
LoginName: optional.FromPtr(form.LoginName),
|
||||
Password: optional.FromNonDefault(form.Password),
|
||||
MustChangePassword: optional.FromPtr(form.MustChangePassword),
|
||||
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
|
||||
|
||||
+6
-17
@@ -926,19 +926,8 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
|
||||
|
||||
func individualPermsChecker(ctx *context.APIContext) {
|
||||
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
|
||||
if ctx.ContextUser.IsIndividual() {
|
||||
switch ctx.ContextUser.Visibility {
|
||||
case api.VisibleTypePrivate:
|
||||
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
case api.VisibleTypeLimited:
|
||||
if ctx.Doer == nil {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
if ctx.ContextUser.IsIndividual() && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,7 +1091,7 @@ func Routes() *web.Router {
|
||||
m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos)
|
||||
|
||||
m.Get("/subscriptions", user.GetWatchedRepos)
|
||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
|
||||
// Users (requires user scope)
|
||||
@@ -1243,7 +1232,7 @@ func Routes() *web.Router {
|
||||
m.Get("/search", repo.Search)
|
||||
|
||||
// (repo scope)
|
||||
m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
|
||||
m.Post("/migrate", reqToken(), rejectPublicOnly(), bind(api.MigrateRepoOptions{}), repo.Migrate)
|
||||
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff)
|
||||
@@ -1710,7 +1699,7 @@ func Routes() *web.Router {
|
||||
m.Group("/users/{username}/orgs", func() {
|
||||
m.Get("", reqToken(), org.ListUserOrgs)
|
||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
||||
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
|
||||
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
|
||||
m.Group("/orgs/{org}", func() {
|
||||
@@ -1719,7 +1708,7 @@ func Routes() *web.Router {
|
||||
Delete(reqToken(), reqOrgOwnership(), org.Delete)
|
||||
m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
|
||||
m.Combo("/repos").Get(user.ListOrgRepos).
|
||||
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
|
||||
Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
|
||||
Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos)
|
||||
m.Group("/members", func() {
|
||||
m.Get("", reqToken(), org.ListMembers)
|
||||
|
||||
@@ -2073,8 +2073,8 @@ func buildSignature(endp string, expires, artifactID int64) []byte {
|
||||
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
|
||||
}
|
||||
|
||||
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
|
||||
return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), artifactID)
|
||||
func buildDownloadRawEndpoint(ownerName, repoName string, artifactID int64) string {
|
||||
return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(ownerName), url.PathEscape(repoName), artifactID)
|
||||
}
|
||||
|
||||
func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string {
|
||||
@@ -2135,7 +2135,7 @@ func DownloadArtifact(ctx *context.APIContext) {
|
||||
|
||||
// @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it
|
||||
// TODO: a perma link to the code for reference
|
||||
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID)
|
||||
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, art.ID), art.ID)
|
||||
ctx.Redirect(redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
@@ -2146,7 +2146,22 @@ func DownloadArtifact(ctx *context.APIContext) {
|
||||
// DownloadArtifactRaw Downloads a specific artifact for a workflow run directly.
|
||||
func DownloadArtifactRaw(ctx *context.APIContext) {
|
||||
// it doesn't use repoAssignment middleware, so it needs to prepare the repo and check permission (sig) by itself
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame"))
|
||||
ownerName, repoName := ctx.PathParam("username"), ctx.PathParam("reponame")
|
||||
query := ctx.Req.URL.Query()
|
||||
sigBytes, _ := base64.RawURLEncoding.DecodeString(query.Get("sig"))
|
||||
expires, _ := strconv.ParseInt(query.Get("expires"), 10, 64)
|
||||
artifactID := ctx.PathParamInt64("artifact_id")
|
||||
|
||||
if !hmac.Equal(sigBytes, buildSignature(buildDownloadRawEndpoint(ownerName, repoName, artifactID), expires, artifactID)) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
if time.Unix(expires, 0).Before(time.Now()) {
|
||||
ctx.APIError(http.StatusUnauthorized, "Error link expired")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound()
|
||||
@@ -2160,22 +2175,6 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
sigStr := ctx.Req.URL.Query().Get("sig")
|
||||
expiresStr := ctx.Req.URL.Query().Get("expires")
|
||||
sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr)
|
||||
expires, _ := strconv.ParseInt(expiresStr, 10, 64)
|
||||
|
||||
expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID)
|
||||
if !hmac.Equal(sigBytes, expectedSig) {
|
||||
ctx.APIError(http.StatusUnauthorized, "Error unauthorized")
|
||||
return
|
||||
}
|
||||
t := time.Unix(expires, 0)
|
||||
if t.Before(time.Now()) {
|
||||
ctx.APIError(http.StatusUnauthorized, "Error link expired")
|
||||
return
|
||||
}
|
||||
|
||||
// if artifacts status is not uploaded-confirmed, treat it as not found
|
||||
if art.Status == actions_model.ArtifactStatusExpired {
|
||||
ctx.APIError(http.StatusNotFound, "Artifact has expired")
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/organization"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
@@ -33,61 +32,6 @@ import (
|
||||
issue_service "gitea.dev/services/issue"
|
||||
)
|
||||
|
||||
// buildSearchIssuesRepoIDs builds the list of repository IDs for issue search based on query parameters.
|
||||
// It returns repoIDs, allPublic flag, and any error that occurred.
|
||||
func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPublic bool, err error) {
|
||||
opts := repo_model.SearchRepoOptions{
|
||||
Private: false,
|
||||
AllPublic: true,
|
||||
TopicOnly: false,
|
||||
Collaborate: optional.None[bool](),
|
||||
// This needs to be a column that is not nil in fixtures or
|
||||
// MySQL will return different results when sorting by null in some cases
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
opts.Private = true
|
||||
opts.AllLimited = true
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
if ctx.FormString("owner") != "" {
|
||||
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
opts.OwnerID = owner.ID
|
||||
opts.AllLimited = false
|
||||
opts.AllPublic = false
|
||||
opts.Collaborate = optional.Some(false)
|
||||
}
|
||||
if ctx.FormString("team") != "" {
|
||||
if ctx.FormString("owner") == "" {
|
||||
return nil, false, util.NewInvalidArgumentErrorf("owner organisation is required for filtering on team")
|
||||
}
|
||||
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
opts.TeamID = team.ID
|
||||
}
|
||||
|
||||
if opts.AllPublic {
|
||||
allPublic = true
|
||||
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
|
||||
}
|
||||
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(repoIDs) == 0 {
|
||||
// no repos found, don't let the indexer return all repos
|
||||
repoIDs = []int64{0}
|
||||
}
|
||||
|
||||
return repoIDs, allPublic, nil
|
||||
}
|
||||
|
||||
// SearchIssues searches for issues across the repositories that the user has access to
|
||||
func SearchIssues(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/issues/search issue issueSearchIssues
|
||||
@@ -193,7 +137,12 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
|
||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
|
||||
repoIDs, allPublic, err := buildSearchIssuesRepoIDs(ctx)
|
||||
repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{
|
||||
Doer: ctx.Doer,
|
||||
PublicOnly: ctx.PublicOnly,
|
||||
OwnerName: ctx.FormString("owner"),
|
||||
TeamName: ctx.FormString("team"),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
||||
@@ -204,10 +153,6 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
if strings.IndexByte(keyword, 0) >= 0 {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||
|
||||
var includedAnyLabels []int64
|
||||
@@ -390,9 +335,6 @@ func ListIssues(ctx *context.APIContext) {
|
||||
|
||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
keyword := ctx.FormTrim("q")
|
||||
if strings.IndexByte(keyword, 0) >= 0 {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
var labelIDs []int64
|
||||
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
|
||||
@@ -435,13 +377,7 @@ func ListIssues(ctx *context.APIContext) {
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||
|
||||
if isPull.Has() && !ctx.Repo.Permission.CanReadIssuesOrPulls(isPull.Value()) {
|
||||
ctx.APIErrorNotFound()
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -1042,7 +1041,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||
if err := pull_service.Merge(pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||
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)))
|
||||
} else if pull_service.IsErrMergeConflicts(err) {
|
||||
@@ -1099,6 +1098,10 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
if !ctx.TokenCanAccessRepo(headRepo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
isSameRepo := baseRepo.ID == headRepo.ID
|
||||
|
||||
@@ -1276,7 +1279,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||
// default merge commit message
|
||||
message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
|
||||
|
||||
if err = pull_service.Update(graceful.GetManager().ShutdownContext(), pr, ctx.Doer, message, rebase); err != nil {
|
||||
if err = pull_service.Update(pr, ctx.Doer, message, rebase); err != nil {
|
||||
if pull_service.IsErrMergeConflicts(err) {
|
||||
ctx.APIError(http.StatusConflict, "merge failed because of conflict")
|
||||
return
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
func ParseIssueFilterStateIsClosed(state string) optional.Option[bool] {
|
||||
@@ -23,3 +29,59 @@ func ParseIssueFilterStateIsClosed(state string) optional.Option[bool] {
|
||||
func ParseIssueFilterTypeIsPull(typ string) optional.Option[bool] {
|
||||
return optional.FromMapLookup(map[string]bool{"pulls": true, "issues": false}, typ)
|
||||
}
|
||||
|
||||
type SearchIssuesRepoIDsOptions struct {
|
||||
Doer *user_model.User
|
||||
PublicOnly bool
|
||||
OwnerName string
|
||||
TeamName string
|
||||
}
|
||||
|
||||
// SearchIssuesRepoIDs resolves the repository filter of an issue search. allPublic makes the indexer
|
||||
// match everything its own is_public covers (modules/indexer/issues/util.go), so repoIDs omits those.
|
||||
func SearchIssuesRepoIDs(ctx context.Context, opts SearchIssuesRepoIDsOptions) (repoIDs []int64, allPublic bool, err error) {
|
||||
searchOpts := repo_model.SearchRepoOptions{
|
||||
Private: opts.Doer != nil,
|
||||
Collaborate: optional.None[bool](),
|
||||
Actor: opts.Doer,
|
||||
}
|
||||
searchOpts.ApplyPublicOnly(opts.PublicOnly)
|
||||
if opts.OwnerName != "" {
|
||||
owner, err := user_model.GetUserByName(ctx, opts.OwnerName)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
searchOpts.OwnerID = owner.ID
|
||||
searchOpts.Collaborate = optional.Some(false)
|
||||
}
|
||||
if opts.TeamName != "" {
|
||||
if opts.OwnerName == "" {
|
||||
return nil, false, util.NewInvalidArgumentErrorf("owner organisation is required for filtering on team")
|
||||
}
|
||||
team, err := organization.GetTeam(ctx, searchOpts.OwnerID, opts.TeamName)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
searchOpts.TeamID = team.ID
|
||||
}
|
||||
|
||||
// SearchRepoOptions.AllPublic and AllLimited only apply under an owner filter, so the indexer covers them
|
||||
allPublic = opts.OwnerName == ""
|
||||
cond := repo_model.SearchRepositoryCondition(searchOpts)
|
||||
if allPublic {
|
||||
if !searchOpts.Private {
|
||||
return []int64{0}, allPublic, nil // sees nothing beyond is_public, so skip the query
|
||||
}
|
||||
cond = cond.And(repo_model.NotPublicRepoUnderPublicOwnerCond()) // enumerating them scales with the instance
|
||||
}
|
||||
repoIDs, err = repo_model.SearchRepositoryIDsByCondition(ctx, cond)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(repoIDs) == 0 {
|
||||
// no repos found, don't let the indexer return all repos
|
||||
repoIDs = []int64{0}
|
||||
}
|
||||
|
||||
return repoIDs, allPublic, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSearchIssuesRepoIDs(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// the indexer's is_public covers repo 1 (public under a public owner) but misses repo 38 (public
|
||||
// under a limited org) and repo 40 (public under a private org)
|
||||
cases := []struct {
|
||||
name string
|
||||
doerID int64
|
||||
opts SearchIssuesRepoIDsOptions
|
||||
allPublic bool
|
||||
want []int64
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "site admin", // admins skip the accessible repository condition entirely
|
||||
doerID: 1,
|
||||
allPublic: true,
|
||||
want: []int64{2, 38, 40},
|
||||
},
|
||||
{
|
||||
name: "regular user",
|
||||
doerID: 2,
|
||||
allPublic: true,
|
||||
want: []int64{2, 38},
|
||||
},
|
||||
{
|
||||
name: "private org member",
|
||||
doerID: 5,
|
||||
allPublic: true,
|
||||
want: []int64{38, 40},
|
||||
},
|
||||
{
|
||||
name: "anonymous",
|
||||
allPublic: true,
|
||||
want: []int64{0}, // the placeholder keeps the indexer off "every repository"
|
||||
},
|
||||
{
|
||||
name: "public-only token",
|
||||
doerID: 2,
|
||||
opts: SearchIssuesRepoIDsOptions{PublicOnly: true},
|
||||
allPublic: true,
|
||||
want: []int64{0},
|
||||
},
|
||||
{
|
||||
name: "owner filter", // turns allPublic off, so public repos must still be enumerated
|
||||
doerID: 2,
|
||||
opts: SearchIssuesRepoIDsOptions{OwnerName: "user2"},
|
||||
want: []int64{1, 2},
|
||||
},
|
||||
{
|
||||
name: "team without owner",
|
||||
opts: SearchIssuesRepoIDsOptions{TeamName: "team1"},
|
||||
wantErr: util.ErrInvalidArgument,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := tc.opts
|
||||
if tc.doerID != 0 {
|
||||
opts.Doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID})
|
||||
}
|
||||
|
||||
repoIDs, allPublic, err := SearchIssuesRepoIDs(t.Context(), opts)
|
||||
if tc.wantErr != nil {
|
||||
assert.ErrorIs(t, err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.allPublic, allPublic)
|
||||
assert.Subset(t, repoIDs, tc.want)
|
||||
if allPublic {
|
||||
assert.NotContains(t, repoIDs, int64(1), "already matched by the indexer's is_public")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -312,8 +312,10 @@ func SubmitInstall(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Save settings.
|
||||
// Some logic also depends on the config values, so EnvironmentToConfig should also be applied first.
|
||||
// EnvironmentToConfig is applied on each start up, so it also must override the "install form", so it must be applied after (twice).
|
||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||
setting.EnvironmentToConfig(cfg, os.Environ())
|
||||
if err != nil {
|
||||
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/private"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// SetDefaultBranch updates the default branch
|
||||
func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
|
||||
ownerName := ctx.PathParam("owner")
|
||||
repoName := ctx.PathParam("repo")
|
||||
branch := ctx.PathParam("branch")
|
||||
|
||||
ctx.Repo.Repository.DefaultBranch = branch
|
||||
if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.UpdateDefaultBranch(ctx, ctx.Repo.Repository); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
@@ -79,7 +79,6 @@ func Routes() *web.Router {
|
||||
r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive)
|
||||
r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive)
|
||||
r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
|
||||
r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch)
|
||||
r.Get("/serv/none/{keyid}", ServNoCommand)
|
||||
r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand)
|
||||
r.Post("/manager/shutdown", Shutdown)
|
||||
@@ -90,8 +89,6 @@ func Routes() *web.Router {
|
||||
r.Post("/manager/resume-logging", ResumeLogging)
|
||||
r.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging)
|
||||
r.Post("/manager/set-log-sql", SetLogSQL)
|
||||
r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
|
||||
r.Post("/manager/remove-logger/{logger}/{writer}", RemoveLogger)
|
||||
r.Get("/manager/processes", Processes)
|
||||
r.Post("/mail/send", SendEmail)
|
||||
r.Post("/restore_repo", RestoreRepo)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
@@ -85,111 +84,3 @@ func SetLogSQL(ctx *context.PrivateContext) {
|
||||
db.SetLogSQL(ctx, ctx.FormBool("on"))
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// RemoveLogger removes a logger
|
||||
func RemoveLogger(ctx *context.PrivateContext) {
|
||||
logger := ctx.PathParam("logger")
|
||||
writer := ctx.PathParam("writer")
|
||||
err := log.GetManager().GetLogger(logger).RemoveWriter(writer)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Failed to remove log writer: %s %s %v", logger, writer, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, fmt.Sprintf("Removed %s %s", logger, writer))
|
||||
}
|
||||
|
||||
// AddLogger adds a logger
|
||||
func AddLogger(ctx *context.PrivateContext) {
|
||||
opts := web.GetForm(ctx).(*private.LoggerOptions)
|
||||
|
||||
if len(opts.Logger) == 0 {
|
||||
opts.Logger = log.DEFAULT
|
||||
}
|
||||
|
||||
writerMode := log.WriterMode{}
|
||||
writerType := opts.Mode
|
||||
|
||||
var flags string
|
||||
var ok bool
|
||||
if flags, ok = opts.Config["flags"].(string); !ok {
|
||||
switch opts.Logger {
|
||||
case "access":
|
||||
flags = ""
|
||||
case "router":
|
||||
flags = "date,time"
|
||||
default:
|
||||
flags = "stdflags"
|
||||
}
|
||||
}
|
||||
writerMode.Flags = log.FlagsFromString(flags)
|
||||
|
||||
if writerMode.Colorize, ok = opts.Config["colorize"].(bool); !ok && opts.Mode == "console" {
|
||||
if _, ok := opts.Config["stderr"]; ok {
|
||||
writerMode.Colorize = log.CanColorStderr
|
||||
} else {
|
||||
writerMode.Colorize = log.CanColorStdout
|
||||
}
|
||||
}
|
||||
|
||||
writerMode.Level = setting.Log.Level
|
||||
if level, ok := opts.Config["level"].(string); ok {
|
||||
writerMode.Level = log.LevelFromString(level)
|
||||
}
|
||||
|
||||
writerMode.StacktraceLevel = setting.Log.StacktraceLogLevel
|
||||
if stacktraceLevel, ok := opts.Config["level"].(string); ok {
|
||||
writerMode.StacktraceLevel = log.LevelFromString(stacktraceLevel)
|
||||
}
|
||||
|
||||
writerMode.Prefix, _ = opts.Config["prefix"].(string)
|
||||
writerMode.Expression, _ = opts.Config["expression"].(string)
|
||||
|
||||
switch writerType {
|
||||
case "console":
|
||||
writerOption := log.WriterConsoleOption{}
|
||||
writerOption.Stderr, _ = opts.Config["stderr"].(bool)
|
||||
writerMode.WriterOption = writerOption
|
||||
case "file":
|
||||
writerOption := log.WriterFileOption{}
|
||||
fileName, _ := opts.Config["filename"].(string)
|
||||
writerOption.FileName = setting.LogPrepareFilenameForWriter(fileName, opts.Writer+".log")
|
||||
writerOption.LogRotate, _ = opts.Config["rotate"].(bool)
|
||||
maxSizeShift, _ := opts.Config["maxsize"].(int)
|
||||
if maxSizeShift == 0 {
|
||||
maxSizeShift = 28
|
||||
}
|
||||
writerOption.MaxSize = 1 << maxSizeShift
|
||||
writerOption.DailyRotate, _ = opts.Config["daily"].(bool)
|
||||
writerOption.MaxDays, _ = opts.Config["maxdays"].(int)
|
||||
if writerOption.MaxDays == 0 {
|
||||
writerOption.MaxDays = 7
|
||||
}
|
||||
writerOption.Compress, _ = opts.Config["compress"].(bool)
|
||||
writerOption.CompressionLevel, _ = opts.Config["compressionLevel"].(int)
|
||||
if writerOption.CompressionLevel == 0 {
|
||||
writerOption.CompressionLevel = -1
|
||||
}
|
||||
writerMode.WriterOption = writerOption
|
||||
case "conn":
|
||||
writerOption := log.WriterConnOption{}
|
||||
writerOption.ReconnectOnMsg, _ = opts.Config["reconnectOnMsg"].(bool)
|
||||
writerOption.Reconnect, _ = opts.Config["reconnect"].(bool)
|
||||
writerOption.Protocol, _ = opts.Config["net"].(string)
|
||||
writerOption.Addr, _ = opts.Config["address"].(string)
|
||||
writerMode.WriterOption = writerOption
|
||||
default:
|
||||
panic("invalid log writer mode: " + writerType)
|
||||
}
|
||||
writer, err := log.NewEventWriter(opts.Writer, writerType, writerMode)
|
||||
if err != nil {
|
||||
log.Error("Failed to create new log writer: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Failed to create new log writer: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.GetManager().GetLogger(opts.Logger).AddWriters(writer)
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
@@ -268,30 +268,35 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
{jobID: "prep-jdk", name: "prep-jdk", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
{jobID: "code-analysis", name: "code-analysis", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
|
||||
// Matrix expansion (the " (...)" suffix is the heuristic the frontend uses to group rows)
|
||||
{jobID: "matrix-e2e-1-chromium", name: "matrix-e2e (1, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-1-firefox", name: "matrix-e2e (1, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-2-chromium", name: "matrix-e2e (2, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-3-chromium", name: "matrix-e2e (3, chromium)", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-3-firefox", name: "matrix-e2e (3, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-99-webkit", name: "matrix-e2e (99, webkit)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
// Matrix expansion: the legs share a single JobID, which is what the frontend groups rows on
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (1, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (1, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (2, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (3, chromium)", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (3, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (99, webkit)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
|
||||
// Matrix legs whose `name:` interpolates matrix values, so no " (...)" suffix is derived
|
||||
{jobID: "e2e-browsers", name: "E2E on chromium", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "e2e-browsers", name: "E2E on firefox", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "e2e-browsers", name: "E2E on webkit", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
|
||||
{jobID: "unit-test", name: "unit-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "arch-test", name: "arch-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "integration-test", name: "integration-test", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
|
||||
|
||||
{jobID: "build-image", name: "build-image", status: actions_model.StatusSuccess, duration: "3s", needs: []string{
|
||||
{jobID: "build-image", name: "build-image with a very long name that does not fit into the sidebar", status: actions_model.StatusSuccess, duration: "3s", needs: []string{
|
||||
"unit-test",
|
||||
"arch-test",
|
||||
"integration-test",
|
||||
"code-analysis",
|
||||
"matrix-e2e-1-chromium",
|
||||
"matrix-e2e-1-firefox",
|
||||
"matrix-e2e-2-chromium",
|
||||
"matrix-e2e-3-chromium",
|
||||
"matrix-e2e-3-firefox",
|
||||
"matrix-e2e-99-webkit",
|
||||
"matrix-e2e",
|
||||
"e2e-browsers",
|
||||
}},
|
||||
|
||||
// Separate jobs that only look like matrix legs, so they must stay separate nodes
|
||||
{jobID: "deploy-staging", name: "Deploy (staging)", status: actions_model.StatusSuccess, duration: "5s", needs: []string{"build-image"}},
|
||||
{jobID: "deploy-prod", name: "Deploy (prod)", status: actions_model.StatusSuccess, duration: "6s", needs: []string{"deploy-staging"}},
|
||||
}
|
||||
|
||||
resp.State.Run.Jobs = nil
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -19,6 +18,7 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
@@ -199,7 +199,7 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow
|
||||
ctx.ServerError("GetContentFromEntry", err)
|
||||
return nil, ""
|
||||
}
|
||||
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
|
||||
wf, err := jobparser.ReadWorkflow(content)
|
||||
if err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
workflows = append(workflows, workflow)
|
||||
@@ -369,7 +369,7 @@ func loadScopedWorkflowModel(ctx *context.Context, repo *repo_model.Repository,
|
||||
if content == nil {
|
||||
return nil // the workflow does not exist on the source's default branch
|
||||
}
|
||||
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
|
||||
wf, err := jobparser.ReadWorkflow(content)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -143,18 +143,18 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
return
|
||||
}
|
||||
|
||||
// prevent visiting attachment from other repository directly
|
||||
// The check will be ignored before this code merged.
|
||||
if attach.CreatedUnix > repo_model.LegacyAttachmentMissingRepoIDCutoff && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID != attach.RepoID {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
unitType, repoID, err := repo_service.GetAttachmentLinkedTypeAndRepoID(ctx, attach)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAttachmentLinkedTypeAndRepoID", err)
|
||||
return
|
||||
}
|
||||
if repoID == 0 {
|
||||
repoID = attach.RepoID
|
||||
}
|
||||
if ctx.Repo.Repository != nil && repoID != 0 && ctx.Repo.Repository.ID != repoID {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if unitType == unit.TypeInvalid { // unlinked attachment can only be accessed by the uploader
|
||||
if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
|
||||
|
||||
@@ -5,6 +5,7 @@ package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/organization"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -56,85 +56,22 @@ func SearchIssues(ctx *context.Context) {
|
||||
|
||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
|
||||
var (
|
||||
repoIDs []int64
|
||||
allPublic bool
|
||||
)
|
||||
{
|
||||
// find repos user can access (for issue search)
|
||||
opts := repo_model.SearchRepoOptions{
|
||||
Private: false,
|
||||
AllPublic: true,
|
||||
TopicOnly: false,
|
||||
Collaborate: optional.None[bool](),
|
||||
// This needs to be a column that is not nil in fixtures or
|
||||
// MySQL will return different results when sorting by null in some cases
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
opts.Private = true
|
||||
opts.AllLimited = true
|
||||
}
|
||||
if ctx.FormString("owner") != "" {
|
||||
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.HTTPError(http.StatusBadRequest, "Owner not found", err.Error())
|
||||
} else {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
opts.OwnerID = owner.ID
|
||||
opts.AllLimited = false
|
||||
opts.AllPublic = false
|
||||
opts.Collaborate = optional.Some(false)
|
||||
}
|
||||
if ctx.FormString("team") != "" {
|
||||
if ctx.FormString("owner") == "" {
|
||||
ctx.HTTPError(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
|
||||
return
|
||||
}
|
||||
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.HTTPError(http.StatusBadRequest, "Team not found", err.Error())
|
||||
} else {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
opts.TeamID = team.ID
|
||||
}
|
||||
|
||||
if opts.AllPublic {
|
||||
allPublic = true
|
||||
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
|
||||
}
|
||||
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
|
||||
return
|
||||
}
|
||||
if len(repoIDs) == 0 {
|
||||
// no repos found, don't let the indexer return all repos
|
||||
repoIDs = []int64{0}
|
||||
repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{
|
||||
Doer: ctx.Doer,
|
||||
OwnerName: ctx.FormString("owner"),
|
||||
TeamName: ctx.FormString("team"),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
ctx.ServerError("SearchIssuesRepoIDs", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
if strings.IndexByte(keyword, 0) >= 0 {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||
|
||||
var includedAnyLabels []int64
|
||||
{
|
||||
@@ -145,7 +82,7 @@ func SearchIssues(ctx *context.Context) {
|
||||
}
|
||||
includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
|
||||
ctx.ServerError("GetLabelIDsByNames", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -159,7 +96,7 @@ func SearchIssues(ctx *context.Context) {
|
||||
}
|
||||
includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
|
||||
ctx.ServerError("GetMilestoneIDsByNames", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -223,12 +160,12 @@ func SearchIssues(ctx *context.Context) {
|
||||
|
||||
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
|
||||
ctx.ServerError("SearchIssues", err)
|
||||
return
|
||||
}
|
||||
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
|
||||
ctx.ServerError("FindIssuesByIDs", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -267,11 +204,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||
}
|
||||
|
||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
if strings.IndexByte(keyword, 0) >= 0 {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
var mileIDs []int64
|
||||
if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
|
||||
@@ -303,13 +236,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||
|
||||
// FIXME: we should be more efficient here
|
||||
createdByID := getUserIDForFilter(ctx, "created_by")
|
||||
|
||||
@@ -516,6 +516,7 @@ func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(labels)
|
||||
labelsData.RepoLabels = labels
|
||||
|
||||
if repo.Owner.IsOrganization() {
|
||||
@@ -523,6 +524,7 @@ func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(orgLabels)
|
||||
labelsData.OrgLabels = orgLabels
|
||||
}
|
||||
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
|
||||
|
||||
+14
-13
@@ -31,7 +31,6 @@ import (
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/graceful"
|
||||
issue_template "gitea.dev/modules/issue/template"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
@@ -1011,7 +1010,7 @@ func UpdatePullRequest(ctx *context.Context) {
|
||||
|
||||
// The update process should not be canceled by the user
|
||||
// so we set the context to be a background context
|
||||
if err = pull_service.Update(graceful.GetManager().ShutdownContext(), issue.PullRequest, ctx.Doer, message, rebase); err != nil {
|
||||
if err = pull_service.Update(issue.PullRequest, ctx.Doer, message, rebase); err != nil {
|
||||
if pull_service.IsErrMergeConflicts(err) {
|
||||
conflictError := err.(pull_service.ErrMergeConflicts)
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
@@ -1158,7 +1157,7 @@ func MergePullRequest(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||
if err := pull_service.Merge(pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||
if pull_service.IsErrInvalidMergeStyle(err) {
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
|
||||
} else if pull_service.IsErrMergeConflicts(err) {
|
||||
@@ -1225,13 +1224,14 @@ func MergePullRequest(ctx *context.Context) {
|
||||
}
|
||||
log.Trace("Pull request merged: %d", pr.ID)
|
||||
|
||||
// FIXME: calling it here is wrong.
|
||||
// 1. the ctx might have been canceled ("Merge" might take a very long time and the user closes their browser)
|
||||
// 2. it is inconsistent with API/AutoMerge which all miss the call
|
||||
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.ServerError("stopTimerIfAvailable", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Pull request merged: %d", pr.ID)
|
||||
|
||||
if deleteBranchAfterMerge {
|
||||
deleteBranchAfterMergeAndFlashMessage(ctx, pr.ID)
|
||||
if ctx.Written() {
|
||||
@@ -1307,18 +1307,19 @@ func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *is
|
||||
}
|
||||
|
||||
func PullsNewRedirect(ctx *context.Context) {
|
||||
branch := ctx.PathParam("*")
|
||||
redirectRepo := ctx.Repo.Repository
|
||||
repo := ctx.Repo.Repository
|
||||
if repo.IsFork {
|
||||
if err := repo.GetBaseRepo(ctx); err != nil {
|
||||
branchName := ctx.PathParam("*")
|
||||
baseRepo, headRepo := ctx.Repo.Repository, ctx.Repo.Repository
|
||||
if headRepo.IsFork {
|
||||
if err := headRepo.GetBaseRepo(ctx); err != nil {
|
||||
ctx.ServerError("GetBaseRepo", err)
|
||||
return
|
||||
}
|
||||
redirectRepo = repo.BaseRepo
|
||||
branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
|
||||
baseRepo = headRepo.BaseRepo
|
||||
}
|
||||
ctx.Redirect(fmt.Sprintf("%s/compare/%s...%s?expand=1", redirectRepo.Link(), util.PathEscapeSegments(redirectRepo.DefaultBranch), util.PathEscapeSegments(branch)))
|
||||
ctx.Redirect(fmt.Sprintf("%s/compare/%s...%s?expand=1", baseRepo.Link(),
|
||||
util.PathEscapeSegments(baseRepo.DefaultBranch),
|
||||
util.PathEscapeSegments(context.CompareHeadRef(baseRepo, headRepo, branchName)),
|
||||
))
|
||||
}
|
||||
|
||||
// CompareAndPullRequestPost response for creating pull request
|
||||
|
||||
@@ -38,6 +38,7 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return ret
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(repoLabels)
|
||||
allLabels = append(allLabels, repoLabels...)
|
||||
}
|
||||
|
||||
@@ -47,6 +48,7 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
|
||||
ctx.ServerError("GetLabelsByOrgID", err)
|
||||
return ret
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(orgLabels)
|
||||
allLabels = append(allLabels, orgLabels...)
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ func Dashboard(ctx *context.Context) {
|
||||
|
||||
prepareHeatmapURL(ctx)
|
||||
|
||||
pageSize := setting.UI.User.RepoPagingNum
|
||||
pageSize := setting.UI.FeedPagingNum
|
||||
feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedUser: ctxUser,
|
||||
RequestedTeam: ctx.Org.Team,
|
||||
|
||||
+7
-5
@@ -327,14 +327,16 @@ var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{Disable
|
||||
|
||||
// registerWebRoutes register routes
|
||||
func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
// required to be signed in or signed out
|
||||
validation.AddBindingRules()
|
||||
|
||||
// middleware: required to be signed in or signed out
|
||||
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
|
||||
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
|
||||
// optional sign in (if signed in, use the user as doer, if not, no doer)
|
||||
// middleware: optional sign in (if signed in, use the user as doer, if not, no doer)
|
||||
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
|
||||
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
|
||||
|
||||
validation.AddBindingRules()
|
||||
// middleware: only apply CrossOriginProtection
|
||||
crossOriginProtect := verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: false})
|
||||
|
||||
openIDSignInEnabled := func(ctx *context.Context) {
|
||||
if !setting.Service.EnableOpenIDSignIn {
|
||||
@@ -530,7 +532,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)
|
||||
m.Post("/-/web-banner/dismiss", misc.WebBannerDismiss)
|
||||
m.Get("/-/web-theme/list", misc.WebThemeList)
|
||||
m.Post("/-/web-theme/apply", optSignIn, misc.WebThemeApply)
|
||||
m.Post("/-/web-theme/apply", crossOriginProtect, misc.WebThemeApply)
|
||||
|
||||
m.Group("/explore", func() {
|
||||
m.Get("", func(ctx *context.Context) {
|
||||
|
||||
@@ -243,6 +243,11 @@ func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (*jo
|
||||
|
||||
// checkJobsOfCurrentRunAttempt resolves blocked jobs of the run's latest attempt.
|
||||
func checkJobsOfCurrentRunAttempt(ctx context.Context, run *actions_model.ActionRun) (*jobsCheckResult, error) {
|
||||
// Approval is the only transition allowed to release an approval-pending run.
|
||||
if run.NeedApproval {
|
||||
return &jobsCheckResult{}, nil
|
||||
}
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunAndAttemptID(ctx, run.ID, run.LatestAttemptID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -386,6 +386,35 @@ jobs:
|
||||
assert.Equal(t, actions_model.StatusBlocked, refreshed.Status)
|
||||
}
|
||||
|
||||
func Test_checkJobsOfCurrentRunAttempt_NeedApprovalKeepsJobsBlocked(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
run := &actions_model.ActionRun{
|
||||
RepoID: 4, OwnerID: 1, TriggerUserID: 1,
|
||||
WorkflowID: "test.yml", Index: 9913, Ref: "refs/heads/main",
|
||||
Status: actions_model.StatusBlocked, NeedApproval: true,
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, run))
|
||||
attempt := &actions_model.ActionRunAttempt{
|
||||
RepoID: 4, RunID: run.ID, Attempt: 1, Status: actions_model.StatusBlocked,
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, attempt))
|
||||
_, err := db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", attempt.ID, run.ID)
|
||||
assert.NoError(t, err)
|
||||
run.LatestAttemptID = attempt.ID
|
||||
job := &actions_model.ActionRunJob{
|
||||
RunID: run.ID, RunAttemptID: attempt.ID, AttemptJobID: 1,
|
||||
RepoID: 4, OwnerID: 1, JobID: "job1", Name: "job1", Status: actions_model.StatusBlocked,
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, job))
|
||||
|
||||
result, err := checkJobsOfCurrentRunAttempt(ctx, run)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, result.UpdatedJobs)
|
||||
assert.Equal(t, actions_model.StatusBlocked, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID}).Status)
|
||||
}
|
||||
|
||||
// Test_checkRunConcurrency_HeldGroupDoesNotWake verifies that only an unoccupied concurrency group can wake up a blocked run/job.
|
||||
func Test_checkRunConcurrency_HeldGroupDoesNotWake(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
@@ -459,8 +459,7 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode
|
||||
return
|
||||
}
|
||||
|
||||
newNotifyInput(review.Issue.Repo, review.Reviewer, reviewHookType).
|
||||
WithRef(review.CommitID).
|
||||
newPullRequestReviewNotifyInput(review.Issue.Repo, review.Reviewer, reviewHookType, review.CommitID, pr).
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: api.HookIssueReviewed,
|
||||
Index: review.Issue.Index,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
@@ -19,6 +18,7 @@ import (
|
||||
unit_model "gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
actions_module "gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
"gitea.dev/services/convert"
|
||||
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
)
|
||||
|
||||
type methodCtxKeyType struct{}
|
||||
@@ -85,6 +83,12 @@ func newNotifyInputForSchedules(repo *repo_model.Repository) *notifyInput {
|
||||
return newNotifyInput(repo, user_model.NewActionsUser(), webhook_module.HookEventSchedule)
|
||||
}
|
||||
|
||||
func newPullRequestReviewNotifyInput(repo *repo_model.Repository, reviewer *user_model.User, event webhook_module.HookEventType, commitID string, pr *issues_model.PullRequest) *notifyInput {
|
||||
return newNotifyInput(repo, reviewer, event).
|
||||
WithRef(commitID).
|
||||
WithPullRequest(pr)
|
||||
}
|
||||
|
||||
func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput {
|
||||
input.Doer = doer
|
||||
return input
|
||||
@@ -414,6 +418,9 @@ func handleFilteredWorkflows(ctx context.Context, input *notifyInput, filteredWo
|
||||
return
|
||||
}
|
||||
for _, dwf := range filteredWorkflows {
|
||||
if !shouldCreateSkippedCommitStatusForFilteredWorkflow(input, dwf) {
|
||||
continue
|
||||
}
|
||||
if err := CreateSkippedCommitStatusForFilteredWorkflow(ctx, input.Repo, input.Event, dwf.TriggerEvent.Name, dwf.EntryName, dwf.Content, input.Payload, "", requiredGlobs); err != nil {
|
||||
log.Error("repo %s: skipped commit status for workflow %s: %v", input.Repo.RelativePath(), dwf.EntryName, err)
|
||||
continue
|
||||
@@ -421,6 +428,10 @@ func handleFilteredWorkflows(ctx context.Context, input *notifyInput, filteredWo
|
||||
}
|
||||
}
|
||||
|
||||
func shouldCreateSkippedCommitStatusForFilteredWorkflow(input *notifyInput, workflow *actions_module.DetectedWorkflow) bool {
|
||||
return !isForkPullRequestInput(input) || workflow.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget
|
||||
}
|
||||
|
||||
func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
|
||||
return newNotifyInput(issue.Repo, issue.Poster, event)
|
||||
}
|
||||
@@ -555,7 +566,7 @@ func handleSchedules(
|
||||
crons := make([]*actions_model.ActionSchedule, 0, len(detectedWorkflows))
|
||||
for _, dwf := range detectedWorkflows {
|
||||
// Check cron job condition. Only working in default branch
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content))
|
||||
workflow, err := jobparser.ReadWorkflow(dwf.Content)
|
||||
if err != nil {
|
||||
log.Error("ReadWorkflow: %v", err)
|
||||
continue
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
actions_module "gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -100,3 +102,25 @@ func TestIfNeedApproval(t *testing.T) {
|
||||
assert.False(t, called, "permission check must not run for restricted user")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilteredWorkflowCommitStatusForForkPullRequest(t *testing.T) {
|
||||
forkPR := &issues_model.PullRequest{
|
||||
Flow: issues_model.PullRequestFlowGithub,
|
||||
BaseRepoID: 1,
|
||||
HeadRepoID: 2,
|
||||
}
|
||||
input := newPullRequestReviewNotifyInput(&repo_model.Repository{ID: 1}, &user_model.User{ID: 2}, actions_module.GithubEventPullRequest, "refs/pull/1/head", forkPR)
|
||||
|
||||
assert.True(t, isForkPullRequestInput(input))
|
||||
assert.Equal(t, "refs/pull/1/head", input.Ref.String())
|
||||
assert.False(t, shouldCreateSkippedCommitStatusForFilteredWorkflow(input, &actions_module.DetectedWorkflow{
|
||||
TriggerEvent: &jobparser.Event{Name: actions_module.GithubEventPullRequest},
|
||||
}))
|
||||
assert.True(t, shouldCreateSkippedCommitStatusForFilteredWorkflow(input, &actions_module.DetectedWorkflow{
|
||||
TriggerEvent: &jobparser.Event{Name: actions_module.GithubEventPullRequestTarget},
|
||||
}))
|
||||
|
||||
assert.True(t, shouldCreateSkippedCommitStatusForFilteredWorkflow(newNotifyInput(&repo_model.Repository{ID: 1}, &user_model.User{ID: 2}, actions_module.GithubEventPullRequest), &actions_module.DetectedWorkflow{
|
||||
TriggerEvent: &jobparser.Event{Name: actions_module.GithubEventPullRequest},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -40,17 +40,13 @@ func parseRawPermissionsExplicit(rawPerms *yaml.Node) *repo_model.ActionsTokenPe
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unwrap DocumentNode and resolve AliasNode
|
||||
// Unwrap DocumentNode
|
||||
node := rawPerms
|
||||
for node.Kind == yaml.DocumentNode || node.Kind == yaml.AliasNode {
|
||||
if node.Kind == yaml.DocumentNode {
|
||||
if len(node.Content) == 0 {
|
||||
return nil
|
||||
}
|
||||
node = node.Content[0]
|
||||
} else {
|
||||
node = node.Alias
|
||||
for node.Kind == yaml.DocumentNode {
|
||||
if len(node.Content) == 0 {
|
||||
return nil
|
||||
}
|
||||
node = node.Content[0]
|
||||
}
|
||||
|
||||
if node.Kind == yaml.ScalarNode && node.Value == "" {
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"gitea.dev/services/convert"
|
||||
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error {
|
||||
@@ -125,12 +124,12 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
||||
return 0, err
|
||||
}
|
||||
|
||||
singleWorkflow := &jobparser.SingleWorkflow{}
|
||||
if err := yaml.Unmarshal(content, singleWorkflow); err != nil {
|
||||
workflow, err := jobparser.ReadWorkflow(content)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to unmarshal workflow content: %w", err)
|
||||
}
|
||||
// get inputs from post
|
||||
workflowDispatch := singleWorkflow.WorkflowDispatchConfig()
|
||||
workflowDispatch := workflow.WorkflowDispatchConfig()
|
||||
if workflowDispatch == nil {
|
||||
return 0, util.ErrorWrapTranslatable(
|
||||
util.NewInvalidArgumentErrorf("workflow %q has no workflow_dispatch event trigger", workflowID),
|
||||
|
||||
@@ -143,7 +143,9 @@ func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer *
|
||||
}
|
||||
}
|
||||
|
||||
if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
|
||||
// an SSH instance key can never verify an OpenPGP signature, and exporting its path through gpg would only yield an empty key
|
||||
if setting.Repository.Signing.SigningFormat != git.SigningKeyFormatSSH &&
|
||||
setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
|
||||
// OK we should try the default key
|
||||
gpgSettings := git.GPGSettings{
|
||||
Sign: true,
|
||||
@@ -167,7 +169,7 @@ func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer *
|
||||
log.Error("Error getting default public gpg key: %v", err)
|
||||
} else if defaultGPGSettings == nil {
|
||||
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
|
||||
} else if defaultGPGSettings.Sign {
|
||||
} else if defaultGPGSettings.Sign && defaultGPGSettings.Format != git.SigningKeyFormatSSH {
|
||||
if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
|
||||
if commitVerification.Reason == asymkey_model.BadSignature {
|
||||
defaultReason = asymkey_model.BadSignature
|
||||
|
||||
@@ -260,7 +260,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(ctx, pr, doer, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
|
||||
if err := pull_service.Merge(pr, doer, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
|
||||
log.Error("pull_service.Merge: %v", err)
|
||||
// FIXME: if merge failed, we should display some error message to the pull request page.
|
||||
// The resolution is add a new column on automerge table named `error_message` to store the error message and displayed
|
||||
|
||||
@@ -63,11 +63,25 @@ func (prc *PullRequestContext) CanCreateNewPull() bool {
|
||||
return can
|
||||
}
|
||||
|
||||
// CompareHeadRef formats the head side of a compare link, "owner/repo:branch" is only needed when a fork can share its base repo's owner
|
||||
func CompareHeadRef(baseRepo, headRepo *repo_model.Repository, headBranch string) string {
|
||||
if baseRepo.ID == headRepo.ID /* same repo */ {
|
||||
return headBranch
|
||||
} else if baseRepo.OwnerID == headRepo.OwnerID /* same owner */ {
|
||||
return headRepo.FullName() + ":" + headBranch
|
||||
}
|
||||
// not the same owner: if there can be multiple forks in one owner, we still need the full name
|
||||
if setting.Repository.AllowForkIntoSameOwner {
|
||||
return headRepo.FullName() + ":" + headBranch
|
||||
}
|
||||
// if there is only one fork in the different owner, we only need the owner's name for the head ref
|
||||
return headRepo.OwnerName + ":" + headBranch
|
||||
}
|
||||
|
||||
func (prc *PullRequestContext) MakeDefaultCompareLink(headBranch string) string {
|
||||
return prc.baseRepo.Link() + "/compare/" +
|
||||
util.PathEscapeSegments(prc.DefaultTargetBranch()) + "..." +
|
||||
util.Iif(prc.SameRepo(), "", util.PathEscapeSegments(prc.headRepo.OwnerName)+":") +
|
||||
util.PathEscapeSegments(headBranch)
|
||||
util.PathEscapeSegments(CompareHeadRef(prc.baseRepo, prc.headRepo, headBranch))
|
||||
}
|
||||
|
||||
func (prc *PullRequestContext) DefaultTargetBranch() string {
|
||||
@@ -703,6 +717,9 @@ func repoAssignmentPrepareGitRepo(ctx *Context, data *repoAssignmentPrepareDataS
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
|
||||
if ctx.Repo.Repository.IsBeingCreated() {
|
||||
return
|
||||
}
|
||||
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RelativePath(), err)
|
||||
ctx.Repo.Repository.MarkAsBrokenEmpty()
|
||||
// Only allow access to base of repo or settings
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompareHeadRef(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner, false)()
|
||||
baseRepo := &repo_model.Repository{ID: 1, OwnerID: 100, OwnerName: "base-owner", Name: "base-repo"}
|
||||
sameRepo := baseRepo
|
||||
sameOwner := &repo_model.Repository{ID: 2, OwnerID: 100, OwnerName: "head-owner", Name: "head-repo"}
|
||||
diffOwner := &repo_model.Repository{ID: 2, OwnerID: 101, OwnerName: "head-owner", Name: "head-repo"}
|
||||
|
||||
assert.Equal(t, "my-branch", CompareHeadRef(baseRepo, sameRepo, "my-branch"))
|
||||
assert.Equal(t, "head-owner/head-repo:my-branch", CompareHeadRef(baseRepo, sameOwner, "my-branch"))
|
||||
assert.Equal(t, "head-owner:my-branch", CompareHeadRef(baseRepo, diffOwner, "my-branch"))
|
||||
setting.Repository.AllowForkIntoSameOwner = true
|
||||
assert.Equal(t, "head-owner/head-repo:my-branch", CompareHeadRef(baseRepo, diffOwner, "my-branch"))
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
@@ -39,8 +39,6 @@ import (
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/gitdiff"
|
||||
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
)
|
||||
|
||||
// ToEmail convert models.EmailAddress to api.Email
|
||||
@@ -557,7 +555,7 @@ func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, co
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
name := entry.Name()
|
||||
if err == nil {
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
workflow, err := jobparser.ReadWorkflow(content)
|
||||
if err == nil {
|
||||
// Only use the name when specified in the workflow file
|
||||
if workflow.Name != "" {
|
||||
|
||||
@@ -28,7 +28,7 @@ type CreateRepoForm struct {
|
||||
Description string `binding:"MaxSize(2048)"`
|
||||
DefaultBranch string `binding:"GitRefName;MaxSize(100)"`
|
||||
AutoInit bool
|
||||
Gitignores string
|
||||
Gitignores string `binding:"MaxSize(1024)"`
|
||||
IssueLabels string `binding:"MaxSize(255)"`
|
||||
License string `binding:"MaxSize(100)"`
|
||||
Readme string `binding:"MaxSize(255)"`
|
||||
|
||||
@@ -42,14 +42,15 @@ func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTi
|
||||
return "", err
|
||||
}
|
||||
|
||||
permission := webCtx.Repo.Permission
|
||||
if webCtx.Repo.Repository == nil || dbRepo.ID != webCtx.Repo.Repository.ID {
|
||||
perms, err := access.GetDoerRepoPermission(ctx, dbRepo, webCtx.Doer)
|
||||
permission, err = access.GetDoerRepoPermission(ctx, dbRepo, webCtx.Doer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !perms.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
return "", util.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
if !permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
return "", util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if issue.IsPull {
|
||||
|
||||
@@ -6,7 +6,10 @@ package markup
|
||||
import (
|
||||
"testing"
|
||||
|
||||
issue_model "gitea.dev/models/issues"
|
||||
perm_model "gitea.dev/models/perm"
|
||||
"gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/templates"
|
||||
@@ -14,36 +17,52 @@ import (
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestRenderHelperIssueIconTitle(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
|
||||
ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
|
||||
htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
|
||||
LinkHref: "/link",
|
||||
IssueIndex: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/link"><span>octicon-issue-opened(16/tw-text-green)</span> issue1 (#1)</a>`, string(htm))
|
||||
t.Run("RenderInCurrentRepo", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
|
||||
LinkHref: "/link",
|
||||
IssueIndex: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/link"><span>octicon-issue-opened(16/tw-text-green)</span> issue1 (#1)</a>`, string(htm))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
|
||||
htm, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo1",
|
||||
LinkHref: "/link",
|
||||
IssueIndex: 1,
|
||||
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode([]*repo.RepoUnit{{Type: unit.TypeWiki}}, perm_model.AccessModeRead)
|
||||
issueA := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{}, builder.Eq{"repo_id": 1, "`index`": 1, "is_pull": false})
|
||||
issueB := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{}, builder.Eq{"repo_id": 1, "`index`": 2, "is_pull": true})
|
||||
for _, issueIndex := range []int64{issueA.Index, issueB.Index} {
|
||||
_, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
|
||||
LinkHref: "/link",
|
||||
IssueIndex: issueIndex,
|
||||
})
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
}
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/link"><span>octicon-issue-opened(16/tw-text-green)</span> issue1 (user2/repo1#1)</a>`, string(htm))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
|
||||
_, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo2",
|
||||
LinkHref: "/link",
|
||||
IssueIndex: 2,
|
||||
t.Run("RenderAcrossRepo", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
|
||||
htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo1",
|
||||
LinkHref: "/link",
|
||||
IssueIndex: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/link"><span>octicon-issue-opened(16/tw-text-green)</span> issue1 (user2/repo1#1)</a>`, string(htm))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
|
||||
_, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo2",
|
||||
LinkHref: "/link",
|
||||
IssueIndex: 2,
|
||||
})
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
})
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, token string) (
|
||||
var resp *gitlab.Response
|
||||
u, _ := url.Parse(baseURL)
|
||||
for len(pathParts) >= 2 {
|
||||
_, resp, err = gitlabClient.Version.GetVersion()
|
||||
_, resp, err = gitlabClient.Version.GetVersion(gitlab.WithContext(ctx))
|
||||
if err == nil || resp != nil && resp.StatusCode == http.StatusUnauthorized {
|
||||
err = nil // if no authentication given, this still should work
|
||||
break
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -359,6 +360,28 @@ func TestGitlabDownloadRepo(t *testing.T) {
|
||||
}, rvs)
|
||||
}
|
||||
|
||||
func TestGitlabVersionProbeUsesMigrationContext(t *testing.T) {
|
||||
started := make(chan struct{})
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
close(started)
|
||||
<-r.Context().Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := NewGitlabDownloader(ctx, server.URL, "owner/repo", "")
|
||||
result <- err
|
||||
}()
|
||||
|
||||
<-started
|
||||
cancel()
|
||||
assert.Error(t, <-result)
|
||||
}
|
||||
|
||||
func gitlabClientMockSetup(t *testing.T) (*http.ServeMux, *httptest.Server, *gitlab.Client) {
|
||||
// mux is the HTTP request multiplexer used with the test server.
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -21,7 +21,10 @@ import (
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const OneDevRequiredVersion = "12.0.1"
|
||||
const (
|
||||
OneDevRequiredVersion = "12.0.1"
|
||||
maxOneDevVersionResponseSize = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
_ base.Downloader = &OneDevDownloader{}
|
||||
@@ -137,10 +140,13 @@ func (d *OneDevDownloader) callAPI(ctx context.Context, endpoint string, paramet
|
||||
|
||||
// special case to read OneDev server version, which is not valid JSON
|
||||
if presult, ok := result.(**version.Version); ok {
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
bytes, err := io.ReadAll(io.LimitReader(resp.Body, maxOneDevVersionResponseSize+1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(bytes) > maxOneDevVersionResponseSize {
|
||||
return fmt.Errorf("OneDev server version response exceeds %d bytes", maxOneDevVersionResponseSize)
|
||||
}
|
||||
vers, err := version.NewVersion(string(bytes))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,9 +17,24 @@ import (
|
||||
"gitea.dev/models/unittest"
|
||||
base "gitea.dev/modules/migration"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOneDevVersionResponseSizeLimit(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(bytes.Repeat([]byte("1"), maxOneDevVersionResponseSize+1))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
baseURL, err := url.Parse(server.URL)
|
||||
assert.NoError(t, err)
|
||||
download := &OneDevDownloader{baseURL: baseURL, client: server.Client()}
|
||||
var version *version.Version
|
||||
err = download.callAPI(t.Context(), "/~api/version/server", nil, &version)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestOneDevDownloadRepo(t *testing.T) {
|
||||
liveMode := os.Getenv("ONEDEV_LIVE") != ""
|
||||
|
||||
|
||||
+37
-27
@@ -28,6 +28,7 @@ import (
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/globallock"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/references"
|
||||
@@ -218,9 +219,28 @@ func (err ErrInvalidMergeStyle) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
func addTestPullRequestTaskAfterWebOperation(pr *issues_model.PullRequest, doer *user_model.User) {
|
||||
// This is a duplicated call to AddTestPullRequestTask (it will also be called by the post-receive hook, via a push queue).
|
||||
// This call will do some operations (push to base repo, sync commit divergence, add PR conflict check queue task, etc)
|
||||
// immediately instead of waiting for the "push queue"'s task. The code is from https://github.com/go-gitea/gitea/pull/7082.
|
||||
// But it's really questionable whether it's worth to do it ahead without waiting for the "push queue" task to run.
|
||||
// TODO: DUPLICATE-PR-TASK: maybe can try to remove this in 1.26 to see if there is any issue.
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: pr.BaseRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: pr.BaseBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}
|
||||
|
||||
// Merge merges pull request to base repository.
|
||||
// Caller should check PR is ready to be merged (review and status checks)
|
||||
func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
|
||||
func Merge(pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
|
||||
ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled
|
||||
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("Unable to load base repo: %v", err)
|
||||
return fmt.Errorf("unable to load base repo: %w", err)
|
||||
@@ -241,37 +261,27 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
|
||||
return ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
|
||||
}
|
||||
|
||||
releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID))
|
||||
if err != nil {
|
||||
log.Error("lock.Lock(): %v", err)
|
||||
return fmt.Errorf("lock.Lock: %w", err)
|
||||
}
|
||||
defer releaser()
|
||||
defer func() {
|
||||
// This is a duplicated call to AddTestPullRequestTask (it will also be called by the post-receive hook, via a push queue).
|
||||
// This call will do some operations (push to base repo, sync commit divergence, add PR conflict check queue task, etc)
|
||||
// immediately instead of waiting for the "push queue"'s task. The code is from https://github.com/go-gitea/gitea/pull/7082.
|
||||
// But it's really questionable whether it's worth to do it ahead without waiting for the "push queue" task to run.
|
||||
// TODO: DUPLICATE-PR-TASK: maybe can try to remove this in 1.26 to see if there is any issue.
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: pr.BaseRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: pr.BaseBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
|
||||
_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
|
||||
releaser()
|
||||
err = globallock.LockAndDo(ctx, getPullWorkingLockKey(pr.ID), func(ctx context.Context) error {
|
||||
_, err := doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
|
||||
return err
|
||||
})
|
||||
defer addTestPullRequestTaskAfterWebOperation(pr, doer) // keep the same behavior as old code: always call AddTestPullRequestTask
|
||||
// TODO: the "merge" operation has finished, there could still be some edge cases:
|
||||
// * if the post-process hook isn't executed correctly:
|
||||
// * the commit has been merged into target branch
|
||||
// * the PR's status is still "open (unmerged)"
|
||||
// * something wrong happens (e.g.: out of sync?)
|
||||
// * maybe this is the reason that why the duplicate AddTestPullRequestTask is called in defer func above
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: it is questionable whether it should return error here, the "merge" operation has succeeded
|
||||
return handleMergePostProcess(ctx, pr.ID, doer, wasAutoMerged)
|
||||
}
|
||||
|
||||
func handleMergePostProcess(ctx context.Context, prID int64, doer *user_model.User, wasAutoMerged bool) error {
|
||||
// reload pull request because it has been updated by post receive hook
|
||||
pr, err = issues_model.GetPullRequestByID(ctx, pr.ID)
|
||||
pr, err := issues_model.GetPullRequestByID(ctx, prID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+14
-15
@@ -16,12 +16,14 @@ import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/globallock"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/repository"
|
||||
)
|
||||
|
||||
// Update updates pull request with base branch.
|
||||
func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
|
||||
func Update(pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
|
||||
ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled
|
||||
if pr.Flow == issues_model.PullRequestFlowAGit {
|
||||
// TODO: update of agit flow pull request's head branch is unsupported
|
||||
return errors.New("update of agit flow pull request's head branch is unsupported")
|
||||
@@ -62,20 +64,10 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||
return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// The code is from https://github.com/go-gitea/gitea/pull/9784,
|
||||
// it seems a simple copy-paste from https://github.com/go-gitea/gitea/pull/7082 without a real reason.
|
||||
// TODO: DUPLICATE-PR-TASK: search and see another TODO comment for more details
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: pr.BaseRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: pr.BaseBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
// TODO: The code is from https://github.com/go-gitea/gitea/pull/9784,
|
||||
// it seems a simple copy-paste from https://github.com/go-gitea/gitea/pull/7082 without a real reason.
|
||||
// TODO: DUPLICATE-PR-TASK: search and see another TODO comment for more details
|
||||
defer addTestPullRequestTaskAfterWebOperation(pr, doer)
|
||||
|
||||
if rebase {
|
||||
return updateHeadByRebaseOnToBase(ctx, pr, doer)
|
||||
@@ -97,6 +89,13 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||
}
|
||||
|
||||
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
|
||||
// TODO: the "update" (merge target branch to PR head branch) operation has finished, there could still be some edge cases:
|
||||
// * the database was already out of sync: the target branch was already in head branch:
|
||||
// * so no post-receive hook is really executed, no PR status update
|
||||
// * then the PR status is stuck in "behind the target branch" (a new push can be used as a workaround)
|
||||
// * "merge" operation does finish, but the post-receive hook isn't correctly executed due to other reasons:
|
||||
// * although the target branch is merged into head branch by this "update" (head branch receives new commits)
|
||||
// * but database isn't updated, so the PR status is still "behind the target branch"
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_
|
||||
BaseBranch: divergingInfo.BaseBranchName,
|
||||
}
|
||||
fakeIssue.PullRequest = fakePR
|
||||
err = pull.Update(ctx, fakePR, doer, "merge upstream", false)
|
||||
err = pull.Update(fakePR, doer, "merge upstream", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
{{range $user, $trackedtime := .WorkingUsers}}
|
||||
<div class="item tw-gap-3">
|
||||
{{template "shared/user/avatarlink" dict "user" $user}}
|
||||
<div>
|
||||
<div class="muted-links">
|
||||
{{template "shared/user/authorlink" $user}}
|
||||
<div class="text">{{$trackedtime|Sec2Hour}}</div>
|
||||
</div>
|
||||
|
||||
@@ -361,9 +361,10 @@
|
||||
</div>
|
||||
{{else if eq .Type 22}}
|
||||
<div class="timeline-item-group" id="{{.HashTag}}">
|
||||
<div class="timeline-item event">
|
||||
{{$reviewType := 2}}{{/* default to "comment" type if the review record is missing */}}
|
||||
{{if .Review}}{{$reviewType = .Review.Type}}{{end}}
|
||||
{{/* default to the "comment" type and the comment anchor if the review record is missing */}}
|
||||
{{$reviewType := 2}}{{$reviewHashTag := .HashTag}}
|
||||
{{if .Review}}{{$reviewType = .Review.Type}}{{$reviewHashTag = .Review.HashTag}}{{end}}
|
||||
<div class="timeline-item event"{{if .Review}} id="{{$reviewHashTag}}"{{end}}>
|
||||
{{if not .OriginalAuthor}}
|
||||
{{/* Some timeline avatars need a offset to correctly align with their speech bubble.
|
||||
The condition depends on whether the comment has contents/attachments,
|
||||
@@ -381,12 +382,13 @@
|
||||
</span>
|
||||
<span class="comment-text-line">
|
||||
{{template "repo/issue/view_content/comments_authorlink" dict "comment" .}}
|
||||
{{$createdLink := HTMLFormat `<a href="#%s">%s</a>` $reviewHashTag $createdStr}}
|
||||
{{if eq $reviewType 1}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.approve" $createdLink}}
|
||||
{{else if eq $reviewType 3}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.reject" $createdStr}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.reject" $createdLink}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.comment" $createdLink}}
|
||||
{{end}}
|
||||
{{if and .Review .Review.Dismissed}}
|
||||
<div class="ui small label">{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-trailing">
|
||||
<span class="color-text-light-2">
|
||||
<span>
|
||||
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
|
||||
</span>
|
||||
<button class="btn interact-bg show-modal tw-p-2"
|
||||
|
||||
@@ -1 +1 @@
|
||||
<a class="muted tw-text-text tw-font-semibold"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsTypeBot}} <span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
|
||||
<a class="tw-font-semibold"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsTypeBot}} <span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-trailing">
|
||||
<span class="color-text-light-2">
|
||||
<span>
|
||||
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
|
||||
</span>
|
||||
<button class="btn interact-bg tw-p-2 show-modal"
|
||||
|
||||
Generated
+1
-5
@@ -25063,7 +25063,6 @@
|
||||
"login_name": {
|
||||
"description": "identifier of the user, provided by the external authenticator (if configured)",
|
||||
"type": "string",
|
||||
"default": "empty",
|
||||
"x-go-name": "LoginName"
|
||||
},
|
||||
"must_change_password": {
|
||||
@@ -26249,8 +26248,7 @@
|
||||
"description": "EditUserOption edit user options",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source_id",
|
||||
"login_name"
|
||||
"source_id"
|
||||
],
|
||||
"properties": {
|
||||
"active": {
|
||||
@@ -26301,7 +26299,6 @@
|
||||
"login_name": {
|
||||
"description": "identifier of the user, provided by the external authenticator (if configured)",
|
||||
"type": "string",
|
||||
"default": "empty",
|
||||
"x-go-name": "LoginName"
|
||||
},
|
||||
"max_repo_creation": {
|
||||
@@ -30658,7 +30655,6 @@
|
||||
"login_name": {
|
||||
"description": "identifier of the user, provided by the external authenticator (if configured)",
|
||||
"type": "string",
|
||||
"default": "empty",
|
||||
"x-go-name": "LoginName"
|
||||
},
|
||||
"prohibit_login": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user