code.gitea.io/gitea@v1.22.3/services/doctor/fix16961.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package doctor
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	repo_model "code.gitea.io/gitea/models/repo"
    14  	"code.gitea.io/gitea/models/unit"
    15  	"code.gitea.io/gitea/modules/json"
    16  	"code.gitea.io/gitea/modules/log"
    17  	"code.gitea.io/gitea/modules/timeutil"
    18  
    19  	"xorm.io/builder"
    20  )
    21  
    22  // #16831 revealed that the dump command that was broken in 1.14.3-1.14.6 and 1.15.0 (#15885).
    23  // This led to repo_unit and login_source cfg not being converted to JSON in the dump
    24  // Unfortunately although it was hoped that there were only a few users affected it
    25  // appears that many users are affected.
    26  
    27  // We therefore need to provide a doctor command to fix this repeated issue #16961
    28  
    29  func parseBool16961(bs []byte) (bool, error) {
    30  	if bytes.EqualFold(bs, []byte("%!s(bool=false)")) {
    31  		return false, nil
    32  	}
    33  
    34  	if bytes.EqualFold(bs, []byte("%!s(bool=true)")) {
    35  		return true, nil
    36  	}
    37  
    38  	return false, fmt.Errorf("unexpected bool format: %s", string(bs))
    39  }
    40  
    41  func fixUnitConfig16961(bs []byte, cfg *repo_model.UnitConfig) (fixed bool, err error) {
    42  	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
    43  	if err == nil {
    44  		return false, nil
    45  	}
    46  
    47  	// Handle #16961
    48  	if string(bs) != "&{}" && len(bs) != 0 {
    49  		return false, err
    50  	}
    51  
    52  	return true, nil
    53  }
    54  
    55  func fixExternalWikiConfig16961(bs []byte, cfg *repo_model.ExternalWikiConfig) (fixed bool, err error) {
    56  	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
    57  	if err == nil {
    58  		return false, nil
    59  	}
    60  
    61  	if len(bs) < 3 {
    62  		return false, err
    63  	}
    64  	if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
    65  		return false, err
    66  	}
    67  	cfg.ExternalWikiURL = string(bs[2 : len(bs)-1])
    68  	return true, nil
    69  }
    70  
    71  func fixExternalTrackerConfig16961(bs []byte, cfg *repo_model.ExternalTrackerConfig) (fixed bool, err error) {
    72  	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
    73  	if err == nil {
    74  		return false, nil
    75  	}
    76  	// Handle #16961
    77  	if len(bs) < 3 {
    78  		return false, err
    79  	}
    80  
    81  	if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
    82  		return false, err
    83  	}
    84  
    85  	parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
    86  	if len(parts) != 3 {
    87  		return false, err
    88  	}
    89  
    90  	cfg.ExternalTrackerURL = string(bytes.Join(parts[:len(parts)-2], []byte{' '}))
    91  	cfg.ExternalTrackerFormat = string(parts[len(parts)-2])
    92  	cfg.ExternalTrackerStyle = string(parts[len(parts)-1])
    93  	return true, nil
    94  }
    95  
    96  func fixPullRequestsConfig16961(bs []byte, cfg *repo_model.PullRequestsConfig) (fixed bool, err error) {
    97  	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
    98  	if err == nil {
    99  		return false, nil
   100  	}
   101  
   102  	// Handle #16961
   103  	if len(bs) < 3 {
   104  		return false, err
   105  	}
   106  
   107  	if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
   108  		return false, err
   109  	}
   110  
   111  	// PullRequestsConfig was the following in 1.14
   112  	// type PullRequestsConfig struct {
   113  	// 	IgnoreWhitespaceConflicts bool
   114  	// 	AllowMerge                bool
   115  	// 	AllowRebase               bool
   116  	// 	AllowRebaseMerge          bool
   117  	// 	AllowSquash               bool
   118  	// 	AllowManualMerge          bool
   119  	// 	AutodetectManualMerge     bool
   120  	// }
   121  	//
   122  	// 1.15 added in addition:
   123  	// DefaultDeleteBranchAfterMerge bool
   124  	// DefaultMergeStyle             MergeStyle
   125  	parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
   126  	if len(parts) < 7 {
   127  		return false, err
   128  	}
   129  
   130  	var parseErr error
   131  	cfg.IgnoreWhitespaceConflicts, parseErr = parseBool16961(parts[0])
   132  	if parseErr != nil {
   133  		return false, errors.Join(err, parseErr)
   134  	}
   135  	cfg.AllowMerge, parseErr = parseBool16961(parts[1])
   136  	if parseErr != nil {
   137  		return false, errors.Join(err, parseErr)
   138  	}
   139  	cfg.AllowRebase, parseErr = parseBool16961(parts[2])
   140  	if parseErr != nil {
   141  		return false, errors.Join(err, parseErr)
   142  	}
   143  	cfg.AllowRebaseMerge, parseErr = parseBool16961(parts[3])
   144  	if parseErr != nil {
   145  		return false, errors.Join(err, parseErr)
   146  	}
   147  	cfg.AllowSquash, parseErr = parseBool16961(parts[4])
   148  	if parseErr != nil {
   149  		return false, errors.Join(err, parseErr)
   150  	}
   151  	cfg.AllowManualMerge, parseErr = parseBool16961(parts[5])
   152  	if parseErr != nil {
   153  		return false, errors.Join(err, parseErr)
   154  	}
   155  	cfg.AutodetectManualMerge, parseErr = parseBool16961(parts[6])
   156  	if parseErr != nil {
   157  		return false, errors.Join(err, parseErr)
   158  	}
   159  
   160  	// 1.14 unit
   161  	if len(parts) == 7 {
   162  		return true, nil
   163  	}
   164  
   165  	if len(parts) < 9 {
   166  		return false, err
   167  	}
   168  
   169  	cfg.DefaultDeleteBranchAfterMerge, parseErr = parseBool16961(parts[7])
   170  	if parseErr != nil {
   171  		return false, errors.Join(err, parseErr)
   172  	}
   173  
   174  	cfg.DefaultMergeStyle = repo_model.MergeStyle(string(bytes.Join(parts[8:], []byte{' '})))
   175  	return true, nil
   176  }
   177  
   178  func fixIssuesConfig16961(bs []byte, cfg *repo_model.IssuesConfig) (fixed bool, err error) {
   179  	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
   180  	if err == nil {
   181  		return false, nil
   182  	}
   183  
   184  	// Handle #16961
   185  	if len(bs) < 3 {
   186  		return false, err
   187  	}
   188  
   189  	if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
   190  		return false, err
   191  	}
   192  
   193  	parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
   194  	if len(parts) != 3 {
   195  		return false, err
   196  	}
   197  	var parseErr error
   198  	cfg.EnableTimetracker, parseErr = parseBool16961(parts[0])
   199  	if parseErr != nil {
   200  		return false, errors.Join(err, parseErr)
   201  	}
   202  	cfg.AllowOnlyContributorsToTrackTime, parseErr = parseBool16961(parts[1])
   203  	if parseErr != nil {
   204  		return false, errors.Join(err, parseErr)
   205  	}
   206  	cfg.EnableDependencies, parseErr = parseBool16961(parts[2])
   207  	if parseErr != nil {
   208  		return false, errors.Join(err, parseErr)
   209  	}
   210  	return true, nil
   211  }
   212  
   213  func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed bool, err error) {
   214  	// Shortcut empty or null values
   215  	if len(bs) == 0 {
   216  		return false, nil
   217  	}
   218  
   219  	var cfg any
   220  	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
   221  	if err == nil {
   222  		return false, nil
   223  	}
   224  
   225  	switch repoUnit.Type {
   226  	case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
   227  		cfg := &repo_model.UnitConfig{}
   228  		repoUnit.Config = cfg
   229  		if fixed, err := fixUnitConfig16961(bs, cfg); !fixed {
   230  			return false, err
   231  		}
   232  	case unit.TypeExternalWiki:
   233  		cfg := &repo_model.ExternalWikiConfig{}
   234  		repoUnit.Config = cfg
   235  
   236  		if fixed, err := fixExternalWikiConfig16961(bs, cfg); !fixed {
   237  			return false, err
   238  		}
   239  	case unit.TypeExternalTracker:
   240  		cfg := &repo_model.ExternalTrackerConfig{}
   241  		repoUnit.Config = cfg
   242  		if fixed, err := fixExternalTrackerConfig16961(bs, cfg); !fixed {
   243  			return false, err
   244  		}
   245  	case unit.TypePullRequests:
   246  		cfg := &repo_model.PullRequestsConfig{}
   247  		repoUnit.Config = cfg
   248  
   249  		if fixed, err := fixPullRequestsConfig16961(bs, cfg); !fixed {
   250  			return false, err
   251  		}
   252  	case unit.TypeIssues:
   253  		cfg := &repo_model.IssuesConfig{}
   254  		repoUnit.Config = cfg
   255  		if fixed, err := fixIssuesConfig16961(bs, cfg); !fixed {
   256  			return false, err
   257  		}
   258  	default:
   259  		panic(fmt.Sprintf("unrecognized repo unit type: %v", repoUnit.Type))
   260  	}
   261  	return true, nil
   262  }
   263  
   264  func fixBrokenRepoUnits16961(ctx context.Context, logger log.Logger, autofix bool) error {
   265  	// RepoUnit describes all units of a repository
   266  	type RepoUnit struct {
   267  		ID          int64
   268  		RepoID      int64
   269  		Type        unit.Type
   270  		Config      []byte
   271  		CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
   272  	}
   273  
   274  	count := 0
   275  
   276  	err := db.Iterate(
   277  		ctx,
   278  		builder.Gt{
   279  			"id": 0,
   280  		},
   281  		func(ctx context.Context, unit *RepoUnit) error {
   282  			bs := unit.Config
   283  			repoUnit := &repo_model.RepoUnit{
   284  				ID:          unit.ID,
   285  				RepoID:      unit.RepoID,
   286  				Type:        unit.Type,
   287  				CreatedUnix: unit.CreatedUnix,
   288  			}
   289  
   290  			if fixed, err := fixBrokenRepoUnit16961(repoUnit, bs); !fixed {
   291  				return err
   292  			}
   293  
   294  			count++
   295  			if !autofix {
   296  				return nil
   297  			}
   298  
   299  			return repo_model.UpdateRepoUnit(ctx, repoUnit)
   300  		},
   301  	)
   302  	if err != nil {
   303  		logger.Critical("Unable to iterate across repounits to fix the broken units: Error %v", err)
   304  		return err
   305  	}
   306  
   307  	if !autofix {
   308  		if count == 0 {
   309  			logger.Info("Found no broken repo_units")
   310  		} else {
   311  			logger.Warn("Found %d broken repo_units", count)
   312  		}
   313  		return nil
   314  	}
   315  	logger.Info("Fixed %d broken repo_units", count)
   316  
   317  	return nil
   318  }
   319  
   320  func init() {
   321  	Register(&Check{
   322  		Title:     "Check for incorrectly dumped repo_units (See #16961)",
   323  		Name:      "fix-broken-repo-units",
   324  		IsDefault: false,
   325  		Run:       fixBrokenRepoUnits16961,
   326  		Priority:  7,
   327  	})
   328  }