Skip to content

Commit

Permalink
Add LFS Migration and Mirror (#14726)
Browse files Browse the repository at this point in the history
* Implemented LFS client.

* Implemented scanning for pointer files.

* Implemented downloading of lfs files.

* Moved model-dependent code into services.

* Removed models dependency. Added TryReadPointerFromBuffer.

* Migrated code from service to module.

* Centralised storage creation.

* Removed dependency from models.

* Moved ContentStore into modules.

* Share structs between server and client.

* Moved method to services.

* Implemented lfs download on clone.

* Implemented LFS sync on clone and mirror update.

* Added form fields.

* Updated templates.

* Fixed condition.

* Use alternate endpoint.

* Added missing methods.

* Fixed typo and make linter happy.

* Detached pointer parser from gogit dependency.

* Fixed TestGetLFSRange test.

* Added context to support cancellation.

* Use ReadFull to probably read more data.

* Removed duplicated code from models.

* Moved scan implementation into pointer_scanner_nogogit.

* Changed method name.

* Added comments.

* Added more/specific log/error messages.

* Embedded lfs.Pointer into models.LFSMetaObject.

* Moved code from models to module.

* Moved code from models to module.

* Moved code from models to module.

* Reduced pointer usage.

* Embedded type.

* Use promoted fields.

* Fixed unexpected eof.

* Added unit tests.

* Implemented migration of local file paths.

* Show an error on invalid LFS endpoints.

* Hide settings if not used.

* Added LFS info to mirror struct.

* Fixed comment.

* Check LFS endpoint.

* Manage LFS settings from mirror page.

* Fixed selector.

* Adjusted selector.

* Added more tests.

* Added local filesystem migration test.

* Fixed typo.

* Reset settings.

* Added special windows path handling.

* Added unit test for HTTPClient.

* Added unit test for BasicTransferAdapter.

* Moved into util package.

* Test if LFS endpoint is allowed.

* Added support for git://

* Just use a static placeholder as the displayed url may be invalid.

* Reverted to original code.

* Added "Advanced Settings".

* Updated wording.

* Added discovery info link.

* Implemented suggestion.

* Fixed missing format parameter.

* Added Pointer.IsValid().

* Always remove model on error.

* Added suggestions.

* Use channel instead of array.

* Update routers/repo/migrate.go

* fmt

Signed-off-by: Andrew Thornton <[email protected]>

Co-authored-by: zeripath <[email protected]>
  • Loading branch information
KN4CK3R and zeripath authored Apr 8, 2021
1 parent f544414 commit c03e488
Show file tree
Hide file tree
Showing 75 changed files with 2,158 additions and 710 deletions.
2 changes: 1 addition & 1 deletion cmd/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import (
"time"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/pprof"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/lfs"

"github.com/dgrijalva/jwt-go"
jsoniter "github.com/json-iterator/go"
Expand Down
49 changes: 49 additions & 0 deletions integrations/api_repo_lfs_migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package integrations

import (
"net/http"
"path"
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)

func TestAPIRepoLFSMigrateLocal(t *testing.T) {
defer prepareTestEnv(t)()

oldImportLocalPaths := setting.ImportLocalPaths
oldAllowLocalNetworks := setting.Migrations.AllowLocalNetworks
setting.ImportLocalPaths = true
setting.Migrations.AllowLocalNetworks = true

user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session)

req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate?token="+token, &api.MigrateRepoOptions{
CloneAddr: path.Join(setting.RepoRootPath, "migration/lfs-test.git"),
RepoOwnerID: user.ID,
RepoName: "lfs-test-local",
LFS: true,
})
resp := MakeRequest(t, req, NoExpectedStatus)
assert.EqualValues(t, http.StatusCreated, resp.Code)

store := lfs.NewContentStore()
ok, _ := store.Verify(lfs.Pointer{Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041", Size: 6})
assert.True(t, ok)
ok, _ = store.Verify(lfs.Pointer{Oid: "d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0152", Size: 6})
assert.True(t, ok)

setting.ImportLocalPaths = oldImportLocalPaths
setting.Migrations.AllowLocalNetworks = oldAllowLocalNetworks
}
5 changes: 3 additions & 2 deletions integrations/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -218,7 +219,7 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
assert.NotEqual(t, littleSize, resp.Body.Len())
assert.LessOrEqual(t, resp.Body.Len(), 1024)
if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 {
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
}
}

