Remove unneeded doctor sub-commands (#37156)

Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
wxiaoguang
2026-04-10 04:22:17 +08:00
committed by GitHub
parent 980a8995bc
commit c10a5b908a
15 changed files with 1 additions and 1268 deletions

View File

@@ -32,7 +32,7 @@ func TestUpdateRepoRunsNumbers(t *testing.T) {
err = UpdateRepoRunsNumbers(t.Context(), repo)
assert.NoError(t, err)
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.Equal(t, 5, repo.NumActionRuns)
assert.Equal(t, 4, repo.NumActionRuns)
assert.Equal(t, 3, repo.NumClosedActionRuns)
}

View File

@@ -140,25 +140,4 @@
need_approval: 0
approved_by: 0
-
id: 805
title: "update actions"
repo_id: 4
owner_id: 1
workflow_id: "artifact.yaml"
index: 191
trigger_user_id: 1
ref: "refs/heads/master"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push"
trigger_event: "push"
is_fork_pull_request: 0
status: 5
started: 1683636528
stopped: 1683636626
created: 1683636108
updated: 1683636626
need_approval: 0
approved_by: 0
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly

View File

@@ -130,19 +130,4 @@
started: 1683636528
stopped: 1683636626
-
id: 206
run_id: 805
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
name: job_2
attempt: 1
job_id: job_2
task_id: 56
status: 3
started: 1683636528
stopped: 1683636626
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly

View File

@@ -178,24 +178,4 @@
log_size: 0
log_expired: 0
-
id: 56
attempt: 1
runner_id: 1
status: 3 # 3 is the status code for "cancelled"
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab
token_salt: eeeeeeee
token_last_eight: eeeeeeee
log_filename: artifact-test2/2f/47.log
log_in_storage: 1
log_length: 707
log_size: 90179
log_expired: 0
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly

View File

@@ -1462,16 +1462,6 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
return false
}
// CountWrongUserType count OrgUser who have wrong type
func CountWrongUserType(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Count(new(User))
}
// FixWrongUserType fix OrgUser who have wrong type
func FixWrongUserType(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Cols("type").NoAutoTime().Update(&User{Type: 1})
}
func GetOrderByName() string {
if setting.UI.DefaultShowFullName {
return "full_name, name"

View File

@@ -7,17 +7,12 @@ import (
"context"
"fmt"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
repo_service "code.gitea.io/gitea/services/repository"
"xorm.io/builder"
)
func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error {
@@ -64,95 +59,6 @@ func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bo
return nil
}
func fixUnfinishedRunStatus(ctx context.Context, logger log.Logger, autofix bool) error {
total := 0
inconsistent := 0
fixed := 0
cond := builder.In("status", []actions_model.Status{
actions_model.StatusWaiting,
actions_model.StatusRunning,
actions_model.StatusBlocked,
}).And(builder.Lt{"updated": timeutil.TimeStampNow().AddDuration(-setting.Actions.ZombieTaskTimeout)})
err := db.Iterate(
ctx,
cond,
func(ctx context.Context, run *actions_model.ActionRun) error {
total++
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
return fmt.Errorf("GetRunJobsByRunID: %w", err)
}
expected := actions_model.AggregateJobStatus(jobs)
if expected == run.Status {
return nil
}
inconsistent++
logger.Warn("Run %d (repo_id=%d, index=%d) has status %s, expected %s", run.ID, run.RepoID, run.Index, run.Status, expected)
if !autofix {
return nil
}
run.Started, run.Stopped = getRunTimestampsFromJobs(run, expected, jobs)
run.Status = expected
if err := actions_model.UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
return fmt.Errorf("UpdateRun: %w", err)
}
fixed++
return nil
},
)
if err != nil {
logger.Critical("Unable to iterate unfinished runs: %v", err)
return err
}
if inconsistent == 0 {
logger.Info("Checked %d unfinished runs; all statuses are consistent.", total)
return nil
}
if autofix {
logger.Info("Checked %d unfinished runs; fixed %d of %d runs.", total, fixed, inconsistent)
} else {
logger.Warn("Checked %d unfinished runs; found %d runs need to be fixed", total, inconsistent)
}
return nil
}
func getRunTimestampsFromJobs(run *actions_model.ActionRun, newStatus actions_model.Status, jobs actions_model.ActionJobList) (started, stopped timeutil.TimeStamp) {
started = run.Started
if (newStatus.IsRunning() || newStatus.IsDone()) && started.IsZero() {
var earliest timeutil.TimeStamp
for _, job := range jobs {
if job.Started > 0 && (earliest.IsZero() || job.Started < earliest) {
earliest = job.Started
}
}
started = earliest
}
stopped = run.Stopped
if newStatus.IsDone() && stopped.IsZero() {
var latest timeutil.TimeStamp
for _, job := range jobs {
if job.Stopped > latest {
latest = job.Stopped
}
}
stopped = latest
}
return started, stopped
}
func init() {
Register(&Check{
Title: "Disable the actions unit for all mirrors",
@@ -161,11 +67,4 @@ func init() {
Run: disableMirrorActionsUnit,
Priority: 9,
})
Register(&Check{
Title: "Fix inconsistent status for unfinished actions runs",
Name: "fix-actions-unfinished-run-status",
IsDefault: false,
Run: fixUnfinishedRunStatus,
Priority: 9,
})
}

View File

@@ -1,24 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"testing"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/log"
"github.com/stretchr/testify/assert"
)
func Test_fixUnfinishedRunStatus(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
fixUnfinishedRunStatus(t.Context(), log.GetLogger(log.DEFAULT), true)
// check if the run is cancelled by id
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 805})
assert.Equal(t, actions_model.StatusCancelled, run.Status)
}

