0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-16 12:37:25 +02:00

Merge branch 'main' into lunny/giteabot

This commit is contained in:
Lunny Xiao 2026-05-04 08:05:57 -07:00 committed by GitHub
commit 80b366975f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 458 additions and 421 deletions

View File

@ -835,14 +835,14 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
} }
canDelete := false canDelete := false
allowMerge := false
canWriteToHeadRepo := false canWriteToHeadRepo := false
pull_service.StartPullRequestCheckOnView(ctx, pull) pull_service.StartPullRequestCheckOnView(ctx, pull)
if !prInfo.IsPullRequestBroken { if !prInfo.IsPullRequestBroken {
data.ShowUpdatePullInfo = pull.CommitsBehind > 0 && !issue.IsClosed && !pull.IsChecking() && !pull.IsFilesConflicted() && !prInfo.IsPullRequestBroken
var err error var err error
ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer) data.UpdateAllowed, data.UpdateByRebaseAllowed, err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer)
if err != nil { if err != nil {
ctx.ServerError("IsUserAllowedToUpdate", err) ctx.ServerError("IsUserAllowedToUpdate", err)
return return
@ -888,7 +888,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it
canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode) canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode)
} }
allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) data.allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
if err != nil { if err != nil {
ctx.ServerError("IsUserAllowedToMerge", err) ctx.ServerError("IsUserAllowedToMerge", err)
return return
@ -903,7 +903,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0) data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0)
data.ShowMergeInstructions = canWriteToHeadRepo data.ShowMergeInstructions = canWriteToHeadRepo
data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed
ctx.Data["AllowMerge"] = allowMerge ctx.Data["AllowMerge"] = data.allowMerge
pb := prInfo.ProtectedBranchRule pb := prInfo.ProtectedBranchRule
if pb != nil { if pb != nil {
@ -947,18 +947,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
data.AutodetectManualMerge = prConfig.AutodetectManualMerge data.AutodetectManualMerge = prConfig.AutodetectManualMerge
stillCanManualMerge := func() bool {
if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
return false
}
if pull.IsStatusMergeable() || pull.IsWorkInProgress(ctx) || pull.IsChecking() {
return false
}
return allowMerge && prConfig.AllowManualMerge
}
ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
enableStatusCheck := pb != nil && pb.EnableStatusCheck enableStatusCheck := pb != nil && pb.EnableStatusCheck
ctx.Data["EnableStatusCheck"] = enableStatusCheck ctx.Data["EnableStatusCheck"] = enableStatusCheck
@ -989,6 +977,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData
prInfo.prepareMergeBoxFormProps(ctx) prInfo.prepareMergeBoxFormProps(ctx)
prInfo.prepareMergeBoxIconColor()
} }
func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {

View File

@ -266,8 +266,15 @@ type pullMergeBoxData struct {
ShowMergeBox bool ShowMergeBox bool
ReloadingInterval int ReloadingInterval int
TimelineIconClass string
HasOverridableBlockers bool HasOverridableBlockers bool
CanMergeNow bool CanMergeNow bool // PR is mergeable, either no blocker, or doer is admin and can bypass the blockers
allowMerge bool // doer has permission to merge
ShowUpdatePullInfo bool
UpdateAllowed bool
UpdateByRebaseAllowed bool
MergeFormProps map[string]any MergeFormProps map[string]any
ShowPullCommands bool ShowPullCommands bool
@ -302,6 +309,9 @@ type pullRequestViewInfo struct {
StatusCheckData *pullCommitStatusCheckData StatusCheckData *pullCommitStatusCheckData
CommitStatuses []*git_model.CommitStatus CommitStatuses []*git_model.CommitStatus
MergeBoxData *pullMergeBoxData MergeBoxData *pullMergeBoxData
enableStatusCheck bool
workInProgressPrefix string
} }
func newPullRequestViewInfo() *pullRequestViewInfo { func newPullRequestViewInfo() *pullRequestViewInfo {
@ -430,8 +440,8 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c
} }
pb := prInfo.ProtectedBranchRule pb := prInfo.ProtectedBranchRule
enableStatusCheck := pb != nil && pb.EnableStatusCheck prInfo.enableStatusCheck = pb != nil && pb.EnableStatusCheck
if !enableStatusCheck { if !prInfo.enableStatusCheck {
return return
} }
@ -549,9 +559,10 @@ func (prInfo *pullRequestViewInfo) prepareViewOpenPullInfo(ctx *context.Context)
} }
// this one is used by both sidebar and merge-box // this one is used by both sidebar and merge-box
prInfo.workInProgressPrefix = pull.GetWorkInProgressPrefix(ctx)
if pull.IsWorkInProgress(ctx) { if pull.IsWorkInProgress(ctx) {
ctx.Data["IsPullWorkInProgress"] = true ctx.Data["IsPullWorkInProgress"] = prInfo.workInProgressPrefix != ""
ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx) ctx.Data["WorkInProgressPrefix"] = prInfo.workInProgressPrefix
} }
} }

View File

@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
func (prInfo *pullRequestViewInfo) prepareMergeBoxIconColor() {
pull := prInfo.issue.PullRequest
mergeBoxData := prInfo.MergeBoxData
statusCheckData := prInfo.StatusCheckData
switch {
case pull.HasMerged:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-purple"
case prInfo.issue.IsClosed, prInfo.workInProgressPrefix != "", pull.IsFilesConflicted():
prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light"
case prInfo.IsPullRequestBroken, mergeBoxData.isBlockedByApprovals, mergeBoxData.isBlockedByRejection,
mergeBoxData.isBlockedByOfficialReviewRequests, mergeBoxData.isBlockedByOutdatedBranch, mergeBoxData.isBlockedByChangedProtectedFiles:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red"
case prInfo.enableStatusCheck && (statusCheckData.RequiredChecksState.IsFailure() || statusCheckData.RequiredChecksState.IsError()):
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red"
case prInfo.enableStatusCheck && (statusCheckData.LatestCommitStatus == nil || statusCheckData.RequiredChecksState.IsPending() || statusCheckData.RequiredChecksState.IsWarning()):
prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow"
case mergeBoxData.allowMerge && mergeBoxData.requireSigned && !mergeBoxData.willSign:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red"
case pull.IsChecking():
prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow"
case pull.IsEmpty():
prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light"
case pull.IsStatusMergeable():
prInfo.MergeBoxData.TimelineIconClass = "tw-text-green"
default:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red"
}
}

View File

