0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-22 01:22:01 +02:00

Merge branch 'main' into lunny/refactor_org_setting

This commit is contained in:
Lunny Xiao 2025-07-08 19:00:39 -07:00
commit 8599143c7f
19 changed files with 265 additions and 135 deletions

View File

@ -5,12 +5,14 @@ package pull
import (
"context"
"errors"
"fmt"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
)
// AutoMerge represents a pull request scheduled for merging when checks succeed
@ -76,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
return false, nil, err
}
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
if errors.Is(err, util.ErrNotExist) {
doer, err = user_model.NewGhostUser(), nil
}
if err != nil {
return false, nil, err
}

View File

@ -2355,6 +2355,7 @@ settings.payload_url=URL spriocdhírithe
settings.http_method=Modh HTTP
settings.content_type=Cineál Ábhar POST
settings.secret=Rúnda
settings.webhook_secret_desc=Más féidir le freastalaí an webhook rún a úsáid, is féidir leat lámhleabhar an webhook a leanúint agus rún a líonadh isteach anseo.
settings.slack_username=Ainm úsáideora
settings.slack_icon_url=URL deilbhín
settings.slack_color=Dath

View File

@ -2353,6 +2353,7 @@ settings.payload_url=URL de destino
settings.http_method=Método HTTP
settings.content_type=Tipo de conteúdo POST
settings.secret=Segredo
settings.webhook_secret_desc=Se o servidor de automatismos web suportar a utilização de segredos, você pode seguir o manual do automatismo web e preencher um segredo aqui.
settings.slack_username=Nome de utilizador
settings.slack_icon_url=URL do ícone
settings.slack_color=Cor

View File

@ -1400,12 +1400,12 @@ editor.revert=将 %s 还原到:
editor.failed_to_commit=提交更改失败。
editor.failed_to_commit_summary=错误信息:
editor.fork_create=派生仓库发起请求变更
editor.fork_create_description=您不能直接编辑此仓库。您可以从此仓库派生,进行编辑并创建一个拉取请求。
editor.fork_edit_description=您不能直接编辑此仓库。 更改将写入您的派生仓库 <b>%s</b>,以便您可以创建一个拉取请求。
editor.fork_not_editable=你已经派生了这个仓库,但是你的分叉是不可编辑的。
editor.fork_create=派生仓库请求变更
editor.fork_create_description=您不能直接编辑此仓库。您可以派生此仓库,进行编辑并创建一个合并请求。
editor.fork_edit_description=您不能直接编辑此仓库。 更改将写入您的派生仓库 <b>%s</b>,以便您可以创建一个合并请求。
editor.fork_not_editable=您已经派生了此仓库,但您的派生是不可编辑的。
editor.fork_failed_to_push_branch=推送分支 %s 到仓库失败。
editor.fork_branch_exists=分支 "%s" 已存在于您的派生仓库中,请选择一个新的分支名称。
editor.fork_branch_exists=分支「%s」已存在于您的派生仓库中,请选择一个新的分支名称。
commits.desc=浏览代码修改历史
commits.commits=次代码提交
@ -2171,8 +2171,8 @@ settings.hooks=Web 钩子
settings.githooks=管理 Git 钩子
settings.basic_settings=基本设置
settings.mirror_settings=镜像设置
settings.mirror_settings.docs=设置您的仓库以自动同步另一个仓库的提交、标签和分支。
settings.mirror_settings.docs.disabled_pull_mirror.instructions=设置您的项目以自动将提交、标签和分支推送到另一个仓库。您的站点管理员已禁用了拉取镜像。
settings.mirror_settings.docs=将您的仓库设置为自动同步另一个仓库的提交、标签和分支。
settings.mirror_settings.docs.disabled_pull_mirror.instructions=将您的项目设置为自动将提交、标签和分支推送到另一个仓库。您的站点管理员已禁用了拉取镜像。
settings.mirror_settings.docs.disabled_push_mirror.instructions=将您的项目设置为自动从一个仓库拉取提交、标签和分支。
settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=现在,这只能在「迁移外部仓库」菜单中完成。欲了解更多信息,请参考:
settings.mirror_settings.docs.disabled_push_mirror.info=您的站点管理员已禁用推送镜像。
@ -2335,6 +2335,8 @@ settings.hooks_desc=当 Gitea 事件发生时Web 钩子自动发出 HTTP POST
settings.webhook_deletion=删除 Web 钩子
settings.webhook_deletion_desc=删除 Web 钩子将删除其设置和历史记录。继续?
settings.webhook_deletion_success=Web 钩子删除成功!
settings.webhook.test_delivery=测试推送事件
settings.webhook.test_delivery_desc=用假推送事件测试这个 Web 钩子。
settings.webhook.test_delivery_desc_disabled=要用假事件测试这个 Web钩子请激活它。
settings.webhook.request=请求内容
settings.webhook.response=响应内容
@ -2354,6 +2356,7 @@ settings.payload_url=目标 URL
settings.http_method=HTTP 方法
settings.content_type=POST 内容类型
settings.secret=密钥
settings.webhook_secret_desc=如果 Webhook 服务器支持使用密钥,您可以按照 Webhook 的手册在此处填写一个密钥。
settings.slack_username=服务名称
settings.slack_icon_url=图标 URL
settings.slack_color=颜色
@ -2768,6 +2771,8 @@ branch.new_branch_from=基于「%s」创建新分支
branch.renamed=分支 %s 已重命名为 %s。
branch.rename_default_or_protected_branch_error=只有管理员能重命名默认分支和受保护的分支。
branch.rename_protected_branch_failed=此分支受到 glob 语法规则的保护。
branch.commits_divergence_from=提交分歧:落后 %[3]s %[1]d 个提交,领先 %[2]d 个提交
branch.commits_no_divergence=与分支 %[1]s 相同
tag.create_tag=创建标签 %s
tag.create_tag_operation=创建标签
@ -2781,6 +2786,7 @@ topic.done=保存
topic.count_prompt=您最多选择25个主题
topic.format_prompt=主题必须以字母或数字开头,可以包含连字符 ('-') 和句点 ('.')长度不得超过35个字符。字符必须为小写。
find_file.follow_symlink=跟随此符号链接的指向位置
find_file.go_to_file=转到文件
find_file.no_matching=没有找到匹配的文件