View File

@@ -1,97 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"xorm.io/builder"
)
func iterateUserAccounts(ctx context.Context, each func(*user.User) error) error {
err := db.Iterate(
ctx,
builder.Gt{"id": 0},
func(ctx context.Context, bean *user.User) error {
return each(bean)
},
)
return err
}
// Since 1.16.4 new restrictions has been set on email addresses. However users with invalid email
// addresses would be currently facing a error due to their invalid email address.
// Ref: https://github.com/go-gitea/gitea/pull/19085 & https://github.com/go-gitea/gitea/pull/17688
func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error {
// We could use quirky SQL to get all users that start without a [a-zA-Z0-9], but that would mean
// DB provider-specific SQL and only works _now_. So instead we iterate through all user accounts
// and use the user.ValidateEmail function to be future-proof.
var invalidUserCount int64
if err := iterateUserAccounts(ctx, func(u *user.User) error {
// Only check for users, skip
if u.Type != user.UserTypeIndividual {
return nil
}
if err := user.ValidateEmail(u.Email); err != nil {
invalidUserCount++
logger.Warn("User[id=%d name=%q] have not a valid e-mail: %v", u.ID, u.Name, err)
}
return nil
}); err != nil {
return fmt.Errorf("iterateUserAccounts: %w", err)
}
if invalidUserCount == 0 {
logger.Info("All users have a valid e-mail.")
} else {
logger.Warn("%d user(s) have a non-valid e-mail.", invalidUserCount)
}
return nil
}
// From time to time Gitea makes changes to the reserved usernames and which symbols
// are allowed for various reasons. This check helps with detecting users that, according
// to our reserved names, don't have a valid username.
func checkUserName(ctx context.Context, logger log.Logger, _ bool) error {
var invalidUserCount int64
if err := iterateUserAccounts(ctx, func(u *user.User) error {
if err := user.IsUsableUsername(u.Name); err != nil {
invalidUserCount++
logger.Warn("User[id=%d] does not have a valid username: %v", u.ID, err)
}
return nil
}); err != nil {
return fmt.Errorf("iterateUserAccounts: %w", err)
}
if invalidUserCount == 0 {
logger.Info("All users have a valid username.")
} else {
logger.Warn("%d user(s) have a non-valid username.", invalidUserCount)
}
return nil
}
func init() {
Register(&Check{
Title: "Check if users has an valid email address",
Name: "check-user-email",
IsDefault: false,
Run: checkUserEmail,
Priority: 9,
})
Register(&Check{
Title: "Check if users have a valid username",
Name: "check-user-names",
IsDefault: false,
Run: checkUserName,
Priority: 9,
})
}