@ -16,6 +16,13 @@ import (
func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context) { func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context) {
pull := prInfo.issue.PullRequest pull := prInfo.issue.PullRequest
if pull.HasMerged || prInfo.issue.IsClosed {
return
}
if !prInfo.MergeBoxData.allowMerge {
return
}
prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
// Check correct values and select default // Check correct values and select default
@ -69,7 +76,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
} }
allOverridableChecksOk := !prInfo.MergeBoxData.HasOverridableBlockers allOverridableChecksOk := !prInfo.MergeBoxData.HasOverridableBlockers
prInfo.MergeBoxData.MergeFormProps = map[string]any{ mergeFormProps := map[string]any{
"baseLink": prInfo.issue.Link(), "baseLink": prInfo.issue.Link(),
"textCancel": ctx.Locale.Tr("cancel"), "textCancel": ctx.Locale.Tr("cancel"),
"textDeleteBranch": ctx.Locale.Tr("repo.branch.delete", prInfo.headTarget), "textDeleteBranch": ctx.Locale.Tr("repo.branch.delete", prInfo.headTarget),
@ -97,51 +104,75 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
// if this pr can be merged now, then hide the auto merge // if this pr can be merged now, then hide the auto merge
generalHideAutoMerge := prInfo.MergeBoxData.CanMergeNow && allOverridableChecksOk generalHideAutoMerge := prInfo.MergeBoxData.CanMergeNow && allOverridableChecksOk
prInfo.MergeBoxData.MergeFormProps["mergeStyles"] = []any{ var mergeStyles []any
map[string]any{ if pull.IsStatusMergeable() {
"name": "merge", mergeStyles = []any{
"allowed": prConfig.AllowMerge, map[string]any{
"textDoMerge": ctx.Locale.Tr("repo.pulls.merge_pull_request"), "name": "merge",
"mergeTitleFieldText": defaultMergeTitle, "allowed": prConfig.AllowMerge,
"mergeMessageFieldText": defaultMergeBody, "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_pull_request"),
"hideAutoMerge": generalHideAutoMerge, "mergeTitleFieldText": defaultMergeTitle,
}, "mergeMessageFieldText": defaultMergeBody,
map[string]any{ "hideAutoMerge": generalHideAutoMerge,
"name": "rebase", },
"allowed": prConfig.AllowRebase, map[string]any{
"textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_pull_request"), "name": "rebase",
"hideMergeMessageTexts": true, "allowed": prConfig.AllowRebase,
"hideAutoMerge": generalHideAutoMerge, "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_pull_request"),
}, "hideMergeMessageTexts": true,
map[string]any{ "hideAutoMerge": generalHideAutoMerge,
"name": "rebase-merge", },
"allowed": prConfig.AllowRebaseMerge, map[string]any{
"textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_commit_pull_request"), "name": "rebase-merge",
"mergeTitleFieldText": defaultMergeTitle, "allowed": prConfig.AllowRebaseMerge,
"mergeMessageFieldText": defaultMergeBody, "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_commit_pull_request"),
"hideAutoMerge": generalHideAutoMerge, "mergeTitleFieldText": defaultMergeTitle,
}, "mergeMessageFieldText": defaultMergeBody,
map[string]any{ "hideAutoMerge": generalHideAutoMerge,
"name": "squash", },
"allowed": prConfig.AllowSquash, map[string]any{
"textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"), "name": "squash",
"mergeTitleFieldText": defaultSquashMergeTitle, "allowed": prConfig.AllowSquash,
"mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody, "textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"),
"hideAutoMerge": generalHideAutoMerge, "mergeTitleFieldText": defaultSquashMergeTitle,
}, "mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody,
map[string]any{ "hideAutoMerge": generalHideAutoMerge,
"name": "fast-forward-only", },
"allowed": prConfig.AllowFastForwardOnly && pull.CommitsBehind == 0, map[string]any{
"textDoMerge": ctx.Locale.Tr("repo.pulls.fast_forward_only_merge_pull_request"), "name": "fast-forward-only",
"hideMergeMessageTexts": true, "allowed": prConfig.AllowFastForwardOnly && pull.CommitsBehind == 0,
"hideAutoMerge": generalHideAutoMerge, "textDoMerge": ctx.Locale.Tr("repo.pulls.fast_forward_only_merge_pull_request"),
}, "hideMergeMessageTexts": true,
map[string]any{ "hideAutoMerge": generalHideAutoMerge,
},
}
}
canUseManualMerge := func() bool {
if pull.IsWorkInProgress(ctx) || pull.IsChecking() {
return false
}
return prConfig.AllowManualMerge
}
// Manually Merged is not a well-known feature, it is used to mark a non-mergeable PR (already merged, conflicted) as merged
// To test it:
// Enable "Manually Merged" feature in the Repository Settings
// Create a pull request, either:
// - Merge the pull request branch locally and push the merged commit to Gitea
// - Make some conflicts between the base branch and the pull request branch
// Then the Manually Merged form will be shown in the merge form
if canUseManualMerge() {
mergeStyles = append(mergeStyles, map[string]any{
"name": "manually-merged", "name": "manually-merged",
"allowed": prConfig.AllowManualMerge, "allowed": prConfig.AllowManualMerge,
"textDoMerge": ctx.Locale.Tr("repo.pulls.merge_manually"), "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_manually"),
"hideMergeMessageTexts": true, "hideMergeMessageTexts": true,
"hideAutoMerge": true, "hideAutoMerge": true,
}, })
}
if len(mergeStyles) > 0 {
mergeFormProps["mergeStyles"] = mergeStyles
prInfo.MergeBoxData.MergeFormProps = mergeFormProps
} }
} }

View File

@ -228,6 +228,24 @@ func checkJobsOfCurrentRunAttempt(ctx context.Context, run *actions_model.Action
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
// The resolver below only considers needs and job-level concurrency, so a run blocked
// solely by run-level concurrency would have its jobs unblocked here. checkRunConcurrency
// re-evaluates when the holding run finishes.
if run.Status.IsBlocked() {
attempt, has, err := run.GetLatestAttempt(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("GetLatestAttempt: %w", err)
}
if has {
shouldBlock, err := shouldBlockRunByConcurrency(ctx, attempt)
if err != nil {
return nil, nil, nil, fmt.Errorf("shouldBlockRunByConcurrency: %w", err)
}
if shouldBlock {
return jobs, nil, nil, nil
}
}
}
vars, err := actions_model.GetVariablesOfRun(ctx, run) vars, err := actions_model.GetVariablesOfRun(ctx, run)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err

View File

@ -228,3 +228,68 @@ func Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck(t *testing.T) {
assert.Equal(t, jobBBlocked.ID, jobs[0].ID) assert.Equal(t, jobBBlocked.ID, jobs[0].ID)
} }
} }
// Test_checkJobsOfCurrentRunAttempt_RunLevelConcurrencyKeepsJobsBlocked verifies that
// the resolver does not transition a job out of Blocked while another run still holds
// the workflow-level concurrency group. Regression for #37446.
func Test_checkJobsOfCurrentRunAttempt_RunLevelConcurrencyKeepsJobsBlocked(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
const group = "test-run-level-concurrency-keeps-blocked"
// Holder run: Running attempt in the concurrency group.
holderRun := &actions_model.ActionRun{
RepoID: 4, OwnerID: 1, TriggerUserID: 1,
WorkflowID: "test.yml", Index: 9911, Ref: "refs/heads/main",
Status: actions_model.StatusRunning,
}
assert.NoError(t, db.Insert(ctx, holderRun))
holderAttempt := &actions_model.ActionRunAttempt{
RepoID: 4, RunID: holderRun.ID, Attempt: 1,
Status: actions_model.StatusRunning, ConcurrencyGroup: group,
}
assert.NoError(t, db.Insert(ctx, holderAttempt))
_, err := db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", holderAttempt.ID, holderRun.ID)
assert.NoError(t, err)
// Blocked run: Blocked attempt in the same group, with one Blocked job that has
// no needs and no job-level concurrency. Without the run-level guard in
// checkJobsOfCurrentRunAttempt, the resolver would transition this job to Waiting.
blockedRun := &actions_model.ActionRun{
RepoID: 4, OwnerID: 1, TriggerUserID: 1,
WorkflowID: "test.yml", Index: 9912, Ref: "refs/heads/main",
Status: actions_model.StatusBlocked,
}
assert.NoError(t, db.Insert(ctx, blockedRun))
blockedAttempt := &actions_model.ActionRunAttempt{
RepoID: 4, RunID: blockedRun.ID, Attempt: 1,
Status: actions_model.StatusBlocked, ConcurrencyGroup: group,
}
assert.NoError(t, db.Insert(ctx, blockedAttempt))
_, err = db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", blockedAttempt.ID, blockedRun.ID)
assert.NoError(t, err)
blockedRun.LatestAttemptID = blockedAttempt.ID
blockedJob := &actions_model.ActionRunJob{
RunID: blockedRun.ID, RunAttemptID: blockedAttempt.ID, AttemptJobID: 1,
RepoID: 4, OwnerID: 1, JobID: "job1", Name: "job1",
Status: actions_model.StatusBlocked,
WorkflowPayload: []byte(`
name: test
on: push
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo
`),
}
assert.NoError(t, db.Insert(ctx, blockedJob))
_, updated, _, err := checkJobsOfCurrentRunAttempt(ctx, blockedRun)
assert.NoError(t, err)
assert.Empty(t, updated)
refreshed := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: blockedJob.ID})
assert.Equal(t, actions_model.StatusBlocked, refreshed.Status)
}

View File

@ -12,7 +12,7 @@
{{ctx.ScriptImport "js/index.js" "module"}} {{ctx.ScriptImport "js/index.js" "module"}}
{{template "custom/footer" .}} {{template "custom/footer" .}}
<script nonce="{{ctx.CspScriptNonce}}" type="module"> <script nonce="{{ctx.CspScriptNonce}}" type="module">
if (!window.config?.frontendInited) alert("Frontend is not initialized, check console errors or asset files.") if (!window.config?.frontendInited && window.config?.runModeIsProd) alert("Frontend is not initialized, check console errors or asset files.");
</script> </script>
</body> </body>
</html> </html>

View File