View File

@ -22,23 +22,21 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/services/automergequeue"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
)
// prAutoMergeQueue represents a queue to handle update pull request tests
var prAutoMergeQueue *queue.WorkerPoolQueue[string]
// Init runs the task queue to that handles auto merges
func Init() error {
notify_service.RegisterNotifier(NewNotifier())
prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
if prAutoMergeQueue == nil {
automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
if automergequeue.AutoMergeQueue == nil {
return errors.New("unable to create pr_auto_merge queue")
}
go graceful.GetManager().RunWithCancel(prAutoMergeQueue)
go graceful.GetManager().RunWithCancel(automergequeue.AutoMergeQueue)
return nil
}
@ -56,24 +54,23 @@ func handler(items ...string) []string {
return nil
}
func addToQueue(pr *issues_model.PullRequest, sha string) {
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
}
}
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) {
err = db.WithTx(ctx, func(ctx context.Context) error {
if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil {
return err
}
scheduled = true
_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
return err
})
// Old code made "scheduled" to be true after "ScheduleAutoMerge", but it's not right:
// If the transaction rolls back, then the pull request is not scheduled to auto merge.
// So we should only set "scheduled" to true if there is no error.
scheduled = err == nil
if scheduled {
log.Trace("Pull request [%d] scheduled for auto merge with style [%s] and message [%s]", pull.ID, style, message)
automergequeue.StartPRCheckAndAutoMerge(ctx, pull)
}
return scheduled, err
}
@ -99,38 +96,12 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m
}
for _, pr := range pulls {
addToQueue(pr, sha)
automergequeue.AddToQueue(pr, sha)
}
return nil
}
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
return
}
if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
return
}
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}
addToQueue(pull, commitID)
}
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {

View File

@ -12,6 +12,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/services/automergequeue"
notify_service "code.gitea.io/gitea/services/notify"
)
@ -45,7 +46,7 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
return
}
// as reviews could have blocked a pending automerge let's recheck
StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
automergequeue.StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
}
func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {

View File

@ -0,0 +1,49 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package automergequeue
import (
"context"
"fmt"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
)
var AutoMergeQueue *queue.WorkerPoolQueue[string]
var AddToQueue = func(pr *issues_model.PullRequest, sha string) {
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
if err := AutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
}
}
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
return
}
if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
return
}
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}
AddToQueue(pull, commitID)
}

View File

@ -1,5 +1,4 @@
// Copyright 2019 The Gitea Authors.
// All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
@ -16,6 +15,7 @@ import (
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automergequeue"
notify_service "code.gitea.io/gitea/services/notify"
)
@ -238,7 +239,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
// and set to be either conflict or mergeable.
func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) {
// If status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
// If the status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
if pr.Status == issues_model.PullRequestStatusChecking {
pr.Status = issues_model.PullRequestStatusMergeable
}
@ -257,6 +258,16 @@ func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullReques
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
log.Error("Update[%-v]: %v", pr, err)
}
// if there is a scheduled merge for this pull request, start the auto merge check (again)
exist, _, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
if err != nil {
log.Error("GetScheduledMergeByPullID[%-v]: %v", pr, err)
return
} else if !exist {
return
}
automergequeue.StartPRCheckAndAutoMerge(ctx, pr)
}
// getMergeCommit checks if a pull request has been merged

