diff --git a/integrations/lfs_getobject_test.go b/integrations/lfs_getobject_test.go index f364349ef140a..477fc4a61ae19 100644 --- a/integrations/lfs_getobject_test.go +++ b/integrations/lfs_getobject_test.go @@ -16,7 +16,6 @@ import ( "testing" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/routers/routes" @@ -50,7 +49,7 @@ func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string lfsID++ lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject) assert.NoError(t, err) - contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} exist, err := contentStore.Exists(lfsMetaObject) assert.NoError(t, err) if !exist { diff --git a/models/error.go b/models/error.go index 84b7ebbfa35b0..8ce3db4e6132c 100644 --- a/models/error.go +++ b/models/error.go @@ -56,6 +56,20 @@ func (err ErrNamePatternNotAllowed) Error() string { return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern) } +// ErrMirrorLFSServerNotValid represents an "LFS Server not valid" error. +type ErrMirrorLFSServerNotValid struct { +} + +// IsMirrorLFSServerValid checks if an error is an ErrMirrorLFSServerNotValid. +func IsMirrorLFSServerValid(err error) bool { + _, ok := err.(ErrMirrorLFSServerNotValid) + return ok +} + +func (err ErrMirrorLFSServerNotValid) Error() string { + return "LFS Server not valid" +} + // ErrNameCharsNotAllowed represents a "character not allowed in name" error. type ErrNameCharsNotAllowed struct { Name string diff --git a/models/lfs.go b/models/lfs.go index 274b32a736758..1f52678df78c3 100644 --- a/models/lfs.go +++ b/models/lfs.go @@ -11,7 +11,12 @@ import ( "fmt" "io" "path" + "strconv" + "strings" + "time" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -27,6 +32,70 @@ type LFSMetaObject struct { CreatedUnix timeutil.TimeStamp `xorm:"created"` } +// LFSMetaObjectBasic represents basic LFS metadata. +type LFSMetaObjectBasic struct { + Oid string `json:"oid"` + Size int64 `json:"size"` +} + +// IsPointerFileAndStored will return a partially filled LFSMetaObject if the provided byte slice is a pointer file and stored in contentStore +func IsPointerFileAndStored(buf *[]byte) *LFSMetaObject { + if !setting.LFS.StartServer { + return nil + } + + headString := string(*buf) + if !strings.HasPrefix(headString, LFSMetaFileIdentifier) { + return nil + } + + splitLines := strings.Split(headString, "\n") + if len(splitLines) < 3 { + return nil + } + + oid := strings.TrimPrefix(splitLines[1], LFSMetaFileOidPrefix) + size, err := strconv.ParseInt(strings.TrimPrefix(splitLines[2], "size "), 10, 64) + if len(oid) != 64 || err != nil { + return nil + } + + contentStore := &ContentStore{ObjectStorage: storage.LFS} + meta := &LFSMetaObject{Oid: oid, Size: size} + exist, err := contentStore.Exists(meta) + if err != nil || !exist { + return nil + } + + return meta +} + +// IsPointerFile will return a partially filled LFSMetaObject if the provided byte slice is a pointer file +func IsPointerFile(buf *[]byte) *LFSMetaObjectBasic { + if !setting.LFS.StartServer { + return nil + } + + headString := string(*buf) + if !strings.HasPrefix(headString, LFSMetaFileIdentifier) { + return nil + } + + splitLines := strings.Split(headString, "\n") + if len(splitLines) < 3 { + return nil + } + + oid := strings.TrimPrefix(splitLines[1], LFSMetaFileOidPrefix) + size, err := strconv.ParseInt(strings.TrimPrefix(splitLines[2], "size "), 10, 64) + if len(oid) != 64 || err != nil { + return nil + } + meta := &LFSMetaObjectBasic{Oid: oid, Size: size} + + return meta +} + // RelativePath returns the relative path of the lfs object func (m *LFSMetaObject) RelativePath() string { if len(m.Oid) < 5 { @@ -234,3 +303,31 @@ func IterateLFS(f func(mo *LFSMetaObject) error) error { } } } + +// BatchResponse contains multiple object metadata Representation structures +// for use with the batch API. +type BatchResponse struct { + Transfer string `json:"transfer,omitempty"` + Objects []*Representation `json:"objects"` +} + +// Representation is object metadata as seen by clients of the lfs server. +type Representation struct { + Oid string `json:"oid"` + Size int64 `json:"size"` + Actions map[string]*Link `json:"actions"` + Error *ObjectError `json:"error,omitempty"` +} + +// ObjectError defines the JSON structure returned to the client in case of an error +type ObjectError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// Link provides a structure used to build a hypermedia representation of an HTTP link. +type Link struct { + Href string `json:"href"` + Header map[string]string `json:"header,omitempty"` + ExpiresAt time.Time `json:"expires_at,omitempty"` +} diff --git a/modules/lfs/content_store.go b/models/lfs_content_store.go similarity index 91% rename from modules/lfs/content_store.go rename to models/lfs_content_store.go index 788ef5b9a6950..8122b0b49deb7 100644 --- a/modules/lfs/content_store.go +++ b/models/lfs_content_store.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package lfs +package models import ( "crypto/sha256" @@ -13,7 +13,6 @@ import ( "io" "os" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" ) @@ -45,7 +44,7 @@ type ContentStore struct { // Get takes a Meta object and retrieves the content from the store, returning // it as an io.Reader. If fromByte > 0, the reader starts from that byte -func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadCloser, error) { +func (s *ContentStore) Get(meta *LFSMetaObject, fromByte int64) (io.ReadCloser, error) { f, err := s.Open(meta.RelativePath()) if err != nil { log.Error("Whilst trying to read LFS OID[%s]: Unable to open Error: %v", meta.Oid, err) @@ -66,7 +65,7 @@ func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadC } // Put takes a Meta object and an io.Reader and writes the content to the store. -func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error { +func (s *ContentStore) Put(meta *LFSMetaObject, r io.Reader) error { p := meta.RelativePath() // Wrap the provided reader with an inline hashing and size checker @@ -92,7 +91,7 @@ func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error { } // Exists returns true if the object exists in the content store. -func (s *ContentStore) Exists(meta *models.LFSMetaObject) (bool, error) { +func (s *ContentStore) Exists(meta *LFSMetaObject) (bool, error) { _, err := s.ObjectStorage.Stat(meta.RelativePath()) if err != nil { if os.IsNotExist(err) { @@ -104,7 +103,7 @@ func (s *ContentStore) Exists(meta *models.LFSMetaObject) (bool, error) { } // Verify returns true if the object exists in the content store and size is correct. -func (s *ContentStore) Verify(meta *models.LFSMetaObject) (bool, error) { +func (s *ContentStore) Verify(meta *LFSMetaObject) (bool, error) { p := meta.RelativePath() fi, err := s.ObjectStorage.Stat(p) if os.IsNotExist(err) || (err == nil && fi.Size() != meta.Size) { diff --git a/models/repo.go b/models/repo.go index 2c71fc3e1eede..874951ea6f5d8 100644 --- a/models/repo.go +++ b/models/repo.go @@ -215,10 +215,12 @@ type Repository struct { NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"` NumOpenProjects int `xorm:"-"` - IsPrivate bool `xorm:"INDEX"` - IsEmpty bool `xorm:"INDEX"` - IsArchived bool `xorm:"INDEX"` - IsMirror bool `xorm:"INDEX"` + IsPrivate bool `xorm:"INDEX"` + IsEmpty bool `xorm:"INDEX"` + IsArchived bool `xorm:"INDEX"` + IsMirror bool `xorm:"INDEX"` + LFS bool `xorm:"INDEX"` + LFSServer string `xorm:"TEXT"` *Mirror `xorm:"-"` Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` @@ -945,7 +947,7 @@ func (repo *Repository) CloneLink() (cl *CloneLink) { } // CheckCreateRepository check if could created a repository -func CheckCreateRepository(doer, u *User, name string, overwriteOrAdopt bool) error { +func CheckCreateRepository(doer, u *User, name string, lfs bool, lfsServer string, overwriteOrAdopt bool) error { if !doer.CanCreateRepo() { return ErrReachLimitOfRepo{u.MaxRepoCreation} } @@ -969,6 +971,13 @@ func CheckCreateRepository(doer, u *User, name string, overwriteOrAdopt bool) er if !overwriteOrAdopt && isExist { return ErrRepoFilesAlreadyExist{u.Name, name} } + + if lfs { + _, err := url.ParseRequestURI(lfsServer) + if err != nil { + return ErrMirrorLFSServerNotValid{} + } + } return nil } diff --git a/modules/forms/repo_form.go b/modules/forms/repo_form.go index ab88aef571f0b..775f4956e16d1 100644 --- a/modules/forms/repo_form.go +++ b/modules/forms/repo_form.go @@ -73,6 +73,9 @@ type MigrateRepoForm struct { // required: true RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` Mirror bool `json:"mirror"` + LFS bool `json:"lfs"` + LFSServer string `json:"lfs_server"` + LFSFetchOlder bool `json:"lfs_fetch_older"` Private bool `json:"private"` Description string `json:"description" binding:"MaxSize(255)"` Wiki bool `json:"wiki"` diff --git a/modules/git/blob.go b/modules/git/blob.go index 674a6a9592778..e3fa0f2fdd672 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -32,6 +32,19 @@ func (b *Blob) GetBlobContent() (string, error) { return string(buf), nil } +// GetBlobFirstBytes gets limited content of the blob as bytes +func (b *Blob) GetBlobFirstBytes(limit int) ([]byte, error) { + dataRc, err := b.DataAsync() + buf := make([]byte, limit) + if err != nil { + return buf, err + } + defer dataRc.Close() + n, _ := dataRc.Read(buf) + buf = buf[:n] + return buf, nil +} + // GetBlobLineCount gets line count of lob as raw text func (b *Blob) GetBlobLineCount() (int, error) { reader, err := b.DataAsync() diff --git a/modules/lfs/pointers.go b/modules/lfs/pointers.go index c6fbf090e5164..30850145a347c 100644 --- a/modules/lfs/pointers.go +++ b/modules/lfs/pointers.go @@ -6,8 +6,6 @@ package lfs import ( "io" - "strconv" - "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" @@ -29,43 +27,11 @@ func ReadPointerFile(reader io.Reader) (*models.LFSMetaObject, *[]byte) { return nil, nil } - return IsPointerFile(&buf), &buf -} - -// IsPointerFile will return a partially filled LFSMetaObject if the provided byte slice is a pointer file -func IsPointerFile(buf *[]byte) *models.LFSMetaObject { - if !setting.LFS.StartServer { - return nil - } - - headString := string(*buf) - if !strings.HasPrefix(headString, models.LFSMetaFileIdentifier) { - return nil - } - - splitLines := strings.Split(headString, "\n") - if len(splitLines) < 3 { - return nil - } - - oid := strings.TrimPrefix(splitLines[1], models.LFSMetaFileOidPrefix) - size, err := strconv.ParseInt(strings.TrimPrefix(splitLines[2], "size "), 10, 64) - if len(oid) != 64 || err != nil { - return nil - } - - contentStore := &ContentStore{ObjectStorage: storage.LFS} - meta := &models.LFSMetaObject{Oid: oid, Size: size} - exist, err := contentStore.Exists(meta) - if err != nil || !exist { - return nil - } - - return meta + return models.IsPointerFileAndStored(&buf), &buf } // ReadMetaObject will read a models.LFSMetaObject and return a reader func ReadMetaObject(meta *models.LFSMetaObject) (io.ReadCloser, error) { - contentStore := &ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} return contentStore.Get(meta, 0) } diff --git a/modules/lfs/server.go b/modules/lfs/server.go index 45cba9d9b7512..a40ae3bfb7eb7 100644 --- a/modules/lfs/server.go +++ b/modules/lfs/server.go @@ -6,6 +6,8 @@ package lfs import ( "encoding/base64" + + "errors" "fmt" "io" "net/http" @@ -13,7 +15,6 @@ import ( "regexp" "strconv" "strings" - "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" @@ -29,6 +30,11 @@ const ( metaMediaType = "application/vnd.git-lfs+json" ) +var ( + errHashMismatch = errors.New("Content hash does not match OID") + errSizeMismatch = errors.New("Content size does not match") +) + // RequestVars contain variables from the HTTP request. Variables from routing, json body decoding, and // some headers are stored. type RequestVars struct { @@ -48,27 +54,6 @@ type BatchVars struct { Objects []*RequestVars `json:"objects"` } -// BatchResponse contains multiple object metadata Representation structures -// for use with the batch API. -type BatchResponse struct { - Transfer string `json:"transfer,omitempty"` - Objects []*Representation `json:"objects"` -} - -// Representation is object metadata as seen by clients of the lfs server. -type Representation struct { - Oid string `json:"oid"` - Size int64 `json:"size"` - Actions map[string]*link `json:"actions"` - Error *ObjectError `json:"error,omitempty"` -} - -// ObjectError defines the JSON structure returned to the client in case of an error -type ObjectError struct { - Code int `json:"code"` - Message string `json:"message"` -} - // Claims is a JWT Token Claims type Claims struct { RepoID int64 @@ -87,13 +72,6 @@ func (v *RequestVars) VerifyLink() string { return setting.AppURL + path.Join(v.User, v.Repo+".git", "info/lfs/verify") } -// link provides a structure used to build a hypermedia representation of an HTTP link. -type link struct { - Href string `json:"href"` - Header map[string]string `json:"header,omitempty"` - ExpiresAt time.Time `json:"expires_at,omitempty"` -} - var oidRegExp = regexp.MustCompile(`^[A-Fa-f0-9]+$`) func isOidValid(oid string) bool { @@ -187,10 +165,10 @@ func getContentHandler(ctx *context.Context) { } } - contentStore := &ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} content, err := contentStore.Get(meta, fromByte) if err != nil { - if IsErrRangeNotSatisfiable(err) { + if models.IsErrRangeNotSatisfiable(err) { writeStatus(ctx, http.StatusRequestedRangeNotSatisfiable) } else { // Errors are logged in contentStore.Get @@ -293,7 +271,7 @@ func PostHandler(ctx *context.Context) { ctx.Resp.Header().Set("Content-Type", metaMediaType) sentStatus := 202 - contentStore := &ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} exist, err := contentStore.Exists(meta) if err != nil { log.Error("Unable to check if LFS OID[%s] exist on %s / %s. Error: %v", rv.Oid, rv.User, rv.Repo, err) @@ -329,7 +307,7 @@ func BatchHandler(ctx *context.Context) { bv := unpackbatch(ctx) - var responseObjects []*Representation + var responseObjects []*models.Representation // Create a response object for _, object := range bv.Objects { @@ -355,7 +333,7 @@ func BatchHandler(ctx *context.Context) { return } - contentStore := &ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} meta, err := repository.GetLFSMetaObjectByOid(object.Oid) if err == nil { // Object is found and exists @@ -394,7 +372,7 @@ func BatchHandler(ctx *context.Context) { ctx.Resp.Header().Set("Content-Type", metaMediaType) - respobj := &BatchResponse{Objects: responseObjects} + respobj := &models.BatchResponse{Objects: responseObjects} json := jsoniter.ConfigCompatibleWithStandardLibrary enc := json.NewEncoder(ctx.Resp) @@ -414,7 +392,7 @@ func PutHandler(ctx *context.Context) { return } - contentStore := &ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} defer ctx.Req.Body.Close() if err := contentStore.Put(meta, ctx.Req.Body); err != nil { // Put will log the error itself @@ -455,7 +433,7 @@ func VerifyHandler(ctx *context.Context) { return } - contentStore := &ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} ok, err := contentStore.Verify(meta) if err != nil { // Error will be logged in Verify @@ -473,11 +451,11 @@ func VerifyHandler(ctx *context.Context) { // Represent takes a RequestVars and Meta and turns it into a Representation suitable // for json encoding -func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload bool) *Representation { - rep := &Representation{ +func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload bool) *models.Representation { + rep := &models.Representation{ Oid: meta.Oid, Size: meta.Size, - Actions: make(map[string]*link), + Actions: make(map[string]*models.Link), } header := make(map[string]string) @@ -490,11 +468,11 @@ func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload boo } if download { - rep.Actions["download"] = &link{Href: rv.ObjectLink(), Header: header} + rep.Actions["download"] = &models.Link{Href: rv.ObjectLink(), Header: header} } if upload { - rep.Actions["upload"] = &link{Href: rv.ObjectLink(), Header: header} + rep.Actions["upload"] = &models.Link{Href: rv.ObjectLink(), Header: header} } if upload && !download { @@ -507,7 +485,7 @@ func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload boo // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662 verifyHeader["Accept"] = metaMediaType - rep.Actions["verify"] = &link{Href: rv.VerifyLink(), Header: verifyHeader} + rep.Actions["verify"] = &models.Link{Href: rv.VerifyLink(), Header: verifyHeader} } return rep diff --git a/modules/lfsclient/client.go b/modules/lfsclient/client.go new file mode 100644 index 0000000000000..417d7271340f2 --- /dev/null +++ b/modules/lfsclient/client.go @@ -0,0 +1,141 @@ +// Copyright 2020 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 lfsclient + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" +) + +const ( + metaMediaType = "application/vnd.git-lfs+json" +) + +// BatchRequest encodes json object using in a lfs batch api request +type BatchRequest struct { + Operation string `json:"operation"` + Transfers []string `json:"transfers,omitempty"` + Ref *Reference `json:"ref,omitempty"` + Objects []*models.LFSMetaObjectBasic `json:"objects"` +} + +// Reference is a reference field of BatchRequest +type Reference struct { + Name string `json:"name"` +} + +// packbatch packs lfs batch request to json encoded as bytes +func packbatch(operation string, transfers []string, ref *Reference, metaObjects []*models.LFSMetaObject) (*bytes.Buffer, error) { + metaObjectsBasic := []*models.LFSMetaObjectBasic{} + for _, meta := range metaObjects { + metaBasic := &models.LFSMetaObjectBasic{Oid: meta.Oid, Size: meta.Size} + metaObjectsBasic = append(metaObjectsBasic, metaBasic) + } + + reqobj := &BatchRequest{operation, transfers, ref, metaObjectsBasic} + + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(reqobj); err != nil { + return buf, fmt.Errorf("Failed to encode BatchRequest as json. Error: %v", err) + } + return buf, nil +} + +// BasicTransferAdapter makes request to lfs server and returns io.ReadCLoser +func BasicTransferAdapter(ctx context.Context, client *http.Client, href string, size int64) (io.ReadCloser, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, href, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-type", "application/octet-stream") + req.Header.Set("Content-Length", strconv.Itoa(int(size))) + + resp, err := client.Do(req) + if err != nil { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Failed to query BasicTransferAdapter with response: %s", resp.Status) + } + return resp.Body, nil +} + +// FetchLFSFilesToContentStore downloads []LFSMetaObject from lfsServer to ContentStore +func FetchLFSFilesToContentStore(ctx context.Context, metaObjects []*models.LFSMetaObject, userName string, repo *models.Repository, lfsServer string, contentStore *models.ContentStore) error { + client := http.DefaultClient + + rv, err := packbatch("download", []string{"basic"}, nil, metaObjects) + if err != nil { + return err + } + batchAPIURL := lfsServer + "/objects/batch" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, batchAPIURL, rv) + if err != nil { + return err + } + req.Header.Set("Content-type", metaMediaType) + req.Header.Set("Accept", metaMediaType) + + resp, err := client.Do(req) + if err != nil { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Failed to query Batch with response: %s", resp.Status) + } + + var respBatch models.BatchResponse + err = json.NewDecoder(resp.Body).Decode(&respBatch) + if err != nil { + return err + } + + if len(respBatch.Transfer) == 0 { + respBatch.Transfer = "basic" + } + + for _, rep := range respBatch.Objects { + rc, err := BasicTransferAdapter(ctx, client, rep.Actions["download"].Href, rep.Size) + if err != nil { + log.Error("Unable to use BasicTransferAdapter. Error: %v", err) + return err + } + meta, err := repo.GetLFSMetaObjectByOid(rep.Oid) + if err != nil { + log.Error("Unable to get LFS OID[%s] Error: %v", rep.Oid, err) + return err + } + + // put LFS file to contentStore + if err := contentStore.Put(meta, rc); err != nil { + if _, err2 := repo.RemoveLFSMetaObjectByOid(meta.Oid); err2 != nil { + return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", meta.Oid, err2, err) + } + return err + } + } + return nil +} diff --git a/modules/migrations/base/options.go b/modules/migrations/base/options.go index 168f9848c813d..2adb772572356 100644 --- a/modules/migrations/base/options.go +++ b/modules/migrations/base/options.go @@ -20,6 +20,9 @@ type MigrateOptions struct { // required: true RepoName string `json:"repo_name" binding:"Required"` Mirror bool `json:"mirror"` + LFS bool `json:"lfs"` + LFSServer string `json:"lfs_server"` + LFSFetchOlder bool `json:"lfs_fetch_older"` Private bool `json:"private"` Description string `json:"description"` OriginalURL string diff --git a/modules/migrations/base/repo.go b/modules/migrations/base/repo.go index 693a96314dc1f..1c8f91339028d 100644 --- a/modules/migrations/base/repo.go +++ b/modules/migrations/base/repo.go @@ -9,8 +9,10 @@ package base type Repository struct { Name string Owner string - IsPrivate bool `yaml:"is_private"` - IsMirror bool `yaml:"is_mirror"` + IsPrivate bool `yaml:"is_private"` + IsMirror bool `yaml:"is_mirror"` + LFS bool `yaml:"lfs"` + LFSServer string `yaml:"lfs_server"` Description string CloneURL string `yaml:"clone_url"` OriginalURL string `yaml:"original_url"` diff --git a/modules/migrations/gitea_uploader.go b/modules/migrations/gitea_uploader.go index aa1ea4bc09d04..6e16ce3ab18e3 100644 --- a/modules/migrations/gitea_uploader.go +++ b/modules/migrations/gitea_uploader.go @@ -116,6 +116,9 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate OriginalURL: repo.OriginalURL, GitServiceType: opts.GitServiceType, Mirror: repo.IsMirror, + LFS: opts.LFS, + LFSServer: opts.LFSServer, + LFSFetchOlder: opts.LFSFetchOlder, CloneAddr: repo.CloneURL, Private: repo.IsPrivate, Wiki: opts.Wiki, diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 656b78a584895..71ded71b93a1f 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -147,6 +147,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts } repo.IsPrivate = opts.Private repo.IsMirror = opts.Mirror + repo.LFS = opts.LFS + repo.LFSServer = opts.LFSServer if opts.Description != "" { repo.Description = opts.Description } diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index d25e109b29ede..affef0799fb20 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -70,7 +70,7 @@ func detectEncodingAndBOM(entry *git.TreeEntry, repo *models.Repository) (string buf = buf[:n] if setting.LFS.StartServer { - meta := lfs.IsPointerFile(&buf) + meta := models.IsPointerFileAndStored(&buf) if meta != nil { meta, err = repo.GetLFSMetaObjectByOid(meta.Oid) if err != nil && err != models.ErrLFSObjectNotExist { @@ -435,7 +435,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up if err != nil { return nil, err } - contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} exist, err := contentStore.Exists(lfsMetaObject) if err != nil { return nil, err diff --git a/modules/repofiles/upload.go b/modules/repofiles/upload.go index 2846e6c44b8c1..cadff5b3159a8 100644 --- a/modules/repofiles/upload.go +++ b/modules/repofiles/upload.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" ) @@ -169,7 +168,7 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep // OK now we can insert the data into the store - there's no way to clean up the store // once it's in there, it's in there. - contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS} + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} for _, uploadInfo := range infos { if uploadInfo.lfsMetaObject == nil { continue diff --git a/modules/repository/repo.go b/modules/repository/repo.go index ede714673ab16..4094d26889c5c 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -13,9 +13,11 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/lfsclient" "code.gitea.io/gitea/modules/log" migration "code.gitea.io/gitea/modules/migrations/base" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -98,6 +100,13 @@ func MigrateRepositoryGitData(ctx context.Context, u *models.User, repo *models. } defer gitRepo.Close() + if opts.LFS { + err := FetchMissingLFSFilesToContentStore(ctx, repo, u.Name, gitRepo, opts.LFSServer, opts.LFSFetchOlder) + if err != nil { + return repo, fmt.Errorf("Failed to fetch LFS files: %v", err) + } + } + repo.IsEmpty, err = gitRepo.IsEmpty() if err != nil { return repo, fmt.Errorf("git.IsEmpty: %v", err) @@ -166,6 +175,109 @@ func MigrateRepositoryGitData(ctx context.Context, u *models.User, repo *models. return repo, err } +// FetchMissingLFSFilesToContentStore downloads lfs files to a ContentStore +func FetchMissingLFSFilesToContentStore(ctx context.Context, repo *models.Repository, userName string, gitRepo *git.Repository, lfsServer string, lfsFetchOlder bool) error { + fetchingMetaObjectsSet := make(map[string]*models.LFSMetaObject) + var err error + contentStore := &models.ContentStore{ObjectStorage: storage.LFS} + + // scan repo for pointer files + headBranch, _ := gitRepo.GetHEADBranch() + headCommit, _ := gitRepo.GetCommit(headBranch.Name) + + err = FindLFSMetaObjectsBelowMaxFileSizeWithMissingFiles(headCommit, userName, repo, &fetchingMetaObjectsSet, contentStore) + if err != nil { + log.Error("Failed to access git LFS meta objects on commit %s: %v", headCommit.ID.String(), err) + return err + } + + if lfsFetchOlder { + opts := git.NewSearchCommitsOptions("before:"+headCommit.ID.String(), true) + commitIDsList, _ := headCommit.SearchCommits(opts) + var commitIDs = []string{} + for e := commitIDsList.Front(); e != nil; e = e.Next() { + commitIDs = append(commitIDs, e.Value.(string)) + } + commitsList := gitRepo.GetCommitsFromIDs(commitIDs) + + for e := commitsList.Front(); e != nil; e = e.Next() { + commit := e.Value.(*git.Commit) + err = FindLFSMetaObjectsBelowMaxFileSizeWithMissingFiles(commit, userName, repo, &fetchingMetaObjectsSet, contentStore) + if err != nil { + log.Error("Failed to access git LFS meta objects on commit %s: %v", commit.ID.String(), err) + return err + } + } + } + + fetchingMetaObjects := []*models.LFSMetaObject{} + for metaID := range fetchingMetaObjectsSet { + fetchingMetaObjects = append(fetchingMetaObjects, fetchingMetaObjectsSet[metaID]) + } + + // fetch LFS files from external server + err = lfsclient.FetchLFSFilesToContentStore(ctx, fetchingMetaObjects, userName, repo, lfsServer, contentStore) + if err != nil { + log.Error("Unable to fetch LFS files in %v/%v to content store. Error: %v", userName, repo.Name, err) + return err + } + + return nil +} + +// FindLFSMetaObjectsBelowMaxFileSizeWithMissingFiles finds lfs pointers in a repo and adds them to a passed fetchingMetaObjectsSet +func FindLFSMetaObjectsBelowMaxFileSizeWithMissingFiles(commit *git.Commit, userName string, repo *models.Repository, fetchingMetaObjectsSet *map[string]*models.LFSMetaObject, contentStore *models.ContentStore) error { + entries, err := commit.Tree.ListEntriesRecursive() + if err != nil { + log.Error("Failed to access git commit %s tree: %v", commit.ID.String(), err) + return err + } + + for _, entry := range entries { + buf, _ := entry.Blob().GetBlobFirstBytes(1024) + metaBasic := models.IsPointerFile(&buf) + if metaBasic == nil { + continue + } + + if setting.LFS.MaxFileSize > 0 && metaBasic.Size > setting.LFS.MaxFileSize { + log.Info("Denied LFS oid[%s] download of size %d to %s/%s because of LFS_MAX_FILE_SIZE=%d", metaBasic.Oid, metaBasic.Size, userName, repo.Name, setting.LFS.MaxFileSize) + continue + } + + meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Oid: metaBasic.Oid, Size: metaBasic.Size, RepositoryID: repo.ID}) + if err != nil { + log.Error("Unable to write LFS OID[%s] size %d meta object in %v/%v to database. Error: %v", meta.Oid, meta.Size, userName, repo.Name, err) + return err + } + + exist, err := contentStore.Exists(meta) + if err != nil { + log.Error("Unable to check if LFS OID[%s] exist on %s/%s. Error: %v", meta.Oid, userName, repo.Name, err) + return err + } + + if exist { + fileSizeValid, err := contentStore.Verify(meta) + if err != nil { + log.Error("Unable to verify LFS OID[%s] size on %s/%s. Error: %v", meta.Oid, userName, repo.Name, err) + return err + } + // remove file collision if exists and size not matching + if !fileSizeValid { + if err := contentStore.Delete(meta.RelativePath()); err != nil { + return fmt.Errorf("Error whilst deleting contentStore file by LFS oid %s: %v", meta.Oid, err) + } + (*fetchingMetaObjectsSet)[meta.Oid] = meta + } + // if exists and size matching, do not fetch + } else { + (*fetchingMetaObjectsSet)[meta.Oid] = meta + } + } + return nil +} + // cleanUpMigrateGitConfig removes mirror info which prevents "push --all". // This also removes possible user credentials. func cleanUpMigrateGitConfig(configPath string) error { diff --git a/modules/structs/repo.go b/modules/structs/repo.go index c47700cd00934..0ae3b4dba6ce6 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -257,6 +257,9 @@ type MigrateRepoOptions struct { AuthToken string `json:"auth_token"` Mirror bool `json:"mirror"` + LFS bool `json:"lfs"` + LFSServer string `json:"lfs_server"` + LFSFetchOlder bool `json:"lfs_fetch_older"` Private bool `json:"private"` Description string `json:"description" binding:"MaxSize(255)"` Wiki bool `json:"wiki"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 08b202d192f84..4918a0fe59822 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -393,6 +393,7 @@ user_not_exist = The user does not exist. team_not_exist = The team does not exist. last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization. cannot_add_org_to_team = An organization cannot be added as a team member. +lfs_server_not_valid = LFS Server not valid. invalid_ssh_key = Can not verify your SSH key: %s invalid_gpg_key = Can not verify your GPG key: %s @@ -775,7 +776,10 @@ need_auth = Clone Authorization migrate_options = Migration Options migrate_service = Migration Service migrate_options_mirror_helper = This repository will be a mirror +migrate_options_mirror_lfs = Mirror LFS data migrate_options_mirror_disabled = Your site administrator has disabled new mirrors. +migrate_options_lfs_server = LFS Server +migrate_options_lfs_fetch_older = Fetch LFS files of older commits migrate_items = Migration Items migrate_items_wiki = Wiki migrate_items_milestones = Milestones diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 61cd12b991cb1..6a068f69f7ba5 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -134,6 +134,9 @@ func Migrate(ctx *context.APIContext) { Description: form.Description, Private: form.Private || setting.Repository.ForcePrivate, Mirror: form.Mirror, + LFS: form.LFS, + LFSServer: form.LFSServer, + LFSFetchOlder: form.LFSFetchOlder, AuthUsername: form.AuthUsername, AuthPassword: form.AuthPassword, AuthToken: form.AuthToken, diff --git a/routers/repo/lfs.go b/routers/repo/lfs.go index fb0e3b10eae9a..32a3910af70fa 100644 --- a/routers/repo/lfs.go +++ b/routers/repo/lfs.go @@ -493,7 +493,7 @@ type pointerResult struct { func createPointerResultsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pointerChan chan<- pointerResult, repo *models.Repository, user *models.User) { defer wg.Done() defer catFileBatchReader.Close() - contentStore := lfs.ContentStore{ObjectStorage: storage.LFS} + contentStore := models.ContentStore{ObjectStorage: storage.LFS} bufferedReader := bufio.NewReader(catFileBatchReader) buf := make([]byte, 1025) @@ -526,7 +526,7 @@ func createPointerResultsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg } pointerBuf = pointerBuf[:size] // Now we need to check if the pointerBuf is an LFS pointer - pointer := lfs.IsPointerFile(&pointerBuf) + pointer := models.IsPointerFileAndStored(&pointerBuf) if pointer == nil { continue } diff --git a/routers/repo/migrate.go b/routers/repo/migrate.go index e1ff8e13606ab..77995b04e76f7 100644 --- a/routers/repo/migrate.go +++ b/routers/repo/migrate.go @@ -46,6 +46,8 @@ func Migrate(ctx *context.Context) { ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["mirror"] = ctx.Query("mirror") == "1" + ctx.Data["LFS"] = ctx.Query("lfs") == "1" + ctx.Data["LFSFetchOlder"] = ctx.Query("lfs_fetch_older") == "1" ctx.Data["wiki"] = ctx.Query("wiki") == "1" ctx.Data["milestones"] = ctx.Query("milestones") == "1" ctx.Data["labels"] = ctx.Query("labels") == "1" @@ -96,6 +98,9 @@ func handleMigrateError(ctx *context.Context, owner *models.User, err error, nam case models.IsErrNamePatternNotAllowed(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) + case models.IsMirrorLFSServerValid(err): + ctx.Data["Err_LFSServerNotValid"] = true + ctx.RenderWithErr(ctx.Tr("form.lfs_server_not_valid"), tpl, form) default: remoteAddr, _ := auth.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword, owner) err = util.URLSanitizedError(err, remoteAddr) @@ -167,6 +172,9 @@ func MigratePost(ctx *context.Context) { Description: form.Description, Private: form.Private || setting.Repository.ForcePrivate, Mirror: form.Mirror && !setting.Repository.DisableMirrors, + LFS: form.LFS, + LFSServer: form.LFSServer, + LFSFetchOlder: form.LFSFetchOlder, AuthUsername: form.AuthUsername, AuthPassword: form.AuthPassword, AuthToken: form.AuthToken, @@ -187,7 +195,7 @@ func MigratePost(ctx *context.Context) { opts.Releases = false } - err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, false) + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, opts.LFS, opts.LFSServer, false) if err != nil { handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form) return diff --git a/routers/repo/view.go b/routers/repo/view.go index 39f16d183c3b1..91b46d9c8fc79 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -273,7 +273,7 @@ func renderDirectory(ctx *context.Context, treeLink string) { // FIXME: what happens when README file is an image? if isTextFile && setting.LFS.StartServer { - meta := lfs.IsPointerFile(&buf) + meta := models.IsPointerFileAndStored(&buf) if meta != nil { meta, err = ctx.Repo.Repository.GetLFSMetaObjectByOid(meta.Oid) if err != nil && err != models.ErrLFSObjectNotExist { @@ -399,7 +399,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st //Check for LFS meta file if isTextFile && setting.LFS.StartServer { - meta := lfs.IsPointerFile(&buf) + meta := models.IsPointerFileAndStored(&buf) if meta != nil { meta, err = ctx.Repo.Repository.GetLFSMetaObjectByOid(meta.Oid) if err != nil && err != models.ErrLFSObjectNotExist { diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index e4981b8c00e64..7f3178be75081 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -206,7 +206,7 @@ func parseRemoteUpdateOutput(output string) []*mirrorSyncResult { } // runSync returns true if sync finished without error. -func runSync(m *models.Mirror) ([]*mirrorSyncResult, bool) { +func runSync(ctx context.Context, m *models.Mirror) ([]*mirrorSyncResult, bool) { repoPath := m.Repo.RepoPath() wikiPath := m.Repo.WikiPath() timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second @@ -253,13 +253,20 @@ func runSync(m *models.Mirror) ([]*mirrorSyncResult, bool) { log.Error("OpenRepository: %v", err) return nil, false } + defer gitRepo.Close() + + if m.Repo.LFS { + log.Trace("SyncMirrors [repo: %-v]: fetching LFS files...", m.Repo) + err := repo_module.FetchMissingLFSFilesToContentStore(ctx, m.Repo, Username(m), gitRepo, m.Repo.LFSServer, false) + if err != nil { + log.Error("Failed to fetch LFS files %v:\nErr: %v", m.Repo, err) + } + } log.Trace("SyncMirrors [repo: %-v]: syncing releases with tags...", m.Repo) if err = repo_module.SyncReleasesWithTags(m.Repo, gitRepo); err != nil { - gitRepo.Close() log.Error("Failed to synchronize tags to releases for repository: %v", err) } - gitRepo.Close() log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo) if err := m.Repo.UpdateSize(models.DefaultDBContext()); err != nil { @@ -378,12 +385,12 @@ func SyncMirrors(ctx context.Context) { mirrorQueue.Close() return case repoID := <-mirrorQueue.Queue(): - syncMirror(repoID) + syncMirror(ctx, repoID) } } } -func syncMirror(repoID string) { +func syncMirror(ctx context.Context, repoID string) { log.Trace("SyncMirrors [repo_id: %v]", repoID) defer func() { err := recover() @@ -403,7 +410,7 @@ func syncMirror(repoID string) { } log.Trace("SyncMirrors [repo: %-v]: Running Sync", m.Repo) - results, ok := runSync(m) + results, ok := runSync(ctx, m) if !ok { return } diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 57628aa68dec6..0fa8ca216e586 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -48,7 +48,8 @@ func TestRelease_MirrorDelete(t *testing.T) { }) assert.NoError(t, err) - mirror, err := repository.MigrateRepositoryGitData(context.Background(), user, mirrorRepo, opts) + ctx := context.Background() + mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts) assert.NoError(t, err) gitRepo, err := git.OpenRepository(repoPath) @@ -74,7 +75,7 @@ func TestRelease_MirrorDelete(t *testing.T) { err = mirror.GetMirror() assert.NoError(t, err) - _, ok := runSync(mirror.Mirror) + _, ok := runSync(ctx, mirror.Mirror) assert.True(t, ok) count, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions) @@ -85,7 +86,7 @@ func TestRelease_MirrorDelete(t *testing.T) { assert.NoError(t, err) assert.NoError(t, release_service.DeleteReleaseByID(release.ID, user, true)) - _, ok = runSync(mirror.Mirror) + _, ok = runSync(ctx, mirror.Mirror) assert.True(t, ok) count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions) diff --git a/services/pull/lfs.go b/services/pull/lfs.go index a1981b8253690..965510937e1f1 100644 --- a/services/pull/lfs.go +++ b/services/pull/lfs.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git/pipeline" - "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" ) @@ -101,7 +100,7 @@ func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg } pointerBuf = pointerBuf[:size] // Now we need to check if the pointerBuf is an LFS pointer - pointer := lfs.IsPointerFile(&pointerBuf) + pointer := models.IsPointerFileAndStored(&pointerBuf) if pointer == nil { continue } diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl index 233a019435308..fb1c1c78d03fa 100644 --- a/templates/repo/migrate/git.tmpl +++ b/templates/repo/migrate/git.tmpl @@ -15,7 +15,6 @@ {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} - {{if .LFSActive}}
{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}}
@@ -30,14 +29,35 @@
+ {{if .DisableMirrors}}
- {{if .DisableMirrors}} - {{else}} - +
+ {{else}} +
+ - {{end}} +
+
+ + +
+ {{end}} +
+ +
+
+ + +
+ +
+ +
+ + +
diff --git a/templates/repo/migrate/gitea.tmpl b/templates/repo/migrate/gitea.tmpl index b21e6b18ffdfc..adc3d2818e81b 100644 --- a/templates/repo/migrate/gitea.tmpl +++ b/templates/repo/migrate/gitea.tmpl @@ -15,7 +15,6 @@ {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} - {{if .LFSActive}}
{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}}
@@ -27,14 +26,35 @@
+ {{if .DisableMirrors}}
- {{if .DisableMirrors}} - {{else}} +
+ {{else}} +
- {{end}} +
+
+ + +
+ {{end}} +
+ +
+
+ + +
+ +
+ +
+ + +
diff --git a/templates/repo/migrate/github.tmpl b/templates/repo/migrate/github.tmpl index 06f76d72980e6..47c545e7c491b 100644 --- a/templates/repo/migrate/github.tmpl +++ b/templates/repo/migrate/github.tmpl @@ -15,7 +15,6 @@ {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} - {{if .LFSActive}}
{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}}
@@ -27,14 +26,35 @@
+ {{if .DisableMirrors}}
- {{if .DisableMirrors}} - {{else}} - +
+ {{else}} +
+ - {{end}} +
+
+ + +
+ {{end}} +
+ +
+
+ + +
+ +
+ +
+ + +
diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl index 545a1ff43717f..fd07dd2d31e4f 100644 --- a/templates/repo/migrate/gitlab.tmpl +++ b/templates/repo/migrate/gitlab.tmpl @@ -15,7 +15,6 @@ {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} - {{if .LFSActive}}
{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}}
@@ -27,14 +26,35 @@
+ {{if .DisableMirrors}}
- {{if .DisableMirrors}} - {{else}} - +
+ {{else}} +
+ - {{end}} +
+
+ + +
+ {{end}} +
+ +
+
+ + +
+ +
+ +
+ + +
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 930af907ea8bd..daf9060204339 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -14664,6 +14664,18 @@ "type": "boolean", "x-go-name": "Labels" }, + "lfs": { + "type": "boolean", + "x-go-name": "LFS" + }, + "lfs_fetch_older": { + "type": "boolean", + "x-go-name": "LFSFetchOlder" + }, + "lfs_server": { + "type": "string", + "x-go-name": "LFSServer" + }, "milestones": { "type": "boolean", "x-go-name": "Milestones" @@ -14743,6 +14755,18 @@ "type": "boolean", "x-go-name": "Labels" }, + "lfs": { + "type": "boolean", + "x-go-name": "LFS" + }, + "lfs_fetch_older": { + "type": "boolean", + "x-go-name": "LFSFetchOlder" + }, + "lfs_server": { + "type": "string", + "x-go-name": "LFSServer" + }, "milestones": { "type": "boolean", "x-go-name": "Milestones" diff --git a/web_src/js/features/migration.js b/web_src/js/features/migration.js index 09ab49b3e1e48..7f3c2f5709f2c 100644 --- a/web_src/js/features/migration.js +++ b/web_src/js/features/migration.js @@ -4,14 +4,19 @@ const $pass = $('#auth_password'); const $token = $('#auth_token'); const $mirror = $('#mirror'); const $items = $('#migrate_items').find('input[type=checkbox]'); +const $lfs = $('#lfs'); +const $lfsserveritems = $('#lfs_server_items'); +const $lfsserver = $('#lfs_server'); export default function initMigration() { checkAuth(); + checkLFSInputs(); $user.on('keyup', () => {checkItems(false)}); $pass.on('keyup', () => {checkItems(false)}); $token.on('keyup', () => {checkItems(true)}); $mirror.on('change', () => {checkItems(true)}); + $lfs.on('change', () => {checkLFSInputs()}); const $cloneAddr = $('#clone_addr'); $cloneAddr.on('change', () => { @@ -20,6 +25,12 @@ export default function initMigration() { $repoName.val($cloneAddr.val().match(/^(.*\/)?((.+?)(\.git)?)$/)[3]); } }); + + $cloneAddr.on('keyup', () => { + if ($cloneAddr.val().length > 0) { + $lfsserver.val(`${$cloneAddr.val()}/info/lfs`); + } + }); } function checkAuth() { @@ -39,10 +50,18 @@ function checkItems(tokenAuth) { if ($mirror.is(':checked')) { $items.not('[name="wiki"]').attr('disabled', true); $items.filter('[name="wiki"]').attr('disabled', false); - return; + } else { + $items.attr('disabled', false); } - $items.attr('disabled', false); } else { $items.attr('disabled', true); } } + +function checkLFSInputs() { + if ($lfs.is(':checked')) { + $lfsserveritems.css({'display': 'block'}); + } else { + $lfsserveritems.css({'display': 'none'}); + } +}