View File

@@ -1,43 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/versioned_migration"
)
func checkDBVersion(ctx context.Context, logger log.Logger, autofix bool) error {
logger.Info("Expected database version: %d", migrations.ExpectedDBVersion())
if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
if !autofix {
logger.Critical("Error: %v during ensure up to date", err)
return err
}
logger.Warn("Got Error: %v during ensure up to date", err)
logger.Warn("Attempting to migrate to the latest DB version to fix this.")
err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate)
if err != nil {
logger.Critical("Error: %v during migration", err)
}
return err
}
return nil
}
func init() {
Register(&Check{
Title: "Check Database Version",
Name: "check-db-version",
IsDefault: true,
Run: checkDBVersion,
AbortIfFailed: false,
Priority: 2,
})
}

View File

@@ -1,328 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"bytes"
"context"
"errors"
"fmt"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)
// #16831 revealed that the dump command that was broken in 1.14.3-1.14.6 and 1.15.0 (#15885).
// This led to repo_unit and login_source cfg not being converted to JSON in the dump
// Unfortunately although it was hoped that there were only a few users affected it
// appears that many users are affected.
// We therefore need to provide a doctor command to fix this repeated issue #16961
func parseBool16961(bs []byte) (bool, error) {
if bytes.EqualFold(bs, []byte("%!s(bool=false)")) {
return false, nil
}
if bytes.EqualFold(bs, []byte("%!s(bool=true)")) {
return true, nil
}
return false, fmt.Errorf("unexpected bool format: %s", string(bs))
}
func fixUnitConfig16961(bs []byte, cfg *repo_model.UnitConfig) (fixed bool, err error) {
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return false, nil
}
// Handle #16961
if string(bs) != "&{}" && len(bs) != 0 {
return false, err
}
return true, nil
}
func fixExternalWikiConfig16961(bs []byte, cfg *repo_model.ExternalWikiConfig) (fixed bool, err error) {
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return false, nil
}
if len(bs) < 3 {
return false, err
}
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return false, err
}
cfg.ExternalWikiURL = string(bs[2 : len(bs)-1])
return true, nil
}
func fixExternalTrackerConfig16961(bs []byte, cfg *repo_model.ExternalTrackerConfig) (fixed bool, err error) {
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return false, nil
}
// Handle #16961
if len(bs) < 3 {
return false, err
}
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return false, err
}
parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
if len(parts) != 3 {
return false, err
}
cfg.ExternalTrackerURL = string(bytes.Join(parts[:len(parts)-2], []byte{' '}))
cfg.ExternalTrackerFormat = string(parts[len(parts)-2])
cfg.ExternalTrackerStyle = string(parts[len(parts)-1])
return true, nil
}
func fixPullRequestsConfig16961(bs []byte, cfg *repo_model.PullRequestsConfig) (fixed bool, err error) {
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return false, nil
}
// Handle #16961
if len(bs) < 3 {
return false, err
}
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return false, err
}
// PullRequestsConfig was the following in 1.14
// type PullRequestsConfig struct {
// IgnoreWhitespaceConflicts bool
// AllowMerge bool
// AllowRebase bool
// AllowRebaseMerge bool
// AllowSquash bool
// AllowManualMerge bool
// AutodetectManualMerge bool
// }
//
// 1.15 added in addition:
// DefaultDeleteBranchAfterMerge bool
// DefaultMergeStyle MergeStyle
parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
if len(parts) < 7 {
return false, err
}
var parseErr error
cfg.IgnoreWhitespaceConflicts, parseErr = parseBool16961(parts[0])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.AllowMerge, parseErr = parseBool16961(parts[1])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.AllowRebase, parseErr = parseBool16961(parts[2])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.AllowRebaseMerge, parseErr = parseBool16961(parts[3])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.AllowSquash, parseErr = parseBool16961(parts[4])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.AllowManualMerge, parseErr = parseBool16961(parts[5])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.AutodetectManualMerge, parseErr = parseBool16961(parts[6])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
// 1.14 unit
if len(parts) == 7 {
return true, nil
}
if len(parts) < 9 {
return false, err
}
cfg.DefaultDeleteBranchAfterMerge, parseErr = parseBool16961(parts[7])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.DefaultMergeStyle = repo_model.MergeStyle(string(bytes.Join(parts[8:], []byte{' '})))
return true, nil
}
func fixIssuesConfig16961(bs []byte, cfg *repo_model.IssuesConfig) (fixed bool, err error) {
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return false, nil
}
// Handle #16961
if len(bs) < 3 {
return false, err
}
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return false, err
}
parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
if len(parts) != 3 {
return false, err
}
var parseErr error
cfg.EnableTimetracker, parseErr = parseBool16961(parts[0])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.AllowOnlyContributorsToTrackTime, parseErr = parseBool16961(parts[1])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
cfg.EnableDependencies, parseErr = parseBool16961(parts[2])
if parseErr != nil {
return false, errors.Join(err, parseErr)
}
return true, nil
}
func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed bool, err error) {
// Shortcut empty or null values
if len(bs) == 0 {
return false, nil
}
var cfg any
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return false, nil
}
switch repoUnit.Type {
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
cfg := &repo_model.UnitConfig{}
repoUnit.Config = cfg
if fixed, err := fixUnitConfig16961(bs, cfg); !fixed {
return false, err
}
case unit.TypeExternalWiki:
cfg := &repo_model.ExternalWikiConfig{}
repoUnit.Config = cfg
if fixed, err := fixExternalWikiConfig16961(bs, cfg); !fixed {
return false, err
}
case unit.TypeExternalTracker:
cfg := &repo_model.ExternalTrackerConfig{}
repoUnit.Config = cfg
if fixed, err := fixExternalTrackerConfig16961(bs, cfg); !fixed {
return false, err
}
case unit.TypePullRequests:
cfg := &repo_model.PullRequestsConfig{}
repoUnit.Config = cfg
if fixed, err := fixPullRequestsConfig16961(bs, cfg); !fixed {
return false, err
}
case unit.TypeIssues:
cfg := &repo_model.IssuesConfig{}
repoUnit.Config = cfg
if fixed, err := fixIssuesConfig16961(bs, cfg); !fixed {
return false, err
}
default:
panic(fmt.Sprintf("unrecognized repo unit type: %v", repoUnit.Type))
}
return true, nil
}
func fixBrokenRepoUnits16961(ctx context.Context, logger log.Logger, autofix bool) error {
// RepoUnit describes all units of a repository
type RepoUnit struct {
ID int64
RepoID int64
Type unit.Type
Config []byte
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
}
count := 0
err := db.Iterate(
ctx,
builder.Gt{
"id": 0,
},
func(ctx context.Context, unit *RepoUnit) error {
bs := unit.Config
repoUnit := &repo_model.RepoUnit{
ID: unit.ID,
RepoID: unit.RepoID,
Type: unit.Type,
CreatedUnix: unit.CreatedUnix,
}
if fixed, err := fixBrokenRepoUnit16961(repoUnit, bs); !fixed {
return err
}
count++
if !autofix {
return nil
}
return repo_model.UpdateRepoUnitConfig(ctx, repoUnit)
},
)
if err != nil {
logger.Critical("Unable to iterate across repounits to fix the broken units: Error %v", err)
return err
}
if !autofix {
if count == 0 {
logger.Info("Found no broken repo_units")
} else {
logger.Warn("Found %d broken repo_units", count)
}
return nil
}
logger.Info("Fixed %d broken repo_units", count)
return nil
}
func init() {
Register(&Check{
Title: "Check for incorrectly dumped repo_units (See #16961)",
Name: "fix-broken-repo-units",
IsDefault: false,
Run: fixBrokenRepoUnits16961,
Priority: 7,
})
}

