diff options
Diffstat (limited to 'modules/migration')
31 files changed, 1471 insertions, 0 deletions
diff --git a/modules/migration/bindata.go b/modules/migration/bindata.go new file mode 100644 index 00000000..a2dca9ba --- /dev/null +++ b/modules/migration/bindata.go @@ -0,0 +1,220 @@ +// Code generated by vfsgen; DO NOT EDIT. + +// +build bindata + +package migration + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + pathpkg "path" + "time" +) + +// Assets statically implements the virtual filesystem provided to vfsgen. +var Assets = func() http.FileSystem { + fs := vfsgen۰FS{ + "/": &vfsgen۰DirInfo{ + name: "/", + + modTime: time.Date(2024, 9, 6, 12, 33, 46, 971993889, time.UTC), + + }, + "/issue.json": &vfsgen۰CompressedFileInfo{ + name: "issue.json", + + modTime: time.Date(2024, 9, 6, 12, 33, 46, 971993889, time.UTC), + + uncompressedSize: 2525, + + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xac\x56\xc1\x6e\xe4\x36\x0c\x3d\xdb\x5f\x41\xb8\x01\xa6\x05\x9c\x99\x1e\x7a\xca\xad\x28\x8a\x45\x81\xa0\x2d\xd0\xf6\xb4\x58\x2c\x38\x16\x3d\xe6\x56\x96\xbc\x12\x9d\xd4\x08\xf2\xef\x85\x24\xdb\x75\x3c\xe3\xdd\x49\x91\xa3\x49\x8a\xef\x89\x7a\x24\xfd\x94\x03\x00\x14\xc2\xa2\xa9\xb8\x83\xe2\x17\xef\x7b\x2a\xca\x64\x55\xe4\x2b\xc7\x9d\xb0\x35\xb3\xcf\x03\x7a\x6f\x2b\x46\x21\x05\x62\x01\xc1\x51\x67\x3d\x8b\x75\x03\x3c\xb2\x34\x6c\x00\xa1\xb6\xee\x44\xf0\xed\x3b\x16\xc2\x12\xde\xb1\xdc\xe3\xb1\x04\x92\x6a\xff\xdd\xbe\x28\xf3\x11\x74\xe8\x22\x26\x3a\x87\xc3\x84\xc9\x42\xad\x2f\xee\xe0\x29\xcf\xe6\x00\x7b\xfc\x44\x95\x14\x65\x9e\x15\xa8\x14\x07\x3e\xa8\x7f\x77\xb6\x23\x27\x4c\x21\xba\x46\xed\x29\xf8\xbb\xa5\xf5\x29\xcf\x62\x4e\xd3\xb7\x47\x72\xc9\x90\xad\x6f\xf5\x97\xe1\xcf\x3d\x01\x2b\x32\xc2\x35\x93\x2b\xc1\x91\x46\xe1\x07\x0a\xf7\x93\x86\x16\x37\x0c\xe4\xb3\xff\x88\x8d\x89\x13\xcc\x73\x39\xc2\x75\xd6\x0b\xb9\x8f\xac\xae\x45\x04\x5b\x47\x9c\xde\x93\x83\xc7\xc6\x02\xf6\xd2\x58\x17\x0a\xdc\x10\x70\x28\xfb\x6b\x80\x0d\xb6\xb4\x01\xfd\x2b\xb6\xf4\x7a\x34\x2f\x8e\xcd\x69\x03\x8d\x5a\x64\xbd\x01\xf7\x73\xf0\xbd\x15\xde\xa4\xd1\x4b\x40\x7f\x34\xd6\x09\x2c\x8c\xa0\xd8\x77\x1a\x07\x52\x80\x3e\x02\xc5\xe3\x57\x01\x55\xd6\x08\x19\xd9\x80\xba\xb7\xe6\x54\x42\xdb\x6b\x61\xcd\x86\xca\x25\xea\x55\xe9\x1d\xd5\x1b\xa9\xff\x44\x77\x22\x81\xa3\x43\x53\x35\xc0\xe6\xcb\xe2\xbb\x9c\xbd\x65\x4d\x5e\xac\xb9\x46\x01\x73\xec\x55\x99\xbd\xa0\x6c\x65\xfd\x11\x76\x95\xb6\x9e\xd4\x2e\x3d\x28\x3c\xb2\xd6\x60\xac\x80\x27\x02\x34\x03\x60\x25\xfc\xc0\x32\x4c\xd7\xaa\x7b\xe9\x1d\x95\x60\xa5\x21\xf7\xc8\x9e\x80\x05\xd8\xc3\xce\x76\x64\x76\x23\x21\x32\x7d\x5b\xdc\xc1\xfb\x3c\x1b\x5f\x26\x62\x44\x5f\xfc\x0e\xb1\x45\x9e\x65\x1f\x56\x54\xd9\x7f\xd4\xb6\xfa\x9b\xd4\x26\xdd\xe4\x1e\xc9\x56\x68\xc0\x1a\x3d\xc0\x91\xa0\xb5\x2a\xf4\xa4\x82\xe3\x00\x9d\xe3\x07\xd6\x74\x22\x15\xc5\xeb\x57\x65\x3a\x5a\xab\x09\xcd\x99\x7c\x1c\x85\xe1\xb8\x01\xfd\x53\xf0\x06\x81\x0a\xb7\x1b\x75\x8f\xb6\xda\xba\x16\x83\x06\x0b\x85\x42\xb7\x21\x7a\x0d\xd4\x77\xea\x0b\x40\xf7\xe8\x05\x52\xc8\x1b\x60\x8d\x95\xdf\xd0\x6d\x43\xa0\x03\x5c\x38\x09\xbb\x28\x94\x1d\x54\x0d\x9a\x53\x5a\x12\x93\x38\x46\x0e\x68\x86\xdf\xea\xc5\xbb\x86\x9c\x17\x99\x6d\x51\x9b\xb9\x5d\x38\x6e\x7a\xad\xe7\x90\x0b\xda\xd0\x78\x24\xed\xb7\x8a\xc6\x5e\x42\x77\xa4\xa0\x55\xc9\xa6\x3d\x95\x65\xcb\x2d\x95\xb2\xde\xa4\xae\x4e\xd9\xf7\x9f\xbc\x8d\xba\x7c\x3e\x6b\xfd\xd0\x06\xd6\x7c\x0d\x7e\x8e\xfb\x1f\x0c\xa6\xb3\x9b\x24\xd0\x7b\x3e\x19\xa2\xaf\x91\x98\xe3\x5e\x41\x62\x63\xd8\x60\x1a\xfe\x63\x46\x35\xed\xd5\xc5\xec\x7f\xf1\x43\x30\xcf\x9f\x99\x7b\x9e\x05\xfa\x85\xa3\xcf\x3d\xbb\xa8\xc3\xf7\x2f\x77\xfb\xf9\xee\xbd\xb4\x14\x5f\xae\x92\xf5\xbc\x7f\x39\xe8\xce\x67\xc9\xba\xc1\xd7\x7d\x98\x67\x1f\xf2\xb1\xd2\xe9\x5f\xe6\xc6\x57\x0d\xb5\x18\xae\xd4\x88\x74\x77\x87\x43\x78\x93\xdb\x64\xdd\x5b\x77\x3a\x28\x87\xb5\xdc\x7e\xff\xc3\x21\xd9\xbe\x99\xfe\x82\x6e\xe2\xcf\xc3\x74\x8a\xfe\xc1\xb6\xd3\xb4\xaf\x6c\x7b\x48\x35\x8b\x6f\x3b\xc5\xde\x48\xdc\x1b\xe1\xc0\xc2\x9b\x3f\xe7\xff\x06\x00\x00\xff\xff\x9d\xc3\xf0\xca\xdd\x09\x00\x00"), + }, + "/label.json": &vfsgen۰CompressedFileInfo{ + name: "label.json", + + modTime: time.Date(2024, 9, 6, 12, 33, 46, 971993889, time.UTC), + + uncompressedSize: 600, + + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x84\x91\xb1\x6e\xc2\x30\x10\x86\xe7\xf8\x29\x4e\x2e\x63\x20\x1d\x3a\x65\xed\x8a\xaa\xee\x55\x07\x13\x1f\xc9\x21\xc7\x67\xec\x8b\x2a\x84\x78\xf7\xca\x09\x41\xa4\x2a\xed\x66\xdd\x67\x7f\xf7\xfb\xee\xac\x00\x00\xb4\x90\x38\xd4\x35\xe8\xad\xd9\xa1\xd3\xe5\x54\xb5\x98\x9a\x48\x41\x88\xfd\x8d\x81\x49\x89\x1b\x32\x82\x16\x84\xc1\x78\xa0\x94\x06\xdc\xe8\x52\x5d\x55\xa7\x30\x9a\x78\x77\xc0\x46\x66\x95\xb1\x96\xb2\xc7\xb8\xf7\xc8\x01\xa3\x10\x26\x5d\xc3\xde\xb8\x84\xd7\x2b\xe1\x1e\x9c\x55\xa1\xbd\xe9\x71\x3a\xfe\x96\xe6\xcd\xf4\x08\xbc\x07\xe9\x10\x5c\x4e\x56\xc2\xe0\xe9\x38\x20\x7c\x91\x74\xe4\x47\x10\x31\x70\x22\xe1\x78\xca\x01\x8b\x45\xc0\x24\x91\x7c\xab\x55\x71\x29\x55\xa1\x1b\x76\x1c\x1f\x77\x7b\xcd\x18\x1a\xb6\xcb\x9e\xff\x59\x97\x96\x07\xee\x2d\xfb\xb6\x84\x7e\x70\x42\x8e\x3c\x96\x70\xc7\xff\x6a\x30\x8e\xed\x72\x9d\x5e\xc4\xe3\x40\x11\xad\xae\xe1\x63\x9e\xdd\x48\x3e\xe7\xc5\xac\x52\xd3\x61\x6f\xb2\xa3\x13\x09\x75\x55\x1d\x12\xfb\xf5\x54\xdd\x70\x6c\x2b\x1b\xcd\x5e\xd6\xcf\x2f\xd5\x54\x7b\x9a\x97\xb7\xa2\xac\xd5\xd3\x8f\xf3\xa3\x1b\x58\x89\x89\x2d\xca\x0f\xaa\x2e\xea\x3b\x00\x00\xff\xff\xec\x1f\xb4\x2f\x58\x02\x00\x00"), + }, + "/milestone.json": &vfsgen۰CompressedFileInfo{ + name: "milestone.json", + + modTime: time.Date(2024, 9, 6, 12, 33, 46, 971993889, time.UTC), + + uncompressedSize: 1394, + + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xac\x54\x41\x8f\xd3\x4c\x0c\x3d\x67\x7e\x85\x35\x5f\xa5\x5e\xd2\xf6\x3b\x70\xea\x0d\xc1\x71\x11\x48\x70\x5b\xed\xc1\x24\x4e\xe2\xd5\x64\x26\x78\x9c\x2d\x51\xb5\xff\x1d\x4d\xd2\x86\x36\xb4\x1c\x80\xab\xfd\xfc\xde\xb3\xc7\x9e\xa3\x01\x00\xb0\xca\xea\xc8\xee\xc1\x7e\x60\x47\x51\x83\x27\x9b\x4f\x99\x92\x62\x21\xdc\x29\x07\x7f\x95\x07\x8c\x31\x14\x8c\x4a\x25\x68\x00\x04\xa1\x2e\x44\xd6\x20\x03\x1c\x58\x1b\xf6\x80\x50\x05\xa9\x69\x6b\x73\x73\x52\x19\xba\x51\x04\x45\x70\x38\x0b\xb0\x52\x1b\xed\x1e\x8e\x26\x9b\x01\xe1\xeb\x33\x15\x6a\x73\x93\x59\x2c\x4b\x4e\xe2\xe8\x3e\x49\xe8\x48\x94\x29\xa1\x2b\x74\x91\x52\xbe\xbb\x8c\x1e\x4d\x76\xd5\xce\xd1\x64\xd9\xb2\x83\xcf\x4d\x10\x85\x8b\x60\xf2\x97\xfd\xd4\x8e\x2a\xec\x6b\x3b\x31\xbd\xe6\x27\xc6\x6b\x92\x5b\xbc\x0f\xc1\xd7\x39\xb4\xbd\x53\x76\xec\x29\xff\x03\x09\x2c\x53\xe5\x1d\xfe\xf7\xa7\x34\x60\xa5\x24\x70\x68\xb8\x68\x40\x1b\x82\x76\x7e\x12\x8e\x10\x5e\x48\xca\x9e\x6e\x0b\x8e\xb1\x2a\x48\x8b\x9a\xa2\x25\x2a\x6d\x94\x5b\x5a\x3a\x29\x84\xd2\xbb\xde\x31\xf2\x2e\x65\x39\x78\x48\xa5\x7f\x27\xd4\x77\xe5\x6f\x84\x1e\x30\x2a\x4c\x90\x7f\xa0\x55\xb8\x10\xef\x4a\x7d\x69\x08\x5c\x92\x4b\x95\xb0\x8e\x8a\x4a\x6b\x28\x1a\xf4\xf5\xb4\xdf\xeb\xa9\x7c\x7d\xf2\x80\x7e\xf8\x58\xd9\x3d\x3c\x9a\x6c\x64\x4f\x9c\x37\x9d\xdd\xb3\x36\x7b\xbb\x51\xee\x7b\xe7\x66\x88\xc9\xb2\xa7\x45\x27\xa3\xbb\x3b\x8d\xbc\x9d\x9d\x02\xc7\xd8\x13\x1c\xd8\x39\xf0\x41\x21\x12\x01\xfa\x01\xb0\x50\x7e\x61\x1d\x80\xfd\xb8\x3e\x55\xaf\xbd\x50\x0e\x41\x1b\x92\x03\x47\x02\xd6\xb4\x49\xeb\xd0\x91\x3f\xf7\x4b\xbe\x6f\x2f\xda\x3d\x0f\xf3\xec\xdf\x26\xac\xbd\xb0\x6a\xb2\xe4\xd6\x0a\x7d\xeb\x59\xc6\xa9\x3f\x5e\x9d\xe6\xad\xb3\xfa\xe5\x0e\x96\xeb\xb8\xdc\x9a\xc5\xcb\x5e\x8f\xc7\x64\x4f\xe6\x34\xb5\xe9\x9f\x59\xc5\xa2\xa1\x16\xd3\x90\x1a\xd5\x6e\xbf\xdb\x3d\xc7\xe0\x37\x53\x74\x1b\xa4\xde\x95\x82\x95\x6e\xfe\x7f\xb3\x9b\x62\xff\x9d\x7f\xa8\x15\x97\x17\x55\xf4\x1d\xdb\xce\xd1\xb6\x08\xed\x6e\x3e\xbd\x6d\xe2\x9a\xf1\x2b\x45\xa9\x69\x7c\xf3\x05\xc2\xbc\x9a\x1f\x01\x00\x00\xff\xff\xc6\xa6\xf2\x9e\x72\x05\x00\x00"), + }, + "/reaction.json": &vfsgen۰CompressedFileInfo{ + name: "reaction.json", + + modTime: time.Date(2024, 9, 6, 12, 33, 46, 971993889, time.UTC), + + uncompressedSize: 690, + + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x9c\x51\xcb\x8e\xe2\x30\x10\x3c\x93\xaf\x68\x79\x39\x02\xd9\xc3\x9e\xf8\x88\xd5\x0a\x69\x4f\xab\xd5\xa8\x71\x2a\xc4\x28\xb6\x43\xbb\xa3\x99\x11\xe2\xdf\x47\xce\x83\x09\xd2\x70\x99\xa3\xbb\xab\xab\xca\x55\xd7\x82\x88\xc8\xa8\xd3\x16\x66\x4f\xe6\x00\xb6\xea\x62\x30\x9b\x71\x51\x21\x59\x71\xdd\x30\x5a\xac\x89\x53\x8a\xd6\xb1\xa2\x22\x8d\xc4\x81\x5c\x4a\x3d\x28\x0a\x31\xd9\xe8\x3d\x82\xee\xcc\xa6\x98\xd8\xdf\xbb\x81\x3c\x1e\xcf\xb0\x3a\x53\x73\x55\xb9\xcc\xc5\xed\x1f\x89\x1d\x44\x1d\x92\xd9\x53\xcd\x6d\xc2\x04\xe9\x96\x8b\x6b\xb1\x32\x7d\x82\xbc\xb8\x6a\x7c\x7d\x65\xf0\x6f\x70\x97\x1e\xe4\x2a\x04\x75\xb5\x83\x50\xac\x49\x1b\x50\xbe\xa4\xd7\x26\x12\xf7\xda\x44\xc9\xbe\x1b\x90\x4c\xff\xc9\x5e\x57\x0f\x5e\x43\xef\x8f\x10\x53\xac\x6e\x9b\x59\x37\xb0\xc7\x73\xe5\xdf\xec\xf1\x5d\xb1\xa4\xe2\xc2\x69\x16\xb3\x31\x28\x82\x3e\x97\x3a\xa0\x13\x24\x04\xe5\xa1\x8b\x49\x54\x3e\xab\x7b\xc6\x3e\xa4\x7a\x9b\xc2\x15\x5c\x7a\x27\xc8\x61\xfe\x5b\x44\xbb\x74\x30\x00\xff\xcf\x35\xae\x93\x6d\xe0\x39\x53\x36\xaa\xdd\xbe\x2c\xcf\x29\x86\xed\x38\xdd\x45\x39\x95\x95\x70\xad\xdb\x9f\xbf\xca\x71\xf6\x63\xae\x7a\x3d\x54\x36\x5f\xe1\x8d\x7d\xd7\x62\x67\xa3\x2f\xef\x99\x64\xaa\x3b\x7c\xad\x2c\x27\xe4\x04\xcc\x23\xa0\xb8\x15\x1f\x01\x00\x00\xff\xff\x3d\x35\x68\x23\xb2\x02\x00\x00"), + }, + } + fs["/"].(*vfsgen۰DirInfo).entries = []os.FileInfo{ + fs["/issue.json"].(os.FileInfo), + fs["/label.json"].(os.FileInfo), + fs["/milestone.json"].(os.FileInfo), + fs["/reaction.json"].(os.FileInfo), + } + + return fs +}() + +type vfsgen۰FS map[string]interface{} + +func (fs vfsgen۰FS) Open(path string) (http.File, error) { + path = pathpkg.Clean("/" + path) + f, ok := fs[path] + if !ok { + return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist} + } + + switch f := f.(type) { + case *vfsgen۰CompressedFileInfo: + gr, err := gzip.NewReader(bytes.NewReader(f.compressedContent)) + if err != nil { + // This should never happen because we generate the gzip bytes such that they are always valid. + panic("unexpected error reading own gzip compressed bytes: " + err.Error()) + } + return &vfsgen۰CompressedFile{ + vfsgen۰CompressedFileInfo: f, + gr: gr, + }, nil + case *vfsgen۰DirInfo: + return &vfsgen۰Dir{ + vfsgen۰DirInfo: f, + }, nil + default: + // This should never happen because we generate only the above types. + panic(fmt.Sprintf("unexpected type %T", f)) + } +} + +// vfsgen۰CompressedFileInfo is a static definition of a gzip compressed file. +type vfsgen۰CompressedFileInfo struct { + name string + modTime time.Time + compressedContent []byte + uncompressedSize int64 +} + +func (f *vfsgen۰CompressedFileInfo) Readdir(count int) ([]os.FileInfo, error) { + return nil, fmt.Errorf("cannot Readdir from file %s", f.name) +} +func (f *vfsgen۰CompressedFileInfo) Stat() (os.FileInfo, error) { return f, nil } + +func (f *vfsgen۰CompressedFileInfo) GzipBytes() []byte { + return f.compressedContent +} + +func (f *vfsgen۰CompressedFileInfo) Name() string { return f.name } +func (f *vfsgen۰CompressedFileInfo) Size() int64 { return f.uncompressedSize } +func (f *vfsgen۰CompressedFileInfo) Mode() os.FileMode { return 0444 } +func (f *vfsgen۰CompressedFileInfo) ModTime() time.Time { return f.modTime } +func (f *vfsgen۰CompressedFileInfo) IsDir() bool { return false } +func (f *vfsgen۰CompressedFileInfo) Sys() interface{} { return nil } + +// vfsgen۰CompressedFile is an opened compressedFile instance. +type vfsgen۰CompressedFile struct { + *vfsgen۰CompressedFileInfo + gr *gzip.Reader + grPos int64 // Actual gr uncompressed position. + seekPos int64 // Seek uncompressed position. +} + +func (f *vfsgen۰CompressedFile) Read(p []byte) (n int, err error) { + if f.grPos > f.seekPos { + // Rewind to beginning. + err = f.gr.Reset(bytes.NewReader(f.compressedContent)) + if err != nil { + return 0, err + } + f.grPos = 0 + } + if f.grPos < f.seekPos { + // Fast-forward. + _, err = io.CopyN(ioutil.Discard, f.gr, f.seekPos-f.grPos) + if err != nil { + return 0, err + } + f.grPos = f.seekPos + } + n, err = f.gr.Read(p) + f.grPos += int64(n) + f.seekPos = f.grPos + return n, err +} +func (f *vfsgen۰CompressedFile) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + f.seekPos = 0 + offset + case io.SeekCurrent: + f.seekPos += offset + case io.SeekEnd: + f.seekPos = f.uncompressedSize + offset + default: + panic(fmt.Errorf("invalid whence value: %v", whence)) + } + return f.seekPos, nil +} +func (f *vfsgen۰CompressedFile) Close() error { + return f.gr.Close() +} + +// vfsgen۰DirInfo is a static definition of a directory. +type vfsgen۰DirInfo struct { + name string + modTime time.Time + entries []os.FileInfo +} + +func (d *vfsgen۰DirInfo) Read([]byte) (int, error) { + return 0, fmt.Errorf("cannot Read from directory %s", d.name) +} +func (d *vfsgen۰DirInfo) Close() error { return nil } +func (d *vfsgen۰DirInfo) Stat() (os.FileInfo, error) { return d, nil } + +func (d *vfsgen۰DirInfo) Name() string { return d.name } +func (d *vfsgen۰DirInfo) Size() int64 { return 0 } +func (d *vfsgen۰DirInfo) Mode() os.FileMode { return 0755 | os.ModeDir } +func (d *vfsgen۰DirInfo) ModTime() time.Time { return d.modTime } +func (d *vfsgen۰DirInfo) IsDir() bool { return true } +func (d *vfsgen۰DirInfo) Sys() interface{} { return nil } + +// vfsgen۰Dir is an opened dir instance. +type vfsgen۰Dir struct { + *vfsgen۰DirInfo + pos int // Position within entries for Seek and Readdir. +} + +func (d *vfsgen۰Dir) Seek(offset int64, whence int) (int64, error) { + if offset == 0 && whence == io.SeekStart { + d.pos = 0 + return 0, nil + } + return 0, fmt.Errorf("unsupported Seek in directory %s", d.name) +} + +func (d *vfsgen۰Dir) Readdir(count int) ([]os.FileInfo, error) { + if d.pos >= len(d.entries) && count > 0 { + return nil, io.EOF + } + if count <= 0 || count > len(d.entries)-d.pos { + count = len(d.entries) - d.pos + } + e := d.entries[d.pos : d.pos+count] + d.pos += count + return e, nil +} diff --git a/modules/migration/bindata.go.hash b/modules/migration/bindata.go.hash new file mode 100644 index 00000000..c1166d35 --- /dev/null +++ b/modules/migration/bindata.go.hash @@ -0,0 +1,2 @@ +ut<J' +d
\ No newline at end of file diff --git a/modules/migration/comment.go b/modules/migration/comment.go new file mode 100644 index 00000000..e0417584 --- /dev/null +++ b/modules/migration/comment.go @@ -0,0 +1,34 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import "time" + +// Commentable can be commented upon +type Commentable interface { + Reviewable + GetContext() DownloaderContext +} + +// Comment is a standard comment information +type Comment struct { + IssueIndex int64 `yaml:"issue_index"` + Index int64 + CommentType string `yaml:"comment_type"` // see `commentStrings` in models/issues/comment.go + PosterID int64 `yaml:"poster_id"` + PosterName string `yaml:"poster_name"` + PosterEmail string `yaml:"poster_email"` + Created time.Time + Updated time.Time + Content string + Reactions []*Reaction + Meta map[string]any `yaml:"meta,omitempty"` // see models/issues/comment.go for fields in Comment struct +} + +// GetExternalName ExternalUserMigrated interface +func (c *Comment) GetExternalName() string { return c.PosterName } + +// ExternalID ExternalUserMigrated interface +func (c *Comment) GetExternalID() int64 { return c.PosterID } diff --git a/modules/migration/downloader.go b/modules/migration/downloader.go new file mode 100644 index 00000000..08dbbc29 --- /dev/null +++ b/modules/migration/downloader.go @@ -0,0 +1,37 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import ( + "context" + + "code.gitea.io/gitea/modules/structs" +) + +// Downloader downloads the site repo information +type Downloader interface { + SetContext(context.Context) + GetRepoInfo() (*Repository, error) + GetTopics() ([]string, error) + GetMilestones() ([]*Milestone, error) + GetReleases() ([]*Release, error) + GetLabels() ([]*Label, error) + GetIssues(page, perPage int) ([]*Issue, bool, error) + GetComments(commentable Commentable) ([]*Comment, bool, error) + GetAllComments(page, perPage int) ([]*Comment, bool, error) + SupportGetRepoComments() bool + GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) + GetReviews(reviewable Reviewable) ([]*Review, error) + FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) +} + +// DownloaderFactory defines an interface to match a downloader implementation and create a downloader +type DownloaderFactory interface { + New(ctx context.Context, opts MigrateOptions) (Downloader, error) + GitServiceType() structs.GitServiceType +} + +// DownloaderContext has opaque information only relevant to a given downloader +type DownloaderContext any diff --git a/modules/migration/error.go b/modules/migration/error.go new file mode 100644 index 00000000..64cda9d0 --- /dev/null +++ b/modules/migration/error.go @@ -0,0 +1,25 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import "fmt" + +// ErrNotSupported represents status if a downloader do not supported something. +type ErrNotSupported struct { + Entity string +} + +// IsErrNotSupported checks if an error is an ErrNotSupported +func IsErrNotSupported(err error) bool { + _, ok := err.(ErrNotSupported) + return ok +} + +// Error return error message +func (err ErrNotSupported) Error() string { + if len(err.Entity) != 0 { + return fmt.Sprintf("'%s' not supported", err.Entity) + } + return "not supported" +} diff --git a/modules/migration/file_format.go b/modules/migration/file_format.go new file mode 100644 index 00000000..e8b6891c --- /dev/null +++ b/modules/migration/file_format.go @@ -0,0 +1,110 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import ( + "fmt" + "os" + "strings" + "time" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + + "github.com/santhosh-tekuri/jsonschema/v5" + "gopkg.in/yaml.v3" +) + +// Load project data from file, with optional validation +func Load(filename string, data any, 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 any, 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 any, isJSON bool) error { + var v any + 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:\n%#v", schemaFilename, err) + } + return err +} + +func toStringKeys(val any) (any, error) { + var err error + switch val := val.(type) { + case map[string]any: + m := make(map[string]any) + for k, v := range val { + m[k], err = toStringKeys(v) + if err != nil { + return nil, err + } + } + return m, nil + case []any: + l := make([]any, len(val)) + for i, v := range val { + l[i], err = toStringKeys(v) + if err != nil { + return nil, err + } + } + return l, nil + case time.Time: + return val.Format(time.RFC3339), nil + default: + return val, nil + } +} diff --git a/modules/migration/file_format_test.go b/modules/migration/file_format_test.go new file mode 100644 index 00000000..9638d824 --- /dev/null +++ b/modules/migration/file_format_test.go @@ -0,0 +1,39 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import ( + "strings" + "testing" + + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMigrationJSON_IssueOK(t *testing.T) { + issues := make([]*Issue, 0, 10) + err := Load("file_format_testdata/issue_a.json", &issues, true) + require.NoError(t, err) + err = Load("file_format_testdata/issue_a.yml", &issues, true) + require.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) + require.NoError(t, err) +} diff --git a/modules/migration/file_format_testdata/issue_a.json b/modules/migration/file_format_testdata/issue_a.json new file mode 100644 index 00000000..33d7759f --- /dev/null +++ b/modules/migration/file_format_testdata/issue_a.json @@ -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" + } +] diff --git a/modules/migration/file_format_testdata/issue_a.yml b/modules/migration/file_format_testdata/issue_a.yml new file mode 100644 index 00000000..d03bfb31 --- /dev/null +++ b/modules/migration/file_format_testdata/issue_a.yml @@ -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 diff --git a/modules/migration/file_format_testdata/issue_b.json b/modules/migration/file_format_testdata/issue_b.json new file mode 100644 index 00000000..2a824d42 --- /dev/null +++ b/modules/migration/file_format_testdata/issue_b.json @@ -0,0 +1,5 @@ +[ + { + "number": 1 + } +] diff --git a/modules/migration/file_format_testdata/milestones.json b/modules/migration/file_format_testdata/milestones.json new file mode 100644 index 00000000..8fb770d8 --- /dev/null +++ b/modules/migration/file_format_testdata/milestones.json @@ -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" + } +] diff --git a/modules/migration/issue.go b/modules/migration/issue.go new file mode 100644 index 00000000..3d1d1b4e --- /dev/null +++ b/modules/migration/issue.go @@ -0,0 +1,48 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import "time" + +// Issue is a standard issue information +type Issue struct { + 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"` + ForeignIndex int64 `json:"foreign_id"` + Context DownloaderContext `yaml:"-"` +} + +// GetExternalName ExternalUserMigrated interface +func (issue *Issue) GetExternalName() string { return issue.PosterName } + +// GetExternalID ExternalUserMigrated interface +func (issue *Issue) GetExternalID() int64 { return issue.PosterID } + +func (issue *Issue) GetLocalIndex() int64 { return issue.Number } + +func (issue *Issue) GetForeignIndex() int64 { + // see the comment of Reviewable.GetForeignIndex + // if there is no ForeignIndex, then use LocalIndex + if issue.ForeignIndex == 0 { + return issue.Number + } + return issue.ForeignIndex +} + +func (issue *Issue) GetContext() DownloaderContext { return issue.Context } diff --git a/modules/migration/label.go b/modules/migration/label.go new file mode 100644 index 00000000..4927be3c --- /dev/null +++ b/modules/migration/label.go @@ -0,0 +1,13 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +// Label defines a standard label information +type Label struct { + Name string `json:"name"` + Color string `json:"color"` + Description string `json:"description"` + Exclusive bool `json:"exclusive"` +} diff --git a/modules/migration/messenger.go b/modules/migration/messenger.go new file mode 100644 index 00000000..6f9cad3f --- /dev/null +++ b/modules/migration/messenger.go @@ -0,0 +1,10 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +// Messenger is a formatting function similar to i18n.TrString +type Messenger func(key string, args ...any) + +// NilMessenger represents an empty formatting function +func NilMessenger(string, ...any) {} diff --git a/modules/migration/milestone.go b/modules/migration/milestone.go new file mode 100644 index 00000000..34355b8f --- /dev/null +++ b/modules/migration/milestone.go @@ -0,0 +1,18 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import "time" + +// Milestone defines a standard milestone +type Milestone struct { + 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 +} diff --git a/modules/migration/null_downloader.go b/modules/migration/null_downloader.go new file mode 100644 index 00000000..e5b69331 --- /dev/null +++ b/modules/migration/null_downloader.go @@ -0,0 +1,88 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import ( + "context" + "net/url" +) + +// NullDownloader implements a blank downloader +type NullDownloader struct{} + +var _ Downloader = &NullDownloader{} + +// SetContext set context +func (n NullDownloader) SetContext(_ context.Context) {} + +// GetRepoInfo returns a repository information +func (n NullDownloader) GetRepoInfo() (*Repository, error) { + return nil, ErrNotSupported{Entity: "RepoInfo"} +} + +// GetTopics return repository topics +func (n NullDownloader) GetTopics() ([]string, error) { + return nil, ErrNotSupported{Entity: "Topics"} +} + +// GetMilestones returns milestones +func (n NullDownloader) GetMilestones() ([]*Milestone, error) { + return nil, ErrNotSupported{Entity: "Milestones"} +} + +// GetReleases returns releases +func (n NullDownloader) GetReleases() ([]*Release, error) { + return nil, ErrNotSupported{Entity: "Releases"} +} + +// GetLabels returns labels +func (n NullDownloader) GetLabels() ([]*Label, error) { + return nil, ErrNotSupported{Entity: "Labels"} +} + +// GetIssues returns issues according start and limit +func (n NullDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { + return nil, false, ErrNotSupported{Entity: "Issues"} +} + +// GetComments returns comments of an issue or PR +func (n NullDownloader) GetComments(commentable Commentable) ([]*Comment, bool, error) { + return nil, false, ErrNotSupported{Entity: "Comments"} +} + +// GetAllComments returns paginated comments +func (n NullDownloader) GetAllComments(page, perPage int) ([]*Comment, bool, error) { + return nil, false, ErrNotSupported{Entity: "AllComments"} +} + +// GetPullRequests returns pull requests according page and perPage +func (n NullDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) { + return nil, false, ErrNotSupported{Entity: "PullRequests"} +} + +// GetReviews returns pull requests review +func (n NullDownloader) GetReviews(reviewable Reviewable) ([]*Review, error) { + return nil, ErrNotSupported{Entity: "Reviews"} +} + +// FormatCloneURL add authentication into remote URLs +func (n NullDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) { + if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 { + u, err := url.Parse(remoteAddr) + if err != nil { + return "", err + } + u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword) + if len(opts.AuthToken) > 0 { + u.User = url.UserPassword("oauth2", opts.AuthToken) + } + return u.String(), nil + } + return remoteAddr, nil +} + +// SupportGetRepoComments return true if it supports get repo comments +func (n NullDownloader) SupportGetRepoComments() bool { + return false +} diff --git a/modules/migration/options.go b/modules/migration/options.go new file mode 100644 index 00000000..234e72c2 --- /dev/null +++ b/modules/migration/options.go @@ -0,0 +1,41 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import "code.gitea.io/gitea/modules/structs" + +// MigrateOptions defines the way a repository gets migrated +// this is for internal usage by migrations module and func who interact with it +type MigrateOptions struct { + // required: true + CloneAddr string `json:"clone_addr" binding:"Required"` + CloneAddrEncrypted string `json:"clone_addr_encrypted,omitempty"` + AuthUsername string `json:"auth_username"` + AuthPassword string `json:"-"` + AuthPasswordEncrypted string `json:"auth_password_encrypted,omitempty"` + AuthToken string `json:"-"` + AuthTokenEncrypted string `json:"auth_token_encrypted,omitempty"` + // required: true + UID int `json:"uid" binding:"Required"` + // required: true + RepoName string `json:"repo_name" binding:"Required"` + Mirror bool `json:"mirror"` + LFS bool `json:"lfs"` + LFSEndpoint string `json:"lfs_endpoint"` + Private bool `json:"private"` + Description string `json:"description"` + OriginalURL string + GitServiceType structs.GitServiceType + Wiki bool + Issues bool + Milestones bool + Labels bool + Releases bool + Comments bool + PullRequests bool + ReleaseAssets bool + MigrateToRepoID int64 + MirrorInterval string `json:"mirror_interval"` +} diff --git a/modules/migration/pullrequest.go b/modules/migration/pullrequest.go new file mode 100644 index 00000000..1435991b --- /dev/null +++ b/modules/migration/pullrequest.go @@ -0,0 +1,74 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import ( + "fmt" + "time" + + "code.gitea.io/gitea/modules/git" +) + +// PullRequest defines a standard pull request information +type PullRequest struct { + Number int64 + Title string + PosterName string `yaml:"poster_name"` + PosterID int64 `yaml:"poster_id"` + PosterEmail string `yaml:"poster_email"` + Content string + Milestone string + State string + Created time.Time + Updated time.Time + Closed *time.Time + Labels []*Label + PatchURL string `yaml:"patch_url"` // SECURITY: This must be safe to download directly from + Merged bool + MergedTime *time.Time `yaml:"merged_time"` + MergeCommitSHA string `yaml:"merge_commit_sha"` + Head PullRequestBranch + Base PullRequestBranch + Assignees []string + IsLocked bool `yaml:"is_locked"` + Reactions []*Reaction + ForeignIndex int64 + Context DownloaderContext `yaml:"-"` + EnsuredSafe bool `yaml:"ensured_safe"` +} + +func (p *PullRequest) GetLocalIndex() int64 { return p.Number } +func (p *PullRequest) GetForeignIndex() int64 { return p.ForeignIndex } +func (p *PullRequest) GetContext() DownloaderContext { return p.Context } + +// IsForkPullRequest returns true if the pull request from a forked repository but not the same repository +func (p *PullRequest) IsForkPullRequest() bool { + return p.Head.RepoFullName() != p.Base.RepoFullName() +} + +// GetGitRefName returns pull request relative path to head +func (p PullRequest) GetGitRefName() string { + return fmt.Sprintf("%s%d/head", git.PullPrefix, p.Number) +} + +// PullRequestBranch represents a pull request branch +type PullRequestBranch struct { + CloneURL string `yaml:"clone_url"` // SECURITY: This must be safe to download from + Ref string // SECURITY: this must be a git.IsValidRefPattern + SHA string // SECURITY: this must be a git.IsValidSHAPattern + RepoName string `yaml:"repo_name"` + OwnerName string `yaml:"owner_name"` +} + +// RepoFullName returns pull request repo full name +func (p PullRequestBranch) RepoFullName() string { + return fmt.Sprintf("%s/%s", p.OwnerName, p.RepoName) +} + +// GetExternalName ExternalUserMigrated interface +func (p *PullRequest) GetExternalName() string { return p.PosterName } + +// ExternalID ExternalUserMigrated interface +func (p *PullRequest) GetExternalID() int64 { return p.PosterID } diff --git a/modules/migration/reaction.go b/modules/migration/reaction.go new file mode 100644 index 00000000..ca1df6c7 --- /dev/null +++ b/modules/migration/reaction.go @@ -0,0 +1,17 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +// Reaction represents a reaction to an issue/pr/comment. +type Reaction struct { + UserID int64 `yaml:"user_id" json:"user_id"` + UserName string `yaml:"user_name" json:"user_name"` + Content string `json:"content"` +} + +// GetExternalName ExternalUserMigrated interface +func (r *Reaction) GetExternalName() string { return r.UserName } + +// GetExternalID ExternalUserMigrated interface +func (r *Reaction) GetExternalID() int64 { return r.UserID } diff --git a/modules/migration/release.go b/modules/migration/release.go new file mode 100644 index 00000000..f92cf25e --- /dev/null +++ b/modules/migration/release.go @@ -0,0 +1,46 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import ( + "io" + "time" +) + +// ReleaseAsset represents a release asset +type ReleaseAsset struct { + ID int64 + Name string + ContentType *string `yaml:"content_type"` + Size *int + DownloadCount *int `yaml:"download_count"` + Created time.Time + Updated time.Time + + DownloadURL *string `yaml:"download_url"` // SECURITY: It is the responsibility of downloader to make sure this is safe + // if DownloadURL is nil, the function should be invoked + DownloadFunc func() (io.ReadCloser, error) `yaml:"-"` // SECURITY: It is the responsibility of downloader to make sure this is safe +} + +// Release represents a release +type Release struct { + TagName string `yaml:"tag_name"` // SECURITY: This must pass git.IsValidRefPattern + TargetCommitish string `yaml:"target_commitish"` // SECURITY: This must pass git.IsValidRefPattern + Name string + Body string + Draft bool + Prerelease bool + PublisherID int64 `yaml:"publisher_id"` + PublisherName string `yaml:"publisher_name"` + PublisherEmail string `yaml:"publisher_email"` + Assets []*ReleaseAsset + Created time.Time + Published time.Time +} + +// GetExternalName ExternalUserMigrated interface +func (r *Release) GetExternalName() string { return r.PublisherName } + +// GetExternalID ExternalUserMigrated interface +func (r *Release) GetExternalID() int64 { return r.PublisherID } diff --git a/modules/migration/repo.go b/modules/migration/repo.go new file mode 100644 index 00000000..22c2cf6f --- /dev/null +++ b/modules/migration/repo.go @@ -0,0 +1,17 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +// Repository defines a standard repository information +type Repository struct { + Name string + Owner string + IsPrivate bool `yaml:"is_private"` + IsMirror bool `yaml:"is_mirror"` + Description string + CloneURL string `yaml:"clone_url"` // SECURITY: This must be checked to ensure that is safe to be used + OriginalURL string `yaml:"original_url"` + DefaultBranch string +} diff --git a/modules/migration/retry_downloader.go b/modules/migration/retry_downloader.go new file mode 100644 index 00000000..1cacf5f3 --- /dev/null +++ b/modules/migration/retry_downloader.go @@ -0,0 +1,194 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import ( + "context" + "time" +) + +var _ Downloader = &RetryDownloader{} + +// RetryDownloader retry the downloads +type RetryDownloader struct { + Downloader + ctx context.Context + RetryTimes int // the total execute times + RetryDelay int // time to delay seconds +} + +// NewRetryDownloader creates a retry downloader +func NewRetryDownloader(ctx context.Context, downloader Downloader, retryTimes, retryDelay int) *RetryDownloader { + return &RetryDownloader{ + Downloader: downloader, + ctx: ctx, + RetryTimes: retryTimes, + RetryDelay: retryDelay, + } +} + +func (d *RetryDownloader) retry(work func() error) error { + var ( + times = d.RetryTimes + err error + ) + for ; times > 0; times-- { + if err = work(); err == nil { + return nil + } + if IsErrNotSupported(err) { + return err + } + select { + case <-d.ctx.Done(): + return d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return err +} + +// SetContext set context +func (d *RetryDownloader) SetContext(ctx context.Context) { + d.ctx = ctx + d.Downloader.SetContext(ctx) +} + +// GetRepoInfo returns a repository information with retry +func (d *RetryDownloader) GetRepoInfo() (*Repository, error) { + var ( + repo *Repository + err error + ) + + err = d.retry(func() error { + repo, err = d.Downloader.GetRepoInfo() + return err + }) + + return repo, err +} + +// GetTopics returns a repository's topics with retry +func (d *RetryDownloader) GetTopics() ([]string, error) { + var ( + topics []string + err error + ) + + err = d.retry(func() error { + topics, err = d.Downloader.GetTopics() + return err + }) + + return topics, err +} + +// GetMilestones returns a repository's milestones with retry +func (d *RetryDownloader) GetMilestones() ([]*Milestone, error) { + var ( + milestones []*Milestone + err error + ) + + err = d.retry(func() error { + milestones, err = d.Downloader.GetMilestones() + return err + }) + + return milestones, err +} + +// GetReleases returns a repository's releases with retry +func (d *RetryDownloader) GetReleases() ([]*Release, error) { + var ( + releases []*Release + err error + ) + + err = d.retry(func() error { + releases, err = d.Downloader.GetReleases() + return err + }) + + return releases, err +} + +// GetLabels returns a repository's labels with retry +func (d *RetryDownloader) GetLabels() ([]*Label, error) { + var ( + labels []*Label + err error + ) + + err = d.retry(func() error { + labels, err = d.Downloader.GetLabels() + return err + }) + + return labels, err +} + +// GetIssues returns a repository's issues with retry +func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { + var ( + issues []*Issue + isEnd bool + err error + ) + + err = d.retry(func() error { + issues, isEnd, err = d.Downloader.GetIssues(page, perPage) + return err + }) + + return issues, isEnd, err +} + +// GetComments returns a repository's comments with retry +func (d *RetryDownloader) GetComments(commentable Commentable) ([]*Comment, bool, error) { + var ( + comments []*Comment + isEnd bool + err error + ) + + err = d.retry(func() error { + comments, isEnd, err = d.Downloader.GetComments(commentable) + return err + }) + + return comments, isEnd, err +} + +// GetPullRequests returns a repository's pull requests with retry +func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) { + var ( + prs []*PullRequest + err error + isEnd bool + ) + + err = d.retry(func() error { + prs, isEnd, err = d.Downloader.GetPullRequests(page, perPage) + return err + }) + + return prs, isEnd, err +} + +// GetReviews returns pull requests reviews +func (d *RetryDownloader) GetReviews(reviewable Reviewable) ([]*Review, error) { + var ( + reviews []*Review + err error + ) + + err = d.retry(func() error { + reviews, err = d.Downloader.GetReviews(reviewable) + return err + }) + + return reviews, err +} diff --git a/modules/migration/review.go b/modules/migration/review.go new file mode 100644 index 00000000..79e821b2 --- /dev/null +++ b/modules/migration/review.go @@ -0,0 +1,67 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +import "time" + +// Reviewable can be reviewed +type Reviewable interface { + GetLocalIndex() int64 + + // GetForeignIndex presents the foreign index, which could be misused: + // For example, if there are 2 Gitea sites: site-A exports a dataset, then site-B imports it: + // * if site-A exports files by using its LocalIndex + // * from site-A's view, LocalIndex is site-A's IssueIndex while ForeignIndex is site-B's IssueIndex + // * but from site-B's view, LocalIndex is site-B's IssueIndex while ForeignIndex is site-A's IssueIndex + // + // So the exporting/importing must be paired, but the meaning of them looks confusing then: + // * either site-A and site-B both use LocalIndex during dumping/restoring + // * or site-A and site-B both use ForeignIndex + GetForeignIndex() int64 +} + +// enumerate all review states +const ( + ReviewStatePending = "PENDING" + ReviewStateApproved = "APPROVED" + ReviewStateChangesRequested = "CHANGES_REQUESTED" + ReviewStateCommented = "COMMENTED" + ReviewStateRequestReview = "REQUEST_REVIEW" +) + +// Review is a standard review information +type Review struct { + ID int64 + IssueIndex int64 `yaml:"issue_index"` + ReviewerID int64 `yaml:"reviewer_id"` + ReviewerName string `yaml:"reviewer_name"` + Official bool + CommitID string `yaml:"commit_id"` + Content string + CreatedAt time.Time `yaml:"created_at"` + State string // PENDING, APPROVED, REQUEST_CHANGES, or COMMENT + Comments []*ReviewComment +} + +// GetExternalName ExternalUserMigrated interface +func (r *Review) GetExternalName() string { return r.ReviewerName } + +// GetExternalID ExternalUserMigrated interface +func (r *Review) GetExternalID() int64 { return r.ReviewerID } + +// ReviewComment represents a review comment +type ReviewComment struct { + ID int64 + InReplyTo int64 `yaml:"in_reply_to"` + Content string + TreePath string `yaml:"tree_path"` + DiffHunk string `yaml:"diff_hunk"` + Position int + Line int + CommitID string `yaml:"commit_id"` + PosterID int64 `yaml:"poster_id"` + Reactions []*Reaction + CreatedAt time.Time `yaml:"created_at"` + UpdatedAt time.Time `yaml:"updated_at"` +} diff --git a/modules/migration/schemas/issue.json b/modules/migration/schemas/issue.json new file mode 100644 index 00000000..25753c39 --- /dev/null +++ b/modules/migration/schemas/issue.json @@ -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" +} diff --git a/modules/migration/schemas/label.json b/modules/migration/schemas/label.json new file mode 100644 index 00000000..561a2e33 --- /dev/null +++ b/modules/migration/schemas/label.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" +} diff --git a/modules/migration/schemas/milestone.json b/modules/migration/schemas/milestone.json new file mode 100644 index 00000000..7024ef45 --- /dev/null +++ b/modules/migration/schemas/milestone.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" +} diff --git a/modules/migration/schemas/reaction.json b/modules/migration/schemas/reaction.json new file mode 100644 index 00000000..25652514 --- /dev/null +++ b/modules/migration/schemas/reaction.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" +} diff --git a/modules/migration/schemas_bindata.go b/modules/migration/schemas_bindata.go new file mode 100644 index 00000000..c5db3b34 --- /dev/null +++ b/modules/migration/schemas_bindata.go @@ -0,0 +1,8 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build bindata + +package migration + +//go:generate go run ../../build/generate-bindata.go ../../modules/migration/schemas migration bindata.go diff --git a/modules/migration/schemas_dynamic.go b/modules/migration/schemas_dynamic.go new file mode 100644 index 00000000..dca109d6 --- /dev/null +++ b/modules/migration/schemas_dynamic.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go: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) +} diff --git a/modules/migration/schemas_static.go b/modules/migration/schemas_static.go new file mode 100644 index 00000000..8a0c340a --- /dev/null +++ b/modules/migration/schemas_static.go @@ -0,0 +1,15 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build bindata + +package migration + +import ( + "io" + "path" +) + +func openSchema(filename string) (io.ReadCloser, error) { + return Assets.Open(path.Base(filename)) +} diff --git a/modules/migration/uploader.go b/modules/migration/uploader.go new file mode 100644 index 00000000..ff642aa4 --- /dev/null +++ b/modules/migration/uploader.go @@ -0,0 +1,23 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2018 Jonas Franz. All rights reserved. +// SPDX-License-Identifier: MIT + +package migration + +// Uploader uploads all the information of one repository +type Uploader interface { + MaxBatchInsertSize(tp string) int + CreateRepo(repo *Repository, opts MigrateOptions) error + CreateTopics(topic ...string) error + CreateMilestones(milestones ...*Milestone) error + CreateReleases(releases ...*Release) error + SyncTags() error + CreateLabels(labels ...*Label) error + CreateIssues(issues ...*Issue) error + CreateComments(comments ...*Comment) error + CreatePullRequests(prs ...*PullRequest) error + CreateReviews(reviews ...*Review) error + Rollback() error + Finish() error + Close() +} |