Merge branch 'main' into Badge

pull/31262/head
techknowlogick 1 week ago committed by GitHub
commit 4890a15467
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -403,7 +403,7 @@ module.exports = {
'github/a11y-svg-has-accessible-name': [0],
'github/array-foreach': [0],
'github/async-currenttarget': [2],
'github/async-preventdefault': [2],
'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599
'github/authenticity-token': [0],
'github/get-attribute': [0],
'github/js-class-name': [0],

@ -18,10 +18,12 @@ import (
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install"
@ -218,6 +220,8 @@ func serveInstalled(ctx *cli.Context) error {
}
}
gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond))
// Set up Chi routes
webRoutes := routers.NormalRoutes()
err := listen(webRoutes, true)

@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1736798957,
"narHash": "sha256-qwpCtZhSsSNQtK4xYGzMiyEDhkNzOCz/Vfu4oL2ETsQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "9abb87b552b7f55ac8916b6fc9e5cb486656a2f3",
"type": "github"
},
"original": {

@ -29,9 +29,14 @@
poetry
# backend
go_1_23
gofumpt
sqlite
];
shellHook = ''
export GO="${pkgs.go_1_23}/bin/go"
export GOROOT="${pkgs.go_1_23}/share/go"
'';
};
}
);

@ -7,23 +7,36 @@ import (
"context"
"time"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm/contexts"
)
type SlowQueryHook struct {
type EngineHook struct {
Threshold time.Duration
Logger log.Logger
}
var _ contexts.Hook = (*SlowQueryHook)(nil)
var _ contexts.Hook = (*EngineHook)(nil)
func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
return ctx, nil
}
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
span := gtprof.GetContextSpan(c.Ctx)
if span != nil {
// Do not record SQL parameters here:
// * It shouldn't expose the parameters because they contain sensitive information, end users need to report the trace details safely.
// * Some parameters contain quite long texts, waste memory and are difficult to display.
span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL)
span.End()
} else {
setting.PanicInDevOrTesting("span in database engine hook is nil")
}
if c.ExecuteTime >= h.Threshold {
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
// is being displayed (the function that ultimately wants to execute the query in the code)

@ -72,7 +72,7 @@ func InitEngine(ctx context.Context) error {
xe.SetDefaultContext(ctx)
if setting.Database.SlowQueryThreshold > 0 {
xe.AddHook(&SlowQueryHook{
xe.AddHook(&EngineHook{
Threshold: setting.Database.SlowQueryThreshold,
Logger: log.GetLogger("xorm"),
})

@ -171,3 +171,9 @@
user_id: 40
repo_id: 61
mode: 4
-
id: 30
user_id: 40
repo_id: 1
mode: 2

@ -167,6 +167,9 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
BranchName: branchName,
}
}
// FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
// It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
// In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
return &branch, nil
}
@ -440,6 +443,8 @@ type FindRecentlyPushedNewBranchesOptions struct {
}
type RecentlyPushedNewBranch struct {
BranchRepo *repo_model.Repository
BranchName string
BranchDisplayName string
BranchLink string
BranchCompareURL string
@ -540,7 +545,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
}
newBranches = append(newBranches, &RecentlyPushedNewBranch{
BranchRepo: branch.Repo,
BranchDisplayName: branchDisplayName,
BranchName: branch.Name,
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
CommitTime: branch.CommitTime,

@ -46,11 +46,6 @@ func (s Stopwatch) Seconds() int64 {
return int64(timeutil.TimeStampNow() - s.CreatedUnix)
}
// Duration returns a human-readable duration string based on local server time
func (s Stopwatch) Duration() string {
return util.SecToTime(s.Seconds())
}
func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
sw = new(Stopwatch)
exists, err = db.GetEngine(ctx).
@ -201,7 +196,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
Doer: user,
Issue: issue,
Repo: issue.Repo,
Content: util.SecToTime(timediff),
Content: util.SecToHours(timediff),
Type: CommentTypeStopTracking,
TimeID: tt.ID,
}); err != nil {

@ -18,6 +18,7 @@ import (
"time"
"code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util"
@ -54,7 +55,7 @@ func logArgSanitize(arg string) string {
} else if filepath.IsAbs(arg) {
base := filepath.Base(arg)
dir := filepath.Dir(arg)
return filepath.Join(filepath.Base(dir), base)
return ".../" + filepath.Join(filepath.Base(dir), base)
}
return arg
}
@ -295,15 +296,20 @@ func (c *Command) run(skip int, opts *RunOpts) error {
timeout = defaultCommandExecutionTimeout
}
var desc string
cmdLogString := c.LogString()
callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */)
if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
callerInfo = callerInfo[pos+1:]
}
// these logs are for debugging purposes only, so no guarantee of correctness or stability
desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString())
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString)
log.Debug("git.Command: %s", desc)
_, span := gtprof.GetTracer().Start(c.parentContext, gtprof.TraceSpanGitRun)
defer span.End()
span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo)
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
var ctx context.Context
var cancel context.CancelFunc
var finished context.CancelFunc

@ -58,5 +58,5 @@ func TestCommandString(t *testing.T) {
assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString())
cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b")
assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString())
assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString())
}

@ -64,7 +64,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
} else if commit.ParentCount() == 0 {
cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
} else {
c, _ := commit.Parent(0)
c, err := commit.Parent(0)
if err != nil {
return err
}
cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...)
}
case RawDiffPatch:
@ -74,7 +77,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
} else if commit.ParentCount() == 0 {
cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
} else {
c, _ := commit.Parent(0)
c, err := commit.Parent(0)
if err != nil {
return err
}
query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...)
}

@ -57,7 +57,7 @@ func (repo *Repository) IsBranchExist(name string) bool {
// GetBranches returns branches from the repository, skipping "skip" initial branches and
// returning at most "limit" branches, or all branches if "limit" is 0.
// Branches are returned with sort of `-commiterdate` as the nogogit
// Branches are returned with sort of `-committerdate` as the nogogit
// implementation. This requires full fetch, sort and then the
// skip/limit applies later as gogit returns in undefined order.
func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) {

@ -0,0 +1,32 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gtprof
type EventConfig struct {
attributes []*TraceAttribute
}
type EventOption interface {
applyEvent(*EventConfig)
}
type applyEventFunc func(*EventConfig)
func (f applyEventFunc) applyEvent(cfg *EventConfig) {
f(cfg)
}
func WithAttributes(attrs ...*TraceAttribute) EventOption {
return applyEventFunc(func(cfg *EventConfig) {
cfg.attributes = append(cfg.attributes, attrs...)
})
}
func eventConfigFromOptions(options ...EventOption) *EventConfig {
cfg := &EventConfig{}
for _, opt := range options {
opt.applyEvent(cfg)
}
return cfg
}

@ -0,0 +1,175 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gtprof
import (
"context"
"fmt"
"sync"
"time"
"code.gitea.io/gitea/modules/util"
)
type contextKey struct {
name string
}
var contextKeySpan = &contextKey{"span"}
type traceStarter interface {
start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal)
}
type traceSpanInternal interface {
addEvent(name string, cfg *EventConfig)
recordError(err error, cfg *EventConfig)
end()
}
type TraceSpan struct {
// immutable
parent *TraceSpan
internalSpans []traceSpanInternal
internalContexts []context.Context
// mutable, must be protected by mutex
mu sync.RWMutex
name string
statusCode uint32
statusDesc string
startTime time.Time
endTime time.Time
attributes []*TraceAttribute
children []*TraceSpan
}
type TraceAttribute struct {
Key string
Value TraceValue
}
type TraceValue struct {
v any
}
func (t *TraceValue) AsString() string {
return fmt.Sprint(t.v)
}
func (t *TraceValue) AsInt64() int64 {
v, _ := util.ToInt64(t.v)
return v
}
func (t *TraceValue) AsFloat64() float64 {
v, _ := util.ToFloat64(t.v)
return v
}
var globalTraceStarters []traceStarter
type Tracer struct {
starters []traceStarter
}
func (s *TraceSpan) SetName(name string) {
s.mu.Lock()
defer s.mu.Unlock()
s.name = name
}
func (s *TraceSpan) SetStatus(code uint32, desc string) {
s.mu.Lock()
defer s.mu.Unlock()
s.statusCode, s.statusDesc = code, desc
}
func (s *TraceSpan) AddEvent(name string, options ...EventOption) {
cfg := eventConfigFromOptions(options...)
for _, tsp := range s.internalSpans {
tsp.addEvent(name, cfg)
}
}
func (s *TraceSpan) RecordError(err error, options ...EventOption) {
cfg := eventConfigFromOptions(options...)
for _, tsp := range s.internalSpans {
tsp.recordError(err, cfg)
}
}
func (s *TraceSpan) SetAttributeString(key, value string) *TraceSpan {
s.mu.Lock()
defer s.mu.Unlock()
s.attributes = append(s.attributes, &TraceAttribute{Key: key, Value: TraceValue{v: value}})
return s
}
func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *TraceSpan) {
starters := t.starters
if starters == nil {
starters = globalTraceStarters
}
ts := &TraceSpan{name: spanName, startTime: time.Now()}
parentSpan := GetContextSpan(ctx)
if parentSpan != nil {
parentSpan.mu.Lock()
parentSpan.children = append(parentSpan.children, ts)
parentSpan.mu.Unlock()
ts.parent = parentSpan
}
parentCtx := ctx
for internalSpanIdx, tsp := range starters {
var internalSpan traceSpanInternal
if parentSpan != nil {
parentCtx = parentSpan.internalContexts[internalSpanIdx]
}
ctx, internalSpan = tsp.start(parentCtx, ts, internalSpanIdx)
ts.internalContexts = append(ts.internalContexts, ctx)
ts.internalSpans = append(ts.internalSpans, internalSpan)
}
ctx = context.WithValue(ctx, contextKeySpan, ts)
return ctx, ts
}
type mutableContext interface {
context.Context
SetContextValue(key, value any)
GetContextValue(key any) any
}
// StartInContext starts a trace span in Gitea's mutable context (usually the web request context).
// Due to the design limitation of Gitea's web framework, it can't use `context.WithValue` to bind a new span into a new context.
// So here we use our "reqctx" framework to achieve the same result: web request context could always see the latest "span".
func (t *Tracer) StartInContext(ctx mutableContext, spanName string) (*TraceSpan, func()) {
curTraceSpan := GetContextSpan(ctx)
_, newTraceSpan := GetTracer().Start(ctx, spanName)
ctx.SetContextValue(contextKeySpan, newTraceSpan)
return newTraceSpan, func() {
newTraceSpan.End()
ctx.SetContextValue(contextKeySpan, curTraceSpan)
}
}
func (s *TraceSpan) End() {
s.mu.Lock()
s.endTime = time.Now()
s.mu.Unlock()
for _, tsp := range s.internalSpans {
tsp.end()
}
}
func GetTracer() *Tracer {
return &Tracer{}
}
func GetContextSpan(ctx context.Context) *TraceSpan {
ts, _ := ctx.Value(contextKeySpan).(*TraceSpan)
return ts
}

