Merge branch 'main' into dev/hezi/add-raw-diff-patch

This commit is contained in:
badhezi 2025-05-31 12:23:20 +03:00 committed by GitHub
commit f4a0fe369f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 333 additions and 509 deletions

View File

@ -230,18 +230,24 @@ func (status *CommitStatus) HideActionsURL(ctx context.Context) {
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
// This function is widely used, but it is not quite right.
// Ideally it should return something like "CommitStatusSummary" with properly aggregated state.
// GitHub's behavior: if all statuses are "skipped", GitHub will return "success" as the combined status.
var lastStatus *CommitStatus
state := api.CommitStatusSuccess
for _, status := range statuses {
if status.State.NoBetterThan(state) {
if state == status.State || status.State.HasHigherPriorityThan(state) {
state = status.State
lastStatus = status
}
}
if lastStatus == nil {
if len(statuses) > 0 {
// FIXME: a bad case: Gitea just returns the first commit status, its status is "skipped" in this case.
lastStatus = statuses[0]
} else {
// FIXME: another bad case: if the "statuses" slice is empty, the returned value is an invalid CommitStatus, all its fields are empty.
// Frontend code (tmpl&vue) sometimes depend on the empty fields to skip rendering commit status elements (need to double check in the future)
lastStatus = &CommitStatus{}
}
}

View File

@ -59,7 +59,11 @@ func UpdateCommitStatusSummary(ctx context.Context, repoID int64, sha string) er
if err != nil {
return err
}
state := CalcCommitStatus(commitStatuses)
// it guarantees that commitStatuses is not empty because this function is always called after a commit status is created
if len(commitStatuses) == 0 {
setting.PanicInDevOrTesting("no commit statuses found for repo %d and sha %s", repoID, sha)
}
state := CalcCommitStatus(commitStatuses) // non-empty commitStatuses is guaranteed
// mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database,
// so we need to use insert in on duplicate
if setting.Database.Type.IsMySQL() {

View File

@ -12,9 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@ -715,138 +713,13 @@ func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.Git
return err
}
// DeleteIssuesByRepoID deletes issues by repositories id
func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
sess := db.GetEngine(ctx)
for {
issueIDs := make([]int64, 0, db.DefaultMaxInSize)
err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
if err != nil {
return nil, err
}
if len(issueIDs) == 0 {
break
}
// Delete content histories
_, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
if err != nil {
return nil, err
}
// Delete comments and attachments
_, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
if err != nil {
return nil, err
}
// Dependencies for issues in this repository
_, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
if err != nil {
return nil, err
}
// Delete dependencies for issues in other repositories
_, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
if err != nil {
return nil, err
}
_, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
if err != nil {
return nil, err
}
_, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
if err != nil {
return nil, err
}
_, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
if err != nil {
return nil, err
}
_, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
if err != nil {
return nil, err
}
_, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
if err != nil {
return nil, err
}
_, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
if err != nil {
return nil, err
}
_, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
if err != nil {
return nil, err
}
var attachments []*repo_model.Attachment
err = sess.In("issue_id", issueIDs).Find(&attachments)
if err != nil {
return nil, err
}
for j := range attachments {
attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
}
_, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
if err != nil {
return nil, err
}
_, err = sess.In("id", issueIDs).Delete(&Issue{})
if err != nil {
return nil, err
}
func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) {
var repoIDs []int64
if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
Join("LEFT", "repository", "issue.repo_id=repository.id").
Where(builder.IsNull{"repository.id"}).
Find(&repoIDs); err != nil {
return nil, err
}
return attachmentPaths, err
}
// DeleteOrphanedIssues delete issues without a repo
func DeleteOrphanedIssues(ctx context.Context) error {
var attachmentPaths []string
err := db.WithTx(ctx, func(ctx context.Context) error {
var ids []int64
if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
Join("LEFT", "repository", "issue.repo_id=repository.id").
Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
Find(&ids); err != nil {
return err
}
for i := range ids {
paths, err := DeleteIssuesByRepoID(ctx, ids[i])
if err != nil {
return err
}
attachmentPaths = append(attachmentPaths, paths...)
}
return nil
})
if err != nil {
return err
}
// Remove issue attachment files.
for i := range attachmentPaths {
// FIXME: it's not right, because the attachment might not be on local filesystem
system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
}
return nil
return repoIDs, nil
}

View File

@ -43,21 +43,23 @@ func IsWorkflow(path string) bool {
return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows")
}
func ListWorkflows(commit *git.Commit) (git.Entries, error) {
tree, err := commit.SubTree(".gitea/workflows")
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
rpath := ".gitea/workflows"
tree, err := commit.SubTree(rpath)
if _, ok := err.(git.ErrNotExist); ok {
tree, err = commit.SubTree(".github/workflows")
rpath = ".github/workflows"
tree, err = commit.SubTree(rpath)
}
if _, ok := err.(git.ErrNotExist); ok {
return nil, nil
return "", nil, nil
}
if err != nil {
return nil, err
return "", nil, err
}
entries, err := tree.ListEntriesRecursiveFast()
if err != nil {
return nil, err
return "", nil, err
}
ret := make(git.Entries, 0, len(entries))
@ -66,7 +68,7 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) {
ret = append(ret, entry)
}
}
return ret, nil
return rpath, ret, nil
}
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
@ -102,7 +104,7 @@ func DetectWorkflows(
payload api.Payloader,
detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit)
_, entries, err := ListWorkflows(commit)
if err != nil {
return nil, nil, err
}
@ -147,7 +149,7 @@ func DetectWorkflows(
}
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit)
_, entries, err := ListWorkflows(commit)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,8 @@ const (
CommitStatusFailure CommitStatusState = "failure"
// CommitStatusWarning is for when the CommitStatus is Warning
CommitStatusWarning CommitStatusState = "warning"
// CommitStatusSkipped is for when CommitStatus is Skipped
CommitStatusSkipped CommitStatusState = "skipped"
)
var commitStatusPriorities = map[CommitStatusState]int{
@ -26,25 +28,17 @@ var commitStatusPriorities = map[CommitStatusState]int{
CommitStatusWarning: 2,
CommitStatusPending: 3,
CommitStatusSuccess: 4,
CommitStatusSkipped: 5,
}
func (css CommitStatusState) String() string {
return string(css)
}
// NoBetterThan returns true if this State is no better than the given State
// This function only handles the states defined in CommitStatusPriorities
func (css CommitStatusState) NoBetterThan(css2 CommitStatusState) bool {
// NoBetterThan only handles the 5 states above
if _, exist := commitStatusPriorities[css]; !exist {
return false
}
if _, exist := commitStatusPriorities[css2]; !exist {
return false
}
return commitStatusPriorities[css] <= commitStatusPriorities[css2]
// HasHigherPriorityThan returns true if this state has higher priority than the other
// Undefined states are considered to have the highest priority like CommitStatusError(0)
func (css CommitStatusState) HasHigherPriorityThan(other CommitStatusState) bool {
return commitStatusPriorities[css] < commitStatusPriorities[other]
}
// IsPending represents if commit status state is pending

View File

@ -10,165 +10,21 @@ import (
)
func TestNoBetterThan(t *testing.T) {
type args struct {
css CommitStatusState
css2 CommitStatusState
}
var unExpectedState CommitStatusState
tests := []struct {
name string
args args
want bool
s1, s2 CommitStatusState
higher bool
}{
{
name: "success is no better than success",
args: args{
css: CommitStatusSuccess,
css2: CommitStatusSuccess,
},
want: true,
},
{
name: "success is no better than pending",
args: args{
css: CommitStatusSuccess,
css2: CommitStatusPending,
},
want: false,
},
{
name: "success is no better than failure",
args: args{
css: CommitStatusSuccess,
css2: CommitStatusFailure,
},
want: false,
},
{
name: "success is no better than error",
args: args{
css: CommitStatusSuccess,
css2: CommitStatusError,
},
want: false,
},
{
name: "pending is no better than success",
args: args{
css: CommitStatusPending,
css2: CommitStatusSuccess,
},
want: true,
},
{
name: "pending is no better than pending",
args: args{
css: CommitStatusPending,
css2: CommitStatusPending,
},
want: true,
},
{
name: "pending is no better than failure",
args: args{
css: CommitStatusPending,
css2: CommitStatusFailure,
},
want: false,
},
{
name: "pending is no better than error",
args: args{
css: CommitStatusPending,
css2: CommitStatusError,
},
want: false,
},
{
name: "failure is no better than success",
args: args{
css: CommitStatusFailure,
css2: CommitStatusSuccess,
},
want: true,
},
{
name: "failure is no better than pending",
args: args{
css: CommitStatusFailure,
css2: CommitStatusPending,
},
want: true,
},
{
name: "failure is no better than failure",
args: args{
css: CommitStatusFailure,
css2: CommitStatusFailure,
},
want: true,
},
{
name: "failure is no better than error",
args: args{
css: CommitStatusFailure,
css2: CommitStatusError,
},
want: false,
},
{
name: "error is no better than success",
args: args{
css: CommitStatusError,
css2: CommitStatusSuccess,
},
want: true,
},
{
name: "error is no better than pending",
args: args{
css: CommitStatusError,
css2: CommitStatusPending,
},
want: true,
},
{
name: "error is no better than failure",
args: args{
css: CommitStatusError,
css2: CommitStatusFailure,
},
want: true,
},
{
name: "error is no better than error",
args: args{
css: CommitStatusError,
css2: CommitStatusError,
},
want: true,
},
{
name: "unExpectedState is no better than success",
args: args{
css: unExpectedState,
css2: CommitStatusSuccess,
},
want: false,
},
{
name: "unExpectedState is no better than unExpectedState",
args: args{
css: unExpectedState,
css2: unExpectedState,
},
want: false,
},
{CommitStatusError, CommitStatusFailure, true},
{CommitStatusFailure, CommitStatusWarning, true},
{CommitStatusWarning, CommitStatusPending, true},
{CommitStatusPending, CommitStatusSuccess, true},
{CommitStatusSuccess, CommitStatusSkipped, true},
{CommitStatusError, "unknown-xxx", false},
{"unknown-xxx", CommitStatusFailure, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.args.css.NoBetterThan(tt.args.css2)
assert.Equal(t, tt.want, result)
})
assert.Equal(t, tt.higher, tt.s1.HasHigherPriorityThan(tt.s2), "s1=%s, s2=%s, expected=%v", tt.s1, tt.s2, tt.higher)
}
assert.False(t, CommitStatusError.HasHigherPriorityThan(CommitStatusError))
}

View File

@ -3815,6 +3815,7 @@ runs.expire_log_message = Logs have been purged because they were too old.
runs.delete = Delete workflow run
runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone.
runs.not_done = This workflow run is not done.
runs.view_workflow_file = View workflow file
workflow.disable = Disable Workflow
workflow.disable_success = Workflow '%s' disabled successfully.

View File

@ -1228,6 +1228,7 @@ migrate.migrating_issues=Migration des tickets
migrate.migrating_pulls=Migration des demandes d'ajout
migrate.cancel_migrating_title=Annuler la migration
migrate.cancel_migrating_confirm=Voulez-vous abandonner cette migration ?
migrating_status=Migration des statuts
mirror_from=miroir de
forked_from=bifurqué depuis
@ -3813,6 +3814,7 @@ runs.expire_log_message=Les journaux ont été supprimés car ils étaient trop
runs.delete=Supprimer cette exécution
runs.delete.description=Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée.
runs.not_done=Cette exécution du flux de travail nest pas terminée.
runs.view_workflow_file=Voir le fichier du flux de travail
workflow.disable=Désactiver le flux de travail
workflow.disable_success=Le flux de travail « %s » a bien été désactivé.

View File

@ -3814,6 +3814,7 @@ runs.expire_log_message=Glanadh logaí toisc go raibh siad ró-sean.
runs.delete=Scrios rith sreabha oibre
runs.delete.description=An bhfuil tú cinnte gur mian leat an rith sreabha oibre seo a scriosadh go buan? Ní féidir an gníomh seo a chealú.
runs.not_done=Níl an rith sreabha oibre seo críochnaithe.
runs.view_workflow_file=Féach ar chomhad sreabha oibre
workflow.disable=Díchumasaigh sreabhadh oibre
workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhíchumasú.

View File

@ -130,6 +130,7 @@ pin=ピン留め
unpin=ピン留め解除
artifacts=成果物
expired=期限切れ
confirm_delete_artifact=アーティファクト %s を削除してよろしいですか?
archived=アーカイブ
@ -1227,6 +1228,7 @@ migrate.migrating_issues=イシュー移行中
migrate.migrating_pulls=プルリクエスト移行中
migrate.cancel_migrating_title=移行のキャンセル
migrate.cancel_migrating_confirm=この移行をキャンセルしますか?
migrating_status=移行状況
mirror_from=ミラー元
forked_from=フォーク元
@ -3812,6 +3814,7 @@ runs.expire_log_message=ログは古すぎるため消去されています。
runs.delete=ワークフローの実行を削除
runs.delete.description=このワークフローを完全に削除してもよろしいですか?この操作は元に戻せません。
runs.not_done=このワークフローの実行は完了していません。
runs.view_workflow_file=ワークフローファイルを表示
workflow.disable=ワークフローを無効にする
workflow.disable_success=ワークフロー '%s' が無効になりました。

View File

@ -3813,6 +3813,7 @@ runs.expire_log_message=Os registros foram removidos porque eram muito antigos.
runs.delete=Eliminar execução da sequência de trabalho
runs.delete.description=Tem a certeza que pretende eliminar permanentemente a execução desta sequência de trabalho? Esta operação não poderá ser desfeita.
runs.not_done=A execução desta sequência de trabalho ainda não terminou.
runs.view_workflow_file=Ver ficheiro da sequência de trabalho
workflow.disable=Desabilitar sequência de trabalho
workflow.disable_success=A sequência de trabalho '%s' foi desabilitada com sucesso.

View File

@ -126,7 +126,7 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
var curWorkflow *model.Workflow
entries, err := actions.ListWorkflows(commit)
_, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return nil

View File

@ -64,6 +64,36 @@ func View(ctx *context_module.Context) {
ctx.HTML(http.StatusOK, tplViewActions)
}
func ViewWorkflowFile(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
return errors.Is(err, util.ErrNotExist)
}, err)
return
}
commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA)
if err != nil {
ctx.NotFoundOrServerError("GetCommit", func(err error) bool {
return errors.Is(err, util.ErrNotExist)
}, err)
return
}
rpath, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return
}
for _, entry := range entries {
if entry.Name() == run.WorkflowID {
ctx.Redirect(fmt.Sprintf("%s/src/commit/%s/%s/%s", ctx.Repo.RepoLink, url.PathEscape(run.CommitSHA), util.PathEscapeSegments(rpath), util.PathEscapeSegments(run.WorkflowID)))
return
}
}
ctx.NotFound(nil)
}
type LogCursor struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`

View File

@ -1445,6 +1445,7 @@ func registerWebRoutes(m *web.Router) {
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
m.Get("/logs", actions.Logs)
})
m.Get("/workflow", actions.ViewWorkflowFile)
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
m.Post("/delete", reqRepoActionsWriter, actions.Delete)

View File

@ -149,12 +149,14 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
func toCommitStatus(status actions_model.Status) api.CommitStatusState {
switch status {
case actions_model.StatusSuccess, actions_model.StatusSkipped:
case actions_model.StatusSuccess:
return api.CommitStatusSuccess
case actions_model.StatusFailure, actions_model.StatusCancelled:
return api.CommitStatusFailure
case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning:
return api.CommitStatusPending
case actions_model.StatusSkipped:
return api.CommitStatusSkipped
default:
return api.CommitStatusError
}

View File

@ -31,16 +31,6 @@ import (
"github.com/nektos/act/pkg/model"
)
func getActionWorkflowPath(commit *git.Commit) string {
paths := []string{".gitea/workflows", ".github/workflows"}
for _, treePath := range paths {
if _, err := commit.SubTree(treePath); err == nil {
return treePath
}
}
return ""
}
func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
@ -109,14 +99,12 @@ func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error)
return nil, err
}
entries, err := actions.ListWorkflows(defaultBranchCommit)
folder, entries, err := actions.ListWorkflows(defaultBranchCommit)
if err != nil {
ctx.APIError(http.StatusNotFound, err.Error())
return nil, err
}
folder := getActionWorkflowPath(defaultBranchCommit)
workflows := make([]*api.ActionWorkflow, len(entries))
for i, entry := range entries {
workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry)
@ -185,7 +173,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
}
// get workflow entry from runTargetCommit
entries, err := actions.ListWorkflows(runTargetCommit)
_, entries, err := actions.ListWorkflows(runTargetCommit)
if err != nil {
return err
}

View File

@ -42,13 +42,14 @@ func ToCombinedStatus(ctx context.Context, statuses []*git_model.CommitStatus, r
SHA: statuses[0].SHA,
TotalCount: len(statuses),
Repository: repo,
URL: "",
URL: "", // never set or used?
State: api.CommitStatusSuccess,
}
retStatus.Statuses = make([]*api.CommitStatus, 0, len(statuses))
for _, status := range statuses {
retStatus.Statuses = append(retStatus.Statuses, ToCommitStatus(ctx, status))
if retStatus.State == "" || status.State.NoBetterThan(retStatus.State) {
if status.State.HasHigherPriorityThan(retStatus.State) {
retStatus.State = status.State
}
}
@ -57,9 +58,13 @@ func ToCombinedStatus(ctx context.Context, statuses []*git_model.CommitStatus, r
// > failure if any of the contexts report as error or failure
// > pending if there are no statuses or a context is pending
// > success if the latest status for all contexts is success
if retStatus.State.IsError() {
retStatus.State = api.CommitStatusFailure
switch retStatus.State {
case api.CommitStatusSkipped:
retStatus.State = api.CommitStatusSuccess // all skipped means success
case api.CommitStatusPending, api.CommitStatusSuccess:
// use the current state for pending or success
default:
retStatus.State = api.CommitStatusFailure // otherwise, it is a failure
}
return retStatus
}

View File

@ -15,6 +15,7 @@ import (
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
issue_service "code.gitea.io/gitea/services/issue"
)
type consistencyCheck struct {
@ -93,7 +94,7 @@ func prepareDBConsistencyChecks() []consistencyCheck {
// find issues without existing repository
Name: "Orphaned Issues without existing repository",
Counter: issues_model.CountOrphanedIssues,
Fixer: asFixer(issues_model.DeleteOrphanedIssues),
Fixer: asFixer(issue_service.DeleteOrphanedIssues),
},
// find releases without existing repository
genericOrphanCheck("Orphaned Releases without existing repository",

View File

@ -190,9 +190,13 @@ func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Reposi
}
// delete entries in database
if err := deleteIssue(ctx, issue); err != nil {
attachmentPaths, err := deleteIssue(ctx, issue)
if err != nil {
return err
}
for _, attachmentPath := range attachmentPaths {
system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPath)
}
// delete pull request related git data
if issue.IsPull && gitRepo != nil {
@ -256,45 +260,45 @@ func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[i
}
// deleteIssue deletes the issue
func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
return nil, err
}
defer committer.Close()
e := db.GetEngine(ctx)
if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
return err
if _, err := db.GetEngine(ctx).ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
return nil, err
}
// update the total issue numbers
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil {
return err
return nil, err
}
// if the issue is closed, update the closed issue numbers
if issue.IsClosed {
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
return err
return nil, err
}
}
if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
return fmt.Errorf("error updating counters for milestone id %d: %w",
return nil, fmt.Errorf("error updating counters for milestone id %d: %w",
issue.MilestoneID, err)
}
if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil {
return err
return nil, err
}
// find attachments related to this issue and remove them
if err := issue.LoadAttributes(ctx); err != nil {
return err
if err := issue.LoadAttachments(ctx); err != nil {
return nil, err
}
var attachmentPaths []string
for i := range issue.Attachments {
system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath())
attachmentPaths = append(attachmentPaths, issue.Attachments[i].RelativePath())
}
// delete all database data still assigned to this issue
@ -318,8 +322,68 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
&issues_model.Comment{DependentIssueID: issue.ID},
&issues_model.IssuePin{IssueID: issue.ID},
); err != nil {
return nil, err
}
if err := committer.Commit(); err != nil {
return nil, err
}
return attachmentPaths, nil
}
// DeleteOrphanedIssues delete issues without a repo
func DeleteOrphanedIssues(ctx context.Context) error {
var attachmentPaths []string
err := db.WithTx(ctx, func(ctx context.Context) error {
repoIDs, err := issues_model.GetOrphanedIssueRepoIDs(ctx)
if err != nil {
return err
}
for i := range repoIDs {
paths, err := DeleteIssuesByRepoID(ctx, repoIDs[i])
if err != nil {
return err
}
attachmentPaths = append(attachmentPaths, paths...)
}
return nil
})
if err != nil {
return err
}
return committer.Commit()
// Remove issue attachment files.
for i := range attachmentPaths {
system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
}
return nil
}
// DeleteIssuesByRepoID deletes issues by repositories id
func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
for {
issues := make([]*issues_model.Issue, 0, db.DefaultMaxInSize)
if err := db.GetEngine(ctx).
Where("repo_id = ?", repoID).
OrderBy("id").
Limit(db.DefaultMaxInSize).
Find(&issues); err != nil {
return nil, err
}
if len(issues) == 0 {
break
}
for _, issue := range issues {
issueAttachPaths, err := deleteIssue(ctx, issue)
if err != nil {
return nil, fmt.Errorf("deleteIssue [issue_id: %d]: %w", issue.ID, err)
}
attachmentPaths = append(attachmentPaths, issueAttachPaths...)
}
}
return attachmentPaths, err
}

View File

@ -44,7 +44,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
ID: issueIDs[2],
}
err = deleteIssue(db.DefaultContext, issue)
_, err = deleteIssue(db.DefaultContext, issue)
assert.NoError(t, err)
issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
assert.NoError(t, err)
@ -55,7 +55,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
assert.NoError(t, err)
issue, err = issues_model.GetIssueByID(db.DefaultContext, 4)
assert.NoError(t, err)
err = deleteIssue(db.DefaultContext, issue)
_, err = deleteIssue(db.DefaultContext, issue)
assert.NoError(t, err)
assert.Len(t, attachments, 2)
for i := range attachments {
@ -78,7 +78,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
assert.NoError(t, err)
assert.False(t, left)
err = deleteIssue(db.DefaultContext, issue2)
_, err = deleteIssue(db.DefaultContext, issue2)
assert.NoError(t, err)
left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
assert.NoError(t, err)

View File

@ -46,59 +46,33 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
// If required rule not match any action, then it is pending
if targetStatus == "" {
if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
if structs.CommitStatusPending.HasHigherPriorityThan(returnedStatus) {
returnedStatus = structs.CommitStatusPending
}
break
}
if targetStatus.NoBetterThan(returnedStatus) {
if targetStatus.HasHigherPriorityThan(returnedStatus) {
returnedStatus = targetStatus
}
}
}
if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess {
status := git_model.CalcCommitStatus(commitStatuses)
if status != nil {
return status.State
if len(commitStatuses) == 0 {
// "no statuses" should mean "pending"
return structs.CommitStatusPending
}
return structs.CommitStatusSuccess
status := git_model.CalcCommitStatus(commitStatuses)
if status.State == structs.CommitStatusSkipped {
return structs.CommitStatusSuccess // if all statuses are skipped, return success
}
return status.State
}
return returnedStatus
}
// IsCommitStatusContextSuccess returns true if all required status check contexts succeed.
func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requiredContexts []string) bool {
// If no specific context is required, require that last commit status is a success
if len(requiredContexts) == 0 {
status := git_model.CalcCommitStatus(commitStatuses)
if status == nil || status.State != structs.CommitStatusSuccess {
return false
}
return true
}
for _, ctx := range requiredContexts {
var found bool
for _, commitStatus := range commitStatuses {
if commitStatus.Context == ctx {
if commitStatus.State != structs.CommitStatusSuccess {
return false
}
found = true
break
}
}
if !found {
return false
}
}
return true
}
// IsPullCommitStatusPass returns if all required status checks PASS
func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)

View File

@ -14,52 +14,70 @@ import (
)
func TestMergeRequiredContextsCommitStatus(t *testing.T) {
testCases := [][]*git_model.CommitStatus{
cases := []struct {
commitStatuses []*git_model.CommitStatus
requiredContexts []string
expected structs.CommitStatusState
}{
{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 3", State: structs.CommitStatusSuccess},
commitStatuses: []*git_model.CommitStatus{},
requiredContexts: []string{},
expected: structs.CommitStatusPending,
},
{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusPending},
commitStatuses: []*git_model.CommitStatus{
{Context: "Build xxx", State: structs.CommitStatusSkipped},
},
requiredContexts: []string{"Build*"},
expected: structs.CommitStatusSuccess,
},
{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusFailure},
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSkipped},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 3", State: structs.CommitStatusSuccess},
},
requiredContexts: []string{"Build*"},
expected: structs.CommitStatusSuccess,
},
{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusSuccess},
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusPending},
},
requiredContexts: []string{"Build*", "Build 2t*"},
expected: structs.CommitStatusPending,
},
{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusSuccess},
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusFailure},
},
requiredContexts: []string{"Build*", "Build 2t*"},
expected: structs.CommitStatusFailure,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusSuccess},
},
requiredContexts: []string{"Build*", "Build 2t*", "Build 3*"},
expected: structs.CommitStatusPending,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusSuccess},
},
requiredContexts: []string{"Build*", "Build *", "Build 2t*", "Build 1*"},
expected: structs.CommitStatusSuccess,
},
}
testCasesRequiredContexts := [][]string{
{"Build*"},
{"Build*", "Build 2t*"},
{"Build*", "Build 2t*"},
{"Build*", "Build 2t*", "Build 3*"},
{"Build*", "Build *", "Build 2t*", "Build 1*"},
}
testCasesExpected := []structs.CommitStatusState{
structs.CommitStatusSuccess,
structs.CommitStatusPending,
structs.CommitStatusFailure,
structs.CommitStatusPending,
structs.CommitStatusSuccess,
}
for i, commitStatuses := range testCases {
if MergeRequiredContextsCommitStatus(commitStatuses, testCasesRequiredContexts[i]) != testCasesExpected[i] {
assert.Fail(t, "Test case failed", "Test case %d failed", i+1)
}
for i, c := range cases {
assert.Equal(t, c.expected, MergeRequiredContextsCommitStatus(c.commitStatuses, c.requiredContexts), "case %d", i)
}
}

View File

@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/storage"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
issue_service "code.gitea.io/gitea/services/issue"
"xorm.io/builder"
)
@ -193,7 +194,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
// Delete Issues and related objects
var attachmentPaths []string
if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil {
if attachmentPaths, err = issue_service.DeleteIssuesByRepoID(ctx, repoID); err != nil {
return err
}

View File

@ -39,10 +39,7 @@
<div class="ui dropdown jump tw-p-2">
{{svg "octicon-kebab-horizontal"}}
<div class="menu flex-items-menu">
<!-- TODO: This redundant link should be replaced by something else in future,
because have not figured out how to add "View Workflow" or anything similar to GitHub.
Related: https://github.com/go-gitea/gitea/pull/34530 -->
<a class="item" href="{{$run.Link}}">{{svg "octicon-play"}}{{ctx.Locale.Tr "view"}}</a>
<a class="item" href="{{$run.Link}}/workflow">{{svg "octicon-play"}}{{ctx.Locale.Tr "actions.runs.view_workflow_file"}}</a>
{{if and $.AllowDeleteWorkflowRuns $run.Status.IsDone}}
<a class="item link-action"
data-url="{{$run.Link}}/delete"

View File

@ -14,3 +14,6 @@
{{if eq .State "warning"}}
{{svg "gitea-exclamation" 18 "commit-status icon text yellow"}}
{{end}}
{{if eq .State "skipped"}}
{{svg "octicon-skip" 18 "commit-status icon text grey"}}
{{end}}

View File

@ -815,10 +815,6 @@ overflow-menu .ui.label {
display: block;
}
.code-view .lines-num span::after {
cursor: pointer;
}
.lines-type-marker {
vertical-align: top;
white-space: nowrap;
@ -855,39 +851,13 @@ overflow-menu .ui.label {
.lines-escape {
width: 0;
white-space: nowrap;
padding: 0;
}
.lines-code {
padding-left: 5px;
}
.file-view tr.active {
color: inherit !important;
background: inherit !important;
}
.file-view tr.active .lines-num,
.file-view tr.active .lines-code {
background: var(--color-highlight-bg) !important;
}
.file-view tr.active:last-of-type .lines-code {
border-bottom-right-radius: var(--border-radius);
}
.file-view tr.active .lines-num {
position: relative;
}
.file-view tr.active .lines-num::before {
content: "";
position: absolute;
left: 0;
width: 2px;
height: 100%;
background: var(--color-highlight-fg);
}
.code-inner {
font: 12px var(--fonts-monospace);
white-space: pre-wrap;
@ -938,12 +908,12 @@ overflow-menu .ui.label {
margin-right: 4px;
}
.top-line-blame {
tr.top-line-blame {
border-top: 1px solid var(--color-secondary);
}
.code-view tr.top-line-blame:first-of-type {
border-top: none;
tr.top-line-blame:first-of-type {
border-top: none; /* merge code lines belonging to the same commit into one block */
}
.lines-code .bottom-line,
@ -951,15 +921,6 @@ overflow-menu .ui.label {
border-bottom: 1px solid var(--color-secondary);
}
.code-view {
background: var(--color-code-bg);
border-radius: var(--border-radius);
}
.code-view table {
width: 100%;
}
.migrate .svg.gitea-git {
color: var(--color-git);
}

View File

@ -62,7 +62,7 @@
@import "./repo/issue-label.css";
@import "./repo/issue-list.css";
@import "./repo/list-header.css";
@import "./repo/linebutton.css";
@import "./repo/file-view.css";
@import "./repo/wiki.css";
@import "./repo/header.css";
@import "./repo/home.css";

View File

@ -309,10 +309,18 @@
box-sizing: initial;
}
.file-view.markup {
padding: 1em 2em;
}
.file-view.markup:has(.file-not-rendered-prompt) {
padding: 0; /* let the file-not-rendered-prompt layout itself */
}
/* this background ensures images can break <hr>. We can only do this on
cases where the background is known and not transparent. */
.markup.file-view img,
.markup.file-view video,
.file-view.markup img,
.file-view.markup video,
.comment-body .markup img, /* regular comment */
.comment-body .markup video,
.comment-content .markup img, /* code comment */

View File

@ -1238,21 +1238,6 @@ td .commit-summary {
white-space: nowrap;
}
.file-view.markup {
padding: 1em 2em;
}
.file-view.markup:has(.file-not-rendered-prompt) {
padding: 0; /* let the file-not-rendered-prompt layout itself */
}
.file-not-rendered-prompt {
padding: 1rem;
text-align: center;
font-size: 1rem !important; /* use consistent styles for various containers (code, markup, etc) */
line-height: var(--line-height-default) !important; /* same as above */
}
.repository .activity-header {
display: flex;
justify-content: space-between;

View File

@ -0,0 +1,58 @@
.file-view tr.active {
background: var(--color-highlight-bg);
}
.file-view tr.active:last-of-type .lines-code {
border-bottom-right-radius: var(--border-radius);
}
.file-view tr.active .lines-num {
position: relative;
}
.file-view tr.active .lines-num::before {
content: "";
position: absolute;
left: 0;
width: 2px;
height: 100%;
background: var(--color-highlight-fg);
}
.file-view .file-not-rendered-prompt {
padding: 1rem;
text-align: center;
font-size: 1rem !important; /* use consistent styles for various containers (code, markup, etc) */
line-height: var(--line-height-default) !important; /* same as above */
}
/* ".code-view" is always used with ".file-view", to show the code of a file */
.file-view.code-view {
background: var(--color-code-bg);
border-radius: var(--border-radius);
}
.file-view.code-view table {
width: 100%;
}
.file-view.code-view .lines-num span::after {
cursor: pointer;
}
.file-view.code-view .lines-num:hover {
color: var(--color-text-dark);
}
.file-view.code-view .ui.button.code-line-button {
border: 1px solid var(--color-secondary);
padding: 1px 4px;
margin: 0;
min-height: 0;
position: absolute;
left: 6px;
}
.file-view.code-view .ui.button.code-line-button:hover {
background: var(--color-secondary);
}

View File

@ -1,16 +0,0 @@
.code-view .lines-num:hover {
color: var(--color-text-dark) !important;
}
.ui.button.code-line-button {
border: 1px solid var(--color-secondary);
padding: 1px 4px;
margin: 0;
min-height: 0;
position: absolute;
left: 6px;
}
.ui.button.code-line-button:hover {
background: var(--color-secondary);
}

View File

@ -6,7 +6,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning';
type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped';
type CommitStatusMap = {
[status in CommitStatus]: {
@ -22,6 +22,7 @@ const commitStatus: CommitStatusMap = {
error: {name: 'gitea-exclamation', color: 'red'},
failure: {name: 'octicon-x', color: 'red'},
warning: {name: 'gitea-exclamation', color: 'yellow'},
skipped: {name: 'octicon-skip', color: 'grey'},
};
export default defineComponent({