Skip to content

Commit b5856c4

Browse files
authored
Create doctor command to fix repo_units broken by dumps from 1.14.3-1.14.6 (#17136)
There was a serious issue with the `gitea dump` command in 1.14.3-1.14.6 which led to corruption of the `config` field of the `repo_unit` table. This PR adds a doctor command to attempt to fix the broken repo_units. Users affected by #16961 should run: ``` gitea doctor --fix --run fix-broken-repo-units ``` Fix #16961 Signed-off-by: Andrew Thornton <[email protected]>
1 parent 4e0cca3 commit b5856c4

File tree

4 files changed

+596
-1
lines changed

4 files changed

+596
-1
lines changed

models/helper.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func JSONUnmarshalHandleDoubleEncode(bs []byte, v interface{}) error {
5151
rs = append(rs, temp...)
5252
}
5353
if ok {
54-
if rs[0] == 0xff && rs[1] == 0xfe {
54+
if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
5555
rs = rs[2:]
5656
}
5757
err = json.Unmarshal(rs, v)

models/repo_unit.go

+6
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,9 @@ func getUnitsByRepoID(e db.Engine, repoID int64) (units []*RepoUnit, err error)
220220

221221
return units, nil
222222
}
223+
224+
// UpdateRepoUnit updates the provided repo unit
225+
func UpdateRepoUnit(unit *RepoUnit) error {
226+
_, err := db.GetEngine(db.DefaultContext).ID(unit.ID).Update(unit)
227+
return err
228+
}

modules/doctor/fix16961.go

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

0 commit comments

Comments
 (0)