@ -0,0 +1,96 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gtprof
import (
"context"
"fmt"
"strings"
"sync/atomic"
"time"
"code.gitea.io/gitea/modules/tailmsg"
)
type traceBuiltinStarter struct{}
type traceBuiltinSpan struct {
ts *TraceSpan
internalSpanIdx int
}
func (t *traceBuiltinSpan) addEvent(name string, cfg *EventConfig) {
// No-op because builtin tracer doesn't need it.
// In the future we might use it to mark the time point between backend logic and network response.
}
func (t *traceBuiltinSpan) recordError(err error, cfg *EventConfig) {
// No-op because builtin tracer doesn't need it.
// Actually Gitea doesn't handle err this way in most cases
}
func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) {
t.ts.mu.RLock()
defer t.ts.mu.RUnlock()
out.WriteString(strings.Repeat(" ", indent))
out.WriteString(t.ts.name)
if t.ts.endTime.IsZero() {
out.WriteString(" duration: (not ended)")
} else {
out.WriteString(fmt.Sprintf(" duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds()))
}
for _, a := range t.ts.attributes {
out.WriteString(" ")
out.WriteString(a.Key)
out.WriteString("=")
value := a.Value.AsString()
if strings.ContainsAny(value, " \t\r\n") {
quoted := false
for _, c := range "\"'`" {
if quoted = !strings.Contains(value, string(c)); quoted {
value = string(c) + value + string(c)
break
}
}
if !quoted {
value = fmt.Sprintf("%q", value)
}
}
out.WriteString(value)
}
out.WriteString("\n")
for _, c := range t.ts.children {
span := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan)
span.toString(out, indent+2)
}
}
func (t *traceBuiltinSpan) end() {
if t.ts.parent == nil {
// TODO: debug purpose only
// TODO: it should distinguish between http response network lag and actual processing time
threshold := time.Duration(traceBuiltinThreshold.Load())
if threshold != 0 && t.ts.endTime.Sub(t.ts.startTime) > threshold {
sb := &strings.Builder{}
t.toString(sb, 0)
tailmsg.GetManager().GetTraceRecorder().Record(sb.String())
}
}
}
func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
return ctx, &traceBuiltinSpan{ts: traceSpan, internalSpanIdx: internalSpanIdx}
}
func init() {
globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{})
}
var traceBuiltinThreshold atomic.Int64
func EnableBuiltinTracer(threshold time.Duration) {
traceBuiltinThreshold.Store(int64(threshold))
}

@ -0,0 +1,19 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gtprof
// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
const (
TraceSpanHTTP = "http"
TraceSpanGitRun = "git-run"
TraceSpanDatabase = "database"
)
const (
TraceAttrFuncCaller = "func.caller"
TraceAttrDbSQL = "db.sql"
TraceAttrGitCommand = "git.command"
TraceAttrHTTPRoute = "http.route"
)

@ -0,0 +1,93 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gtprof
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
// "vendor span" is a simple demo for a span from a vendor library
var vendorContextKey any = "vendorContextKey"
type vendorSpan struct {
name string
children []*vendorSpan
}
func vendorTraceStart(ctx context.Context, name string) (context.Context, *vendorSpan) {
span := &vendorSpan{name: name}
parentSpan, ok := ctx.Value(vendorContextKey).(*vendorSpan)
if ok {
parentSpan.children = append(parentSpan.children, span)
}
ctx = context.WithValue(ctx, vendorContextKey, span)
return ctx, span
}
// below "testTrace*" integrate the vendor span into our trace system
type testTraceSpan struct {
vendorSpan *vendorSpan
}
func (t *testTraceSpan) addEvent(name string, cfg *EventConfig) {}
func (t *testTraceSpan) recordError(err error, cfg *EventConfig) {}
func (t *testTraceSpan) end() {}
type testTraceStarter struct{}
func (t *testTraceStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
ctx, span := vendorTraceStart(ctx, traceSpan.name)
return ctx, &testTraceSpan{span}
}
func TestTraceStarter(t *testing.T) {
globalTraceStarters = []traceStarter{&testTraceStarter{}}
ctx := context.Background()
ctx, span := GetTracer().Start(ctx, "root")
defer span.End()
func(ctx context.Context) {
ctx, span := GetTracer().Start(ctx, "span1")
defer span.End()
func(ctx context.Context) {
_, span := GetTracer().Start(ctx, "spanA")
defer span.End()
}(ctx)
func(ctx context.Context) {
_, span := GetTracer().Start(ctx, "spanB")
defer span.End()
}(ctx)
}(ctx)
func(ctx context.Context) {
_, span := GetTracer().Start(ctx, "span2")
defer span.End()
}(ctx)
var spanFullNames []string
var collectSpanNames func(parentFullName string, s *vendorSpan)
collectSpanNames = func(parentFullName string, s *vendorSpan) {
fullName := parentFullName + "/" + s.name
spanFullNames = append(spanFullNames, fullName)
for _, c := range s.children {
collectSpanNames(fullName, c)
}
}
collectSpanNames("", span.internalSpans[0].(*testTraceSpan).vendorSpan)
assert.Equal(t, []string{
"/root",
"/root/span1",
"/root/span1/spanA",
"/root/span1/spanB",
"/root/span2",
}, spanFullNames)
}

@ -6,7 +6,6 @@ package repository
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@ -52,9 +51,6 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
{
branches, _, err := gitRepo.GetBranchNames(0, 0)
if err != nil {
if strings.Contains(err.Error(), "ref file is empty") {
return 0, nil
}
return 0, err
}
log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)

@ -0,0 +1,73 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package tailmsg
import (
"sync"
"time"
)
type MsgRecord struct {
Time time.Time
Content string
}
type MsgRecorder interface {
Record(content string)
GetRecords() []*MsgRecord
}
type memoryMsgRecorder struct {
mu sync.RWMutex
msgs []*MsgRecord
limit int
}
// TODO: use redis for a clustered environment
func (m *memoryMsgRecorder) Record(content string) {
m.mu.Lock()
defer m.mu.Unlock()
m.msgs = append(m.msgs, &MsgRecord{
Time: time.Now(),
Content: content,
})
if len(m.msgs) > m.limit {
m.msgs = m.msgs[len(m.msgs)-m.limit:]
}
}
func (m *memoryMsgRecorder) GetRecords() []*MsgRecord {
m.mu.RLock()
defer m.mu.RUnlock()
ret := make([]*MsgRecord, len(m.msgs))
copy(ret, m.msgs)
return ret
}
func NewMsgRecorder(limit int) MsgRecorder {
return &memoryMsgRecorder{
limit: limit,
}
}
type Manager struct {
traceRecorder MsgRecorder
logRecorder MsgRecorder
}
func (m *Manager) GetTraceRecorder() MsgRecorder {
return m.traceRecorder
}
func (m *Manager) GetLogRecorder() MsgRecorder {
return m.logRecorder
}
var GetManager = sync.OnceValue(func() *Manager {
return &Manager{
traceRecorder: NewMsgRecorder(100),
logRecorder: NewMsgRecorder(1000),
}
})

@ -69,7 +69,7 @@ func NewFuncMap() template.FuncMap {
// time / number / format
"FileSize": base.FileSize,
"CountFmt": countFmt,
"Sec2Time": util.SecToTime,
"Sec2Time": util.SecToHours,
"TimeEstimateString": timeEstimateString,

@ -8,59 +8,17 @@ import (
"strings"
)
// SecToTime converts an amount of seconds to a human-readable string. E.g.
// 66s -> 1 minute 6 seconds
// 52410s -> 14 hours 33 minutes
// 563418 -> 6 days 12 hours
// 1563418 -> 2 weeks 4 days
// 3937125s -> 1 month 2 weeks
// 45677465s -> 1 year 6 months
func SecToTime(durationVal any) string {
// SecToHours converts an amount of seconds to a human-readable hours string.
// This is stable for planning and managing timesheets.
// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours.
func SecToHours(durationVal any) string {
duration, _ := ToInt64(durationVal)
formattedTime := ""
// The following four variables are calculated by taking
// into account the previously calculated variables, this avoids
// pitfalls when using remainders. As that could lead to incorrect
// results when the calculated number equals the quotient number.
remainingDays := duration / (60 * 60 * 24)
years := remainingDays / 365
remainingDays -= years * 365
months := remainingDays * 12 / 365
remainingDays -= months * 365 / 12
weeks := remainingDays / 7
remainingDays -= weeks * 7
days := remainingDays
// The following three variables are calculated without depending
// on the previous calculated variables.
hours := (duration / 3600) % 24
hours := duration / 3600
minutes := (duration / 60) % 60
seconds := duration % 60
// Extract only the relevant information of the time
// If the time is greater than a year, it makes no sense to display seconds.
switch {
case years > 0:
formattedTime = formatTime(years, "year", formattedTime)
formattedTime = formatTime(months, "month", formattedTime)
case months > 0:
formattedTime = formatTime(months, "month", formattedTime)
formattedTime = formatTime(weeks, "week", formattedTime)
case weeks > 0:
formattedTime = formatTime(weeks, "week", formattedTime)
formattedTime = formatTime(days, "day", formattedTime)
case days > 0:
formattedTime = formatTime(days, "day", formattedTime)
formattedTime = formatTime(hours, "hour", formattedTime)
case hours > 0:
formattedTime := ""
formattedTime = formatTime(hours, "hour", formattedTime)
formattedTime = formatTime(minutes, "minute", formattedTime)
default:
formattedTime = formatTime(minutes, "minute", formattedTime)
formattedTime = formatTime(seconds, "second", formattedTime)
}
// The formatTime() function always appends a space at the end. This will be trimmed
return strings.TrimRight(formattedTime, " ")
@ -76,6 +34,5 @@ func formatTime(value int64, name, formattedTime string) string {
} else if value > 1 {
formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name)
}
return formattedTime
}

@ -9,22 +9,17 @@ import (
"github.com/stretchr/testify/assert"
)
func TestSecToTime(t *testing.T) {
func TestSecToHours(t *testing.T) {
second := int64(1)
minute := 60 * second
hour := 60 * minute
day := 24 * hour
year := 365 * day
assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second))
assert.Equal(t, "1 hour", SecToTime(hour))
assert.Equal(t, "1 hour", SecToTime(hour+second))
assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second))
assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second))
assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second))
assert.Equal(t, "4 weeks", SecToTime(4*7*day))
assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day))
assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second))
assert.Equal(t, "11 months", SecToTime(year-25*day))
assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second))
assert.Equal(t, "1 minute", SecToHours(minute+6*second))
assert.Equal(t, "1 hour", SecToHours(hour))
assert.Equal(t, "1 hour", SecToHours(hour+second))
assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second))
assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second))
assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second))
assert.Equal(t, "672 hours", SecToHours(4*7*day))
}

@ -121,7 +121,7 @@ func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo
return func(next http.Handler) http.Handler {
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
routing.UpdateFuncInfo(req.Context(), funcInfo)
defer routing.RecordFuncInfo(req.Context(), funcInfo)()
h.ServeHTTP(resp, req)
})
}
@ -157,7 +157,7 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
return // it's doing pre-check, just return
}
routing.UpdateFuncInfo(req.Context(), funcInfo)
defer routing.RecordFuncInfo(req.Context(), funcInfo)()
ret := fn.Call(argsIn)
// handle the return value (no-op at the moment)