View File

@ -1,5 +1,4 @@
// Copyright 2019 The Gitea Authors.
// All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
@ -11,11 +10,18 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/automergequeue"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPullRequest_AddToTaskQueue(t *testing.T) {
@ -63,6 +69,46 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
prPatchCheckerQueue.ShutdownWait(5 * time.Second)
prPatchCheckerQueue.ShutdownWait(time.Second)
prPatchCheckerQueue = nil
}
func TestMarkPullRequestAsMergeable(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", func(items ...string) []string { return nil })
go prPatchCheckerQueue.Run()
defer func() {
prPatchCheckerQueue.ShutdownWait(time.Second)
prPatchCheckerQueue = nil
}()
addToQueueShaChan := make(chan string, 1)
defer test.MockVariableValue(&automergequeue.AddToQueue, func(pr *issues_model.PullRequest, sha string) {
addToQueueShaChan <- sha
})()
ctx := t.Context()
_, _ = db.GetEngine(ctx).ID(2).Update(&issues_model.PullRequest{Status: issues_model.PullRequestStatusChecking})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
require.False(t, pr.HasMerged)
require.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
err := pull.ScheduleAutoMerge(ctx, &user_model.User{ID: 99999}, pr.ID, repo_model.MergeStyleMerge, "test msg", true)
require.NoError(t, err)
exist, scheduleMerge, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
require.NoError(t, err)
assert.True(t, exist)
assert.True(t, scheduleMerge.Doer.IsGhost())
markPullRequestAsMergeable(ctx, pr)
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
require.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
select {
case sha := <-addToQueueShaChan:
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", sha) // ref: refs/pull/3/head
case <-time.After(1 * time.Second):
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
}
}

View File

@ -2,13 +2,15 @@
<div class="page-content devtest ui container">
{{template "base/alert" .}}
<div class="modal-buttons flex-text-block tw-flex-wrap"></div>
<script type="module">
for (const el of $('.ui.modal:not([data-skip-button])')) {
const $btn = $('<button class="ui button">').text(`${el.id}`).on('click', () => {
$(el).modal({onApprove() {alert('confirmed')}}).modal('show');
});
$('.modal-buttons').append($btn);
}
<script>
document.addEventListener('gitea:index-ready', () => {
for (const el of $('.ui.modal:not([data-skip-button])')) {
const $btn = $('<button class="ui button">').text(`${el.id}`).on('click', () => {
$(el).modal({onApprove() {alert('confirmed')}}).modal('show');
});
$('.modal-buttons').append($btn);
}
});
</script>
<div id="test-modal-form-1" class="ui mini modal">

View File

@ -45,14 +45,16 @@
</div>
</li>
</ul>
<script type="module">
const $buttons = $('#devtest-button-samples').find('button.ui');
<script>
document.addEventListener('gitea:index-ready', () => {
const $buttons = $('#devtest-button-samples').find('button.ui');
const $buttonStyles = $('input[name*="button-style"]');
$buttonStyles.on('click', () => $buttonStyles.map((_ ,el) => $buttons.toggleClass(el.value, el.checked)));
const $buttonStyles = $('input[name*="button-style"]');
$buttonStyles.on('click', () => $buttonStyles.map((_, el) => $buttons.toggleClass(el.value, el.checked)));
const $buttonStates = $('input[name*="button-state"]');
$buttonStates.on('click', () => $buttonStates.map((_ ,el) => $buttons.prop(el.value, el.checked)));
const $buttonStates = $('input[name*="button-state"]');
$buttonStates.on('click', () => $buttonStates.map((_, el) => $buttons.prop(el.value, el.checked)));
});
</script>
</div>
</div>

View File

@ -16,7 +16,7 @@
<td class="author">
<div class="tw-flex">
{{$userName := .Author.Name}}
{{if .User}}
{{if and .User (gt .User.ID 0)}} /* User with id == 0 is a fake user from git author */
{{if and .User.FullName DefaultShowFullName}}
{{$userName = .User.FullName}}
{{end}}

View File

