From 8c1d77847ba0d48a53faa064cdf1b3fc9d5f4185 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 7 Jun 2026 15:47:01 -0700 Subject: [PATCH 1/4] Fix the panic when ssh rmote lfs endpoint parsing failure --- modules/lfs/client.go | 14 ++++++++++++++ modules/lfs/client_test.go | 12 ++++++++++++ modules/lfs/endpoint.go | 14 +++++++++++++- modules/lfs/endpoint_test.go | 18 ++++++++++++++++++ services/mirror/mirror_pull.go | 7 ++++--- services/mirror/mirror_push.go | 6 ++++-- services/repository/migrate.go | 6 ++++-- 7 files changed, 69 insertions(+), 8 deletions(-) diff --git a/modules/lfs/client.go b/modules/lfs/client.go index f810e5c7aa..c353ecdad2 100644 --- a/modules/lfs/client.go +++ b/modules/lfs/client.go @@ -5,6 +5,7 @@ package lfs import ( "context" + "fmt" "io" "net/http" "net/url" @@ -30,3 +31,16 @@ func NewClient(endpoint *url.URL, httpTransport *http.Transport) Client { } return newHTTPClient(endpoint, httpTransport) } + +// NewClientFromEndpoint creates a LFS client after resolving its endpoint. +func NewClientFromEndpoint(cloneurl, lfsurl string, httpTransport *http.Transport) (Client, error) { + endpoint := DetermineEndpoint(cloneurl, lfsurl) + if endpoint == nil { + source := cloneurl + if lfsurl != "" { + source = lfsurl + } + return nil, fmt.Errorf("unable to determine LFS endpoint from %q", source) + } + return NewClient(endpoint, httpTransport), nil +} diff --git a/modules/lfs/client_test.go b/modules/lfs/client_test.go index a1369301e0..7e7d3883d7 100644 --- a/modules/lfs/client_test.go +++ b/modules/lfs/client_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewClient(t *testing.T) { @@ -19,3 +20,14 @@ func TestNewClient(t *testing.T) { c = NewClient(u, nil) assert.IsType(t, &HTTPClient{}, c) } + +func TestNewClientFromEndpoint(t *testing.T) { + client, err := NewClientFromEndpoint("ssh://git@example.com/owner/repo.git", "", nil) + require.NoError(t, err) + assert.NotNil(t, client) + + client, err = NewClientFromEndpoint("ftp://example.com/owner/repo.git", "", nil) + assert.Nil(t, client) + require.Error(t, err) + assert.Contains(t, err.Error(), "unable to determine LFS endpoint") +} diff --git a/modules/lfs/endpoint.go b/modules/lfs/endpoint.go index a7999fc1c4..83363f3b3b 100644 --- a/modules/lfs/endpoint.go +++ b/modules/lfs/endpoint.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + giturl "gitea.dev/modules/git/url" "gitea.dev/modules/log" "gitea.dev/modules/util" ) @@ -44,15 +45,20 @@ func endpointFromCloneURL(rawurl string) *url.URL { } func endpointFromURL(rawurl string) *url.URL { + if rawurl == "" { + return nil + } + if strings.HasPrefix(rawurl, "/") { return endpointFromLocalPath(rawurl) } - u, err := url.Parse(rawurl) + gitURL, err := giturl.ParseGitURL(rawurl) if err != nil { log.Error("lfs.endpointFromUrl: %v", err) return nil } + u := gitURL.URL switch u.Scheme { case "http", "https": @@ -60,6 +66,12 @@ func endpointFromURL(rawurl string) *url.URL { case "git": u.Scheme = "https" return u + case "ssh", "git+ssh": + u.Scheme = "https" + u.Host = util.IfZero(u.Hostname(), u.Host) + u.Path = "/" + strings.TrimPrefix(u.Path, "/") + u.User = nil + return u case "file": return u default: diff --git a/modules/lfs/endpoint_test.go b/modules/lfs/endpoint_test.go index 118abe2d4e..b1ed364c01 100644 --- a/modules/lfs/endpoint_test.go +++ b/modules/lfs/endpoint_test.go @@ -64,6 +64,24 @@ func TestDetermineEndpoint(t *testing.T) { lfsurl: "git://gitlfs.com/repo", expected: str2url("https://gitlfs.com/repo"), }, + // case 7 + { + cloneurl: "ssh://git@git.com/owner/repo.git", + lfsurl: "", + expected: str2url("https://git.com/owner/repo.git/info/lfs"), + }, + // case 8 + { + cloneurl: "git@git.com:owner/repo.git", + lfsurl: "", + expected: str2url("https://git.com/owner/repo.git/info/lfs"), + }, + // case 9 + { + cloneurl: "", + lfsurl: "ssh://git@gitlfs.com/owner/repo.git/info/lfs", + expected: str2url("https://gitlfs.com/owner/repo.git/info/lfs"), + }, } for n, c := range cases { diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index db569c8550..7037b9b921 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -172,9 +172,10 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu if m.LFS && setting.LFS.StartServer { log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) - endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport()) - if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { + lfsClient, err := lfs.NewClientFromEndpoint(remoteURL.String(), m.LFSEndpoint, migrations.NewMigrationHTTPTransport()) + if err != nil { + log.Error("SyncMirrors [repo: %-v]: failed to initialize LFS client: %v", m.Repo.FullName(), err) + } else if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo.FullName(), err) } } diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 44a25825ee..39d8a725ed 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -144,8 +144,10 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { } defer gitRepo.Close() - endpoint := lfs.DetermineEndpoint(remoteURL.String(), "") - lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport()) + lfsClient, err := lfs.NewClientFromEndpoint(remoteURL.String(), "", migrations.NewMigrationHTTPTransport()) + if err != nil { + return util.SanitizeErrorCredentialURLs(err) + } if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { return util.SanitizeErrorCredentialURLs(err) } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index a0182769f4..c328d070c8 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -159,8 +159,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } if opts.LFS { - endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, httpTransport) + lfsClient, err := lfs.NewClientFromEndpoint(opts.CloneAddr, opts.LFSEndpoint, httpTransport) + if err != nil { + return repo, fmt.Errorf("NewClientFromEndpoint: %w", err) + } if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil { log.Error("Failed to store missing LFS objects for repository: %v", err) return repo, fmt.Errorf("StoreMissingLfsObjectsInRepository: %w", err) From 091ed6b756f2b6d5cec70185d8aba33f44a8e2f2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 7 Jun 2026 15:50:48 -0700 Subject: [PATCH 2/4] update --- modules/lfs/client_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/lfs/client_test.go b/modules/lfs/client_test.go index 7e7d3883d7..cfdcd84aeb 100644 --- a/modules/lfs/client_test.go +++ b/modules/lfs/client_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestNewClient(t *testing.T) { @@ -23,11 +22,11 @@ func TestNewClient(t *testing.T) { func TestNewClientFromEndpoint(t *testing.T) { client, err := NewClientFromEndpoint("ssh://git@example.com/owner/repo.git", "", nil) - require.NoError(t, err) + assert.NoError(t, err) assert.NotNil(t, client) client, err = NewClientFromEndpoint("ftp://example.com/owner/repo.git", "", nil) assert.Nil(t, client) - require.Error(t, err) + assert.Error(t, err) assert.Contains(t, err.Error(), "unable to determine LFS endpoint") } From 072561159d876c57e0bdffd655611753089b73e4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 8 Jun 2026 18:41:37 -0700 Subject: [PATCH 3/4] fix --- modules/lfs/client.go | 10 ++++++---- modules/lfs/client_test.go | 4 ++-- services/mirror/mirror_push.go | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/lfs/client.go b/modules/lfs/client.go index c353ecdad2..b1fba18e81 100644 --- a/modules/lfs/client.go +++ b/modules/lfs/client.go @@ -9,6 +9,8 @@ import ( "io" "net/http" "net/url" + + "gitea.dev/modules/util" ) // DownloadCallback gets called for every requested LFS object to process its content @@ -24,8 +26,8 @@ type Client interface { Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error } -// NewClient creates a LFS client -func NewClient(endpoint *url.URL, httpTransport *http.Transport) Client { +// newClient creates a LFS client +func newClient(endpoint *url.URL, httpTransport *http.Transport) Client { if endpoint.Scheme == "file" { return newFilesystemClient(endpoint) } @@ -40,7 +42,7 @@ func NewClientFromEndpoint(cloneurl, lfsurl string, httpTransport *http.Transpor if lfsurl != "" { source = lfsurl } - return nil, fmt.Errorf("unable to determine LFS endpoint from %q", source) + return nil, fmt.Errorf("unable to determine LFS endpoint from %q", util.SanitizeCredentialURLs(source)) } - return NewClient(endpoint, httpTransport), nil + return newClient(endpoint, httpTransport), nil } diff --git a/modules/lfs/client_test.go b/modules/lfs/client_test.go index cfdcd84aeb..db6403e07a 100644 --- a/modules/lfs/client_test.go +++ b/modules/lfs/client_test.go @@ -12,11 +12,11 @@ import ( func TestNewClient(t *testing.T) { u, _ := url.Parse("file:///test") - c := NewClient(u, nil) + c := newClient(u, nil) assert.IsType(t, &FilesystemClient{}, c) u, _ = url.Parse("https://test.com/lfs") - c = NewClient(u, nil) + c = newClient(u, nil) assert.IsType(t, &HTTPClient{}, c) } diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 39d8a725ed..5df0c8a066 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -146,7 +146,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { lfsClient, err := lfs.NewClientFromEndpoint(remoteURL.String(), "", migrations.NewMigrationHTTPTransport()) if err != nil { - return util.SanitizeErrorCredentialURLs(err) + return err } if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { return util.SanitizeErrorCredentialURLs(err) From afdb051961ed4c19cdfa93478d55533f29c40260 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 9 Jun 2026 23:14:39 -0700 Subject: [PATCH 4/4] fix --- modules/lfs/endpoint.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/lfs/endpoint.go b/modules/lfs/endpoint.go index 83363f3b3b..bf847c8434 100644 --- a/modules/lfs/endpoint.go +++ b/modules/lfs/endpoint.go @@ -67,8 +67,8 @@ func endpointFromURL(rawurl string) *url.URL { u.Scheme = "https" return u case "ssh", "git+ssh": - u.Scheme = "https" - u.Host = util.IfZero(u.Hostname(), u.Host) + u.Scheme = "https" // is it possible http? + u.Host = u.Hostname() // remove ssh port if any u.Path = "/" + strings.TrimPrefix(u.Path, "/") u.User = nil return u