@ -6,23 +6,30 @@ package routing
import (
"context"
"net/http"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/reqctx"
)
type contextKeyType struct{}
var contextKey contextKeyType
// UpdateFuncInfo updates a context's func info
func UpdateFuncInfo(ctx context.Context, funcInfo *FuncInfo) {
record, ok := ctx.Value(contextKey).(*requestRecord)
if !ok {
return
// RecordFuncInfo records a func info into context
func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) {
end = func() {}
if reqCtx := reqctx.FromContext(ctx); reqCtx != nil {
var traceSpan *gtprof.TraceSpan
traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func")
traceSpan.SetAttributeString("func", funcInfo.shortName)
}
if record, ok := ctx.Value(contextKey).(*requestRecord); ok {
record.lock.Lock()
record.funcInfo = funcInfo
record.lock.Unlock()
}
return end
}
// MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it
func MarkLongPolling(resp http.ResponseWriter, req *http.Request) {

@ -104,6 +104,12 @@ dist
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus

@ -167,5 +167,8 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc

@ -3,10 +3,6 @@
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

@ -1683,16 +1683,13 @@ issues.timetracker_timer_manually_add=Přidat čas
issues.time_estimate_set=Nastavit odhadovaný čas
issues.time_estimate_display=Odhad: %s
issues.change_time_estimate_at=změnil/a odhad času na <b>%s</b> %s
issues.remove_time_estimate_at=odstranil/a odhad času %s
issues.time_estimate_invalid=Formát odhadu času je neplatný
issues.start_tracking_history=započal/a práci %s
issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu
issues.tracking_already_started=`Již jste spustili sledování času na <a href="%s">jiném úkolu</a>!`
issues.stop_tracking_history=pracoval/a <b>%s</b> %s
issues.cancel_tracking_history=`zrušil/a sledování času %s`
issues.del_time=Odstranit tento časový záznam
issues.add_time_history=přidal/a strávený čas <b>%s</b> %s
issues.del_time_history=`odstranil/a strávený čas %s`
issues.add_time_manually=Přidat čas ručně
issues.add_time_hours=Hodiny
@ -3369,7 +3366,6 @@ monitor.execute_time=Doba provádění
monitor.last_execution_result=Výsledek
monitor.process.cancel=Zrušit proces
monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat
monitor.process.cancel_notices=Zrušit: <strong>%s</strong>?
monitor.process.children=Potomek
monitor.queues=Fronty
@ -3566,7 +3562,6 @@ conda.install=Pro instalaci balíčku pomocí Conda spusťte následující př
container.details.type=Typ obrazu
container.details.platform=Platforma
container.pull=Stáhněte obraz z příkazové řádky:
container.digest=Výběr:
container.multi_arch=OS/architektura
container.layers=Vrstvy obrazů
container.labels=Štítky

@ -1678,16 +1678,13 @@ issues.timetracker_timer_manually_add=Zeit hinzufügen
issues.time_estimate_set=Geschätzte Zeit festlegen
issues.time_estimate_display=Schätzung: %s
issues.change_time_estimate_at=Zeitschätzung geändert zu <b>%s</b> %s
issues.remove_time_estimate_at=Zeitschätzung %s entfernt
issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig
issues.start_tracking_history=hat die Zeiterfassung %s gestartet
issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird
issues.tracking_already_started=`Du hast die Zeiterfassung bereits in <a href="%s">diesem Issue</a> gestartet!`
issues.stop_tracking_history=hat für <b>%s</b> gearbeitet %s
issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen`
issues.del_time=Diese Zeiterfassung löschen
issues.add_time_history=hat <b>%s</b> gearbeitete Zeit hinzugefügt %s
issues.del_time_history=`hat %s gearbeitete Zeit gelöscht`
issues.add_time_manually=Zeit manuell hinzufügen
issues.add_time_hours=Stunden
@ -3359,7 +3356,6 @@ monitor.execute_time=Ausführungszeit
monitor.last_execution_result=Ergebnis
monitor.process.cancel=Prozess abbrechen
monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen
monitor.process.cancel_notices=Abbrechen: <strong>%s</strong>?
monitor.process.children=Subprozesse
monitor.queues=Warteschlangen
@ -3555,7 +3551,6 @@ conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befeh
container.details.type=Container-Image Typ
container.details.platform=Plattform
container.pull=Downloade das Container-Image aus der Kommandozeile:
container.digest=Digest:
container.multi_arch=Betriebsystem / Architektur
container.layers=Container-Image Ebenen
container.labels=Labels

@ -3236,7 +3236,6 @@ conda.install=Για να εγκαταστήσετε το πακέτο χρησ
container.details.type=Τύπος Εικόνας
container.details.platform=Πλατφόρμα
container.pull=Κατεβάστε την εικόνα από τη γραμμή εντολών:
container.digest=Σύνοψη:
container.multi_arch=ΛΣ / Αρχιτεκτονική
container.layers=Στρώματα Εικόνας
container.labels=Ετικέτες

@ -1690,16 +1690,16 @@ issues.timetracker_timer_manually_add = Add Time
issues.time_estimate_set = Set estimated time
issues.time_estimate_display = Estimate: %s
issues.change_time_estimate_at = changed time estimate to <b>%s</b> %s
issues.change_time_estimate_at = changed time estimate to <b>%[1]s</b> %[2]s
issues.remove_time_estimate_at = removed time estimate %s
issues.time_estimate_invalid = Time estimate format is invalid
issues.start_tracking_history = started working %s
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
issues.stop_tracking_history = worked for <b>%s</b> %s
issues.stop_tracking_history = worked for <b>%[1]s</b> %[2]s
issues.cancel_tracking_history = `canceled time tracking %s`
issues.del_time = Delete this time log
issues.add_time_history = added spent time <b>%s</b> %s
issues.add_time_history = added spent time <b>%[1]s</b> %[2]s
issues.del_time_history= `deleted spent time %s`
issues.add_time_manually = Manually Add Time
issues.add_time_hours = Hours
@ -1958,7 +1958,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[
pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
pulls.upstream_diverging_merge = Sync fork
pulls.upstream_diverging_merge_confirm = Would you like to merge base repository's default branch onto this repository's branch %s?
pulls.upstream_diverging_merge_confirm = Would you like to merge "%[1]s" onto "%[2]s"?
pull.deleted_branch = (deleted):%s
pull.agit_documentation = Review documentation about AGit
@ -2719,6 +2719,8 @@ branch.create_branch_operation = Create branch
branch.new_branch = Create new branch
branch.new_branch_from = Create new branch from "%s"
branch.renamed = Branch %s was renamed to %s.
branch.rename_default_or_protected_branch_error = Only admins can rename default or protected branches.
branch.rename_protected_branch_failed = This branch is protected by glob-based protection rules.
tag.create_tag = Create tag %s
tag.create_tag_operation = Create tag
@ -3396,6 +3398,8 @@ monitor.previous = Previous Time
monitor.execute_times = Executions
monitor.process = Running Processes
monitor.stacktrace = Stacktrace
monitor.trace = Trace
monitor.performance_logs = Performance Logs
monitor.processes_count = %d Processes
monitor.download_diagnosis_report = Download diagnosis report
monitor.desc = Description
@ -3404,7 +3408,6 @@ monitor.execute_time = Execution Time
monitor.last_execution_result = Result
monitor.process.cancel = Cancel process
monitor.process.cancel_desc = Cancelling a process may cause data loss
monitor.process.cancel_notices = Cancel: <strong>%s</strong>?
monitor.process.children = Children
monitor.queues = Queues
@ -3601,7 +3604,8 @@ conda.install = To install the package using Conda, run the following command:
container.details.type = Image Type
container.details.platform = Platform
container.pull = Pull the image from the command line:
container.digest = Digest:
container.images = Images
container.digest = Digest
container.multi_arch = OS / Arch
container.layers = Image Layers
container.labels = Labels

@ -3215,7 +3215,6 @@ conda.install=Para instalar el paquete usando Conda, ejecute el siguiente comand
container.details.type=Tipo de imagen
container.details.platform=Plataforma
container.pull=Arrastra la imagen desde la línea de comandos:
container.digest=Resumen:
container.multi_arch=SO / Arquitectura
container.layers=Capas de imagen
container.labels=Etiquetas

@ -1683,16 +1683,13 @@ issues.timetracker_timer_manually_add=Pointer du temps
issues.time_estimate_set=Définir le temps estimé
issues.time_estimate_display=Estimation : %s
issues.change_time_estimate_at=a changé le temps estimé à <b>%s</b> %s
issues.remove_time_estimate_at=a supprimé le temps estimé %s
issues.time_estimate_invalid=Le format du temps estimé est invalide
issues.start_tracking_history=`a commencé son travail %s.`
issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé.
issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur <a href="%s">un autre ticket</a> !`
issues.stop_tracking_history=`a fini de travailler sur <b>%s</b> %s.`
issues.cancel_tracking_history=`a abandonné son minuteur %s.`
issues.del_time=Supprimer ce minuteur du journal
issues.add_time_history=`a pointé du temps de travail %s.`
issues.del_time_history=`a supprimé son temps de travail %s.`
issues.add_time_manually=Temps pointé manuellement
issues.add_time_hours=Heures
@ -3370,7 +3367,6 @@ monitor.execute_time=Heure d'Éxécution
monitor.last_execution_result=Résultat
monitor.process.cancel=Annuler le processus
monitor.process.cancel_desc=Lannulation dun processus peut entraîner une perte de données.
monitor.process.cancel_notices=Annuler : <strong>%s</strong> ?
monitor.process.children=Enfant
monitor.queues=Files d'attente
@ -3567,7 +3563,6 @@ conda.install=Pour installer le paquet en utilisant Conda, exécutez la commande
container.details.type=Type d'image
container.details.platform=Plateforme
container.pull=Tirez l'image depuis un terminal :
container.digest=Empreinte :
container.multi_arch=SE / Arch
container.layers=Calques d'image
container.labels=Labels

@ -1684,16 +1684,13 @@ issues.timetracker_timer_manually_add=Cuir Am leis
issues.time_estimate_set=Socraigh am measta
issues.time_estimate_display=Meastachán: %s
issues.change_time_estimate_at=d'athraigh an meastachán ama go <b>%s</b> %s
issues.remove_time_estimate_at=baineadh meastachán ama %s
issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí
issues.start_tracking_history=thosaigh ag obair %s
issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo
issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar <a href="%s">eagrán eile</a>!`
issues.stop_tracking_history=d'oibrigh do <b>%s</b> %s
issues.cancel_tracking_history=`rianú ama curtha ar ceal %s`
issues.del_time=Scrios an log ama seo
issues.add_time_history=cuireadh am caite <b>%s</b> %s leis
issues.del_time_history=`an t-am caite scriosta %s`
issues.add_time_manually=Cuir Am leis de Láimh
issues.add_time_hours=Uaireanta
@ -3371,7 +3368,6 @@ monitor.execute_time=Am Forghníomhaithe
monitor.last_execution_result=Toradh
monitor.process.cancel=Cealaigh próiseas
monitor.process.cancel_desc=Má chuirtear próiseas ar ceal d'fhéadfadh go gcaillfí sonraí
monitor.process.cancel_notices=Cealaigh: <strong>%s</strong>?
monitor.process.children=Leanaí
monitor.queues=Scuaineanna
@ -3568,7 +3564,6 @@ conda.install=Chun an pacáiste a shuiteáil ag úsáid Conda, reáchtáil an t-
container.details.type=Cineál Íomhá
container.details.platform=Ardán
container.pull=Tarraing an íomhá ón líne ordaithe:
container.digest=Díleáigh:
container.multi_arch=Córas Oibriúcháin / Ailtireacht
container.layers=Sraitheanna Íomhá
container.labels=Lipéid

@ -1034,6 +1034,8 @@ fork_to_different_account=別のアカウントにフォークする
fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。
fork_branch=フォークにクローンされるブランチ
all_branches=すべてのブランチ
view_all_branches=すべてのブランチを表示
view_all_tags=すべてのタグを表示
fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
fork.blocked_user=リポジトリのオーナーがあなたをブロックしているため、リポジトリをフォークできません。
use_template=このテンプレートを使用
@ -1108,6 +1110,7 @@ delete_preexisting_success=%s の未登録ファイルを削除しました
blame_prior=この変更より前のBlameを表示
blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> で指定されたリビジョンは除外しています。 これを迂回して通常のBlame表示を見るには <a href="%s">ここ</a>をクリック。
blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> によるリビジョンの無視は失敗しました。
user_search_tooltip=最大30人までのユーザーを表示
transfer.accept=移転を承認
@ -1226,6 +1229,7 @@ create_new_repo_command=コマンドラインから新しいリポジトリを
push_exist_repo=コマンドラインから既存のリポジトリをプッシュ
empty_message=このリポジトリの中には何もありません。
broken_message=このリポジトリの基礎となる Git のデータを読み取れません。このインスタンスの管理者に相談するか、このリポジトリを削除してください。
no_branch=このリポジトリにはブランチがありません。
code=コード
code.desc=ソースコード、ファイル、コミット、ブランチにアクセス。
@ -1523,6 +1527,8 @@ issues.filter_assignee=担当者
issues.filter_assginee_no_select=すべての担当者
issues.filter_assginee_no_assignee=担当者なし
issues.filter_poster=作成者
issues.filter_user_placeholder=ユーザーを検索
issues.filter_user_no_select=すべてのユーザー
issues.filter_type=タイプ
issues.filter_type.all_issues=すべてのイシュー
issues.filter_type.assigned_to_you=自分が担当
@ -1674,16 +1680,16 @@ issues.timetracker_timer_manually_add=時間を追加
issues.time_estimate_set=見積時間を設定
issues.time_estimate_display=見積時間: %s
issues.change_time_estimate_at=が見積時間を <b>%s</b> に変更 %s
issues.change_time_estimate_at=が見積時間を <b>%[1]s</b> に変更 %[2]s
issues.remove_time_estimate_at=が見積時間を削除 %s
issues.time_estimate_invalid=見積時間のフォーマットが不正です
issues.start_tracking_history=が作業を開始 %s
issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します
issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!`
issues.stop_tracking_history=が <b>%s</b> の作業を終了 %s
issues.stop_tracking_history=が <b>%[1]s</b> の作業を終了 %[2]s
issues.cancel_tracking_history=`がタイムトラッキングを中止 %s`
issues.del_time=このタイムログを削除
issues.add_time_history=が作業時間 <b>%s</b> を追加 %s
issues.add_time_history=が作業時間 <b>%[1]s</b> を追加 %[2]s
issues.del_time_history=`が作業時間を削除 %s`
issues.add_time_manually=時間の手入力
issues.add_time_hours=時間
@ -1938,6 +1944,8 @@ pulls.delete.title=このプルリクエストを削除しますか?
pulls.delete.text=本当にこのプルリクエストを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください)
pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1]s</strong> にプッシュしました
pulls.upstream_diverging_prompt_behind_1=このブランチは %[2]s よりも %[1]d コミット遅れています
pulls.upstream_diverging_prompt_behind_n=このブランチは %[2]s よりも %[1]d コミット遅れています
pulls.upstream_diverging_prompt_base_newer=ベースブランチ %s に新しい変更があります
pulls.upstream_diverging_merge=フォークを同期
@ -2621,6 +2629,7 @@ release.new_release=新しいリリース
release.draft=下書き
release.prerelease=プレリリース
release.stable=安定版
release.latest=最新
release.compare=比較
release.edit=編集
release.ahead.commits=<strong>%d</strong>件のコミット
@ -2849,6 +2858,7 @@ teams.invite.title=あなたは組織 <strong>%[2]s</strong> 内のチーム <st
teams.invite.by=%s からの招待
teams.invite.description=下のボタンをクリックしてチームに参加してください。
view_as_role=表示: %s
view_as_public_hint=READMEを公開ユーザーとして見ています。
view_as_member_hint=READMEをこの組織のメンバーとして見ています。
@ -3354,7 +3364,6 @@ monitor.execute_time=実行時間
monitor.last_execution_result=結果
monitor.process.cancel=処理をキャンセル
monitor.process.cancel_desc=処理をキャンセルするとデータが失われる可能性があります
monitor.process.cancel_notices=キャンセル: <strong>%s</strong>?
monitor.process.children=子プロセス
monitor.queues=キュー
@ -3550,7 +3559,7 @@ conda.install=Conda を使用してパッケージをインストールするに
container.details.type=イメージタイプ
container.details.platform=プラットフォーム
container.pull=コマンドラインでイメージを取得します:
container.digest=ダイジェスト:
container.digest=ダイジェスト
container.multi_arch=OS / アーキテクチャ
container.layers=イメージレイヤー
container.labels=ラベル