@ -2,35 +2,28 @@
{{if and .CanUseTimetracker (not .Repository.IsArchived)}}
<div class="divider"></div>
<div>
<div class="ui dropdown full-width jump">
<a class="fixed-text muted">
<div>
<strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong>
{{if $.IsStopwatchRunning}}{{svg "octicon-stopwatch"}}{{end}}
</div>
{{svg "octicon-gear"}}
</a>
<div class="menu">
<a class="item issue-set-time-estimate show-modal" data-modal="#issue-time-set-estimate-modal">
{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.time_estimate_set"}}
</a>
<div class="divider"></div>
{{if $.IsStopwatchRunning}}
<a class="item issue-stop-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/stop">
{{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_stop"}}
</a>
<a class="item issue-cancel-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/cancel">
{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_discard"}}
</a>
{{else}}
<a class="item issue-start-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/start">
{{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_start"}}
</a>
<a class="item issue-add-time show-modal" data-modal="#issue-time-manually-add-modal">
{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_manually_add"}}
</a>
{{end}}
</div>
<div class="flex-text-block">
<strong class="tw-flex-1">{{ctx.Locale.Tr "repo.issues.tracker"}}</strong>
<button class="btn interact-fg show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.time_estimate_set"}}" data-modal="#issue-time-set-estimate-modal">
{{svg "octicon-pencil"}}
</button>
</div>
<div class="ui buttons tw-mt-2 tw-w-full">
{{if $.IsStopwatchRunning}}
<button class="ui button tw-flex-1 issue-stop-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/stop">
{{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_stop"}}
</button>
<button class="ui icon button issue-cancel-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/cancel" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.timetracker_timer_discard"}}">
{{svg "octicon-trash"}}
</button>
{{else}}
<button class="ui button tw-flex-1 issue-start-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/start">
{{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_start"}}
</button>
<button class="ui icon button issue-add-time show-modal" data-modal="#issue-time-manually-add-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.timetracker_timer_manually_add"}}">
{{svg "octicon-plus"}}
</button>
{{end}}
</div>
{{if and (not $.IsStopwatchRunning) .HasUserStopwatch}}
@ -74,23 +67,19 @@
</div>
{{end}}
{{if .WorkingUsers}}
<div class="ui comments tw-mt-2">
<div class="tw-mt-2">
{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Hour)}}
<div>
{{range $user, $trackedtime := .WorkingUsers}}
<div class="comment tw-mt-2">
<a class="avatar">
{{ctx.AvatarUtils.Avatar $user}}
</a>
<div class="content">
{{template "shared/user/authorlink" $user}}
<div class="text">
{{$trackedtime|Sec2Hour}}
</div>
</div>
</div>
<div class="ui list flex-items-block">
{{range $user, $trackedtime := .WorkingUsers}}
<div class="item tw-gap-3">
{{template "shared/user/avatarlink" dict "user" $user}}
<div>
{{template "shared/user/authorlink" $user}}
<div class="text">{{$trackedtime|Sec2Hour}}</div>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
{{end}}

View File

@ -16,6 +16,7 @@ import (
"path/filepath"
"slices"
"strconv"
"strings"
"testing"
"time"
@ -489,40 +490,60 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
}
func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns, protectedFilePatterns string) func(t *testing.T) {
return doProtectBranchExt(ctx, branch, doProtectBranchOptions{
UserToWhitelistPush: userToWhitelistPush,
UserToWhitelistForcePush: userToWhitelistForcePush,
UnprotectedFilePatterns: unprotectedFilePatterns,
ProtectedFilePatterns: protectedFilePatterns,
})
}
type doProtectBranchOptions struct {
UserToWhitelistPush, UserToWhitelistForcePush, UnprotectedFilePatterns, ProtectedFilePatterns string
StatusCheckPatterns []string
}
func doProtectBranchExt(ctx APITestContext, ruleName string, opts doProtectBranchOptions) func(t *testing.T) {
// We are going to just use the owner to set the protection.
return func(t *testing.T) {
csrf := GetUserCSRFToken(t, ctx.Session)
formData := map[string]string{
"_csrf": csrf,
"rule_name": branch,
"unprotected_file_patterns": unprotectedFilePatterns,
"protected_file_patterns": protectedFilePatterns,
"rule_name": ruleName,
"unprotected_file_patterns": opts.UnprotectedFilePatterns,
"protected_file_patterns": opts.ProtectedFilePatterns,
}
if userToWhitelistPush != "" {
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush)
if opts.UserToWhitelistPush != "" {
user, err := user_model.GetUserByName(db.DefaultContext, opts.UserToWhitelistPush)
assert.NoError(t, err)
formData["whitelist_users"] = strconv.FormatInt(user.ID, 10)
formData["enable_push"] = "whitelist"
formData["enable_whitelist"] = "on"
}
if userToWhitelistForcePush != "" {
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush)
if opts.UserToWhitelistForcePush != "" {
user, err := user_model.GetUserByName(db.DefaultContext, opts.UserToWhitelistForcePush)
assert.NoError(t, err)
formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10)
formData["enable_force_push"] = "whitelist"
formData["enable_force_push_allowlist"] = "on"
}
if len(opts.StatusCheckPatterns) > 0 {
formData["enable_status_check"] = "on"
formData["status_check_contexts"] = strings.Join(opts.StatusCheckPatterns, "\n")
}
// Send the request to update branch protection settings
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData)
ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
// Check if master branch has been locked successfully
// Check if the "master" branch has been locked successfully
flashMsg := ctx.Session.GetCookieFlashMessage()
assert.Equal(t, `Branch protection for rule "`+branch+`" has been updated.`, flashMsg.SuccessMsg)
assert.Equal(t, `Branch protection for rule "`+ruleName+`" has been updated.`, flashMsg.SuccessMsg)
}
}
@ -688,6 +709,10 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
// automerge will merge immediately if the PR is mergeable and there is no "status check" because no status check also means "all checks passed"
// so we must set up a status check to test the auto merge feature
doProtectBranchExt(ctx, "protected", doProtectBranchOptions{StatusCheckPatterns: []string{"*"}})(t)
t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
t.Run("GenerateCommit", func(t *testing.T) {
@ -728,13 +753,13 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
// Cancel not existing auto merge
ctx.ExpectedCode = http.StatusNotFound
t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
t.Run("CancelAutoMergePRNotExist", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
// Add auto merge request
ctx.ExpectedCode = http.StatusCreated
t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
// Can not create schedule twice
// Cannot create schedule twice
ctx.ExpectedCode = http.StatusConflict
t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))

View File

@ -35,6 +35,7 @@ import (
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/automergequeue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
@ -727,7 +728,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
// add protected branch for commit status
csrf := GetUserCSRFToken(t, session)
// Change master branch to protected
// Change the "master" branch to "protected"
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"_csrf": csrf,
"rule_name": "master",
@ -737,10 +738,22 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
})
session.MakeRequest(t, req, http.StatusSeeOther)
oldAutoMergeAddToQueue := automergequeue.AddToQueue
addToQueueShaChan := make(chan string, 1)
automergequeue.AddToQueue = func(pr *issues_model.PullRequest, sha string) {
addToQueueShaChan <- sha
}
// first time insert automerge record, return true
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
assert.NoError(t, err)
assert.True(t, scheduled)
// and the pr should be added to automergequeue, in case it is already "mergeable"
select {
case <-addToQueueShaChan:
case <-time.After(time.Second):
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
}
automergequeue.AddToQueue = oldAutoMergeAddToQueue
// second time insert automerge record, return false because it does exist
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
@ -775,13 +788,11 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
})
assert.NoError(t, err)
time.Sleep(2 * time.Second)
// realod pr again
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.True(t, pr.HasMerged)
assert.Eventually(t, func() bool {
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
return pr.HasMerged
}, 2*time.Second, 100*time.Millisecond)
assert.NotEmpty(t, pr.MergedCommitID)
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
})
}

