Skip to content

Create doctor command to fix repo_units broken by dumps from 1.14.3-1.14.6 #17136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion models/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func JSONUnmarshalHandleDoubleEncode(bs []byte, v interface{}) error {
rs = append(rs, temp...)
}
if ok {
if rs[0] == 0xff && rs[1] == 0xfe {
if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
rs = rs[2:]
}
err = json.Unmarshal(rs, v)
Expand Down
6 changes: 6 additions & 0 deletions models/repo_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,9 @@ func getUnitsByRepoID(e db.Engine, repoID int64) (units []*RepoUnit, err error)

return units, nil
}

// UpdateRepoUnit updates the provided repo unit
func UpdateRepoUnit(unit *RepoUnit) error {
_, err := db.GetEngine(db.DefaultContext).ID(unit.ID).Update(unit)
return err
}
318 changes: 318 additions & 0 deletions modules/doctor/fix16961.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package doctor

import (
"bytes"
"fmt"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"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 *models.UnitConfig) (fixed bool, err error) {
err = models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return
}

// Handle #16961
if string(bs) != "&{}" && len(bs) != 0 {
return
}

return true, nil
}

func fixExternalUncycloConfig16961(bs []byte, cfg *models.ExternalUncycloConfig) (fixed bool, err error) {
err = models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return
}

if len(bs) < 3 {
return
}
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return
}
cfg.ExternalUncycloURL = string(bs[2 : len(bs)-1])
return true, nil
}

func fixExternalTrackerConfig16961(bs []byte, cfg *models.ExternalTrackerConfig) (fixed bool, err error) {
err = models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return
}
// Handle #16961
if len(bs) < 3 {
return
}

if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return
}

parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
if len(parts) != 3 {
return
}

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 *models.PullRequestsConfig) (fixed bool, err error) {
err = models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return
}

// Handle #16961
if len(bs) < 3 {
return
}

if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return
}

// 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
}

var parseErr error
cfg.IgnoreWhitespaceConflicts, parseErr = parseBool16961(parts[0])
if parseErr != nil {
return
}
cfg.AllowMerge, parseErr = parseBool16961(parts[1])
if parseErr != nil {
return
}
cfg.AllowRebase, parseErr = parseBool16961(parts[2])
if parseErr != nil {
return
}
cfg.AllowRebaseMerge, parseErr = parseBool16961(parts[3])
if parseErr != nil {
return
}
cfg.AllowSquash, parseErr = parseBool16961(parts[4])
if parseErr != nil {
return
}
cfg.AllowManualMerge, parseErr = parseBool16961(parts[5])
if parseErr != nil {
return
}
cfg.AutodetectManualMerge, parseErr = parseBool16961(parts[6])
if parseErr != nil {
return
}

// 1.14 unit
if len(parts) == 7 {
return true, nil
}

if len(parts) < 9 {
return
}

cfg.DefaultDeleteBranchAfterMerge, parseErr = parseBool16961(parts[7])
if parseErr != nil {
return
}

cfg.DefaultMergeStyle = models.MergeStyle(string(bytes.Join(parts[8:], []byte{' '})))
return true, nil
}

func fixIssuesConfig16961(bs []byte, cfg *models.IssuesConfig) (fixed bool, err error) {
err = models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
if err == nil {
return
}

// Handle #16961
if len(bs) < 3 {
return
}

if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
return
}

parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
if len(parts) != 3 {
return
}
var parseErr error
cfg.EnableTimetracker, parseErr = parseBool16961(parts[0])
if parseErr != nil {
return
}
cfg.AllowOnlyContributorsToTrackTime, parseErr = parseBool16961(parts[1])
if parseErr != nil {
return
}
cfg.EnableDependencies, parseErr = parseBool16961(parts[2])
if parseErr != nil {
return
}
return true, nil
}

func fixBrokenRepoUnit16961(repoUnit *models.RepoUnit, bs []byte) (fixed bool, err error) {
// Shortcut empty or null values
if len(bs) == 0 {
return false, nil
}

switch models.UnitType(repoUnit.Type) {
case models.UnitTypeCode, models.UnitTypeReleases, models.UnitTypeUncyclo, models.UnitTypeProjects:
cfg := &models.UnitConfig{}
repoUnit.Config = cfg
if fixed, err := fixUnitConfig16961(bs, cfg); !fixed {
return false, err
}
case models.UnitTypeExternalUncyclo:
cfg := &models.ExternalUncycloConfig{}
repoUnit.Config = cfg

if fixed, err := fixExternalUncycloConfig16961(bs, cfg); !fixed {
return false, err
}
case models.UnitTypeExternalTracker:
cfg := &models.ExternalTrackerConfig{}
repoUnit.Config = cfg
if fixed, err := fixExternalTrackerConfig16961(bs, cfg); !fixed {
return false, err
}
case models.UnitTypePullRequests:
cfg := &models.PullRequestsConfig{}
repoUnit.Config = cfg

if fixed, err := fixPullRequestsConfig16961(bs, cfg); !fixed {
return false, err
}
case models.UnitTypeIssues:
cfg := &models.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(logger log.Logger, autofix bool) error {
// RepoUnit describes all units of a repository
type RepoUnit struct {
ID int64
RepoID int64
Type models.UnitType
Config []byte
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
}

count := 0

err := db.Iterate(
db.DefaultContext,
new(RepoUnit),
builder.Gt{
"id": 0,
},
func(idx int, bean interface{}) error {
unit := bean.(*RepoUnit)

bs := unit.Config
repoUnit := &models.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 models.UpdateRepoUnit(repoUnit)
},
)

if err != nil {
logger.Critical("Unable to iterate acrosss repounits to fix the broken units: Error %v", err)
return err
}

if !autofix {
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,
})
}
Loading