@ -101,26 +101,27 @@
<div class="item">item 2</div> <div class="item">item 2</div>
</div> </div>
</div> </div>
<h3>Flex List (with "ui segment fitted")</h3> <h3>Flex List (with "ui segment fitted", items have their own padding)</h3>
<div class="ui attached segment fitted"> <div class="ui fitted segment">
<div class="flex-divided-list"> <div class="flex-divided-list items-px-default">
<div class="item">item 1</div> <div class="item">item 1</div>
<div class="item">item 2</div> <div class="item">item 2</div>
<div class="item">item 3</div>
</div> </div>
</div> </div>
<h3>If parent provides border or padding:</h3> <h3>If parent provides padding or items need their own flex and/or padding:</h3>
<div class="container-segmented tw-border tw-border-secondary"> <div class="tw-border tw-border-secondary">
<div class="tw-m-3">before divider</div> <div class="tw-m-3">before divider</div>
<div class="divider"></div> <div class="divider"></div>
<div class="flex-divided-list"> <div class="flex-divided-list flex-items-block items-px-default">
<div class="item">item 1</div> <div class="item">item 1</div>
<div class="item">item 2</div> <div class="item">item 2</div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="tw-m-3">after divider</div> <div class="tw-m-3">after divider</div>
</div> </div>
<div class="container-padded tw-border tw-border-secondary tw-p-4 tw-my-2"> <div class="tw-border tw-border-secondary tw-p-4 tw-my-2">
<div>before divider</div> <div>before divider</div>
<div class="divider"></div> <div class="divider"></div>
<div class="flex-divided-list"> <div class="flex-divided-list">

View File

@ -9,6 +9,8 @@
</span> </span>
{{end}} {{end}}
<div class="tippy-target"> <div class="tippy-target">
{{template "repo/pulls/status" (dict "CommitStatuses" .Statuses "CommitStatus" .Status)}} <div class="flex-divided-list items-px-default">
{{template "repo/pulls/status_items" (dict "CommitStatuses" .Statuses)}}
</div>
</div> </div>
{{end}} {{end}}

View File