Expand All @@ -232,7 +233,7 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
resp := session.MakeRequest(t, req, http.StatusOK)
assert.NotEqual(t, bigSize, resp.Body.Len())
if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 {
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ref: refs/heads/master
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[core]
bare = false
repositoryformatversion = 0
filemode = false
symlinks = false
ignorecase = true
logallrefupdates = true
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout.\n"; exit 2; }
git lfs post-checkout "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-commit.\n"; exit 2; }
git lfs post-commit "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-merge.\n"; exit 2; }
git lfs post-merge "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.\n"; exit 2; }
git lfs pre-push "$@"
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dummy2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dummy1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
546244003622c64b2fc3c2cd544d7a29882c8383
27 changes: 7 additions & 20 deletions integrations/lfs_getobject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ package integrations
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand All @@ -18,46 +15,36 @@ import (
"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"

gzipp "github.com/klauspost/compress/gzip"
"github.com/stretchr/testify/assert"
)

func GenerateLFSOid(content io.Reader) (string, error) {
h := sha256.New()
if _, err := io.Copy(h, content); err != nil {
return "", err
}
sum := h.Sum(nil)
return hex.EncodeToString(sum), nil
}

var lfsID = int64(20000)

func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string {
oid, err := GenerateLFSOid(bytes.NewReader(*content))
pointer, err := lfs.GeneratePointer(bytes.NewReader(*content))
assert.NoError(t, err)
var lfsMetaObject *models.LFSMetaObject

if setting.Database.UsePostgreSQL {
lfsMetaObject = &models.LFSMetaObject{ID: lfsID, Oid: oid, Size: int64(len(*content)), RepositoryID: repositoryID}
lfsMetaObject = &models.LFSMetaObject{ID: lfsID, Pointer: pointer, RepositoryID: repositoryID}
} else {
lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(*content)), RepositoryID: repositoryID}
lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repositoryID}
}

lfsID++
lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
assert.NoError(t, err)
contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS}
exist, err := contentStore.Exists(lfsMetaObject)
contentStore := lfs.NewContentStore()
exist, err := contentStore.Exists(pointer)
assert.NoError(t, err)
if !exist {
err := contentStore.Put(lfsMetaObject, bytes.NewReader(*content))
err := contentStore.Put(pointer, bytes.NewReader(*content))
assert.NoError(t, err)
}
return oid
return pointer.Oid
}

func storeAndGetLfs(t *testing.T, content *[]byte, extraHeader *http.Header, expectedStatus int) *httptest.ResponseRecorder {
Expand Down
117 changes: 117 additions & 0 deletions integrations/lfs_local_endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package integrations

import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"testing"

"code.gitea.io/gitea/modules/lfs"

"github.com/stretchr/testify/assert"
)

func str2url(raw string) *url.URL {
u, _ := url.Parse(raw)
return u
}

func TestDetermineLocalEndpoint(t *testing.T) {
defer prepareTestEnv(t)()

root, _ := ioutil.TempDir("", "lfs_test")
defer os.RemoveAll(root)

rootdotgit, _ := ioutil.TempDir("", "lfs_test")
defer os.RemoveAll(rootdotgit)
os.Mkdir(filepath.Join(rootdotgit, ".git"), 0700)

lfsroot, _ := ioutil.TempDir("", "lfs_test")
defer os.RemoveAll(lfsroot)

// Test cases
var cases = []struct {
cloneurl string
lfsurl string
expected *url.URL
}{
// case 0
{
cloneurl: root,
lfsurl: "",
expected: str2url(fmt.Sprintf("file://%s", root)),
},
// case 1
{
cloneurl: root,
lfsurl: lfsroot,
expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
},
// case 2
{
cloneurl: "https://git.com/repo.git",
lfsurl: lfsroot,
expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
},
// case 3
{
cloneurl: rootdotgit,
lfsurl: "",
expected: str2url(fmt.Sprintf("file://%s", filepath.Join(rootdotgit, ".git"))),
},
// case 4
{
cloneurl: "",
lfsurl: rootdotgit,
expected: str2url(fmt.Sprintf("file://%s", filepath.Join(rootdotgit, ".git"))),
},
// case 5
{
cloneurl: rootdotgit,
lfsurl: rootdotgit,
expected: str2url(fmt.Sprintf("file://%s", filepath.Join(rootdotgit, ".git"))),
},
// case 6
{
cloneurl: fmt.Sprintf("file://%s", root),
lfsurl: "",
expected: str2url(fmt.Sprintf("file://%s", root)),
},
// case 7
{
cloneurl: fmt.Sprintf("file://%s", root),
lfsurl: fmt.Sprintf("file://%s", lfsroot),
expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
},
// case 8
{
cloneurl: root,
lfsurl: fmt.Sprintf("file://%s", lfsroot),
expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
},
// case 9
{
cloneurl: "",
lfsurl: "/does/not/exist",
expected: nil,
},
// case 10
{
cloneurl: "",
lfsurl: "file:///does/not/exist",
expected: str2url("file:///does/not/exist"),
},
}

for n, c := range cases {
ep := lfs.DetermineEndpoint(c.cloneurl, c.lfsurl)

assert.Equal(t, c.expected, ep, "case %d: error should match", n)
}
}
Loading

0 comments on commit c03e488

Please sign in to comment.