@ -3239,7 +3239,6 @@ conda.install=Lai instalētu Conda pakotni, izpildiet sekojošu komandu:
container.details.type=Attēla formāts
container.details.platform=Platforma
container.pull=Atgādājiet šo attēlu no komandrindas:
container.digest=Īssavilkums:
container.multi_arch=OS / arhitektūra
container.layers=Attēla slāņi
container.labels=Iezīmes

@ -2310,7 +2310,6 @@ monitor.start=Czas rozpoczęcia
monitor.execute_time=Czas wykonania
monitor.process.cancel=Anuluj proces
monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych
monitor.process.cancel_notices=Anuluj: <strong>%s</strong>?
monitor.queues=Kolejki
monitor.queue=Kolejka: %s

@ -3180,7 +3180,6 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando:
container.details.type=Tipo de Imagem
container.details.platform=Plataforma
container.pull=Puxe a imagem pela linha de comando:
container.digest=Digest:
container.multi_arch=S.O. / Arquitetura
container.layers=Camadas da Imagem
container.labels=Rótulos

@ -1684,16 +1684,16 @@ issues.timetracker_timer_manually_add=Adicionar tempo
issues.time_estimate_set=Definir tempo estimado
issues.time_estimate_display=Estimativa: %s
issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%s</b> %s
issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%[1]s</b> %[2]s
issues.remove_time_estimate_at=removeu a estimativa de tempo %s
issues.time_estimate_invalid=O formato da estimativa de tempo é inválido
issues.start_tracking_history=começou a trabalhar %s
issues.tracker_auto_close=O cronómetro será parado automaticamente quando esta questão for fechada
issues.tracking_already_started=`Você já iniciou a contagem de tempo <a href="%s">noutra questão</a>!`
issues.stop_tracking_history=trabalhou durante <b>%s</b> %s
issues.stop_tracking_history=trabalhou durante <b>%[1]s</b> %[2]s
issues.cancel_tracking_history=`cancelou a contagem de tempo %s`
issues.del_time=Eliminar este registo de tempo
issues.add_time_history=adicionou <b>%s</b> de tempo gasto %s
issues.add_time_history=adicionou <b>%[1]s</b> de tempo gasto %[2]s
issues.del_time_history=`eliminou o tempo gasto nesta questão %s`
issues.add_time_manually=Adicionar tempo manualmente
issues.add_time_hours=Horas
@ -2157,6 +2157,7 @@ settings.advanced_settings=Configurações avançadas
settings.wiki_desc=Habilitar wiki do repositório
settings.use_internal_wiki=Usar o wiki integrado
settings.default_wiki_branch_name=Nome do ramo predefinido do wiki
settings.default_permission_everyone_access=Permissão de acesso predefinida para todos os utilizadores registados:
settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki.
settings.use_external_wiki=Usar um wiki externo
settings.external_wiki_url=URL do wiki externo
@ -2711,6 +2712,8 @@ branch.create_branch_operation=Criar ramo
branch.new_branch=Criar um novo ramo
branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"`
branch.renamed=O ramo %s foi renomeado para %s.
branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos.
branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob.
tag.create_tag=Criar etiqueta %s
tag.create_tag_operation=Criar etiqueta
@ -3371,7 +3374,6 @@ monitor.execute_time=Tempo de execução
monitor.last_execution_result=Resultado
monitor.process.cancel=Cancelar processo
monitor.process.cancel_desc=Cancelar um processo pode resultar na perda de dados
monitor.process.cancel_notices=Cancelar: <strong>%s</strong>?
monitor.process.children=Descendentes
monitor.queues=Filas
@ -3568,7 +3570,8 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando:
container.details.type=Tipo de imagem
container.details.platform=Plataforma
container.pull=Puxar a imagem usando a linha de comandos:
container.digest=Resumo:
container.images=Imagens
container.digest=Resumo
container.multi_arch=S.O. / Arquit.
container.layers=Camadas de imagem
container.labels=Rótulos

@ -3176,7 +3176,6 @@ conda.install=Чтобы установить пакет с помощью Conda
container.details.type=Тип образа
container.details.platform=Платформа
container.pull=Загрузите образ из командной строки:
container.digest=Отпечаток:
container.multi_arch=ОС / архитектура
container.layers=Слои образа
container.labels=Метки

@ -3430,7 +3430,6 @@ conda.install=Conda ile paket kurmak için aşağıdaki komutu çalıştırın:
container.details.type=Görüntü Türü
container.details.platform=Platform
container.pull=Görüntüyü komut satırını kullanarak çekin:
container.digest=Özet:
container.multi_arch=İşletim Sistemi / Mimari
container.layers=Görüntü Katmanları
container.labels=Etiketler

