Merge branch 'main' into Badge

pull/31262/head
techknowlogick 2 weeks 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/a11y-svg-has-accessible-name': [0],
'github/array-foreach': [0], 'github/array-foreach': [0],
'github/async-currenttarget': [2], '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/authenticity-token': [0],
'github/get-attribute': [0], 'github/get-attribute': [0],
'github/js-class-name': [0], 'github/js-class-name': [0],

@ -18,10 +18,12 @@ import (
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install" "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 // Set up Chi routes
webRoutes := routers.NormalRoutes() webRoutes := routers.NormalRoutes()
err := listen(webRoutes, true) err := listen(webRoutes, true)

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

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

@ -7,23 +7,36 @@ import (
"context" "context"
"time" "time"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm/contexts" "xorm.io/xorm/contexts"
) )
type SlowQueryHook struct { type EngineHook struct {
Threshold time.Duration Threshold time.Duration
Logger log.Logger Logger log.Logger
} }
var _ contexts.Hook = (*SlowQueryHook)(nil) var _ contexts.Hook = (*EngineHook)(nil)
func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil 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 { if c.ExecuteTime >= h.Threshold {
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function // 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) // 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) xe.SetDefaultContext(ctx)
if setting.Database.SlowQueryThreshold > 0 { if setting.Database.SlowQueryThreshold > 0 {
xe.AddHook(&SlowQueryHook{ xe.AddHook(&EngineHook{
Threshold: setting.Database.SlowQueryThreshold, Threshold: setting.Database.SlowQueryThreshold,
Logger: log.GetLogger("xorm"), Logger: log.GetLogger("xorm"),
}) })

@ -171,3 +171,9 @@
user_id: 40 user_id: 40
repo_id: 61 repo_id: 61
mode: 4 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, 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 return &branch, nil
} }
@ -440,6 +443,8 @@ type FindRecentlyPushedNewBranchesOptions struct {
} }
type RecentlyPushedNewBranch struct { type RecentlyPushedNewBranch struct {
BranchRepo *repo_model.Repository
BranchName string
BranchDisplayName string BranchDisplayName string
BranchLink string BranchLink string
BranchCompareURL 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) branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
} }
newBranches = append(newBranches, &RecentlyPushedNewBranch{ newBranches = append(newBranches, &RecentlyPushedNewBranch{
BranchRepo: branch.Repo,
BranchDisplayName: branchDisplayName, BranchDisplayName: branchDisplayName,
BranchName: branch.Name,
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)), BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name), BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
CommitTime: branch.CommitTime, CommitTime: branch.CommitTime,

@ -46,11 +46,6 @@ func (s Stopwatch) Seconds() int64 {
return int64(timeutil.TimeStampNow() - s.CreatedUnix) 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) { func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
sw = new(Stopwatch) sw = new(Stopwatch)
exists, err = db.GetEngine(ctx). exists, err = db.GetEngine(ctx).
@ -201,7 +196,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
Doer: user, Doer: user,
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Content: util.SecToTime(timediff), Content: util.SecToHours(timediff),
Type: CommentTypeStopTracking, Type: CommentTypeStopTracking,
TimeID: tt.ID, TimeID: tt.ID,
}); err != nil { }); err != nil {

@ -18,6 +18,7 @@ import (
"time" "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/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/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -54,7 +55,7 @@ func logArgSanitize(arg string) string {
} else if filepath.IsAbs(arg) { } else if filepath.IsAbs(arg) {
base := filepath.Base(arg) base := filepath.Base(arg)
dir := filepath.Dir(arg) dir := filepath.Dir(arg)
return filepath.Join(filepath.Base(dir), base) return ".../" + filepath.Join(filepath.Base(dir), base)
} }
return arg return arg
} }
@ -295,15 +296,20 @@ func (c *Command) run(skip int, opts *RunOpts) error {
timeout = defaultCommandExecutionTimeout timeout = defaultCommandExecutionTimeout
} }
var desc string cmdLogString := c.LogString()
callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */)
if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
callerInfo = callerInfo[pos+1:] callerInfo = callerInfo[pos+1:]
} }
// these logs are for debugging purposes only, so no guarantee of correctness or stability // these logs are for debugging purposes only, so no guarantee of correctness or stability
desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", 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) 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 ctx context.Context
var cancel context.CancelFunc var cancel context.CancelFunc
var finished 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()) 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") 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 { } else if commit.ParentCount() == 0 {
cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
} else { } 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...) cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...)
} }
case RawDiffPatch: case RawDiffPatch:
@ -74,7 +77,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
} else if commit.ParentCount() == 0 { } else if commit.ParentCount() == 0 {
cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...) cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
} else { } else {
c, _ := commit.Parent(0) c, err := commit.Parent(0)
if err != nil {
return err
}
query := fmt.Sprintf("%s...%s", endCommit, c.ID.String()) query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...) 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 // GetBranches returns branches from the repository, skipping "skip" initial branches and
// returning at most "limit" branches, or all branches if "limit" is 0. // 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 // implementation. This requires full fetch, sort and then the
// skip/limit applies later as gogit returns in undefined order. // skip/limit applies later as gogit returns in undefined order.
func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { 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 ( import (
"context" "context"
"fmt" "fmt"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" 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) branches, _, err := gitRepo.GetBranchNames(0, 0)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "ref file is empty") {
return 0, nil
}
return 0, err return 0, err
} }
log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches) 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 // time / number / format
"FileSize": base.FileSize, "FileSize": base.FileSize,
"CountFmt": countFmt, "CountFmt": countFmt,
"Sec2Time": util.SecToTime, "Sec2Time": util.SecToHours,
"TimeEstimateString": timeEstimateString, "TimeEstimateString": timeEstimateString,

