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