View File

@@ -1,265 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"github.com/stretchr/testify/assert"
)
func Test_fixUnitConfig_16961(t *testing.T) {
tests := []struct {
name string
bs string
wantFixed bool
wantErr bool
}{
{
name: "normal: {}",
bs: "{}",
wantFixed: false,
wantErr: false,
},
{
name: "broken but fixable: &{}",
bs: "&{}",
wantFixed: true,
wantErr: false,
},
{
name: "broken but unfixable: &{asdasd}",
bs: "&{asdasd}",
wantFixed: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotFixed, err := fixUnitConfig16961([]byte(tt.bs), &repo_model.UnitConfig{})
if (err != nil) != tt.wantErr {
t.Errorf("fixUnitConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotFixed != tt.wantFixed {
t.Errorf("fixUnitConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
}
})
}
}
func Test_fixExternalWikiConfig_16961(t *testing.T) {
tests := []struct {
name string
bs string
expected string
wantFixed bool
wantErr bool
}{
{
name: "normal: {\"ExternalWikiURL\":\"http://someurl\"}",
bs: "{\"ExternalWikiURL\":\"http://someurl\"}",
expected: "http://someurl",
wantFixed: false,
wantErr: false,
},
{
name: "broken: &{http://someurl}",
bs: "&{http://someurl}",
expected: "http://someurl",
wantFixed: true,
wantErr: false,
},
{
name: "broken but unfixable: http://someurl",
bs: "http://someurl",
wantFixed: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &repo_model.ExternalWikiConfig{}
gotFixed, err := fixExternalWikiConfig16961([]byte(tt.bs), cfg)
if (err != nil) != tt.wantErr {
t.Errorf("fixExternalWikiConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotFixed != tt.wantFixed {
t.Errorf("fixExternalWikiConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
}
if cfg.ExternalWikiURL != tt.expected {
t.Errorf("fixExternalWikiConfig_16961().ExternalWikiURL = %v, want %v", cfg.ExternalWikiURL, tt.expected)
}
})
}
}
func Test_fixExternalTrackerConfig_16961(t *testing.T) {
tests := []struct {
name string
bs string
expected repo_model.ExternalTrackerConfig
wantFixed bool
wantErr bool
}{
{
name: "normal",
bs: `{"ExternalTrackerURL":"a","ExternalTrackerFormat":"b","ExternalTrackerStyle":"c"}`,
expected: repo_model.ExternalTrackerConfig{
ExternalTrackerURL: "a",
ExternalTrackerFormat: "b",
ExternalTrackerStyle: "c",
},
wantFixed: false,
wantErr: false,
},
{
name: "broken",
bs: "&{a b c}",
expected: repo_model.ExternalTrackerConfig{
ExternalTrackerURL: "a",
ExternalTrackerFormat: "b",
ExternalTrackerStyle: "c",
},
wantFixed: true,
wantErr: false,
},
{
name: "broken - too many fields",
bs: "&{a b c d}",
wantFixed: false,
wantErr: true,
},
{
name: "broken - wrong format",
bs: "a b c d}",
wantFixed: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &repo_model.ExternalTrackerConfig{}
gotFixed, err := fixExternalTrackerConfig16961([]byte(tt.bs), cfg)
if (err != nil) != tt.wantErr {
t.Errorf("fixExternalTrackerConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotFixed != tt.wantFixed {
t.Errorf("fixExternalTrackerConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
}
if cfg.ExternalTrackerFormat != tt.expected.ExternalTrackerFormat {
t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerFormat = %v, want %v", tt.expected.ExternalTrackerFormat, cfg.ExternalTrackerFormat)
}
if cfg.ExternalTrackerStyle != tt.expected.ExternalTrackerStyle {
t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerStyle = %v, want %v", tt.expected.ExternalTrackerStyle, cfg.ExternalTrackerStyle)
}
if cfg.ExternalTrackerURL != tt.expected.ExternalTrackerURL {
t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerURL = %v, want %v", tt.expected.ExternalTrackerURL, cfg.ExternalTrackerURL)
}
})
}
}
func Test_fixPullRequestsConfig_16961(t *testing.T) {
tests := []struct {
name string
bs string
expected repo_model.PullRequestsConfig
wantFixed bool
wantErr bool
}{
{
name: "normal",
bs: `{"IgnoreWhitespaceConflicts":false,"AllowMerge":false,"AllowRebase":false,"AllowRebaseMerge":false,"AllowSquash":false,"AllowManualMerge":false,"AutodetectManualMerge":false,"DefaultDeleteBranchAfterMerge":false,"DefaultMergeStyle":""}`,
},
{
name: "broken - 1.14",
bs: `&{%!s(bool=false) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=false) %!s(bool=false)}`,
expected: repo_model.PullRequestsConfig{
IgnoreWhitespaceConflicts: false,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowManualMerge: false,
AutodetectManualMerge: false,
},
wantFixed: true,
},
{
name: "broken - 1.15",
bs: `&{%!s(bool=false) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=false) %!s(bool=false) %!s(bool=false) merge}`,
expected: repo_model.PullRequestsConfig{
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
DefaultMergeStyle: repo_model.MergeStyleMerge,
},
wantFixed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &repo_model.PullRequestsConfig{}
gotFixed, err := fixPullRequestsConfig16961([]byte(tt.bs), cfg)
if (err != nil) != tt.wantErr {
t.Errorf("fixPullRequestsConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotFixed != tt.wantFixed {
t.Errorf("fixPullRequestsConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
}
assert.Equal(t, &tt.expected, cfg)
})
}
}
func Test_fixIssuesConfig_16961(t *testing.T) {
tests := []struct {
name string
bs string
expected repo_model.IssuesConfig
wantFixed bool
wantErr bool
}{
{
name: "normal",
bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true}`,
expected: repo_model.IssuesConfig{
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
},
},
{
name: "broken",
bs: `&{%!s(bool=true) %!s(bool=true) %!s(bool=true)}`,
expected: repo_model.IssuesConfig{
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
},
wantFixed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &repo_model.IssuesConfig{}
gotFixed, err := fixIssuesConfig16961([]byte(tt.bs), cfg)
if (err != nil) != tt.wantErr {
t.Errorf("fixIssuesConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotFixed != tt.wantFixed {
t.Errorf("fixIssuesConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
}
assert.Equal(t, &tt.expected, cfg)
})
}
}

View File

@@ -1,115 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"xorm.io/builder"
)
func iteratePRs(ctx context.Context, repo *repo_model.Repository, each func(*repo_model.Repository, *issues_model.PullRequest) error) error {
return db.Iterate(
ctx,
builder.Eq{"base_repo_id": repo.ID},
func(ctx context.Context, bean *issues_model.PullRequest) error {
return each(repo, bean)
},
)
}
func checkPRMergeBase(ctx context.Context, logger log.Logger, autofix bool) error {
numRepos := 0
numPRs := 0
numPRsUpdated := 0
err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
numRepos++
return iteratePRs(ctx, repo, func(repo *repo_model.Repository, pr *issues_model.PullRequest) error {
numPRs++
pr.BaseRepo = repo
oldMergeBase := pr.MergeBase
if !pr.HasMerged {
var err error
pr.MergeBase, _, err = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, pr.GetGitHeadRefName()))
if err != nil {
var err2 error
pr.MergeBase, _, err2 = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix+pr.BaseBranch))
if err2 != nil {
logger.Warn("Unable to get merge base for PR ID %d, #%d onto %s in %s/%s. Error: %v & %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err, err2)
return nil
}
}
} else {
parentsString, _, err := gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID))
if err != nil {
logger.Warn("Unable to get parents for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
return nil
}
parents := strings.Split(strings.TrimSpace(parentsString), " ")
if len(parents) < 2 {
return nil
}
refs := append([]string{}, parents[1:]...)
refs = append(refs, pr.GetGitHeadRefName())
cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...)
pr.MergeBase, _, err = gitrepo.RunCmdString(ctx, repo, cmd)
if err != nil {
logger.Warn("Unable to get merge base for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
return nil
}
}
pr.MergeBase = strings.TrimSpace(pr.MergeBase)
if pr.MergeBase != oldMergeBase {
if autofix {
if err := pr.UpdateCols(ctx, "merge_base"); err != nil {
logger.Critical("Failed to update merge_base. ERROR: %v", err)
return fmt.Errorf("Failed to update merge_base. ERROR: %w", err)
}
} else {
logger.Info("#%d onto %s in %s/%s: MergeBase should be %s but is %s", pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, oldMergeBase, pr.MergeBase)
}
numPRsUpdated++
}
return nil
})
})
if autofix {
logger.Info("%d PR mergebases updated of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
} else {
if numPRsUpdated == 0 {
logger.Info("All %d PRs in %d repos have a correct mergebase", numPRs, numRepos)
} else if err == nil {
logger.Critical("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
return fmt.Errorf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
} else {
logger.Warn("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
}
}
return err
}
func init() {
Register(&Check{
Title: "Recalculate merge bases",
Name: "recalculate-merge-bases",
IsDefault: false,
Run: checkPRMergeBase,
Priority: 7,
})
}

View File

@@ -6,17 +6,13 @@ package doctor
import (
"context"
"fmt"
"os/exec"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
lru "github.com/hashicorp/golang-lru/v2"
@@ -34,16 +30,6 @@ func iterateRepositories(ctx context.Context, each func(*repo_model.Repository)
return err
}
func checkScriptType(ctx context.Context, logger log.Logger, autofix bool) error {
path, err := exec.LookPath(setting.ScriptType)
if err != nil {
logger.Critical("ScriptType \"%q\" is not on the current PATH. Error: %v", setting.ScriptType, err)
return fmt.Errorf("ScriptType \"%q\" is not on the current PATH. Error: %w", setting.ScriptType, err)
}
logger.Info("ScriptType %s is on the current PATH at %s", setting.ScriptType, path)
return nil
}
func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error {
if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
results, err := gitrepo.CheckDelegateHooks(ctx, repo)
@@ -82,42 +68,6 @@ func checkUserStarNum(ctx context.Context, logger log.Logger, autofix bool) erro
return nil
}
func checkEnablePushOptions(ctx context.Context, logger log.Logger, autofix bool) error {
numRepos := 0
numNeedUpdate := 0
if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
numRepos++
if autofix {
return gitrepo.GitConfigSet(ctx, repo, "receive.advertisePushOptions", "true")
}
value, err := gitrepo.GitConfigGet(ctx, repo, "receive.advertisePushOptions")
if err != nil {
return err
}
result, valid := git.ParseBool(strings.TrimSpace(value))
if !result || !valid {
numNeedUpdate++
logger.Info("%s: does not have receive.advertisePushOptions set correctly: %q", repo.FullName(), value)
}
return nil
}); err != nil {
logger.Critical("Unable to EnablePushOptions: %v", err)
return err
}
if autofix {
logger.Info("Enabled push options for %d repositories.", numRepos)
} else {
logger.Info("Checked %d repositories, %d need updates.", numRepos, numNeedUpdate)
}
return nil
}
func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) error {
numRepos := 0
numNeedUpdate := 0
@@ -244,13 +194,6 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro
}
func init() {
Register(&Check{
Title: "Check if SCRIPT_TYPE is available",
Name: "script-type",
IsDefault: false,
Run: checkScriptType,
Priority: 5,
})
Register(&Check{
Title: "Check if hook files are up-to-date and executable",
Name: "hooks",
@@ -265,13 +208,6 @@ func init() {
Run: checkUserStarNum,
Priority: 6,
})
Register(&Check{
Title: "Check that all git repositories have receive.advertisePushOptions set to true",
Name: "enable-push-options",
IsDefault: false,
Run: checkEnablePushOptions,
Priority: 7,
})
Register(&Check{
Title: "Check git-daemon-export-ok files",
Name: "check-git-daemon-export-ok",

View File

@@ -1,123 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"context"
"fmt"
"os"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
type configurationFile struct {
Name string
Path string
IsDirectory bool
Required bool
Writable bool
}
func checkConfigurationFile(logger log.Logger, autofix bool, fileOpts configurationFile) error {
logger.Info(`%-26s %q`, log.NewColoredValue(fileOpts.Name+":", log.Reset), fileOpts.Path)
fi, err := os.Stat(fileOpts.Path)
if err != nil {
if os.IsNotExist(err) && autofix && fileOpts.IsDirectory {
if err := os.MkdirAll(fileOpts.Path, 0o777); err != nil {
logger.Error(" Directory does not exist and could not be created. ERROR: %v", err)
return fmt.Errorf("Configuration directory: \"%q\" does not exist and could not be created. ERROR: %w", fileOpts.Path, err)
}
fi, err = os.Stat(fileOpts.Path)
}
}
if err != nil {
if fileOpts.Required {
logger.Error(" Is REQUIRED but is not accessible. ERROR: %v", err)
return fmt.Errorf("Configuration file \"%q\" is not accessible but is required. Error: %w", fileOpts.Path, err)
}
logger.Warn(" NOTICE: is not accessible (Error: %v)", err)
// this is a non-critical error
return nil
}
if fileOpts.IsDirectory && !fi.IsDir() {
logger.Error(" ERROR: not a directory")
return fmt.Errorf("Configuration directory \"%q\" is not a directory. Error: %w", fileOpts.Path, err)
} else if !fileOpts.IsDirectory && !fi.Mode().IsRegular() {
logger.Error(" ERROR: not a regular file")
return fmt.Errorf("Configuration file \"%q\" is not a regular file. Error: %w", fileOpts.Path, err)
} else if fileOpts.Writable {
if err := isWritableDir(fileOpts.Path); err != nil {
logger.Error(" ERROR: is required to be writable but is not writable: %v", err)
return fmt.Errorf("Configuration file \"%q\" is required to be writable but is not. Error: %w", fileOpts.Path, err)
}
}
return nil
}
func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix bool) error {
if fi, err := os.Stat(setting.CustomConf); err != nil || !fi.Mode().IsRegular() {
logger.Error("Failed to find configuration file at '%s'.", setting.CustomConf)
logger.Error("If you've never ran Gitea yet, this is normal and '%s' will be created for you on first run.", setting.CustomConf)
logger.Error("Otherwise check that you are running this command from the correct path and/or provide a `--config` parameter.")
logger.Critical("Cannot proceed without a configuration file")
return err
}
setting.MustInstalled()
configurationFiles := []configurationFile{
{"Configuration File Path", setting.CustomConf, false, true, false},
{"Repository Root Path", setting.RepoRootPath, true, true, true},
{"Data Root Path", setting.AppDataPath, true, true, true},
{"Custom File Root Path", setting.CustomPath, true, false, false},
{"Work directory", setting.AppWorkPath, true, true, false},
{"Log Root Path", setting.Log.RootPath, true, true, true},
}
if !setting.HasBuiltinBindata {
configurationFiles = append(configurationFiles, configurationFile{"Static File Root Path", setting.StaticRootPath, true, true, false})
}
numberOfErrors := 0
for _, configurationFile := range configurationFiles {
if err := checkConfigurationFile(logger, autofix, configurationFile); err != nil {
numberOfErrors++
}
}
if numberOfErrors > 0 {
logger.Critical("Please check your configuration files and try again.")
return fmt.Errorf("%d configuration files with errors", numberOfErrors)
}
return nil
}
func isWritableDir(path string) error {
// There's no platform-independent way of checking if a directory is writable
// https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable
tmpFile, err := os.CreateTemp(path, "doctors-order")
if err != nil {
return err
}
if err := os.Remove(tmpFile.Name()); err != nil {
log.Warn("can't remove temporary file: %q", tmpFile.Name())
}
_ = tmpFile.Close()
return nil
}
func init() {
Register(&Check{
Title: "Check paths and basic configuration",
Name: "paths",
IsDefault: true,
Run: checkConfigurationFiles,
AbortIfFailed: true,
SkipDatabaseInitialization: true,
Priority: 1,
})
}

View File

@@ -1,41 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"context"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
)
func checkUserType(ctx context.Context, logger log.Logger, autofix bool) error {
count, err := user_model.CountWrongUserType(ctx)
if err != nil {
logger.Critical("Error: %v whilst counting wrong user types")
return err
}
if count > 0 {
if autofix {
if count, err = user_model.FixWrongUserType(ctx); err != nil {
logger.Critical("Error: %v whilst fixing wrong user types")
return err
}
logger.Info("%d users with wrong type fixed", count)
} else {
logger.Warn("%d users with wrong type exist", count)
}
}
return nil
}
func init() {
Register(&Check{
Title: "Check if user with wrong type exist",
Name: "check-user-type",
IsDefault: true,
Run: checkUserType,
Priority: 3,
})
}