@ -8,59 +8,17 @@ import (
"strings" "strings"
) )
// SecToTime converts an amount of seconds to a human-readable string. E.g. // SecToHours converts an amount of seconds to a human-readable hours string.
// 66s -> 1 minute 6 seconds // This is stable for planning and managing timesheets.
// 52410s -> 14 hours 33 minutes // Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours.
// 563418 -> 6 days 12 hours func SecToHours(durationVal any) string {
// 1563418 -> 2 weeks 4 days
// 3937125s -> 1 month 2 weeks
// 45677465s -> 1 year 6 months
func SecToTime(durationVal any) string {
duration, _ := ToInt64(durationVal) duration, _ := ToInt64(durationVal)
hours := duration / 3600
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
minutes := (duration / 60) % 60 minutes := (duration / 60) % 60
seconds := duration % 60
// Extract only the relevant information of the time formattedTime := ""
// 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 = formatTime(hours, "hour", formattedTime) formattedTime = formatTime(hours, "hour", formattedTime)
formattedTime = formatTime(minutes, "minute", 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 // The formatTime() function always appends a space at the end. This will be trimmed
return strings.TrimRight(formattedTime, " ") return strings.TrimRight(formattedTime, " ")
@ -76,6 +34,5 @@ func formatTime(value int64, name, formattedTime string) string {
} else if value > 1 { } else if value > 1 {
formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name) formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name)
} }
return formattedTime return formattedTime
} }

@ -9,22 +9,17 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestSecToTime(t *testing.T) { func TestSecToHours(t *testing.T) {
second := int64(1) second := int64(1)
minute := 60 * second minute := 60 * second
hour := 60 * minute hour := 60 * minute
day := 24 * hour day := 24 * hour
year := 365 * day
assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second)) assert.Equal(t, "1 minute", SecToHours(minute+6*second))
assert.Equal(t, "1 hour", SecToTime(hour)) assert.Equal(t, "1 hour", SecToHours(hour))
assert.Equal(t, "1 hour", SecToTime(hour+second)) assert.Equal(t, "1 hour", SecToHours(hour+second))
assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second)) assert.Equal(t, "14 hours 33 minutes", SecToHours(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, "156 hours 30 minutes", SecToHours(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, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second))
assert.Equal(t, "4 weeks", SecToTime(4*7*day)) assert.Equal(t, "672 hours", SecToHours(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))
} }

@ -121,7 +121,7 @@ func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo
return func(next http.Handler) http.Handler { 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 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) { 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) 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 return // it's doing pre-check, just return
} }
routing.UpdateFuncInfo(req.Context(), funcInfo) defer routing.RecordFuncInfo(req.Context(), funcInfo)()
ret := fn.Call(argsIn) ret := fn.Call(argsIn)
// handle the return value (no-op at the moment) // handle the return value (no-op at the moment)

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

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

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

@ -3,10 +3,6 @@
debug/ debug/
target/ 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 # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk

@ -1683,16 +1683,13 @@ issues.timetracker_timer_manually_add=Přidat čas
issues.time_estimate_set=Nastavit odhadovaný čas issues.time_estimate_set=Nastavit odhadovaný čas
issues.time_estimate_display=Odhad: %s 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.remove_time_estimate_at=odstranil/a odhad času %s
issues.time_estimate_invalid=Formát odhadu času je neplatný issues.time_estimate_invalid=Formát odhadu času je neplatný
issues.start_tracking_history=započal/a práci %s issues.start_tracking_history=započal/a práci %s
issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu 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.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.cancel_tracking_history=`zrušil/a sledování času %s`
issues.del_time=Odstranit tento časový záznam 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.del_time_history=`odstranil/a strávený čas %s`
issues.add_time_manually=Přidat čas ručně issues.add_time_manually=Přidat čas ručně
issues.add_time_hours=Hodiny issues.add_time_hours=Hodiny
@ -3369,7 +3366,6 @@ monitor.execute_time=Doba provádění
monitor.last_execution_result=Výsledek monitor.last_execution_result=Výsledek
monitor.process.cancel=Zrušit proces monitor.process.cancel=Zrušit proces
monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat 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.process.children=Potomek
monitor.queues=Fronty 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.type=Typ obrazu
container.details.platform=Platforma container.details.platform=Platforma
container.pull=Stáhněte obraz z příkazové řádky: container.pull=Stáhněte obraz z příkazové řádky:
container.digest=Výběr:
container.multi_arch=OS/architektura container.multi_arch=OS/architektura
container.layers=Vrstvy obrazů container.layers=Vrstvy obrazů
container.labels=Štítky 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_set=Geschätzte Zeit festlegen
issues.time_estimate_display=Schätzung: %s 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.remove_time_estimate_at=Zeitschätzung %s entfernt
issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig
issues.start_tracking_history=hat die Zeiterfassung %s gestartet issues.start_tracking_history=hat die Zeiterfassung %s gestartet
issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird 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.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.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen`
issues.del_time=Diese Zeiterfassung löschen 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.del_time_history=`hat %s gearbeitete Zeit gelöscht`
issues.add_time_manually=Zeit manuell hinzufügen issues.add_time_manually=Zeit manuell hinzufügen
issues.add_time_hours=Stunden issues.add_time_hours=Stunden
@ -3359,7 +3356,6 @@ monitor.execute_time=Ausführungszeit
monitor.last_execution_result=Ergebnis monitor.last_execution_result=Ergebnis
monitor.process.cancel=Prozess abbrechen monitor.process.cancel=Prozess abbrechen
monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen
monitor.process.cancel_notices=Abbrechen: <strong>%s</strong>?
monitor.process.children=Subprozesse monitor.process.children=Subprozesse
monitor.queues=Warteschlangen 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.type=Container-Image Typ
container.details.platform=Plattform container.details.platform=Plattform
container.pull=Downloade das Container-Image aus der Kommandozeile: container.pull=Downloade das Container-Image aus der Kommandozeile:
container.digest=Digest:
container.multi_arch=Betriebsystem / Architektur container.multi_arch=Betriebsystem / Architektur
container.layers=Container-Image Ebenen container.layers=Container-Image Ebenen
container.labels=Labels container.labels=Labels

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

@ -1690,16 +1690,16 @@ issues.timetracker_timer_manually_add = Add Time
issues.time_estimate_set = Set estimated time issues.time_estimate_set = Set estimated time
issues.time_estimate_display = Estimate: %s 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.remove_time_estimate_at = removed time estimate %s
issues.time_estimate_invalid = Time estimate format is invalid issues.time_estimate_invalid = Time estimate format is invalid
issues.start_tracking_history = started working %s issues.start_tracking_history = started working %s
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed 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.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.cancel_tracking_history = `canceled time tracking %s`
issues.del_time = Delete this time log 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.del_time_history= `deleted spent time %s`
issues.add_time_manually = Manually Add Time issues.add_time_manually = Manually Add Time
issues.add_time_hours = Hours 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_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_prompt_base_newer = The base branch %s has new changes
pulls.upstream_diverging_merge = Sync fork 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.deleted_branch = (deleted):%s
pull.agit_documentation = Review documentation about AGit 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 = Create new branch
branch.new_branch_from = Create new branch from "%s" branch.new_branch_from = Create new branch from "%s"
branch.renamed = Branch %s was renamed to %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 = Create tag %s
tag.create_tag_operation = Create tag tag.create_tag_operation = Create tag
@ -3396,6 +3398,8 @@ monitor.previous = Previous Time
monitor.execute_times = Executions monitor.execute_times = Executions
monitor.process = Running Processes monitor.process = Running Processes
monitor.stacktrace = Stacktrace monitor.stacktrace = Stacktrace
monitor.trace = Trace
monitor.performance_logs = Performance Logs
monitor.processes_count = %d Processes monitor.processes_count = %d Processes
monitor.download_diagnosis_report = Download diagnosis report monitor.download_diagnosis_report = Download diagnosis report
monitor.desc = Description monitor.desc = Description
@ -3404,7 +3408,6 @@ monitor.execute_time = Execution Time
monitor.last_execution_result = Result monitor.last_execution_result = Result
monitor.process.cancel = Cancel process monitor.process.cancel = Cancel process
monitor.process.cancel_desc = Cancelling a process may cause data loss monitor.process.cancel_desc = Cancelling a process may cause data loss
monitor.process.cancel_notices = Cancel: <strong>%s</strong>?
monitor.process.children = Children monitor.process.children = Children
monitor.queues = Queues 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.type = Image Type
container.details.platform = Platform container.details.platform = Platform
container.pull = Pull the image from the command line: container.pull = Pull the image from the command line:
container.digest = Digest: container.images = Images
container.digest = Digest
container.multi_arch = OS / Arch container.multi_arch = OS / Arch
container.layers = Image Layers container.layers = Image Layers
container.labels = Labels 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.type=Tipo de imagen
container.details.platform=Plataforma container.details.platform=Plataforma
container.pull=Arrastra la imagen desde la línea de comandos: container.pull=Arrastra la imagen desde la línea de comandos:
container.digest=Resumen:
container.multi_arch=SO / Arquitectura container.multi_arch=SO / Arquitectura
container.layers=Capas de imagen container.layers=Capas de imagen
container.labels=Etiquetas 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_set=Définir le temps estimé
issues.time_estimate_display=Estimation : %s 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.remove_time_estimate_at=a supprimé le temps estimé %s
issues.time_estimate_invalid=Le format du temps estimé est invalide issues.time_estimate_invalid=Le format du temps estimé est invalide
issues.start_tracking_history=`a commencé son travail %s.` 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.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.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.cancel_tracking_history=`a abandonné son minuteur %s.`
issues.del_time=Supprimer ce minuteur du journal 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.del_time_history=`a supprimé son temps de travail %s.`
issues.add_time_manually=Temps pointé manuellement issues.add_time_manually=Temps pointé manuellement
issues.add_time_hours=Heures issues.add_time_hours=Heures
@ -3370,7 +3367,6 @@ monitor.execute_time=Heure d'Éxécution
monitor.last_execution_result=Résultat monitor.last_execution_result=Résultat
monitor.process.cancel=Annuler le processus monitor.process.cancel=Annuler le processus
monitor.process.cancel_desc=Lannulation dun processus peut entraîner une perte de données. 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.process.children=Enfant
monitor.queues=Files d'attente 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.type=Type d'image
container.details.platform=Plateforme container.details.platform=Plateforme
container.pull=Tirez l'image depuis un terminal : container.pull=Tirez l'image depuis un terminal :
container.digest=Empreinte :
container.multi_arch=SE / Arch container.multi_arch=SE / Arch
container.layers=Calques d'image container.layers=Calques d'image
container.labels=Labels 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_set=Socraigh am measta
issues.time_estimate_display=Meastachán: %s 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.remove_time_estimate_at=baineadh meastachán ama %s
issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí
issues.start_tracking_history=thosaigh ag obair %s 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.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.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.cancel_tracking_history=`rianú ama curtha ar ceal %s`
issues.del_time=Scrios an log ama seo 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.del_time_history=`an t-am caite scriosta %s`
issues.add_time_manually=Cuir Am leis de Láimh issues.add_time_manually=Cuir Am leis de Láimh
issues.add_time_hours=Uaireanta issues.add_time_hours=Uaireanta
@ -3371,7 +3368,6 @@ monitor.execute_time=Am Forghníomhaithe
monitor.last_execution_result=Toradh monitor.last_execution_result=Toradh
monitor.process.cancel=Cealaigh próiseas 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_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.process.children=Leanaí
monitor.queues=Scuaineanna 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.type=Cineál Íomhá
container.details.platform=Ardán container.details.platform=Ardán
container.pull=Tarraing an íomhá ón líne ordaithe: container.pull=Tarraing an íomhá ón líne ordaithe:
container.digest=Díleáigh:
container.multi_arch=Córas Oibriúcháin / Ailtireacht container.multi_arch=Córas Oibriúcháin / Ailtireacht
container.layers=Sraitheanna Íomhá container.layers=Sraitheanna Íomhá
container.labels=Lipéid container.labels=Lipéid

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

@ -2310,7 +2310,6 @@ monitor.start=Czas rozpoczęcia
monitor.execute_time=Czas wykonania monitor.execute_time=Czas wykonania
monitor.process.cancel=Anuluj proces monitor.process.cancel=Anuluj proces
monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych
monitor.process.cancel_notices=Anuluj: <strong>%s</strong>?
monitor.queues=Kolejki monitor.queues=Kolejki
monitor.queue=Kolejka: %s 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.type=Tipo de Imagem
container.details.platform=Plataforma container.details.platform=Plataforma
container.pull=Puxe a imagem pela linha de comando: container.pull=Puxe a imagem pela linha de comando:
container.digest=Digest:
container.multi_arch=S.O. / Arquitetura container.multi_arch=S.O. / Arquitetura
container.layers=Camadas da Imagem container.layers=Camadas da Imagem
container.labels=Rótulos 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_set=Definir tempo estimado
issues.time_estimate_display=Estimativa: %s 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.remove_time_estimate_at=removeu a estimativa de tempo %s
issues.time_estimate_invalid=O formato da estimativa de tempo é inválido issues.time_estimate_invalid=O formato da estimativa de tempo é inválido
issues.start_tracking_history=começou a trabalhar %s 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.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.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.cancel_tracking_history=`cancelou a contagem de tempo %s`
issues.del_time=Eliminar este registo de tempo 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.del_time_history=`eliminou o tempo gasto nesta questão %s`
issues.add_time_manually=Adicionar tempo manualmente issues.add_time_manually=Adicionar tempo manualmente
issues.add_time_hours=Horas 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.wiki_desc=Habilitar wiki do repositório
settings.use_internal_wiki=Usar o wiki integrado settings.use_internal_wiki=Usar o wiki integrado
settings.default_wiki_branch_name=Nome do ramo predefinido do wiki 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.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.use_external_wiki=Usar um wiki externo
settings.external_wiki_url=URL do 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=Criar um novo ramo
branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"` branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"`
branch.renamed=O ramo %s foi renomeado para %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=Criar etiqueta %s
tag.create_tag_operation=Criar etiqueta tag.create_tag_operation=Criar etiqueta
@ -3371,7 +3374,6 @@ monitor.execute_time=Tempo de execução
monitor.last_execution_result=Resultado monitor.last_execution_result=Resultado
monitor.process.cancel=Cancelar processo monitor.process.cancel=Cancelar processo
monitor.process.cancel_desc=Cancelar um processo pode resultar na perda de dados 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.process.children=Descendentes
monitor.queues=Filas 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.type=Tipo de imagem
container.details.platform=Plataforma container.details.platform=Plataforma
container.pull=Puxar a imagem usando a linha de comandos: 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.multi_arch=S.O. / Arquit.
container.layers=Camadas de imagem container.layers=Camadas de imagem
container.labels=Rótulos container.labels=Rótulos