@ -1678,16 +1678,13 @@ issues.timetracker_timer_manually_add=添加时间
issues.time_estimate_set=设置预计时间
issues.time_estimate_display=预计: %s
issues.change_time_estimate_at=将预计时间修改为 <b>%s</b> %s
issues.remove_time_estimate_at=删除预计时间 %s
issues.time_estimate_invalid=预计时间格式无效
issues.start_tracking_history=`开始工作 %s`
issues.tracker_auto_close=当此工单关闭时,自动停止计时器
issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!`
issues.stop_tracking_history=`停止工作 %s`
issues.cancel_tracking_history=`取消时间跟踪 %s`
issues.del_time=删除此时间跟踪日志
issues.add_time_history=`添加计时 %s`
issues.del_time_history=`已删除时间 %s`
issues.add_time_manually=手动添加时间
issues.add_time_hours=小时
@ -3359,7 +3356,6 @@ monitor.execute_time=执行时长
monitor.last_execution_result=结果
monitor.process.cancel=中止进程
monitor.process.cancel_desc=中止一个进程可能导致数据丢失
monitor.process.cancel_notices=中止:<strong>%s</strong>
monitor.process.children=子进程
monitor.queues=队列
@ -3555,7 +3551,6 @@ conda.install=要使用 Conda 安装软件包,请运行以下命令:
container.details.type=镜像类型
container.details.platform=平台
container.pull=从命令行拉取镜像:
container.digest=摘要:
container.multi_arch=OS / Arch
container.layers=镜像层
container.labels=标签

@ -1672,16 +1672,13 @@ issues.timetracker_timer_manually_add=手動新增時間
issues.time_estimate_set=設定預估時間
issues.time_estimate_display=預估時間:%s
issues.change_time_estimate_at=將預估時間更改為 <b>%s</b> %s
issues.remove_time_estimate_at=移除預估時間 %s
issues.time_estimate_invalid=預估時間格式無效
issues.start_tracking_history=`開始工作 %s`
issues.tracker_auto_close=當這個問題被關閉時,自動停止計時器
issues.tracking_already_started=`您已在<a href="%s">另一個問題</a>上開始時間追蹤!`
issues.stop_tracking_history=`結束工作 %s`
issues.cancel_tracking_history=`取消時間追蹤 %s`
issues.del_time=刪除此時間記錄
issues.add_time_history=`加入了花費時間 %s`
issues.del_time_history=`刪除了花費時間 %s`
issues.add_time_manually=手動新增時間
issues.add_time_hours=小時
@ -3350,7 +3347,6 @@ monitor.execute_time=已執行時間
monitor.last_execution_result=結果
monitor.process.cancel=結束處理程序
monitor.process.cancel_desc=結束處理程序可能造成資料遺失
monitor.process.cancel_notices=結束: <strong>%s</strong>?
monitor.process.children=子程序
monitor.queues=佇列
@ -3546,7 +3542,6 @@ conda.install=執行下列命令以使用 Conda 安裝此套件:
container.details.type=映像檔類型
container.details.platform=平台
container.pull=透過下列命令拉取映像檔:
container.digest=摘要:
container.multi_arch=作業系統 / 架構
container.layers=映像檔 Layers
container.labels=標籤

908
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -5,18 +5,18 @@
},
"dependencies": {
"@citation-js/core": "0.7.14",
"@citation-js/plugin-bibtex": "0.7.16",
"@citation-js/plugin-bibtex": "0.7.17",
"@citation-js/plugin-csl": "0.7.14",
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.4",
"@github/relative-time-element": "4.4.5",
"@github/text-expander-element": "2.8.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
"add-asset-webpack-plugin": "3.0.0",
"ansi_up": "6.0.2",
"asciinema-player": "3.8.1",
"asciinema-player": "3.8.2",
"chart.js": "4.4.7",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0",
@ -28,11 +28,11 @@
"easymde": "2.18.0",
"esbuild-loader": "4.2.2",
"escape-goat": "4.0.0",
"fast-glob": "3.3.2",
"fast-glob": "3.3.3",
"htmx.org": "2.0.4",
"idiomorph": "0.3.0",
"idiomorph": "0.4.0",
"jquery": "3.7.1",
"katex": "0.16.18",
"katex": "0.16.20",
"license-checker-webpack-plugin": "0.2.1",
"mermaid": "11.4.1",
"mini-css-extract-plugin": "2.9.2",
@ -41,7 +41,7 @@
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0",
"perfect-debounce": "1.0.0",
"postcss": "8.4.49",
"postcss": "8.5.1",
"postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1",
"sortablejs": "1.15.6",
@ -52,7 +52,7 @@
"tippy.js": "6.3.7",
"toastify-js": "1.12.0",
"tributejs": "5.1.3",
"typescript": "5.7.2",
"typescript": "5.7.3",
"uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2",
"vue": "3.5.13",
@ -60,14 +60,14 @@
"vue-chartjs": "5.3.2",
"vue-loader": "17.4.2",
"webpack": "5.97.1",
"webpack-cli": "5.1.4",
"webpack-cli": "6.0.1",
"wrap-ansi": "9.0.0"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.49.1",
"@stoplight/spectral-cli": "6.14.2",
"@stylistic/eslint-plugin-js": "2.12.1",
"@stylistic/eslint-plugin-js": "2.13.0",
"@stylistic/stylelint-plugin": "3.1.1",
"@types/dropzone": "5.7.9",
"@types/jquery": "3.5.32",
@ -79,8 +79,8 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.3",
"@typescript-eslint/eslint-plugin": "8.18.1",
"@typescript-eslint/parser": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.20.0",
"@vitejs/plugin-vue": "5.2.1",
"eslint": "8.57.0",
"eslint-import-resolver-typescript": "3.7.0",
@ -98,16 +98,16 @@
"eslint-plugin-vue": "9.32.0",
"eslint-plugin-vue-scoped-css": "2.9.0",
"eslint-plugin-wc": "2.2.0",
"happy-dom": "15.11.7",
"happy-dom": "16.6.0",
"markdownlint-cli": "0.43.0",
"nolyfill": "1.0.43",
"postcss-html": "1.7.0",
"stylelint": "16.12.0",
"postcss-html": "1.8.0",
"stylelint": "16.13.2",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.6",
"stylelint-declaration-strict-value": "1.10.7",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.3.2",
"type-fest": "4.30.2",
"type-fest": "4.32.0",
"updates": "16.4.1",
"vite-string-plugin": "1.3.4",
"vitest": "2.1.8",

73
poetry.lock generated

@ -1,14 +1,14 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
[[package]]
name = "click"
version = "8.1.7"
version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
]
[package.dependencies]
@ -42,33 +42,33 @@ six = ">=1.13.0"
[[package]]
name = "djlint"
version = "1.36.3"
version = "1.36.4"
description = "HTML Template Linter and Formatter"
optional = false
python-versions = ">=3.9"
files = [
{file = "djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2"},
{file = "djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b"},
{file = "djlint-1.36.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e8bb0406e60cc696806aa6226df137618f3889c72f2dbdfa76c908c99151579"},
{file = "djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414"},
{file = "djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730"},
{file = "djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e"},
{file = "djlint-1.36.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c2e0dd1f26eb472b8c84eb70d6482877b6497a1fd031d7534864088f016d5ea"},
{file = "djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d"},
{file = "djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084"},
{file = "djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba"},
{file = "djlint-1.36.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ac4b7370d80bd82281e57a470de8923ac494ffb571b89d8787cef57c738c69a"},
{file = "djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a"},
{file = "djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433"},
{file = "djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302"},
{file = "djlint-1.36.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f65a80e0b5cb13d357ea51ca6570b34c2d9d18974c1e57142de760ea27d49ed0"},
{file = "djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e"},
{file = "djlint-1.36.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e2317a32094d525bc41cd11c8dc064bf38d1b442c99cc3f7c4a2616b5e6ce6e"},
{file = "djlint-1.36.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e82266c28793cd15f97b93535d72bfbc77306eaaf6b210dd90910383a814ee6c"},
{file = "djlint-1.36.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01b2101c2d1b079e8d545e6d9d03487fcca14d2371e44cbfdedee15b0bf4567c"},
{file = "djlint-1.36.3-cp39-cp39-win_amd64.whl", hash = "sha256:15cde63ef28beb5194ff4137883025f125676ece1b574b64a3e1c6daed734639"},
{file = "djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c"},
{file = "djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78"},
{file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"},
{file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"},
{file = "djlint-1.36.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1"},
{file = "djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c"},
{file = "djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7"},
{file = "djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7"},
{file = "djlint-1.36.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483"},
{file = "djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08"},
{file = "djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b"},
{file = "djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e"},
{file = "djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675"},
{file = "djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08"},
{file = "djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2"},
{file = "djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835"},
{file = "djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f"},
{file = "djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4"},
{file = "djlint-1.36.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89678661888c03d7bc6cadd75af69db29962b5ecbf93a81518262f5c48329f04"},
{file = "djlint-1.36.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b01a98df3e1ab89a552793590875bc6e954cad661a9304057db75363d519fa0"},
{file = "djlint-1.36.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbb4f7b93223d471d09ae34ed515fef98b2233cbca2449ad117416c44b1351"},
{file = "djlint-1.36.4-cp39-cp39-win_amd64.whl", hash = "sha256:7a483390d17e44df5bc23dcea29bdf6b63f3ed8b4731d844773a4829af4f5e0b"},
{file = "djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd"},
{file = "djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1"},
]
[package.dependencies]
@ -82,15 +82,17 @@ pyyaml = ">=6"
regex = ">=2023"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
tqdm = ">=4.62.2"
typing-extensions = {version = ">=3.6.6", markers = "python_version < \"3.11\""}
[[package]]
name = "editorconfig"
version = "0.12.4"
version = "0.17.0"
description = "EditorConfig File Locator and Interpreter for Python"
optional = false
python-versions = "*"
files = [
{file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
{file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"},
{file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"},
]
[[package]]
@ -370,6 +372,17 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "yamllint"
version = "1.35.1"
@ -391,4 +404,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "01b1e2f910276dd20a70ebb665c83415c37531709d90874f5b7a86a5305e2369"
content-hash = "f2e8260efe6e25f77ef387daff9551e41d25027e4794b42bc7a851ed0dfafd85"

@ -5,7 +5,7 @@ package-mode = false
python = "^3.10"
[tool.poetry.group.dev.dependencies]
djlint = "1.36.3"
djlint = "1.36.4"
yamllint = "1.35.1"
[tool.djlint]

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
@ -443,7 +444,14 @@ func UpdateBranch(ctx *context.APIContext) {
msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name)
if err != nil {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
ctx.Error(http.StatusForbidden, "", "User must be a repo or site admin to rename default or protected branches.")
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Error(http.StatusForbidden, "", "Branch is protected by glob-based protection rules.")
default:
ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
}
return
}
if msg == "target_exist" {

@ -23,7 +23,7 @@ func TestTestHook(t *testing.T) {
contexttest.LoadRepoCommit(t, ctx)
contexttest.LoadUser(t, ctx, 2)
TestHook(ctx)
assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status())
assert.EqualValues(t, http.StatusNoContent, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
HookID: 1,

@ -58,7 +58,7 @@ func TestRepoEdit(t *testing.T) {
web.SetForm(ctx, &opts)
Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
ID: 1,
}, unittest.Cond("name = ? AND is_archived = 1", *opts.Name))
@ -78,7 +78,7 @@ func TestRepoEditNameChange(t *testing.T) {
web.SetForm(ctx, &opts)
Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
ID: 1,

@ -9,6 +9,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
@ -43,14 +44,26 @@ func ProtocolMiddlewares() (handlers []any) {
func RequestContextHandler() func(h http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI)
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
// this response writer might not be the same as the one in context.Base.Resp
// because there might be a "gzip writer" in the middle, so the "written size" here is the compressed size
respWriter := context.WrapResponseWriter(respOrig)
profDesc := fmt.Sprintf("HTTP: %s %s", req.Method, req.RequestURI)
ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc)
defer finished()
ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanHTTP)
req = req.WithContext(ctx)
defer func() {
chiCtx := chi.RouteContext(req.Context())
span.SetAttributeString(gtprof.TraceAttrHTTPRoute, chiCtx.RoutePattern())
span.End()
}()
defer func() {
if err := recover(); err != nil {
RenderPanicErrorPage(resp, req, err) // it should never panic
RenderPanicErrorPage(respWriter, req, err) // it should never panic
}
}()
@ -62,7 +75,7 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
}
})
next.ServeHTTP(context.WrapResponseWriter(resp), req)
next.ServeHTTP(respWriter, req)
})
}
}
@ -71,11 +84,11 @@ func ChiRoutePathHandler() func(h http.Handler) http.Handler {
// make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx := chi.RouteContext(req.Context())
chiCtx := chi.RouteContext(req.Context())
if req.URL.RawPath == "" {
ctx.RoutePath = req.URL.EscapedPath()
chiCtx.RoutePath = req.URL.EscapedPath()
} else {
ctx.RoutePath = req.URL.RawPath
chiCtx.RoutePath = req.URL.RawPath
}
next.ServeHTTP(resp, req)
})

@ -213,7 +213,7 @@ func NormalRoutes() *web.Router {
}
r.NotFound(func(w http.ResponseWriter, req *http.Request) {
routing.UpdateFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))
defer routing.RecordFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))()
http.NotFound(w, req)
})
return r

@ -37,6 +37,7 @@ const (
tplSelfCheck templates.TplName = "admin/self_check"
tplCron templates.TplName = "admin/cron"
tplQueue templates.TplName = "admin/queue"
tplPerfTrace templates.TplName = "admin/perftrace"
tplStacktrace templates.TplName = "admin/stacktrace"
tplQueueManage templates.TplName = "admin/queue_manage"
tplStats templates.TplName = "admin/stats"

@ -10,13 +10,15 @@ import (
"time"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/tailmsg"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
func MonitorDiagnosis(ctx *context.Context) {
seconds := ctx.FormInt64("seconds")
if seconds <= 5 {
seconds = 5
if seconds <= 1 {
seconds = 1
}
if seconds > 300 {
seconds = 300
@ -65,4 +67,16 @@ func MonitorDiagnosis(ctx *context.Context) {
return
}
_ = pprof.Lookup("heap").WriteTo(f, 0)
f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "perftrace.txt", Method: zip.Deflate, Modified: time.Now()})
if err != nil {
ctx.ServerError("Failed to create zip file", err)
return
}
for _, record := range tailmsg.GetManager().GetTraceRecorder().GetRecords() {
_, _ = f.Write(util.UnsafeStringToBytes(record.Time.Format(time.RFC3339)))
_, _ = f.Write([]byte(" "))
_, _ = f.Write(util.UnsafeStringToBytes((record.Content)))
_, _ = f.Write([]byte("\n\n"))
}
}

@ -0,0 +1,18 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
"code.gitea.io/gitea/modules/tailmsg"
"code.gitea.io/gitea/services/context"
)
func PerfTrace(ctx *context.Context) {
monitorTraceCommon(ctx)
ctx.Data["PageIsAdminMonitorPerfTrace"] = true
ctx.Data["PerfTraceRecords"] = tailmsg.GetManager().GetTraceRecorder().GetRecords()
ctx.HTML(http.StatusOK, tplPerfTrace)
}

@ -12,10 +12,17 @@ import (
"code.gitea.io/gitea/services/context"
)
func monitorTraceCommon(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor")
ctx.Data["PageIsAdminMonitorTrace"] = true
// Hide the performance trace tab in production, because it shows a lot of SQLs and is not that useful for end users.
// To avoid confusing end users, do not let them know this tab. End users should "download diagnosis report" instead.
ctx.Data["ShowAdminPerformanceTraceTab"] = !setting.IsProd
}
// Stacktrace show admin monitor goroutines page
func Stacktrace(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor")
ctx.Data["PageIsAdminMonitorStacktrace"] = true
monitorTraceCommon(ctx)
ctx.Data["GoroutineCount"] = runtime.NumGoroutine()

@ -29,6 +29,7 @@ var tplLinkAccount templates.TplName = "user/auth/link_account"
// LinkAccount shows the page where the user can decide to login or create a new account
func LinkAccount(ctx *context.Context) {
// FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again.
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
@ -43,6 +44,7 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template
@ -50,6 +52,11 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
// If you'd like to quickly debug the "link account" page layout, just uncomment the blow line
// Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign)
// gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check
if !ok {
// no account in session, so just redirect to the login page, then the user could restart the process
ctx.Redirect(setting.AppSubURL + "/user/login")
@ -135,6 +142,8 @@ func LinkAccountPostSignIn(ctx *context.Context) {
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template
@ -223,6 +232,8 @@ func LinkAccountPostRegister(ctx *context.Context) {
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template

@ -34,7 +34,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
routing.UpdateFuncInfo(req.Context(), funcInfo)
defer routing.RecordFuncInfo(req.Context(), funcInfo)()
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
rPath = util.PathJoinRelX(rPath)
@ -65,7 +65,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
routing.UpdateFuncInfo(req.Context(), funcInfo)
defer routing.RecordFuncInfo(req.Context(), funcInfo)()
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
rPath = util.PathJoinRelX(rPath)

@ -850,7 +850,7 @@ func Run(ctx *context_module.Context) {
inputs := make(map[string]any)
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
for name, config := range workflowDispatch.Inputs {
value := ctx.Req.PostForm.Get(name)
value := ctx.Req.PostFormValue(name)
if config.Type == "boolean" {
// https://www.w3.org/TR/html401/interact/forms.html
// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked

@ -37,7 +37,6 @@ const (
// Branches render repository branch page
func Branches(ctx *context.Context) {
ctx.Data["Title"] = "Branches"
ctx.Data["IsRepoToolbarBranches"] = true
ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx)
ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode)
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror

@ -29,7 +29,7 @@ func CodeFrequency(ctx *context.Context) {
// CodeFrequencyData returns JSON of code frequency data
func CodeFrequencyData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return

@ -62,11 +62,7 @@ func Commits(ctx *context.Context) {
}
ctx.Data["PageIsViewCode"] = true
commitsCount, err := ctx.Repo.GetCommitsCount()
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
}
commitsCount := ctx.Repo.CommitsCount
page := ctx.FormInt("page")
if page <= 1 {
@ -129,12 +125,6 @@ func Graph(ctx *context.Context) {
ctx.Data["SelectedBranches"] = realBranches
files := ctx.FormStrings("file")
commitsCount, err := ctx.Repo.GetCommitsCount()
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
}
graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files)
if err != nil {
log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err)
@ -171,7 +161,6 @@ func Graph(ctx *context.Context) {
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
paginator.AddParamFromRequest(ctx.Req)

@ -26,7 +26,7 @@ func Contributors(ctx *context.Context) {
// ContributorsData renders JSON of contributors along with their weekly commit statistics
func ContributorsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return

@ -109,7 +109,7 @@ func RemoveDependency(ctx *context.Context) {
}
// Dependency Type
depTypeStr := ctx.Req.PostForm.Get("dependencyType")
depTypeStr := ctx.Req.PostFormValue("dependencyType")
var depType issues_model.DependencyType

@ -38,7 +38,7 @@ func TestInitializeLabels(t *testing.T) {
contexttest.LoadRepo(t, ctx, 2)
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
RepoID: 2,
Name: "enhancement",
@ -84,7 +84,7 @@ func TestNewLabel(t *testing.T) {
Color: "#abcdef",
})
NewLabel(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
Name: "newlabel",
Color: "#abcdef",
@ -104,7 +104,7 @@ func TestUpdateLabel(t *testing.T) {
IsArchived: true,
})
UpdateLabel(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
ID: 2,
Name: "newnameforlabel",
@ -120,7 +120,7 @@ func TestDeleteLabel(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("id", "2")
DeleteLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
@ -134,7 +134,7 @@ func TestUpdateIssueLabel_Clear(t *testing.T) {
ctx.Req.Form.Set("issue_ids", "1,3")
ctx.Req.Form.Set("action", "clear")
UpdateIssueLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
unittest.CheckConsistencyFor(t, &issues_model.Label{})
@ -160,7 +160,7 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
ctx.Req.Form.Set("action", testCase.Action)
ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
UpdateIssueLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
for _, issueID := range testCase.IssueIDs {
if testCase.ExpectedAdd {
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID})

@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) {
return
}
c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time)))
c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToHours(t.Time)))
c.JSONRedirect("")
}

@ -46,7 +46,7 @@ func IssueWatch(ctx *context.Context) {
return
}
watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch"))
watch, err := strconv.ParseBool(ctx.Req.PostFormValue("watch"))
if err != nil {
ctx.ServerError("watch is not bool", err)
return

@ -29,7 +29,7 @@ func RecentCommits(ctx *context.Context) {
// RecentCommitsData returns JSON of recent commits data
func RecentCommitsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return

@ -67,10 +67,11 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
}
} else {
searchRefName := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) // BranchName should be default branch or the first existing branch
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{
ContextLineNumber: 1,
IsFuzzy: prepareSearch.IsFuzzy,
RefName: git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch).String(), // BranchName should be default branch or the first existing branch
RefName: searchRefName.String(),
PathspecList: indexSettingToGitGrepPathspecList(),
})
if err != nil {
@ -78,6 +79,11 @@ func Search(ctx *context.Context) {
ctx.ServerError("GrepSearch", err)
return
}
commitID, err := ctx.Repo.GitRepo.GetRefCommitID(searchRefName.String())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
}
total = len(res)
pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res))
pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res))
@ -86,7 +92,7 @@ func Search(ctx *context.Context) {
searchResults = append(searchResults, &code_indexer.Result{
RepoID: ctx.Repo.Repository.ID,
Filename: r.Filename,
CommitID: ctx.Repo.CommitID,
CommitID: commitID,
// UpdatedUnix: not supported yet
// Language: not supported yet
// Color: not supported yet

@ -4,6 +4,7 @@
package setting
import (
"errors"
"fmt"
"net/http"
"net/url"
@ -14,6 +15,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
@ -351,9 +353,15 @@ func RenameBranchPost(ctx *context.Context) {
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
if err != nil {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
ctx.Flash.Error(ctx.Tr("repo.branch.rename_default_or_protected_branch_error"))
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
case git_model.IsErrBranchAlreadyExists(err):
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Flash.Error(ctx.Tr("repo.branch.rename_protected_branch_failed"))
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
default:
ctx.ServerError("RenameBranch", err)
}

@ -54,7 +54,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@ -84,7 +84,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@ -121,7 +121,7 @@ func TestCollaborationPost(t *testing.T) {
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
@ -147,7 +147,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) {
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@ -179,7 +179,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
@ -188,7 +188,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
// Try adding the same collaborator again
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@ -210,7 +210,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@ -250,7 +250,7 @@ func TestAddTeamPost(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Empty(t, ctx.Flash.ErrorMsg)
}
@ -290,7 +290,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) {
AddTeamPost(ctx)
assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@ -331,7 +331,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@ -364,7 +364,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) {
ctx.Repo = repo
AddTeamPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}

@ -654,6 +654,8 @@ func TestWebhook(ctx *context.Context) {
}
// Grab latest commit or fake one if it's empty repository.
// Note: in old code, the "ctx.Repo.Commit" is the last commit of the default branch.
// New code doesn't set that commit, so it always uses the fake commit to test webhook.
commit := ctx.Repo.Commit
if commit == nil {
ghost := user_model.NewGhostUser()

@ -215,10 +215,28 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
baseRepoPerm.CanRead(unit_model.TypePullRequests) {
ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
var finalBranches []*git_model.RecentlyPushedNewBranch
branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
if err != nil {
log.Error("FindRecentlyPushedNewBranches failed: %v", err)
}
for _, branch := range branches {
divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx,
branch.BranchRepo, branch.BranchName, // "base" repo for diverging info
opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info
)
if err != nil {
log.Error("GetBranchDivergingInfo failed: %v", err)
continue
}
branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits
baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind
if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 {
finalBranches = append(finalBranches, branch)
}
}
ctx.Data["RecentlyPushedNewBranches"] = finalBranches
}
}
}

@ -82,7 +82,7 @@ func TestWiki(t *testing.T) {
ctx.SetPathParam("*", "Home")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
@ -90,7 +90,7 @@ func TestWiki(t *testing.T) {
ctx.SetPathParam("*", "jpeg.jpg")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Equal(t, "/user2/repo1/wiki/raw/jpeg.jpg", ctx.Resp.Header().Get("Location"))
}
@ -100,7 +100,7 @@ func TestWikiPages(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
contexttest.LoadRepo(t, ctx, 1)
WikiPages(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
}
@ -111,7 +111,7 @@ func TestNewWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
NewWiki(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
}
@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
}
@ -149,7 +149,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
}
@ -162,7 +162,7 @@ func TestEditWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
@ -171,7 +171,7 @@ func TestEditWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx)
assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status())
assert.EqualValues(t, http.StatusForbidden, ctx.Resp.WrittenStatus())
}
func TestEditWikiPost(t *testing.T) {
@ -190,7 +190,7 @@ func TestEditWikiPost(t *testing.T) {
Message: message,
})
EditWikiPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
if title != "Home" {
@ -206,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
DeleteWikiPagePost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertWikiNotExists(t, ctx.Repo.Repository, "Home")
}
@ -228,9 +228,9 @@ func TestWikiRaw(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1)
WikiRaw(ctx)
if filetype == "" {
assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath)
assert.EqualValues(t, http.StatusNotFound, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
} else {
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath)
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
}
}

@ -576,17 +576,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
func(o *issue_indexer.SearchOptions) {
o.IsFuzzyKeyword = isFuzzy
// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
// because the doer may create issues or be mentioned in any public repo.
// So we need search issues in all public repos.
o.AllPublic = ctx.Doer.ID == ctxUser.ID
o.MentionID = nil
o.ReviewRequestedID = nil
o.ReviewedID = nil
},
))
if err != nil {
@ -775,10 +767,19 @@ func UsernameSubRoute(ctx *context.Context) {
}
}
func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
ret = &issues_model.IssueStats{}
doerID := ctx.Doer.ID
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
// because the doer may create issues or be mentioned in any public repo.
// So we need search issues in all public repos.
o.AllPublic = doerID == ctxUser.ID
})
// Open/Closed are for the tabs of the issue list
{
openClosedOpts := opts.Copy()
switch filterMode {
@ -809,6 +810,15 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer
}
}
// Below stats are for the left sidebar
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
o.AssigneeID = nil
o.PosterID = nil
o.MentionID = nil
o.ReviewRequestedID = nil
o.ReviewedID = nil
})
ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false }))
if err != nil {
return nil, err

@ -45,7 +45,7 @@ func TestArchivedIssues(t *testing.T) {
Issues(ctx)
// Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.Len(t, ctx.Data["Issues"], 1)
}
@ -58,7 +58,7 @@ func TestIssues(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "closed")
Issues(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.Len(t, ctx.Data["Issues"], 1)
@ -72,7 +72,7 @@ func TestPulls(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "open")
Pulls(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.Len(t, ctx.Data["Issues"], 5)
}
@ -87,7 +87,7 @@ func TestMilestones(t *testing.T) {
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
@ -107,7 +107,7 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])

@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) {
AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
})
}
}

@ -720,6 +720,7 @@ func registerRoutes(m *web.Router) {
m.Group("/monitor", func() {
m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks)
m.Get("/perftrace", admin.PerfTrace)
m.Get("/stacktrace", admin.Stacktrace)
m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel)
m.Get("/queue", admin.Queues)
@ -1156,7 +1157,7 @@ func registerRoutes(m *web.Router) {
m.Post("/cancel", repo.MigrateCancelPost)
})
},
reqSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoRef(),
reqSignIn, context.RepoAssignment, reqRepoAdmin,
ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer),
)
// end "/{username}/{reponame}/settings"
@ -1342,7 +1343,7 @@ func registerRoutes(m *web.Router) {
m.Group("/{username}/{reponame}", func() { // repo tags
m.Group("/tags", func() {
m.Get("", repo.TagsList)
m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList)
m.Get(".rss", feedEnabled, repo.TagsListFeedRSS)
m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
m.Get("/list", repo.GetTagList)
@ -1523,7 +1524,7 @@ func registerRoutes(m *web.Router) {
m.Group("/activity_author_data", func() {
m.Get("", repo.ActivityAuthors)
m.Get("/{period}", repo.ActivityAuthors)
}, context.RepoRef(), repo.MustBeNotEmpty)
}, repo.MustBeNotEmpty)
m.Group("/archive", func() {
m.Get("/*", repo.Download)
@ -1532,8 +1533,8 @@ func registerRoutes(m *web.Router) {
m.Group("/branches", func() {
m.Get("/list", repo.GetBranchesList)
m.Get("", repo.Branches)
}, repo.MustBeNotEmpty, context.RepoRef())
m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.Branches)
}, repo.MustBeNotEmpty)
m.Group("/media", func() {
m.Get("/blob/{sha}", repo.DownloadByIDOrLFS)
@ -1577,8 +1578,10 @@ func registerRoutes(m *web.Router) {
m.Get("/graph", repo.Graph)
m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
}, repo.MustBeNotEmpty, context.RepoRef())
// FIXME: this route `/cherry-pick/{sha}` doesn't seem useful or right, the new code always uses `/_cherrypick/` which could handle branch name correctly
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick)
}, repo.MustBeNotEmpty)
m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed)
m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed)
@ -1632,7 +1635,7 @@ func registerRoutes(m *web.Router) {
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
ctx := context.GetWebContext(req)
routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))
defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()
ctx.NotFound("", nil)
})
}

@ -18,11 +18,12 @@ import (
"code.gitea.io/gitea/modules/web/middleware"
)
type routerLoggerOptions struct {
req *http.Request
type accessLoggerTmplData struct {
Identity *string
Start *time.Time
ResponseWriter http.ResponseWriter
ResponseWriter struct {
Status, Size int
}
Ctx map[string]any
RequestID *string
}
@ -51,17 +52,15 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
return requestID
}
// AccessLogger returns a middleware to log access logger
func AccessLogger() func(http.Handler) http.Handler {
logger := log.GetLogger("access")
needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate)
logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
type accessLogRecorder struct {
logger log.BaseLogger
logTemplate *template.Template
needRequestID bool
}
func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) {
var requestID string
if needRequestID {
if lr.needRequestID {
requestID = parseRequestIDFromRequestHeader(req)
}
@ -70,32 +69,48 @@ func AccessLogger() func(http.Handler) http.Handler {
reqHost = req.RemoteAddr
}
next.ServeHTTP(w, req)
rw := w.(ResponseWriter)
identity := "-"
data := middleware.GetContextData(req.Context())
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
identity = signedUser.Name
}
buf := bytes.NewBuffer([]byte{})
err = logTemplate.Execute(buf, routerLoggerOptions{
req: req,
tmplData := accessLoggerTmplData{
Identity: &identity,
Start: &start,
ResponseWriter: rw,
Ctx: map[string]any{
"RemoteAddr": req.RemoteAddr,
"RemoteHost": reqHost,
"Req": req,
},
RequestID: &requestID,
})
}
tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
tmplData.ResponseWriter.Size = respWriter.WrittenSize()
err = lr.logTemplate.Execute(buf, tmplData)
if err != nil {
log.Error("Could not execute access logger template: %v", err.Error())
}
logger.Info("%s", buf.String())
lr.logger.Log(1, log.INFO, "%s", buf.String())
}
func newAccessLogRecorder() *accessLogRecorder {
return &accessLogRecorder{
logger: log.GetLogger("access"),
logTemplate: template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)),
needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate),
}
}
// AccessLogger returns a middleware to log access logger
func AccessLogger() func(http.Handler) http.Handler {
recorder := newAccessLogRecorder()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
next.ServeHTTP(w, req)
recorder.record(start, w.(ResponseWriter), req)
})
}
}

@ -0,0 +1,71 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
type testAccessLoggerMock struct {
logs []string
}
func (t *testAccessLoggerMock) Log(skip int, level log.Level, format string, v ...any) {
t.logs = append(t.logs, fmt.Sprintf(format, v...))
}
func (t *testAccessLoggerMock) GetLevel() log.Level {
return log.INFO
}
type testAccessLoggerResponseWriterMock struct{}
func (t testAccessLoggerResponseWriterMock) Header() http.Header {
return nil
}
func (t testAccessLoggerResponseWriterMock) Before(f func(ResponseWriter)) {}
func (t testAccessLoggerResponseWriterMock) WriteHeader(statusCode int) {}
func (t testAccessLoggerResponseWriterMock) Write(bytes []byte) (int, error) {
return 0, nil
}
func (t testAccessLoggerResponseWriterMock) Flush() {}
func (t testAccessLoggerResponseWriterMock) WrittenStatus() int {
return http.StatusOK
}
func (t testAccessLoggerResponseWriterMock) WrittenSize() int {
return 123123
}
func TestAccessLogger(t *testing.T) {
setting.Log.AccessLogTemplate = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`
recorder := newAccessLogRecorder()
mockLogger := &testAccessLoggerMock{}
recorder.logger = mockLogger
req := &http.Request{
RemoteAddr: "remote-addr",
Method: "GET",
Proto: "https",
URL: &url.URL{Path: "/path"},
}
req.Header = http.Header{}
req.Header.Add("Referer", "referer")
req.Header.Add("User-Agent", "user-agent")
recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req)
assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs)
}