@ -9,36 +9,17 @@
> >
{{$statusCheckData := .StatusCheckData}} {{$statusCheckData := .StatusCheckData}}
{{$requiredStatusCheckState := $statusCheckData.RequiredChecksState}} {{$requiredStatusCheckState := $statusCheckData.RequiredChecksState}}
<div class="timeline-avatar {{if .Issue.PullRequest.HasMerged}}tw-text-purple <div class="timeline-avatar {{$data.TimelineIconClass}}">{{svg "octicon-git-merge" 40}}</div>
{{- else if .Issue.IsClosed}}tw-text-text-light
{{- else if .IsPullWorkInProgress}}tw-text-text-light
{{- else if .IsFilesConflicted}}tw-text-text-light
{{- else if .IsPullRequestBroken}}tw-text-red
{{- else if .IsBlockedByApprovals}}tw-text-red
{{- else if .IsBlockedByRejection}}tw-text-red
{{- else if .IsBlockedByOfficialReviewRequests}}tw-text-red
{{- else if .IsBlockedByOutdatedBranch}}tw-text-red
{{- else if .IsBlockedByChangedProtectedFiles}}tw-text-red
{{- else if and .EnableStatusCheck (or $requiredStatusCheckState.IsFailure $requiredStatusCheckState.IsError)}}tw-text-red
{{- else if and .EnableStatusCheck (or (not $.LatestCommitStatus) $requiredStatusCheckState.IsPending $requiredStatusCheckState.IsWarning)}}tw-text-yellow
{{- else if and .AllowMerge .RequireSigned (not .WillSign)}}tw-text-red
{{- else if .Issue.PullRequest.IsChecking}}tw-text-yellow
{{- else if .Issue.PullRequest.IsEmpty}}tw-text-text-light
{{- else if .Issue.PullRequest.IsStatusMergeable}}tw-text-green
{{- else}}tw-text-red{{end}}">{{svg "octicon-git-merge" 40}}</div>
<div class="content"> <div class="content">
{{if .LatestCommitStatus}} <div class="ui segment fitted avatar-content-left-arrow">
<div class="ui attached segment fitted"> <div class="merge-section flex-divided-list flex-items-block items-px-default">
{{template "repo/pulls/status" (dict {{if .LatestCommitStatus}}
"CommitStatus" .LatestCommitStatus {{template "repo/issue/view_content/pull_merge_status_checks" (dict
"CommitStatuses" .LatestCommitStatuses "CommitStatuses" .LatestCommitStatuses
"ShowHideChecks" true "StatusCheckData" $statusCheckData
"StatusCheckData" $statusCheckData )}}
)}} {{end}}
</div>
{{end}}
{{$showGeneralMergeForm := false}}
<div class="ui attached segment merge-section {{if not $.LatestCommitStatus}}avatar-content-left-arrow{{end}} flex-items-block">
{{if .Issue.PullRequest.HasMerged}} {{if .Issue.PullRequest.HasMerged}}
{{if .IsPullBranchDeletable}} {{if .IsPullBranchDeletable}}
<div class="item item-section text tw-flex-1"> <div class="item item-section text tw-flex-1">
@ -78,7 +59,7 @@
{{svg "octicon-x"}} {{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.files_conflicted"}} {{ctx.Locale.Tr "repo.pulls.files_conflicted"}}
</div> </div>
<ul> <ul class="item">
{{range .ConflictedFiles}} {{range .ConflictedFiles}}
<li>{{.}}</li> <li>{{.}}</li>
{{else}} {{else}}
@ -113,7 +94,7 @@
{{svg "octicon-alert"}} {{svg "octicon-alert"}}
{{ctx.Locale.Tr "repo.pulls.is_ancestor"}} {{ctx.Locale.Tr "repo.pulls.is_ancestor"}}
</div> </div>
{{else if or .Issue.PullRequest.IsStatusMergeable .Issue.PullRequest.IsEmpty}} {{else}}
{{if .IsBlockedByApprovals}} {{if .IsBlockedByApprovals}}
<div class="item"> <div class="item">
{{svg "octicon-x"}} {{svg "octicon-x"}}
@ -143,7 +124,7 @@
{{svg "octicon-x"}} {{svg "octicon-x"}}
{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}} {{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}}
</div> </div>
<ul> <ul class="item">
{{range .ChangedProtectedFiles}} {{range .ChangedProtectedFiles}}
<li>{{.}}</li> <li>{{.}}</li>
{{end}} {{end}}
@ -167,6 +148,15 @@
{{svg "octicon-unlock"}} {{svg "octicon-unlock"}}
{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason)}} {{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason)}}
</div> </div>
{{else if not .Issue.PullRequest.IsStatusMergeable}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_desc"}}
</div>
<div class="item">
{{svg "octicon-info"}}
{{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_helper"}}
</div>
{{end}} {{end}}
{{$notAllOverridableChecksOk := $data.HasOverridableBlockers}} {{$notAllOverridableChecksOk := $data.HasOverridableBlockers}}
@ -197,119 +187,47 @@
{{end}} {{end}}
{{end}} {{end}}
{{template "repo/issue/view_content/update_branch_by_merge" $}}
{{if .Issue.PullRequest.IsEmpty}} {{if .Issue.PullRequest.IsEmpty}}
<div class="divider"></div> <div class="item">
<div class="item"> {{svg "octicon-alert"}}
{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.pulls.is_empty"}}
{{ctx.Locale.Tr "repo.pulls.is_empty"}} </div>
</div>
{{end}} {{end}}
{{if .AllowMerge}} {{/* user is allowed to merge */}} {{template "repo/issue/view_content/update_branch_by_merge" (dict "MergeBoxData" $data "Issue" $.Issue)}}
{{if $data.MergeFormProps}}
<div class="divider"></div> {{if not .AllowMerge}} {{/* user is allowed to merge */}}
{{$showGeneralMergeForm = true}}
{{/* The merge form is a Vue component. After mounted, it has a button for choosing merge style, so make it have min-height to avoid layout shifting */}}
<div id="pull-request-merge-form" class="tw-min-h-[40px]" data-merge-form-props="{{JsonUtils.EncodeToString $data.MergeFormProps}}"></div>
{{else}}
{{/* no merge style was set in repo setting: not or ($prUnit.PullRequestsConfig.AllowMerge ...) */}}
<div class="divider"></div>
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.no_merge_desc"}}
</div>
<div class="item">
{{svg "octicon-info"}}
{{ctx.Locale.Tr "repo.pulls.no_merge_helper"}}
</div>
{{end}} {{/* end if the repo was set to use any merge style */}}
{{else}}
{{/* user is not allowed to merge */}} {{/* user is not allowed to merge */}}
<div class="divider"></div>
<div class="item"> <div class="item">
{{svg "octicon-info"}} {{svg "octicon-info"}}
{{ctx.Locale.Tr "repo.pulls.no_merge_access"}} {{ctx.Locale.Tr "repo.pulls.no_merge_access"}}
</div> </div>
{{end}} {{/* end if user is allowed to merge or not */}}
{{else}}
{{/* Merge conflict without specific file. Suggest manual merge, only if all reviews and status checks OK. */}}
{{if .IsBlockedByApprovals}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .ProtectedBranch.RequiredApprovals}}
</div>
{{else if .IsBlockedByRejection}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_rejection"}}
</div>
{{else if .IsBlockedByOfficialReviewRequests}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_official_review_requests"}}
</div>
{{else if .IsBlockedByOutdatedBranch}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_outdated_branch"}}
</div>
{{else if .IsBlockedByChangedProtectedFiles}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}}
</div>
<ul>
{{range .ChangedProtectedFiles}}
<li>{{.}}</li>
{{end}}
</ul>
{{else if and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess)}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.required_status_check_failed"}}
</div>
{{else if and .RequireSigned (not .WillSign)}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.require_signed_wont_sign"}}
</div>
{{else}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_desc"}}
</div>
<div class="item">
{{svg "octicon-info"}}
{{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_helper"}}
</div>
{{end}} {{end}}
{{end}}{{/* end if: pull request status */}} {{end}}{{/* end if: pull request status */}}
{{/* Manually Merged is not a well-known feature, it is used to mark a non-mergeable PR (already merged, conflicted) as merged {{if $data.MergeFormProps}}
To test it: {{/* The merge form is a Vue component. After mounted, it has a button for choosing merge style, so make it have min-height to avoid layout shifting */}}
* Enable "Manually Merged" feature in the Repository Settings <div class="item">
* Create a pull request, either: <div id="pull-request-merge-form" class="tw-min-h-[40px] tw-w-full" data-merge-form-props="{{JsonUtils.EncodeToString $data.MergeFormProps}}"></div>
* - Merge the pull request branch locally and push the merged commit to Gitea </div>
* - Make some conflicts between the base branch and the pull request branch {{else if and .AllowMerge .Issue.PullRequest.IsStatusMergeable}}
* Then the Manually Merged form will be shown in the merge form {{/* no merge style was set in repo setting */}}
*/}} <div class="item tw-text-red">
{{if and $.StillCanManualMerge (not $showGeneralMergeForm)}} {{svg "octicon-x"}}
<div class="divider"></div> {{ctx.Locale.Tr "repo.pulls.no_merge_desc"}}
<form class="ui form form-fetch-action" action="{{.Issue.Link}}/merge" method="post">{{/* another similar form is in PullRequestMergeForm.vue*/}} </div>
<div class="field"> <div class="item">
<input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}"> {{svg "octicon-info"}}
</div> {{ctx.Locale.Tr "repo.pulls.no_merge_helper"}}
<button class="ui red button" type="submit" name="do" value="manually-merged"> </div>
{{ctx.Locale.Tr "repo.pulls.merge_manually"}}
</button>
</form>
{{end}} {{end}}
{{if $data.ShowPullCommands}} {{if $data.ShowPullCommands}}
{{template "repo/issue/view_content/pull_merge_instruction" dict "PullRequest" .Issue.PullRequest "MergeBoxData" $data}} <div class="item">
{{template "repo/issue/view_content/pull_merge_instruction" dict "PullRequest" .Issue.PullRequest "MergeBoxData" $data}}
</div>
{{end}} {{end}}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,5 @@
{{$data := $.MergeBoxData}} {{$data := $.MergeBoxData}}
{{$pull := $.PullRequest}} {{$pull := $.PullRequest}}
<div class="divider"></div>
<details> <details>
<summary>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}}</summary> <summary>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}}</summary>
<div class="tw-mt-2"> <div class="tw-mt-2">

View File

@ -0,0 +1,35 @@
{{/* Template Attributes:
* CommitStatuses: all commit status elements
* StatusCheckData: additional status check data, see backend pullCommitStatusCheckData struct
*/}}
{{$commitStatuses := $.CommitStatuses}}
{{$statusCheckData := $.StatusCheckData}}
{{if $statusCheckData}}
<div class="item flex-left-right commit-status-toggle">
<div>{{$statusCheckData.CommitStatusCheckPrompt ctx.Locale}}</div>
<button data-global-click="onCommitStatusChecksToggle" class="btn interact-fg"
data-show-all="{{ctx.Locale.Tr "repo.pulls.status_checks_show_all"}}"
data-hide-all="{{ctx.Locale.Tr "repo.pulls.status_checks_hide_all"}}"
>{{ctx.Locale.Tr "repo.pulls.status_checks_hide_all"}}</button>
</div>
{{if $statusCheckData.RequireApprovalRunCount}}
<div class="item flex-left-right" id="approve-status-checks">
<div>
<strong>{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals" $statusCheckData.RequireApprovalRunCount}}</strong>
<p>{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals_helper"}}</p>
</div>
{{if $statusCheckData.CanApprove}}
<button class="ui basic button link-action" data-url="{{$statusCheckData.ApproveLink}}">
{{ctx.Locale.Tr "repo.pulls.status_checks_approve_all"}}
</button>
{{end}}
</div>
{{end}}
<div class="item tw-p-0">
<div class="commit-status-list flex-divided-list items-px-default">
{{template "repo/pulls/status_items" (dict "CommitStatuses" $commitStatuses "StatusCheckData" $statusCheckData)}}
</div>
</div>
{{end}}

View File

@ -1,31 +1,30 @@
{{if and (gt $.Issue.PullRequest.CommitsBehind 0) (not $.Issue.IsClosed) (not $.Issue.PullRequest.IsChecking) (not $.IsPullFilesConflicted) (not $.IsPullRequestBroken)}} {{$data := $.MergeBoxData}}
<div class="divider"></div> {{$issue := $.Issue}}
<div class="item item-section"> {{if $data.ShowUpdatePullInfo}}
<div class="item-section-left flex-text-inline"> <div class="item flex-left-right">
<div class="flex-text-block">
{{svg "octicon-alert"}} {{svg "octicon-alert"}}
{{ctx.Locale.Tr "repo.pulls.outdated_with_base_branch"}} {{ctx.Locale.Tr "repo.pulls.outdated_with_base_branch"}}
</div> </div>
<div class="item-section-right"> <div>
{{if and $.UpdateAllowed $.UpdateByRebaseAllowed}} {{if and $data.UpdateAllowed $data.UpdateByRebaseAllowed}}
<div class="tw-inline-block"> <div id="update-pr-branch-with-base" class="ui buttons">
<div id="update-pr-branch-with-base" class="ui buttons"> <button class="ui button" data-do="{{$issue.Link}}/update">
<button class="ui button" data-do="{{$.Issue.Link}}/update"> <span class="button-text">
<span class="button-text"> {{ctx.Locale.Tr "repo.pulls.update_branch"}}
{{ctx.Locale.Tr "repo.pulls.update_branch"}} </span>
</span> </button>
</button> <div class="ui dropdown icon button">
<div class="ui dropdown icon button"> {{svg "octicon-triangle-down"}}
{{svg "octicon-triangle-down"}} <div class="menu">
<div class="menu"> <a class="item active selected" data-do="{{$issue.Link}}/update">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</a>
<a class="item active selected" data-do="{{$.Issue.Link}}/update">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</a> <a class="item" data-do="{{$issue.Link}}/update?style=rebase">{{ctx.Locale.Tr "repo.pulls.update_branch_rebase"}}</a>
<a class="item" data-do="{{$.Issue.Link}}/update?style=rebase">{{ctx.Locale.Tr "repo.pulls.update_branch_rebase"}}</a>
</div>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{end}}
{{if and $.UpdateAllowed (not $.UpdateByRebaseAllowed)}} {{if and $data.UpdateAllowed (not $data.UpdateByRebaseAllowed)}}
<form action="{{$.Issue.Link}}/update" method="post" class="ui update-branch-form"> <form action="{{$issue.Link}}/update" method="post">
<button class="ui compact button"> <button class="ui compact button">
<span class="ui text">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</span> <span class="ui text">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</span>
</button> </button>

View File

@ -1,63 +0,0 @@
{{/* Template Attributes:
* CommitStatus: summary of all commit status state
* CommitStatuses: all commit status elements
* ShowHideChecks: whether use a button to show/hide the checks
* StatusCheckData: additional status check data, see backend pullCommitStatusCheckData struct
*/}}
{{$statusCheckData := .StatusCheckData}}
{{if .CommitStatus}}
<div class="commit-status-panel">
<div class="ui top attached header commit-status-header">
{{$statusCheckData.CommitStatusCheckPrompt ctx.Locale}}
{{if .ShowHideChecks}}
<div class="ui right">
<button class="commit-status-hide-checks btn interact-fg"
data-show-all="{{ctx.Locale.Tr "repo.pulls.status_checks_show_all"}}"
data-hide-all="{{ctx.Locale.Tr "repo.pulls.status_checks_hide_all"}}">
{{ctx.Locale.Tr "repo.pulls.status_checks_hide_all"}}</button>
</div>
{{end}}
</div>
{{if and $statusCheckData $statusCheckData.RequireApprovalRunCount}}
<div class="ui attached segment flex-left-right" id="approve-status-checks">
<div>
<strong>
{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals" $statusCheckData.RequireApprovalRunCount}}
</strong>
<p>{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals_helper"}}</p>
</div>
{{if $statusCheckData.CanApprove}}
<button class="ui basic button link-action" data-url="{{$statusCheckData.ApproveLink}}">
{{ctx.Locale.Tr "repo.pulls.status_checks_approve_all"}}
</button>
{{end}}
</div>
{{end}}
<div class="commit-status-list">
{{range .CommitStatuses}}
<div class="commit-status-item">
{{template "repo/commit_status" .}}
<div class="status-context gt-ellipsis">{{.Context}} <span class="tw-text-text-light-2">{{.Description}}</span></div>
<div class="ui status-details">
{{if and $statusCheckData $statusCheckData.IsContextRequired}}
{{if (call $statusCheckData.IsContextRequired .Context)}}<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>{{end}}
{{end}}
<span>{{if .TargetURL}}<a href="{{.TargetURL}}">{{ctx.Locale.Tr "repo.pulls.status_checks_details"}}</a>{{end}}</span>
</div>
</div>
{{end}}
{{if $statusCheckData}}
{{range $statusCheckData.MissingRequiredChecks}}
<div class="commit-status-item">
{{svg "octicon-dot-fill" 18 "commit-status icon tw-text-yellow"}}
<div class="status-context gt-ellipsis">{{.}}</div>
<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
</div>
{{end}}
{{end}}
</div>
</div>
{{end}}

View File

@ -0,0 +1,32 @@
{{/* Template Attributes:
* CommitStatuses: all commit status elements
* StatusCheckData: optional, additional status check data, see backend pullCommitStatusCheckData struct
*/}}
{{$statusCheckData := $.StatusCheckData}}
{{range $cs := $.CommitStatuses}}
<div class="item commit-status-item">
<div class="flex-text-block">
{{template "repo/commit_status" $cs}}
<div class="status-context gt-ellipsis">
{{$cs.Context}} <span class="tw-text-text-light-2">{{$cs.Description}}</span>
</div>
</div>
<div class="status-details">
{{if and $statusCheckData $statusCheckData.IsContextRequired}}
{{if (call $statusCheckData.IsContextRequired $cs.Context)}}
<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
{{end}}
{{end}}
{{if $cs.TargetURL}}<a href="{{$cs.TargetURL}}">{{ctx.Locale.Tr "repo.pulls.status_checks_details"}}</a>{{end}}
</div>
</div>
{{end}}
{{range $missingCheck := $statusCheckData.MissingRequiredChecks}}
<div class="item commit-status-item">
<div class="flex-text-block">
{{svg "octicon-dot-fill" 16 "commit-status icon tw-text-yellow"}}
<div class="status-context gt-ellipsis">{{$missingCheck}}</div>
</div>
<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
</div>
{{end}}

View File

@ -149,7 +149,7 @@ func TestActionsArtifactDownload(t *testing.T) {
assert.Contains(t, listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") assert.Contains(t, listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
idx := strings.Index(listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") idx := strings.Index(listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
url := listResp.Value[artifactIdx].FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download" url := listResp.Value[artifactIdx].FileContainerResourceURL[idx:] + "?itemPath=artifact-download"
req = NewRequest(t, "GET", url). req = NewRequest(t, "GET", url).
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK) resp = MakeRequest(t, req, http.StatusOK)
@ -245,7 +245,7 @@ func TestActionsArtifactDownloadMultiFiles(t *testing.T) {
assert.Contains(t, fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") assert.Contains(t, fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
idx := strings.Index(fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") idx := strings.Index(fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
url := fileContainerResourceURL[idx+1:] + "?itemPath=" + testArtifactName url := fileContainerResourceURL[idx:] + "?itemPath=" + testArtifactName
req = NewRequest(t, "GET", url). req = NewRequest(t, "GET", url).
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK) resp = MakeRequest(t, req, http.StatusOK)
@ -323,7 +323,7 @@ func TestActionsArtifactOverwrite(t *testing.T) {
listResp := DecodeJSON(t, resp, &listArtifactsResponse{}) listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download" url := listResp.Value[0].FileContainerResourceURL[idx:] + "?itemPath=artifact-download"
req = NewRequest(t, "GET", url). req = NewRequest(t, "GET", url).
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK) resp = MakeRequest(t, req, http.StatusOK)
@ -380,7 +380,7 @@ func TestActionsArtifactOverwrite(t *testing.T) {
assert.Equal(t, "artifact-download", uploadedItem.Name) assert.Equal(t, "artifact-download", uploadedItem.Name)
idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download" url := uploadedItem.FileContainerResourceURL[idx:] + "?itemPath=artifact-download"
req = NewRequest(t, "GET", url). req = NewRequest(t, "GET", url).
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK) resp = MakeRequest(t, req, http.StatusOK)

View File

@ -348,7 +348,7 @@ func TestAPIUpdateBranchReference(t *testing.T) {
func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder { func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository) token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.RenameBranchRepoOption{ req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.RenameBranchRepoOption{
Name: to, Name: to,
}).AddTokenAuth(token) }).AddTokenAuth(token)
return MakeRequest(t, req, expectedHTTPStatus) return MakeRequest(t, req, expectedHTTPStatus)

View File

@ -96,18 +96,18 @@ func testUploadAttachmentDeleteTemp(t *testing.T) {
defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) { defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
tmpFileCountDuringUpload = countTmpFile() tmpFileCountDuringUpload = countTmpFile()
})() })()
_ = testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK) _ = testCreateIssueAttachment(t, session, "/user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory") assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload") assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
} }
func testCreateAnonymousAttachment(t *testing.T) { func testCreateAnonymousAttachment(t *testing.T) {
session := emptyTestSession(t) session := emptyTestSession(t)
testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther) testCreateIssueAttachment(t, session, "/user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
} }
func testCreateUser2IssueAttachment(t *testing.T) { func testCreateUser2IssueAttachment(t *testing.T) {
const repoURL = "user2/repo1" const repoURL = "/user2/repo1"
session := loginUser(t, "user2") session := loginUser(t, "user2")
uuid := testCreateIssueAttachment(t, session, repoURL, "image.png", testGeneratePngBytes(), http.StatusOK) uuid := testCreateIssueAttachment(t, session, repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
@ -177,7 +177,7 @@ func testGetAttachment(t *testing.T) {
} }
func testDeleteAttachmentPermissions(t *testing.T) { func testDeleteAttachmentPermissions(t *testing.T) {
const repoURL = "user2/repo1" const repoURL = "/user2/repo1"
ownerSession := loginUser(t, "user2") ownerSession := loginUser(t, "user2")
readonlySession := loginUser(t, "user5") readonlySession := loginUser(t, "user5")
@ -191,12 +191,12 @@ func testDeleteAttachmentPermissions(t *testing.T) {
testCreateReleaseAttachment(t, readonlySession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound) testCreateReleaseAttachment(t, readonlySession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK) crossRepoUUID := testCreateIssueAttachment(t, ownerSession, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest) testDeleteIssueAttachment(t, ownerSession, "/user2/repo2", crossRepoUUID, http.StatusBadRequest)
testDeleteIssueAttachment(t, ownerSession, repoURL, crossRepoUUID, http.StatusOK) testDeleteIssueAttachment(t, ownerSession, repoURL, crossRepoUUID, http.StatusOK)
releaseUUID := testCreateReleaseAttachment(t, ownerSession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK) releaseUUID := testCreateReleaseAttachment(t, ownerSession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
testDeleteReleaseAttachment(t, ownerSession, repoURL, releaseUUID, http.StatusOK) testDeleteReleaseAttachment(t, ownerSession, repoURL, releaseUUID, http.StatusOK)
// test deleting release attachment from another repo // test deleting release attachment from another repo
testDeleteReleaseAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest) testDeleteReleaseAttachment(t, ownerSession, "/user2/repo2", crossRepoUUID, http.StatusBadRequest)
} }

View File

@ -124,7 +124,7 @@ func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editor
resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params) resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params)
assert.Equal(t, http.StatusOK, resp.Code) assert.Equal(t, http.StatusOK, resp.Code)
assert.NotEmpty(t, test.RedirectURL(resp)) assert.NotEmpty(t, test.RedirectURL(resp))
req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"])) req := NewRequest(t, "GET", "/"+path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"]))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, params["content"], resp.Body.String()) assert.Equal(t, params["content"], resp.Body.String())
return resp return resp
@ -330,18 +330,18 @@ index 0000000000..bbbbbbbbbb
func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) { func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) {
forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) { forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) {
// visit the base repo, see the "Add File" button // visit the base repo, see the "Add File" button
req := NewRequest(t, "GET", path.Join(owner, repo)) req := NewRequest(t, "GET", "/"+path.Join(owner, repo))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1) AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1)
// attempt to edit a file, see the guideline page // attempt to edit a file, see the guideline page
req = NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath)) req = NewRequest(t, "GET", "/"+path.Join(owner, repo, operation, branch, filePath))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes") assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes")
// fork the repository // fork the repository
req = NewRequest(t, "POST", path.Join(owner, repo, "_fork", branch)) req = NewRequest(t, "POST", "/"+path.Join(owner, repo, "_fork", branch))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
assert.JSONEq(t, `{"redirect":""}`, resp.Body.String()) assert.JSONEq(t, `{"redirect":""}`, resp.Body.String())
} }
@ -351,7 +351,7 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
forkToEdit(t, session, owner, repo, "_edit", branch, filePath) forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
// Archive the repository // Archive the repository
req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), req := NewRequestWithValues(t, "POST", "/"+path.Join(user, repo, "settings"),
map[string]string{ map[string]string{
"repo_name": repo, "repo_name": repo,
"action": "archive", "action": "archive",
@ -360,12 +360,12 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusSeeOther)
// Check editing archived repository is disabled // Check editing archived repository is disabled
req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html") req = NewRequest(t, "GET", "/"+path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html")
resp := session.MakeRequest(t, req, http.StatusNotFound) resp := session.MakeRequest(t, req, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.") assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.")
// Unfork the repository // Unfork the repository
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), req = NewRequestWithValues(t, "POST", "/"+path.Join(user, repo, "settings"),
map[string]string{ map[string]string{
"repo_name": repo, "repo_name": repo,
"action": "convert_fork", "action": "convert_fork",
@ -381,7 +381,7 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
t.Run("CheckBaseRepoForm", func(t *testing.T) { t.Run("CheckBaseRepoForm", func(t *testing.T) {
// the base repo's edit form should have the correct action and upload links (pointing to the forked repo) // the base repo's edit form should have the correct action and upload links (pointing to the forked repo)
req := NewRequest(t, "GET", path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar") req := NewRequest(t, "GET", "/"+path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar")
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
@ -399,7 +399,7 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
}) })
t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) { t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) {
req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)) req := NewRequest(t, "GET", "/"+path.Join(owner, repo, "_edit", branch, filePath))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
editRequestForm := map[string]string{ editRequestForm := map[string]string{
@ -437,16 +437,16 @@ func testEditFileNotAllowed(t *testing.T) {
for _, operation := range operations { for _, operation := range operations {
t.Run(operation, func(t *testing.T) { t.Run(operation, func(t *testing.T) {
// Branch does not exist // Branch does not exist
targetLink := path.Join("user2", "repo1", operation, "missing", "README.md") targetLink := path.Join("/user2/repo1", operation, "missing", "README.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
// Private repository // Private repository
targetLink = path.Join("user2", "repo2", operation, "master", "Home.md") targetLink = path.Join("/user2/repo2", operation, "master", "Home.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK) sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK)
sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
// Empty repository // Empty repository
targetLink = path.Join("org41", "repo61", operation, "master", "README.md") targetLink = path.Join("/org41/repo61", operation, "master", "README.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
}) })
} }

View File

@ -326,14 +326,18 @@ func NewRequestWithJSON(t testing.TB, method, urlStr string, v any) *RequestWrap
func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *RequestWrapper { func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *RequestWrapper {
t.Helper() t.Helper()
if !strings.HasPrefix(urlStr, "http") && !strings.HasPrefix(urlStr, "/") { if !strings.HasPrefix(urlStr, "http:") && !strings.HasPrefix(urlStr, "https:") && !strings.HasPrefix(urlStr, "/") {
urlStr = "/" + urlStr t.Fatalf("invalid url str: %s", urlStr)
} }
req, err := http.NewRequest(method, urlStr, body) req, err := http.NewRequest(method, urlStr, body)
require.NoError(t, err) require.NoError(t, err)
if req.URL.User != nil { if req.URL.User != nil {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(req.URL.User.String()))) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(req.URL.User.String())))
} }
req.RequestURI = req.URL.Path
if req.URL.RawQuery != "" {
req.RequestURI += "?" + req.URL.RawQuery
}
return &RequestWrapper{req} return &RequestWrapper{req}
} }

View File

@ -123,7 +123,7 @@ func TestNoLoginViewIssue(t *testing.T) {
} }
func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string { func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string {
req := NewRequest(t, "GET", path.Join(user, repo, "issues", "new")) req := NewRequest(t, "GET", "/"+path.Join(user, repo, "issues", "new"))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
@ -667,7 +667,7 @@ func TestUpdateIssueDeadline(t *testing.T) {
assert.Equal(t, api.StateOpen, issueBefore.State()) assert.Equal(t, api.StateOpen, issueBefore.State())
session := loginUser(t, owner.Name) session := loginUser(t, owner.Name)
urlStr := fmt.Sprintf("%s/%s/issues/%d/deadline", owner.Name, repoBefore.Name, issueBefore.Index) urlStr := fmt.Sprintf("/%s/%s/issues/%d/deadline", owner.Name, repoBefore.Name, issueBefore.Index)
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": "2022-04-06"}) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": "2022-04-06"})
session.MakeRequest(t, req, http.StatusOK) session.MakeRequest(t, req, http.StatusOK)
@ -687,7 +687,7 @@ func TestIssueReferenceURL(t *testing.T) {
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
req := NewRequest(t, "GET", fmt.Sprintf("%s/issues/%d", repo.FullName(), issue.Index)) req := NewRequest(t, "GET", fmt.Sprintf("%s/issues/%d", repo.Link(), issue.Index))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)

View File

@ -66,7 +66,7 @@ func testOrgProfile(t *testing.T, u *url.URL) {
createTestProfile(t, "org3", user.RepoNameProfilePrivate, contentPrivateReadme) createTestProfile(t, "org3", user.RepoNameProfilePrivate, contentPrivateReadme)
// Anonymous User // Anonymous User
req := NewRequest(t, "GET", "org3") req := NewRequest(t, "GET", "/org3")
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
bodyString := util.UnsafeBytesToString(resp.Body.Bytes()) bodyString := util.UnsafeBytesToString(resp.Body.Bytes())
assert.Contains(t, bodyString, contentPublicReadme) assert.Contains(t, bodyString, contentPublicReadme)
@ -74,7 +74,7 @@ func testOrgProfile(t *testing.T, u *url.URL) {
// Logged in but not member // Logged in but not member
session := loginUser(t, "user24") session := loginUser(t, "user24")
req = NewRequest(t, "GET", "org3") req = NewRequest(t, "GET", "/org3")
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
assert.Contains(t, bodyString, contentPublicReadme) assert.Contains(t, bodyString, contentPublicReadme)

View File

@ -26,7 +26,7 @@ import (
) )
func testPullCreate(t *testing.T, session *TestSession, user, repo string, toSelf bool, targetBranch, sourceBranch, title string) *httptest.ResponseRecorder { func testPullCreate(t *testing.T, session *TestSession, user, repo string, toSelf bool, targetBranch, sourceBranch, title string) *httptest.ResponseRecorder {
req := NewRequest(t, "GET", path.Join(user, repo)) req := NewRequest(t, "GET", "/"+path.Join(user, repo))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
// Click the PR button to create a pull // Click the PR button to create a pull

View File

@ -80,7 +80,7 @@ func testPullMerge(t *testing.T, session *TestSession, user, repo, pullNum strin
} }
func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder { func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum)) req := NewRequest(t, "GET", "/"+path.Join(user, repo, "pulls", pullnum))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
// Click the little button to create a pull // Click the little button to create a pull
@ -322,11 +322,8 @@ func TestCantMergeWorkInProgress(t *testing.T) {
req := NewRequest(t, "GET", test.RedirectURL(resp)) req := NewRequest(t, "GET", test.RedirectURL(resp))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text()) wipToggleButtonCount := htmlDoc.Find(`.merge-section > .item button[data-global-init="initPullRequestWipToggle"]`).Length()
assert.NotEmpty(t, text, "Can't find WIP text") assert.Equal(t, 1, wipToggleButtonCount)
assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
assert.Contains(t, text, "[wip]", "Unable to find WIP text")
}) })
} }