@ -3176,7 +3176,6 @@ conda.install=Чтобы установить пакет с помощью Conda
container.details.type=Тип образа container.details.type=Тип образа
container.details.platform=Платформа container.details.platform=Платформа
container.pull=Загрузите образ из командной строки: container.pull=Загрузите образ из командной строки:
container.digest=Отпечаток:
container.multi_arch=ОС / архитектура container.multi_arch=ОС / архитектура
container.layers=Слои образа container.layers=Слои образа
container.labels=Метки 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.type=Görüntü Türü
container.details.platform=Platform container.details.platform=Platform
container.pull=Görüntüyü komut satırını kullanarak çekin: container.pull=Görüntüyü komut satırını kullanarak çekin:
container.digest=Özet:
container.multi_arch=İşletim Sistemi / Mimari container.multi_arch=İşletim Sistemi / Mimari
container.layers=Görüntü Katmanları container.layers=Görüntü Katmanları
container.labels=Etiketler container.labels=Etiketler

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

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

908
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -5,18 +5,18 @@
}, },
"dependencies": { "dependencies": {
"@citation-js/core": "0.7.14", "@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-csl": "0.7.14",
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3", "@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", "@github/text-expander-element": "2.8.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0", "@primer/octicons": "19.14.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6", "@silverwind/vue3-calendar-heatmap": "2.0.6",
"add-asset-webpack-plugin": "3.0.0", "add-asset-webpack-plugin": "3.0.0",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
"asciinema-player": "3.8.1", "asciinema-player": "3.8.2",
"chart.js": "4.4.7", "chart.js": "4.4.7",
"chartjs-adapter-dayjs-4": "1.0.4", "chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
@ -28,11 +28,11 @@
"easymde": "2.18.0", "easymde": "2.18.0",
"esbuild-loader": "4.2.2", "esbuild-loader": "4.2.2",
"escape-goat": "4.0.0", "escape-goat": "4.0.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.3",
"htmx.org": "2.0.4", "htmx.org": "2.0.4",
"idiomorph": "0.3.0", "idiomorph": "0.4.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.18", "katex": "0.16.20",
"license-checker-webpack-plugin": "0.2.1", "license-checker-webpack-plugin": "0.2.1",
"mermaid": "11.4.1", "mermaid": "11.4.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
@ -41,7 +41,7 @@
"monaco-editor-webpack-plugin": "7.1.0", "monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0", "pdfobject": "2.3.0",
"perfect-debounce": "1.0.0", "perfect-debounce": "1.0.0",
"postcss": "8.4.49", "postcss": "8.5.1",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1", "postcss-nesting": "13.0.1",
"sortablejs": "1.15.6", "sortablejs": "1.15.6",
@ -52,7 +52,7 @@
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
"toastify-js": "1.12.0", "toastify-js": "1.12.0",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"typescript": "5.7.2", "typescript": "5.7.3",
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2", "vanilla-colorful": "0.7.2",
"vue": "3.5.13", "vue": "3.5.13",
@ -60,14 +60,14 @@
"vue-chartjs": "5.3.2", "vue-chartjs": "5.3.2",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"webpack": "5.97.1", "webpack": "5.97.1",
"webpack-cli": "5.1.4", "webpack-cli": "6.0.1",
"wrap-ansi": "9.0.0" "wrap-ansi": "9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.49.1", "@playwright/test": "1.49.1",
"@stoplight/spectral-cli": "6.14.2", "@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", "@stylistic/stylelint-plugin": "3.1.1",
"@types/dropzone": "5.7.9", "@types/dropzone": "5.7.9",
"@types/jquery": "3.5.32", "@types/jquery": "3.5.32",
@ -79,8 +79,8 @@
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.3", "@types/toastify-js": "1.12.3",
"@typescript-eslint/eslint-plugin": "8.18.1", "@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.18.1", "@typescript-eslint/parser": "8.20.0",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-import-resolver-typescript": "3.7.0", "eslint-import-resolver-typescript": "3.7.0",
@ -98,16 +98,16 @@
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.32.0",
"eslint-plugin-vue-scoped-css": "2.9.0", "eslint-plugin-vue-scoped-css": "2.9.0",
"eslint-plugin-wc": "2.2.0", "eslint-plugin-wc": "2.2.0",
"happy-dom": "15.11.7", "happy-dom": "16.6.0",
"markdownlint-cli": "0.43.0", "markdownlint-cli": "0.43.0",
"nolyfill": "1.0.43", "nolyfill": "1.0.43",
"postcss-html": "1.7.0", "postcss-html": "1.8.0",
"stylelint": "16.12.0", "stylelint": "16.13.2",
"stylelint-declaration-block-no-ignored-properties": "2.8.0", "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", "stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.3.2", "svgo": "3.3.2",
"type-fest": "4.30.2", "type-fest": "4.32.0",
"updates": "16.4.1", "updates": "16.4.1",
"vite-string-plugin": "1.3.4", "vite-string-plugin": "1.3.4",
"vitest": "2.1.8", "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]] [[package]]
name = "click" name = "click"
version = "8.1.7" version = "8.1.8"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
] ]
[package.dependencies] [package.dependencies]
@ -42,33 +42,33 @@ six = ">=1.13.0"
[[package]] [[package]]
name = "djlint" name = "djlint"
version = "1.36.3" version = "1.36.4"
description = "HTML Template Linter and Formatter" description = "HTML Template Linter and Formatter"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2"}, {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"},
{file = "djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b"}, {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"},
{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.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1"},
{file = "djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414"}, {file = "djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c"},
{file = "djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730"}, {file = "djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7"},
{file = "djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e"}, {file = "djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7"},
{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.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483"},
{file = "djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d"}, {file = "djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08"},
{file = "djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084"}, {file = "djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b"},
{file = "djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba"}, {file = "djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e"},
{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.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675"},
{file = "djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a"}, {file = "djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08"},
{file = "djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433"}, {file = "djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2"},
{file = "djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302"}, {file = "djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835"},
{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.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f"},
{file = "djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e"}, {file = "djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4"},
{file = "djlint-1.36.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e2317a32094d525bc41cd11c8dc064bf38d1b442c99cc3f7c4a2616b5e6ce6e"}, {file = "djlint-1.36.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89678661888c03d7bc6cadd75af69db29962b5ecbf93a81518262f5c48329f04"},
{file = "djlint-1.36.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e82266c28793cd15f97b93535d72bfbc77306eaaf6b210dd90910383a814ee6c"}, {file = "djlint-1.36.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b01a98df3e1ab89a552793590875bc6e954cad661a9304057db75363d519fa0"},
{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.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbb4f7b93223d471d09ae34ed515fef98b2233cbca2449ad117416c44b1351"},
{file = "djlint-1.36.3-cp39-cp39-win_amd64.whl", hash = "sha256:15cde63ef28beb5194ff4137883025f125676ece1b574b64a3e1c6daed734639"}, {file = "djlint-1.36.4-cp39-cp39-win_amd64.whl", hash = "sha256:7a483390d17e44df5bc23dcea29bdf6b63f3ed8b4731d844773a4829af4f5e0b"},
{file = "djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c"}, {file = "djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd"},
{file = "djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78"}, {file = "djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1"},
] ]
[package.dependencies] [package.dependencies]
@ -82,15 +82,17 @@ pyyaml = ">=6"
regex = ">=2023" regex = ">=2023"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
tqdm = ">=4.62.2" tqdm = ">=4.62.2"
typing-extensions = {version = ">=3.6.6", markers = "python_version < \"3.11\""}
[[package]] [[package]]
name = "editorconfig" name = "editorconfig"
version = "0.12.4" version = "0.17.0"
description = "EditorConfig File Locator and Interpreter for Python" description = "EditorConfig File Locator and Interpreter for Python"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ 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]] [[package]]
@ -370,6 +372,17 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"] slack = ["slack-sdk"]
telegram = ["requests"] 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]] [[package]]
name = "yamllint" name = "yamllint"
version = "1.35.1" version = "1.35.1"
@ -391,4 +404,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "01b1e2f910276dd20a70ebb665c83415c37531709d90874f5b7a86a5305e2369" content-hash = "f2e8260efe6e25f77ef387daff9551e41d25027e4794b42bc7a851ed0dfafd85"

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

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "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) msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name)
if err != nil { 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) ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
}
return return
} }
if msg == "target_exist" { if msg == "target_exist" {

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

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

@ -9,6 +9,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -43,14 +44,26 @@ func ProtocolMiddlewares() (handlers []any) {
func RequestContextHandler() func(h http.Handler) http.Handler { func RequestContextHandler() func(h http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI) // 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) ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc)
defer finished() 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() { defer func() {
if err := recover(); err != nil { 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 _ = 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 // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx := chi.RouteContext(req.Context()) chiCtx := chi.RouteContext(req.Context())
if req.URL.RawPath == "" { if req.URL.RawPath == "" {
ctx.RoutePath = req.URL.EscapedPath() chiCtx.RoutePath = req.URL.EscapedPath()
} else { } else {
ctx.RoutePath = req.URL.RawPath chiCtx.RoutePath = req.URL.RawPath
} }
next.ServeHTTP(resp, req) next.ServeHTTP(resp, req)
}) })

@ -213,7 +213,7 @@ func NormalRoutes() *web.Router {
} }
r.NotFound(func(w http.ResponseWriter, req *http.Request) { 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) http.NotFound(w, req)
}) })
return r return r

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