View File

@ -366,8 +366,8 @@ It needs some tricks to tweak the left/right borders with active state */
.ui.buttons .button {
border-right: none;
flex: 1 0 auto;
border-radius: 0;
flex-shrink: 0;
margin: 0;
}
.ui.buttons .button:first-child {

View File

@ -138,7 +138,14 @@ function initDiffHeaderPopup() {
btn.setAttribute('data-header-popup-initialized', '');
const popup = btn.nextElementSibling;
if (!popup?.matches('.tippy-target')) throw new Error('Popup element not found');
createTippy(btn, {content: popup, theme: 'menu', placement: 'bottom', trigger: 'click', interactive: true, hideOnClick: true});
createTippy(btn, {
content: popup,
theme: 'menu',
placement: 'bottom-end',
trigger: 'click',
interactive: true,
hideOnClick: true,
});
}
}

View File

@ -175,3 +175,5 @@ const initDur = performance.now() - initStartTime;
if (initDur > 500) {
console.error(`slow init functions took ${initDur.toFixed(3)}ms`);
}
document.dispatchEvent(new CustomEvent('gitea:index-ready'));

View File

@ -11,4 +11,5 @@ function initDevtestToast() {
}
}
// NOTICE: keep in mind that this file is not in "index.js", they do not share the same module system.
initDevtestToast();