mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-31 01:36:00 +02:00
Merge branch 'main' into Badge
This commit is contained in:
commit
4890a15467
@ -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)
|
||||||
|
12
flake.lock
generated
12
flake.lock
generated
@ -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) {
|
||||||
|
32
modules/gtprof/event.go
Normal file
32
modules/gtprof/event.go
Normal file
@ -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
|
||||||
|
}
|
175
modules/gtprof/trace.go
Normal file
175
modules/gtprof/trace.go
Normal file
@ -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
|
||||||
|
}
|
96
modules/gtprof/trace_builtin.go
Normal file
96
modules/gtprof/trace_builtin.go
Normal file
@ -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))
|
||||||
|
}
|
19
modules/gtprof/trace_const.go
Normal file
19
modules/gtprof/trace_const.go
Normal file
@ -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"
|
||||||
|
)
|
93
modules/gtprof/trace_test.go
Normal file
93
modules/gtprof/trace_test.go
Normal file
@ -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)
|
||||||
|
73
modules/tailmsg/talimsg.go
Normal file
73
modules/tailmsg/talimsg.go
Normal file
@ -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
|
||||||
|
minutes := (duration / 60) % 60
|
||||||
|
|
||||||
formattedTime := ""
|
formattedTime := ""
|
||||||
|
formattedTime = formatTime(hours, "hour", formattedTime)
|
||||||
// The following four variables are calculated by taking
|
formattedTime = formatTime(minutes, "minute", formattedTime)
|
||||||
// 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
|
|
||||||
seconds := duration % 60
|
|
||||||
|
|
||||||
// Extract only the relevant information of the time
|
|
||||||
// If the time is greater than a year, it makes no sense to display seconds.
|
|
||||||
switch {
|
|
||||||
case years > 0:
|
|
||||||
formattedTime = formatTime(years, "year", formattedTime)
|
|
||||||
formattedTime = formatTime(months, "month", formattedTime)
|
|
||||||
case months > 0:
|
|
||||||
formattedTime = formatTime(months, "month", formattedTime)
|
|
||||||
formattedTime = formatTime(weeks, "week", formattedTime)
|
|
||||||
case weeks > 0:
|
|
||||||
formattedTime = formatTime(weeks, "week", formattedTime)
|
|
||||||
formattedTime = formatTime(days, "day", formattedTime)
|
|
||||||
case days > 0:
|
|
||||||
formattedTime = formatTime(days, "day", formattedTime)
|
|
||||||
formattedTime = formatTime(hours, "hour", formattedTime)
|
|
||||||
case hours > 0:
|
|
||||||
formattedTime = formatTime(hours, "hour", formattedTime)
|
|
||||||
formattedTime = formatTime(minutes, "minute", formattedTime)
|
|
||||||
default:
|
|
||||||
formattedTime = formatTime(minutes, "minute", formattedTime)
|
|
||||||
formattedTime = formatTime(seconds, "second", formattedTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The formatTime() function always appends a space at the end. This will be trimmed
|
// 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,22 +6,29 @@ 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
|
||||||
|
@ -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=L’annulation d’un processus peut entraîner une perte de données.
|
monitor.process.cancel_desc=L’annulation d’un 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
908
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -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
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 {
|
||||||
ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
|
switch {
|
||||||
|
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
|
||||||
|
ctx.Error(http.StatusForbidden, "", "User must be a repo or site admin to rename default or protected branches.")
|
||||||
|
case errors.Is(err, git_model.ErrBranchIsProtected):
|
||||||
|
ctx.Error(http.StatusForbidden, "", "Branch is protected by glob-based protection rules.")
|
||||||
|
default:
|
||||||
|
ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
|
||||||
|
}
|
||||||
return
|
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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
18
routers/web/admin/perftrace.go
Normal file
18
routers/web/admin/perftrace.go
Normal file
@ -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,13 +18,14 @@ 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 {
|
||||||
Ctx map[string]any
|
Status, Size int
|
||||||
RequestID *string
|
}
|
||||||
|
Ctx map[string]any
|
||||||
|
RequestID *string
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyOfRequestIDInTemplate = ".RequestID"
|
const keyOfRequestIDInTemplate = ".RequestID"
|
||||||
@ -51,51 +52,65 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
|
|||||||
return requestID
|
return requestID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type accessLogRecorder struct {
|
||||||
|
logger log.BaseLogger
|
||||||
|
logTemplate *template.Template
|
||||||
|
needRequestID bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) {
|
||||||
|
var requestID string
|
||||||
|
if lr.needRequestID {
|
||||||
|
requestID = parseRequestIDFromRequestHeader(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
reqHost = req.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
identity := "-"
|
||||||
|
data := middleware.GetContextData(req.Context())
|
||||||
|
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
||||||
|
identity = signedUser.Name
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
tmplData := accessLoggerTmplData{
|
||||||
|
Identity: &identity,
|
||||||
|
Start: &start,
|
||||||
|
Ctx: map[string]any{
|
||||||
|
"RemoteAddr": req.RemoteAddr,
|
||||||
|
"RemoteHost": reqHost,
|
||||||
|
"Req": req,
|
||||||
|
},
|
||||||
|
RequestID: &requestID,
|
||||||
|
}
|
||||||
|
tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
|
||||||
|
tmplData.ResponseWriter.Size = respWriter.WrittenSize()
|
||||||
|
err = lr.logTemplate.Execute(buf, tmplData)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Could not execute access logger template: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// AccessLogger returns a middleware to log access logger
|
||||||
func AccessLogger() func(http.Handler) http.Handler {
|
func AccessLogger() func(http.Handler) http.Handler {
|
||||||
logger := log.GetLogger("access")
|
recorder := newAccessLogRecorder()
|
||||||
needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate)
|
|
||||||
logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate)
|
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
var requestID string
|
|
||||||
if needRequestID {
|
|
||||||
requestID = parseRequestIDFromRequestHeader(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
reqHost = req.RemoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, req)
|
next.ServeHTTP(w, req)
|
||||||
rw := w.(ResponseWriter)
|
recorder.record(start, w.(ResponseWriter), req)
|
||||||
|
|
||||||
identity := "-"
|
|
||||||
data := middleware.GetContextData(req.Context())
|
|
||||||
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
|
||||||
identity = signedUser.Name
|
|
||||||
}
|
|
||||||
buf := bytes.NewBuffer([]byte{})
|
|
||||||
err = logTemplate.Execute(buf, routerLoggerOptions{
|
|
||||||
req: req,
|
|
||||||
Identity: &identity,
|
|
||||||
Start: &start,
|
|
||||||
ResponseWriter: rw,
|
|
||||||
Ctx: map[string]any{
|
|
||||||
"RemoteAddr": req.RemoteAddr,
|
|
||||||
"RemoteHost": reqHost,
|
|
||||||
"Req": req,
|
|
||||||
},
|
|
||||||
RequestID: &requestID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Could not execute access logger template: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("%s", buf.String())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
71
services/context/access_log_test.go
Normal file
71
services/context/access_log_test.go
Normal file
@ -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))
|
Before(fn func(ResponseWriter))
|
||||||
|
WrittenSize() int
|
||||||
Status() int // used by access logger template
|
|
||||||
Size() int // used by access logger template
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch)
|
// Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo:
|
||||||
if err != nil {
|
// * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a`
|
||||||
return nil, err
|
// * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a`
|
||||||
|
info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch)
|
||||||
|
if err == nil {
|
||||||
|
return &UpstreamDivergingInfo{
|
||||||
|
BaseBranchName: forkBranch,
|
||||||
|
BaseBranchHasNewCommits: info.BaseHasNewCommits,
|
||||||
|
HeadBranchCommitsBehind: info.HeadCommitsBehind,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, repo.BaseRepo.DefaultBranch)
|
info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return nil, err
|
return &UpstreamDivergingInfo{
|
||||||
|
BaseBranchName: forkRepo.BaseRepo.DefaultBranch,
|
||||||
|
BaseBranchHasNewCommits: info.BaseHasNewCommits,
|
||||||
|
HeadBranchCommitsBehind: info.HeadCommitsBehind,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
info := &UpstreamDivergingInfo{}
|
|
||||||
if forkBranch.CommitID == baseBranch.CommitID {
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the fork repo has new commits, this call will fail because they are not in the base repo
|
|
||||||
// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
|
|
||||||
// so at the moment, we first check the update time, then check whether the fork branch has base's head
|
|
||||||
diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID)
|
|
||||||
if err != nil {
|
|
||||||
info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix
|
|
||||||
if info.BaseHasNewCommits {
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the base's update time is before the fork, check whether the base's head is in the fork
|
|
||||||
baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo.BaseRepo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
|
|
||||||
info.BaseHasNewCommits = !hasPreviousCommit
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead
|
|
||||||
return info, nil
|
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,7 @@
|
|||||||
<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices">
|
<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>
|
||||||
|
13
templates/admin/perftrace.tmpl
Normal file
13
templates/admin/perftrace.tmpl
Normal file
@ -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" .}}
|
||||||
|
19
templates/admin/trace_tabs.tmpl
Normal file
19
templates/admin/trace_tabs.tmpl
Normal file
@ -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>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<div class="markup"><pre class="code-block"><code><repositories>
|
<div class="markup"><pre class="code-block"><code><repositories>
|
||||||
<repository>
|
<repository>
|
||||||
<id>gitea</id>
|
<id>gitea</id>
|
||||||
<url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url>
|
<url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url>
|
||||||
</repository>
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user