@ -4,7 +4,6 @@
package context
import (
"context"
"fmt"
"html/template"
"io"
@ -25,8 +24,7 @@ type BaseContextKeyType struct{}
var BaseContextKey BaseContextKeyType
type Base struct {
context.Context
reqctx.RequestDataStore
reqctx.RequestContext
Resp ResponseWriter
Req *http.Request
@ -172,19 +170,19 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
}
func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base {
ds := reqctx.GetRequestDataStore(req.Context())
reqCtx := reqctx.FromContext(req.Context())
b := &Base{
Context: req.Context(),
RequestDataStore: ds,
RequestContext: reqCtx,
Req: req,
Resp: WrapResponseWriter(resp),
Locale: middleware.Locale(resp, req),
Data: ds.GetData(),
Data: reqCtx.GetData(),
}
b.Req = b.Req.WithContext(b)
ds.SetContextValue(BaseContextKey, b)
ds.SetContextValue(translation.ContextKey, b.Locale)
ds.SetContextValue(httplib.RequestContextKey, b.Req)
reqCtx.SetContextValue(BaseContextKey, b)
reqCtx.SetContextValue(translation.ContextKey, b.Locale)
reqCtx.SetContextValue(httplib.RequestContextKey, b.Req)
return b
}

@ -777,6 +777,18 @@ func repoRefFullName(typ git.RefType, shortName string) git.RefName {
}
}
func RepoRefByDefaultBranch() func(*Context) {
return func(ctx *Context) {
ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
ctx.Data["RefFullName"] = ctx.Repo.RefFullName
ctx.Data["BranchName"] = ctx.Repo.BranchName
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
}
}
// RepoRefByType handles repository reference name for a specific type
// of repository reference
func RepoRefByType(detectRefType git.RefType) func(*Context) {

@ -11,31 +11,29 @@ import (
// ResponseWriter represents a response writer for HTTP
type ResponseWriter interface {
http.ResponseWriter
http.Flusher
web_types.ResponseStatusProvider
Before(func(ResponseWriter))
http.ResponseWriter // provides Header/Write/WriteHeader
http.Flusher // provides Flush
web_types.ResponseStatusProvider // provides WrittenStatus
Status() int // used by access logger template
Size() int // used by access logger template
Before(fn func(ResponseWriter))
WrittenSize() int
}
var _ ResponseWriter = &Response{}
var _ ResponseWriter = (*Response)(nil)
// Response represents a response
type Response struct {
http.ResponseWriter
written int
status int
befores []func(ResponseWriter)
beforeFuncs []func(ResponseWriter)
beforeExecuted bool
}
// Write writes bytes to HTTP endpoint
func (r *Response) Write(bs []byte) (int, error) {
if !r.beforeExecuted {
for _, before := range r.befores {
for _, before := range r.beforeFuncs {
before(r)
}
r.beforeExecuted = true
@ -51,18 +49,14 @@ func (r *Response) Write(bs []byte) (int, error) {
return size, nil
}
func (r *Response) Status() int {
return r.status
}
func (r *Response) Size() int {
func (r *Response) WrittenSize() int {
return r.written
}
// WriteHeader write status code
func (r *Response) WriteHeader(statusCode int) {
if !r.beforeExecuted {
for _, before := range r.befores {
for _, before := range r.beforeFuncs {
before(r)
}
r.beforeExecuted = true
@ -87,17 +81,13 @@ func (r *Response) WrittenStatus() int {
// Before allows for a function to be called before the ResponseWriter has been written to. This is
// useful for setting headers or any other operations that must happen before a response has been written.
func (r *Response) Before(f func(ResponseWriter)) {
r.befores = append(r.befores, f)
func (r *Response) Before(fn func(ResponseWriter)) {
r.beforeFuncs = append(r.beforeFuncs, fn)
}
func WrapResponseWriter(resp http.ResponseWriter) *Response {
if v, ok := resp.(*Response); ok {
return v
}
return &Response{
ResponseWriter: resp,
status: 0,
befores: make([]func(ResponseWriter), 0),
}
return &Response{ResponseWriter: resp}
}

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
@ -186,7 +187,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
result = append(result, api.StopWatch{
Created: sw.CreatedUnix.AsTime(),
Seconds: sw.Seconds(),
Duration: sw.Duration(),
Duration: util.SecToHours(sw.Seconds()),
IssueIndex: issue.Index,
IssueTitle: issue.Title,
RepoOwnerName: repo.OwnerName,

@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
c.Content[0] == '|' {
// TimeTracking Comments from v1.21 on store the seconds instead of an formatted string
// so we check for the "|" delimiter and convert new to legacy format on demand
c.Content = util.SecToTime(c.Content[1:])
c.Content = util.SecToHours(c.Content[1:])
}
if c.Type == issues_model.CommentTypeChangeTimeEstimate {

@ -1136,7 +1136,10 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
} else {
actualBeforeCommitID := opts.BeforeCommitID
if len(actualBeforeCommitID) == 0 {
parentCommit, _ := commit.Parent(0)
parentCommit, err := commit.Parent(0)
if err != nil {
return nil, err
}
actualBeforeCommitID = parentCommit.ID.String()
}
@ -1145,7 +1148,6 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID)
opts.BeforeCommitID = actualBeforeCommitID
var err error
beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID)
if err != nil {
return nil, err

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/queue"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
@ -416,6 +417,29 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
return "from_not_exist", nil
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, doer)
if err != nil {
return "", err
}
isDefault := from == repo.DefaultBranch
if isDefault && !perm.IsAdmin() {
return "", repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: doer.ID,
RepoName: repo.LowerName,
}
}
// If from == rule name, admins are allowed to modify them.
if protectedBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, from); err != nil {
return "", err
} else if protectedBranch != nil && !perm.IsAdmin() {
return "", repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: doer.ID,
RepoName: repo.LowerName,
}
}
if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error {
err2 := gitRepo.RenameBranch(from, to)
if err2 != nil {
@ -642,3 +666,72 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
return nil
}
// BranchDivergingInfo contains the information about the divergence of a head branch to the base branch.
type BranchDivergingInfo struct {
// whether the base branch contains new commits which are not in the head branch
BaseHasNewCommits bool
// behind/after are number of commits that the head branch is behind/after the base branch, it's 0 if it's unable to calculate.
// there could be a case that BaseHasNewCommits=true while the behind/after are both 0 (unable to calculate).
HeadCommitsBehind int
HeadCommitsAhead int
}
// GetBranchDivergingInfo returns the information about the divergence of a patch branch to the base branch.
func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repository, baseBranch string, headRepo *repo_model.Repository, headBranch string) (*BranchDivergingInfo, error) {
headGitBranch, err := git_model.GetBranch(ctx, headRepo.ID, headBranch)
if err != nil {
return nil, err
}
if headGitBranch.IsDeleted {
return nil, git_model.ErrBranchNotExist{
BranchName: headBranch,
}
}
baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch)
if err != nil {
return nil, err
}
if baseGitBranch.IsDeleted {
return nil, git_model.ErrBranchNotExist{
BranchName: baseBranch,
}
}
info := &BranchDivergingInfo{}
if headGitBranch.CommitID == baseGitBranch.CommitID {
return info, nil
}
// if the fork repo has new commits, this call will fail because they are not in the base repo
// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
// so at the moment, we first check the update time, then check whether the fork branch has base's head
diff, err := git.GetDivergingCommits(ctx, baseRepo.RepoPath(), baseGitBranch.CommitID, headGitBranch.CommitID)
if err != nil {
info.BaseHasNewCommits = baseGitBranch.UpdatedUnix > headGitBranch.UpdatedUnix
if headRepo.IsFork && info.BaseHasNewCommits {
return info, nil
}
// if the base's update time is before the fork, check whether the base's head is in the fork
headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo)
if err != nil {
return nil, err
}
headCommit, err := headGitRepo.GetCommit(headGitBranch.CommitID)
if err != nil {
return nil, err
}
baseCommitID, err := git.NewIDFromString(baseGitBranch.CommitID)
if err != nil {
return nil, err
}
hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
info.BaseHasNewCommits = !hasPreviousCommit
return info, nil
}
info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead
info.BaseHasNewCommits = info.HeadCommitsBehind > 0
return info, nil
}

@ -4,38 +4,38 @@
package repository
import (
"context"
"errors"
"fmt"
git_model "code.gitea.io/gitea/models/git"
issue_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/pull"
)
type UpstreamDivergingInfo struct {
BaseHasNewCommits bool
CommitsBehind int
CommitsAhead int
}
// MergeUpstream merges the base repository's default branch into the fork repository's current branch.
func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
if err = repo.MustNotBeArchived(); err != nil {
return "", err
}
if err = repo.GetBaseRepo(ctx); err != nil {
return "", err
}
divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch)
if err != nil {
return "", err
}
if !divergingInfo.BaseBranchHasNewCommits {
return "up-to-date", nil
}
err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
Remote: repo.RepoPath(),
Branch: fmt.Sprintf("%s:%s", repo.BaseRepo.DefaultBranch, branch),
Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch),
Env: repo_module.PushingEnvironment(doer, repo),
})
if err == nil {
@ -67,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
BaseRepoID: repo.BaseRepo.ID,
BaseRepo: repo.BaseRepo,
HeadBranch: branch, // maybe HeadCommitID is not needed
BaseBranch: repo.BaseRepo.DefaultBranch,
BaseBranch: divergingInfo.BaseBranchName,
}
fakeIssue.PullRequest = fakePR
err = pull.Update(ctx, fakePR, doer, "merge upstream", false)
@ -77,68 +77,47 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
return "merge", nil
}
// UpstreamDivergingInfo is also used in templates, so it needs to search for all references before changing it.
type UpstreamDivergingInfo struct {
BaseBranchName string
BaseBranchHasNewCommits bool
HeadBranchCommitsBehind int
}
// GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch.
func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) {
if !repo.IsFork {
func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) {
if !forkRepo.IsFork {
return nil, util.NewInvalidArgumentErrorf("repo is not a fork")
}
if repo.IsArchived {
if forkRepo.IsArchived {
return nil, util.NewInvalidArgumentErrorf("repo is archived")
}
if err := repo.GetBaseRepo(ctx); err != nil {
return nil, err
}
forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch)
if err != nil {
return nil, err
}
baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, repo.BaseRepo.DefaultBranch)
if err != nil {
if err := forkRepo.GetBaseRepo(ctx); err != nil {
return nil, err
}
info := &UpstreamDivergingInfo{}
if forkBranch.CommitID == baseBranch.CommitID {
return info, nil
}
// if the fork repo has new commits, this call will fail because they are not in the base repo
// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
// so at the moment, we first check the update time, then check whether the fork branch has base's head
diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID)
if err != nil {
info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix
if info.BaseHasNewCommits {
return info, nil
}
// if the base's update time is before the fork, check whether the base's head is in the fork
baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo.BaseRepo)
if err != nil {
return nil, err
}
headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
return nil, err
// Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo:
// * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a`
// * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a`
info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch)
if err == nil {
return &UpstreamDivergingInfo{
BaseBranchName: forkBranch,
BaseBranchHasNewCommits: info.BaseHasNewCommits,
HeadBranchCommitsBehind: info.HeadCommitsBehind,
}, nil
}
if errors.Is(err, util.ErrNotExist) {
info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
if err == nil {
return &UpstreamDivergingInfo{
BaseBranchName: forkRepo.BaseRepo.DefaultBranch,
BaseBranchHasNewCommits: info.BaseHasNewCommits,
HeadBranchCommitsBehind: info.HeadCommitsBehind,
}, nil
}
baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID)
if err != nil {
return nil, err
}
headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID)
if err != nil {
return nil, err
}
hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
info.BaseHasNewCommits = !hasPreviousCommit
return info, nil
}
info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead
return info, nil
}