@ -10,13 +10,15 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/tailmsg"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
func MonitorDiagnosis(ctx *context.Context) { func MonitorDiagnosis(ctx *context.Context) {
seconds := ctx.FormInt64("seconds") seconds := ctx.FormInt64("seconds")
if seconds <= 5 { if seconds <= 1 {
seconds = 5 seconds = 1
} }
if seconds > 300 { if seconds > 300 {
seconds = 300 seconds = 300
@ -65,4 +67,16 @@ func MonitorDiagnosis(ctx *context.Context) {
return return
} }
_ = pprof.Lookup("heap").WriteTo(f, 0) _ = 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" "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 // Stacktrace show admin monitor goroutines page
func Stacktrace(ctx *context.Context) { func Stacktrace(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor") monitorTraceCommon(ctx)
ctx.Data["PageIsAdminMonitorStacktrace"] = true
ctx.Data["GoroutineCount"] = runtime.NumGoroutine() 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 // LinkAccount shows the page where the user can decide to login or create a new account
func LinkAccount(ctx *context.Context) { 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["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true ctx.Data["LinkAccountMode"] = true
@ -43,6 +44,7 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template // 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" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User) 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 { if !ok {
// no account in session, so just redirect to the login page, then the user could restart the process // no account in session, so just redirect to the login page, then the user could restart the process
ctx.Redirect(setting.AppSubURL + "/user/login") ctx.Redirect(setting.AppSubURL + "/user/login")
@ -135,6 +142,8 @@ func LinkAccountPostSignIn(ctx *context.Context) {
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template // 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["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template // 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) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
} }
routing.UpdateFuncInfo(req.Context(), funcInfo) defer routing.RecordFuncInfo(req.Context(), funcInfo)()
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
rPath = util.PathJoinRelX(rPath) 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) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
} }
routing.UpdateFuncInfo(req.Context(), funcInfo) defer routing.RecordFuncInfo(req.Context(), funcInfo)()
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
rPath = util.PathJoinRelX(rPath) rPath = util.PathJoinRelX(rPath)

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

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

@ -29,7 +29,7 @@ func CodeFrequency(ctx *context.Context) {
// CodeFrequencyData returns JSON of code frequency data // CodeFrequencyData returns JSON of code frequency data
func CodeFrequencyData(ctx *context.Context) { 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) { if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted) ctx.Status(http.StatusAccepted)
return return

@ -62,11 +62,7 @@ func Commits(ctx *context.Context) {
} }
ctx.Data["PageIsViewCode"] = true ctx.Data["PageIsViewCode"] = true
commitsCount, err := ctx.Repo.GetCommitsCount() commitsCount := ctx.Repo.CommitsCount
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
}
page := ctx.FormInt("page") page := ctx.FormInt("page")
if page <= 1 { if page <= 1 {
@ -129,12 +125,6 @@ func Graph(ctx *context.Context) {
ctx.Data["SelectedBranches"] = realBranches ctx.Data["SelectedBranches"] = realBranches
files := ctx.FormStrings("file") 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) graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files)
if err != nil { 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) 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["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
paginator.AddParamFromRequest(ctx.Req) paginator.AddParamFromRequest(ctx.Req)

@ -26,7 +26,7 @@ func Contributors(ctx *context.Context) {
// ContributorsData renders JSON of contributors along with their weekly commit statistics // ContributorsData renders JSON of contributors along with their weekly commit statistics
func ContributorsData(ctx *context.Context) { 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) { if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted) ctx.Status(http.StatusAccepted)
return return

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

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

@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) {
return 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("") c.JSONRedirect("")
} }

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

@ -29,7 +29,7 @@ func RecentCommits(ctx *context.Context) {
// RecentCommitsData returns JSON of recent commits data // RecentCommitsData returns JSON of recent commits data
func RecentCommitsData(ctx *context.Context) { 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) { if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted) ctx.Status(http.StatusAccepted)
return return

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

@ -4,6 +4,7 @@
package setting package setting
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -14,6 +15,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" 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/base"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web" "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) msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
if err != nil { if err != nil {
switch { 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): case git_model.IsErrBranchAlreadyExists(err):
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To)) ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink)) 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: default:
ctx.ServerError("RenameBranch", err) ctx.ServerError("RenameBranch", err)
} }

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

