{{template "repo/header" .}}
- {{$class := ""}} - {{if .Commit.Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} -
+

{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}

{{if not $.PageIsWiki}} @@ -142,125 +128,59 @@ {{end}} {{template "repo/commit_load_branches_and_tags" .}}
-
-
- {{if .Author}} - {{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}} - {{if .Author.FullName}} - {{.Author.FullName}} - {{else}} - {{.Commit.Author.Name}} - {{end}} + +
+
+ {{if .Author}} + {{ctx.AvatarUtils.Avatar .Author 20}} + {{if .Author.FullName}} + {{.Author.FullName}} {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}} - {{.Commit.Author.Name}} + {{.Commit.Author.Name}} {{end}} - {{DateUtils.TimeSince .Commit.Author.When}} - {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} - {{ctx.Locale.Tr "repo.diff.committed_by"}} - {{if ne .Verification.CommittingUser.ID 0}} - {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}} - {{.Commit.Committer.Name}} - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}} - {{.Commit.Committer.Name}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 20}} + {{.Commit.Author.Name}} + {{end}} +
+ + {{DateUtils.TimeSince .Commit.Author.When}} + +
+ {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} + {{ctx.Locale.Tr "repo.diff.committed_by"}} + {{if ne .Verification.CommittingUser.ID 0}} + {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}} + {{.Commit.Committer.Name}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 20}} + {{.Commit.Committer.Name}} + {{end}} + {{end}} +
+ + {{if .Verification}} + {{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}} + {{end}} + +
+ +
+ {{if .Parents}} +
+ {{ctx.Locale.Tr "repo.diff.parent"}} + {{range .Parents}} + {{ShortSha .}} {{end}} - {{end}} -
-
- {{if .Parents}} -
- {{ctx.Locale.Tr "repo.diff.parent"}} - {{range .Parents}} - {{if $.PageIsWiki}} - {{ShortSha .}} - {{else}} - {{ShortSha .}} - {{end}} - {{end}} -
- {{end}} -
- {{ctx.Locale.Tr "repo.diff.commit"}} - {{ShortSha .CommitID}}
-
-
- {{if .Commit.Signature}} -
-
- {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - {{svg "gitea-lock" 16 "tw-mr-2"}} - {{if eq .Verification.TrustStatus "trusted"}} - {{ctx.Locale.Tr "repo.commits.signed_by"}}: - {{else if eq .Verification.TrustStatus "untrusted"}} - {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}: - {{else}} - {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: - {{end}} - {{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}} - {{.Verification.SigningUser.GetDisplayName}} - {{else}} - {{svg "gitea-lock-cog" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.signed_by"}}: - {{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}} - {{.Verification.SigningUser.GetDisplayName}} - {{end}} - {{else}} - {{svg "gitea-unlock" 16 "tw-mr-2"}} - {{ctx.Locale.Tr .Verification.Reason}} - {{end}} -
-
- {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{svg "octicon-unverified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{else if .Verification.Warning}} - {{svg "octicon-unverified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{if .Verification.SigningKey}} - {{if ne .Verification.SigningKey.KeyID ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{if .Verification.SigningSSHKey}} - {{if ne .Verification.SigningSSHKey.Fingerprint ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{end}} - {{end}} - {{end}} + {{end}} +
+ {{ctx.Locale.Tr "repo.diff.commit"}} + {{ShortSha .CommitID}}
- {{end}} +
+ {{if .NoteRendered}}
{{svg "octicon-note" 16 "tw-mr-2"}} @@ -276,12 +196,13 @@ {{else}} {{.NoteCommit.Author.Name}} {{end}} - {{DateUtils.TimeSince .NoteCommit.Author.When}} + {{DateUtils.TimeSince .NoteCommit.Author.When}}
{{.NoteRendered | SanitizeHTML}}
{{end}} + {{template "repo/diff/box" .}}
diff --git a/templates/repo/commit_sign_badge.tmpl b/templates/repo/commit_sign_badge.tmpl new file mode 100644 index 0000000000..aa68e9dd23 --- /dev/null +++ b/templates/repo/commit_sign_badge.tmpl @@ -0,0 +1,78 @@ +{{/* Template attributes: +* Commit +* CommitBaseLink +* CommitSignVerification +If you'd like to modify this template, you could test it on the devtest page. +ATTENTION: this template could be re-rendered many times (on the graph and commit list page), +so this template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. +*/}} +{{- $commit := $.Commit -}} +{{- $commitBaseLink := $.CommitBaseLink -}} +{{- $verification := $.CommitSignVerification -}} + +{{- $extraClass := "" -}} +{{- $verified := false -}} +{{- $signingUser := NIL -}} +{{- $signingEmail := "" -}} +{{- $msgReasonPrefix := "" -}} +{{- $msgReason := "" -}} +{{- $msgSigningKey := "" -}} + +{{- if $verification -}} + {{- $signingUser = $verification.SigningUser -}} + {{- $signingEmail = $verification.SigningEmail -}} + {{- $extraClass = print $extraClass " commit-is-signed" -}} + {{- if $verification.Verified -}} + {{- /* reason is "{name} / {key-id}" */ -}} + {{- $msgReason = $verification.Reason -}} + {{- $verified = true -}} + {{- if eq $verification.TrustStatus "trusted" -}} + {{- $extraClass = print $extraClass " sign-trusted" -}} + {{- else if eq $verification.TrustStatus "untrusted" -}} + {{- $extraClass = print $extraClass " sign-untrusted" -}} + {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user" -}} + {{- else -}} + {{- $extraClass = print $extraClass " sign-unmatched" -}} + {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched" -}} + {{- end -}} + {{- else -}} + {{- if $verification.Warning -}} + {{- $extraClass = print $extraClass " sign-warning" -}} + {{- end -}} + {{- $msgReason = ctx.Locale.Tr $verification.Reason -}}{{- /* dirty part: it is the translation key ..... */ -}} + {{- end -}} + + {{- if $msgReasonPrefix -}} + {{- $msgReason = print $msgReasonPrefix ": " $msgReason -}} + {{- end -}} + + {{- if $verification.SigningSSHKey -}} + {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.ssh_key_fingerprint") ": " $verification.SigningSSHKey.Fingerprint -}} + {{- else if $verification.SigningKey -}} + {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.gpg_key_id") ": " $verification.SigningKey.PaddedKeyID -}} + {{- end -}} +{{- end -}} + +{{- if $commit -}} + + {{- ShortSha $commit.ID.String -}} +{{- end -}} + + {{- if $verified -}} + {{- if and $signingUser $signingUser.ID -}} + {{svg "gitea-lock"}} + {{ctx.AvatarUtils.Avatar $signingUser 16}} + {{- else -}} + {{svg "gitea-lock-cog"}} + {{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}} + {{- end -}} + {{- else -}} + {{svg "gitea-unlock"}} + {{- end -}} + + +{{- if $commit -}} + +{{- end -}} + +{{- /* This template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. */ -}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 50b754cc23..329dc45149 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -28,33 +28,15 @@
- {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - {{$commitShaLink := ""}} + {{$commitBaseLink := ""}} {{if $.PageIsWiki}} - {{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}} {{else if $.PageIsPullCommits}} - {{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}} {{else if $.Reponame}} - {{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} {{end}} - - {{ShortSha .ID.String}} - {{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}} - + {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 0657eaba1d..2acf7c58b8 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -3,7 +3,7 @@ {{range .comment.Commits}} {{$tag := printf "%s-%d" $.comment.HashTag $index}} {{$index = Eval $index "+" 1}} -
+
{{/*singular-commit*/}} {{svg "octicon-git-commit"}} {{if .User}} {{ctx.AvatarUtils.Avatar .User 20}} @@ -11,7 +11,8 @@ {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}} {{end}} - {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} + {{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}} + {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} @@ -21,29 +22,9 @@ {{end}} - + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - - {{ShortSha .ID.String}} - {{if .Signature}} - {{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}} - {{end}} - + {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}}
{{if IsMultilineCommitMessage .Message}} diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index a06cd2ddd1..cb612bc27c 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -1,5 +1,7 @@ {{$file := .file}} -{{$blobExcerptLink := print (or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink) (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.root.AfterCommitID) "?"}} +{{$repoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} +{{$afterCommitID := or $.root.AfterCommitID "no-after-commit-id"}}{{/* this tmpl is also used by the PR Conversation page, so the "AfterCommitID" may not exist */}} +{{$blobExcerptLink := print $repoLink (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $afterCommitID) "?"}} diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index f1d0e62330..6af0ba1f0f 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -5,33 +5,13 @@ {{if $commit.OnlyRelation}} {{else}} - - {{$class := "ui sha label"}} - {{if $commit.Commit.Signature}} - {{$class = (print $class " isSigned")}} - {{if $commit.Verification.Verified}} - {{if eq $commit.Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq $commit.Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if $commit.Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - - {{ShortSha $commit.Commit.ID.String}} - {{- if $commit.Commit.Signature -}} - {{template "repo/shabox_badge" dict "root" $ "verification" $commit.Verification}} - {{- end -}} - - - + {{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} + + {{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}} - + + {{range $commit.Refs}} {{$refGroup := .RefGroup}} {{if eq $refGroup "pull"}} @@ -56,7 +36,8 @@ {{end}} {{end}} - + + {{$userName := $commit.Commit.Author.Name}} {{if $commit.User}} {{if and $commit.User.FullName DefaultShowFullName}} @@ -69,7 +50,8 @@ {{$userName}} {{end}} - {{DateUtils.FullTime $commit.Date}} + + {{DateUtils.FullTime $commit.Date}} {{end}} {{end}} diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index 34a5df8f77..b176b4190c 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -1,3 +1,4 @@ +
{{if not .LatestCommit}} … {{else}} @@ -14,13 +15,11 @@ {{.LatestCommit.Author.Name}} {{end}} {{end}} - - {{ShortSha .LatestCommit.ID.String}} - {{if .LatestCommit.Signature}} - {{template "repo/shabox_badge" dict "root" $ "verification" .LatestCommitVerification}} - {{end}} - + + {{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}} + {{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} + {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} {{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}} {{if IsMultilineCommitMessage .LatestCommit.Message}} @@ -29,3 +28,4 @@ {{end}} {{end}} +
diff --git a/templates/repo/shabox_badge.tmpl b/templates/repo/shabox_badge.tmpl deleted file mode 100644 index 36fc9e04b1..0000000000 --- a/templates/repo/shabox_badge.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -
- {{if .verification.Verified}} -
- {{if ne .verification.SigningUser.ID 0}} - {{svg "gitea-lock"}} - {{ctx.AvatarUtils.Avatar .verification.SigningUser 16 "signature"}} - {{else}} - {{svg "gitea-lock-cog"}} - {{ctx.AvatarUtils.AvatarByEmail .verification.SigningEmail "" 16 "signature"}} - {{end}} -
- {{else}} - {{svg "gitea-unlock"}} - {{end}} -
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 86366ae053..0a43e3db54 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -12,9 +12,7 @@ {{if not .ReadmeInList}}
-
- {{template "repo/latest_commit" .}} -
+ {{template "repo/latest_commit" .}} {{if .LatestCommit}} {{if .LatestCommit.Committer}}
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 2d555e4c2e..01bb70e06f 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -1,7 +1,7 @@ {{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
-
{{template "repo/latest_commit" .}}
+ {{template "repo/latest_commit" .}}
{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}
{{if .HasParentPath}} diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index f04f1ef6c4..91f04e0b53 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -92,6 +92,9 @@ {{end}} {{end}} + {{if .ShowMoreOrgs}} +
  • {{svg "octicon-kebab-horizontal" 28 "icon tw-p-1"}}
  • + {{end}} {{end}} diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index cf61bb906a..2c83ce97cd 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -27,6 +27,8 @@ {{template "repo/user_cards" .}} {{else if eq .TabName "overview"}}
    {{.ProfileReadme}}
    + {{else if eq .TabName "organizations"}} + {{template "repo/user_cards" .}} {{else}} {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go new file mode 100644 index 0000000000..e13277678d --- /dev/null +++ b/tests/integration/actions_job_test.go @@ -0,0 +1,410 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/stretchr/testify/assert" +) + +func TestJobWithNeeds(t *testing.T) { + testCases := []struct { + treePath string + fileContent string + outcomes map[string]*mockTaskOutcome + expectedStatuses map[string]string + }{ + { + treePath: ".gitea/workflows/job-with-needs.yml", + fileContent: `name: job-with-needs +on: + push: + paths: + - '.gitea/workflows/job-with-needs.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + "job2": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusSuccess.String(), + "job2": actions_model.StatusSuccess.String(), + }, + }, + { + treePath: ".gitea/workflows/job-with-needs-fail.yml", + fileContent: `name: job-with-needs-fail +on: + push: + paths: + - '.gitea/workflows/job-with-needs-fail.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_FAILURE, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusFailure.String(), + "job2": actions_model.StatusSkipped.String(), + }, + }, + { + treePath: ".gitea/workflows/job-with-needs-fail-if.yml", + fileContent: `name: job-with-needs-fail-if +on: + push: + paths: + - '.gitea/workflows/job-with-needs-fail-if.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_FAILURE, + }, + "job2": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusFailure.String(), + "job2": actions_model.StatusSuccess.String(), + }, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + // create the workflow file + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + + // fetch and execute task + for i := 0; i < len(tc.outcomes); i++ { + task := runner.fetchTask(t) + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) + outcome := tc.outcomes[jobName] + assert.NotNil(t, outcome) + runner.execTask(t, task, outcome) + } + + // check result + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var actionTaskRespAfter api.ActionTaskResponse + DecodeJSON(t, resp, &actionTaskRespAfter) + for _, apiTask := range actionTaskRespAfter.Entries { + if apiTask.HeadSHA != fileResp.Commit.SHA { + continue + } + status := apiTask.Status + assert.Equal(t, status, tc.expectedStatuses[apiTask.Name]) + } + }) + } + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} + +func TestJobNeedsMatrix(t *testing.T) { + testCases := []struct { + treePath string + fileContent string + outcomes map[string]*mockTaskOutcome + expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed + }{ + { + treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml", + fileContent: `name: jobs-outputs-with-matrix +on: + push: + paths: + - '.gitea/workflows/jobs-outputs-with-matrix.yml' +jobs: + job1: + runs-on: ubuntu-latest + outputs: + output_1: ${{ steps.gen_output.outputs.output_1 }} + output_2: ${{ steps.gen_output.outputs.output_2 }} + output_3: ${{ steps.gen_output.outputs.output_3 }} + strategy: + matrix: + version: [1, 2, 3] + steps: + - name: Generate output + id: gen_output + run: | + version="${{ matrix.version }}" + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo '${{ toJSON(needs.job1.outputs) }}' +`, + outcomes: map[string]*mockTaskOutcome{ + "job1 (1)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "", + }, + }, + "job1 (2)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "2", + "output_3": "", + }, + }, + "job1 (3)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "3", + }, + }, + }, + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ + "job1": { + Result: runnerv1.Result_RESULT_SUCCESS, + Outputs: map[string]string{ + "output_1": "1", + "output_2": "2", + "output_3": "3", + }, + }, + }, + }, + { + treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml", + fileContent: `name: jobs-outputs-with-matrix-failure +on: + push: + paths: + - '.gitea/workflows/jobs-outputs-with-matrix-failure.yml' +jobs: + job1: + runs-on: ubuntu-latest + outputs: + output_1: ${{ steps.gen_output.outputs.output_1 }} + output_2: ${{ steps.gen_output.outputs.output_2 }} + output_3: ${{ steps.gen_output.outputs.output_3 }} + strategy: + matrix: + version: [1, 2, 3] + steps: + - name: Generate output + id: gen_output + run: | + version="${{ matrix.version }}" + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" + job2: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [job1] + steps: + - run: echo '${{ toJSON(needs.job1.outputs) }}' +`, + outcomes: map[string]*mockTaskOutcome{ + "job1 (1)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "", + }, + }, + "job1 (2)": { + result: runnerv1.Result_RESULT_FAILURE, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "", + }, + }, + "job1 (3)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "3", + }, + }, + }, + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ + "job1": { + Result: runnerv1.Result_RESULT_FAILURE, + Outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "3", + }, + }, + }, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + + for i := 0; i < len(tc.outcomes); i++ { + task := runner.fetchTask(t) + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) + outcome := tc.outcomes[jobName] + assert.NotNil(t, outcome) + runner.execTask(t, task, outcome) + } + + task := runner.fetchTask(t) + actualTaskNeeds := task.Needs + assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds)) + for jobID, tn := range tc.expectedTaskNeeds { + actualNeed := actualTaskNeeds[jobID] + assert.Equal(t, tn.Result, actualNeed.Result) + assert.Len(t, actualNeed.Outputs, len(tn.Outputs)) + for outputKey, outputValue := range tn.Outputs { + assert.Equal(t, outputValue, actualNeed.Outputs[outputKey]) + } + } + }) + } + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} + +func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository { + req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{ + Name: repoName, + Private: isPrivate, + Readme: "Default", + AutoInit: true, + DefaultBranch: "main", + }).AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusCreated) + var apiRepo api.Repository + DecodeJSON(t, resp, &apiRepo) + return &apiRepo +} + +func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions { + return &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: branch, + Message: msg, + Author: api.Identity{ + Name: u.Name, + Email: u.Email, + }, + Committer: api.Identity{ + Name: u.Name, + Email: u.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)), + } +} + +func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts). + AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusCreated) + var fileResponse api.FileResponse + DecodeJSON(t, resp, &fileResponse) + return &fileResponse +} + +// getTaskJobNameByTaskID get the job name of the task by task ID +// there is currently not an API for querying a task by ID so we have to list all the tasks +func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string { + // FIXME: we may need to query several pages + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)). + AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusOK) + var taskRespBefore api.ActionTaskResponse + DecodeJSON(t, resp, &taskRespBefore) + for _, apiTask := range taskRespBefore.Entries { + if apiTask.ID == taskID { + return apiTask.Name + } + } + return "" +} diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go new file mode 100644 index 0000000000..fd055fc4c4 --- /dev/null +++ b/tests/integration/actions_log_test.go @@ -0,0 +1,159 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + 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/setting" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/test" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestDownloadTaskLogs(t *testing.T) { + now := time.Now() + testCases := []struct { + treePath string + fileContent string + outcome *mockTaskOutcome + zstdEnabled bool + }{ + { + treePath: ".gitea/workflows/download-task-logs-zstd.yml", + fileContent: `name: download-task-logs-zstd +on: + push: + paths: + - '.gitea/workflows/download-task-logs-zstd.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 with zstd enabled +`, + outcome: &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(1 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(2 * time.Second)), + Content: "job1 zstd enabled", + }, + { + Time: timestamppb.New(now.Add(3 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, + }, + }, + zstdEnabled: true, + }, + { + treePath: ".gitea/workflows/download-task-logs-no-zstd.yml", + fileContent: `name: download-task-logs-no-zstd +on: + push: + paths: + - '.gitea/workflows/download-task-logs-no-zstd.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 with zstd disabled +`, + outcome: &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(4 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(5 * time.Second)), + Content: "job1 zstd disabled", + }, + { + Time: timestamppb.New(now.Add(6 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, + }, + }, + zstdEnabled: false, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-download-task-logs", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + var resetFunc func() + if tc.zstdEnabled { + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd") + assert.True(t, setting.Actions.LogCompression.IsZstd()) + } else { + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none") + assert.False(t, setting.Actions.LogCompression.IsZstd()) + } + + // create the workflow file + opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts) + + // fetch and execute task + task := runner.fetchTask(t) + runner.execTask(t, task, tc.outcome) + + // check whether the log file exists + logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id) + if setting.Actions.LogCompression.IsZstd() { + logFileName += ".zst" + } + _, err := storage.Actions.Stat(logFileName) + assert.NoError(t, err) + + // download task logs and check content + runIndex := task.Context.GetFields()["run_number"].GetStringValue() + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n") + assert.Len(t, logTextLines, len(tc.outcome.logRows)) + for idx, lr := range tc.outcome.logRows { + assert.Equal( + t, + fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content), + logTextLines[idx], + ) + } + + resetFunc() + }) + } + + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} diff --git a/tests/integration/actions_runner_test.go b/tests/integration/actions_runner_test.go new file mode 100644 index 0000000000..355ea1705e --- /dev/null +++ b/tests/integration/actions_runner_test.go @@ -0,0 +1,157 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/setting" + + pingv1 "code.gitea.io/actions-proto-go/ping/v1" + "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type mockRunner struct { + client *mockRunnerClient +} + +type mockRunnerClient struct { + pingServiceClient pingv1connect.PingServiceClient + runnerServiceClient runnerv1connect.RunnerServiceClient +} + +func newMockRunner() *mockRunner { + client := newMockRunnerClient("", "") + return &mockRunner{client: client} +} + +func newMockRunnerClient(uuid, token string) *mockRunnerClient { + baseURL := fmt.Sprintf("%sapi/actions", setting.AppURL) + + opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if uuid != "" { + req.Header().Set("x-runner-uuid", uuid) + } + if token != "" { + req.Header().Set("x-runner-token", token) + } + return next(ctx, req) + } + })) + + client := &mockRunnerClient{ + pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt), + runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt), + } + + return client +} + +func (r *mockRunner) doPing(t *testing.T) { + resp, err := r.client.pingServiceClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ + Data: "mock-runner", + })) + assert.NoError(t, err) + assert.Equal(t, "Hello, mock-runner!", resp.Msg.Data) +} + +func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) { + r.doPing(t) + resp, err := r.client.runnerServiceClient.Register(context.Background(), connect.NewRequest(&runnerv1.RegisterRequest{ + Name: name, + Token: token, + Version: "mock-runner-version", + Labels: labels, + })) + assert.NoError(t, err) + r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token) +} + +func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) { + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var registrationToken struct { + Token string `json:"token"` + } + DecodeJSON(t, resp, ®istrationToken) + r.doRegister(t, runnerName, registrationToken.Token, labels) +} + +func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task { + fetchTimeout := 10 * time.Second + if len(timeout) > 0 { + fetchTimeout = timeout[0] + } + ddl := time.Now().Add(fetchTimeout) + var task *runnerv1.Task + for time.Now().Before(ddl) { + resp, err := r.client.runnerServiceClient.FetchTask(context.Background(), connect.NewRequest(&runnerv1.FetchTaskRequest{ + TasksVersion: 0, + })) + assert.NoError(t, err) + if resp.Msg.Task != nil { + task = resp.Msg.Task + break + } + time.Sleep(time.Second) + } + assert.NotNil(t, task, "failed to fetch a task") + return task +} + +type mockTaskOutcome struct { + result runnerv1.Result + outputs map[string]string + logRows []*runnerv1.LogRow + execTime time.Duration +} + +func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) { + for idx, lr := range outcome.logRows { + resp, err := r.client.runnerServiceClient.UpdateLog(context.Background(), connect.NewRequest(&runnerv1.UpdateLogRequest{ + TaskId: task.Id, + Index: int64(idx), + Rows: []*runnerv1.LogRow{lr}, + NoMore: idx == len(outcome.logRows)-1, + })) + assert.NoError(t, err) + assert.EqualValues(t, idx+1, resp.Msg.AckIndex) + } + sentOutputKeys := make([]string, 0, len(outcome.outputs)) + for outputKey, outputValue := range outcome.outputs { + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: runnerv1.Result_RESULT_UNSPECIFIED, + }, + Outputs: map[string]string{outputKey: outputValue}, + })) + assert.NoError(t, err) + sentOutputKeys = append(sentOutputKeys, outputKey) + assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs) + } + time.Sleep(outcome.execTime) + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: outcome.result, + StoppedAt: timestamppb.Now(), + }, + })) + assert.NoError(t, err) + assert.Equal(t, outcome.result, resp.Msg.State.Result) +} diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go index 0e4cd8243b..c9cdd46b9a 100644 --- a/tests/integration/api_issue_label_test.go +++ b/tests/integration/api_issue_label_test.go @@ -117,27 +117,33 @@ func TestAPIAddIssueLabels(t *testing.T) { func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) { assert.NoError(t, unittest.LoadFixtures()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: repo.ID}) + orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID}) - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", - repo.OwnerName, repo.Name, issue.Index) + user1Session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue) + + // add the org label and the repo label to the issue + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index) req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ - Labels: []any{"label1", "label2"}, + Labels: []any{repoLabel.Name, orgLabel.Name}, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiLabels []*api.Label DecodeJSON(t, resp, &apiLabels) assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID})) - var apiLabelNames []string for _, label := range apiLabels { apiLabelNames = append(apiLabelNames, label.Name) } - assert.ElementsMatch(t, apiLabelNames, []string{"label1", "label2"}) + assert.ElementsMatch(t, apiLabelNames, []string{repoLabel.Name, orgLabel.Name}) + + // delete labels + req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) } func TestAPIReplaceIssueLabels(t *testing.T) { diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 274fde4085..4c1053702e 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -274,7 +274,8 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist") assert.NoError(t, err) - activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com")) + activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user) + activateURL := fmt.Sprintf("/user/activate?code=%s", activationCode) req = NewRequestWithValues(t, "POST", activateURL, map[string]string{ "password": "examplePassword!1", }) diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 9812d2073d..4bc2a1da9a 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -226,3 +227,21 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) { assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url) }) } + +func TestCreateAgitPullWithReadPermission(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + dstPath := t.TempDir() + + u.Path = "user2/repo1.git" + u.User = url.UserPassword("user4", userPassword) + + t.Run("Clone", doGitClone(dstPath, u)) + + t.Run("add commit", doGitAddSomeCommits(dstPath, "master")) + + t.Run("do agit pull create", func(t *testing.T) { + err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + "test-topic").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + }) + }) +} diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index bb65d9e04a..fc066e06d3 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -30,7 +30,7 @@ func TestRepoCommits(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) } @@ -46,7 +46,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -64,7 +64,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc = NewHTMLParser(t, resp.Body) // Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger) - sel := doc.doc.Find("#commits-table tbody tr td.message .tippy-target .commit-status") + sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status") assert.Equal(t, 1, sel.Length()) for _, class := range classes { assert.True(t, sel.HasClass(class)) @@ -140,7 +140,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -175,7 +175,7 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -200,6 +200,6 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc = NewHTMLParser(t, resp.Body) // Check that the data-tippy="commit-statuses" (for trigger) and commit-status (svg) are present - sel := doc.doc.Find("#commits-table tbody tr td.message [data-tippy=\"commit-statuses\"] .commit-status") + sel := doc.doc.Find("#commits-table .message [data-tippy=\"commit-statuses\"] .commit-status") assert.Equal(t, 1, sel.Length()) } diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index e9a05201ee..e86851352e 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -99,34 +100,39 @@ func TestSignupEmailActive(t *testing.T) { // try to sign up and send the activation email req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ - "user_name": "test-user-1", - "email": "email-1@example.com", + "user_name": "Test-User-1", + "email": "EmAiL-1@example.com", "password": "password1", "retype": "password1", }) resp := MakeRequest(t, req, http.StatusOK) - assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to email-1@example.com.`) + assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to EmAiL-1@example.com.`) // access "user/activate" means trying to re-send the activation email session := loginUserWithPassword(t, "test-user-1", "password1") resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK) assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently") - // access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email + // access anywhere else will see an "Activate Your Account" prompt, and there is a chance to change email resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK) assert.Contains(t, resp.Body.String(), ` .commit-status-link { float: right; margin-right: 8px; @@ -936,14 +919,6 @@ td .commit-summary { width: 200px; } -.repository #commits-table thead .shatd { - text-align: center; -} - -.repository #commits-table td.sha .sha.label { - margin: 0; -} - .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) { background-color: var(--color-light) !important; } @@ -1440,12 +1415,6 @@ td .commit-summary { padding-top: 15px; } -.commit-header-row { - min-height: 50px !important; - padding-top: 0 !important; - padding-bottom: 0 !important; -} - .commit-header-buttons { display: flex; gap: 4px; @@ -1622,7 +1591,7 @@ td .commit-summary { align-items: center; } -.labels-list .label { +.labels-list .label, .scope-parent > .label { padding: 0 6px; min-height: 20px; line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ @@ -2128,18 +2097,6 @@ tbody.commit-list { .repository.view.issue .comment-list .timeline .comment-header-right .role-label { display: none; } - .commit-header-row .ui.horizontal.list { - width: 100%; - overflow-x: auto; - margin-top: 2px; - } - .commit-header-row .ui.horizontal.list .item { - align-items: center; - display: flex; - } - .commit-header-row .author { - padding: 3px 0; - } .commit-header h3 { flex-basis: auto !important; margin-bottom: 0.5rem !important; diff --git a/web_src/css/repo/commit-sign.css b/web_src/css/repo/commit-sign.css index e757030419..834fdd95d1 100644 --- a/web_src/css/repo/commit-sign.css +++ b/web_src/css/repo/commit-sign.css @@ -1,272 +1,60 @@ - -.repository .ui.attached.isSigned.isWarning { - border-left: 1px solid var(--color-error-border); - border-right: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.top, -.repository .ui.attached.isSigned.isWarning.message { - border-top: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.message { - box-shadow: none; - background-color: var(--color-error-bg); - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning.message .ui.text { - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning:last-child, -.repository .ui.attached.isSigned.isWarning.bottom { - border-bottom: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isVerified { - border-left: 1px solid var(--color-success-border); - border-right: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.top, -.repository .ui.attached.isSigned.isVerified.message { - border-top: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.message { - box-shadow: none; - background-color: var(--color-success-bg); - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified.message .pull-right { - color: var(--color-text); -} - -.repository .ui.attached.isSigned.isVerified.message .ui.text { - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified:last-child, -.repository .ui.attached.isSigned.isVerified.bottom { - border-bottom: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted, -.repository .ui.attached.isSigned.isVerifiedUnmatched { - border-left: 1px solid var(--color-warning-border); - border-right: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.top, -.repository .ui.attached.isSigned.isVerifiedUnmatched.top, -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - border-top: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - box-shadow: none; - background-color: var(--color-warning-bg); - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message .ui.text, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message .ui.text { - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted:last-child, -.repository .ui.attached.isSigned.isVerifiedUnmatched:last-child, -.repository .ui.attached.isSigned.isVerifiedUntrusted.bottom, -.repository .ui.attached.isSigned.isVerifiedUnmatched.bottom { - border-bottom: 1px solid var(--color-warning-border); -} - -.repository #commits-table td.sha .sha.label, -.repository #repo-files-table .sha.label, -.repository #repo-file-commit-box .sha.label, -.repository #rev-list .sha.label, -.repository .timeline-item.commits-list .singular-commit .sha.label { +.ui.label.commit-id-short, +.ui.label.commit-sign-badge { border: 1px solid var(--color-light-border); + font-size: 13px; + font-weight: var(--font-weight-normal); + padding: 3px 5px; + flex-shrink: 0; } -.repository #commits-table td.sha .sha.label .detail.icon, -.repository #repo-files-table .sha.label .detail.icon, -.repository #repo-file-commit-box .sha.label .detail.icon, -.repository #rev-list .sha.label .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon { - background: var(--color-light); - margin: -6px -10px -4px 0; - padding: 5px 4px 5px 6px; - border-left: 1px solid var(--color-light-border); - border-top: 0; - border-right: 0; - border-bottom: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; +.ui.label.commit-id-short { + font-family: var(--fonts-monospace); } -.repository #commits-table td.sha .sha.label .detail.icon .svg, -.repository #repo-files-table .sha.label .detail.icon .svg, -.repository #repo-file-commit-box .sha.label .detail.icon .svg, -.repository #rev-list .sha.label .detail.icon .svg, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon .svg { - margin: 0 0.25em 0 0; -} - -.repository #commits-table td.sha .sha.label .detail.icon > div, -.repository #repo-files-table .sha.label .detail.icon > div, -.repository #repo-file-commit-box .sha.label .detail.icon > div, -.repository #rev-list .sha.label .detail.icon > div, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon > div { - display: flex; - align-items: center; -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning, -.repository #repo-files-table .sha.label.isSigned.isWarning, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning, -.repository #rev-list .sha.label.isSigned.isWarning, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning .detail.icon, -.repository #rev-list .sha.label.isSigned.isWarning .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning .detail.icon { - border-left: 1px solid var(--color-red-badge); - color: var(--color-red-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning:hover, -.repository #repo-files-table .sha.label.isSigned.isWarning:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning:hover, -.repository #rev-list .sha.label.isSigned.isWarning:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified, -.repository #repo-files-table .sha.label.isSigned.isVerified, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified, -.repository #rev-list .sha.label.isSigned.isVerified, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerified .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified .detail.icon { - border-left: 1px solid var(--color-green-badge); - color: var(--color-green-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover, -.repository #repo-files-table .sha.label.isSigned.isVerified:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified:hover, -.repository #rev-list .sha.label.isSigned.isVerified:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified:hover { - background: var(--color-green-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted .detail.icon { - border-left: 1px solid var(--color-yellow-badge); - color: var(--color-yellow-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted:hover { - background: var(--color-yellow-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched .detail.icon { - border-left: 1px solid var(--color-orange-badge); - color: var(--color-orange-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched:hover { - background: var(--color-orange-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label { +.ui.label.commit-id-short > .commit-sign-badge { margin: 0; - border: 1px solid var(--color-light-border); + padding: 0; + border: 0 !important; + border-radius: 0; + background: transparent; } -.singular-commit .shabox .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); +.ui.label.commit-id-short > .commit-sign-badge:hover { + background: transparent !important; } -.singular-commit .shabox .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; +.commit-is-signed.sign-trusted { + border: 1px solid var(--color-green-badge) !important; + background: var(--color-green-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerified:hover { +.commit-is-signed.sign-trusted:hover { background: var(--color-green-badge-hover-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); +.commit-is-signed.sign-untrusted { + border: 1px solid var(--color-yellow-badge) !important; + background: var(--color-yellow-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { +.commit-is-signed.sign-untrusted:hover { background: var(--color-yellow-badge-hover-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); +.commit-is-signed.sign-unmatched { + border: 1px solid var(--color-orange-badge) !important; + background: var(--color-orange-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { +.commit-is-signed.sign-unmatched:hover { background: var(--color-orange-badge-hover-bg) !important; } + +.commit-is-signed.sign-warning { + border: 1px solid var(--color-red-badge) !important; + background: var(--color-red-badge-bg) !important; +} + +.commit-is-signed.sign-warning:hover { + background: var(--color-red-badge-hover-bg) !important; +}