@ -98,7 +98,7 @@
<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices">
{{ctx.Locale.Tr "admin.notices"}}
</a>
<details class="item toggleable-item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorStacktrace}}open{{end}}>
<details class="item toggleable-item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorTrace}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.monitor"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stats">
@ -110,8 +110,8 @@
<a class="{{if .PageIsAdminMonitorQueue}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/queue">
{{ctx.Locale.Tr "admin.monitor.queues"}}
</a>
<a class="{{if .PageIsAdminMonitorStacktrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace">
{{ctx.Locale.Tr "admin.monitor.stacktrace"}}
<a class="{{if .PageIsAdminMonitorTrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace">
{{ctx.Locale.Tr "admin.monitor.trace"}}
</a>
</div>
</details>

@ -0,0 +1,13 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
<div class="admin-setting-content">
{{template "admin/trace_tabs" .}}
{{range $record := .PerfTraceRecords}}
<div class="ui segment tw-w-full tw-overflow-auto">
<pre class="tw-whitespace-pre">{{$record.Content}}</pre>
</div>
{{end}}
</div>
{{template "admin/layout_footer" .}}

@ -17,7 +17,10 @@
</div>
<div>
{{if or (eq .Process.Type "request") (eq .Process.Type "normal")}}
<a class="delete-button icon" href="" data-url="{{.root.Link}}/cancel/{{.Process.PID}}" data-id="{{.Process.PID}}" data-name="{{.Process.Description}}">{{svg "octicon-trash" 16 "text-red"}}</a>
<a class="link-action" data-url="{{.root.Link}}/cancel/{{.Process.PID}}"
data-modal-confirm-header="{{ctx.Locale.Tr "admin.monitor.process.cancel"}}"
data-modal-confirm-content="{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}"
>{{svg "octicon-trash" 16 "text-red"}}</a>
{{end}}
</div>
</div>