@ -215,10 +215,28 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
baseRepoPerm.CanRead(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 { if err != nil {
log.Error("FindRecentlyPushedNewBranches failed: %v", err) 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") ctx.SetPathParam("*", "Home")
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx) 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"]) assert.EqualValues(t, "Home", ctx.Data["Title"])
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"]) 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") ctx.SetPathParam("*", "jpeg.jpg")
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx) 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")) 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") ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
WikiPages(ctx) 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"]) 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.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
NewWiki(ctx) 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"]) assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
} }
@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) {
Message: message, Message: message,
}) })
NewWikiPost(ctx) 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)) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(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, Message: message,
}) })
NewWikiPost(ctx) 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) assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit") assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
} }
@ -162,7 +162,7 @@ func TestEditWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx) 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.EqualValues(t, "Home", ctx.Data["Title"])
assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) 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.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx) EditWiki(ctx)
assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status()) assert.EqualValues(t, http.StatusForbidden, ctx.Resp.WrittenStatus())
} }
func TestEditWikiPost(t *testing.T) { func TestEditWikiPost(t *testing.T) {
@ -190,7 +190,7 @@ func TestEditWikiPost(t *testing.T) {
Message: message, Message: message,
}) })
EditWikiPost(ctx) 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)) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
if title != "Home" { if title != "Home" {
@ -206,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) {
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
DeleteWikiPagePost(ctx) DeleteWikiPagePost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertWikiNotExists(t, ctx.Repo.Repository, "Home") assertWikiNotExists(t, ctx.Repo.Repository, "Home")
} }
@ -228,9 +228,9 @@ func TestWikiRaw(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
WikiRaw(ctx) WikiRaw(ctx)
if filetype == "" { 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 { } 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) 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. // 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) { func(o *issue_indexer.SearchOptions) {
o.IsFuzzyKeyword = isFuzzy 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 { 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{} ret = &issues_model.IssueStats{}
doerID := ctx.Doer.ID 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() openClosedOpts := opts.Copy()
switch filterMode { 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 })) ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false }))
if err != nil { if err != nil {
return nil, err return nil, err

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

@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) {
AccountPost(ctx) AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message) 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.Group("/monitor", func() {
m.Get("/stats", admin.MonitorStats) m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks) m.Get("/cron", admin.CronTasks)
m.Get("/perftrace", admin.PerfTrace)
m.Get("/stacktrace", admin.Stacktrace) m.Get("/stacktrace", admin.Stacktrace)
m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel) m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel)
m.Get("/queue", admin.Queues) m.Get("/queue", admin.Queues)
@ -1156,7 +1157,7 @@ func registerRoutes(m *web.Router) {
m.Post("/cancel", repo.MigrateCancelPost) m.Post("/cancel", repo.MigrateCancelPost)
}) })
}, },
reqSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoRef(), reqSignIn, context.RepoAssignment, reqRepoAdmin,
ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer),
) )
// end "/{username}/{reponame}/settings" // end "/{username}/{reponame}/settings"
@ -1342,7 +1343,7 @@ func registerRoutes(m *web.Router) {
m.Group("/{username}/{reponame}", func() { // repo tags m.Group("/{username}/{reponame}", func() { // repo tags
m.Group("/tags", func() { 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(".rss", feedEnabled, repo.TagsListFeedRSS)
m.Get(".atom", feedEnabled, repo.TagsListFeedAtom) m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
m.Get("/list", repo.GetTagList) m.Get("/list", repo.GetTagList)
@ -1523,7 +1524,7 @@ func registerRoutes(m *web.Router) {
m.Group("/activity_author_data", func() { m.Group("/activity_author_data", func() {
m.Get("", repo.ActivityAuthors) m.Get("", repo.ActivityAuthors)
m.Get("/{period}", repo.ActivityAuthors) m.Get("/{period}", repo.ActivityAuthors)
}, context.RepoRef(), repo.MustBeNotEmpty) }, repo.MustBeNotEmpty)
m.Group("/archive", func() { m.Group("/archive", func() {
m.Get("/*", repo.Download) m.Get("/*", repo.Download)
@ -1532,8 +1533,8 @@ func registerRoutes(m *web.Router) {
m.Group("/branches", func() { m.Group("/branches", func() {
m.Get("/list", repo.GetBranchesList) m.Get("/list", repo.GetBranchesList)
m.Get("", repo.Branches) m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.Branches)
}, repo.MustBeNotEmpty, context.RepoRef()) }, repo.MustBeNotEmpty)
m.Group("/media", func() { m.Group("/media", func() {
m.Get("/blob/{sha}", repo.DownloadByIDOrLFS) m.Get("/blob/{sha}", repo.DownloadByIDOrLFS)
@ -1577,8 +1578,10 @@ func registerRoutes(m *web.Router) {
m.Get("/graph", repo.Graph) 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})$}", 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("/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("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed)
m.Get("/atom/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) { m.NotFound(func(w http.ResponseWriter, req *http.Request) {
ctx := context.GetWebContext(req) ctx := context.GetWebContext(req)
routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound")) defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()
ctx.NotFound("", nil) ctx.NotFound("", nil)
}) })
} }

@ -18,11 +18,12 @@ import (
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
) )
type routerLoggerOptions struct { type accessLoggerTmplData struct {
req *http.Request
Identity *string Identity *string
Start *time.Time Start *time.Time
ResponseWriter http.ResponseWriter ResponseWriter struct {
Status, Size int
}
Ctx map[string]any Ctx map[string]any
RequestID *string RequestID *string
} }
@ -51,17 +52,15 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
return requestID return requestID
} }
// AccessLogger returns a middleware to log access logger type accessLogRecorder struct {
func AccessLogger() func(http.Handler) http.Handler { logger log.BaseLogger
logger := log.GetLogger("access") logTemplate *template.Template
needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate) needRequestID bool
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()
func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) {
var requestID string var requestID string
if needRequestID { if lr.needRequestID {
requestID = parseRequestIDFromRequestHeader(req) requestID = parseRequestIDFromRequestHeader(req)
} }
@ -70,32 +69,48 @@ func AccessLogger() func(http.Handler) http.Handler {
reqHost = req.RemoteAddr reqHost = req.RemoteAddr
} }
next.ServeHTTP(w, req)
rw := w.(ResponseWriter)
identity := "-" identity := "-"
data := middleware.GetContextData(req.Context()) data := middleware.GetContextData(req.Context())
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
identity = signedUser.Name identity = signedUser.Name
} }
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
err = logTemplate.Execute(buf, routerLoggerOptions{ tmplData := accessLoggerTmplData{
req: req,
Identity: &identity, Identity: &identity,
Start: &start, Start: &start,
ResponseWriter: rw,
Ctx: map[string]any{ Ctx: map[string]any{
"RemoteAddr": req.RemoteAddr, "RemoteAddr": req.RemoteAddr,
"RemoteHost": reqHost, "RemoteHost": reqHost,
"Req": req, "Req": req,
}, },
RequestID: &requestID, RequestID: &requestID,
}) }
tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
tmplData.ResponseWriter.Size = respWriter.WrittenSize()
err = lr.logTemplate.Execute(buf, tmplData)
if err != nil { if err != nil {
log.Error("Could not execute access logger template: %v", err.Error()) 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 package context
import ( import (
"context"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
@ -25,8 +24,7 @@ type BaseContextKeyType struct{}
var BaseContextKey BaseContextKeyType var BaseContextKey BaseContextKeyType
type Base struct { type Base struct {
context.Context reqctx.RequestContext
reqctx.RequestDataStore
Resp ResponseWriter Resp ResponseWriter
Req *http.Request 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 { func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base {
ds := reqctx.GetRequestDataStore(req.Context()) reqCtx := reqctx.FromContext(req.Context())
b := &Base{ b := &Base{
Context: req.Context(), RequestContext: reqCtx,
RequestDataStore: ds,
Req: req, Req: req,
Resp: WrapResponseWriter(resp), Resp: WrapResponseWriter(resp),
Locale: middleware.Locale(resp, req), Locale: middleware.Locale(resp, req),
Data: ds.GetData(), Data: reqCtx.GetData(),
} }
b.Req = b.Req.WithContext(b) b.Req = b.Req.WithContext(b)
ds.SetContextValue(BaseContextKey, b) reqCtx.SetContextValue(BaseContextKey, b)
ds.SetContextValue(translation.ContextKey, b.Locale) reqCtx.SetContextValue(translation.ContextKey, b.Locale)
ds.SetContextValue(httplib.RequestContextKey, b.Req) reqCtx.SetContextValue(httplib.RequestContextKey, b.Req)
return b 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 // RepoRefByType handles repository reference name for a specific type
// of repository reference // of repository reference
func RepoRefByType(detectRefType git.RefType) func(*Context) { func RepoRefByType(detectRefType git.RefType) func(*Context) {

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

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" 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 { 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{ result = append(result, api.StopWatch{
Created: sw.CreatedUnix.AsTime(), Created: sw.CreatedUnix.AsTime(),
Seconds: sw.Seconds(), Seconds: sw.Seconds(),
Duration: sw.Duration(), Duration: util.SecToHours(sw.Seconds()),
IssueIndex: issue.Index, IssueIndex: issue.Index,
IssueTitle: issue.Title, IssueTitle: issue.Title,
RepoOwnerName: repo.OwnerName, RepoOwnerName: repo.OwnerName,

@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
c.Content[0] == '|' { c.Content[0] == '|' {
// TimeTracking Comments from v1.21 on store the seconds instead of an formatted string // 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 // 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 { if c.Type == issues_model.CommentTypeChangeTimeEstimate {

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

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
repo_module "code.gitea.io/gitea/modules/repository" 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/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook" 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 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 { if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error {
err2 := gitRepo.RenameBranch(from, to) err2 := gitRepo.RenameBranch(from, to)
if err2 != nil { if err2 != nil {
@ -642,3 +666,72 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
return nil 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 package repository
import ( import (
"context" "errors"
"fmt" "fmt"
git_model "code.gitea.io/gitea/models/git"
issue_model "code.gitea.io/gitea/models/issues" issue_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/pull" "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. // 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 { if err = repo.MustNotBeArchived(); err != nil {
return "", err return "", err
} }
if err = repo.GetBaseRepo(ctx); err != nil { if err = repo.GetBaseRepo(ctx); err != nil {
return "", err 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{ err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
Remote: repo.RepoPath(), 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), Env: repo_module.PushingEnvironment(doer, repo),
}) })
if err == nil { if err == nil {
@ -67,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
BaseRepoID: repo.BaseRepo.ID, BaseRepoID: repo.BaseRepo.ID,
BaseRepo: repo.BaseRepo, BaseRepo: repo.BaseRepo,
HeadBranch: branch, // maybe HeadCommitID is not needed HeadBranch: branch, // maybe HeadCommitID is not needed
BaseBranch: repo.BaseRepo.DefaultBranch, BaseBranch: divergingInfo.BaseBranchName,
} }
fakeIssue.PullRequest = fakePR fakeIssue.PullRequest = fakePR
err = pull.Update(ctx, fakePR, doer, "merge upstream", false) 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 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. // 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) { func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) {
if !repo.IsFork { if !forkRepo.IsFork {
return nil, util.NewInvalidArgumentErrorf("repo is not a fork") return nil, util.NewInvalidArgumentErrorf("repo is not a fork")
} }
if repo.IsArchived { if forkRepo.IsArchived {
return nil, util.NewInvalidArgumentErrorf("repo is archived") return nil, util.NewInvalidArgumentErrorf("repo is archived")
} }
if err := repo.GetBaseRepo(ctx); err != nil { if err := forkRepo.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 {
return nil, err return nil, err
} }
info := &UpstreamDivergingInfo{} // Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo:
if forkBranch.CommitID == baseBranch.CommitID { // * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a`
return info, nil // * 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 {
// if the fork repo has new commits, this call will fail because they are not in the base repo return &UpstreamDivergingInfo{
// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb BaseBranchName: forkBranch,
// so at the moment, we first check the update time, then check whether the fork branch has base's head BaseBranchHasNewCommits: info.BaseHasNewCommits,
diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID) HeadBranchCommitsBehind: info.HeadCommitsBehind,
if err != nil { }, nil
info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix }
if info.BaseHasNewCommits { if errors.Is(err, util.ErrNotExist) {
return info, nil info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
} if err == nil {
return &UpstreamDivergingInfo{
// if the base's update time is before the fork, check whether the base's head is in the fork BaseBranchName: forkRepo.BaseRepo.DefaultBranch,
baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo.BaseRepo) BaseBranchHasNewCommits: info.BaseHasNewCommits,
if err != nil { HeadBranchCommitsBehind: info.HeadCommitsBehind,
return nil, err }, nil
}
headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
return nil, err
} }
baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID)
if err != nil {
return nil, err
} }
headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID)
if err != nil {
return nil, err 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"> <a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices">
{{ctx.Locale.Tr "admin.notices"}} {{ctx.Locale.Tr "admin.notices"}}
</a> </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> <summary>{{ctx.Locale.Tr "admin.monitor"}}</summary>
<div class="menu"> <div class="menu">
<a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stats"> <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"> <a class="{{if .PageIsAdminMonitorQueue}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/queue">
{{ctx.Locale.Tr "admin.monitor.queues"}} {{ctx.Locale.Tr "admin.monitor.queues"}}
</a> </a>
<a class="{{if .PageIsAdminMonitorStacktrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace"> <a class="{{if .PageIsAdminMonitorTrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace">
{{ctx.Locale.Tr "admin.monitor.stacktrace"}} {{ctx.Locale.Tr "admin.monitor.trace"}}
</a> </a>
</div> </div>
</details> </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>
<div> <div>
{{if or (eq .Process.Type "request") (eq .Process.Type "normal")}} {{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}} {{end}}
</div> </div>
</div> </div>

@ -1,22 +1,7 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}} {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
<div class="admin-setting-content"> <div class="admin-setting-content">
<div class="tw-flex tw-items-center"> {{template "admin/trace_tabs" .}}
<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>
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}} {{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}}
@ -34,15 +19,4 @@
{{end}} {{end}}
</div> </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" .}} {{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>
</div> </div>
{{if .PackageDescriptor.Metadata.Manifests}} {{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"> <div class="ui attached segment">
<table class="ui very basic compact table"> <table class="ui very basic compact table">
<thead> <thead>

@ -143,7 +143,7 @@
{{if .LatestPullRequest.HasMerged}} {{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> <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}} {{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}} {{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> <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}} {{end}}

@ -1,5 +1,6 @@
<button class="ui primary button js-btn-clone-panel"> <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"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button> </button>
<div class="clone-panel-popup tippy-target"> <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="ui message flex-text-block">
<div class="tw-flex-1"> <div class="tw-flex-1">
{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}} {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.UpstreamDivergingInfo.BaseBranchName|PathEscapeSegments)}}
{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}} {{$upstreamRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.BaseRepo.FullName .UpstreamDivergingInfo.BaseBranchName}}
{{if .UpstreamDivergingInfo.CommitsBehind}} {{$thisRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.FullName .BranchName}}
{{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}} {{$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}} {{else}}
{{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}} {{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}}
{{end}} {{end}}
@ -12,7 +14,7 @@
{{if .CanWriteCode}} {{if .CanWriteCode}}
<button class="ui compact primary button tw-m-0 link-action" <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-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}}"> data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}} {{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
</button> </button>

@ -42,7 +42,7 @@
{{if .HasMerged}} {{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> <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}} {{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}} {{else if .Issue.IsPull}}
{{if .IsPullWorkInProgress}} {{if .IsPullWorkInProgress}}
<div class="ui grey label issue-state-label">{{svg "octicon-git-pull-request-draft"}} {{ctx.Locale.Tr "repo.issues.draft_title"}}</div> <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 segments repository-summary tw-mt-1 tw-mb-0">
<div class="ui segment sub-menu repository-menu"> <div class="ui segment sub-menu repository-menu">
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}} {{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"}} {{svg "octicon-history"}} <b>{{ctx.Locale.PrettyNumber .CommitsCount}}</b> {{ctx.Locale.TrN .CommitsCount "repo.commit" "repo.commits"}}
</a> </a>
<a class="item muted {{if .PageIsBranches}}active{{end}}" href="{{.RepoLink}}/branches"> <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