JSON Schema validation for data used by Gitea during migrations Discussion at https://forum.forgefriends.org/t/common-json-schema-for-repository-information/563 Co-authored-by: Loïc Dachary <loic@dachary.org>tags/v1.18.0-dev
@@ -36,6 +36,8 @@ _testmain.go | |||
coverage.all | |||
cpu.out | |||
/modules/migration/bindata.go | |||
/modules/migration/bindata.go.hash | |||
/modules/options/bindata.go | |||
/modules/options/bindata.go.hash | |||
/modules/public/bindata.go |
@@ -43,6 +43,10 @@ var CmdRestoreRepository = cli.Command{ | |||
Usage: `Which items will be restored, one or more units should be separated as comma. | |||
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, | |||
}, | |||
cli.BoolFlag{ | |||
Name: "validation", | |||
Usage: "Sanity check the content of the files before trying to load them", | |||
}, | |||
}, | |||
} | |||
@@ -58,6 +62,7 @@ func runRestoreRepository(c *cli.Context) error { | |||
c.String("owner_name"), | |||
c.String("repo_name"), | |||
c.StringSlice("units"), | |||
c.Bool("validation"), | |||
) | |||
if statusCode == http.StatusOK { | |||
return nil |
@@ -97,6 +97,7 @@ require ( | |||
github.com/quasoft/websspi v1.0.0 | |||
github.com/rs/xid v1.3.0 // indirect | |||
github.com/russross/blackfriday/v2 v2.1.0 // indirect | |||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect | |||
github.com/sergi/go-diff v1.2.0 | |||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect | |||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 |
@@ -1039,6 +1039,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf | |||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | |||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= | |||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= | |||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= | |||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= | |||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= | |||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= | |||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= |
@@ -81,7 +81,7 @@ func TestDumpRestore(t *testing.T) { | |||
// | |||
newreponame := "restoredrepo" | |||
err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{"labels", "milestones", "issues", "comments"}) | |||
err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{"labels", "milestones", "issues", "comments"}, false) | |||
assert.NoError(t, err) | |||
newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame}).(*repo_model.Repository) |
@@ -0,0 +1,112 @@ | |||
// Copyright 2022 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 migration | |||
import ( | |||
"fmt" | |||
"os" | |||
"strings" | |||
"code.gitea.io/gitea/modules/json" | |||
"code.gitea.io/gitea/modules/log" | |||
"github.com/santhosh-tekuri/jsonschema/v5" | |||
"gopkg.in/yaml.v2" | |||
) | |||
// Load project data from file, with optional validation | |||
func Load(filename string, data interface{}, validation bool) error { | |||
isJSON := strings.HasSuffix(filename, ".json") | |||
bs, err := os.ReadFile(filename) | |||
if err != nil { | |||
return err | |||
} | |||
if validation { | |||
err := validate(bs, data, isJSON) | |||
if err != nil { | |||
return err | |||
} | |||
} | |||
return unmarshal(bs, data, isJSON) | |||
} | |||
func unmarshal(bs []byte, data interface{}, isJSON bool) error { | |||
if isJSON { | |||
return json.Unmarshal(bs, data) | |||
} | |||
return yaml.Unmarshal(bs, data) | |||
} | |||
func getSchema(filename string) (*jsonschema.Schema, error) { | |||
c := jsonschema.NewCompiler() | |||
c.LoadURL = openSchema | |||
return c.Compile(filename) | |||
} | |||
func validate(bs []byte, datatype interface{}, isJSON bool) error { | |||
var v interface{} | |||
err := unmarshal(bs, &v, isJSON) | |||
if err != nil { | |||
return err | |||
} | |||
if !isJSON { | |||
v, err = toStringKeys(v) | |||
if err != nil { | |||
return err | |||
} | |||
} | |||
var schemaFilename string | |||
switch datatype := datatype.(type) { | |||
case *[]*Issue: | |||
schemaFilename = "issue.json" | |||
case *[]*Milestone: | |||
schemaFilename = "milestone.json" | |||
default: | |||
return fmt.Errorf("file_format:validate: %T has not a validation implemented", datatype) | |||
} | |||
sch, err := getSchema(schemaFilename) | |||
if err != nil { | |||
return err | |||
} | |||
err = sch.Validate(v) | |||
if err != nil { | |||
log.Error("migration validation with %s failed for\n%s", schemaFilename, string(bs)) | |||
} | |||
return err | |||
} | |||
func toStringKeys(val interface{}) (interface{}, error) { | |||
var err error | |||
switch val := val.(type) { | |||
case map[interface{}]interface{}: | |||
m := make(map[string]interface{}) | |||
for k, v := range val { | |||
k, ok := k.(string) | |||
if !ok { | |||
return nil, fmt.Errorf("found non-string key %T %s", k, k) | |||
} | |||
m[k], err = toStringKeys(v) | |||
if err != nil { | |||
return nil, err | |||
} | |||
} | |||
return m, nil | |||
case []interface{}: | |||
l := make([]interface{}, len(val)) | |||
for i, v := range val { | |||
l[i], err = toStringKeys(v) | |||
if err != nil { | |||
return nil, err | |||
} | |||
} | |||
return l, nil | |||
default: | |||
return val, nil | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
// Copyright 2022 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 migration | |||
import ( | |||
"strings" | |||
"testing" | |||
"github.com/santhosh-tekuri/jsonschema/v5" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestMigrationJSON_IssueOK(t *testing.T) { | |||
issues := make([]*Issue, 0, 10) | |||
err := Load("file_format_testdata/issue_a.json", &issues, true) | |||
assert.NoError(t, err) | |||
err = Load("file_format_testdata/issue_a.yml", &issues, true) | |||
assert.NoError(t, err) | |||
} | |||
func TestMigrationJSON_IssueFail(t *testing.T) { | |||
issues := make([]*Issue, 0, 10) | |||
err := Load("file_format_testdata/issue_b.json", &issues, true) | |||
if _, ok := err.(*jsonschema.ValidationError); ok { | |||
errors := strings.Split(err.(*jsonschema.ValidationError).GoString(), "\n") | |||
assert.Contains(t, errors[1], "missing properties") | |||
assert.Contains(t, errors[1], "poster_id") | |||
} else { | |||
t.Fatalf("got: type %T with value %s, want: *jsonschema.ValidationError", err, err) | |||
} | |||
} | |||
func TestMigrationJSON_MilestoneOK(t *testing.T) { | |||
milestones := make([]*Milestone, 0, 10) | |||
err := Load("file_format_testdata/milestones.json", &milestones, true) | |||
assert.NoError(t, err) | |||
} |
@@ -0,0 +1,14 @@ | |||
[ | |||
{ | |||
"number": 1, | |||
"poster_id": 1, | |||
"poster_name": "name_a", | |||
"title": "title_a", | |||
"content": "content_a", | |||
"state": "closed", | |||
"is_locked": false, | |||
"created": "1985-04-12T23:20:50.52Z", | |||
"updated": "1986-04-12T23:20:50.52Z", | |||
"closed": "1987-04-12T23:20:50.52Z" | |||
} | |||
] |
@@ -0,0 +1,10 @@ | |||
- number: 1 | |||
poster_id: 1 | |||
poster_name: name_a | |||
title: title_a | |||
content: content_a | |||
state: closed | |||
is_locked: false | |||
created: 2021-05-27T15:24:13+02:00 | |||
updated: 2021-11-11T10:52:45+01:00 | |||
closed: 2021-11-11T10:52:45+01:00 |
@@ -0,0 +1,5 @@ | |||
[ | |||
{ | |||
"number": 1 | |||
} | |||
] |
@@ -0,0 +1,20 @@ | |||
[ | |||
{ | |||
"title": "title_a", | |||
"description": "description_a", | |||
"deadline": "1988-04-12T23:20:50.52Z", | |||
"created": "1985-04-12T23:20:50.52Z", | |||
"updated": "1986-04-12T23:20:50.52Z", | |||
"closed": "1987-04-12T23:20:50.52Z", | |||
"state": "closed" | |||
}, | |||
{ | |||
"title": "title_b", | |||
"description": "description_b", | |||
"deadline": "1998-04-12T23:20:50.52Z", | |||
"created": "1995-04-12T23:20:50.52Z", | |||
"updated": "1996-04-12T23:20:50.52Z", | |||
"closed": null, | |||
"state": "open" | |||
} | |||
] |
@@ -28,21 +28,21 @@ func (c BasicIssueContext) ForeignID() int64 { | |||
// Issue is a standard issue information | |||
type Issue struct { | |||
Number int64 | |||
PosterID int64 `yaml:"poster_id"` | |||
PosterName string `yaml:"poster_name"` | |||
PosterEmail string `yaml:"poster_email"` | |||
Title string | |||
Content string | |||
Ref string | |||
Milestone string | |||
State string // closed, open | |||
IsLocked bool `yaml:"is_locked"` | |||
Created time.Time | |||
Updated time.Time | |||
Closed *time.Time | |||
Labels []*Label | |||
Reactions []*Reaction | |||
Assignees []string | |||
Number int64 `json:"number"` | |||
PosterID int64 `yaml:"poster_id" json:"poster_id"` | |||
PosterName string `yaml:"poster_name" json:"poster_name"` | |||
PosterEmail string `yaml:"poster_email" json:"poster_email"` | |||
Title string `json:"title"` | |||
Content string `json:"content"` | |||
Ref string `json:"ref"` | |||
Milestone string `json:"milestone"` | |||
State string `json:"state"` // closed, open | |||
IsLocked bool `yaml:"is_locked" json:"is_locked"` | |||
Created time.Time `json:"created"` | |||
Updated time.Time `json:"updated"` | |||
Closed *time.Time `json:"closed"` | |||
Labels []*Label `json:"labels"` | |||
Reactions []*Reaction `json:"reactions"` | |||
Assignees []string `json:"assignees"` | |||
Context IssueContext `yaml:"-"` | |||
} |
@@ -7,7 +7,7 @@ package migration | |||
// Label defines a standard label information | |||
type Label struct { | |||
Name string | |||
Color string | |||
Description string | |||
Name string `json:"name"` | |||
Color string `json:"color"` | |||
Description string `json:"description"` | |||
} |
@@ -9,11 +9,11 @@ import "time" | |||
// Milestone defines a standard milestone | |||
type Milestone struct { | |||
Title string | |||
Description string | |||
Deadline *time.Time | |||
Created time.Time | |||
Updated *time.Time | |||
Closed *time.Time | |||
State string // open, closed | |||
Title string `json:"title"` | |||
Description string `json:"description"` | |||
Deadline *time.Time `json:"deadline"` | |||
Created time.Time `json:"created"` | |||
Updated *time.Time `json:"updated"` | |||
Closed *time.Time `json:"closed"` | |||
State string `json:"state"` // open, closed | |||
} |
@@ -6,7 +6,7 @@ package migration | |||
// Reaction represents a reaction to an issue/pr/comment. | |||
type Reaction struct { | |||
UserID int64 `yaml:"user_id"` | |||
UserName string `yaml:"user_name"` | |||
Content string | |||
UserID int64 `yaml:"user_id" json:"user_id"` | |||
UserName string `yaml:"user_name" json:"user_name"` | |||
Content string `json:"content"` | |||
} |
@@ -0,0 +1,114 @@ | |||
{ | |||
"title": "Issue", | |||
"description": "Issues associated to a repository within a forge (Gitea, GitLab, etc.).", | |||
"type": "array", | |||
"items": { | |||
"type": "object", | |||
"additionalProperties": false, | |||
"properties": { | |||
"number": { | |||
"description": "Unique identifier, relative to the repository.", | |||
"type": "number" | |||
}, | |||
"poster_id": { | |||
"description": "Unique identifier of the user who authored the issue.", | |||
"type": "number" | |||
}, | |||
"poster_name": { | |||
"description": "Name of the user who authored the issue.", | |||
"type": "string" | |||
}, | |||
"poster_email": { | |||
"description": "Email of the user who authored the issue.", | |||
"type": "string" | |||
}, | |||
"title": { | |||
"description": "Short description displayed as the title.", | |||
"type": "string" | |||
}, | |||
"content": { | |||
"description": "Long, multiline, description.", | |||
"type": "string" | |||
}, | |||
"ref": { | |||
"description": "Target branch in the repository.", | |||
"type": "string" | |||
}, | |||
"milestone": { | |||
"description": "Name of the milestone.", | |||
"type": "string" | |||
}, | |||
"state": { | |||
"description": "A 'closed' issue will not see any activity in the future, otherwise it is 'open'.", | |||
"enum": [ | |||
"closed", | |||
"open" | |||
] | |||
}, | |||
"is_locked": { | |||
"description": "A locked issue can only be modified by privileged users.", | |||
"type": "boolean" | |||
}, | |||
"created": { | |||
"description": "Creation time.", | |||
"type": "string", | |||
"format": "date-time" | |||
}, | |||
"updated": { | |||
"description": "Last update time.", | |||
"type": "string", | |||
"format": "date-time" | |||
}, | |||
"closed": { | |||
"description": "The last time 'state' changed to 'closed'.", | |||
"anyOf": [ | |||
{ | |||
"type": "string", | |||
"format": "date-time" | |||
}, | |||
{ | |||
"type": "null" | |||
} | |||
] | |||
}, | |||
"labels": { | |||
"description": "List of labels.", | |||
"type": "array", | |||
"items": { | |||
"$ref": "label.json" | |||
} | |||
}, | |||
"reactions": { | |||
"description": "List of reactions.", | |||
"type": "array", | |||
"items": { | |||
"$ref": "reaction.json" | |||
} | |||
}, | |||
"assignees": { | |||
"description": "List of assignees.", | |||
"type": "array", | |||
"items": { | |||
"description": "Name of a user assigned to the issue.", | |||
"type": "string" | |||
} | |||
} | |||
}, | |||
"required": [ | |||
"number", | |||
"poster_id", | |||
"poster_name", | |||
"title", | |||
"content", | |||
"state", | |||
"is_locked", | |||
"created", | |||
"updated" | |||
] | |||
}, | |||
"$schema": "http://json-schema.org/draft-04/schema#", | |||
"$id": "http://example.com/issue.json", | |||
"$$target": "issue.json" | |||
} |
@@ -0,0 +1,28 @@ | |||
{ | |||
"title": "Label", | |||
"description": "Label associated to an issue.", | |||
"type": "object", | |||
"additionalProperties": false, | |||
"properties": { | |||
"name": { | |||
"description": "Name of the label, unique within the repository.", | |||
"type": "string" | |||
}, | |||
"color": { | |||
"description": "Color code of the label.", | |||
"type": "string" | |||
}, | |||
"description": { | |||
"description": "Long, multiline, description.", | |||
"type": "string" | |||
} | |||
}, | |||
"required": [ | |||
"name" | |||
], | |||
"$schema": "http://json-schema.org/draft-04/schema#", | |||
"$id": "label.json", | |||
"$$target": "label.json" | |||
} |
@@ -0,0 +1,67 @@ | |||
{ | |||
"title": "Milestone", | |||
"description": "Milestone associated to a repository within a forge.", | |||
"type": "array", | |||
"items": { | |||
"type": "object", | |||
"additionalProperties": false, | |||
"properties": { | |||
"title": { | |||
"description": "Short description.", | |||
"type": "string" | |||
}, | |||
"description": { | |||
"description": "Long, multiline, description.", | |||
"type": "string" | |||
}, | |||
"deadline": { | |||
"description": "Deadline after which the milestone is overdue.", | |||
"type": "string", | |||
"format": "date-time" | |||
}, | |||
"created": { | |||
"description": "Creation time.", | |||
"type": "string", | |||
"format": "date-time" | |||
}, | |||
"updated": { | |||
"description": "Last update time.", | |||
"type": "string", | |||
"format": "date-time" | |||
}, | |||
"closed": { | |||
"description": "The last time 'state' changed to 'closed'.", | |||
"anyOf": [ | |||
{ | |||
"type": "string", | |||
"format": "date-time" | |||
}, | |||
{ | |||
"type": "null" | |||
} | |||
] | |||
}, | |||
"state": { | |||
"description": "A 'closed' issue will not see any activity in the future, otherwise it is 'open'.", | |||
"enum": [ | |||
"closed", | |||
"open" | |||
] | |||
} | |||
}, | |||
"required": [ | |||
"title", | |||
"description", | |||
"deadline", | |||
"created", | |||
"updated", | |||
"closed", | |||
"state" | |||
] | |||
}, | |||
"$schema": "http://json-schema.org/draft-04/schema#", | |||
"$id": "http://example.com/milestone.json", | |||
"$$target": "milestone.json" | |||
} |
@@ -0,0 +1,29 @@ | |||
{ | |||
"title": "Reaction", | |||
"description": "Reaction associated to an issue or a comment.", | |||
"type": "object", | |||
"additionalProperties": false, | |||
"properties": { | |||
"user_id": { | |||
"description": "Unique identifier of the user who authored the reaction.", | |||
"type": "number" | |||
}, | |||
"user_name": { | |||
"description": "Name of the user who authored the reaction.", | |||
"type": "string" | |||
}, | |||
"content": { | |||
"description": "Representation of the reaction", | |||
"type": "string" | |||
} | |||
}, | |||
"required": [ | |||
"user_id", | |||
"content" | |||
], | |||
"$schema": "http://json-schema.org/draft-04/schema#", | |||
"$id": "http://example.com/reaction.json", | |||
"$$target": "reaction.json" | |||
} |
@@ -0,0 +1,10 @@ | |||
// Copyright 2022 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. | |||
//go:build bindata | |||
// +build bindata | |||
package migration | |||
//go:generate go run ../../build/generate-bindata.go ../../modules/migration/schemas migration bindata.go |
@@ -0,0 +1,40 @@ | |||
// Copyright 2022 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. | |||
//go:build !bindata | |||
// +build !bindata | |||
package migration | |||
import ( | |||
"io" | |||
"net/url" | |||
"os" | |||
"path" | |||
"path/filepath" | |||
) | |||
func openSchema(s string) (io.ReadCloser, error) { | |||
u, err := url.Parse(s) | |||
if err != nil { | |||
return nil, err | |||
} | |||
basename := path.Base(u.Path) | |||
filename := basename | |||
// | |||
// Schema reference each other within the schemas directory but | |||
// the tests run in the parent directory. | |||
// | |||
if _, err := os.Stat(filename); os.IsNotExist(err) { | |||
filename = filepath.Join("schemas", basename) | |||
// | |||
// Integration tests run from the git root directory, not the | |||
// directory in which the test source is located. | |||
// | |||
if _, err := os.Stat(filename); os.IsNotExist(err) { | |||
filename = filepath.Join("modules/migration/schemas", basename) | |||
} | |||
} | |||
return os.Open(filename) | |||
} |
@@ -0,0 +1,17 @@ | |||
// Copyright 2022 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. | |||
//go:build bindata | |||
// +build bindata | |||
package migration | |||
import ( | |||
"io" | |||
"path" | |||
) | |||
func openSchema(filename string) (io.ReadCloser, error) { | |||
return Assets.Open(path.Base(filename)) | |||
} |
@@ -17,24 +17,26 @@ import ( | |||
// RestoreParams structure holds a data for restore repository | |||
type RestoreParams struct { | |||
RepoDir string | |||
OwnerName string | |||
RepoName string | |||
Units []string | |||
RepoDir string | |||
OwnerName string | |||
RepoName string | |||
Units []string | |||
Validation bool | |||
} | |||
// RestoreRepo calls the internal RestoreRepo function | |||
func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string) (int, string) { | |||
func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) (int, string) { | |||
reqURL := setting.LocalURL + "api/internal/restore_repo" | |||
req := newInternalRequest(ctx, reqURL, "POST") | |||
req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout | |||
req = req.Header("Content-Type", "application/json") | |||
jsonBytes, _ := json.Marshal(RestoreParams{ | |||
RepoDir: repoDir, | |||
OwnerName: ownerName, | |||
RepoName: repoName, | |||
Units: units, | |||
RepoDir: repoDir, | |||
OwnerName: ownerName, | |||
RepoName: repoName, | |||
Units: units, | |||
Validation: validation, | |||
}) | |||
req.Body(jsonBytes) | |||
resp, err := req.Response() |
@@ -24,10 +24,11 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { | |||
return | |||
} | |||
params := struct { | |||
RepoDir string | |||
OwnerName string | |||
RepoName string | |||
Units []string | |||
RepoDir string | |||
OwnerName string | |||
RepoName string | |||
Units []string | |||
Validation bool | |||
}{} | |||
if err = json.Unmarshal(bs, ¶ms); err != nil { | |||
ctx.JSON(http.StatusInternalServerError, private.Response{ | |||
@@ -42,6 +43,7 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { | |||
params.OwnerName, | |||
params.RepoName, | |||
params.Units, | |||
params.Validation, | |||
); err != nil { | |||
ctx.JSON(http.StatusInternalServerError, private.Response{ | |||
Err: err.Error(), |
@@ -604,13 +604,13 @@ func updateOptionsUnits(opts *base.MigrateOptions, units []string) { | |||
} | |||
// RestoreRepository restore a repository from the disk directory | |||
func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string) error { | |||
func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string, validation bool) error { | |||
doer, err := user_model.GetAdminUser() | |||
if err != nil { | |||
return err | |||
} | |||
uploader := NewGiteaLocalUploader(ctx, doer, ownerName, repoName) | |||
downloader, err := NewRepositoryRestorer(ctx, baseDir, ownerName, repoName) | |||
downloader, err := NewRepositoryRestorer(ctx, baseDir, ownerName, repoName, validation) | |||
if err != nil { | |||
return err | |||
} |
@@ -19,23 +19,25 @@ import ( | |||
// RepositoryRestorer implements an Downloader from the local directory | |||
type RepositoryRestorer struct { | |||
base.NullDownloader | |||
ctx context.Context | |||
baseDir string | |||
repoOwner string | |||
repoName string | |||
ctx context.Context | |||
baseDir string | |||
repoOwner string | |||
repoName string | |||
validation bool | |||
} | |||
// NewRepositoryRestorer creates a repository restorer which could restore repository from a dumped folder | |||
func NewRepositoryRestorer(ctx context.Context, baseDir, owner, repoName string) (*RepositoryRestorer, error) { | |||
func NewRepositoryRestorer(ctx context.Context, baseDir, owner, repoName string, validation bool) (*RepositoryRestorer, error) { | |||
baseDir, err := filepath.Abs(baseDir) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &RepositoryRestorer{ | |||
ctx: ctx, | |||
baseDir: baseDir, | |||
repoOwner: owner, | |||
repoName: repoName, | |||
ctx: ctx, | |||
baseDir: baseDir, | |||
repoOwner: owner, | |||
repoName: repoName, | |||
validation: validation, | |||
}, nil | |||
} | |||
@@ -114,7 +116,7 @@ func (r *RepositoryRestorer) GetTopics() ([]string, error) { | |||
func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) { | |||
milestones := make([]*base.Milestone, 0, 10) | |||
p := filepath.Join(r.baseDir, "milestone.yml") | |||
_, err := os.Stat(p) | |||
err := base.Load(p, &milestones, r.validation) | |||
if err != nil { | |||
if os.IsNotExist(err) { | |||
return nil, nil | |||
@@ -122,15 +124,6 @@ func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) { | |||
return nil, err | |||
} | |||
bs, err := os.ReadFile(p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
err = yaml.Unmarshal(bs, &milestones) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return milestones, nil | |||
} | |||
@@ -193,7 +186,7 @@ func (r *RepositoryRestorer) GetLabels() ([]*base.Label, error) { | |||
func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | |||
issues := make([]*base.Issue, 0, 10) | |||
p := filepath.Join(r.baseDir, "issue.yml") | |||
_, err := os.Stat(p) | |||
err := base.Load(p, &issues, r.validation) | |||
if err != nil { | |||
if os.IsNotExist(err) { | |||
return nil, true, nil | |||
@@ -201,15 +194,6 @@ func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, | |||
return nil, false, err | |||
} | |||
bs, err := os.ReadFile(p) | |||
if err != nil { | |||
return nil, false, err | |||
} | |||
err = yaml.Unmarshal(bs, &issues) | |||
if err != nil { | |||
return nil, false, err | |||
} | |||
for _, issue := range issues { | |||
issue.Context = base.BasicIssueContext(issue.Number) | |||
} |