@ -1,22 +1,7 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
<div class="admin-setting-content">
<div class="tw-flex tw-items-center">
<div class="tw-flex-1">
<div class="ui compact small menu">
<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
<a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
</div>
</div>
<form target="_blank" action="{{AppSubUrl}}/-/admin/monitor/diagnosis" class="ui form">
<div class="ui inline field">
<button class="ui primary small button">{{ctx.Locale.Tr "admin.monitor.download_diagnosis_report"}}</button>
<input name="seconds" size="3" maxlength="3" value="10"> {{ctx.Locale.Tr "tool.raw_seconds"}}
</div>
</form>
</div>
<div class="divider"></div>
{{template "admin/trace_tabs" .}}
<h4 class="ui top attached header">
{{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}}
@ -34,15 +19,4 @@
{{end}}
</div>
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{ctx.Locale.Tr "admin.monitor.process.cancel"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|SafeHTML)}}</p>
<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
{{template "admin/layout_footer" .}}

@ -0,0 +1,19 @@
<div class="flex-text-block">
<div class="tw-flex-1">
<div class="ui compact small menu">
{{if .ShowAdminPerformanceTraceTab}}
<a class="item {{Iif .PageIsAdminMonitorPerfTrace "active"}}" href="{{AppSubUrl}}/-/admin/monitor/perftrace">{{ctx.Locale.Tr "admin.monitor.performance_logs"}}</a>
{{end}}
<a class="item {{Iif (eq .ShowGoroutineList "process") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
<a class="item {{Iif (eq .ShowGoroutineList "stacktrace") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
</div>
</div>
<form target="_blank" action="{{AppSubUrl}}/-/admin/monitor/diagnosis" class="ui form">
<div class="ui inline field">
<button class="ui primary small button">{{ctx.Locale.Tr "admin.monitor.download_diagnosis_report"}}</button>
<input name="seconds" size="3" maxlength="3" value="10"> {{ctx.Locale.Tr "tool.raw_seconds"}}
</div>
</form>
</div>
<div class="divider"></div>

@ -24,7 +24,7 @@
</div>
</div>
{{if .PackageDescriptor.Metadata.Manifests}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.multi_arch"}}</h4>
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.images"}}</h4>
<div class="ui attached segment">
<table class="ui very basic compact table">
<thead>

@ -143,7 +143,7 @@
{{if .LatestPullRequest.HasMerged}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
{{else if .LatestPullRequest.Issue.IsClosed}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request-closed" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
{{else}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
{{end}}

@ -1,5 +1,6 @@
<button class="ui primary button js-btn-clone-panel">
<span>{{svg "octicon-code" 16}} Code</span>
{{svg "octicon-code" 16}}
<span>Code</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button>
<div class="clone-panel-popup tippy-target">

@ -1,10 +1,12 @@
{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.CommitsBehind)}}
{{if and .UpstreamDivergingInfo .UpstreamDivergingInfo.BaseBranchHasNewCommits}}
<div class="ui message flex-text-block">
<div class="tw-flex-1">
{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}}
{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}}
{{if .UpstreamDivergingInfo.CommitsBehind}}
{{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}}
{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.UpstreamDivergingInfo.BaseBranchName|PathEscapeSegments)}}
{{$upstreamRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.BaseRepo.FullName .UpstreamDivergingInfo.BaseBranchName}}
{{$thisRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.FullName .BranchName}}
{{$upstreamHtml := HTMLFormat `<a href="%s">%s</a>` $upstreamLink $upstreamRepoBranchDisplay}}
{{if .UpstreamDivergingInfo.HeadBranchCommitsBehind}}
{{ctx.Locale.TrN .UpstreamDivergingInfo.HeadBranchCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadBranchCommitsBehind $upstreamHtml}}
{{else}}
{{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}}
{{end}}
@ -12,7 +14,7 @@
{{if .CanWriteCode}}
<button class="ui compact primary button tw-m-0 link-action"
data-modal-confirm-header="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}"
data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" .BranchName}}"
data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" $upstreamRepoBranchDisplay $thisRepoBranchDisplay}}"
data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
</button>

@ -42,7 +42,7 @@
{{if .HasMerged}}
<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
{{else if .Issue.IsClosed}}
<div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
<div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
{{else if .Issue.IsPull}}
{{if .IsPullWorkInProgress}}
<div class="ui grey label issue-state-label">{{svg "octicon-git-pull-request-draft"}} {{ctx.Locale.Tr "repo.issues.draft_title"}}</div>

@ -2,7 +2,7 @@
<div class="ui segments repository-summary tw-mt-1 tw-mb-0">
<div class="ui segment sub-menu repository-menu">
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}}
<a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}">
<a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.RefFullName.RefWebLinkPath}}">
{{svg "octicon-history"}} <b>{{ctx.Locale.PrettyNumber .CommitsCount}}</b> {{ctx.Locale.TrN .CommitsCount "repo.commit" "repo.commits"}}
</a>
<a class="item muted {{if .PageIsBranches}}active{{end}}" href="{{.RepoLink}}/branches">

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save