From 9a88d1f9f733b4cba0e246bf3ad11452913d359d Mon Sep 17 00:00:00 2001 From: pomidorry Date: Tue, 9 Jun 2026 00:12:31 +0300 Subject: [PATCH] ssh clone from all except CodeCommit --- services/migrations/codebase.go | 17 ++--- services/migrations/codecommit.go | 13 ++-- services/migrations/common.go | 31 ++++++++ services/migrations/common_test.go | 98 +++++++++++++++++++++++++ services/migrations/gitbucket.go | 22 +++--- services/migrations/gitea_downloader.go | 12 +-- services/migrations/github.go | 12 +-- services/migrations/gitlab.go | 7 +- services/migrations/gogs.go | 15 ++-- services/migrations/onedev.go | 11 +-- templates/repo/migrate/codebase.tmpl | 5 +- templates/repo/migrate/git.tmpl | 21 +----- templates/repo/migrate/gitbucket.tmpl | 5 +- templates/repo/migrate/gitea.tmpl | 1 + templates/repo/migrate/github.tmpl | 1 + templates/repo/migrate/gitlab.tmpl | 1 + templates/repo/migrate/gogs.tmpl | 1 + templates/repo/migrate/onedev.tmpl | 5 +- templates/repo/migrate/ssh_options.tmpl | 19 +++++ web_src/js/features/repo-migrate.ts | 37 +++++----- 20 files changed, 229 insertions(+), 105 deletions(-) create mode 100644 services/migrations/common_test.go create mode 100644 templates/repo/migrate/ssh_options.tmpl diff --git a/services/migrations/codebase.go b/services/migrations/codebase.go index 97c6cfe7aa..e85e64bb06 100644 --- a/services/migrations/codebase.go +++ b/services/migrations/codebase.go @@ -33,22 +33,19 @@ type CodebaseDownloaderFactory struct{} // New returns a downloader related to this factory according MigrateOptions func (f *CodebaseDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } - u.User = nil - - fields := strings.Split(strings.Trim(u.Path, "/"), "/") - if len(fields) != 2 { - return nil, fmt.Errorf("invalid path: %s", u.Path) + if len(info.segments) != 2 { + return nil, fmt.Errorf("invalid path: %s", info.repoPath) } - project := fields[0] - repoName := strings.TrimSuffix(fields[1], ".git") + project := info.segments[0] + repoName := strings.TrimSuffix(info.segments[1], ".git") - log.Trace("Create Codebase downloader. BaseURL: %v RepoName: %s", u, repoName) + log.Trace("Create Codebase downloader. BaseURL: %v RepoName: %s", info.apiURL, repoName) - return NewCodebaseDownloader(ctx, u, project, repoName, opts.AuthUsername, opts.AuthPassword), nil + return NewCodebaseDownloader(ctx, info.apiURL, project, repoName, opts.AuthUsername, opts.AuthPassword), nil } // GitServiceType returns the type of git service diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go index 2c2019470a..05745ae5a2 100644 --- a/services/migrations/codecommit.go +++ b/services/migrations/codecommit.go @@ -34,26 +34,23 @@ type CodeCommitDownloaderFactory struct{} // New returns a Downloader related to this factory according MigrateOptions func (c *CodeCommitDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } - hostElems := strings.Split(u.Host, ".") + hostElems := strings.Split(info.apiURL.Host, ".") if len(hostElems) != 4 { return nil, errors.New("cannot get the region from clone URL") } region := hostElems[1] - pathElems := strings.Split(u.Path, "/") - if len(pathElems) == 0 { + if len(info.segments) == 0 || info.segments[len(info.segments)-1] == "" { return nil, errors.New("cannot get the repo name from clone URL") } - repoName := pathElems[len(pathElems)-1] + repoName := info.segments[len(info.segments)-1] - baseURL := u.Scheme + "://" + u.Host - - return NewCodeCommitDownloader(ctx, repoName, baseURL, opts.AWSAccessKeyID, opts.AWSSecretAccessKey, region), nil + return NewCodeCommitDownloader(ctx, repoName, info.apiURL.String(), opts.AWSAccessKeyID, opts.AWSSecretAccessKey, region), nil } // GitServiceType returns the type of git service diff --git a/services/migrations/common.go b/services/migrations/common.go index e9895d00c2..fce6567a34 100644 --- a/services/migrations/common.go +++ b/services/migrations/common.go @@ -5,6 +5,7 @@ package migrations import ( "fmt" + "net/url" "strings" system_model "gitea.dev/models/system" @@ -13,6 +14,36 @@ import ( base "gitea.dev/modules/migration" ) +// serviceCloneURLInfo bundles the API base URL and parsed repo path of a clone +// address, hiding scheme conversion (ssh→https) needed for forge API calls. +type serviceCloneURLInfo struct { + apiURL *url.URL + repoPath string + segments []string +} + +// parseServiceCloneURL parses a clone address and returns its API base URL +// (always http/https — ssh/git are promoted to https for API calls) together +// with the repo path and its segments. +func parseServiceCloneURL(cloneAddr string) (*serviceCloneURLInfo, error) { + u, err := url.Parse(cloneAddr) + if err != nil { + return nil, err + } + apiURL := *u + apiURL.User = nil + apiURL.Path = "" + apiURL.RawQuery = "" + apiURL.Fragment = "" + // Forge APIs are HTTP(S) only; promote ssh/git clone schemes to https. + if apiURL.Scheme == "ssh" || apiURL.Scheme == "git" { + apiURL.Scheme = "https" + } + repoPath := strings.TrimPrefix(u.Path, "/") + segments := strings.Split(repoPath, "/") + return &serviceCloneURLInfo{apiURL: &apiURL, repoPath: repoPath, segments: segments}, nil +} + // WarnAndNotice will log the provided message and send a repository notice func WarnAndNotice(fmtStr string, args ...any) { log.Warn(fmtStr, args...) diff --git a/services/migrations/common_test.go b/services/migrations/common_test.go new file mode 100644 index 0000000000..1fdfc861b5 --- /dev/null +++ b/services/migrations/common_test.go @@ -0,0 +1,98 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migrations + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseServiceCloneURL(t *testing.T) { + tests := []struct { + name string + cloneURL string + apiURL string + repoPath string + pathSegment []string + }{ + { + name: "https", + cloneURL: "https://github.com/go-gitea/gitea.git", + apiURL: "https://github.com", + repoPath: "go-gitea/gitea.git", + pathSegment: []string{"go-gitea", "gitea.git"}, + }, + { + name: "ssh", + cloneURL: "ssh://github.com/go-gitea/gitea.git", + apiURL: "https://github.com", + repoPath: "go-gitea/gitea.git", + pathSegment: []string{"go-gitea", "gitea.git"}, + }, + { + name: "git protocol", + cloneURL: "git://github.com/go-gitea/gitea.git", + apiURL: "https://github.com", + repoPath: "go-gitea/gitea.git", + pathSegment: []string{"go-gitea", "gitea.git"}, + }, + { + name: "sub-path instance", + cloneURL: "ssh://example.com/gitbucket/team/repo.git", + apiURL: "https://example.com", + repoPath: "gitbucket/team/repo.git", + pathSegment: []string{"gitbucket", "team", "repo.git"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info, err := parseServiceCloneURL(tt.cloneURL) + require.NoError(t, err) + assert.Equal(t, tt.apiURL, info.apiURL.String()) + assert.Equal(t, tt.repoPath, info.repoPath) + assert.Equal(t, tt.pathSegment, info.segments) + }) + } +} + +func TestGithubDownloaderFactoryNewWithSSHCloneURL(t *testing.T) { + downloader, err := (&GithubDownloaderV3Factory{}).New(t.Context(), MigrateOptions{ + CloneAddr: "ssh://github.com/go-gitea/gitea.git", + }) + require.NoError(t, err) + + githubDownloader, ok := downloader.(*GithubDownloaderV3) + require.True(t, ok) + assert.Equal(t, "https://github.com", githubDownloader.baseURL) + assert.Equal(t, "go-gitea", githubDownloader.repoOwner) + assert.Equal(t, "gitea", githubDownloader.repoName) +} + +func TestGitBucketDownloaderFactoryNewWithSSHCloneURL(t *testing.T) { + downloader, err := (&GitBucketDownloaderFactory{}).New(t.Context(), MigrateOptions{ + CloneAddr: "ssh://example.com/git/team/repo.git", + }) + require.NoError(t, err) + + gitBucketDownloader, ok := downloader.(*GitBucketDownloader) + require.True(t, ok) + assert.Equal(t, "https://example.com", gitBucketDownloader.baseURL) + assert.Equal(t, "team", gitBucketDownloader.repoOwner) + assert.Equal(t, "repo", gitBucketDownloader.repoName) +} + +func TestCodeCommitDownloaderFactoryNewWithSSHCloneURL(t *testing.T) { + downloader, err := (&CodeCommitDownloaderFactory{}).New(t.Context(), MigrateOptions{ + CloneAddr: "ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/test-repo", + }) + require.NoError(t, err) + + codeCommitDownloader, ok := downloader.(*CodeCommitDownloader) + require.True(t, ok) + assert.Equal(t, "https://git-codecommit.us-east-1.amazonaws.com", codeCommitDownloader.baseURL) + assert.Equal(t, "test-repo", codeCommitDownloader.repoName) +} diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go index 026fa3c12b..69f5f7ead8 100644 --- a/services/migrations/gitbucket.go +++ b/services/migrations/gitbucket.go @@ -6,7 +6,6 @@ package migrations import ( "context" "fmt" - "net/url" "strings" "gitea.dev/modules/log" @@ -28,19 +27,24 @@ type GitBucketDownloaderFactory struct{} // New returns a Downloader related to this factory according MigrateOptions func (f *GitBucketDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } - - fields := strings.Split(u.Path, "/") - if len(fields) < 2 { - return nil, fmt.Errorf("invalid path: %s", u.Path) + if len(info.segments) < 2 { + return nil, fmt.Errorf("invalid path: %s", info.repoPath) } - baseURL := u.Scheme + "://" + u.Host + strings.TrimSuffix(strings.Join(fields[:len(fields)-2], "/"), "/git") - oldOwner := fields[len(fields)-2] - oldName := strings.TrimSuffix(fields[len(fields)-1], ".git") + // GitBucket exposes its API at "/" where is the URL + // minus the trailing "/git//.git" used for the git clone endpoint. + subPath := strings.Join(info.segments[:len(info.segments)-2], "/") + if subPath != "" { + subPath = "/" + subPath + } + baseURL := info.apiURL.String() + strings.TrimSuffix(subPath, "/git") + + oldOwner := info.segments[len(info.segments)-2] + oldName := strings.TrimSuffix(info.segments[len(info.segments)-1], ".git") log.Trace("Create GitBucket downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, oldOwner, oldName) return NewGitBucketDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName) diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go index c754d70090..2fbadff3c6 100644 --- a/services/migrations/gitea_downloader.go +++ b/services/migrations/gitea_downloader.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "net/http" - "net/url" "strings" "time" @@ -33,24 +32,21 @@ type GiteaDownloaderFactory struct{} // New returns a Downloader related to this factory according MigrateOptions func (f *GiteaDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } - baseURL := u.Scheme + "://" + u.Host - repoNameSpace := strings.TrimPrefix(u.Path, "/") - repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git") - + repoNameSpace := strings.TrimSuffix(info.repoPath, ".git") path := strings.Split(repoNameSpace, "/") if len(path) < 2 { return nil, fmt.Errorf("invalid path: %s", repoNameSpace) } + baseURL := info.apiURL.String() repoPath := strings.Join(path[len(path)-2:], "/") if len(path) > 2 { - subPath := strings.Join(path[:len(path)-2], "/") - baseURL += "/" + subPath + baseURL += "/" + strings.Join(path[:len(path)-2], "/") } log.Trace("Create gitea downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace) diff --git a/services/migrations/github.go b/services/migrations/github.go index b8bf6aee45..f45f09191d 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -40,15 +40,17 @@ type GithubDownloaderV3Factory struct{} // New returns a Downloader related to this factory according MigrateOptions func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } + if len(info.segments) < 2 { + return nil, fmt.Errorf("invalid path: %s", info.repoPath) + } - baseURL := u.Scheme + "://" + u.Host - fields := strings.Split(u.Path, "/") - oldOwner := fields[1] - oldName := strings.TrimSuffix(fields[2], ".git") + baseURL := info.apiURL.String() + oldOwner := info.segments[0] + oldName := strings.TrimSuffix(info.segments[1], ".git") log.Trace("Create github downloader BaseURL: %s %s/%s", baseURL, oldOwner, oldName) diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index cf4c2f24e9..9a77bf1d83 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -39,14 +39,13 @@ type GitlabDownloaderFactory struct{} // New returns a Downloader related to this factory according MigrateOptions func (f *GitlabDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } - baseURL := u.Scheme + "://" + u.Host - repoNameSpace := strings.TrimPrefix(u.Path, "/") - repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git") + baseURL := info.apiURL.String() + repoNameSpace := strings.TrimSuffix(info.repoPath, ".git") log.Trace("Create gitlab downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace) diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go index 7a41851e14..73ea856bc9 100644 --- a/services/migrations/gogs.go +++ b/services/migrations/gogs.go @@ -32,22 +32,19 @@ type GogsDownloaderFactory struct{} // New returns a Downloader related to this factory according MigrateOptions func (f *GogsDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } - baseURL := u.Scheme + "://" + u.Host - repoNameSpace := strings.TrimSuffix(u.Path, ".git") - repoNameSpace = strings.Trim(repoNameSpace, "/") - - fields := strings.Split(repoNameSpace, "/") - if len(fields) < 2 { + repoNameSpace := strings.TrimSuffix(info.repoPath, ".git") + if len(info.segments) < 2 || info.segments[0] == "" { return nil, fmt.Errorf("invalid path: %s", repoNameSpace) } - log.Trace("Create gogs downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, fields[0], fields[1]) - return NewGogsDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, fields[0], fields[1]), nil + baseURL := info.apiURL.String() + log.Trace("Create gogs downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, info.segments[0], info.segments[1]) + return NewGogsDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, info.segments[0], strings.TrimSuffix(info.segments[1], ".git")), nil } // GitServiceType returns the type of git service diff --git a/services/migrations/onedev.go b/services/migrations/onedev.go index 262b12cf1c..af1ca26d66 100644 --- a/services/migrations/onedev.go +++ b/services/migrations/onedev.go @@ -37,19 +37,14 @@ type OneDevDownloaderFactory struct{} // New returns a downloader related to this factory according MigrateOptions func (f *OneDevDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.CloneAddr) + info, err := parseServiceCloneURL(opts.CloneAddr) if err != nil { return nil, err } - repoPath := strings.Trim(u.Path, "/") + log.Trace("Create onedev downloader. BaseURL: %v RepoPath: %s", info.apiURL, info.repoPath) - u.Path = "" - u.Fragment = "" - - log.Trace("Create onedev downloader. BaseURL: %v RepoPath: %s", u, repoPath) - - return NewOneDevDownloader(ctx, u, opts.AuthUsername, opts.AuthPassword, repoPath), nil + return NewOneDevDownloader(ctx, info.apiURL, opts.AuthUsername, opts.AuthPassword, info.repoPath), nil } // GitServiceType returns the type of git service diff --git a/templates/repo/migrate/codebase.tmpl b/templates/repo/migrate/codebase.tmpl index 33fd9a2e8c..d5497e10bc 100644 --- a/templates/repo/migrate/codebase.tmpl +++ b/templates/repo/migrate/codebase.tmpl @@ -19,14 +19,15 @@ -
+
-
+
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl index 2bfc425b4a..2590eef966 100644 --- a/templates/repo/migrate/git.tmpl +++ b/templates/repo/migrate/git.tmpl @@ -17,31 +17,16 @@ {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - -
{{ctx.Locale.Tr "repo.migrate.ssh_helper_title"}}: {{ctx.Locale.Tr "repo.migrate.ssh_helper_desc"}} - {{ctx.Locale.Tr "repo.migrate.ssh_helper_link"}} -
-
+
-
+
-
- - -
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/gitbucket.tmpl b/templates/repo/migrate/gitbucket.tmpl index 8a77274a39..db60603651 100644 --- a/templates/repo/migrate/gitbucket.tmpl +++ b/templates/repo/migrate/gitbucket.tmpl @@ -19,14 +19,15 @@
-
+
-
+
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/gitea.tmpl b/templates/repo/migrate/gitea.tmpl index 4b5ce2f76e..7308e22f61 100644 --- a/templates/repo/migrate/gitea.tmpl +++ b/templates/repo/migrate/gitea.tmpl @@ -23,6 +23,7 @@ {{svg "octicon-question"}}
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/github.tmpl b/templates/repo/migrate/github.tmpl index 87a95d3ce4..3e46ff2351 100644 --- a/templates/repo/migrate/github.tmpl +++ b/templates/repo/migrate/github.tmpl @@ -26,6 +26,7 @@ {{ctx.Locale.Tr "repo.migrate.github_token_desc"}}
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl index 6754ecdfa8..6f6a33ec77 100644 --- a/templates/repo/migrate/gitlab.tmpl +++ b/templates/repo/migrate/gitlab.tmpl @@ -23,6 +23,7 @@ {{svg "octicon-question"}}
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/gogs.tmpl b/templates/repo/migrate/gogs.tmpl index ab67c956b0..82fd37e704 100644 --- a/templates/repo/migrate/gogs.tmpl +++ b/templates/repo/migrate/gogs.tmpl @@ -23,6 +23,7 @@
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/onedev.tmpl b/templates/repo/migrate/onedev.tmpl index f176a3f758..135c0787a8 100644 --- a/templates/repo/migrate/onedev.tmpl +++ b/templates/repo/migrate/onedev.tmpl @@ -19,14 +19,15 @@ -
+
-
+
+ {{template "repo/migrate/ssh_options" .}} {{template "repo/migrate/options" .}} diff --git a/templates/repo/migrate/ssh_options.tmpl b/templates/repo/migrate/ssh_options.tmpl new file mode 100644 index 0000000000..a19b5d3d29 --- /dev/null +++ b/templates/repo/migrate/ssh_options.tmpl @@ -0,0 +1,19 @@ +
+ + + {{ctx.Locale.Tr "repo.migrate.ssh_helper_title"}}: {{ctx.Locale.Tr "repo.migrate.ssh_helper_desc"}} + {{ctx.Locale.Tr "repo.migrate.ssh_helper_link"}} + +
+
+ + +
diff --git a/web_src/js/features/repo-migrate.ts b/web_src/js/features/repo-migrate.ts index f73c37dc6d..287352111b 100644 --- a/web_src/js/features/repo-migrate.ts +++ b/web_src/js/features/repo-migrate.ts @@ -68,30 +68,27 @@ function isSSHURL(url: string): boolean { export function initRepoMigrationForm() { const cloneAddrInput = document.querySelector('#clone_addr'); - const authUsernameInput = document.querySelector('#auth_username'); - const authPasswordInput = document.querySelector('#auth_password'); - const sshHelpText = document.querySelector('.help.ssh-help'); + if (!cloneAddrInput) return; - if (!cloneAddrInput || !authUsernameInput || !authPasswordInput || !sshHelpText) return; + // SSH URLs use key-based auth, so username/password fields become useless + // and are hidden. Forge token fields stay visible — the token is still + // needed for API calls (issues/PRs/etc.) regardless of the git transport. + const userpassFields = document.querySelectorAll('.auth-userpass-field'); + const sshHelpField = document.querySelector('.ssh-help-field'); function updateAuthFields() { - const url = cloneAddrInput!.value.trim(); - const isSSH = isSSHURL(url); + const isSSH = isSSHURL(cloneAddrInput!.value.trim()); - const usernameField = authUsernameInput!.parentElement!; - const passwordField = authPasswordInput!.parentElement!; - - if (isSSH) { - // Hide auth fields entirely for SSH URLs (key-based auth only) - authUsernameInput!.value = ''; - authPasswordInput!.value = ''; - hideElem(usernameField); - hideElem(passwordField); - showElem(sshHelpText!); - } else { - showElem(usernameField); - showElem(passwordField); - hideElem(sshHelpText!); + for (const field of userpassFields) { + if (isSSH) { + for (const input of field.querySelectorAll('input')) input.value = ''; + hideElem(field); + } else { + showElem(field); + } + } + if (sshHelpField) { + if (isSSH) showElem(sshHelpField); else hideElem(sshHelpField); } }