View File

@ -264,13 +264,13 @@ func testSubmitReview(t *testing.T, session *TestSession, owner, repo, pullNumbe
"type": reviewType, "type": reviewType,
} }
submitURL := path.Join(owner, repo, "pulls", pullNumber, "files", "reviews", "submit") submitURL := "/" + path.Join(owner, repo, "pulls", pullNumber, "files", "reviews", "submit")
req := NewRequestWithValues(t, "POST", submitURL, options) req := NewRequestWithValues(t, "POST", submitURL, options)
return session.MakeRequest(t, req, expectedSubmitStatus) return session.MakeRequest(t, req, expectedSubmitStatus)
} }
func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder { func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder {
closeURL := path.Join(owner, repo, "issues", issueNumber, "comments") closeURL := "/" + path.Join(owner, repo, "issues", issueNumber, "comments")
options := map[string]string{ options := map[string]string{
"status": "close", "status": "close",

View File

@ -31,7 +31,7 @@ func TestPullCreate_CommitStatus(t *testing.T) {
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
url := path.Join("user1", "repo1", "compare", "master...status1") url := "/" + path.Join("user1", "repo1", "compare", "master...status1")
req := NewRequestWithValues(t, "POST", url, req := NewRequestWithValues(t, "POST", url,
map[string]string{ map[string]string{
"title": "pull request from status1", "title": "pull request from status1",
@ -121,7 +121,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1") testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1")
url := path.Join("user1", "repo1", "compare", "master...status1") url := "/" + path.Join("user1", "repo1", "compare", "master...status1")
req := NewRequestWithValues(t, "POST", url, req := NewRequestWithValues(t, "POST", url,
map[string]string{ map[string]string{
"title": "pull request from status1", "title": "pull request from status1",
@ -143,7 +143,7 @@ func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
session := loginUser(t, "user1") session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther) testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther)
url := path.Join("user1", "repo1", "compare", "master...status1") url := "/" + path.Join("user1", "repo1", "compare", "master...status1")
req := NewRequestWithValues(t, "POST", url, req := NewRequestWithValues(t, "POST", url,
map[string]string{ map[string]string{
"title": "pull request from status1", "title": "pull request from status1",

View File

@ -21,7 +21,7 @@ import (
) )
func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string { func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string {
req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefSubURL), map[string]string{ req := NewRequestWithValues(t, "POST", "/"+path.Join(user, repo, "branches/_new", oldRefSubURL), map[string]string{
"new_branch_name": newBranchName, "new_branch_name": newBranchName,
}) })
resp := session.MakeRequest(t, req, expectedStatus) resp := session.MakeRequest(t, req, expectedStatus)
@ -221,7 +221,7 @@ func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo
func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath string, expected []string) { func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath string, expected []string) {
branches := make([]string, 0, 2) branches := make([]string, 0, 2)
req := NewRequest(t, "GET", repoPath) req := NewRequest(t, "GET", "/"+repoPath)
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body) doc := NewHTMLParser(t, resp.Body)
doc.doc.Find(".ui.positive.message div a").Each(func(index int, branch *goquery.Selection) { doc.doc.Find(".ui.positive.message div a").Each(func(index int, branch *goquery.Selection) {

View File

@ -37,7 +37,7 @@ func TestViewTimetrackingControls(t *testing.T) {
} }
func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo, issue string, canTrackTime bool) { func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo, issue string, canTrackTime bool) {
req := NewRequest(t, "GET", path.Join(user, repo, "issues", issue)) req := NewRequest(t, "GET", "/"+path.Join(user, repo, "issues", issue))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
@ -45,7 +45,7 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo
AssertHTMLElement(t, htmlDoc, ".issue-start-time", canTrackTime) AssertHTMLElement(t, htmlDoc, ".issue-start-time", canTrackTime)
AssertHTMLElement(t, htmlDoc, ".issue-add-time", canTrackTime) AssertHTMLElement(t, htmlDoc, ".issue-add-time", canTrackTime)
issueLink := path.Join(user, repo, "issues", issue) issueLink := "/" + path.Join(user, repo, "issues", issue)
reqStart := NewRequest(t, "POST", path.Join(issueLink, "times", "stopwatch", "start")) reqStart := NewRequest(t, "POST", path.Join(issueLink, "times", "stopwatch", "start"))
if canTrackTime { if canTrackTime {
session.MakeRequest(t, reqStart, http.StatusOK) session.MakeRequest(t, reqStart, http.StatusOK)

View File

@ -179,13 +179,8 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.ui.fitted.segment:not(.horizontally) { .ui.fitted.segment {
padding-top: 0; padding: 0;
padding-bottom: 0;
}
.ui.fitted.segment:not(.vertically) {
padding-left: 0;
padding-right: 0;
} }
.ui.segments .segment, .ui.segments .segment,

View File

@ -596,8 +596,10 @@ td .commit-summary {
} }
} }
.repository.view.issue .comment-list .comment .merge-section { .repository.view.issue .comment-list .comment .merge-section .item + ul.item {
background-color: var(--color-box-body); border-top: 0;
padding: 0 1em 0 52px;
margin-top: -0.5em;
} }
.repository.view.issue .comment-list .comment .merge-section .item-section { .repository.view.issue .comment-list .comment .merge-section .item-section {
@ -605,15 +607,9 @@ td .commit-summary {
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0;
gap: 0.5em; gap: 0.5em;
} }
.repository.view.issue .comment-list .comment .merge-section .divider {
margin-left: -1rem;
width: calc(100% + 2rem);
}
.merge-section-info code { .merge-section-info code {
border: 1px solid var(--color-light-border); border: 1px solid var(--color-light-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
@ -1933,46 +1929,16 @@ tbody.commit-list {
max-height: 240px; /* fit exactly 6 items, commit-status-item.height * 6 */ max-height: 240px; /* fit exactly 6 items, commit-status-item.height * 6 */
overflow-x: hidden; overflow-x: hidden;
transition: max-height .2s; transition: max-height .2s;
width: 100%;
} }
.commit-status-item { .commit-status-item {
height: 40px; height: 40px;
padding: 0 10px;
display: flex; display: flex;
gap: 8px; gap: var(--gap-block);
align-items: center; align-items: center;
} justify-content: space-between;
flex-wrap: wrap;
.commit-status-item + .commit-status-item {
border-top: 1px solid var(--color-secondary);
}
.commit-status-item .commit-status {
flex-shrink: 0;
}
.commit-status-item .status-context {
color: var(--color-text);
flex: 1;
}
.commit-status-item .status-details {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
@media (max-width: 767.98px) {
.commit-status-item .status-details {
flex-direction: column;
align-items: flex-end;
justify-content: center;
}
}
.commit-status-item .status-details > span {
padding-right: 0.5em; /* To match the alignment with the "required" label */
} }
.username-display { .username-display {

View File

@ -9,17 +9,23 @@
margin: 0; margin: 0;
} }
/* items have dividers between them, the dividers align with items (use parent padding) */ /* items have dividers between them, the dividers align with items */
.flex-divided-list { .flex-divided-list,
list-style: none; .flex-divided-list > .item.flex-divided-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch;
gap: 0;
} }
.flex-divided-list > .item { .flex-divided-list > .item {
padding: 10px 0; padding: 10px 0;
} }
.flex-divided-list > .divider {
margin: 0;
}
.flex-divided-list > .item + .item { .flex-divided-list > .item + .item {
border-top: 1px solid var(--color-secondary); border-top: 1px solid var(--color-secondary);
} }
@ -99,21 +105,15 @@
} }
/* special rules to make the list work with existing UI elements */ /* special rules to make the list work with existing UI elements */
.container-segmented > .flex-divided-list > .item { .flex-divided-list.items-px-default > .item {
padding-left: 1em; padding-left: 1em; /* matches ".ui.segment" padding */
padding-right: 1em; padding-right: 1em;
} }
.ui.segment.fitted > .flex-divided-list > .item {
padding: 1em;
}
.container-padded > .flex-divided-list > .item:first-child,
.ui.segment:not(.fitted) > .flex-divided-list > .item:first-child { .ui.segment:not(.fitted) > .flex-divided-list > .item:first-child {
padding-top: 0; padding-top: 0;
} }
.container-padded > .flex-divided-list > .item:last-child,
.ui.segment:not(.fitted) > .flex-divided-list > .item:last-child { .ui.segment:not(.fitted) > .flex-divided-list > .item:last-child {
padding-bottom: 0; padding-bottom: 0;
} }
@ -126,7 +126,3 @@
.flex-divided-list + .divider { .flex-divided-list + .divider {
margin-top: 0; margin-top: 0;
} }
.container-padded > .flex-divided-list + .divider {
margin-top: 10px;
}

View File

@ -7,6 +7,8 @@ const props = defineProps<{
mergeFormProps: any, // TODO: this is a huge object, need to be refactored in the future mergeFormProps: any, // TODO: this is a huge object, need to be refactored in the future
}>(); }>();
const mergeStyleManuallyMerged = 'manually-merged';
const mergeForm = props.mergeFormProps; const mergeForm = props.mergeFormProps;
const mergeTitleFieldValue = shallowRef(''); const mergeTitleFieldValue = shallowRef('');
@ -29,10 +31,17 @@ const showMergeStyleMenu = shallowRef(false);
const showActionForm = shallowRef(false); const showActionForm = shallowRef(false);
const mergeButtonStyleClass = computed(() => { const mergeButtonStyleClass = computed(() => {
if (mergeStyle.value === mergeStyleManuallyMerged) return 'red';
if (mergeForm.allOverridableChecksOk) return 'primary'; if (mergeForm.allOverridableChecksOk) return 'primary';
return autoMergeWhenSucceed.value ? 'primary' : 'red'; return autoMergeWhenSucceed.value ? 'primary' : 'red';
}); });
const mergeSelectStyleClass = computed(() => {
if (mergeForm.emptyCommit) return '';
if (mergeStyle.value === mergeStyleManuallyMerged) return 'red';
return 'primary';
});
const forceMerge = computed(() => { const forceMerge = computed(() => {
return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk; return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk;
}); });
@ -115,30 +124,32 @@ function clearMergeMessage() {
</div> </div>
</template> </template>
<div class="field" v-if="mergeStyle === 'manually-merged'"> <div class="field" v-if="mergeStyle === mergeStyleManuallyMerged">
<input type="text" name="merge_commit_id" :placeholder="mergeForm.textMergeCommitId"> <input type="text" name="merge_commit_id" :placeholder="mergeForm.textMergeCommitId">
</div> </div>
<button class="ui button" :class="mergeButtonStyleClass" type="submit" name="do" :value="mergeStyle"> <div class="flex-text-block tw-gap-3">
{{ mergeStyleDetail.textDoMerge }} <button class="ui button" :class="mergeButtonStyleClass" type="submit" name="do" :value="mergeStyle">
<template v-if="autoMergeWhenSucceed"> {{ mergeStyleDetail.textDoMerge }}
{{ mergeForm.textAutoMergeButtonWhenSucceed }} <template v-if="autoMergeWhenSucceed">
</template> {{ mergeForm.textAutoMergeButtonWhenSucceed }}
</button> </template>
</button>
<button class="ui button merge-cancel" @click="toggleActionForm(false)"> <button class="ui button merge-cancel" type="button" @click="toggleActionForm(false)">
{{ mergeForm.textCancel }} {{ mergeForm.textCancel }}
</button> </button>
<div class="ui checkbox tw-ml-1" v-if="mergeForm.isPullBranchDeletable"> <div class="ui checkbox" v-if="mergeForm.isPullBranchDeletable">
<input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge"> <input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge">
<label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label> <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label>
</div>
</div> </div>
</form> </form>
<div v-if="!showActionForm" class="tw-flex"> <div v-if="!showActionForm" class="tw-flex">
<!-- the merge button --> <!-- the merge button -->
<div class="ui buttons merge-button" :class="[mergeForm.emptyCommit ? '' : mergeForm.allOverridableChecksOk ? 'primary' : 'red']" @click="toggleActionForm(true)"> <div class="ui buttons merge-button" :class="mergeSelectStyleClass" @click="toggleActionForm(true)">
<button class="ui button"> <button class="ui button">
<svg-icon name="octicon-git-merge"/> <svg-icon name="octicon-git-merge"/>
<span class="button-text"> <span class="button-text">

View File

@ -2,6 +2,7 @@ import {createApp} from 'vue';
import {GET, POST} from '../modules/fetch.ts'; import {GET, POST} from '../modules/fetch.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts';
import {createElementFromHTML} from '../utils/dom.ts'; import {createElementFromHTML} from '../utils/dom.ts';
import {registerGlobalEventFunc} from '../modules/observer.ts';
function initRepoPullRequestUpdate(el: HTMLElement) { function initRepoPullRequestUpdate(el: HTMLElement) {
const prUpdateButtonContainer = el.querySelector('#update-pr-branch-with-base'); const prUpdateButtonContainer = el.querySelector('#update-pr-branch-with-base');
@ -22,6 +23,7 @@ function initRepoPullRequestUpdate(el: HTMLElement) {
} }
let data: Record<string, any> | undefined; let data: Record<string, any> | undefined;
try { try {
// TODO: the response is indeed not JSON, need to fix (see backend UpdatePullRequest)
data = await response?.json(); // the response is probably not a JSON data = await response?.json(); // the response is probably not a JSON
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -48,15 +50,11 @@ function initRepoPullRequestUpdate(el: HTMLElement) {
}); });
} }
function initRepoPullRequestCommitStatus(el: HTMLElement) { function onCommitStatusChecksToggle(btn: HTMLElement) {
for (const btn of el.querySelectorAll('.commit-status-hide-checks')) { const panel = btn.closest('.commit-status-toggle')!.parentElement!;
const panel = btn.closest('.commit-status-panel')!; const list = panel.querySelector<HTMLElement>('.commit-status-list')!;
const list = panel.querySelector<HTMLElement>('.commit-status-list')!; list.style.maxHeight = list.style.maxHeight ? '' : '0px'; // toggle
btn.addEventListener('click', () => { btn.textContent = btn.getAttribute(list.style.maxHeight ? 'data-show-all' : 'data-hide-all');
list.style.maxHeight = list.style.maxHeight ? '' : '0px'; // toggle
btn.textContent = btn.getAttribute(list.style.maxHeight ? 'data-show-all' : 'data-hide-all');
});
}
} }
async function initRepoPullRequestMergeForm(box: HTMLElement) { async function initRepoPullRequestMergeForm(box: HTMLElement) {
@ -70,7 +68,7 @@ async function initRepoPullRequestMergeForm(box: HTMLElement) {
} }
export function initRepoPullMergeBox(el: HTMLElement) { export function initRepoPullMergeBox(el: HTMLElement) {
initRepoPullRequestCommitStatus(el); registerGlobalEventFunc('click', 'onCommitStatusChecksToggle', onCommitStatusChecksToggle);
initRepoPullRequestUpdate(el); initRepoPullRequestUpdate(el);
initRepoPullRequestMergeForm(el); initRepoPullRequestMergeForm(el);